Skip to content

Commit 7f30aae

Browse files
committed
esm: leverage loaders when resolving subsequent loaders
1 parent 0496b85 commit 7f30aae

14 files changed

+119
-14
lines changed

lib/internal/modules/esm/loader.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ class ESMLoader {
306306

307307
/**
308308
* Collect custom/user-defined hook(s). After all hooks have been collected,
309-
* calls global preload hook(s).
309+
* the global preload hook(s) must be called.
310310
* @param {KeyedExports} customLoaders
311311
* A list of exports from user-defined loaders (as returned by
312312
* ESMLoader.import()).
@@ -353,8 +353,6 @@ class ESMLoader {
353353
);
354354
}
355355
}
356-
357-
this.preload();
358356
}
359357

360358
async eval(

lib/internal/process/esm_loader.js

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const {
44
ArrayIsArray,
5-
ObjectCreate,
5+
ArrayPrototypePushApply,
66
} = primordials;
77

88
const {
@@ -13,6 +13,7 @@ const {
1313
hasUncaughtExceptionCaptureCallback,
1414
} = require('internal/process/execution');
1515
const { pathToFileURL } = require('internal/url');
16+
const { kEmptyObject } = require('internal/util');
1617
const {
1718
getModuleFromWrap,
1819
} = require('internal/vm/module');
@@ -58,15 +59,41 @@ async function initializeLoader() {
5859
const { getOptionValue } = require('internal/options');
5960
const customLoaders = getOptionValue('--experimental-loader');
6061
const preloadModules = getOptionValue('--import');
61-
const loaders = await loadModulesInIsolation(customLoaders);
62+
63+
let cwd;
64+
try {
65+
cwd = process.cwd() + '/';
66+
} catch {
67+
cwd = '/';
68+
}
69+
70+
const internalEsmLoader = new ESMLoader();
71+
const allLoaders = [];
72+
73+
const parentUrl = pathToFileURL(cwd).href;
74+
75+
for (let i = 0; i < customLoaders.length; i++) {
76+
const customLoader = customLoaders[i];
77+
78+
// Importation must be handled by internal loader to avoid polluting user-land
79+
const keyedExportsSublist = await internalEsmLoader.import(
80+
[customLoader],
81+
parentUrl,
82+
kEmptyObject,
83+
);
84+
85+
internalEsmLoader.addCustomLoaders(keyedExportsSublist);
86+
ArrayPrototypePushApply(allLoaders, keyedExportsSublist);
87+
}
6288

6389
// Hooks must then be added to external/public loader
6490
// (so they're triggered in userland)
65-
esmLoader.addCustomLoaders(loaders);
91+
esmLoader.addCustomLoaders(allLoaders);
92+
esmLoader.preload();
6693

6794
// Preload after loaders are added so they can be used
6895
if (preloadModules?.length) {
69-
await loadModulesInIsolation(preloadModules, loaders);
96+
await loadModulesInIsolation(preloadModules, allLoaders);
7097
}
7198

7299
isESMInitialized = true;
@@ -79,20 +106,21 @@ function loadModulesInIsolation(specifiers, loaders = []) {
79106
try {
80107
cwd = process.cwd() + '/';
81108
} catch {
82-
cwd = 'file:///';
109+
cwd = '/';
83110
}
84111

85112
// A separate loader instance is necessary to avoid cross-contamination
86113
// between internal Node.js and userland. For example, a module with internal
87114
// state (such as a counter) should be independent.
88115
const internalEsmLoader = new ESMLoader();
89116
internalEsmLoader.addCustomLoaders(loaders);
117+
internalEsmLoader.preload();
90118

91119
// Importation must be handled by internal loader to avoid poluting userland
92120
return internalEsmLoader.import(
93121
specifiers,
94122
pathToFileURL(cwd).href,
95-
ObjectCreate(null),
123+
kEmptyObject,
96124
);
97125
}
98126

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { spawnPromisified } from '../common/index.mjs';
22
import * as fixtures from '../common/fixtures.mjs';
33
import assert from 'node:assert';
4+
import { relative } from 'node:path';
45
import { execPath } from 'node:process';
56
import { describe, it } from 'node:test';
67

7-
88
const setupArgs = [
99
'--no-warnings',
1010
'--input-type=module',
@@ -253,6 +253,23 @@ describe('ESM: loader chaining', { concurrency: true }, () => {
253253
assert.strictEqual(code, 0);
254254
});
255255

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

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
export async function load(url) {
1+
export async function load(url, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (url.includes('loader')) {
5+
return next(url);
6+
}
7+
28
const val = url.includes('42')
39
? '42'
410
: '"foo"';

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
export async function load() {
1+
export async function load(url, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (url.includes('loader')) {
5+
return next(url);
6+
}
7+
28
return {
39
format: 'module',
410
source: 'export default 42',
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
export async function load(url, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (url.includes('loader')) {
5+
return next(url);
6+
}
7+
28
console.log('load passthru'); // This log is deliberate
39
return next(url);
410
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
export async function resolve(specifier, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (specifier.includes('loader')) {
5+
return next(specifier);
6+
}
7+
28
console.log('resolve 42'); // This log is deliberate
39
console.log('next<HookName>:', next.name); // This log is deliberate
410

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
export async function resolve(specifier, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (specifier.includes('loader')) {
5+
return next(specifier);
6+
}
7+
28
console.log('resolve foo'); // This log is deliberate
39
return next('file:///foo.mjs');
410
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
export async function resolve() {
1+
export async function resolve(specifier, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (specifier.includes('loader')) {
5+
return next(specifier);
6+
}
7+
28
return {
39
url: 'file:///incomplete-resolve-chain.js',
410
};

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
export async function resolve(url, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (url.includes('loader')) {
5+
return next(url);
6+
}
7+
28
const {
39
format,
410
url: nextUrl,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
export async function resolve(specifier, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (specifier.includes('loader')) {
5+
return next(specifier);
6+
}
7+
28
console.log('resolve passthru'); // This log is deliberate
39
return next(specifier);
410
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
export async function resolve(specifier) {
1+
export async function resolve(specifier, context, next) {
2+
// Otherwise we can't use this loader in chaining context, as it would
3+
// overwrite other subsequent loaders
4+
if (specifier.includes('loader')) {
5+
return next(specifier);
6+
}
7+
28
return {
39
shortCircuit: true,
410
url: specifier,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export async function resolve(specifier, context, defaultResolve) {
2+
console.log(`loader-a`, {specifier});
3+
return defaultResolve(specifier.replace(/^xxx\//, `./`));
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export async function resolve(specifier, context, defaultResolve) {
2+
console.log(`loader-b`, {specifier});
3+
return defaultResolve(specifier.replace(/^yyy\//, `./`));
4+
}

0 commit comments

Comments
 (0)