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", + }); + }); +});