Skip to content

Commit

Permalink
module: allow omitting context in synchronous next hooks
Browse files Browse the repository at this point in the history
This aligns the behavior of synchronous hooks with asynchronous
hooks by allowing omission of the context parameter in the
invocation of next hooks. The contexts are merged along the
chain.

PR-URL: nodejs#57056
Fixes: nodejs#57030
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
joyeecheung authored Feb 18, 2025
1 parent ecf803d commit ea2004a
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 8 deletions.
29 changes: 21 additions & 8 deletions lib/internal/modules/customization_hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
ArrayPrototypeFindIndex,
ArrayPrototypePush,
ArrayPrototypeSplice,
ObjectAssign,
ObjectFreeze,
StringPrototypeStartsWith,
Symbol,
Expand Down Expand Up @@ -162,19 +163,31 @@ function convertURLToCJSFilename(url) {
* @param {'load'|'resolve'} name Name of the hook in ModuleHooks.
* @param {Function} defaultStep The default step in the chain.
* @param {Function} validate A function that validates and sanitize the result returned by the chain.
* @returns {Function}
*/
function buildHooks(hooks, name, defaultStep, validate) {
function buildHooks(hooks, name, defaultStep, validate, mergedContext) {
let lastRunIndex = hooks.length;
function wrapHook(index, userHook, next) {
return function wrappedHook(...args) {
/**
* Helper function to wrap around invocation of user hook or the default step
* in order to fill in missing arguments or check returned results.
* Due to the merging of the context, this must be a closure.
* @param {number} index Index in the chain. Default step is 0, last added hook is 1,
* and so on.
* @param {Function} userHookOrDefault Either the user hook or the default step to invoke.
* @param {Function|undefined} next The next wrapped step. If this is the default step, it's undefined.
* @returns {Function} Wrapped hook or default step.
*/
function wrapHook(index, userHookOrDefault, next) {
return function nextStep(arg0, context) {
lastRunIndex = index;
const hookResult = userHook(...args, next);
if (context && context !== mergedContext) {
ObjectAssign(mergedContext, context);
}
const hookResult = userHookOrDefault(arg0, mergedContext, next);
if (lastRunIndex > 0 && lastRunIndex === index && !hookResult.shortCircuit) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE('true', name, 'shortCircuit',
hookResult.shortCircuit);
}
return validate(...args, hookResult);
return validate(arg0, mergedContext, hookResult);
};
}
const chain = [wrapHook(0, defaultStep)];
Expand Down Expand Up @@ -330,7 +343,7 @@ function loadWithHooks(url, originalFormat, importAttributes, conditions, defaul
return defaultLoad(url, context);
}

const runner = buildHooks(loadHooks, 'load', defaultLoad, validateLoad);
const runner = buildHooks(loadHooks, 'load', defaultLoad, validateLoad, context);

const result = runner(url, context);
const { source, format } = result;
Expand Down Expand Up @@ -373,7 +386,7 @@ function resolveWithHooks(specifier, parentURL, importAttributes, conditions, de
return defaultResolve(specifier, context);
}

const runner = buildHooks(resolveHooks, 'resolve', defaultResolve, validateResolve);
const runner = buildHooks(resolveHooks, 'resolve', defaultResolve, validateResolve, context);

return runner(specifier, context);
}
Expand Down
31 changes: 31 additions & 0 deletions test/module-hooks/test-module-hooks-load-context-merged-esm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Test that the context parameter will be merged in multiple load hooks.

import * as common from '../common/index.mjs';
import assert from 'node:assert';
import { registerHooks } from 'node:module';

const hook1 = registerHooks({
load: common.mustCall(function(url, context, nextLoad) {
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 2 and 3.
return nextLoad(url, context);
}, 1),
});

const hook2 = registerHooks({
load: common.mustCall(function(url, context, nextLoad) {
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 3.
return nextLoad(url); // Omit the context.
}, 1),
});

const hook3 = registerHooks({
load: common.mustCall(function(url, context, nextLoad) {
return nextLoad(url, { testProp: 'custom' }); // Add a custom property
}, 1),
});

await import('../fixtures/es-modules/message.mjs');

hook3.deregister();
hook2.deregister();
hook1.deregister();
33 changes: 33 additions & 0 deletions test/module-hooks/test-module-hooks-load-context-merged.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

// Test that the context parameter will be merged in multiple load hooks.

const common = require('../common');
const assert = require('assert');
const { registerHooks } = require('module');

const hook1 = registerHooks({
load: common.mustCall(function(url, context, nextLoad) {
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 2 and 3.
return nextLoad(url, context);
}, 1),
});

const hook2 = registerHooks({
load: common.mustCall(function(url, context, nextLoad) {
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 3.
return nextLoad(url); // Omit the context.
}, 1),
});

const hook3 = registerHooks({
load: common.mustCall(function(url, context, nextLoad) {
return nextLoad(url, { testProp: 'custom' }); // Add a custom property
}, 1),
});

require('../fixtures/empty.js');

hook3.deregister();
hook2.deregister();
hook1.deregister();
14 changes: 14 additions & 0 deletions test/module-hooks/test-module-hooks-load-context-optional-esm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Test that the context parameter can be omitted in the nextLoad invocation.

import * as common from '../common/index.mjs';
import { registerHooks } from 'node:module';

const hook = registerHooks({
load: common.mustCall(function(url, context, nextLoad) {
return nextLoad(url);
}, 1),
});

await import('../fixtures/es-modules/message.mjs');

hook.deregister();
16 changes: 16 additions & 0 deletions test/module-hooks/test-module-hooks-load-context-optional.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

// Test that the context parameter can be omitted in the nextLoad invocation.

const common = require('../common');
const { registerHooks } = require('module');

const hook = registerHooks({
load: common.mustCall(function(url, context, nextLoad) {
return nextLoad(url);
}, 1),
});

require('../fixtures/empty.js');

hook.deregister();
32 changes: 32 additions & 0 deletions test/module-hooks/test-module-hooks-resolve-context-merged-esm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Test that the context parameter will be merged in multiple resolve hooks.

import * as common from '../common/index.mjs';
import assert from 'node:assert';
import { registerHooks } from 'node:module';

const hook1 = registerHooks({
resolve: common.mustCall(function(specifier, context, nextResolve) {
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 2 and 3.
const result = nextResolve(specifier, context);
return result;
}, 1),
});

const hook2 = registerHooks({
resolve: common.mustCall(function(specifier, context, nextResolve) {
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 3.
return nextResolve(specifier); // Omit the context.
}, 1),
});

const hook3 = registerHooks({
resolve: common.mustCall(function(specifier, context, nextResolve) {
return nextResolve(specifier, { testProp: 'custom' }); // Add a custom property
}, 1),
});

await import('../fixtures/es-modules/message.mjs');

hook3.deregister();
hook2.deregister();
hook1.deregister();
34 changes: 34 additions & 0 deletions test/module-hooks/test-module-hooks-resolve-context-merged.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

// Test that the context parameter will be merged in multiple resolve hooks.

const common = require('../common');
const assert = require('assert');
const { registerHooks } = require('module');

const hook1 = registerHooks({
resolve: common.mustCall(function(specifier, context, nextResolve) {
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 2 and 3.
const result = nextResolve(specifier, context);
return result;
}, 1),
});

const hook2 = registerHooks({
resolve: common.mustCall(function(specifier, context, nextResolve) {
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 3.
return nextResolve(specifier); // Omit the context.
}, 1),
});

const hook3 = registerHooks({
resolve: common.mustCall(function(specifier, context, nextResolve) {
return nextResolve(specifier, { testProp: 'custom' }); // Add a custom property
}, 1),
});

require('../fixtures/empty.js');

hook3.deregister();
hook2.deregister();
hook1.deregister();
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Test that the context parameter can be omitted in the nextResolve invocation.

import * as common from '../common/index.mjs';
import { registerHooks } from 'node:module';

const hook = registerHooks({
resolve: common.mustCall(function(specifier, context, nextResolve) {
return nextResolve(specifier);
}, 1),
});

await import('../fixtures/es-modules/message.mjs');

hook.deregister();
16 changes: 16 additions & 0 deletions test/module-hooks/test-module-hooks-resolve-context-optional.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

// Test that the context parameter can be omitted in the nextResolve invocation.

const common = require('../common');
const { registerHooks } = require('module');

const hook = registerHooks({
resolve: common.mustCall(function(specifier, context, nextResolve) {
return nextResolve(specifier);
}, 1),
});

require('../fixtures/empty.js');

hook.deregister();

0 comments on commit ea2004a

Please # to comment.