Skip to content

Commit ca4f765

Browse files
esm: unflag Module.register and allow nested loader import()
Major functional changes: - Allow `import()` to work within loaders that require other loaders, - Unflag the use of `Module.register`. A new interface `Customizations` has been created in order to unify `ModuleLoader` (previously `DefaultModuleLoader`), `Hooks` and `CustomizedModuleLoader` all of which now implement it: ```ts interface LoadResult { format: ModuleFormat; source: ModuleSource; } interface ResolveResult { format: string; url: URL['href']; } interface Customizations { allowImportMetaResolve: boolean; load(url: string, context: object): Promise<LoadResult> resolve( originalSpecifier: string, parentURL: string, importAssertions: Record<string, string> ): Promise<ResolveResult> resolveSync( originalSpecifier: string, parentURL: string, importAssertions: Record<string, string> ) ResolveResult; register(specifier: string, parentUrl: string): any; forceLoadHooks(): void; importMetaInitialize(meta, context, loader): void; } ``` The `ModuleLoader` class now has `setCustomizations` which takes an object of this shape and delegates its responsibilities to this object if present. Note that two properties `allowImportMetaResolve` and `resolveSync` exist now as a mechanism for `import.meta.resolve` – since `Hooks` does not implement `resolveSync` other loaders cannot use `import.meta.resolve`; `allowImportMetaResolve` is a way of checking for that case instead of invoking `resolveSync` and erroring. Fixes nodejs#48515 Closes nodejs#48439
1 parent 77ea173 commit ca4f765

File tree

11 files changed

+268
-157
lines changed

11 files changed

+268
-157
lines changed

doc/api/errors.md

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,23 +1233,6 @@ provided.
12331233
Encoding provided to `TextDecoder()` API was not one of the
12341234
[WHATWG Supported Encodings][].
12351235

1236-
<a id="ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE"></a>
1237-
1238-
### `ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE`
1239-
1240-
<!-- YAML
1241-
added: REPLACEME
1242-
-->
1243-
1244-
Programmatically registering custom ESM loaders
1245-
currently requires at least one custom loader to have been
1246-
registered via the `--experimental-loader` flag. A no-op
1247-
loader registered via CLI is sufficient
1248-
(for example: `--experimental-loader data:text/javascript,`;
1249-
do not omit the necessary trailing comma).
1250-
A future version of Node.js will support the programmatic
1251-
registration of loaders without needing to also use the flag.
1252-
12531236
<a id="ERR_EVAL_ESM_CANNOT_PRINT"></a>
12541237

12551238
### `ERR_EVAL_ESM_CANNOT_PRINT`

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/hooks.js

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const {
3131
ERR_INVALID_RETURN_PROPERTY_VALUE,
3232
ERR_INVALID_RETURN_VALUE,
3333
ERR_LOADER_CHAIN_INCOMPLETE,
34+
ERR_METHOD_NOT_IMPLEMENTED,
3435
ERR_UNKNOWN_BUILTIN_MODULE,
3536
ERR_WORKER_UNSERIALIZABLE_ERROR,
3637
} = require('internal/errors').codes;
@@ -65,6 +66,8 @@ const {
6566
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
6667
debug = fn;
6768
});
69+
let importMetaInitializer;
70+
6871

6972

7073
/**
@@ -82,7 +85,6 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
8285

8386
// [2] `validate...()`s throw the wrong error
8487

85-
8688
class Hooks {
8789
#chains = {
8890
/**
@@ -121,20 +123,20 @@ class Hooks {
121123
// Cache URLs we've already validated to avoid repeated validation
122124
#validatedUrls = new SafeSet();
123125

126+
allowImportMetaResolve = false;
127+
124128
/**
125129
* Import and register custom/user-defined module loader hook(s).
126130
* @param {string} urlOrSpecifier
127131
* @param {string} parentURL
128132
*/
129133
async register(urlOrSpecifier, parentURL) {
130134
const moduleLoader = require('internal/process/esm_loader').esmLoader;
131-
132135
const keyedExports = await moduleLoader.import(
133136
urlOrSpecifier,
134137
parentURL,
135138
kEmptyObject,
136139
);
137-
138140
this.addCustomLoader(urlOrSpecifier, keyedExports);
139141
}
140142

@@ -152,13 +154,15 @@ class Hooks {
152154
} = pluckHooks(exports);
153155

154156
if (globalPreload) {
155-
ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url });
157+
ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url });
156158
}
157159
if (resolve) {
158-
ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url });
160+
const next = this.#chains.resolve[this.#chains.resolve.length - 1];
161+
ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next });
159162
}
160163
if (load) {
161-
ArrayPrototypePush(this.#chains.load, { fn: load, url });
164+
const next = this.#chains.load[this.#chains.load.length - 1];
165+
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
162166
}
163167
}
164168

@@ -235,7 +239,6 @@ class Hooks {
235239
chainFinished: null,
236240
context,
237241
hookErrIdentifier: '',
238-
hookIndex: chain.length - 1,
239242
hookName: 'resolve',
240243
shortCircuited: false,
241244
};
@@ -258,7 +261,7 @@ class Hooks {
258261
}
259262
};
260263

