Skip to content

Commit 7aac21e

Browse files
arcanisruyadorno
authored andcommitted
esm: leverage loaders when resolving subsequent loaders
PR-URL: #43772 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jacob Smith <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent 8b473af commit 7aac21e

15 files changed

+140
-23
lines changed

doc/api/esm.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,9 @@ changes:
701701
To customize the default module resolution, loader hooks can optionally be
702702
provided via a `--experimental-loader ./loader-name.mjs` argument to Node.js.
703703
704-
When hooks are used they apply to the entry point and all `import` calls. They
705-
won't apply to `require` calls; those still follow [CommonJS][] rules.
704+
When hooks are used they apply to each subsequent loader, the entry point, and
705+
all `import` calls. They won't apply to `require` calls; those still follow
706+
[CommonJS][] rules.
706707
707708
Loaders follow the pattern of `--require`:
708709

lib/internal/modules/esm/loader.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ class ESMLoader {
309309

310310
/**
311311
* Collect custom/user-defined hook(s). After all hooks have been collected,
312-
* calls global preload hook(s).
312+
* the global preload hook(s) must be called.
313313
* @param {KeyedExports} customLoaders
314314
* A list of exports from user-defined loaders (as returned by
315315
* ESMLoader.import()).
@@ -356,8 +356,6 @@ class ESMLoader {
356356
);
357357
}
358358
}
359-
360-
this.preload();
361359
}
362360

363361
async eval(

lib/internal/process/esm_loader.js

+35-13
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
const {
44
ArrayIsArray,
5+
ArrayPrototypePushApply,
56
} = primordials;
67

78
const { ESMLoader } = require('internal/modules/esm/loader');
89
const {
910
hasUncaughtExceptionCaptureCallback,
1011
} = require('internal/process/execution');
1112
const { pathToFileURL } = require('internal/url');
13+
const { kEmptyObject } = require('internal/util');
1214

1315
const esmLoader = new ESMLoader();
1416
exports.esmLoader = esmLoader;
@@ -28,41 +30,61 @@ async function initializeLoader() {
2830
const { getOptionValue } = require('internal/options');
2931
const customLoaders = getOptionValue('--experimental-loader');
3032
const preloadModules = getOptionValue('--import');
31-
const loaders = await loadModulesInIsolation(customLoaders);
33+
34+
let cwd;
35+
try {
36+
cwd = process.cwd() + '/';
37+
} catch {
38+
cwd = '/';
39+
}
40+
41+
const internalEsmLoader = new ESMLoader();
42+
const allLoaders = [];
43+
44+
const parentURL = pathToFileURL(cwd).href;
45+
46+
for (let i = 0; i < customLoaders.length; i++) {
47+
const customLoader = customLoaders[i];
48+
49+
// Importation must be handled by internal loader to avoid polluting user-land
50+
const keyedExportsSublist = await internalEsmLoader.import(
51+
[customLoader],
52+
parentURL,
53+
kEmptyObject,
54+
);
55+
56+
internalEsmLoader.addCustomLoaders(keyedExportsSublist);
57+
ArrayPrototypePushApply(allLoaders, keyedExportsSublist);
58+
}
3259

3360
// Hooks must then be added to external/public loader
3461
// (so they're triggered in userland)
35-
esmLoader.addCustomLoaders(loaders);
62+
esmLoader.addCustomLoaders(allLoaders);
63+
esmLoader.preload();
3664

3765
// Preload after loaders are added so they can be used
3866
if (preloadModules?.length) {
39-
await loadModulesInIsolation(preloadModules, loaders);
67+
await loadModulesInIsolation(parentURL, preloadModules, allLoaders);
4068
}
4169

4270
isESMInitialized = true;
4371
}
4472

45-
function loadModulesInIsolation(specifiers, loaders = []) {
73+
function loadModulesInIsolation(parentURL, specifiers, loaders = []) {
4674
if (!ArrayIsArray(specifiers) || specifiers.length === 0) { return; }
4775

48-
let cwd;
49-
try {
50-
cwd = process.cwd() + '/';
51-
} catch {
52-
cwd = 'file:///';
53-
}
54-
5576
// A separate loader instance is necessary to avoid cross-contamination
5677
// between internal Node.js and userland. For example, a module with internal
5778
// state (such as a counter) should be independent.
5879
const internalEsmLoader = new ESMLoader();
5980
internalEsmLoader.addCustomLoaders(loaders);
81+
internalEsmLoader.preload();
6082

6183
// Importation must be handled by internal loader to avoid poluting userland
6284
return internalEsmLoader.import(
6385
specifiers,
64-
pathToFileURL(cwd).href,
65-
{ __proto__: null },
86+
parentURL,
87+
kEmptyObject,
6688
);
6789
}
6890

test/es-module/test-esm-loader-chaining.mjs

+17-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import assert from 'node:assert';
44
import { execPath } from 'node:process';
55
import { describe, it } from 'node:test';
66

7-
87
const setupArgs = [
98
'--no-warnings',
109
'--input-type=module',
@@ -253,6 +252,23 @@ describe('ESM: loader chaining', { concurrency: true }, () => {
253252
assert.strictEqual(code, 0);
254253
});
255254

255+
it('should allow loaders to influence subsequent loader resolutions', async () => {
256+
const { code, stderr } = await spawnPromisified(
257+
execPath,
258+
[
259+
'--loader',
260+
fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
261+
'--loader',
262+
'xxx/loader-resolve-strip-yyy.mjs',
263+
...commonArgs,
264+
],
265+
{ encoding: 'utf8', cwd: fixtures.path('es-module-loaders') },
266+
);
267+
268+
assert.strictEqual(stderr, '');
269+
assert.strictEqual(code, 0);
270+
});
271+
256272
it('should throw when the resolve chain is broken', async () => {
257273
const { code, stderr, stdout } = await spawnPromisified(
258274
execPath,

test/fixtures/es-module-loaders/loader-load-foo-or-42.mjs

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
export async function load(url) {
1+
export async function load(url, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (url.includes('loader')) {
7+
return next(url);
8+
}
9+
210
const val = url.includes('42')
311
? '42'
412
: '"foo"';

test/fixtures/es-module-loaders/loader-load-incomplete.mjs

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
export async function load() {
1+
export async function load(url, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (url.includes('loader')) {
7+
return next(url);
8+
}
9+
210
return {
311
format: 'module',
412
source: 'export default 42',
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
export async function load(url, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (url.includes('loader')) {
7+
return next(url);
8+
}
9+
210
console.log('load passthru'); // This log is deliberate
311
return next(url);
412
}

test/fixtures/es-module-loaders/loader-resolve-42.mjs

+8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
export async function resolve(specifier, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (specifier.includes('loader')) {
7+
return next(specifier);
8+
}
9+
210
console.log('resolve 42'); // This log is deliberate
311
console.log('next<HookName>:', next.name); // This log is deliberate
412

Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
export async function resolve(specifier, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (specifier.includes('loader')) {
7+
return next(specifier);
8+
}
9+
210
console.log('resolve foo'); // This log is deliberate
311
return next('file:///foo.mjs');
412
}

test/fixtures/es-module-loaders/loader-resolve-incomplete.mjs

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
export async function resolve() {
1+
export async function resolve(specifier, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (specifier.includes('loader')) {
7+
return next(specifier);
8+
}
9+
210
return {
311
url: 'file:///incomplete-resolve-chain.js',
412
};

test/fixtures/es-module-loaders/loader-resolve-next-modified.mjs

+8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
export async function resolve(url, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (url.includes('loader')) {
7+
return next(url);
8+
}
9+
210
const {
311
format,
412
url: nextUrl,
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
export async function resolve(specifier, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (specifier.includes('loader')) {
7+
return next(specifier);
8+
}
9+
210
console.log('resolve passthru'); // This log is deliberate
311
return next(specifier);
412
}

test/fixtures/es-module-loaders/loader-resolve-shortcircuit.mjs

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
export async function resolve(specifier) {
1+
export async function resolve(specifier, context, next) {
2+
// This check is needed to make sure that we don't prevent the
3+
// resolution from follow-up loaders. It wouldn't be a problem
4+
// in real life because loaders aren't supposed to break the
5+
// resolution, but the ones used in our tests do, for convenience.
6+
if (specifier.includes('loader')) {
7+
return next(specifier);
8+
}
9+
210
return {
311
shortCircuit: true,
412
url: specifier,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export async function resolve(specifier, context, nextResolve) {
2+
console.log(`loader-a`, {specifier});
3+
return nextResolve(specifier.replace(/^xxx\//, `./`));
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export async function resolve(specifier, context, nextResolve) {
2+
console.log(`loader-b`, {specifier});
3+
return nextResolve(specifier.replace(/^yyy\//, `./`));
4+
}

0 commit comments

Comments
 (0)