diff --git a/extensions/vscode/src/server/server-manager.ts b/extensions/vscode/src/server/server-manager.ts index 802bfd1e..9f394b9d 100644 --- a/extensions/vscode/src/server/server-manager.ts +++ b/extensions/vscode/src/server/server-manager.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { execFile } from 'child_process'; import { access, readFile, mkdir } from 'fs/promises'; -import { accessSync, constants } from 'fs'; +import { accessSync, constants, readFileSync } from 'fs'; import { join } from 'path'; import { DisposableObject } from '../common/disposable'; import type { Logger } from '../common/logger'; @@ -92,11 +92,21 @@ export class ServerManager extends DisposableObject { /** * Get the extension's own version from its packageJSON. * + * Reads `package.json` from the extension root (`context.extensionUri`) + * so that this works in all environments (VSIX, Extension Development + * Host, and tests) without relying on `vscode.extensions.getExtension`. + * * This is the version baked into the VSIX and is used to determine * whether the locally installed npm package is still up-to-date. */ getExtensionVersion(): string { - return this.context.extension.packageJSON.version as string; + try { + const pkgPath = join(this.context.extensionUri.fsPath, 'package.json'); + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string }; + return pkg.version ?? 'unknown'; + } catch { + return 'unknown'; + } } /** @@ -111,10 +121,10 @@ export class ServerManager extends DisposableObject { * `globalStorage`. Returns `true` if a fresh install was performed. */ async ensureInstalled(): Promise { - // VSIX bundle is self-contained — no npm install required. + // VSIX bundle or monorepo server is present — no npm install required. if (this.getBundledQlRoot()) { this.logger.info( - `Using VSIX-bundled server (v${this.getExtensionVersion()}). ` + + `Using bundled server (v${this.getExtensionVersion()}). ` + 'No npm install required.', ); return false; @@ -164,13 +174,15 @@ export class ServerManager extends DisposableObject { // --------------------------------------------------------------------------- /** - * Root of the bundled `server/` directory inside the VSIX. + * Root of the `server/` directory, checked in two locations: + * + * 1. **VSIX layout**: `/server/` (created by `vscode:prepublish`) + * — the extension is self-contained, no npm install required. + * 2. **Monorepo dev layout**: `/../../server/` — used when + * running from the Extension Development Host without a prepublish build. * - * In VSIX layout the `vscode:prepublish` step copies `server/dist/`, - * `server/ql/`, and `server/package.json` into the extension so the VSIX - * is self-contained. Returns the path to that `server/` directory, or - * `undefined` if the bundle is missing (local dev without a prepublish - * build). + * Returns the first location whose `server/package.json` is readable, or + * `undefined` if neither location exists. */ getBundledQlRoot(): string | undefined { const extensionRoot = this.context.extensionUri.fsPath; diff --git a/extensions/vscode/test/server/server-manager.test.ts b/extensions/vscode/test/server/server-manager.test.ts index cf48b0cd..8d2c9579 100644 --- a/extensions/vscode/test/server/server-manager.test.ts +++ b/extensions/vscode/test/server/server-manager.test.ts @@ -13,12 +13,13 @@ vi.mock('fs/promises', () => ({ vi.mock('fs', () => ({ accessSync: vi.fn(), + readFileSync: vi.fn(), constants: { R_OK: 4 }, })); import { execFile } from 'child_process'; import { access, readFile } from 'fs/promises'; -import { accessSync } from 'fs'; +import { accessSync, readFileSync } from 'fs'; function createMockContext(extensionVersion = '2.24.2') { return { @@ -176,6 +177,7 @@ describe('ServerManager', () => { }); it('should return extension version from context', () => { + vi.mocked(readFileSync).mockReturnValue(JSON.stringify({ version: '2.24.2' })); expect(manager.getExtensionVersion()).toBe('2.24.2'); }); @@ -212,7 +214,7 @@ describe('ServerManager', () => { expect(installed).toBe(false); expect(execFile).not.toHaveBeenCalled(); expect(logger.info).toHaveBeenCalledWith( - expect.stringContaining('VSIX-bundled server'), + expect.stringContaining('bundled server'), ); }); @@ -239,6 +241,8 @@ describe('ServerManager', () => { it('should skip npm install when bundle is missing but matching version installed', async () => { // No bundle vi.mocked(accessSync).mockImplementation(() => { throw new Error('ENOENT'); }); + // Extension version from package.json + vi.mocked(readFileSync).mockReturnValue(JSON.stringify({ version: '2.24.2' })); // Already installed with matching version vi.mocked(access).mockResolvedValue(undefined); vi.mocked(readFile).mockResolvedValue(JSON.stringify({ version: '2.24.2' }));