261-
const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput });
264+
const nextResolve = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
262265

263266
const resolution = await nextResolve(originalSpecifier, context);
264267
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -335,6 +338,10 @@ class Hooks {
335338
};
336339
}
337340

341+
resolveSync(_originalSpecifier, _parentURL, _importAssertions) {
342+
throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()');
343+
}
344+
338345
/**
339346
* Provide source that is understood by one of Node's translators.
340347
*
@@ -351,7 +358,6 @@ class Hooks {
351358
chainFinished: null,
352359
context,
353360
hookErrIdentifier: '',
354-
hookIndex: chain.length - 1,
355361
hookName: 'load',
356362
shortCircuited: false,
357363
};
@@ -393,7 +399,7 @@ class Hooks {
393399
}
394400
};
395401

396-
const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput });
402+
const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
397403

398404
const loaded = await nextLoad(url, context);
399405
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -468,6 +474,16 @@ class Hooks {
468474
source,
469475
};
470476
}
477+
478+
forceLoadHooks() {
479+
// No-op
480+
}
481+
482+
importMetaInitialize(meta, context, loader) {
483+
importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
484+
meta = importMetaInitializer(meta, context, loader);
485+
return meta;
486+
}
471487
}
472488
ObjectSetPrototypeOf(Hooks.prototype, null);
473489

@@ -717,46 +733,39 @@ function pluckHooks({
717733
* A utility function to iterate through a hook chain, track advancement in the
718734
* chain, and generate and supply the `next<HookName>` argument to the custom
719735
* hook.
720-
* @param {KeyedHook[]} chain The whole hook chain.
736+
* @param {Hook} current The (currently) first hook in the chain (this shifts
737+
* on every call).
721738
* @param {object} meta Properties that change as the current hook advances
722739
* along the chain.
723740
* @param {boolean} meta.chainFinished Whether the end of the chain has been
724741
* reached AND invoked.
725742
* @param {string} meta.hookErrIdentifier A user-facing identifier to help
726743
* pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
727-
* @param {number} meta.hookIndex A non-negative integer tracking the current
728-
* position in the hook chain.
729744
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
730745
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
731746
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
732747
* containing all validation of a custom loader hook's intermediary output. Any
733748
* validation within MUST throw.
734749
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
735750
*/
736-
function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
751+
function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
737752
// First, prepare the current
738753
const { hookName } = meta;
739754
const {
740755
fn: hook,
741756
url: hookFilePath,
742-
} = chain[meta.hookIndex];
757+
next,
758+
} = current;
743759

744760
// ex 'nextResolve'
745761
const nextHookName = `next${
746762
StringPrototypeToUpperCase(hookName[0]) +
747763
StringPrototypeSlice(hookName, 1)
748764
}`;
749765

750-
// When hookIndex is 0, it's reached the default, which does not call next()
751-
// so feed it a noop that blows up if called, so the problem is obvious.
752-
const generatedHookIndex = meta.hookIndex;
753766
let nextNextHook;
754-
if (meta.hookIndex > 0) {
755-
// Now, prepare the next: decrement the pointer so the next call to the
756-
// factory generates the next link in the chain.
757-
meta.hookIndex--;
758-
759-
nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
767+
if (next) {
768+
nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput });
760769
} else {
761770
// eslint-disable-next-line func-name-matching
762771
nextNextHook = function chainAdvancedTooFar() {
@@ -773,17 +782,16 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
773782

774783
validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context);
775784

776-
const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`;
785+
const outputErrIdentifier = `${hookFilePath} '${hookName}' hook's ${nextHookName}()`;
777786

778787
// Set when next<HookName> is actually called, not just generated.
779-
if (generatedHookIndex === 0) { meta.chainFinished = true; }
788+
if (!next) { meta.chainFinished = true; }
780789

781790
if (context) { // `context` has already been validated, so no fancy check needed.
782791
ObjectAssign(meta.context, context);
783792
}
784793

785794
const output = await hook(arg0, meta.context, nextNextHook);
786-
787795
validateOutput(outputErrIdentifier, output);
788796

789797
if (output?.shortCircuit === true) { meta.shortCircuited = true; }

lib/internal/modules/esm/initialize_import_meta.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function createImportMetaResolve(defaultParentUrl, loader) {
1414
let url;
1515

1616
try {
17-
({ url } = loader.resolve(specifier, parentUrl));
17+
({ url } = loader.resolveSync(specifier, parentUrl));
1818
} catch (error) {
1919
if (error?.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
2020
({ url } = error);
@@ -38,7 +38,7 @@ function initializeImportMeta(meta, context, loader) {
3838
const { url } = context;
3939

4040
// Alphabetical
41-
if (experimentalImportMetaResolve && loader.loaderType !== 'internal') {
41+
if (experimentalImportMetaResolve && loader.allowImportMetaResolve) {
4242
meta.resolve = createImportMetaResolve(url, loader);
4343
}
4444

0 commit comments

Comments
 (0)