@@ -9,6 +9,8 @@ import download from '../model/download'
9
9
import fetch , { FetchOptions } from '../model/fetch'
10
10
import { concurrent } from '../util'
11
11
import { loadJson , writeJson } from '../util/fs'
12
+ import { objectLiteral } from '../util/is'
13
+ import { Mutex } from '../util/mutex'
12
14
const logger = require ( '../util/logger' ) ( 'extension-dependency' )
13
15
14
16
export interface Dependencies { [ key : string ] : string }
@@ -33,12 +35,6 @@ interface ModuleInfo {
33
35
}
34
36
}
35
37
36
- interface ResolvedVersion {
37
- name : string
38
- requirement : string
39
- version : string
40
- }
41
-
42
38
export interface DependencyItem {
43
39
name : string
44
40
version : string
@@ -53,8 +49,10 @@ export interface DependencyItem {
53
49
54
50
const NPM_REGISTRY = new URL ( 'https://registry.npmjs.org' )
55
51
const YARN_REGISTRY = new URL ( 'https://registry.yarnpkg.com' )
52
+ const TAOBAO_REGISTRY = new URL ( 'https://registry.npmmirror.com' )
56
53
const DEV_DEPENDENCIES = [ 'coc.nvim' , 'webpack' , 'esbuild' ]
57
54
const INFO_TIMEOUT = global . __TEST__ ? 100 : 10000
55
+ const DOWNLOAD_TIMEOUT = global . __TEST__ ? 500 : 3 * 60 * 1000
58
56
59
57
function toFilename ( item : DependencyItem ) : string {
60
58
return `${ item . name } .${ item . version } .tgz`
@@ -73,14 +71,22 @@ export function getRegistries(registry: URL): URL[] {
73
71
return urls
74
72
}
75
73
74
+ export function validVersionInfo ( info : any ) : info is VersionInfo {
75
+ if ( ! info ) return false
76
+ if ( typeof info . name !== 'string' || typeof info . version !== 'string' || ! info . dist ) return false
77
+ let { tarball, integrity, shasum } = info . dist
78
+ if ( typeof tarball !== 'string' || typeof integrity !== 'string' || typeof shasum !== 'string' ) return false
79
+ return true
80
+ }
81
+
76
82
export function getModuleInfo ( text : string ) : ModuleInfo {
77
83
let obj
78
84
try {
79
85
obj = JSON . parse ( text ) as any
80
86
} catch ( e ) {
81
87
throw new Error ( `Invalid JSON data, ${ e } ` )
82
88
}
83
- if ( typeof obj . name !== 'string' || obj . versions == null ) throw new Error ( `Invalid JSON data, name or versions not found` )
89
+ if ( typeof obj . name !== 'string' || ! objectLiteral ( obj . versions ) ) throw new Error ( `Invalid JSON data, name or versions not found` )
84
90
return {
85
91
name : obj . name ,
86
92
latest : obj [ 'dist-tags' ] ?. latest ,
@@ -109,7 +115,7 @@ export function readDependencies(directory: string): { [key: string]: string } {
109
115
}
110
116
111
117
export function getVersion ( requirement : string , versions : string [ ] , latest ?: string ) : string | undefined {
112
- if ( latest && semver . satisfies ( latest , requirement ) ) return latest
118
+ if ( latest && validVersionInfo ( versions [ latest ] ) && semver . satisfies ( latest , requirement ) ) return latest
113
119
let sorted = semver . rsort ( versions . filter ( v => semver . valid ( v , { includePrerelease : false } ) ) )
114
120
for ( let v of sorted ) {
115
121
if ( semver . satisfies ( v , requirement ) ) return v
@@ -137,7 +143,7 @@ export async function checkFileSha1(filepath: string, shasum: string): Promise<b
137
143
if ( ! fs . existsSync ( filepath ) ) return Promise . resolve ( false )
138
144
return new Promise ( resolve => {
139
145
const input = createReadStream ( filepath )
140
- input . on ( 'error' , ( ) => {
146
+ input . on ( 'error' , e => {
141
147
resolve ( false )
142
148
} )
143
149
input . on ( 'readable' , ( ) => {
@@ -152,8 +158,9 @@ export async function checkFileSha1(filepath: string, shasum: string): Promise<b
152
158
} )
153
159
}
154
160
161
+ const mutex = new Mutex ( )
162
+
155
163
export class DependenciesInstaller {
156
- public resolvedVersions : ResolvedVersion [ ] = [ ]
157
164
public resolvedInfos : Map < string , ModuleInfo > = new Map ( )
158
165
private tokenSource : CancellationTokenSource = new CancellationTokenSource ( )
159
166
constructor (
@@ -168,28 +175,29 @@ export class DependenciesInstaller {
168
175
}
169
176
170
177
public async installDependencies ( directory : string ) : Promise < void > {
171
- // TODO reuse resolved.json
172
178
let dependencies = readDependencies ( directory )
173
179
// no need to install
174
180
if ( ! dependencies || Object . keys ( dependencies ) . length == 0 ) {
175
181
this . onMessage ( `No dependencies` )
176
182
return
177
183
}
178
- this . onMessage ( 'Resolving dependencies.' )
179
- await this . fetchInfos ( dependencies )
180
- this . onMessage ( 'Linking dependencies.' )
181
- // create DependencyItems
182
- let items : DependencyItem [ ] = [ ]
183
- this . linkDependencies ( dependencies , items )
184
- if ( items . length > 0 ) {
184
+ this . onMessage ( 'Waiting for install dependencies.' )
185
+ // TODO reuse resolved.json
186
+ await mutex . use ( async ( ) => {
187
+ this . onMessage ( 'Resolving dependencies.' )
188
+ await this . fetchInfos ( dependencies )
189
+ this . onMessage ( 'Linking dependencies.' )
190
+ // create DependencyItems
191
+ let items : DependencyItem [ ] = [ ]
192
+ this . linkDependencies ( dependencies , items )
185
193
let filepath = path . join ( directory , 'resolved.json' )
186
194
writeJson ( filepath , items )
187
- }
188
- this . onMessage ( 'Downloading dependencies.' )
189
- await this . downloadItems ( items )
190
- this . onMessage ( 'Extract modules.' )
191
- await this . extractDependencies ( items , dependencies , directory )
192
- this . onMessage ( 'Done' )
195
+ this . onMessage ( 'Downloading dependencies.' )
196
+ await this . downloadItems ( items )
197
+ this . onMessage ( 'Extract modules.' )
198
+ await this . extractDependencies ( items , dependencies , directory )
199
+ this . onMessage ( 'Done' )
200
+ } )
193
201
}
194
202
195
203
public async extractDependencies ( items : DependencyItem [ ] , dependencies : Dependencies , directory : string ) : Promise < void > {
@@ -211,19 +219,12 @@ export class DependenciesInstaller {
211
219
addToRoot ( item )
212
220
} )
213
221
rootPackages . clear ( )
214
-
215
- let err : unknown
216
- await concurrent ( rootItems , async item => {
217
- try {
218
- let filename = toFilename ( item )
219
- let tarfile = path . join ( this . dest , filename )
220
- let dest = path . join ( directory , 'node_modules' , item . name )
221
- await untar ( dest , tarfile )
222
- } catch ( e ) {
223
- err = e
224
- }
225
- } , 5 )
226
- if ( err ) throw err
222
+ for ( let item of rootItems ) {
223
+ let filename = toFilename ( item )
224
+ let tarfile = path . join ( this . dest , filename )
225
+ let dest = path . join ( directory , 'node_modules' , item . name )
226
+ await untar ( dest , tarfile )
227
+ }
227
228
for ( let item of rootItems ) {
228
229
let folder = path . join ( directory , 'node_modules' , item . name )
229
230
await this . extractFor ( item , items , rootItems , folder )
@@ -250,25 +251,20 @@ export class DependenciesInstaller {
250
251
} ) )
251
252
newRoot . push ( ...rootItems )
252
253
for ( let item of deps . values ( ) ) {
253
- if ( item . dependencies ) {
254
- let dest = path . join ( folder , 'node_modules' , item . name )
255
- await this . extractFor ( item , items , newRoot , dest )
256
- }
254
+ let dest = path . join ( folder , 'node_modules' , item . name )
255
+ await this . extractFor ( item , items , newRoot , dest )
257
256
}
258
257
}
259
258
260
259
public linkDependencies ( dependencies : Dependencies | undefined , items : DependencyItem [ ] ) : void {
261
260
if ( ! dependencies ) return
262
261
for ( let [ name , requirement ] of Object . entries ( dependencies ) ) {
263
- let version = this . resolveVersion ( name , requirement )
264
- let item = items . find ( o => o . name === name && o . version === version )
262
+ let versionInfo = this . resolveVersion ( name , requirement )
263
+ let item = items . find ( o => o . name === name && o . version === versionInfo . version )
265
264
if ( item ) {
266
265
if ( ! item . satisfiedVersions . includes ( requirement ) ) item . satisfiedVersions . push ( requirement )
267
266
} else {
268
- let info = this . resolvedInfos . get ( name )
269
- let versionInfo = info ? info . versions [ version ] : undefined
270
- if ( ! versionInfo ) throw new Error ( `Version data not found for "${ name } @${ version } "` )
271
- let { dist } = versionInfo
267
+ let { dist, version } = versionInfo
272
268
items . push ( {
273
269
name,
274
270
version,
@@ -283,15 +279,14 @@ export class DependenciesInstaller {
283
279
}
284
280
}
285
281
286
- private resolveVersion ( name : string , requirement : string ) : string {
287
- let item = this . resolvedVersions . find ( o => o . name == name && o . requirement == requirement )
288
- if ( item ) return item . version
282
+ public resolveVersion ( name : string , requirement : string ) : VersionInfo {
289
283
let info = this . resolvedInfos . get ( name )
290
284
if ( info ) {
291
285
let version = getVersion ( requirement , Object . keys ( info . versions ) , info . latest )
292
286
if ( version ) {
293
- this . resolvedVersions . push ( { name, requirement, version } )
294
- return version
287
+ let versionInfo = info . versions [ version ]
288
+ versionInfo . version = version
289
+ if ( validVersionInfo ( versionInfo ) ) return versionInfo
295
290
}
296
291
}
297
292
throw new Error ( `No valid version found for "${ name } " ${ requirement } ` )
@@ -300,25 +295,18 @@ export class DependenciesInstaller {
300
295
/**
301
296
* Recursive fetch
302
297
*/
303
- public async fetchInfos ( dependencies : Dependencies ) : Promise < void > {
304
- let keys = Object . keys ( dependencies )
298
+ public async fetchInfos ( dependencies : Dependencies | undefined ) : Promise < void > {
299
+ let keys = Object . keys ( dependencies ?? { } )
305
300
if ( keys . length === 0 ) return
306
- let fetched : Map < string , ModuleInfo > = new Map ( )
307
301
await Promise . all ( keys . map ( key => {
308
- if ( this . resolvedInfos . has ( key ) ) {
309
- fetched . set ( key , this . resolvedInfos . get ( key ) )
310
- return Promise . resolve ( )
311
- }
302
+ if ( this . resolvedInfos . has ( key ) ) return Promise . resolve ( )
312
303
return this . loadInfo ( this . registry , key , INFO_TIMEOUT ) . then ( info => {
313
- fetched . set ( key , info )
314
304
this . resolvedInfos . set ( key , info )
315
305
} )
316
306
} ) )
317
- for ( let [ key , info ] of fetched . entries ( ) ) {
318
- let requirement = dependencies [ key ]
319
- let version = this . resolveVersion ( key , requirement )
320
- let deps = info . versions [ version ] . dependencies
321
- if ( deps ) await this . fetchInfos ( deps )
307
+ for ( let key of keys ) {
308
+ let versionInfo = this . resolveVersion ( key , dependencies [ key ] )
309
+ await this . fetchInfos ( versionInfo . dependencies )
322
310
}
323
311
}
324
312
@@ -338,13 +326,13 @@ export class DependenciesInstaller {
338
326
let onFinish = ( ) => {
339
327
res . set ( filename , filepath )
340
328
finished ++
341
- this . onMessage ( `Downloaded ${ finished } /${ total } ` )
329
+ this . onMessage ( `Downloaded ${ filename } ${ finished } /${ total } ` )
342
330
}
343
331
if ( checked ) {
344
332
onFinish ( )
345
333
} else {
346
334
// 5min timeout
347
- await this . download ( new URL ( item . resolved ) , filename , item . shasum , retry , global . __TEST__ ? 1000 : 5 * 60 * 1000 )
335
+ await this . download ( new URL ( item . resolved ) , filename , item . shasum , retry , DOWNLOAD_TIMEOUT )
348
336
onFinish ( )
349
337
}
350
338
} catch ( e ) {
@@ -355,7 +343,7 @@ export class DependenciesInstaller {
355
343
return res
356
344
}
357
345
358
- public async fetch ( url : string | URL , options : FetchOptions = { } , retry = 1 ) : Promise < any > {
346
+ public async fetch ( url : string | URL , options : FetchOptions , retry = 1 ) : Promise < any > {
359
347
for ( let i = 0 ; i < retry ; i ++ ) {
360
348
try {
361
349
return await fetch ( url , options , this . tokenSource . token )
@@ -381,8 +369,7 @@ export class DependenciesInstaller {
381
369
this . onMessage ( `Error on fetch ${ url . hostname } /${ name } : ${ e } ` )
382
370
}
383
371
}
384
- if ( ! info ) throw new Error ( `Unable to fetch info for "${ name } "` )
385
- return info
372
+ throw new Error ( `Unable to fetch info for "${ name } "` )
386
373
}
387
374
388
375
public async download ( url : string | URL , filename : string , shasum : string , retry = 1 , timeout ?: number ) : Promise < string > {
0 commit comments