Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions extensions/vscode/src/server/server-manager.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';
}
}

/**
Expand All @@ -111,10 +121,10 @@ export class ServerManager extends DisposableObject {
* `globalStorage`. Returns `true` if a fresh install was performed.
*/
async ensureInstalled(): Promise<boolean> {
// 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;
Expand Down Expand Up @@ -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**: `<extensionRoot>/server/` (created by `vscode:prepublish`)
* β€” the extension is self-contained, no npm install required.
* 2. **Monorepo dev layout**: `<extensionRoot>/../../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;
Expand Down
8 changes: 6 additions & 2 deletions extensions/vscode/test/server/server-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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');
});

Expand Down Expand Up @@ -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'),
);
});

Expand All @@ -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' }));
Expand Down