@@ -16,13 +16,15 @@ const {
1616 PromiseAll,
1717 RegExpPrototypeExec,
1818 SafeArrayIterator,
19+ SafeMap,
1920 SafeWeakMap,
2021 StringPrototypeStartsWith,
2122 globalThis,
2223} = primordials ;
2324const { MessageChannel } = require ( 'internal/worker/io' ) ;
2425
2526const {
27+ ERR_INCOMPLETE_LOADER_CHAIN ,
2628 ERR_INTERNAL_ASSERTION ,
2729 ERR_INVALID_ARG_TYPE ,
2830 ERR_INVALID_ARG_VALUE ,
@@ -70,28 +72,30 @@ class ESMLoader {
7072 /**
7173 * Prior to ESM loading. These are called once before any modules are started.
7274 * @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.
7577 */
76- #globalPreloaders = [ ] ;
78+ #globalPreloaders = new SafeMap ( ) ;
7779
7880 /**
7981 * Phase 2 of 2 in ESM loading.
8082 * @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.
8285 */
83- #loaders = [
84- defaultLoad ,
85- ] ;
86+ #loaders = new SafeMap ( [
87+ [ 'node:esm/load.js' , defaultLoad ] ,
88+ ] ) ;
8689
8790 /**
8891 * Phase 1 of 2 in ESM loading.
8992 * @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.
9195 */
92- #resolvers = [
93- defaultResolve ,
94- ] ;
96+ #resolvers = new SafeMap ( [
97+ [ 'node:esm/resolve.js' , defaultResolve ] ,
98+ ] ) ;
9599
96100 #importMetaInitializer = initializeImportMeta ;
97101
@@ -115,7 +119,9 @@ class ESMLoader {
115119 */
116120 translators = translators ;
117121
118- constructor ( ) {
122+ constructor ( { isInternal = false } = { } ) {
123+ this . isInternal = isInternal ;
124+
119125 if ( getOptionValue ( '--experimental-loader' ) ) {
120126 emitExperimentalWarning ( 'Custom ESM Loaders' ) ;
121127 }
@@ -198,32 +204,46 @@ class ESMLoader {
198204 * user-defined loaders (as returned by ESMLoader.import()).
199205 */
200206 async addCustomLoaders (
201- customLoaders = [ ] ,
207+ customLoaders = new SafeMap ( ) ,
202208 ) {
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 ) {
207217 const {
208218 globalPreloader,
209219 resolver,
210220 loader,
211221 } = ESMLoader . pluckHooks ( exports ) ;
212222
213- if ( globalPreloader ) ArrayPrototypePush (
214- this . #globalPreloaders ,
223+ if ( globalPreloader ) globalPreloaders . set (
224+ url ,
215225 FunctionPrototypeBind ( globalPreloader , null ) , // [1]
216226 ) ;
217- if ( resolver ) ArrayPrototypePush (
218- this . #resolvers ,
227+ if ( resolver ) resolvers . set (
228+ url ,
219229 FunctionPrototypeBind ( resolver , null ) , // [1]
220230 ) ;
221- if ( loader ) ArrayPrototypePush (
222- this . #loaders ,
231+ if ( loader ) loaders . set (
232+ url ,
223233 FunctionPrototypeBind ( loader , null ) , // [1]
224234 ) ;
225235 }
226236
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+
227247 // [1] ensure hook function is not bound to ESMLoader instance
228248
229249 this . preload ( ) ;
@@ -308,14 +328,21 @@ class ESMLoader {
308328 */
309329 async getModuleJob ( specifier , parentURL , importAssertions ) {
310330 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
313334 // 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+ ) ;
316339 }
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+ ) ;
319346
320347 let job = this . moduleMap . get ( url , importAssertions . type ) ;
321348
@@ -408,9 +435,13 @@ class ESMLoader {
408435
409436 const namespaces = await PromiseAll ( new SafeArrayIterator ( jobs ) ) ;
410437
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 ;
414445 }
415446
416447 /**
@@ -423,12 +454,33 @@ class ESMLoader {
423454 * @returns {object }
424455 */
425456 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+ }
427478
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+ ) ;
432484
433485 if ( typeof loaded !== 'object' ) {
434486 throw new ERR_INVALID_RETURN_VALUE (
@@ -440,9 +492,14 @@ class ESMLoader {
440492
441493 const {
442494 format,
495+ shortCircuit,
443496 source,
444497 } = loaded ;
445498
499+ if ( ! chainFinished && ! shortCircuit ) {
500+ throw new ERR_INCOMPLETE_LOADER_CHAIN ( 'load' , loaderFilePath ) ;
501+ }
502+
446503 if ( format == null ) {
447504 const dataUrl = RegExpPrototypeExec (
448505 / ^ d a t a : ( [ ^ / ] + \/ [ ^ ; , ] + ) (?: [ ^ , ] * ?) ( ; b a s e 6 4 ) ? , / ,
@@ -594,21 +651,38 @@ class ESMLoader {
594651 parentURL ,
595652 ) ;
596653
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 ;
598660
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+ }
600681
601- const resolver = this . #resolvers. length === 1 ?
602- defaultResolver :
603- this . #resolvers[ 1 ] ;
604682 const resolution = await resolver (
605683 originalSpecifier ,
606- {
607- conditions,
608- importAssertions,
609- parentURL,
610- } ,
611- defaultResolver ,
684+ context ,
685+ next ,
612686 ) ;
613687
614688 if ( typeof resolution !== 'object' ) {
@@ -619,7 +693,15 @@ class ESMLoader {
619693 ) ;
620694 }
621695
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+ }
623705
624706 if (
625707 format != null &&
0 commit comments