Skip to content

Commit 1cda175

Browse files
JakobJingleheimertargos
authored andcommitted
esm: restore next<HookName>'s context as optional arg
PR-URL: #43553 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 2d31695 commit 1cda175

File tree

6 files changed

+107
-38
lines changed

6 files changed

+107
-38
lines changed

doc/api/esm.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ export async function resolve(specifier, context, nextResolve) {
806806

807807
// Defer to the next hook in the chain, which would be the
808808
// Node.js default resolve if this is the last user-specified loader.
809-
return nextResolve(specifier, context);
809+
return nextResolve(specifier);
810810
}
811811
```
812812
@@ -901,7 +901,7 @@ export async function load(url, context, nextLoad) {
901901
}
902902

903903
// Defer to the next hook in the chain.
904-
return nextLoad(url, context);
904+
return nextLoad(url);
905905
}
906906
```
907907
@@ -1017,7 +1017,7 @@ export function resolve(specifier, context, nextResolve) {
10171017
}
10181018
10191019
// Let Node.js handle all other specifiers.
1020-
return nextResolve(specifier, context);
1020+
return nextResolve(specifier);
10211021
}
10221022
10231023
export function load(url, context, nextLoad) {
@@ -1040,7 +1040,7 @@ export function load(url, context, nextLoad) {
10401040
}
10411041
10421042
// Let Node.js handle all other URLs.
1043-
return nextLoad(url, context);
1043+
return nextLoad(url);
10441044
}
10451045
```
10461046

@@ -1093,7 +1093,7 @@ export async function resolve(specifier, context, nextResolve) {
10931093
}
10941094
10951095
// Let Node.js handle all other specifiers.
1096-
return nextResolve(specifier, context);
1096+
return nextResolve(specifier);
10971097
}
10981098
10991099
export async function load(url, context, nextLoad) {
@@ -1134,7 +1134,7 @@ export async function load(url, context, nextLoad) {
11341134
}
11351135
11361136
// Let Node.js handle all other URLs.
1137-
return nextLoad(url, context);
1137+
return nextLoad(url);
11381138
}
11391139
11401140
async function getPackageType(url) {

lib/internal/modules/esm/loader.js

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ let emittedSpecifierResolutionWarning = false;
114114
* validation within MUST throw.
115115
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
116116
*/
117-
function nextHookFactory(chain, meta, validate) {
117+
function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
118118
// First, prepare the current
119119
const { hookName } = meta;
120120
const {
@@ -137,7 +137,7 @@ function nextHookFactory(chain, meta, validate) {
137137
// factory generates the next link in the chain.
138138
meta.hookIndex--;
139139

140-
nextNextHook = nextHookFactory(chain, meta, validate);
140+
nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
141141
} else {
142142
// eslint-disable-next-line func-name-matching
143143
nextNextHook = function chainAdvancedTooFar() {
@@ -152,14 +152,28 @@ function nextHookFactory(chain, meta, validate) {
152152
// Update only when hook is invoked to avoid fingering the wrong filePath
153153
meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`;
154154

