Skip to content

Commit 26b9ab4

Browse files
feat(esm): support nested loader chains
Fixes #48515
1 parent 42d8143 commit 26b9ab4

File tree

3 files changed

+104
-23
lines changed

3 files changed

+104
-23
lines changed

lib/internal/modules/esm/hooks.js

+45-3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const {
5454
const {
5555
getDefaultConditions,
5656
loaderWorkerId,
57+
createHooksLoader,
5758
} = require('internal/modules/esm/utils');
5859
const { deserializeError } = require('internal/error_serdes');
5960
const {
@@ -136,6 +137,10 @@ class Hooks {
136137
this.addCustomLoader(urlOrSpecifier, keyedExports);
137138
}
138139

140+
getChains() {
141+
return this.#chains;
142+
}
143+
139144
/**
140145
* Collect custom/user-defined module loader hook(s).
141146
* After all hooks have been collected, the global preload hook(s) must be initialized.
@@ -220,16 +225,25 @@ class Hooks {
220225
originalSpecifier,
221226
parentURL,
222227
importAssertions = { __proto__: null },
228+
) {
229+
return this.resolveWithChain(this.#chains.resolve, originalSpecifier, parentURL, importAssertions);
230+
}
231+
232+
async resolveWithChain(
233+
chain,
234+
originalSpecifier,
235+
parentURL,
236+
importAssertions = { __proto__: null },
223237
) {
224238
throwIfInvalidParentURL(parentURL);
225239

226-
const chain = this.#chains.resolve;
227240
const context = {
228241
conditions: getDefaultConditions(),
229242
importAssertions,
230243
parentURL,
231244
};
232245
const meta = {
246+
hooks: this,
233247
chainFinished: null,
234248
context,
235249
hookErrIdentifier: '',
@@ -344,8 +358,12 @@ class Hooks {
344358
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
345359
*/
346360
async load(url, context = {}) {
347-
const chain = this.#chains.load;
361+
return this.loadWithChain(this.#chains.load, url, context)
362+
}
363+
364+
async loadWithChain(chain, url, context = {}) {
348365
const meta = {
366+
hooks: this,
349367
chainFinished: null,
350368
context,
351369
hookErrIdentifier: '',
@@ -749,7 +767,31 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
749767
ObjectAssign(meta.context, context);
750768
}
751769

752-
const output = await hook(arg0, meta.context, nextNextHook);
770+
const withESMLoader = require('internal/process/esm_loader').withESMLoader;
771+
772+
const chains = meta.hooks.getChains();
773+
const loadChain = chain === chains.load ? chains.load.slice(0, generatedHookIndex) : chains.load;
774+
const resolveChain = chain === chains.resolve ? chains.resolve.slice(0, generatedHookIndex) : chains.resolve;
775+
const loader = createHooksLoader({
776+
async resolve(
777+
originalSpecifier,
778+
parentURL,
779+
importAssertions = { __proto__: null }
780+
) {
781+
return await meta.hooks.resolveWithChain(
782+
resolveChain,
783+
originalSpecifier,
784+
parentURL,
785+
importAssertions,
786+
);
787+
},
788+
async load(url, context = {}) {
789+
return await meta.hooks.loadWithChain(loadChain, url, context);
790+
},
791+
})
792+
const output = await withESMLoader(loader, async () => {
793+
return await hook(arg0, meta.context, nextNextHook);
794+
});
753795

754796
validateOutput(outputErrIdentifier, output);
755797

lib/internal/modules/esm/utils.js

+50-20
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const {
2424
} = require('internal/vm/module');
2525
const assert = require('internal/assert');
2626

27+
2728
const callbackMap = new SafeWeakMap();
2829
function setCallbackForWrap(wrap, data) {
2930
callbackMap.set(wrap, data);
@@ -107,26 +108,19 @@ function isLoaderWorker() {
107108
return _isLoaderWorker;
108109
}
109110

110-
async function initializeHooks() {
111-
const customLoaderURLs = getOptionValue('--experimental-loader');
112-
113-
let cwd;
114-
try {
115-
// `process.cwd()` can fail if the parent directory is deleted while the process runs.
116-
cwd = process.cwd() + '/';
117-
} catch {
118-
cwd = '/';
119-
}
120-
121-
122-
const { Hooks } = require('internal/modules/esm/hooks');
123-
const hooks = new Hooks();
124-
111+
const createHooksLoader = (hooks) => {
112+
// TODO: HACK: `DefaultModuleLoader` depends on `getDefaultConditions` defined in
113+
// this file so we have a circular reference going on. If that function was in
114+
// it's on file we could just expose this class generically.
125115
const { DefaultModuleLoader } = require('internal/modules/esm/loader');
126-
class ModuleLoader extends DefaultModuleLoader {
127-
loaderType = 'internal';
116+
class HooksModuleLoader extends DefaultModuleLoader {
117+
#hooks;
118+
constructor(hooks) {
119+
super();
120+
this.#hooks = hooks;
121+
}
128122
async #getModuleJob(specifier, parentURL, importAssertions) {
129-
const resolveResult = await hooks.resolve(specifier, parentURL, importAssertions);
123+
const resolveResult = await this.#hooks.resolve(specifier, parentURL, importAssertions);
130124
return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions);
131125
}
132126
getModuleJob(specifier, parentURL, importAssertions) {
@@ -143,9 +137,44 @@ async function initializeHooks() {
143137
},
144138
};
145139
}
146-
load(url, context) { return hooks.load(url, context); }
140+
resolve(
141+
originalSpecifier,
142+
parentURL,
143+
importAssertions = { __proto__: null },
144+
) {
145+
console.log('PRIVATE RESOLVE', originalSpecifier);
146+
return this.#hooks.resolve(
147+
originalSpecifier,
148+
parentURL,
149+
importAssertions
150+
);
151+
}
152+
load(url, context = {}) {
153+
console.log('PRIVATE LOAD', url);
154+
return this.#hooks.load(url, context);
155+
}
147156
}
148-
const privateModuleLoader = new ModuleLoader();
157+
return new HooksModuleLoader(hooks);
158+
}
159+
160+
async function initializeHooks() {
161+
const customLoaderURLs = getOptionValue('--experimental-loader');
162+
163+
let cwd;
164+
try {
165+
// `process.cwd()` can fail if the parent directory is deleted while the process runs.
166+
cwd = process.cwd() + '/';
167+
} catch {
168+
cwd = '/';
169+
}
170+
171+
172+
const { Hooks } = require('internal/modules/esm/hooks');
173+
const hooks = new Hooks();
174+
175+
176+
const privateModuleLoader = createHooksLoader(hooks);
177+
privateModuleLoader.loaderType = 'internal';
149178
const parentURL = pathToFileURL(cwd).href;
150179

151180
// TODO(jlenon7): reuse the `Hooks.register()` method for registering loaders.
@@ -175,4 +204,5 @@ module.exports = {
175204
getConditionsSet,
176205
loaderWorkerId: 'internal/modules/esm/worker',
177206
isLoaderWorker,
207+
createHooksLoader,
178208
};

lib/internal/process/esm_loader.js

+9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ const { kEmptyObject } = require('internal/util');
1515
let esmLoader;
1616

1717
module.exports = {
18+
async withESMLoader(loader, fn) {
19+
const oldLoader = esmLoader;
20+
esmLoader = loader;
21+
try {
22+
return await fn();
23+
} finally {
24+
esmLoader = oldLoader;
25+
}
26+
},
1827
get esmLoader() {
1928
return esmLoader ??= createModuleLoader(true);
2029
},

0 commit comments

Comments
 (0)