Skip to content

Commit a989338

Browse files
esm: refactor hooks – add loader instances
Previously we had one array for each kind of chain in the variable `#chains`; this is probably the most efficient, but it decouples the connection between the original `register` call and the hook functions themselves. By adding `#loaderInstances` we can keep track of each time `register` is called. This allows, for example, trivially removing the loader later on, or associating shared context with a particular `register` call. The tradeoff here if we just did this would be that during hook traversal we may iterate over a slightly larger number of objects. Instead we keep `#hooks` but it is now a derived property; every time `#loaderInstances` changes we simply rebuild `#hooks`.
1 parent 33710e7 commit a989338

File tree

2 files changed

+63
-60
lines changed

2 files changed

+63
-60
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
const { defaultLoad } = require('internal/modules/esm/load');
4+
const { defaultResolve } = require('internal/modules/esm/resolve');
5+
6+
exports.resolve = defaultResolve;
7+
exports.load = defaultLoad;

lib/internal/modules/esm/hooks.js

Lines changed: 56 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ const {
5454
} = require('internal/util');
5555

5656
const {
57-
defaultResolve,
5857
throwIfInvalidParentURL,
5958
} = require('internal/modules/esm/resolve');
6059
const {
@@ -87,45 +86,40 @@ let importMetaInitializer;
8786
// [2] `validate...()`s throw the wrong error
8887

8988
class Hooks {
90-
#chains = {
91-
/**
92-
* Prior to ESM loading. These are called once before any modules are started.
93-
* @private
94-
* @property {KeyedHook[]} globalPreload Last-in-first-out list of preload hooks.
95-
*/
96-
globalPreload: [],
97-
98-
/**
99-
* Phase 1 of 2 in ESM loading.
100-
* The output of the `resolve` chain of hooks is passed into the `load` chain of hooks.
101-
* @private
102-
* @property {KeyedHook[]} resolve Last-in-first-out collection of resolve hooks.
103-
*/
104-
resolve: [
105-
{
106-
fn: defaultResolve,
107-
url: 'node:internal/modules/esm/resolve',
108-
},
109-
],
110-
111-
/**
112-
* Phase 2 of 2 in ESM loading.
113-
* @private
114-
* @property {KeyedHook[]} load Last-in-first-out collection of loader hooks.
115-
*/
116-
load: [
117-
{
118-
fn: require('internal/modules/esm/load').defaultLoad,
119-
url: 'node:internal/modules/esm/load',
120-
},
121-
],
122-
};
89+
#loaderInstances = [];
90+
#chains = {};
12391

12492
// Cache URLs we've already validated to avoid repeated validation
12593
#validatedUrls = new SafeSet();
12694

12795
allowImportMetaResolve = false;
12896

97+
constructor() {
98+
const defaultLoader = 'internal/modules/esm/default_loader';
99+
this.addCustomLoader(`node:${defaultLoader}`, require(defaultLoader));
100+
}
101+
102+
#rebuildChain(name) {
103+
const chain = this.#chains[name] = [];
104+
let i = 0;
105+
for (const instance of this.#loaderInstances) {
106+
if (typeof instance[name] !== 'function') {
107+
continue;
108+
}
109+
chain.push({
110+
loader: instance,
111+
fn: instance[name],
112+
next: chain[i++ - 1],
113+
});
114+
}
115+
}
116+
117+
#rebuildChains() {
118+
this.#rebuildChain('globalPreload');
119+
this.#rebuildChain('resolve');
120+
this.#rebuildChain('load');
121+
}
122+
129123
/**
130124
* Import and register custom/user-defined module loader hook(s).
131125
* @param {string} urlOrSpecifier
@@ -164,25 +158,26 @@ class Hooks {
164158
emitExperimentalWarning(
165159
'`globalPreload` is planned for removal in favor of `initialize`. `globalPreload`',
166160
);
167-
ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url });
168-
}
169-
if (resolve) {
170-
const next = this.#chains.resolve[this.#chains.resolve.length - 1];
171-
ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next });
172161
}
173-
if (load) {
174-
const next = this.#chains.load[this.#chains.load.length - 1];
175-
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
176-
}
177-
return initialize?.(data);
162+
const instance = {
163+
__proto__: null,
164+
url,
165+
globalPreload,
166+
initialize,
167+
resolve,
168+
load,
169+
};
170+
ArrayPrototypePush(this.#loaderInstances, instance);
171+
this.#rebuildChains();
172+
return initialize?.(data, { __proto__: null, id: instance.id, url });
178173
}
179174

180175
/**
181176
* Initialize `globalPreload` hooks.
182177
*/
183178
initializeGlobalPreload() {
184179
const preloadScripts = [];
185-
for (let i = this.#chains.globalPreload.length - 1; i >= 0; i--) {
180+
for (const chainEntry of this.#chains.globalPreload) {
186181
const { MessageChannel } = require('internal/worker/io');
187182
const channel = new MessageChannel();
188183
const {
@@ -193,10 +188,7 @@ class Hooks {
193188
insidePreload.unref();
194189
insideLoader.unref();
195190

196-
const {
197-
fn: preload,
198-
url: specifier,
199-
} = this.#chains.globalPreload[i];
191+
const preload = chainEntry.fn;
200192

201193
const preloaded = preload({
202194
port: insideLoader,
@@ -207,8 +199,8 @@ class Hooks {
207199
if (typeof preloaded !== 'string') { // [2]
208200
throw new ERR_INVALID_RETURN_VALUE(
209201
'a string',
210-
`${specifier} globalPreload`,
211-
preload,
202+
`${chainEntry.loader.url} globalPreload`,
203+
chainEntry.fn,
212204
);
213205
}
214206

@@ -240,7 +232,6 @@ class Hooks {
240232
) {
241233
throwIfInvalidParentURL(parentURL);
242234

243-
const chain = this.#chains.resolve;
244235
const context = {
245236
conditions: getDefaultConditions(),
246237
importAssertions,
@@ -272,7 +263,11 @@ class Hooks {
272263
}
273264
};
274265

275-
const nextResolve = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
266+
const nextResolve = nextHookFactory(
267+
this.#chains.resolve[this.#chains.resolve.length - 1],
268+
meta,
269+
{ validateArgs, validateOutput },
270+
);
276271

277272
const resolution = await nextResolve(originalSpecifier, context);
278273
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -364,7 +359,6 @@ class Hooks {
364359
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
365360
*/
366361
async load(url, context = {}) {
367-
const chain = this.#chains.load;
368362
const meta = {
369363
chainFinished: null,
370364
context,
@@ -410,7 +404,11 @@ class Hooks {
410404
}
411405
};
412406

413-
const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
407+
const nextLoad = nextHookFactory(
408+
this.#chains.load[this.#chains.load.length - 1],
409+
meta,
410+
{ validateArgs, validateOutput },
411+
);
414412

415413
const loaded = await nextLoad(url, context);
416414
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -789,11 +787,9 @@ function pluckHooks({
789787
function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
790788
// First, prepare the current
791789
const { hookName } = meta;
792-
const {
793-
fn: hook,
794-
url: hookFilePath,
795-
next,
796-
} = current;
790+
791+
const { next, fn: hook, loader } = current;
792+
const { url: hookFilePath } = loader;
797793

798794
// ex 'nextResolve'
799795
const nextHookName = `next${

0 commit comments

Comments
 (0)