@@ -9,6 +9,8 @@ import download from '../model/download'
99import fetch , { FetchOptions } from '../model/fetch'
1010import { concurrent } from '../util'
1111import { loadJson , writeJson } from '../util/fs'
12+ import { objectLiteral } from '../util/is'
13+ import { Mutex } from '../util/mutex'
1214const logger = require ( '../util/logger' ) ( 'extension-dependency' )
1315
1416export interface Dependencies { [ key : string ] : string }
@@ -33,12 +35,6 @@ interface ModuleInfo {
3335 }
3436}
3537
36- interface ResolvedVersion {
37- name : string
38- requirement : string
39- version : string
40- }
41-
4238export interface DependencyItem {
4339 name : string
4440 version : string
@@ -53,8 +49,10 @@ export interface DependencyItem {
5349
5450const NPM_REGISTRY = new URL ( 'https://registry.npmjs.org' )
5551const YARN_REGISTRY = new URL ( 'https://registry.yarnpkg.com' )
52+ const TAOBAO_REGISTRY = new URL ( 'https://registry.npmmirror.com' )
5653const DEV_DEPENDENCIES = [ 'coc.nvim' , 'webpack' , 'esbuild' ]
5754const INFO_TIMEOUT = global . __TEST__ ? 100 : 10000
55+ const DOWNLOAD_TIMEOUT = global . __TEST__ ? 500 : 3 * 60 * 1000
5856
5957function toFilename ( item : DependencyItem ) : string {
6058 return `${ item . name } .${ item . version } .tgz`
@@ -73,14 +71,22 @@ export function getRegistries(registry: URL): URL[] {
7371 return urls
7472}
7573
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+
7682export function getModuleInfo ( text : string ) : ModuleInfo {
7783 let obj
7884 try {
7985 obj = JSON . parse ( text ) as any
8086 } catch ( e ) {
8187 throw new Error ( `Invalid JSON data, ${ e } ` )
8288 }
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` )
8490 return {
8591 name : obj . name ,
8692 latest : obj [ 'dist-tags' ] ?. latest ,
@@ -109,7 +115,7 @@ export function readDependencies(directory: string): { [key: string]: string } {
109115}
110116
111117export 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
113119 let sorted = semver . rsort ( versions . filter ( v => semver . valid ( v , { includePrerelease : false } ) ) )
114120 for ( let v of sorted ) {
115121 if ( semver . satisfies ( v , requirement ) ) return v
@@ -137,7 +143,7 @@ export async function checkFileSha1(filepath: string, shasum: string): Promise<b
137143 if ( ! fs . existsSync ( filepath ) ) return Promise . resolve ( false )
138144 return new Promise ( resolve => {
139145 const input = createReadStream ( filepath )
140- input . on ( 'error' , ( ) => {
146+ input . on ( 'error' , e => {
141147 resolve ( false )
142148 } )
143149 input . on ( 'readable' , ( ) => {
@@ -152,8 +158,9 @@ export async function checkFileSha1(filepath: string, shasum: string): Promise<b
152158 } )
153159}
154160
161+ const mutex = new Mutex ( )
162+
155163export class DependenciesInstaller {
156- public resolvedVersions : ResolvedVersion [ ] = [ ]
157164 public resolvedInfos : Map < string , ModuleInfo > = new Map ( )
158165 private tokenSource : CancellationTokenSource = new CancellationTokenSource ( )
159166 constructor (
@@ -168,28 +175,29 @@ export class DependenciesInstaller {
168175 }
169176
170177 public async installDependencies ( directory : string ) : Promise < void > {
171- // TODO reuse resolved.json
172178 let dependencies = readDependencies ( directory )
173179 // no need to install
174180 if ( ! dependencies || Object . keys ( dependencies ) . length == 0 ) {
175181 this . onMessage ( `No dependencies` )
176182 return
177183 }
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 )
185193 let filepath = path . join ( directory , 'resolved.json' )
186194 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+ } )
193201 }
194202
195203 public async extractDependencies ( items : DependencyItem [ ] , dependencies : Dependencies , directory : string ) : Promise < void > {
@@ -211,19 +219,12 @@ export class DependenciesInstaller {
211219 addToRoot ( item )
212220 } )
213221 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+ }
227228 for ( let item of rootItems ) {
228229 let folder = path . join ( directory , 'node_modules' , item . name )
229230 await this . extractFor ( item , items , rootItems , folder )
@@ -250,25 +251,20 @@ export class DependenciesInstaller {
250251 } ) )
251252 newRoot . push ( ...rootItems )
252253 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 )
257256 }
258257 }
259258
260259 public linkDependencies ( dependencies : Dependencies | undefined , items : DependencyItem [ ] ) : void {
261260 if ( ! dependencies ) return
262261 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 )
265264 if ( item ) {
266265 if ( ! item . satisfiedVersions . includes ( requirement ) ) item . satisfiedVersions . push ( requirement )
267266 } 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
272268 items . push ( {
273269 name,
274270 version,
@@ -283,15 +279,14 @@ export class DependenciesInstaller {
283279 }
284280 }
285281
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 {
289283 let info = this . resolvedInfos . get ( name )
290284 if ( info ) {
291285 let version = getVersion ( requirement , Object . keys ( info . versions ) , info . latest )
292286 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
295290 }
296291 }
297292 throw new Error ( `No valid version found for "${ name } " ${ requirement } ` )
@@ -300,25 +295,18 @@ export class DependenciesInstaller {
300295 /**
301296 * Recursive fetch
302297 */
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 ?? { } )
305300 if ( keys . length === 0 ) return
306- let fetched : Map < string , ModuleInfo > = new Map ( )
307301 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 ( )
312303 return this . loadInfo ( this . registry , key , INFO_TIMEOUT ) . then ( info => {
313- fetched . set ( key , info )
314304 this . resolvedInfos . set ( key , info )
315305 } )
316306 } ) )
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 )
322310 }
323311 }
324312
@@ -338,13 +326,13 @@ export class DependenciesInstaller {
338326 let onFinish = ( ) => {
339327 res . set ( filename , filepath )
340328 finished ++
341- this . onMessage ( `Downloaded ${ finished } /${ total } ` )
329+ this . onMessage ( `Downloaded ${ filename } ${ finished } /${ total } ` )
342330 }
343331 if ( checked ) {
344332 onFinish ( )
345333 } else {
346334 // 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 )
348336 onFinish ( )
349337 }
350338 } catch ( e ) {
@@ -355,7 +343,7 @@ export class DependenciesInstaller {
355343 return res
356344 }
357345
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 > {
359347 for ( let i = 0 ; i < retry ; i ++ ) {
360348 try {
361349 return await fetch ( url , options , this . tokenSource . token )
@@ -381,8 +369,7 @@ export class DependenciesInstaller {
381369 this . onMessage ( `Error on fetch ${ url . hostname } /${ name } : ${ e } ` )
382370 }
383371 }
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 } "` )
386373 }
387374
388375 public async download ( url : string | URL , filename : string , shasum : string , retry = 1 , timeout ?: number ) : Promise < string > {
0 commit comments