@@ -12,15 +12,19 @@ import { promisify } from 'util';
12
12
import * as del from './del' ;
13
13
import { ConsoleReporter , ProgressReporter , ProgressReportStage } from './progress' ;
14
14
import * as request from './request' ;
15
+ import * as semver from 'semver' ;
15
16
import {
16
17
downloadDirToExecutablePath ,
18
+ getInsidersVersionMetadata ,
17
19
getLatestInsidersMetadata ,
18
20
getVSCodeDownloadUrl ,
19
21
insidersDownloadDirMetadata ,
20
22
insidersDownloadDirToExecutablePath ,
21
23
isDefined ,
24
+ isInsiderVersionIdentifier ,
22
25
isStableVersionIdentifier ,
23
26
isSubdirectory ,
27
+ onceWithoutRejections ,
24
28
streamToBuffer ,
25
29
systemDefaultPlatform ,
26
30
} from './util' ;
@@ -29,6 +33,7 @@ const extensionRoot = process.cwd();
29
33
const pipelineAsync = promisify ( pipeline ) ;
30
34
31
35
const vscodeStableReleasesAPI = `https://update.code.visualstudio.com/api/releases/stable` ;
36
+ const vscodeInsiderReleasesAPI = `https://update.code.visualstudio.com/api/releases/insider` ;
32
37
const vscodeInsiderCommitsAPI = ( platform : string ) =>
33
38
`https://update.code.visualstudio.com/api/commits/insider/${ platform } ` ;
34
39
@@ -37,43 +42,117 @@ const makeDownloadDirName = (platform: string, version: string) => `vscode-${pla
37
42
38
43
const DOWNLOAD_ATTEMPTS = 3 ;
39
44
45
+ interface IFetchStableOptions {
46
+ timeout : number ;
47
+ cachePath : string ;
48
+ platform : string ;
49
+ }
50
+
51
+ interface IFetchInferredOptions extends IFetchStableOptions {
52
+ extensionsDevelopmentPath ?: string | string [ ] ;
53
+ }
54
+
55
+ export const fetchStableVersions = onceWithoutRejections ( ( timeout : number ) =>
56
+ request . getJSON < string [ ] > ( vscodeStableReleasesAPI , timeout )
57
+ ) ;
58
+ export const fetchInsiderVersions = onceWithoutRejections ( ( timeout : number ) =>
59
+ request . getJSON < string [ ] > ( vscodeInsiderReleasesAPI , timeout )
60
+ ) ;
61
+
40
62
/**
41
63
* Returns the stable version to run tests against. Attempts to get the latest
42
64
* version from the update sverice, but falls back to local installs if
43
65
* not available (e.g. if the machine is offline).
44
66
*/
45
- async function fetchTargetStableVersion ( timeout : number , cachePath : string , platform : string ) : Promise < string > {
46
- let versions : string [ ] = [ ] ;
67
+ async function fetchTargetStableVersion ( { timeout, cachePath, platform } : IFetchStableOptions ) : Promise < string > {
47
68
try {
48
- versions = await request . getJSON < string [ ] > ( vscodeStableReleasesAPI , timeout ) ;
69
+ const versions = await fetchStableVersions ( timeout ) ;
70
+ return versions [ 0 ] ;
49
71
} catch ( e ) {
50
- const entries = await fs . promises . readdir ( cachePath ) . catch ( ( ) => [ ] as string [ ] ) ;
51
- const [ fallbackTo ] = entries
52
- . map ( ( e ) => downloadDirNameFormat . exec ( e ) )
53
- . filter ( isDefined )
54
- . filter ( ( e ) => e . groups ! . platform === platform )
55
- . map ( ( e ) => e . groups ! . version )
56
- . sort ( ( a , b ) => Number ( b ) - Number ( a ) ) ;
57
-
58
- if ( fallbackTo ) {
59
- console . warn ( `Error retrieving VS Code versions, using already-installed version ${ fallbackTo } ` , e ) ;
60
- return fallbackTo ;
72
+ return fallbackToLocalEntries ( cachePath , platform , e as Error ) ;
73
+ }
74
+ }
75
+
76
+ export async function fetchTargetInferredVersion ( options : IFetchInferredOptions ) {
77
+ if ( ! options . extensionsDevelopmentPath ) {
78
+ return fetchTargetStableVersion ( options ) ;
79
+ }
80
+
81
+ // load all engines versions from all development paths. Then, get the latest
82
+ // stable version (or, latest Insiders version) that satisfies all
83
+ // `engines.vscode` constraints.
84
+ const extPaths = Array . isArray ( options . extensionsDevelopmentPath )
85
+ ? options . extensionsDevelopmentPath
86
+ : [ options . extensionsDevelopmentPath ] ;
87
+ const maybeExtVersions = await Promise . all ( extPaths . map ( getEngineVersionFromExtension ) ) ;
88
+ const extVersions = maybeExtVersions . filter ( isDefined ) ;
89
+ const matches = ( v : string ) => ! extVersions . some ( ( range ) => ! semver . satisfies ( v , range , { includePrerelease : true } ) ) ;
90
+
91
+ try {
92
+ const stable = await fetchStableVersions ( options . timeout ) ;
93
+ const found1 = stable . find ( matches ) ;
94
+ if ( found1 ) {
95
+ return found1 ;
61
96
}
62
97
63
- throw e ;
98
+ const insiders = await fetchInsiderVersions ( options . timeout ) ;
99
+ const found2 = insiders . find ( matches ) ;
100
+ if ( found2 ) {
101
+ return found2 ;
102
+ }
103
+
104
+ console . warn ( `No version of VS Code satisfies all extension engine constraints (${ extVersions . join ( ', ' ) } ). Falling back to stable.` ) ;
105
+
106
+ return stable [ 0 ] ; // 🤷
107
+ } catch ( e ) {
108
+ return fallbackToLocalEntries ( options . cachePath , options . platform , e as Error ) ;
109
+ }
110
+ }
111
+
112
+ async function getEngineVersionFromExtension ( extensionPath : string ) : Promise < string | undefined > {
113
+ try {
114
+ const packageContents = await fs . promises . readFile ( path . join ( extensionPath , 'package.json' ) , 'utf8' ) ;
115
+ const packageJson = JSON . parse ( packageContents ) ;
116
+ return packageJson ?. engines ?. vscode ;
117
+ } catch {
118
+ return undefined ;
119
+ }
120
+ }
121
+
122
+ async function fallbackToLocalEntries ( cachePath : string , platform : string , fromError : Error ) {
123
+ const entries = await fs . promises . readdir ( cachePath ) . catch ( ( ) => [ ] as string [ ] ) ;
124
+ const [ fallbackTo ] = entries
125
+ . map ( ( e ) => downloadDirNameFormat . exec ( e ) )
126
+ . filter ( isDefined )
127
+ . filter ( ( e ) => e . groups ! . platform === platform )
128
+ . map ( ( e ) => e . groups ! . version )
129
+ . sort ( ( a , b ) => Number ( b ) - Number ( a ) ) ;
130
+
131
+ if ( fallbackTo ) {
132
+ console . warn ( `Error retrieving VS Code versions, using already-installed version ${ fallbackTo } ` , fromError ) ;
133
+ return fallbackTo ;
64
134
}
65
135
66
- return versions [ 0 ] ;
136
+ throw fromError ;
67
137
}
68
138
69
139
async function isValidVersion ( version : string , platform : string , timeout : number ) {
70
- if ( version === 'insiders' ) {
140
+ if ( version === 'insiders' || version === 'stable' ) {
71
141
return true ;
72
142
}
73
143
74
- const stableVersionNumbers : string [ ] = await request . getJSON ( vscodeStableReleasesAPI , timeout ) ;
75
- if ( stableVersionNumbers . includes ( version ) ) {
76
- return true ;
144
+ if ( isStableVersionIdentifier ( version ) ) {
145
+ const stableVersionNumbers = await fetchStableVersions ( timeout ) ;
146
+ if ( stableVersionNumbers . includes ( version ) ) {
147
+ return true ;
148
+ }
149
+ }
150
+
151
+ if ( isInsiderVersionIdentifier ( version ) ) {
152
+ const insiderVersionNumbers = await fetchInsiderVersions ( timeout ) ;
153
+ if ( insiderVersionNumbers . includes ( version ) ) {
154
+ return true ;
155
+ }
77
156
}
78
157
79
158
const insiderCommits : string [ ] = await request . getJSON ( vscodeInsiderCommitsAPI ( platform ) , timeout ) ;
@@ -97,6 +176,7 @@ export interface DownloadOptions {
97
176
readonly cachePath : string ;
98
177
readonly version : DownloadVersion ;
99
178
readonly platform : DownloadPlatform ;
179
+ readonly extensionDevelopmentPath ?: string | string [ ] ;
100
180
readonly reporter ?: ProgressReporter ;
101
181
readonly extractSync ?: boolean ;
102
182
readonly timeout ?: number ;
@@ -116,6 +196,7 @@ async function downloadVSCodeArchive(options: DownloadOptions) {
116
196
117
197
const timeout = options . timeout ! ;
118
198
const downloadUrl = getVSCodeDownloadUrl ( options . version , options . platform ) ;
199
+
119
200
options . reporter ?. report ( { stage : ProgressReportStage . ResolvingCDNLocation , url : downloadUrl } ) ;
120
201
const res = await request . getStream ( downloadUrl , timeout ) ;
121
202
if ( res . statusCode !== 302 ) {
@@ -248,7 +329,9 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
248
329
timeout = 15_000 ,
249
330
} = options ;
250
331
251
- if ( version && version !== 'stable' ) {
332
+ if ( version === 'stable' ) {
333
+ version = await fetchTargetStableVersion ( { timeout, cachePath, platform } ) ;
334
+ } else if ( version ) {
252
335
/**
253
336
* Only validate version against server when no local download that matches version exists
254
337
*/
@@ -258,20 +341,27 @@ export async function download(options: Partial<DownloadOptions> = {}): Promise<
258
341
}
259
342
}
260
343
} else {
261
- version = await fetchTargetStableVersion ( timeout , cachePath , platform ) ;
344
+ version = await fetchTargetInferredVersion ( {
345
+ timeout,
346
+ cachePath,
347
+ platform,
348
+ extensionsDevelopmentPath : options . extensionDevelopmentPath ,
349
+ } ) ;
262
350
}
263
351
264
352
reporter . report ( { stage : ProgressReportStage . ResolvedVersion , version } ) ;
265
353
266
354
const downloadedPath = path . resolve ( cachePath , makeDownloadDirName ( platform , version ) ) ;
267
355
if ( fs . existsSync ( downloadedPath ) ) {
268
- if ( version === 'insiders' ) {
356
+ if ( isInsiderVersionIdentifier ( version ) ) {
269
357
reporter . report ( { stage : ProgressReportStage . FetchingInsidersMetadata } ) ;
270
358
const { version : currentHash , date : currentDate } = insidersDownloadDirMetadata ( downloadedPath , platform ) ;
271
359
272
- const { version : latestHash , timestamp : latestTimestamp } = await getLatestInsidersMetadata (
273
- systemDefaultPlatform
274
- ) ;
360
+ const { version : latestHash , timestamp : latestTimestamp } =
361
+ version === 'insiders'
362
+ ? await getLatestInsidersMetadata ( systemDefaultPlatform )
363
+ : await getInsidersVersionMetadata ( systemDefaultPlatform , version ) ;
364
+
275
365
if ( currentHash === latestHash ) {
276
366
reporter . report ( { stage : ProgressReportStage . FoundMatchingInstall , downloadedPath } ) ;
277
367
return Promise . resolve ( insidersDownloadDirToExecutablePath ( downloadedPath , platform ) ) ;
0 commit comments