155-
validate(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, args);
155+
validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, args);
156+
157+
const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`;
156158

157159
// Set when next<HookName> is actually called, not just generated.
158160
if (generatedHookIndex === 0) { meta.chainFinished = true; }
159161

162+
// `context` is an optional argument that only needs to be passed when changed
163+
switch (args.length) {
164+
case 1: // It was omitted, so supply the cached value
165+
ArrayPrototypePush(args, meta.context);
166+
break;
167+
case 2: // Overrides were supplied, so update cached value
168+
ObjectAssign(meta.context, args[1]);
169+
break;
170+
}
171+
160172
ArrayPrototypePush(args, nextNextHook);
161173
const output = await ReflectApply(hook, undefined, args);
162174

175+
validateOutput(outputErrIdentifier, output);
176+
163177
if (output?.shortCircuit === true) { meta.shortCircuited = true; }
164178
return output;
165179

@@ -554,13 +568,14 @@ class ESMLoader {
554568
const chain = this.#loaders;
555569
const meta = {
556570
chainFinished: null,
571+
context,
557572
hookErrIdentifier: '',
558573
hookIndex: chain.length - 1,
559574
hookName: 'load',
560575
shortCircuited: false,
561576
};
562577

563-
const validate = (hookErrIdentifier, { 0: nextUrl, 1: ctx }) => {
578+
const validateArgs = (hookErrIdentifier, { 0: nextUrl, 1: ctx }) => {
564579
if (typeof nextUrl !== 'string') {
565580
// non-strings can be coerced to a url string
566581
// validateString() throws a less-specific error
@@ -584,21 +599,24 @@ class ESMLoader {
584599
}
585600
}
586601

587-
validateObject(ctx, `${hookErrIdentifier} context`);
602+
if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
603+
};
604+
const validateOutput = (hookErrIdentifier, output) => {
605+
if (typeof output !== 'object' || output === null) { // [2]
606+
throw new ERR_INVALID_RETURN_VALUE(
607+
'an object',
608+
hookErrIdentifier,
609+
output,
610+
);
611+
}
588612
};
589613

590-
const nextLoad = nextHookFactory(chain, meta, validate);
614+
const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput });
591615

592616
const loaded = await nextLoad(url, context);
593617
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
594618

595-
if (typeof loaded !== 'object') { // [2]
596-
throw new ERR_INVALID_RETURN_VALUE(
597-
'an object',
598-
hookErrIdentifier,
599-
loaded,
600-
);
601-
}
619+
validateOutput(hookErrIdentifier, loaded);
602620

603621
if (loaded?.shortCircuit === true) { meta.shortCircuited = true; }
604622

@@ -796,41 +814,44 @@ class ESMLoader {
796814
);
797815
}
798816
const chain = this.#resolvers;
817+
const context = {
818+
conditions: DEFAULT_CONDITIONS,
819+
importAssertions,
820+
parentURL,
821+
};
799822
const meta = {
800823
chainFinished: null,
824+
context,
801825
hookErrIdentifier: '',
802826
hookIndex: chain.length - 1,
803827
hookName: 'resolve',
804828
shortCircuited: false,
805829
};
806830

807-
const context = {
808-
conditions: DEFAULT_CONDITIONS,
809-
importAssertions,
810-
parentURL,
811-
};
812-
const validate = (hookErrIdentifier, { 0: suppliedSpecifier, 1: ctx }) => {
813-
831+
const validateArgs = (hookErrIdentifier, { 0: suppliedSpecifier, 1: ctx }) => {
814832
validateString(
815833
suppliedSpecifier,
816834
`${hookErrIdentifier} specifier`,
817835
); // non-strings can be coerced to a url string
818836

819-
validateObject(ctx, `${hookErrIdentifier} context`);
837+
if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
838+
};
839+
const validateOutput = (hookErrIdentifier, output) => {
840+
if (typeof output !== 'object' || output === null) { // [2]
841+
throw new ERR_INVALID_RETURN_VALUE(
842+
'an object',
843+
hookErrIdentifier,
844+
output,
845+
);
846+
}
820847
};
821848

822-
const nextResolve = nextHookFactory(chain, meta, validate);
849+
const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput });
823850

824851
const resolution = await nextResolve(originalSpecifier, context);
825852
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
826853

827-
if (typeof resolution !== 'object') { // [2]
828-
throw new ERR_INVALID_RETURN_VALUE(
829-
'an object',
830-
hookErrIdentifier,
831-
resolution,
832-
);
833-
}
854+
validateOutput(hookErrIdentifier, resolution);
834855

835856
if (resolution?.shortCircuit === true) { meta.shortCircuited = true; }
836857

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ const commonArgs = [
229229
assert.strictEqual(status, 0);
230230
}
231231

232-
{ // Verify chain does break and throws appropriately
232+
{ // Verify resolve chain does break and throws appropriately
233233
const { status, stderr, stdout } = spawnSync(
234234
process.execPath,
235235
[
@@ -273,7 +273,7 @@ const commonArgs = [
273273
assert.strictEqual(status, 1);
274274
}
275275

276-
{ // Verify chain does break and throws appropriately
276+
{ // Verify load chain does break and throws appropriately
277277
const { status, stderr, stdout } = spawnSync(
278278
process.execPath,
279279
[
@@ -314,6 +314,27 @@ const commonArgs = [
314314
assert.match(stderr, /'resolve' hook's nextResolve\(\) specifier/);
315315
}
316316

317+
{ // Verify error thrown when resolve hook is invalid
318+
const { status, stderr } = spawnSync(
319+
process.execPath,
320+
[
321+
'--loader',
322+
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
323+
'--loader',
324+
fixtures.fileURL('es-module-loaders', 'loader-resolve-null-return.mjs'),
325+
...commonArgs,
326+
],
327+
{ encoding: 'utf8' },
328+
);
329+
330+
assert.strictEqual(status, 1);
331+
assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
332+
assert.match(stderr, /loader-resolve-null-return\.mjs/);
333+
assert.match(stderr, /'resolve' hook's nextResolve\(\)/);
334+
assert.match(stderr, /an object/);
335+
assert.match(stderr, /got null/);
336+
}
337+
317338
{ // Verify error thrown when invalid `context` argument passed to `nextResolve`
318339
const { status, stderr } = spawnSync(
319340
process.execPath,
@@ -333,6 +354,27 @@ const commonArgs = [
333354
assert.strictEqual(status, 1);
334355
}
335356

357+
{ // Verify error thrown when load hook is invalid
358+
const { status, stderr } = spawnSync(
359+
process.execPath,
360+
[
361+
'--loader',
362+
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
363+
'--loader',
364+
fixtures.fileURL('es-module-loaders', 'loader-load-null-return.mjs'),
365+
...commonArgs,
366+
],
367+
{ encoding: 'utf8' },
368+
);
369+
370+
assert.strictEqual(status, 1);
371+
assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
372+
assert.match(stderr, /loader-load-null-return\.mjs/);
373+
assert.match(stderr, /'load' hook's nextLoad\(\)/);
374+
assert.match(stderr, /an object/);
375+
assert.match(stderr, /got null/);
376+
}
377+
336378
{ // Verify error thrown when invalid `url` argument passed to `nextLoad`
337379
const { status, stderr } = spawnSync(
338380
process.execPath,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function load(specifier, context, next) {
2+
return null;
3+
}

test/fixtures/es-module-loaders/loader-resolve-42.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ export async function resolve(specifier, context, next) {
22
console.log('resolve 42'); // This log is deliberate
33
console.log('next<HookName>:', next.name); // This log is deliberate
44

5-
return next('file:///42.mjs', context);
5+
return next('file:///42.mjs');
66
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function resolve(specifier, context, next) {
2+
return null;
3+
}

0 commit comments

Comments
 (0)