Skip to content

Commit 021f593

Browse files
committed
feat: allow downloading unreleased versions via -unreleased suffix
1 parent 884a3c8 commit 021f593

File tree

5 files changed

+99
-64
lines changed

5 files changed

+99
-64
lines changed

CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22

3-
### 2.3.10 | 2024-01-19
3+
### 2.4.0 | 2024-05-24
4+
5+
- Allow installing unreleased builds using an `-unreleased` suffix, such as `insiders-unreleased`.
6+
7+
### 2.3.10 | 2024-05-13
48

59
- Add `runVSCodeCommand` method and workaround for Node CVE-2024-27980
610

lib/download.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe('fetchTargetInferredVersion', () => {
7373
let extensionsDevelopmentPath = join(tmpdir(), 'vscode-test-tmp-workspace');
7474

7575
beforeAll(async () => {
76-
[stable, insiders] = await Promise.all([fetchStableVersions(5000), fetchInsiderVersions(5000)]);
76+
[stable, insiders] = await Promise.all([fetchStableVersions(true, 5000), fetchInsiderVersions(true, 5000)]);
7777
});
7878

7979
afterEach(async () => {

lib/download.ts

+43-45
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@ import {
2020
insidersDownloadDirMetadata,
2121
insidersDownloadDirToExecutablePath,
2222
isDefined,
23-
isInsiderVersionIdentifier,
24-
isStableVersionIdentifier,
2523
isSubdirectory,
2624
onceWithoutRejections,
2725
streamToBuffer,
2826
systemDefaultPlatform,
2927
validateStream,
28+
Version,
3029
} from './util';
3130

3231
const extensionRoot = process.cwd();
@@ -36,7 +35,7 @@ const vscodeStableReleasesAPI = `https://update.code.visualstudio.com/api/releas
3635
const vscodeInsiderReleasesAPI = `https://update.code.visualstudio.com/api/releases/insider`;
3736

3837
const downloadDirNameFormat = /^vscode-(?<platform>[a-z]+)-(?<version>[0-9.]+)$/;
39-
const makeDownloadDirName = (platform: string, version: string) => `vscode-${platform}-${version}`;
38+
const makeDownloadDirName = (platform: string, version: Version) => `vscode-${platform}-${version.id}`;
4039

4140
const DOWNLOAD_ATTEMPTS = 3;
4241

@@ -50,28 +49,28 @@ interface IFetchInferredOptions extends IFetchStableOptions {
5049
extensionsDevelopmentPath?: string | string[];
5150
}
5251

53-
export const fetchStableVersions = onceWithoutRejections((timeout: number) =>
54-
request.getJSON<string[]>(vscodeStableReleasesAPI, timeout)
52+
export const fetchStableVersions = onceWithoutRejections((released: boolean, timeout: number) =>
53+
request.getJSON<string[]>(`${vscodeStableReleasesAPI}?released=${released}`, timeout)
5554
);
56-
export const fetchInsiderVersions = onceWithoutRejections((timeout: number) =>
57-
request.getJSON<string[]>(vscodeInsiderReleasesAPI, timeout)
55+
export const fetchInsiderVersions = onceWithoutRejections((released: boolean, timeout: number) =>
56+
request.getJSON<string[]>(`${vscodeInsiderReleasesAPI}?released=${released}`, timeout)
5857
);
5958

6059
/**
6160
* Returns the stable version to run tests against. Attempts to get the latest
6261
* version from the update sverice, but falls back to local installs if
6362
* not available (e.g. if the machine is offline).
6463
*/
65-
async function fetchTargetStableVersion({ timeout, cachePath, platform }: IFetchStableOptions): Promise<string> {
64+
async function fetchTargetStableVersion({ timeout, cachePath, platform }: IFetchStableOptions): Promise<Version> {
6665
try {
67-
const versions = await fetchStableVersions(timeout);
68-
return versions[0];
66+
const versions = await fetchStableVersions(true, timeout);
67+
return new Version(versions[0]);
6968
} catch (e) {
7069
return fallbackToLocalEntries(cachePath, platform, e as Error);
7170
}
7271
}
7372

74-
export async function fetchTargetInferredVersion(options: IFetchInferredOptions) {
73+
export async function fetchTargetInferredVersion(options: IFetchInferredOptions): Promise<Version> {
7574
if (!options.extensionsDevelopmentPath) {
7675
return fetchTargetStableVersion(options);
7776
}
@@ -87,22 +86,22 @@ export async function fetchTargetInferredVersion(options: IFetchInferredOptions)
8786
const matches = (v: string) => !extVersions.some((range) => !semver.satisfies(v, range, { includePrerelease: true }));
8887

8988
try {
90-
const stable = await fetchStableVersions(options.timeout);
89+
const stable = await fetchStableVersions(true, options.timeout);
9190
const found1 = stable.find(matches);
9291
if (found1) {
93-
return found1;
92+
return new Version(found1);
9493
}
9594

96-
const insiders = await fetchInsiderVersions(options.timeout);
95+
const insiders = await fetchInsiderVersions(true, options.timeout);
9796
const found2 = insiders.find(matches);
9897
if (found2) {
99-
return found2;
98+
return new Version(found2);
10099
}
101100

102101
const v = extVersions.join(', ');
103102
console.warn(`No version of VS Code satisfies all extension engine constraints (${v}). Falling back to stable.`);
104103

105-
return stable[0]; // 🤷
104+
return new Version(stable[0]); // 🤷
106105
} catch (e) {
107106
return fallbackToLocalEntries(options.cachePath, options.platform, e as Error);
108107
}
@@ -129,35 +128,31 @@ async function fallbackToLocalEntries(cachePath: string, platform: string, fromE
129128

130129
if (fallbackTo) {
131130
console.warn(`Error retrieving VS Code versions, using already-installed version ${fallbackTo}`, fromError);
132-
return fallbackTo;
131+
return new Version(fallbackTo);
133132
}
134133

135134
throw fromError;
136135
}
137136

138-
async function isValidVersion(version: string, platform: string, timeout: number) {
139-
if (version === 'insiders' || version === 'stable') {
137+
async function isValidVersion(version: Version, timeout: number) {
138+
if (version.id === 'insiders' || version.id === 'stable' || version.isCommit) {
140139
return true;
141140
}
142141

143-
if (isStableVersionIdentifier(version)) {
144-
const stableVersionNumbers = await fetchStableVersions(timeout);
145-
if (stableVersionNumbers.includes(version)) {
142+
if (version.isStable) {
143+
const stableVersionNumbers = await fetchStableVersions(version.isReleased, timeout);
144+
if (stableVersionNumbers.includes(version.id)) {
146145
return true;
147146
}
148147
}
149148

150-
if (isInsiderVersionIdentifier(version)) {
151-
const insiderVersionNumbers = await fetchInsiderVersions(timeout);
152-
if (insiderVersionNumbers.includes(version)) {
149+
if (version.isInsiders) {
150+
const insiderVersionNumbers = await fetchInsiderVersions(version.isReleased, timeout);
151+
if (insiderVersionNumbers.includes(version.id)) {
153152
return true;
154153
}
155154
}
156155

157-
if (/^[0-9a-f]{40}$/.test(version)) {
158-
return true;
159-
}
160-
161156
return false;
162157
}
163158

@@ -267,7 +262,8 @@ async function downloadVSCodeArchive(options: DownloadOptions): Promise<IDownloa
267262
}
268263

269264
const timeout = options.timeout!;
270-
const downloadUrl = getVSCodeDownloadUrl(options.version, options.platform);
265+
const version = Version.parse(options.version);
266+
const downloadUrl = getVSCodeDownloadUrl(version, options.platform);
271267

272268
options.reporter?.report({ stage: ProgressReportStage.ResolvingCDNLocation, url: downloadUrl });
273269
const res = await request.getStream(downloadUrl, timeout);
@@ -429,25 +425,27 @@ const COMPLETE_FILE_NAME = 'is-complete';
429425
* @returns Promise of `vscodeExecutablePath`.
430426
*/
431427
export async function download(options: Partial<DownloadOptions> = {}): Promise<string> {
432-
let version = options?.version;
428+
const inputVersion = options?.version ? Version.parse(options.version) : undefined;
433429
const {
434430
platform = systemDefaultPlatform,
435431
cachePath = defaultCachePath,
436432
reporter = new ConsoleReporter(process.stdout.isTTY),
437433
timeout = 15_000,
438434
} = options;
439435

440-
if (version === 'stable') {
436+
let version: Version;
437+
if (inputVersion?.id === 'stable') {
441438
version = await fetchTargetStableVersion({ timeout, cachePath, platform });
442-
} else if (version) {
439+
} else if (inputVersion) {
443440
/**
444441
* Only validate version against server when no local download that matches version exists
445442
*/
446-
if (!fs.existsSync(path.resolve(cachePath, `vscode-${platform}-${version}`))) {
447-
if (!(await isValidVersion(version, platform, timeout))) {
448-
throw Error(`Invalid version ${version}`);
443+
if (!fs.existsSync(path.resolve(cachePath, makeDownloadDirName(platform, inputVersion)))) {
444+
if (!(await isValidVersion(inputVersion, timeout))) {
445+
throw Error(`Invalid version ${inputVersion.id}`);
449446
}
450447
}
448+
version = inputVersion;
451449
} else {
452450
version = await fetchTargetInferredVersion({
453451
timeout,
@@ -457,22 +455,22 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
457455
});
458456
}
459457

460-
if (platform === 'win32-archive' && semver.satisfies(version, '>= 1.85.0', { includePrerelease: true })) {
458+
if (platform === 'win32-archive' && semver.satisfies(version.id, '>= 1.85.0', { includePrerelease: true })) {
461459
throw new Error('Windows 32-bit is no longer supported from v1.85 onwards');
462460
}
463461

464-
reporter.report({ stage: ProgressReportStage.ResolvedVersion, version });
462+
reporter.report({ stage: ProgressReportStage.ResolvedVersion, version: version.id });
465463

466464
const downloadedPath = path.resolve(cachePath, makeDownloadDirName(platform, version));
467465
if (fs.existsSync(path.join(downloadedPath, COMPLETE_FILE_NAME))) {
468-
if (isInsiderVersionIdentifier(version)) {
466+
if (version.isInsiders) {
469467
reporter.report({ stage: ProgressReportStage.FetchingInsidersMetadata });
470468
const { version: currentHash, date: currentDate } = insidersDownloadDirMetadata(downloadedPath, platform);
471469

472470
const { version: latestHash, timestamp: latestTimestamp } =
473-
version === 'insiders'
474-
? await getLatestInsidersMetadata(systemDefaultPlatform)
475-
: await getInsidersVersionMetadata(systemDefaultPlatform, version);
471+
version.id === 'insiders' // not qualified with a date
472+
? await getLatestInsidersMetadata(systemDefaultPlatform, version.isReleased)
473+
: await getInsidersVersionMetadata(systemDefaultPlatform, version.id, version.isReleased);
476474

477475
if (currentHash === latestHash) {
478476
reporter.report({ stage: ProgressReportStage.FoundMatchingInstall, downloadedPath });
@@ -493,7 +491,7 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
493491
throw Error(`Failed to remove outdated Insiders at ${downloadedPath}.`);
494492
}
495493
}
496-
} else if (isStableVersionIdentifier(version)) {
494+
} else if (version.isStable) {
497495
reporter.report({ stage: ProgressReportStage.FoundMatchingInstall, downloadedPath });
498496
return Promise.resolve(downloadDirToExecutablePath(downloadedPath, platform));
499497
} else {
@@ -507,7 +505,7 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
507505
await fs.promises.rm(downloadedPath, { recursive: true, force: true });
508506

509507
const download = await downloadVSCodeArchive({
510-
version,
508+
version: version.toString(),
511509
platform,
512510
cachePath,
513511
reporter,
@@ -536,7 +534,7 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
536534
}
537535
reporter.report({ stage: ProgressReportStage.NewInstallComplete, downloadedPath });
538536

539-
if (isStableVersionIdentifier(version)) {
537+
if (version.isStable) {
540538
return downloadDirToExecutablePath(downloadedPath, platform);
541539
} else {
542540
return insidersDownloadDirToExecutablePath(downloadedPath, platform);

lib/util.ts

+40-17
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,47 @@ switch (process.platform) {
3232
process.arch === 'arm64' ? 'linux-arm64' : process.arch === 'arm' ? 'linux-armhf' : 'linux-x64';
3333
}
3434

35-
export function isInsiderVersionIdentifier(version: string): boolean {
36-
return version === 'insiders' || version.endsWith('-insider'); // insider or 1.2.3-insider version string
37-
}
35+
const UNRELEASED_SUFFIX = '-unreleased';
36+
37+
export class Version {
38+
public static parse(version: string): Version {
39+
const unreleased = version.endsWith(UNRELEASED_SUFFIX);
40+
if (unreleased) {
41+
version = version.slice(0, -UNRELEASED_SUFFIX.length);
42+
}
3843

39-
export function isStableVersionIdentifier(version: string): boolean {
40-
return version === 'stable' || /^[0-9]+\.[0-9]+\.[0-9]$/.test(version); // stable or 1.2.3 version string
44+
return new Version(version, !unreleased);
45+
}
46+
47+
constructor(public readonly id: string, public readonly isReleased = true) {}
48+
49+
public get isCommit() {
50+
return /^[0-9a-f]{40}$/.test(this.id);
51+
}
52+
53+
public get isInsiders() {
54+
return this.id === 'insiders' || this.id.endsWith('-insider');
55+
}
56+
57+
public get isStable() {
58+
return this.id === 'stable' || /^[0-9]+\.[0-9]+\.[0-9]$/.test(this.id);
59+
}
60+
61+
public toString() {
62+
return this.id + (this.isReleased ? '' : UNRELEASED_SUFFIX);
63+
}
4164
}
4265

43-
export function getVSCodeDownloadUrl(version: string, platform = systemDefaultPlatform) {
44-
if (version === 'insiders') {
45-
return `https://update.code.visualstudio.com/latest/${platform}/insider`;
46-
} else if (isInsiderVersionIdentifier(version)) {
47-
return `https://update.code.visualstudio.com/${version}/${platform}/insider`;
48-
} else if (isStableVersionIdentifier(version)) {
49-
return `https://update.code.visualstudio.com/${version}/${platform}/stable`;
66+
export function getVSCodeDownloadUrl(version: Version, platform: string) {
67+
if (version.id === 'insiders') {
68+
return `https://update.code.visualstudio.com/latest/${platform}/insider?released=${version.isReleased}`;
69+
} else if (version.isInsiders) {
70+
return `https://update.code.visualstudio.com/${version.id}/${platform}/insider?released=${version.isReleased}`;
71+
} else if (version.isStable) {
72+
return `https://update.code.visualstudio.com/${version.id}/${platform}/stable?released=${version.isReleased}`;
5073
} else {
5174
// insiders commit hash
52-
return `https://update.code.visualstudio.com/commit:${version}/${platform}/insider`;
75+
return `https://update.code.visualstudio.com/commit:${version.id}/${platform}/insider`;
5376
}
5477
}
5578

@@ -126,13 +149,13 @@ export interface IUpdateMetadata {
126149
supportsFastUpdate: boolean;
127150
}
128151

129-
export async function getInsidersVersionMetadata(platform: string, version: string) {
130-
const remoteUrl = `https://update.code.visualstudio.com/api/versions/${version}/${platform}/insider`;
152+
export async function getInsidersVersionMetadata(platform: string, version: string, released: boolean) {
153+
const remoteUrl = `https://update.code.visualstudio.com/api/versions/${version}/${platform}/insider?released=${released}`;
131154
return await request.getJSON<IUpdateMetadata>(remoteUrl, 30_000);
132155
}
133156

134-
export async function getLatestInsidersMetadata(platform: string) {
135-
const remoteUrl = `https://update.code.visualstudio.com/api/update/${platform}/insider/latest`;
157+
export async function getLatestInsidersMetadata(platform: string, released: boolean) {
158+
const remoteUrl = `https://update.code.visualstudio.com/api/update/${platform}/insider/latest?released=${released}`;
136159
return await request.getJSON<IUpdateMetadata>(remoteUrl, 30_000);
137160
}
138161

sample/src/test/runTest.ts

+10
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ async function go() {
4747
launchArgs: [testWorkspace],
4848
});
4949

50+
/**
51+
* Use unreleased Insiders (here be dragons!)
52+
*/
53+
await runTests({
54+
version: 'insiders-unreleased',
55+
extensionDevelopmentPath,
56+
extensionTestsPath,
57+
launchArgs: [testWorkspace],
58+
});
59+
5060
/**
5161
* Use a specific Insiders commit for testing
5262
*/

0 commit comments

Comments
 (0)