@@ -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 {
@@ -735,55 +795,34 @@ class ESMLoader {
735
795
parentURL ,
736
796
) ;
737
797
}
738
- const resolvers = this . #resolvers;
739
-
740
- let hookIndex = resolvers . length - 1 ;
741
- let {
742
- fn : resolver ,
743
- url : resolverFilePath ,
744
- } = resolvers [ hookIndex ] ;
745
- let chainFinished = hookIndex === 0 ;
746
- let shortCircuited = false ;
798
+ const chain = this . #resolvers;
799
+ const meta = {
800
+ chainFinished : null ,
801
+ hookErrIdentifier : '' ,
802
+ hookIndex : chain . length - 1 ,
803
+ hookName : 'resolve' ,
804
+ shortCircuited : false ,
805
+ } ;
747
806
748
807
const context = {
749
808
conditions : DEFAULT_CONDITIONS ,
750
809
importAssertions,
751
810
parentURL,
752
811
} ;
753
-
754
- const nextResolve = async ( suppliedSpecifier , ctx = context ) => {
755
- -- hookIndex ; // `nextResolve` has been called, so decrement our pointer.
756
-
757
- ( {
758
- fn : resolver ,
759
- url : resolverFilePath ,
760
- } = resolvers [ hookIndex ] ) ;
761
-
762
- if ( hookIndex === 0 ) { chainFinished = true ; }
763
-
764
- const hookErrIdentifier = `${ resolverFilePath } "resolve"` ;
812
+ const validate = ( hookErrIdentifier , { 0 : suppliedSpecifier , 1 : ctx } ) => {
765
813
766
814
validateString (
767
815
suppliedSpecifier ,
768
- `${ hookErrIdentifier } nextResolve( specifier) ` ,
816
+ `${ hookErrIdentifier } specifier` ,
769
817
) ; // non-strings can be coerced to a url string
770
818
771
- validateObject ( ctx , `${ hookErrIdentifier } nextResolve(, context)` ) ;
772
-
773
- const output = await resolver ( suppliedSpecifier , ctx , nextResolve ) ;
774
-
775
- if ( output ?. shortCircuit === true ) { shortCircuited = true ; }
776
-
777
- return output ;
819
+ validateObject ( ctx , `${ hookErrIdentifier } context` ) ;
778
820
} ;
779
821
780
- const resolution = await resolver (
781
- originalSpecifier ,
782
- context ,
783
- nextResolve ,
784
- ) ;
822
+ const nextResolve = nextHookFactory ( chain , meta , validate ) ;
785
823
786
- const hookErrIdentifier = `${ resolverFilePath } resolve` ;
824
+ const resolution = await nextResolve ( originalSpecifier , context ) ;
825
+ const { hookErrIdentifier } = meta ; // Retrieve the value after all settled
787
826
788
827
if ( typeof resolution !== 'object' ) { // [2]
789
828
throw new ERR_INVALID_RETURN_VALUE (
@@ -793,10 +832,10 @@ class ESMLoader {
793
832
) ;
794
833
}
795
834
796
- if ( resolution ?. shortCircuit === true ) { shortCircuited = true ; }
835
+ if ( resolution ?. shortCircuit === true ) { meta . shortCircuited = true ; }
797
836
798
- if ( ! chainFinished && ! shortCircuited ) {
799
- throw new ERR_LOADER_CHAIN_INCOMPLETE ( 'resolve' , resolverFilePath ) ;
837
+ if ( ! meta . chainFinished && ! meta . shortCircuited ) {
838
+ throw new ERR_LOADER_CHAIN_INCOMPLETE ( hookErrIdentifier ) ;
800
839
}
801
840
802
841
const {
0 commit comments