@@ -15,6 +15,8 @@ const {
1515 ObjectDefineProperty,
1616 ObjectSetPrototypeOf,
1717 PromiseAll,
18+ PromiseResolve,
19+ PromisePrototypeThen,
1820 ReflectApply,
1921 RegExpPrototypeExec,
2022 SafeArrayIterator,
@@ -109,12 +111,14 @@ let emittedSpecifierResolutionWarning = false;
109111 * position in the hook chain.
110112 * @param {string } meta.hookName The kind of hook the chain is (ex 'resolve')
111113 * @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
113115 * containing all validation of a custom loader hook's intermediary output. Any
114116 * validation within MUST throw.
117+ * @param {(hookErrIdentifier, hookArgs) => void } validators.validateArgs
118+ * @param {(hookErrIdentifier, output) => void } validators.validateOutput
115119 * @returns {function next<HookName>(...hookArgs) } The next hook in the chain.
116120 */
117- function nextHookFactory ( chain , meta , validate ) {
121+ function nextHookFactory ( chain , meta , { validateArgs , validateOutput } ) {
118122 // First, prepare the current
119123 const { hookName } = meta ;
120124 const {
@@ -137,7 +141,7 @@ function nextHookFactory(chain, meta, validate) {
137141 // factory generates the next link in the chain.
138142 meta . hookIndex -- ;
139143
140- nextNextHook = nextHookFactory ( chain , meta , validate ) ;
144+ nextNextHook = nextHookFactory ( chain , meta , { validateArgs , validateOutput } ) ;
141145 } else {
142146 // eslint-disable-next-line func-name-matching
143147 nextNextHook = function chainAdvancedTooFar ( ) {
@@ -148,21 +152,36 @@ function nextHookFactory(chain, meta, validate) {
148152 }
149153
150154 return ObjectDefineProperty (
151- async ( ...args ) => {
155+ ( ...args ) => {
152156 // Update only when hook is invoked to avoid fingering the wrong filePath
153157 meta . hookErrIdentifier = `${ hookFilePath } '${ hookName } '` ;
154158
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 } ()` ;
156162
157163 // Set when next<HookName> is actually called, not just generated.
158164 if ( generatedHookIndex === 0 ) { meta . chainFinished = true ; }
159165
160166 ArrayPrototypePush ( args , nextNextHook ) ;
161- const output = await ReflectApply ( hook , undefined , args ) ;
167+ const output = ReflectApply ( hook , undefined , args ) ;
168+
169+ validateOutput ( outputErrIdentifier , output ) ;
162170
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+ }
165183
184+ return checkShortCircuited ( output ) ;
166185 } ,
167186 'name' ,
168187 { __proto__ : null , value : nextHookName } ,
@@ -421,8 +440,11 @@ class ESMLoader {
421440 ) ;
422441 }
423442
424- const { format, url } =
425- await this . resolve ( specifier , parentURL , importAssertionsForResolve ) ;
443+ const { format, url } = this . resolve (
444+ specifier ,
445+ parentURL ,
446+ importAssertionsForResolve ,
447+ ) ;
426448
427449 let job = this . moduleMap . get ( url , importAssertions . type ) ;
428450
@@ -557,10 +579,11 @@ class ESMLoader {
557579 hookErrIdentifier : '' ,
558580 hookIndex : chain . length - 1 ,
559581 hookName : 'load' ,
582+ isChainAsync : true ,
560583 shortCircuited : false ,
561584 } ;
562585
563- const validate = ( hookErrIdentifier , { 0 : nextUrl , 1 : ctx } ) => {
586+ const validateArgs = ( hookErrIdentifier , { 0 : nextUrl , 1 : ctx } ) => {
564587 if ( typeof nextUrl !== 'string' ) {
565588 // non-strings can be coerced to a url string
566589 // validateString() throws a less-specific error
@@ -586,19 +609,22 @@ class ESMLoader {
586609
587610 validateObject ( ctx , `${ hookErrIdentifier } context` ) ;
588611 } ;
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+ } ;
589621
590- const nextLoad = nextHookFactory ( chain , meta , validate ) ;
622+ const nextLoad = nextHookFactory ( chain , meta , { validateArgs , validateOutput } ) ;
591623
592624 const loaded = await nextLoad ( url , context ) ;
593625 const { hookErrIdentifier } = meta ; // Retrieve the value after all settled
594626
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 ) ;
602628
603629 if ( loaded ?. shortCircuit === true ) { meta . shortCircuited = true ; }
604630
@@ -778,7 +804,7 @@ class ESMLoader {
778804 * statement or expression.
779805 * @returns {{ format: string, url: URL['href'] } }
780806 */
781- async resolve (
807+ resolve (
782808 originalSpecifier ,
783809 parentURL ,
784810 importAssertions = ObjectCreate ( null )
@@ -802,36 +828,43 @@ class ESMLoader {
802828 hookErrIdentifier : '' ,
803829 hookIndex : chain . length - 1 ,
804830 hookName : 'resolve' ,
831+ isChainAsync : false ,
805832 shortCircuited : false ,
806833 } ;
807-
808834 const context = {
809835 conditions : DEFAULT_CONDITIONS ,
810836 importAssertions,
811837 parentURL,
812838 } ;
813- const validate = ( hookErrIdentifier , { 0 : suppliedSpecifier , 1 : ctx } ) => {
814839
840+ const validateArgs = ( hookErrIdentifier , { 0 : suppliedSpecifier , 1 : ctx } ) => {
815841 validateString (
816842 suppliedSpecifier ,
817843 `${ hookErrIdentifier } specifier` ,
818844 ) ; // non-strings can be coerced to a url string
819845
820846 validateObject ( ctx , `${ hookErrIdentifier } context` ) ;
821847 } ;
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+ } ;
822861
823- const nextResolve = nextHookFactory ( chain , meta , validate ) ;
862+ const nextResolve = nextHookFactory ( chain , meta , { validateArgs , validateOutput } ) ;
824863
825- const resolution = await nextResolve ( originalSpecifier , context ) ;
864+ const resolution = nextResolve ( originalSpecifier , context ) ;
826865 const { hookErrIdentifier } = meta ; // Retrieve the value after all settled
827866
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 ) ;
835868
836869 if ( resolution ?. shortCircuit === true ) { meta . shortCircuited = true ; }
837870
0 commit comments