Skip to content

Commit b230bc0

Browse files
committed
module: allow omitting context in synchronous next hooks
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.
1 parent 69d32d1 commit b230bc0

9 files changed

+212
-8
lines changed

lib/internal/modules/customization_hooks.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
ArrayPrototypeFindIndex,
55
ArrayPrototypePush,
66
ArrayPrototypeSplice,
7+
ObjectAssign,
78
ObjectFreeze,
89
StringPrototypeStartsWith,
910
Symbol,
@@ -162,20 +163,33 @@ function convertURLToCJSFilename(url) {
162163
* @param {'load'|'resolve'} name Name of the hook in ModuleHooks.
163164
* @param {Function} defaultStep The default step in the chain.
164165
* @param {Function} validate A function that validates and sanitize the result returned by the chain.
165-
* @returns {Function}
166166
*/
167-
function buildHooks(hooks, name, defaultStep, validate) {
167+
function buildHooks(hooks, name, defaultStep, validate, mergedContext) {
168168
let lastRunIndex = hooks.length;
169-
function wrapHook(index, userHook, next) {
170-
return function wrappedHook(...args) {
169+
/**
170+
* Helper function to wrap around invocation of user hook or the default step
171+
* in order to fill in missing arguments or check returned results.
172+
* Due to the merging of the context, this must be a closure.
173+
* @param {number} index Index in the chain. Default step is 0, last added hook is 1,
174+
* and so on.
175+
* @param {Function} userHookOrDefault Either the user hook or the default step to invoke.
176+
* @param {Function|undefined} next The next wrapped step. If this is the default step, it's undefined.
177+
* @returns {Function} Wrapped hook or default step.
178+
*/
179+
function wrapHook(index, userHookOrDefault, next) {
180+
function nextStep(arg0, context) {
171181
lastRunIndex = index;
172-
const hookResult = userHook(...args, next);
182+
if (context && context !== mergedContext) {
183+
ObjectAssign(mergedContext, context);
184+
}
185+
const hookResult = userHookOrDefault(arg0, mergedContext, next);
173186
if (lastRunIndex > 0 && lastRunIndex === index && !hookResult.shortCircuit) {
174187
throw new ERR_INVALID_RETURN_PROPERTY_VALUE('true', name, 'shortCircuit',
175188
hookResult.shortCircuit);
176189
}
177-
return validate(...args, hookResult);
190+
return validate(arg0, mergedContext, hookResult);
178191
};
192+
return nextStep;
179193
}
180194
const chain = [wrapHook(0, defaultStep)];
181195
for (let i = 0; i < hooks.length; ++i) {
@@ -330,7 +344,7 @@ function loadWithHooks(url, originalFormat, importAttributes, conditions, defaul
330344
return defaultLoad(url, context);
331345
}
332346

333-
const runner = buildHooks(loadHooks, 'load', defaultLoad, validateLoad);
347+
const runner = buildHooks(loadHooks, 'load', defaultLoad, validateLoad, context);
334348

335349
const result = runner(url, context);
336350
const { source, format } = result;
@@ -373,7 +387,7 @@ function resolveWithHooks(specifier, parentURL, importAttributes, conditions, de
373387
return defaultResolve(specifier, context);
374388
}
375389

376-
const runner = buildHooks(resolveHooks, 'resolve', defaultResolve, validateResolve);
390+
const runner = buildHooks(resolveHooks, 'resolve', defaultResolve, validateResolve, context);
377391

378392
return runner(specifier, context);
379393
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Test that the context parameter will be merged in multiple load hooks.
2+
3+
import * as common from '../common/index.mjs';
4+
import assert from 'node:assert';
5+
import { registerHooks } from 'node:module';
6+
7+
const hook1 = registerHooks({
8+
load: common.mustCall(function(url, context, nextLoad) {
9+
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 2 and 3.
10+
return nextLoad(url, context);
11+
}, 1),
12+
});
13+
14+
const hook2 = registerHooks({
15+
load: common.mustCall(function(url, context, nextLoad) {
16+
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 3.
17+
return nextLoad(url); // Omit the context.
18+
}, 1),
19+
});
20+
21+
const hook3 = registerHooks({
22+
load: common.mustCall(function(url, context, nextLoad) {
23+
return nextLoad(url, { testProp: 'custom' }); // Add a custom property
24+
}, 1),
25+
});
26+
27+
await import('../fixtures/es-modules/message.mjs');
28+
29+
hook3.deregister();
30+
hook2.deregister();
31+
hook1.deregister();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
// Test that the context parameter will be merged in multiple load hooks.
4+
5+
const common = require('../common');
6+
const assert = require('assert');
7+
const { registerHooks } = require('module');
8+
9+
const hook1 = registerHooks({
10+
load: common.mustCall(function(url, context, nextLoad) {
11+
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 2 and 3.
12+
return nextLoad(url, context);
13+
}, 1),
14+
});
15+
16+
const hook2 = registerHooks({
17+
load: common.mustCall(function(url, context, nextLoad) {
18+
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 3.
19+
return nextLoad(url); // Omit the context.
20+
}, 1),
21+
});
22+
23+
const hook3 = registerHooks({
24+
load: common.mustCall(function(url, context, nextLoad) {
25+
return nextLoad(url, { testProp: 'custom' }); // Add a custom property
26+
}, 1),
27+
});
28+
29+
require('../fixtures/empty.js');
30+
31+
hook3.deregister();
32+
hook2.deregister();
33+
hook1.deregister();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Test that the context parameter can be omitted in the nextLoad invocation.
2+
3+
import * as common from '../common/index.mjs';
4+
import { registerHooks } from 'node:module';
5+
6+
const hook = registerHooks({
7+
load: common.mustCall(function(url, context, nextLoad) {
8+
return nextLoad(url);
9+
}, 1),
10+
});
11+
12+
await import('../fixtures/es-modules/message.mjs');
13+
14+
hook.deregister();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
// Test that the context parameter can be omitted in the nextLoad invocation.
4+
5+
const common = require('../common');
6+
const { registerHooks } = require('module');
7+
8+
const hook = registerHooks({
9+
load: common.mustCall(function(url, context, nextLoad) {
10+
return nextLoad(url);
11+
}, 1),
12+
});
13+
14+
require('../fixtures/empty.js');
15+
16+
hook.deregister();
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Test that the context parameter will be merged in multiple resolve hooks.
2+
3+
import * as common from '../common/index.mjs';
4+
import assert from 'node:assert';
5+
import { registerHooks } from 'node:module';
6+
7+
const hook1 = registerHooks({
8+
resolve: common.mustCall(function(specifier, context, nextResolve) {
9+
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 2 and 3.
10+
const result = nextResolve(specifier, context);
11+
return result;
12+
}, 1),
13+
});
14+
15+
const hook2 = registerHooks({
16+
resolve: common.mustCall(function(specifier, context, nextResolve) {
17+
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 3.
18+
return nextResolve(specifier); // Omit the context.
19+
}, 1),
20+
});
21+
22+
const hook3 = registerHooks({
23+
resolve: common.mustCall(function(specifier, context, nextResolve) {
24+
return nextResolve(specifier, { testProp: 'custom' }); // Add a custom property
25+
}, 1),
26+
});
27+
28+
await import('../fixtures/es-modules/message.mjs');
29+
30+
hook3.deregister();
31+
hook2.deregister();
32+
hook1.deregister();
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
// Test that the context parameter will be merged in multiple resolve hooks.
4+
5+
const common = require('../common');
6+
const assert = require('assert');
7+
const { registerHooks } = require('module');
8+
9+
const hook1 = registerHooks({
10+
resolve: common.mustCall(function(specifier, context, nextResolve) {
11+
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 2 and 3.
12+
const result = nextResolve(specifier, context);
13+
return result;
14+
}, 1),
15+
});
16+
17+
const hook2 = registerHooks({
18+
resolve: common.mustCall(function(specifier, context, nextResolve) {
19+
assert.strictEqual(context.testProp, 'custom'); // It should be merged from hook 3.
20+
return nextResolve(specifier); // Omit the context.
21+
}, 1),
22+
});
23+
24+
const hook3 = registerHooks({
25+
resolve: common.mustCall(function(specifier, context, nextResolve) {
26+
return nextResolve(specifier, { testProp: 'custom' }); // Add a custom property
27+
}, 1),
28+
});
29+
30+
require('../fixtures/empty.js');
31+
32+
hook3.deregister();
33+
hook2.deregister();
34+
hook1.deregister();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Test that the context parameter can be omitted in the nextResolve invocation.
2+
3+
import * as common from '../common/index.mjs';
4+
import { registerHooks } from 'node:module';
5+
6+
const hook = registerHooks({
7+
resolve: common.mustCall(function(specifier, context, nextResolve) {
8+
return nextResolve(specifier);
9+
}, 1),
10+
});
11+
12+
await import('../fixtures/es-modules/message.mjs');
13+
14+
hook.deregister();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
// Test that the context parameter can be omitted in the nextResolve invocation.
4+
5+
const common = require('../common');
6+
const { registerHooks } = require('module');
7+
8+
const hook = registerHooks({
9+
resolve: common.mustCall(function(specifier, context, nextResolve) {
10+
return nextResolve(specifier);
11+
}, 1),
12+
});
13+
14+
require('../fixtures/empty.js');
15+
16+
hook.deregister();

0 commit comments

Comments
 (0)