esm: restore next<HookName>'s context as optional arg

PR-URL: https://github.com/nodejs/node/pull/43553
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Jacob Smith 2022-07-02 06:44:03 +02:00 committed by GitHub
parent 574ad6d89d
commit 1f6d005836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 107 additions and 38 deletions

View File

@ -815,7 +815,7 @@ export async function resolve(specifier, context, nextResolve) {
// Defer to the next hook in the chain, which would be the // Defer to the next hook in the chain, which would be the
// Node.js default resolve if this is the last user-specified loader. // Node.js default resolve if this is the last user-specified loader.
return nextResolve(specifier, context); return nextResolve(specifier);
} }
``` ```
@ -910,7 +910,7 @@ export async function load(url, context, nextLoad) {
} }
// Defer to the next hook in the chain. // Defer to the next hook in the chain.
return nextLoad(url, context); return nextLoad(url);
} }
``` ```
@ -1026,7 +1026,7 @@ export function resolve(specifier, context, nextResolve) {
} }
// Let Node.js handle all other specifiers. // Let Node.js handle all other specifiers.
return nextResolve(specifier, context); return nextResolve(specifier);
} }
export function load(url, context, nextLoad) { export function load(url, context, nextLoad) {
@ -1049,7 +1049,7 @@ export function load(url, context, nextLoad) {
} }
// Let Node.js handle all other URLs. // Let Node.js handle all other URLs.
return nextLoad(url, context); return nextLoad(url);
} }
``` ```
@ -1102,7 +1102,7 @@ export async function resolve(specifier, context, nextResolve) {
} }
// Let Node.js handle all other specifiers. // Let Node.js handle all other specifiers.
return nextResolve(specifier, context); return nextResolve(specifier);
} }
export async function load(url, context, nextLoad) { export async function load(url, context, nextLoad) {
@ -1143,7 +1143,7 @@ export async function load(url, context, nextLoad) {
} }
// Let Node.js handle all other URLs. // Let Node.js handle all other URLs.
return nextLoad(url, context); return nextLoad(url);
} }
async function getPackageType(url) { async function getPackageType(url) {

View File

@ -114,7 +114,7 @@ let emittedSpecifierResolutionWarning = false;
* validation within MUST throw. * validation within MUST throw.
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain. * @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
*/ */
function nextHookFactory(chain, meta, validate) { function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
// First, prepare the current // First, prepare the current
const { hookName } = meta; const { hookName } = meta;
const { const {
@ -137,7 +137,7 @@ function nextHookFactory(chain, meta, validate) {
// factory generates the next link in the chain. // factory generates the next link in the chain.
meta.hookIndex--; meta.hookIndex--;
nextNextHook = nextHookFactory(chain, meta, validate); nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
} else { } else {
// eslint-disable-next-line func-name-matching // eslint-disable-next-line func-name-matching
nextNextHook = function chainAdvancedTooFar() { nextNextHook = function chainAdvancedTooFar() {
@ -152,14 +152,28 @@ function nextHookFactory(chain, meta, validate) {
// Update only when hook is invoked to avoid fingering the wrong filePath // Update only when hook is invoked to avoid fingering the wrong filePath
meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`; 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<HookName> is actually called, not just generated. // Set when next<HookName> is actually called, not just generated.
if (generatedHookIndex === 0) { meta.chainFinished = true; } if (generatedHookIndex === 0) { meta.chainFinished = true; }
// `context` is an optional argument that only needs to be passed when changed
switch (args.length) {
case 1: // It was omitted, so supply the cached value
ArrayPrototypePush(args, meta.context);
break;
case 2: // Overrides were supplied, so update cached value
ObjectAssign(meta.context, args[1]);
break;
}
ArrayPrototypePush(args, nextNextHook); ArrayPrototypePush(args, nextNextHook);
const output = await ReflectApply(hook, undefined, args); const output = await ReflectApply(hook, undefined, args);
validateOutput(outputErrIdentifier, output);
if (output?.shortCircuit === true) { meta.shortCircuited = true; } if (output?.shortCircuit === true) { meta.shortCircuited = true; }
return output; return output;
@ -554,13 +568,14 @@ class ESMLoader {
const chain = this.#loaders; const chain = this.#loaders;
const meta = { const meta = {
chainFinished: null, chainFinished: null,
context,
hookErrIdentifier: '', hookErrIdentifier: '',
hookIndex: chain.length - 1, hookIndex: chain.length - 1,
hookName: 'load', hookName: 'load',
shortCircuited: false, shortCircuited: false,
}; };
const validate = (hookErrIdentifier, { 0: nextUrl, 1: ctx }) => { const validateArgs = (hookErrIdentifier, { 0: nextUrl, 1: ctx }) => {
if (typeof nextUrl !== 'string') { if (typeof nextUrl !== 'string') {
// non-strings can be coerced to a url string // non-strings can be coerced to a url string
// validateString() throws a less-specific error // validateString() throws a less-specific error
@ -584,21 +599,24 @@ class ESMLoader {
} }
} }
validateObject(ctx, `${hookErrIdentifier} context`); if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
};
const validateOutput = (hookErrIdentifier, output) => {
if (typeof output !== 'object' || output === null) { // [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 loaded = await nextLoad(url, context);
const { hookErrIdentifier } = meta; // Retrieve the value after all settled const { hookErrIdentifier } = meta; // Retrieve the value after all settled
if (typeof loaded !== 'object') { // [2] validateOutput(hookErrIdentifier, loaded);
throw new ERR_INVALID_RETURN_VALUE(
'an object',
hookErrIdentifier,
loaded,
);
}
if (loaded?.shortCircuit === true) { meta.shortCircuited = true; } if (loaded?.shortCircuit === true) { meta.shortCircuited = true; }
@ -797,41 +815,44 @@ class ESMLoader {
); );
} }
const chain = this.#resolvers; const chain = this.#resolvers;
const context = {
conditions: DEFAULT_CONDITIONS,
importAssertions,
parentURL,
};
const meta = { const meta = {
chainFinished: null, chainFinished: null,
context,
hookErrIdentifier: '', hookErrIdentifier: '',
hookIndex: chain.length - 1, hookIndex: chain.length - 1,
hookName: 'resolve', hookName: 'resolve',
shortCircuited: false, shortCircuited: false,
}; };
const context = { const validateArgs = (hookErrIdentifier, { 0: suppliedSpecifier, 1: ctx }) => {
conditions: DEFAULT_CONDITIONS,
importAssertions,
parentURL,
};
const validate = (hookErrIdentifier, { 0: suppliedSpecifier, 1: ctx }) => {
validateString( validateString(
suppliedSpecifier, suppliedSpecifier,
`${hookErrIdentifier} specifier`, `${hookErrIdentifier} specifier`,
); // non-strings can be coerced to a url string ); // non-strings can be coerced to a url string
validateObject(ctx, `${hookErrIdentifier} context`); if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
};
const validateOutput = (hookErrIdentifier, output) => {
if (typeof output !== 'object' || output === null) { // [2]
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 = await nextResolve(originalSpecifier, context); const resolution = await nextResolve(originalSpecifier, context);
const { hookErrIdentifier } = meta; // Retrieve the value after all settled const { hookErrIdentifier } = meta; // Retrieve the value after all settled
if (typeof resolution !== 'object') { // [2] validateOutput(hookErrIdentifier, resolution);
throw new ERR_INVALID_RETURN_VALUE(
'an object',
hookErrIdentifier,
resolution,
);
}
if (resolution?.shortCircuit === true) { meta.shortCircuited = true; } if (resolution?.shortCircuit === true) { meta.shortCircuited = true; }

View File

@ -229,7 +229,7 @@ const commonArgs = [
assert.strictEqual(status, 0); assert.strictEqual(status, 0);
} }
{ // Verify chain does break and throws appropriately { // Verify resolve chain does break and throws appropriately
const { status, stderr, stdout } = spawnSync( const { status, stderr, stdout } = spawnSync(
process.execPath, process.execPath,
[ [
@ -273,7 +273,7 @@ const commonArgs = [
assert.strictEqual(status, 1); assert.strictEqual(status, 1);
} }
{ // Verify chain does break and throws appropriately { // Verify load chain does break and throws appropriately
const { status, stderr, stdout } = spawnSync( const { status, stderr, stdout } = spawnSync(
process.execPath, process.execPath,
[ [
@ -314,6 +314,27 @@ const commonArgs = [
assert.match(stderr, /'resolve' hook's nextResolve\(\) specifier/); assert.match(stderr, /'resolve' hook's nextResolve\(\) specifier/);
} }
{ // Verify error thrown when resolve hook is invalid
const { status, stderr } = spawnSync(
process.execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-null-return.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(status, 1);
assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
assert.match(stderr, /loader-resolve-null-return\.mjs/);
assert.match(stderr, /'resolve' hook's nextResolve\(\)/);
assert.match(stderr, /an object/);
assert.match(stderr, /got null/);
}
{ // Verify error thrown when invalid `context` argument passed to `nextResolve` { // Verify error thrown when invalid `context` argument passed to `nextResolve`
const { status, stderr } = spawnSync( const { status, stderr } = spawnSync(
process.execPath, process.execPath,
@ -333,6 +354,27 @@ const commonArgs = [
assert.strictEqual(status, 1); assert.strictEqual(status, 1);
} }
{ // Verify error thrown when load hook is invalid
const { status, stderr } = spawnSync(
process.execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-null-return.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(status, 1);
assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
assert.match(stderr, /loader-load-null-return\.mjs/);
assert.match(stderr, /'load' hook's nextLoad\(\)/);
assert.match(stderr, /an object/);
assert.match(stderr, /got null/);
}
{ // Verify error thrown when invalid `url` argument passed to `nextLoad` { // Verify error thrown when invalid `url` argument passed to `nextLoad`
const { status, stderr } = spawnSync( const { status, stderr } = spawnSync(
process.execPath, process.execPath,

View File

@ -0,0 +1,3 @@
export async function load(specifier, context, next) {
return null;
}

View File

@ -2,5 +2,5 @@ export async function resolve(specifier, context, next) {
console.log('resolve 42'); // This log is deliberate console.log('resolve 42'); // This log is deliberate
console.log('next<HookName>:', next.name); // This log is deliberate console.log('next<HookName>:', next.name); // This log is deliberate
return next('file:///42.mjs', context); return next('file:///42.mjs');
} }

View File

@ -0,0 +1,3 @@
export async function resolve(specifier, context, next) {
return null;
}