@@ -16,13 +16,15 @@ const {
16
16
PromiseAll,
17
17
RegExpPrototypeExec,
18
18
SafeArrayIterator,
19
+ SafeMap,
19
20
SafeWeakMap,
20
21
StringPrototypeStartsWith,
21
22
globalThis,
22
23
} = primordials ;
23
24
const { MessageChannel } = require ( 'internal/worker/io' ) ;
24
25
25
26
const {
27
+ ERR_INCOMPLETE_LOADER_CHAIN ,
26
28
ERR_INTERNAL_ASSERTION ,
27
29
ERR_INVALID_ARG_TYPE ,
28
30
ERR_INVALID_ARG_VALUE ,
@@ -70,28 +72,30 @@ class ESMLoader {
70
72
/**
71
73
* Prior to ESM loading. These are called once before any modules are started.
72
74
* @private
73
- * @property {Function[] } globalPreloaders First -in-first-out list of
74
- * preload hooks.
75
+ * @property {Map<URL['href'], Function> } globalPreloaders Last -in-first-out
76
+ * list of preload hooks.
75
77
*/
76
- #globalPreloaders = [ ] ;
78
+ #globalPreloaders = new SafeMap ( ) ;
77
79
78
80
/**
79
81
* Phase 2 of 2 in ESM loading.
80
82
* @private
81
- * @property {Function[] } loaders First-in-first-out list of loader hooks.
83
+ * @property {Map<URL['href'], Function> } loaders Last-in-first-out
84
+ * collection of loader hooks.
82
85
*/
83
- #loaders = [
84
- defaultLoad ,
85
- ] ;
86
+ #loaders = new SafeMap ( [
87
+ [ 'node:esm/load.js' , defaultLoad ] ,
88
+ ] ) ;
86
89
87
90
/**
88
91
* Phase 1 of 2 in ESM loading.
89
92
* @private
90
- * @property {Function[] } resolvers First-in-first-out list of resolver hooks
93
+ * @property {Map<URL['href'], Function> } resolvers Last-in-first-out
94
+ * collection of resolver hooks.
91
95
*/
92
- #resolvers = [
93
- defaultResolve ,
94
- ] ;
96
+ #resolvers = new SafeMap ( [
97
+ [ 'node:esm/resolve.js' , defaultResolve ] ,
98
+ ] ) ;
95
99
96
100
#importMetaInitializer = initializeImportMeta ;
97
101
@@ -115,7 +119,9 @@ class ESMLoader {
115
119
*/
116
120
translators = translators ;
117
121
118
- constructor ( ) {
122
+ constructor ( { isInternal = false } = { } ) {
123
+ this . isInternal = isInternal ;
124
+
119
125
if ( getOptionValue ( '--experimental-loader' ) ) {
120
126
emitExperimentalWarning ( 'Custom ESM Loaders' ) ;
121
127
}
@@ -198,32 +204,46 @@ class ESMLoader {
198
204
* user-defined loaders (as returned by ESMLoader.import()).
199
205
*/
200
206
async addCustomLoaders (
201
- customLoaders = [ ] ,
207
+ customLoaders = new SafeMap ( ) ,
202
208
) {
203
- if ( ! ArrayIsArray ( customLoaders ) ) customLoaders = [ customLoaders ] ;
204
-
205
- for ( let i = 0 ; i < customLoaders . length ; i ++ ) {
206
- const exports = customLoaders [ i ] ;
209
+ // Maps are first-in-first-out, but hook chains are last-in-first-out,
210
+ // so create a new container for the incoming hooks (which have already
211
+ // been reversed).
212
+ const globalPreloaders = new SafeMap ( ) ;
213
+ const resolvers = new SafeMap ( ) ;
214
+ const loaders = new SafeMap ( ) ;
215
+
216
+ for ( const { 0 : url , 1 : exports } of customLoaders ) {
207
217
const {
208
218
globalPreloader,
209
219
resolver,
210
220
loader,
211
221
} = ESMLoader . pluckHooks ( exports ) ;
212
222
213
- if ( globalPreloader ) ArrayPrototypePush (
214
- this . #globalPreloaders ,
223
+ if ( globalPreloader ) globalPreloaders . set (
224
+ url ,
215
225
FunctionPrototypeBind ( globalPreloader , null ) , // [1]
216
226
) ;
217
- if ( resolver ) ArrayPrototypePush (
218
- this . #resolvers ,
227
+ if ( resolver ) resolvers . set (
228
+ url ,
219
229
FunctionPrototypeBind ( resolver , null ) , // [1]
220
230
) ;
221
- if ( loader ) ArrayPrototypePush (
222
- this . #loaders ,
231
+ if ( loader ) loaders . set (
232
+ url ,
223
233
FunctionPrototypeBind ( loader , null ) , // [1]
224
234
) ;
225
235
}
226
236
237
+ // Append the pre-existing hooks (the builtin/default ones)
238
+ for ( const p of this . #globalPreloaders) globalPreloaders . set ( p [ 0 ] , p [ 1 ] ) ;
239
+ for ( const p of this . #resolvers) resolvers . set ( p [ 0 ] , p [ 1 ] ) ;
240
+ for ( const p of this . #loaders) loaders . set ( p [ 0 ] , p [ 1 ] ) ;
241
+
242
+ // Replace the obsolete maps with the fully-loaded & properly sequenced one
243
+ this . #globalPreloaders = globalPreloaders ;
244
+ this . #resolvers = resolvers ;
245
+ this . #loaders = loaders ;
246
+
227
247
// [1] ensure hook function is not bound to ESMLoader instance
228
248
229
249
this . preload ( ) ;
@@ -308,14 +328,21 @@ class ESMLoader {
308
328
*/
309
329
async getModuleJob ( specifier , parentURL , importAssertions ) {
310
330
let importAssertionsForResolve ;
311
- if ( this . #loaders. length !== 1 ) {
312
- // We can skip cloning if there are no user provided loaders because
331
+
332
+ if ( this . #loaders. size !== 1 ) {
333
+ // We can skip cloning if there are no user-provided loaders because
313
334
// the Node.js default resolve hook does not use import assertions.
314
- importAssertionsForResolve =
315
- ObjectAssign ( ObjectCreate ( null ) , importAssertions ) ;
335
+ importAssertionsForResolve = ObjectAssign (
336
+ ObjectCreate ( null ) ,
337
+ importAssertions ,
338
+ ) ;
316
339
}
317
- const { format, url } =
318
- await this . resolve ( specifier , parentURL , importAssertionsForResolve ) ;
340
+
341
+ const { format, url } = await this . resolve (
342
+ specifier ,
343
+ parentURL ,
344
+ importAssertionsForResolve ,
345
+ ) ;
319
346
320
347
let job = this . moduleMap . get ( url , importAssertions . type ) ;
321
348
@@ -408,9 +435,13 @@ class ESMLoader {
408
435
409
436
const namespaces = await PromiseAll ( new SafeArrayIterator ( jobs ) ) ;
410
437
411
- return wasArr ?
412
- namespaces :
413
- namespaces [ 0 ] ;
438
+ if ( ! wasArr ) return namespaces [ 0 ] ;
439
+
440
+ const namespaceMap = new SafeMap ( ) ;
441
+
442
+ for ( let i = 0 ; i < count ; i ++ ) namespaceMap . set ( specifiers [ i ] , namespaces [ i ] ) ;
443
+
444
+ return namespaceMap ;
414
445
}
415
446
416
447
/**
@@ -423,12 +454,33 @@ class ESMLoader {
423
454
* @returns {object }
424
455
*/
425
456
async load ( url , context = { } ) {
426
- const defaultLoader = this . #loaders[ 0 ] ;
457
+ const loaders = this . #loaders. entries ( ) ;
458
+ let {
459
+ 0 : loaderFilePath ,
460
+ 1 : loader ,
461
+ } = loaders . next ( ) . value ;
462
+ let chainFinished = this . #loaders. size === 1 ;
463
+
464
+ function next ( nextUrl ) {
465
+ const {
466
+ done,
467
+ value,
468
+ } = loaders . next ( ) ;
469
+ ( {
470
+ 0 : loaderFilePath ,
471
+ 1 : loader ,
472
+ } = value ) ;
473
+
474
+ if ( done || loader === defaultLoad ) chainFinished = true ;
475
+
476
+ return loader ( nextUrl , context , next ) ;
477
+ }
427
478
428
- const loader = this . #loaders. length === 1 ?
429
- defaultLoader :
430
- this . #loaders[ 1 ] ;
431
- const loaded = await loader ( url , context , defaultLoader ) ;
479
+ const loaded = await loader (
480
+ url ,
481
+ context ,
482
+ next ,
483
+ ) ;
432
484
433
485
if ( typeof loaded !== 'object' ) {
434
486
throw new ERR_INVALID_RETURN_VALUE (
@@ -440,9 +492,14 @@ class ESMLoader {
440
492
441
493
const {
442
494
format,
495
+ shortCircuit,
443
496
source,
444
497
} = loaded ;
445
498
499
+ if ( ! chainFinished && ! shortCircuit ) {
500
+ throw new ERR_INCOMPLETE_LOADER_CHAIN ( 'load' , loaderFilePath ) ;
501
+ }
502
+
446
503
if ( format == null ) {
447
504
const dataUrl = RegExpPrototypeExec (
448
505
/ ^ d a t a : ( [ ^ / ] + \/ [ ^ ; , ] + ) (?: [ ^ , ] * ?) ( ; b a s e 6 4 ) ? , / ,
@@ -594,21 +651,38 @@ class ESMLoader {
594
651
parentURL ,
595
652
) ;
596
653
597
- const conditions = DEFAULT_CONDITIONS ;
654
+ const resolvers = this . #resolvers. entries ( ) ;
655
+ let {
656
+ 0 : resolverFilePath ,
657
+ 1 : resolver ,
658
+ } = resolvers . next ( ) . value ;
659
+ let chainFinished = this . #resolvers. size === 1 ;
598
660
599
- const defaultResolver = this . #resolvers[ 0 ] ;
661
+ const context = {
662
+ conditions : DEFAULT_CONDITIONS ,
663
+ importAssertions,
664
+ parentURL,
665
+ } ;
666
+
667
+ function next ( suppliedUrl ) {
668
+ const {
669
+ done,
670
+ value,
671
+ } = resolvers . next ( ) ;
672
+ ( {
673
+ 0 : resolverFilePath ,
674
+ 1 : resolver ,
675
+ } = value ) ;
676
+
677
+ if ( done || resolver === defaultResolve ) chainFinished = true ;
678
+
679
+ return resolver ( suppliedUrl , context , next ) ;
680
+ }
600
681
601
- const resolver = this . #resolvers. length === 1 ?
602
- defaultResolver :
603
- this . #resolvers[ 1 ] ;
604
682
const resolution = await resolver (
605
683
originalSpecifier ,
606
- {
607
- conditions,
608
- importAssertions,
609
- parentURL,
610
- } ,
611
- defaultResolver ,
684
+ context ,
685
+ next ,
612
686
) ;
613
687
614
688
if ( typeof resolution !== 'object' ) {
@@ -619,7 +693,15 @@ class ESMLoader {
619
693
) ;
620
694
}
621
695
622
- const { format, url } = resolution ;
696
+ const {
697
+ format,
698
+ shortCircuit,
699
+ url,
700
+ } = resolution ;
701
+
702
+ if ( ! chainFinished && ! shortCircuit ) {
703
+ throw new ERR_INCOMPLETE_LOADER_CHAIN ( 'resolve' , resolverFilePath ) ;
704
+ }
623
705
624
706
if (
625
707
format != null &&
0 commit comments