Skip to content

Commit 28055ff

Browse files
esm: remove CLI flag limitation to programmatic registration
1 parent 1da9099 commit 28055ff

4 files changed

Lines changed: 63 additions & 53 deletions

File tree

lib/internal/errors.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,11 +1036,6 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
10361036
}, TypeError);
10371037
E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
10381038
RangeError);
1039-
E('ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE', 'Programmatically registering custom ESM loaders ' +
1040-
'currently requires at least one custom loader to have been registered via the --experimental-loader ' +
1041-
'flag. A no-op loader registered via CLI is sufficient (for example: `--experimental-loader ' +
1042-
'"data:text/javascript,"` with the necessary trailing comma). A future version of Node.js ' +
1043-
'will remove this requirement.', Error);
10441039
E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error);
10451040
E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error);
10461041
E('ERR_FALSY_VALUE_REJECTION', function(reason) {

lib/internal/modules/esm/loader.js

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@ const {
1111
} = primordials;
1212

1313
const {
14-
ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE,
1514
ERR_UNKNOWN_MODULE_FORMAT,
1615
} = require('internal/errors').codes;
1716
const { getOptionValue } = require('internal/options');
1817
const { pathToFileURL } = require('internal/url');
19-
const { emitExperimentalWarning } = require('internal/util');
18+
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
2019
const {
2120
getDefaultConditions,
2221
} = require('internal/modules/esm/utils');
2322
let defaultResolve, defaultLoad, importMetaInitializer;
23+
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
24+
debug = fn;
25+
});
2426

