@@ -12,17 +12,22 @@ const {
12
12
FunctionPrototypeCall,
13
13
ObjectAssign,
14
14
ObjectCreate,
15
+ ObjectDefineProperty,
15
16
ObjectSetPrototypeOf,
16
17
PromiseAll,
18
+ ReflectApply,
17
19
RegExpPrototypeExec,
18
20
SafeArrayIterator,
19
21
SafeWeakMap,
22
+ StringPrototypeSlice,
23
+ StringPrototypeToUpperCase,
20
24
globalThis,
21
25
} = primordials ;
22
26
const { MessageChannel } = require ( 'internal/worker/io' ) ;
23
27
24
28
const {
25
29
ERR_LOADER_CHAIN_INCOMPLETE ,
30
+ ERR_INTERNAL_ASSERTION ,
26
31
ERR_INVALID_ARG_TYPE ,
27
32
ERR_INVALID_ARG_VALUE ,
28
33
ERR_INVALID_RETURN_PROPERTY_VALUE ,
@@ -89,6 +94,81 @@ const { getOptionValue } = require('internal/options');
89
94
90
95
let emittedSpecifierResolutionWarning = false ;
91
96
97
+ /**
98
+ * A utility function to iterate through a hook chain, track advancement in the
99
+ * chain, and generate and supply the `next<HookName>` argument to the custom
100
+ * hook.
101
+ * @param {KeyedHook[] } chain The whole hook chain.
102
+ * @param {object } meta Properties that change as the current hook advances
103
+ * along the chain.
104
+ * @param {boolean } meta.chainFinished Whether the end of the chain has been
105
+ * reached AND invoked.
106
+ * @param {string } meta.hookErrIdentifier A user-facing identifier to help
107
+ * pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
108
+ * @param {number } meta.hookIndex A non-negative integer tracking the current
109
+ * position in the hook chain.
110
+ * @param {string } meta.hookName The kind of hook the chain is (ex 'resolve')
111
+ * @param {boolean } meta.shortCircuited Whether a hook signaled a short-circuit.
112
+ * @param {(hookErrIdentifier, hookArgs) => void } validate A wrapper function
113
+ * containing all validation of a custom loader hook's intermediary output. Any
114
+ * validation within MUST throw.
115
+ * @returns {function next<HookName>(...hookArgs) } The next hook in the chain.
116
+ */
117
+ function nextHookFactory ( chain , meta , validate ) {
118
+ // First, prepare the current
119
+ const { hookName } = meta ;
120
+ const {
121
+ fn : hook ,
122
+ url : hookFilePath ,
123
+ } = chain [ meta . hookIndex ] ;
124
+
125
+ // ex 'nextResolve'
126
+ const nextHookName = `next${
127
+ StringPrototypeToUpperCase ( hookName [ 0 ] ) +
128
+ StringPrototypeSlice ( hookName , 1 )
129
+ } `;
130
+
131
+ // When hookIndex is 0, it's reached the default, which does not call next()
132
+ // so feed it a noop that blows up if called, so the problem is obvious.
133
+ const generatedHookIndex = meta . hookIndex ;
134
+ let nextNextHook ;
135
+ if ( meta . hookIndex > 0 ) {
136
+ // Now, prepare the next: decrement the pointer so the next call to the
137
+ // factory generates the next link in the chain.
138
+ meta . hookIndex -- ;
139
+
140
+ nextNextHook = nextHookFactory ( chain , meta , validate ) ;
141
+ } else {
142
+ // eslint-disable-next-line func-name-matching
143
+ nextNextHook = function chainAdvancedTooFar ( ) {
144
+ throw new ERR_INTERNAL_ASSERTION (
145
+ `ESM custom loader '${ hookName } ' advanced beyond the end of the chain.`
146
+ ) ;
147
+ } ;
148
+ }
149
+
150
+ return ObjectDefineProperty (
151
+ async ( ...args ) => {
152
+ // Update only when hook is invoked to avoid fingering the wrong filePath
153
+ meta . hookErrIdentifier = `${ hookFilePath } '${ hookName } '` ;
154
+
155
+ validate ( `${ meta . hookErrIdentifier } hook's ${ nextHookName } ()` , args ) ;
156
+
157
+ // Set when next<HookName> is actually called, not just generated.
158
+ if ( generatedHookIndex === 0 ) { meta . chainFinished = true ; }
159
+
160
+ ArrayPrototypePush ( args , nextNextHook ) ;
161
+ const output = await ReflectApply ( hook , undefined , args ) ;
162
+
163
+ if ( output ?. shortCircuit === true ) { meta . shortCircuited = true ; }
164
+ return output ;
165
+
166
+ } ,
167
+ 'name' ,
168
+ { __proto__ : null , value : nextHookName } ,
169
+ ) ;
170
+ }
171
+
92
172
/**
93
173
* An ESMLoader instance is used as the main entry point for loading ES modules.
94
174
* Currently, this is a singleton -- there is only one used for loading
@@ -471,32 +551,21 @@ class ESMLoader {
471
551
* @returns {{ format: ModuleFormat, source: ModuleSource } }
472
552
*/
473
553
async load ( url , context = { } ) {
474
- const loaders = this . #loaders;
475
- let hookIndex = loaders . length - 1 ;
476
- let {
477
- fn : loader ,
478
- url : loaderFilePath ,
479
- } = loaders [ hookIndex ] ;
480
- let chainFinished = hookIndex === 0 ;
481
- let shortCircuited = false ;
482
-
483
- const nextLoad = async ( nextUrl , ctx = context ) => {
484
- -- hookIndex ; // `nextLoad` has been called, so decrement our pointer.
485
-
486
- ( {
487
- fn : loader ,
488
- url : loaderFilePath ,
489
- } = loaders [ hookIndex ] ) ;
490
-
491
- if ( hookIndex === 0 ) { chainFinished = true ; }
492
-
493
- const hookErrIdentifier = `${ loaderFilePath } "load"` ;
554
+ const chain = this . #loaders;
555
+ const meta = {
556
+ chainFinished : null ,
557
+ hookErrIdentifier : '' ,
558
+ hookIndex : chain . length - 1 ,
559
+ hookName : 'load' ,
560
+ shortCircuited : false ,
561
+ } ;
494
562
563
+ const validate = ( hookErrIdentifier , { 0 : nextUrl , 1 : ctx } ) => {
495
564
if ( typeof nextUrl !== 'string' ) {
496
565
// non-strings can be coerced to a url string
497
566
// validateString() throws a less-specific error
498
567
throw new ERR_INVALID_ARG_TYPE (
499
- `${ hookErrIdentifier } nextLoad( url) ` ,
568
+ `${ hookErrIdentifier } url` ,
500
569
'a url string' ,
501
570
nextUrl ,
502
571
) ;
@@ -508,29 +577,20 @@ class ESMLoader {
508
577
new URL ( nextUrl ) ;
509
578
} catch {
510
579
throw new ERR_INVALID_ARG_VALUE (
511
- `${ hookErrIdentifier } nextLoad( url) ` ,
580
+ `${ hookErrIdentifier } url` ,
512
581
nextUrl ,
513
582
'should be a url string' ,
514
583
) ;
515
584
}
516
585
}
517
586
518
- validateObject ( ctx , `${ hookErrIdentifier } nextLoad(, context)` ) ;
519
-
520
- const output = await loader ( nextUrl , ctx , nextLoad ) ;
521
-
522
- if ( output ?. shortCircuit === true ) { shortCircuited = true ; }
523
-
524
- return output ;
587
+ validateObject ( ctx , `${ hookErrIdentifier } context` ) ;
525
588
} ;
526
589
527
- const loaded = await loader (
528
- url ,
529
- context ,
530
- nextLoad ,
531
- ) ;
590
+ const nextLoad = nextHookFactory ( chain , meta , validate ) ;
532
591
533
- const hookErrIdentifier = `${ loaderFilePath } load` ;
592
+ const loaded = await nextLoad ( url , context ) ;
593
+ const { hookErrIdentifier } = meta ; // Retrieve the value after all settled
534
594
535
595
if ( typeof loaded !== 'object' ) { // [2]
536
596
throw new ERR_INVALID_RETURN_VALUE (
@@ -540,10 +600,10 @@ class ESMLoader {
540
600
) ;
541
601
}
542
602
543
- if ( loaded ?. shortCircuit === true ) { shortCircuited = true ; }
603
+ if ( loaded ?. shortCircuit === true ) { meta . shortCircuited = true ; }
544
604
545
- if ( ! chainFinished && ! shortCircuited ) {
546
- throw new ERR_LOADER_CHAIN_INCOMPLETE ( 'load' , loaderFilePath ) ;
605
+ if ( ! meta . chainFinished && ! meta . shortCircuited ) {
606
+ throw new ERR_LOADER_CHAIN_INCOMPLETE ( hookErrIdentifier ) ;
547
607
}
548
608
549
609
const {
@@ -736,55 +796,34 @@ class ESMLoader {
736
796
parentURL ,
737
797
) ;
738
798
}
739
- const resolvers = this . #resolvers;
740
-
741
- let hookIndex = resolvers . length - 1 ;
742
- let {
743
- fn : resolver ,
744
- url : resolverFilePath ,
745
- } = resolvers [ hookIndex ] ;
746
- let chainFinished = hookIndex === 0 ;
747
- let shortCircuited = false ;
799
+ const chain = this . #resolvers;
800
+ const meta = {
801
+ chainFinished : null ,
802
+ hookErrIdentifier : '' ,
803
+ hookIndex : chain . length - 1 ,
804
+ hookName : 'resolve' ,
805
+ shortCircuited : false ,
806
+ } ;
748
807
749
808
const context = {
750
809
conditions : DEFAULT_CONDITIONS ,
751
810
importAssertions,
752
811
parentURL,
753
812
} ;
754
-
755
- const nextResolve = async ( suppliedSpecifier , ctx = context ) => {
756
- -- hookIndex ; // `nextResolve` has been called, so decrement our pointer.
757
-
758
- ( {
759
- fn : resolver ,
760
- url : resolverFilePath ,
761
- } = resolvers [ hookIndex ] ) ;
762
-
763
- if ( hookIndex === 0 ) { chainFinished = true ; }
764
-
765
- const hookErrIdentifier = `${ resolverFilePath } "resolve"` ;
813
+ const validate = ( hookErrIdentifier , { 0 : suppliedSpecifier , 1 : ctx } ) => {
766
814
767
815
validateString (
768
816
suppliedSpecifier ,
769
- `${ hookErrIdentifier } nextResolve( specifier) ` ,
817
+ `${ hookErrIdentifier } specifier` ,
770
818
) ; // non-strings can be coerced to a url string
771
819
772
- validateObject ( ctx , `${ hookErrIdentifier } nextResolve(, context)` ) ;
773
-
774
- const output = await resolver ( suppliedSpecifier , ctx , nextResolve ) ;
775
-
776
- if ( output ?. shortCircuit === true ) { shortCircuited = true ; }
777
-
778
- return output ;
820
+ validateObject ( ctx , `${ hookErrIdentifier } context` ) ;
779
821
} ;
780
822
781
- const resolution = await resolver (
782
- originalSpecifier ,
783
- context ,
784
- nextResolve ,
785
- ) ;
823
+ const nextResolve = nextHookFactory ( chain , meta , validate ) ;
786
824
787
- const hookErrIdentifier = `${ resolverFilePath } resolve` ;
825
+ const resolution = await nextResolve ( originalSpecifier , context ) ;
826
+ const { hookErrIdentifier } = meta ; // Retrieve the value after all settled
788
827
789
828
if ( typeof resolution !== 'object' ) { // [2]
790
829
throw new ERR_INVALID_RETURN_VALUE (
@@ -794,10 +833,10 @@ class ESMLoader {
794
833
) ;
795
834
}
796
835
797
- if ( resolution ?. shortCircuit === true ) { shortCircuited = true ; }
836
+ if ( resolution ?. shortCircuit === true ) { meta . shortCircuited = true ; }
798
837
799
- if ( ! chainFinished && ! shortCircuited ) {
800
- throw new ERR_LOADER_CHAIN_INCOMPLETE ( 'resolve' , resolverFilePath ) ;
838
+ if ( ! meta . chainFinished && ! meta . shortCircuited ) {
839
+ throw new ERR_LOADER_CHAIN_INCOMPLETE ( hookErrIdentifier ) ;
801
840
}
802
841
803
842
const {
0 commit comments