diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 6897c0e9bbfae2..a10851837d9512 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -15,6 +15,8 @@ const { ObjectDefineProperty, ObjectSetPrototypeOf, PromiseAll, + PromiseResolve, + PromisePrototypeThen, ReflectApply, RegExpPrototypeExec, SafeArrayIterator, @@ -114,7 +116,7 @@ let emittedSpecifierResolutionWarning = false; * validation within MUST throw. * @returns {function next(...hookArgs)} The next hook in the chain. */ -function nextHookFactory(chain, meta, validate) { +function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { // First, prepare the current const { hookName } = meta; const { @@ -137,7 +139,7 @@ function nextHookFactory(chain, meta, validate) { // factory generates the next link in the chain. meta.hookIndex--; - nextNextHook = nextHookFactory(chain, meta, validate); + nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput }); } else { // eslint-disable-next-line func-name-matching nextNextHook = function chainAdvancedTooFar() { @@ -152,34 +154,25 @@ function nextHookFactory(chain, meta, validate) { // Update only when hook is invoked to avoid fingering the wrong filePath meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`; - validate(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, args); + validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, args); + + const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`; // Set when next is actually called, not just generated. if (generatedHookIndex === 0) { meta.chainFinished = true; } ArrayPrototypePush(args, nextNextHook); - const output = ReflectApply(hook, undefined, args); + let output = ReflectApply(hook, undefined, args); + + validateOutput(output, outputErrIdentifier); function checkShortCircuited(output) { if (output?.shortCircuit === true) { meta.shortCircuited = true; } } - const then = output?.then; - if (typeof then === 'function') { - if (!meta.isChainAsync) { - throw ERR_INVALID_RETURN_VALUE( - 'an object', - // MUST use generatedHookIndex because the chain has already advanced, - // causing meta.hookIndex to advance - `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`, - output, - ); - } - - ReflectApply(then, output, [ - checkShortCircuited, - // TODO: handle error case - ]); + if (meta.isChainAsync) { + output = PromiseResolve(output); + PromisePrototypeThen(output, checkShortCircuited); } else { checkShortCircuited(output); } @@ -586,7 +579,7 @@ class ESMLoader { shortCircuited: false, }; - const validate = (hookErrIdentifier, { 0: nextUrl, 1: ctx }) => { + const validateArgs = (hookErrIdentifier, { 0: nextUrl, 1: ctx }) => { if (typeof nextUrl !== 'string') { // non-strings can be coerced to a url string // validateString() throws a less-specific error @@ -612,19 +605,22 @@ class ESMLoader { validateObject(ctx, `${hookErrIdentifier} context`); }; + const validateOutput = (output, hookErrIdentifier) => { + if (typeof output !== 'object') { // [2] + throw new ERR_INVALID_RETURN_VALUE( + 'an object', + hookErrIdentifier, + output, + ); + } + }; - const nextLoad = nextHookFactory(chain, meta, validate); + const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput }); const loaded = await nextLoad(url, context); const { hookErrIdentifier } = meta; // Retrieve the value after all settled - if (typeof loaded !== 'object') { // [2] - throw new ERR_INVALID_RETURN_VALUE( - 'an object', - hookErrIdentifier, - loaded, - ); - } + validateOutput(loaded, hookErrIdentifier); if (loaded?.shortCircuit === true) { meta.shortCircuited = true; } @@ -837,7 +833,7 @@ class ESMLoader { parentURL, }; - const validate = (hookErrIdentifier, output) => { + const validateArgs = (hookErrIdentifier, output) => { const { 0: suppliedSpecifier, 1: ctx } = output; validateString( @@ -847,22 +843,25 @@ class ESMLoader { validateObject(ctx, `${hookErrIdentifier} context`); }; + const validateOutput = (output, hookErrIdentifier) => { + if ( + typeof output !== 'object' || // [2] + typeof output.then === 'function' + ) { + throw new ERR_INVALID_RETURN_VALUE( + 'an object', + hookErrIdentifier, + output, + ); + } + }; - const nextResolve = nextHookFactory(chain, meta, validate); + const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput }); const resolution = nextResolve(originalSpecifier, context); const { hookErrIdentifier } = meta; // Retrieve the value after all settled - if ( - typeof resolution !== 'object' || // [2] - typeof resolution?.then === 'function' - ) { - throw new ERR_INVALID_RETURN_VALUE( - 'an object', - hookErrIdentifier, - resolution, - ); - } + validateOutput(resolution, hookErrIdentifier); if (resolution?.shortCircuit === true) { meta.shortCircuited = true; }