@@ -249,73 +249,162 @@ module.exports = cls => class Builder extends cls {
249
249
if ( ! queue . length )
250
250
return
251
251
252
+ // helper class to manage script execution
253
+ class Script {
254
+ constructor ( opts ) {
255
+ const {
256
+ node,
257
+ timer,
258
+ ...runScriptOpts
259
+ } = opts
260
+ this . node = node
261
+ this . timer = timer
262
+ this . opts = runScriptOpts
263
+ this . pkg = opts . pkg
264
+ this . path = opts . path
265
+ this . env = opts . env
266
+ this . ref = this . node . target . path
267
+
268
+ if ( ! Script . runPromises )
269
+ Script . runPromises = new Map ( )
270
+ }
271
+
272
+ // when the script to be run belongs to a link node, this method
273
+ // retrieves a list of all other link nodes that are direct dependencies
274
+ // of the current node, their scripts also have to be part of the current
275
+ // queue, otherwise it just returns an empty list
276
+ // this is necessary in order to enable workspaces lifecycle scripts
277
+ // that depend on each other
278
+ linkLinkedDeps ( ) {
279
+ const res = [ ]
280
+
281
+ if ( ! this . node . isLink )
282
+ return res
283
+
284
+ for ( const edge of this . node . target . edgesOut . values ( ) ) {
285
+ if ( edge . to . isLink &&
286
+ queue . some ( node => node . isLink && node . target . name === edge . name ) )
287
+ res . push ( edge . to . target . path )
288
+ }
289
+
290
+ return res
291
+ }
292
+
293
+ // use the list of depended scripts from linkLinkedDeps()
294
+ // and returns a Promise that resolves once all scripts
295
+ // this current script depends are resolved
296
+ async linkLinkedDepsResolved ( ) {
297
+ const resolved = ( ref ) => {
298
+ const p = Script . runPromises . get ( ref )
299
+ // polling mechanism used in case the depended script is not
300
+ // yet set in Script.runPromises, this allow us to keep the
301
+ // alphabetical order of execution at first and the dependency
302
+ // mechanism for linked deps is only used when necessary
303
+ if ( ! p ) {
304
+ return new Promise ( res =>
305
+ setTimeout ( ( ) => res ( resolved ( ref ) ) , 1 ) )
306
+ }
307
+ return p
308
+ }
309
+
310
+ return await Promise . all (
311
+ this . linkLinkedDeps ( ) . map ( ref => resolved ( ref ) ) )
312
+ }
313
+
314
+ async run ( ) {
315
+ // in case the current script belongs to a link node that also has
316
+ // linked nodes dependencies (e.g: workspaces that depend on each other)
317
+ // then we await for that dependency script to be resolved before
318
+ // executing the current script
319
+ await this . linkLinkedDepsResolved ( )
320
+
321
+ // executes the current script using @npmcli /run-script
322
+ const p = runScript ( this . opts )
323
+
324
+ // keeps a pool of executing scripts in order to track
325
+ // inter-dependencies between these scripts
326
+ Script . runPromises . set ( this . ref , p )
327
+ return p
328
+ }
329
+ }
330
+
252
331
process . emit ( 'time' , `build:run:${ event } ` )
253
332
const stdio = this . options . foregroundScripts ? 'inherit' : 'pipe'
254
333
const limit = this . options . foregroundScripts ? 1 : undefined
255
- await promiseCallLimit ( queue . map ( node => async ( ) => {
256
- const {
257
- path,
258
- integrity,
259
- resolved,
260
- optional,
261
- peer,
262
- dev,
263
- devOptional,
264
- package : pkg ,
265
- location,
266
- } = node . target
267
-
268
- // skip any that we know we'll be deleting
269
- if ( this [ _trashList ] . has ( path ) )
270
- return
271
-
272
- const timer = `build:run:${ event } :${ location } `
273
- process . emit ( 'time' , timer )
274
- this . log . info ( 'run' , pkg . _id , event , location , pkg . scripts [ event ] )
275
- const env = {
276
- npm_package_resolved : resolved ,
277
- npm_package_integrity : integrity ,
278
- npm_package_json : resolve ( path , 'package.json' ) ,
279
- npm_package_optional : boolEnv ( optional ) ,
280
- npm_package_dev : boolEnv ( dev ) ,
281
- npm_package_peer : boolEnv ( peer ) ,
282
- npm_package_dev_optional :
283
- boolEnv ( devOptional && ! dev && ! optional ) ,
284
- }
285
- const runOpts = {
286
- event,
287
- path,
288
- pkg,
289
- stdioString : true ,
290
- stdio,
291
- env,
292
- scriptShell : this [ _scriptShell ] ,
293
- }
294
- const p = runScript ( runOpts ) . catch ( er => {
295
- const { code, signal } = er
296
- this . log . info ( 'run' , pkg . _id , event , { code, signal} )
297
- throw er
298
- } ) . then ( ( { args, code, signal, stdout, stderr} ) => {
299
- this . scriptsRun . add ( {
300
- pkg,
334
+ await promiseCallLimit ( queue
335
+ . map ( node => {
336
+ const {
301
337
path,
338
+ integrity,
339
+ resolved,
340
+ optional,
341
+ peer,
342
+ dev,
343
+ devOptional,
344
+ package : pkg ,
345
+ location,
346
+ } = node . target
347
+
348
+ // skip any that we know we'll be deleting
349
+ if ( this [ _trashList ] . has ( path ) )
350
+ return
351
+
352
+ const timer = `build:run:${ event } :${ location } `
353
+ process . emit ( 'time' , timer )
354
+ this . log . info ( 'run' , pkg . _id , event , location , pkg . scripts [ event ] )
355
+ const env = {
356
+ npm_package_resolved : resolved ,
357
+ npm_package_integrity : integrity ,
358
+ npm_package_json : resolve ( path , 'package.json' ) ,
359
+ npm_package_optional : boolEnv ( optional ) ,
360
+ npm_package_dev : boolEnv ( dev ) ,
361
+ npm_package_peer : boolEnv ( peer ) ,
362
+ npm_package_dev_optional :
363
+ boolEnv ( devOptional && ! dev && ! optional ) ,
364
+ }
365
+ return new Script ( {
366
+ node,
367
+ timer,
302
368
event,
303
- cmd : args && args [ args . length - 1 ] ,
369
+ path,
370
+ pkg,
371
+ stdioString : true ,
372
+ stdio,
304
373
env,
305
- code,
306
- signal,
307
- stdout,
308
- stderr,
374
+ scriptShell : this [ _scriptShell ] ,
309
375
} )
310
- this . log . info ( 'run' , pkg . _id , event , { code, signal} )
311
376
} )
377
+ . filter ( Boolean )
378
+ . map ( script => async ( ) => {
379
+ const p = script . run ( ) . catch ( er => {
380
+ const { code, signal } = er
381
+ this . log . info ( 'run' , script . pkg . _id , event , { code, signal} )
382
+ throw er
383
+ } ) . then ( ( { args, code, signal, stdout, stderr} ) => {
384
+ this . scriptsRun . add ( {
385
+ pkg : script . pkg ,
386
+ path : script . path ,
387
+ event,
388
+ cmd : args && args [ args . length - 1 ] ,
389
+ env : script . env ,
390
+ code,
391
+ signal,
392
+ stdout,
393
+ stderr,
394
+ } )
395
+ this . log . info ( 'run' , script . pkg . _id , event , { code, signal} )
396
+ } )
397
+
398
+ await ( this [ _doHandleOptionalFailure ]
399
+ ? this [ _handleOptionalFailure ] ( script . node , p )
400
+ : p )
401
+
402
+ process . emit ( 'timeEnd' , script . timer )
403
+ } ) , limit )
312
404
313
- await ( this [ _doHandleOptionalFailure ]
314
- ? this [ _handleOptionalFailure ] ( node , p )
315
- : p )
405
+ // release pool refs to scripts promises
406
+ delete Script . runPromises
316
407
317
- process . emit ( 'timeEnd' , timer )
318
- } ) , limit )
319
408
process . emit ( 'timeEnd' , `build:run:${ event } ` )
320
409
}
321
410
0 commit comments