Skip to content

Commit 59b3a0a

Browse files
committed
module: support module.registerHooks() in ESM
1 parent 2e438ab commit 59b3a0a

File tree

2 files changed

+84
-6
lines changed

2 files changed

+84
-6
lines changed

lib/internal/modules/esm/loader.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ const { ModuleWrap, kEvaluating, kEvaluated } = internalBinding('module_wrap');
4242
const {
4343
urlToFilename,
4444
} = require('internal/modules/helpers');
45+
const {
46+
resolveHooks,
47+
resolveWithHooks,
48+
loadHooks,
49+
loadWithHooks,
50+
} = require('internal/modules/sync_hooks');
4551
let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer;
4652

4753
const { tracingChannel } = require('diagnostics_channel');
@@ -137,7 +143,7 @@ class ModuleLoader {
137143

138144
/**
139145
* Customizations to pass requests to.
140-
*
146+
* @type {import('./hooks.js').Hooks}
141147
* Note that this value _MUST_ be set with `setCustomizations`
142148
* because it needs to copy `customizations.allowImportMetaResolve`
143149
* to this property and failure to do so will cause undefined
@@ -580,6 +586,10 @@ class ModuleLoader {
580586
*/
581587
resolve(specifier, parentURL, importAttributes) {
582588
specifier = `${specifier}`;
589+
if (resolveHooks.length) {
590+
// Has module.registerHooks() hooks, use the synchronous variant that can handle both hooks.
591+
return this.resolveSync(specifier, parentURL, importAttributes);
592+
}
583593
if (this.#customizations) { // Only has module.register hooks.
584594
return this.#customizations.resolve(specifier, parentURL, importAttributes);
585595
}
@@ -606,7 +616,7 @@ class ModuleLoader {
606616
}
607617

608618
/**
609-
* This is the default resolve step for future synchronous hooks, which incorporates asynchronous hooks
619+
* This is the default resolve step for module.registerHooks(), which incorporates asynchronous hooks
610620
* from module.register() which are run in a blocking fashion for it to be synchronous.
611621
* @param {string|URL} specifier See {@link resolveSync}.
612622
* @param {{ parentURL?: string, importAttributes: ImportAttributes}} context See {@link resolveSync}.
@@ -624,7 +634,7 @@ class ModuleLoader {
624634
* asynchronous resolve hooks from module.register(), it will block until the results are returned
625635
* from the loader thread for this to be synchornous.
626636
* This is here to support `import.meta.resolve()`, `require()` in imported CJS, and
627-
* future synchronous hooks.
637+
* `module.registerHooks()` hooks.
628638
*
629639
* TODO(joyeecheung): consolidate the cache behavior and use this in require(esm).
630640
* @param {string|URL} specifier See {@link resolve}.
@@ -633,7 +643,13 @@ class ModuleLoader {
633643
* @returns {{ format: string, url: string }}
634644
*/
635645
resolveSync(specifier, parentURL, importAttributes = { __proto__: null }) {
636-
return this.#resolveAndMaybeBlockOnLoaderThread(`${specifier}`, { parentURL, importAttributes });
646+
specifier = `${specifier}`;
647+
if (resolveHooks.length) {
648+
// Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
649+
return resolveWithHooks(specifier, parentURL, importAttributes, this.#defaultConditions,
650+
this.#resolveAndMaybeBlockOnLoaderThread.bind(this));
651+
}
652+
return this.#resolveAndMaybeBlockOnLoaderThread(specifier, { parentURL, importAttributes });
637653
}
638654

639655
/**
@@ -662,6 +678,10 @@ class ModuleLoader {
662678
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
663679
*/
664680
async load(url, context) {
681+
if (loadHooks.length) {
682+
// Has module.registerHooks() hooks, use the synchronous variant that can handle both hooks.
683+
return this.#loadSync(url, context);
684+
}
665685
if (this.#customizations) {
666686
return this.#customizations.load(url, context);
667687
}
@@ -671,7 +691,7 @@ class ModuleLoader {
671691
}
672692

673693
/**
674-
* This is the default load step for future synchronous hooks, which incorporates asynchronous hooks
694+
* This is the default load step for module.registerHooks(), which incorporates asynchronous hooks
675695
* from module.register() which are run in a blocking fashion for it to be synchronous.
676696
* @param {string} url See {@link load}
677697
* @param {object} context See {@link load}
@@ -689,14 +709,21 @@ class ModuleLoader {
689709
* Similar to {@link load} but this is always run synchronously. If there are asynchronous hooks
690710
* from module.register(), this blocks on the loader thread for it to return synchronously.
691711
*
692-
* This is here to support `require()` in imported CJS and future synchronous hooks.
712+
* This is here to support `require()` in imported CJS and `module.registerHooks()` hooks.
693713
*
694714
* TODO(joyeecheung): consolidate the cache behavior and use this in require(esm).
695715
* @param {string} url See {@link load}
696716
* @param {object} [context] See {@link load}
697717
* @returns {{ format: ModuleFormat, source: ModuleSource }}
698718
*/
699719
#loadSync(url, context) {
720+
if (loadHooks.length) {
721+
// Has module.registerHooks() hooks, chain the asynchronous hooks in the default step.
722+
// TODO(joyeecheung): construct the ModuleLoadContext in the loaders directly instead
723+
// of converting them from plain objects in the hooks.
724+
return loadWithHooks(url, context.format, context.importAttributes, this.#defaultConditions,
725+
this.#loadAndMaybeBlockOnLoaderThread.bind(this));
726+
}
700727
return this.#loadAndMaybeBlockOnLoaderThread(url, context);
701728
}
702729

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
// This tests a pirates-like load hook works.
4+
5+
const common = require('../common');
6+
const assert = require('assert');
7+
const fixtures = require('../common/fixtures');
8+
const { readFileSync } = require('fs');
9+
10+
const loader = require('../fixtures/es-modules/module-hooks/load-from-this-dir');
11+
const { addHook } = require('../fixtures/es-modules/module-hooks/add-hook');
12+
13+
const matcherArgs = [];
14+
function matcher(filename) {
15+
matcherArgs.push(filename);
16+
return true;
17+
}
18+
19+
const hookArgs = [];
20+
function hook(code, filename) {
21+
hookArgs.push({ code, filename });
22+
return code.replace('$key', 'hello');
23+
}
24+
25+
(async () => {
26+
const revert = addHook(hook, { exts: ['.js'], matcher });
27+
28+
{
29+
const foo = await loader.import('foo-esm');
30+
const filename = fixtures.path('es-modules', 'module-hooks', 'node_modules', 'foo-esm', 'foo-esm.js');
31+
assert.deepStrictEqual(matcherArgs, [filename]);
32+
const code = readFileSync(filename, 'utf-8');
33+
assert.deepStrictEqual(hookArgs, [{ code, filename }]);
34+
assert.deepStrictEqual({ ...foo }, { hello: 'foo-esm' });
35+
}
36+
37+
matcherArgs.splice(0, 1);
38+
hookArgs.splice(0, 1);
39+
40+
revert();
41+
42+
// Later loads are unaffected.
43+
44+
{
45+
const bar = await loader.import('bar-esm');
46+
assert.deepStrictEqual(matcherArgs, []);
47+
assert.deepStrictEqual(hookArgs, []);
48+
assert.deepStrictEqual({ ...bar }, { $key: 'bar-esm' });
49+
}
50+
51+
})().catch(common.mustNotCall());

0 commit comments

Comments
 (0)