@@ -15,6 +15,8 @@ const {
15
15
ObjectDefineProperty,
16
16
ObjectSetPrototypeOf,
17
17
PromiseAll,
18
+ PromiseResolve,
19
+ PromisePrototypeThen,
18
20
ReflectApply,
19
21
RegExpPrototypeExec,
20
22
SafeArrayIterator,
@@ -109,12 +111,14 @@ let emittedSpecifierResolutionWarning = false;
109
111
* position in the hook chain.
110
112
* @param {string } meta.hookName The kind of hook the chain is (ex 'resolve')
111
113
* @param {boolean } meta.shortCircuited Whether a hook signaled a short-circuit.
112
- * @param {(hookErrIdentifier, hookArgs) => void } validate A wrapper function
114
+ * @param {object } validators A wrapper function
113
115
* containing all validation of a custom loader hook's intermediary output. Any
114
116
* validation within MUST throw.
117
+ * @param {(hookErrIdentifier, hookArgs) => void } validators.validateArgs
118
+ * @param {(hookErrIdentifier, output) => void } validators.validateOutput
115
119
* @returns {function next<HookName>(...hookArgs) } The next hook in the chain.
116
120
*/
117
- function nextHookFactory ( chain , meta , validate ) {
121
+ function nextHookFactory ( chain , meta , { validateArgs , validateOutput } ) {
118
122
// First, prepare the current
119
123
const { hookName } = meta ;
120
124
const {
@@ -137,7 +141,7 @@ function nextHookFactory(chain, meta, validate) {
137
141
// factory generates the next link in the chain.
138
142
meta . hookIndex -- ;
139
143
140
- nextNextHook = nextHookFactory ( chain , meta , validate ) ;
144
+ nextNextHook = nextHookFactory ( chain , meta , { validateArgs , validateOutput } ) ;
141
145
} else {
142
146
// eslint-disable-next-line func-name-matching
143
147
nextNextHook = function chainAdvancedTooFar ( ) {
@@ -148,21 +152,36 @@ function nextHookFactory(chain, meta, validate) {
148
152
}
149
153
150
154
return ObjectDefineProperty (
151
- async ( ...args ) => {
155
+ ( ...args ) => {
152
156
// Update only when hook is invoked to avoid fingering the wrong filePath
153
157
meta . hookErrIdentifier = `${ hookFilePath } '${ hookName } '` ;
154
158
155
- validate ( `${ meta . hookErrIdentifier } hook's ${ nextHookName } ()` , args ) ;
159
+ validateArgs ( `${ meta . hookErrIdentifier } hook's ${ nextHookName } ()` , args ) ;
160
+
161
+ const outputErrIdentifier = `${ chain [ generatedHookIndex ] . url } '${ hookName } ' hook's ${ nextHookName } ()` ;
156
162
157
163
// Set when next<HookName> is actually called, not just generated.
158
164
if ( generatedHookIndex === 0 ) { meta . chainFinished = true ; }
159
165
160
166
ArrayPrototypePush ( args , nextNextHook ) ;
161
- const output = await ReflectApply ( hook , undefined , args ) ;
167
+ const output = ReflectApply ( hook , undefined , args ) ;
168
+
169
+ validateOutput ( outputErrIdentifier , output ) ;
162
170
163
- if ( output ?. shortCircuit === true ) { meta . shortCircuited = true ; }
164
- return output ;
171
+ function checkShortCircuited ( output ) {
172
+ if ( output ?. shortCircuit === true ) { meta . shortCircuited = true ; }
173
+
174
+ return output ;
175
+ }
176
+
177
+ if ( meta . isChainAsync ) {
178
+ return PromisePrototypeThen (
179
+ PromiseResolve ( output ) ,
180
+ checkShortCircuited ,
181
+ ) ;
182
+ }
165
183
184
+ return checkShortCircuited ( output ) ;
166
185
} ,
167
186
'name' ,
168
187
{ __proto__ : null , value : nextHookName } ,
@@ -421,8 +440,11 @@ class ESMLoader {
421
440
) ;
422
441
}
423
442
424
- const { format, url } =
425
- await this . resolve ( specifier , parentURL , importAssertionsForResolve ) ;
443
+ const { format, url } = this . resolve (
444
+ specifier ,
445
+ parentURL ,
446
+ importAssertionsForResolve ,
447
+ ) ;
426
448
427
449
let job = this . moduleMap . get ( url , importAssertions . type ) ;
428
450
@@ -557,10 +579,11 @@ class ESMLoader {
557
579
hookErrIdentifier : '' ,
558
580
hookIndex : chain . length - 1 ,
559
581
hookName : 'load' ,
582
+ isChainAsync : true ,
560
583
shortCircuited : false ,
561
584
} ;
562
585
563
- const validate = ( hookErrIdentifier , { 0 : nextUrl , 1 : ctx } ) => {
586
+ const validateArgs = ( hookErrIdentifier , { 0 : nextUrl , 1 : ctx } ) => {
564
587
if ( typeof nextUrl !== 'string' ) {
565
588
// non-strings can be coerced to a url string
566
589
// validateString() throws a less-specific error
@@ -586,19 +609,22 @@ class ESMLoader {
586
609
587
610
validateObject ( ctx , `${ hookErrIdentifier } context` ) ;
588
611
} ;
612
+ const validateOutput = ( hookErrIdentifier , output ) => {
613
+ if ( typeof output !== 'object' ) { // [2]
614
+ throw new ERR_INVALID_RETURN_VALUE (
615
+ 'an object' ,
616
+ hookErrIdentifier ,
617
+ output ,
618
+ ) ;
619
+ }
620
+ } ;
589
621
590
- const nextLoad = nextHookFactory ( chain , meta , validate ) ;
622
+ const nextLoad = nextHookFactory ( chain , meta , { validateArgs , validateOutput } ) ;
591
623
592
624
const loaded = await nextLoad ( url , context ) ;
593
625
const { hookErrIdentifier } = meta ; // Retrieve the value after all settled
594
626
595
- if ( typeof loaded !== 'object' ) { // [2]
596
- throw new ERR_INVALID_RETURN_VALUE (
597
- 'an object' ,
598
- hookErrIdentifier ,
599
- loaded ,
600
- ) ;
601
- }
627
+ validateOutput ( hookErrIdentifier , loaded ) ;
602
628
603
629
if ( loaded ?. shortCircuit === true ) { meta . shortCircuited = true ; }
604
630
@@ -778,7 +804,7 @@ class ESMLoader {
778
804
* statement or expression.
779
805
* @returns {{ format: string, url: URL['href'] } }
780
806
*/
781
- async resolve (
807
+ resolve (
782
808
originalSpecifier ,
783
809
parentURL ,
784
810
importAssertions = ObjectCreate ( null )
@@ -802,36 +828,43 @@ class ESMLoader {
802
828
hookErrIdentifier : '' ,
803
829
hookIndex : chain . length - 1 ,
804
830
hookName : 'resolve' ,
831
+ isChainAsync : false ,
805
832
shortCircuited : false ,
806
833
} ;
807
-
808
834
const context = {
809
835
conditions : DEFAULT_CONDITIONS ,
810
836
importAssertions,
811
837
parentURL,
812
838
} ;
813
- const validate = ( hookErrIdentifier , { 0 : suppliedSpecifier , 1 : ctx } ) => {
814
839
840
+ const validateArgs = ( hookErrIdentifier , { 0 : suppliedSpecifier , 1 : ctx } ) => {
815
841
validateString (
816
842
suppliedSpecifier ,
817
843
`${ hookErrIdentifier } specifier` ,
818
844
) ; // non-strings can be coerced to a url string
819
845
820
846
validateObject ( ctx , `${ hookErrIdentifier } context` ) ;
821
847
} ;
848
+ const validateOutput = ( hookErrIdentifier , output ) => {
849
+ if (
850
+ typeof output !== 'object' || // [2]
851
+ output === null ||
852
+ ( output . url == null && typeof output . then === 'function' )
853
+ ) {
854
+ throw new ERR_INVALID_RETURN_VALUE (
855
+ 'an object' ,
856
+ hookErrIdentifier ,
857
+ output ,
858
+ ) ;
859
+ }
860
+ } ;
822
861
823
- const nextResolve = nextHookFactory ( chain , meta , validate ) ;
862
+ const nextResolve = nextHookFactory ( chain , meta , { validateArgs , validateOutput } ) ;
824
863
825
- const resolution = await nextResolve ( originalSpecifier , context ) ;
864
+ const resolution = nextResolve ( originalSpecifier , context ) ;
826
865
const { hookErrIdentifier } = meta ; // Retrieve the value after all settled
827
866
828
- if ( typeof resolution !== 'object' ) { // [2]
829
- throw new ERR_INVALID_RETURN_VALUE (
830
- 'an object' ,
831
- hookErrIdentifier ,
832
- resolution ,
833
- ) ;
834
- }
867
+ validateOutput ( hookErrIdentifier , resolution ) ;
835
868
836
869
if ( resolution ?. shortCircuit === true ) { meta . shortCircuited = true ; }
837
870
0 commit comments