diff --git a/CHANGELOG.md b/CHANGELOG.md index 29745a8..5bcbe08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Changelog +## 0.0.58 + * new option `--commit` to specify the build of VS Code to use. By default the latest build is used. + ## 0.0.37 * new option `--testRunnerDataDir` to set the temporary folder for storing the VS Code builds used for running the tests - ## 0.0.28 * new option `--coi` to enable cross origin isolation. diff --git a/README.md b/README.md index 563a5d9..0033abb 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ CLI options: | --extensionDevelopmentPath | A path pointing to an extension under development to include. | | --extensionTestsPath | A path to a test module to run. | | --quality | `insiders` (default), or `stable`. Ignored when sourcesPath is provided. | +| --commit | commitHash The servion of the server to use. Defaults to latest build version of the given quality. Ignored when sourcesPath is provided. | | --sourcesPath | If set, runs the server from VS Code sources located at the given path. Make sure the sources and extensions are compiled (`yarn compile` and `yarn compile-web`). | | --headless | If set, hides the browser. Defaults to true when an extensionTestsPath is provided, otherwise false. | | --permission | Permission granted to the opened browser: e.g. `clipboard-read`, `clipboard-write`. See [full list of options](https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions). Argument can be provided multiple times. | diff --git a/src/index.ts b/src/index.ts index 17e9fbb..ea5cd86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,6 +60,13 @@ export interface Options { */ quality?: VSCodeQuality; + /** + * The commit of the VS Code build to use. If not set, the latest build is used. + * + * The setting is ignored when a vsCodeDevPath is provided. + */ + commit?: string; + /** * @deprecated. Use `quality` or `vsCodeDevPath` instead. */ @@ -230,8 +237,9 @@ async function getBuild(options: Options): Promise { }; } const quality = options.quality || options.version; + const commit = options.commit; const testRunnerDataDir = options.testRunnerDataDir ?? path.resolve(process.cwd(), '.vscode-test-web'); - return await downloadAndUnzipVSCode(quality === 'stable' ? 'stable' : 'insider', testRunnerDataDir); + return await downloadAndUnzipVSCode(testRunnerDataDir, quality === 'stable' ? 'stable' : 'insider', commit); } export async function open(options: Options): Promise { @@ -514,6 +522,21 @@ function validateQuality(quality: unknown, version: unknown, vsCodeDevPath: stri process.exit(-1); } +function validateCommit(commit: unknown, vsCodeDevPath: string | undefined): string | undefined { + + if (vsCodeDevPath && commit) { + console.log(`Sources folder is provided as input, commit is ignored.`); + return undefined; + } + if (commit === undefined || (typeof commit === 'string' && commit.match(/^[0-9a-f]{40}$/))) { + return commit; + } else { + console.log(`Invalid format for commit. Expected a 40 character long SHA1 hash.`); + } + showHelp(); + process.exit(-1); +} + function validatePortNumber(port: unknown): number | undefined { if (typeof port === 'string') { const number = Number.parseInt(port); @@ -531,6 +554,7 @@ interface CommandLineOptions { extensionDevelopmentPath?: string; extensionTestsPath?: string; quality?: string; + commit?: string; sourcesPath?: string; 'open-devtools'?: boolean; headless?: boolean; @@ -556,6 +580,7 @@ function showHelp() { console.log(` --extensionDevelopmentPath path: A path pointing to an extension under development to include. [Optional]`); console.log(` --extensionTestsPath path: A path to a test module to run. [Optional]`); console.log(` --quality 'insiders' | 'stable' [Optional, default 'insiders', ignored when running from sources]`); + console.log(` --commit commitHash [Optional, defaults to latest build version of the given quality, ignored when running from sources]`); console.log(` --sourcesPath path: If provided, running from VS Code sources at the given location. [Optional]`); console.log(` --open-devtools: If set, opens the dev tools. [Optional]`); console.log(` --headless: Whether to hide the browser. Defaults to true when an extensionTestsPath is provided, otherwise false. [Optional]`); @@ -588,7 +613,7 @@ async function cliMain(): Promise { console.log(`${manifest.name}: ${manifest.version}`); const options: minimist.Opts = { - string: ['extensionDevelopmentPath', 'extensionTestsPath', 'browser', 'browserOption', 'browserType', 'quality', 'version', 'waitForDebugger', 'folder-uri', 'permission', 'extensionPath', 'extensionId', 'sourcesPath', 'host', 'port', 'testRunnerDataDir'], + string: ['extensionDevelopmentPath', 'extensionTestsPath', 'browser', 'browserOption', 'browserType', 'quality', 'version', 'commit', 'waitForDebugger', 'folder-uri', 'permission', 'extensionPath', 'extensionId', 'sourcesPath', 'host', 'port', 'testRunnerDataDir'], boolean: ['open-devtools', 'headless', 'hideServerLog', 'printServerLog', 'help', 'verbose', 'coi', 'esm'], unknown: arg => { if (arg.startsWith('-')) { @@ -613,6 +638,7 @@ async function cliMain(): Promise { const extensionIds = await validateExtensionIds(args.extensionId); const vsCodeDevPath = await validatePathOrUndefined(args, 'sourcesPath'); const quality = validateQuality(args.quality, args.version, vsCodeDevPath); + const commit = validateCommit(args.commit, vsCodeDevPath); const devTools = validateBooleanOrUndefined(args, 'open-devtools'); const headless = validateBooleanOrUndefined(args, 'headless'); const permissions = validatePermissions(args.permission); @@ -648,6 +674,7 @@ async function cliMain(): Promise { browserOptions, browserType, quality, + commit, devTools, waitForDebugger, folderUri, @@ -674,6 +701,7 @@ async function cliMain(): Promise { browserOptions, browserType, quality, + commit, devTools, waitForDebugger, folderUri, diff --git a/src/server/download.ts b/src/server/download.ts index 027b231..ba927ad 100644 --- a/src/server/download.ts +++ b/src/server/download.ts @@ -19,9 +19,15 @@ interface DownloadInfo { version: string; } -async function getLatestVersion(quality: 'stable' | 'insider'): Promise { - const update: DownloadInfo = await fetchJSON(`https://update.code.visualstudio.com/api/update/web-standalone/${quality}/latest`); - return update; +async function getDownloadInfo(quality: 'stable' | 'insider', commit: string | undefined): Promise { + if (commit) { + const url = await getRedirect(`https://update.code.visualstudio.com/commit:${commit}/web-standalone/${quality}`); + if (!url) { + throw new Error('Failed to download URL for commit ' + commit + '. Is it valid?'); + } + return { url, version: commit }; + } + return await fetchJSON(`https://update.code.visualstudio.com/api/update/web-standalone/${quality}/latest`); } const reset = '\x1b[G\x1b[0K'; @@ -72,8 +78,9 @@ async function downloadAndUntar(downloadUrl: string, destination: string, messag }); } -export async function downloadAndUnzipVSCode(quality: 'stable' | 'insider', vscodeTestDir: string): Promise { - const info = await getLatestVersion(quality); + +export async function downloadAndUnzipVSCode(vscodeTestDir: string, quality: 'stable' | 'insider', commit: string | undefined): Promise { + const info = await getDownloadInfo(quality, commit); const folderName = `vscode-web-${quality}-${info.version}`; @@ -95,7 +102,7 @@ export async function downloadAndUnzipVSCode(quality: 'stable' | 'insider', vsco await fs.writeFile(path.join(downloadedPath, 'version'), folderName); } catch (err) { console.error(err); - throw Error(`Failed to download and unpack ${productName}`); + throw Error(`Failed to download and unpack ${productName}.${commit ? ' Did you specify a valid commit?' : ''}`); } return { type: 'static', location: downloadedPath, quality, version: info.version }; } @@ -124,6 +131,19 @@ export async function fetch(api: string): Promise { }); }); } +export async function getRedirect(api: string): Promise { + return new Promise((resolve, reject) => { + const httpLibrary = api.startsWith('https') ? https : http; + httpLibrary.get(api, { method: 'HEAD', ...getAgent(api) }, res => { + console.log(res.statusCode, res.headers.location); + if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) { + resolve(res.headers.location); + } else { + resolve(undefined); + } + }); + }); +} export async function fetchJSON(api: string): Promise { const data = await fetch(api);