2527
function newModuleMap() {
2628
const ModuleMap = require('internal/modules/esm/module_map');
@@ -106,15 +108,15 @@ class DefaultModuleLoader {
106108
setCallbackForWrap(module, {
107109
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
108110
importModuleDynamically: (specifier, { url }, importAssertions) => {
111+
debug('importModuleDynamically: %o', { specifier, url, Loader: this.constructor.name });
109112
return this.import(specifier, url, importAssertions);
110113
},
111114
});
112115

113116
return module;
114117
};
115118
const ModuleJob = require('internal/modules/esm/module_job');
116-
const job = new ModuleJob(
117-
this, url, undefined, evalInstance, false, false);
119+
const job = new ModuleJob(this, url, undefined, evalInstance, false, false);
118120
this.moduleMap.set(url, undefined, job);
119121
const { module } = await job.run();
120122

@@ -285,10 +287,21 @@ class CustomizedModuleLoader extends DefaultModuleLoader {
285287
/**
286288
* Instantiate a module loader that uses user-provided custom loader hooks.
287289
*/
288-
constructor() {
290+
constructor({
291+
cjsCache,
292+
evalIndex,
293+
moduleMap,
294+
} = kEmptyObject) {
289295
super();
290296

291-
getHooksProxy();
297+
if (cjsCache != null) { this.cjsCache = cjsCache; }
298+
if (evalIndex != null) { this.evalIndex = evalIndex; }
299+
if (moduleMap != null) { this.moduleMap = moduleMap; }
300+
301+
if (!hooksProxy) {
302+
const { HooksProxy } = require('internal/modules/esm/hooks');
303+
hooksProxy = new HooksProxy();
304+
}
292305
}
293306

294307
/**
@@ -357,40 +370,29 @@ let emittedExperimentalWarning = false;
357370
* A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is
358371
* only one used for loading the main module and everything in its dependency graph, though separate instances of this
359372
* class might be instantiated as part of bootstrap for other purposes.
360-
* @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them.
373+
* @param {boolean} forceCustomizedLoaderInMain Ignore whether custom loader(s) have been provided
374+
* via CLI and instantiate a CustomizedModuleLoader instance regardless.
361375
* @returns {DefaultModuleLoader | CustomizedModuleLoader}
362376
*/
363-
function createModuleLoader(useCustomLoadersIfPresent = true) {
364-
if (useCustomLoadersIfPresent &&
365-
// Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader;
366-
// doing so would cause an infinite loop.
367-
!require('internal/modules/esm/utils').isLoaderWorker()) {
368-
const userLoaderPaths = getOptionValue('--experimental-loader');
369-
if (userLoaderPaths.length > 0) {
377+
function createModuleLoader(customizationSetup) {
378+
// Don't spawn a new worker if we're already in a worker thread (doing so would cause an infinite loop).
379+
if (!require('internal/modules/esm/utils').isLoaderWorker()) {
380+
if (
381+
customizationSetup ||
382+
getOptionValue('--experimental-loader').length > 0
383+
) {
370384
if (!emittedExperimentalWarning) {
371385
emitExperimentalWarning('Custom ESM Loaders');
372386
emittedExperimentalWarning = true;
373387
}
374-
return new CustomizedModuleLoader();
388+
debug('instantiating CustomizedModuleLoader');
389+
return new CustomizedModuleLoader(customizationSetup);
375390
}
376391
}
377392

378393
return new DefaultModuleLoader();
379394
}
380395

381-
/**
382-
* Get the HooksProxy instance. If it is not defined, then create a new one.
383-
* @returns {HooksProxy}
384-
*/
385-
function getHooksProxy() {
386-
if (!hooksProxy) {
387-
const { HooksProxy } = require('internal/modules/esm/hooks');
388-
hooksProxy = new HooksProxy();
389-
}
390-
391-
return hooksProxy;
392-
}
393-
394396
/**
395397
* Register a single loader programmatically.
396398
* @param {string} specifier
@@ -405,19 +407,19 @@ function getHooksProxy() {
405407
* ```
406408
*/
407409
function register(specifier, parentURL = 'data:') {
408-
// TODO: Remove this limitation in a follow-up before `register` is released publicly
409-
if (getOptionValue('--experimental-loader').length < 1) {
410-
throw new ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE();
411-
}
410+
let moduleLoader = require('internal/process/esm_loader').esmLoader;
412411

413-
const moduleLoader = require('internal/process/esm_loader').esmLoader;
412+
if (!(moduleLoader instanceof CustomizedModuleLoader)) {
413+
debug('register called on DefaultModuleLoader; switching to CustomizedModuleLoader');
414+
moduleLoader = createModuleLoader(moduleLoader);
415+
require('internal/process/esm_loader').esmLoader = moduleLoader;
416+
}
414417

415418
moduleLoader.register(`${specifier}`, parentURL);
416419
}
417420

418421
module.exports = {
419422
DefaultModuleLoader,
420423
createModuleLoader,
421-
getHooksProxy,
422424
register,
423425
};

lib/internal/process/esm_loader.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,21 @@ const {
1111
} = require('internal/process/execution');
1212
const { pathToFileURL } = require('internal/url');
1313
const { kEmptyObject } = require('internal/util');
14+
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
15+
debug = fn;
16+
});
1417

1518
let esmLoader;
1619

1720
module.exports = {
1821
get esmLoader() {
19-
return esmLoader ??= createModuleLoader(true);
22+
return esmLoader ??= createModuleLoader();
23+
},
24+
set esmLoader(supplantingLoader) {
25+
if (supplantingLoader) esmLoader = supplantingLoader;
2026
},
2127
async loadESM(callback) {
22-
esmLoader ??= createModuleLoader(true);
28+
esmLoader ??= createModuleLoader();
2329
try {
2430
const userImports = getOptionValue('--import');
2531
if (userImports.length > 0) {

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

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -189,19 +189,26 @@ describe('ESM: programmatically register loaders', { concurrency: true }, () =>
189189
assert.strictEqual(lines[3], '');
190190
});
191191

192-
it('does not work without dummy CLI loader', async () => {
193-
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
194-
'--input-type=module',
195-
'--eval',
196-
"import { register } from 'node:module';" +
197-
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) +
198-
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
199-
]);
192+
it('works without a CLI flag', async () => {
193+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
194+
'--no-warnings',
195+
'--input-type=module',
196+
'--eval',
197+
"import { register } from 'node:module';" +
198+
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) +
199+
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
200+
]);
200201

201-
assert.strictEqual(stdout, '');
202-
assert.strictEqual(code, 1);
203-
assert.strictEqual(signal, null);
204-
assert.match(stderr, /ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE/);
202+
assert.strictEqual(stderr, '');
203+
assert.strictEqual(code, 0);
204+
assert.strictEqual(signal, null);
205+
206+
const lines = stdout.split('\n');
207+
208+
assert.match(lines[0], /load passthru/);
209+
assert.match(lines[1], /Hello from dynamic import/);
210+
211+
assert.strictEqual(lines[2], '');
205212
});
206213

207214
it('does not work with a loader specifier that does not exist', async () => {

0 commit comments

Comments
 (0)