From a1b7714ae6e2caa6e33eeea20c8c06e4722630b4 Mon Sep 17 00:00:00 2001 From: Chuan-kai Lin Date: Tue, 15 Sep 2026 12:52:26 -0700 Subject: [PATCH] Read VS Code Electron version from metadata VS Code no longer consistently declares Electron in its root package.json. Prefer that declaration for older releases and fall back to the Electron target in .npmrc for newer releases. Validate the resolved version and cover both metadata layouts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c8ecd08e-7635-4742-b943-e0fc528b7949 --- extensions/ql-vscode/scripts/util/fetch.ts | 12 +- .../ql-vscode/scripts/util/vscode-versions.ts | 50 +++++++-- .../scripts/util/vscode-versions.test.ts | 104 ++++++++++++++++++ 3 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 extensions/ql-vscode/test/unit-tests/scripts/util/vscode-versions.test.ts diff --git a/extensions/ql-vscode/scripts/util/fetch.ts b/extensions/ql-vscode/scripts/util/fetch.ts index a7ab7fd234d..dcb807713ae 100644 --- a/extensions/ql-vscode/scripts/util/fetch.ts +++ b/extensions/ql-vscode/scripts/util/fetch.ts @@ -1,4 +1,4 @@ -export async function fetchJson(url: string): Promise { +async function fetchResponse(url: string): Promise { const response = await fetch(url); if (!response.ok) { throw new Error( @@ -6,5 +6,15 @@ export async function fetchJson(url: string): Promise { ); } + return response; +} + +export async function fetchJson(url: string): Promise { + const response = await fetchResponse(url); return (await response.json()) as T; } + +export async function fetchText(url: string): Promise { + const response = await fetchResponse(url); + return await response.text(); +} diff --git a/extensions/ql-vscode/scripts/util/vscode-versions.ts b/extensions/ql-vscode/scripts/util/vscode-versions.ts index ce7853620be..f007dcaca8c 100644 --- a/extensions/ql-vscode/scripts/util/vscode-versions.ts +++ b/extensions/ql-vscode/scripts/util/vscode-versions.ts @@ -1,9 +1,9 @@ -import { minVersion } from "semver"; -import { fetchJson } from "./fetch"; +import { minVersion, valid } from "semver"; +import { fetchJson, fetchText } from "./fetch"; type VsCodePackageJson = { - devDependencies: { - electron: string; + devDependencies?: { + electron?: string; }; }; @@ -15,6 +15,40 @@ async function getVsCodePackageJson( ); } +async function getVsCodeNpmrc(version: string): Promise { + return await fetchText( + `https://raw.githubusercontent.com/microsoft/vscode/${version}/.npmrc`, + ); +} + +export function parseElectronVersion(npmrc: string): string { + const electronVersion = /^target="([^"]+)"$/m.exec(npmrc)?.[1]; + if (!electronVersion || !valid(electronVersion)) { + throw new Error( + "Could not find a valid Electron version in VS Code .npmrc", + ); + } + + return electronVersion; +} + +async function getVsCodeElectronVersion(version: string): Promise { + const packageJson = await getVsCodePackageJson(version); + const packageElectronVersion = packageJson.devDependencies?.electron; + if (packageElectronVersion) { + const electronVersion = minVersion(packageElectronVersion)?.version; + if (!electronVersion) { + throw new Error( + "Could not find a valid Electron version in VS Code package.json", + ); + } + + return electronVersion; + } + + return parseElectronVersion(await getVsCodeNpmrc(version)); +} + interface ElectronVersion { version: string; date: string; @@ -44,13 +78,7 @@ type VersionInformation = { export async function getVersionInformation( vscodeVersion: string, ): Promise { - const vsCodePackageJson = await getVsCodePackageJson(vscodeVersion); - const electronVersion = minVersion( - vsCodePackageJson.devDependencies.electron, - )?.version; - if (!electronVersion) { - throw new Error("Could not find Electron version"); - } + const electronVersion = await getVsCodeElectronVersion(vscodeVersion); const electronReleases = await getElectronReleases(); diff --git a/extensions/ql-vscode/test/unit-tests/scripts/util/vscode-versions.test.ts b/extensions/ql-vscode/test/unit-tests/scripts/util/vscode-versions.test.ts new file mode 100644 index 00000000000..0ccb6985122 --- /dev/null +++ b/extensions/ql-vscode/test/unit-tests/scripts/util/vscode-versions.test.ts @@ -0,0 +1,104 @@ +import { + getVersionInformation, + parseElectronVersion, +} from "../../../../scripts/util/vscode-versions"; + +const electronReleases = [ + { + version: "29.4.0", + node: "20.9.0", + chrome: "122.0.6261.156", + }, + { + version: "42.10.0", + node: "24.18.1", + chrome: "148.0.7778.280", + }, +]; + +function mockFetch( + responses: Record, +): jest.SpiedFunction { + return jest.spyOn(globalThis, "fetch").mockImplementation(async (input) => { + const url = input.toString(); + const response = responses[url]; + if (response === undefined) { + throw new Error(`Unexpected URL: ${url}`); + } + + return { + ok: true, + json: async () => response, + text: async () => response, + } as Response; + }); +} + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe("parseElectronVersion", () => { + it("parses the Electron target from a VS Code .npmrc", () => { + expect( + parseElectronVersion( + ['target="42.10.0"', 'runtime="electron"', "disturl=example"].join( + "\n", + ), + ), + ).toBe("42.10.0"); + }); + + it.each([ + ["a missing target", 'runtime="electron"'], + ["an invalid target", 'target="not-a-version"'], + ["an unquoted target", "target=42.10.0"], + ])("rejects %s", (_description, npmrc) => { + expect(() => parseElectronVersion(npmrc)).toThrow( + "Could not find a valid Electron version in VS Code .npmrc", + ); + }); +}); + +describe("getVersionInformation", () => { + it("reads Electron from package.json for older VS Code releases", async () => { + const fetchMock = mockFetch({ + "https://raw.githubusercontent.com/microsoft/vscode/1.90.0/package.json": + { + devDependencies: { + electron: "29.4.0", + }, + }, + "https://releases.electronjs.org/releases.json": electronReleases, + }); + + await expect(getVersionInformation("1.90.0")).resolves.toEqual({ + vscodeVersion: "1.90.0", + electronVersion: "29.4.0", + nodeVersion: "20.9.0", + chromiumVersion: "122.0.6261.156", + }); + expect(fetchMock).not.toHaveBeenCalledWith( + "https://raw.githubusercontent.com/microsoft/vscode/1.90.0/.npmrc", + ); + }); + + it("falls back to .npmrc for newer VS Code releases", async () => { + mockFetch({ + "https://raw.githubusercontent.com/microsoft/vscode/1.137.0/package.json": + { + devDependencies: {}, + }, + "https://raw.githubusercontent.com/microsoft/vscode/1.137.0/.npmrc": + 'target="42.10.0"\nruntime="electron"\n', + "https://releases.electronjs.org/releases.json": electronReleases, + }); + + await expect(getVersionInformation("1.137.0")).resolves.toEqual({ + vscodeVersion: "1.137.0", + electronVersion: "42.10.0", + nodeVersion: "24.18.1", + chromiumVersion: "148.0.7778.280", + }); + }); +});