Skip to content

Commit 8bf7965

Browse files
fixup: support detecting when a resolve hook returns a promise
1 parent ae8ef63 commit 8bf7965

File tree

3 files changed

+30
-15
lines changed

3 files changed

+30
-15
lines changed

lib/internal/modules/esm/loader.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,22 @@ function nextHookFactory(chain, meta, validate) {
164164
if (output?.shortCircuit === true) { meta.shortCircuited = true; }
165165
}
166166

167-
if (output instanceof Promise) { // eslint-disable-line node-core/prefer-primordials
168-
output?.then(checkShortCircuited);
167+
const then = output?.then;
168+
if (typeof then === 'function') {
169+
if (!meta.isChainAsync) {
170+
throw ERR_INVALID_RETURN_VALUE(
171+
'an object',
172+
// MUST use generatedHookIndex because the chain has already advanced,
173+
// causing meta.hookIndex to advance
174+
`${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`,
175+
output,
176+
);
177+
}
178+
179+
ReflectApply(then, output, [
180+
checkShortCircuited,
181+
// TODO: handle error case
182+
]);
169183
} else {
170184
checkShortCircuited(output);
171185
}
@@ -568,6 +582,7 @@ class ESMLoader {
568582
hookErrIdentifier: '',
569583
hookIndex: chain.length - 1,
570584
hookName: 'load',
585+
isChainAsync: true,
571586
shortCircuited: false,
572587
};
573588

@@ -813,6 +828,7 @@ class ESMLoader {
813828
hookErrIdentifier: '',
814829
hookIndex: chain.length - 1,
815830
hookName: 'resolve',
831+
isChainAsync: false,
816832
shortCircuited: false,
817833
};
818834
const context = {
@@ -822,14 +838,6 @@ class ESMLoader {
822838
};
823839

824840
const validate = (hookErrIdentifier, output) => {
825-
if (output instanceof Promise) { // eslint-disable-line node-core/prefer-primordials
826-
throw ERR_INVALID_RETURN_VALUE(
827-
'an object',
828-
hookErrIdentifier,
829-
output,
830-
);
831-
}
832-
833841
const { 0: suppliedSpecifier, 1: ctx } = output;
834842

835843
validateString(
@@ -847,7 +855,7 @@ class ESMLoader {
847855

848856
if (
849857
typeof resolution !== 'object' || // [2]
850-
resolution instanceof Promise // eslint-disable-line node-core/prefer-primordials
858+
typeof resolution?.then === 'function'
851859
) {
852860
throw new ERR_INVALID_RETURN_VALUE(
853861
'an object',

test/es-module/test-esm-loader-chaining.mjs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,17 @@ const commonArgs = [
166166
}
167167

168168
{ // Verify error thrown for an async resolve hook
169-
const { status, stderr, stdout } = spawnSync(
169+
const { status, stderr } = spawnSync(
170170
process.execPath,
171171
[
172+
'--loader',
173+
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
174+
'--loader',
175+
fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
172176
'--loader',
173177
fixtures.fileURL('es-module-loaders', 'loader-resolve-async-fn.mjs'),
178+
'--loader',
179+
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
174180
...commonArgs,
175181
],
176182
{ encoding: 'utf8' },
@@ -180,7 +186,8 @@ const commonArgs = [
180186
assert.match(stderr, /Promise/);
181187
assert.match(stderr, /loader-resolve-async-fn\.mjs/);
182188
assert.match(stderr, /'resolve'/);
183-
assert.strictEqual(stdout, '');
189+
// Cannot expect stdout to be empty because detecting whether a hook has
190+
// returned a promise requires the hook to be executed
184191
assert.strictEqual(status, 1);
185192
}
186193

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export async function resolve() {
2-
return 'whatever';
1+
export async function resolve(specifier, context, next) {
2+
return next(specifier, context);
33
}

0 commit comments

Comments
 (0)