Skip to content

Commit 0a4f7c6

Browse files
izaakschroederRafaelGSS
authored andcommitted
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 #48515 Closes #48439 PR-URL: #48559 Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
1 parent aeac327 commit 0a4f7c6

11 files changed

+265
-158
lines changed

doc/api/errors.md

-17
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

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

lib/internal/modules/esm/hooks.js

+34-28
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,7 +66,7 @@ const {
6566
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
6667
debug = fn;
6768
});
68-
69+
let importMetaInitializer;
6970

7071
/**
7172
* @typedef {object} ExportedHooks
@@ -82,7 +83,6 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
8283

8384
// [2] `validate...()`s throw the wrong error
8485

85-
8686
class Hooks {
8787
#chains = {
8888
/**
@@ -121,20 +121,20 @@ class Hooks {
121121
// Cache URLs we've already validated to avoid repeated validation
122122
#validatedUrls = new SafeSet();
123123

124+
allowImportMetaResolve = false;
125+
124126
/**
125127
* Import and register custom/user-defined module loader hook(s).
126128
* @param {string} urlOrSpecifier
127129
* @param {string} parentURL
128130
*/
129131
async register(urlOrSpecifier, parentURL) {
130132
const moduleLoader = require('internal/process/esm_loader').esmLoader;
131-
132133
const keyedExports = await moduleLoader.import(
133134
urlOrSpecifier,
134135
parentURL,
135136
kEmptyObject,
136137
);
137-
138138
this.addCustomLoader(urlOrSpecifier, keyedExports);
139139
}
140140

@@ -152,13 +152,15 @@ class Hooks {
152152
} = pluckHooks(exports);
153153

154154
if (globalPreload) {
155-
ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url });
155+
ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url });
156156
}
157157
if (resolve) {
158-
ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url });
158+
const next = this.#chains.resolve[this.#chains.resolve.length - 1];
159+
ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next });
159160
}
160161
if (load) {
161-
ArrayPrototypePush(this.#chains.load, { fn: load, url });
162+
const next = this.#chains.load[this.#chains.load.length - 1];
163+
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
162164
}
163165
}
164166

@@ -235,7 +237,6 @@ class Hooks {
235237
chainFinished: null,
236238
context,
237239
hookErrIdentifier: '',
238-
hookIndex: chain.length - 1,
239240
hookName: 'resolve',
240241
shortCircuited: false,
241242
};
@@ -258,7 +259,7 @@ class Hooks {
258259
}
259260
};
260261

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

263264
const resolution = await nextResolve(originalSpecifier, context);
264265
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -335,6 +336,10 @@ class Hooks {
335336
};
336337
}
337338

339+
resolveSync(_originalSpecifier, _parentURL, _importAssertions) {
340+
throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()');
341+
}
342+
338343
/**
339344
* Provide source that is understood by one of Node's translators.
340345
*
@@ -351,7 +356,6 @@ class Hooks {
351356
chainFinished: null,
352357
context,
353358
hookErrIdentifier: '',
354-
hookIndex: chain.length - 1,
355359
hookName: 'load',
356360
shortCircuited: false,
357361
};
@@ -393,7 +397,7 @@ class Hooks {
393397
}
394398
};
395399

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

398402
const loaded = await nextLoad(url, context);
399403
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -468,6 +472,16 @@ class Hooks {
468472
source,
469473
};
470474
}
475+
476+
forceLoadHooks() {
477+
// No-op
478+
}
479+
480+
importMetaInitialize(meta, context, loader) {
481+
importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
482+
meta = importMetaInitializer(meta, context, loader);
483+
return meta;
484+
}
471485
}
472486
ObjectSetPrototypeOf(Hooks.prototype, null);
473487

@@ -717,46 +731,39 @@ function pluckHooks({
717731
* A utility function to iterate through a hook chain, track advancement in the
718732
* chain, and generate and supply the `next<HookName>` argument to the custom
719733
* hook.
720-
* @param {KeyedHook[]} chain The whole hook chain.
734+
* @param {Hook} current The (currently) first hook in the chain (this shifts
735+
* on every call).
721736
* @param {object} meta Properties that change as the current hook advances
722737
* along the chain.
723738
* @param {boolean} meta.chainFinished Whether the end of the chain has been
724739
* reached AND invoked.
725740
* @param {string} meta.hookErrIdentifier A user-facing identifier to help
726741
* 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.
729742
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
730743
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
731744
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
732745
* containing all validation of a custom loader hook's intermediary output. Any
733746
* validation within MUST throw.
734747
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
735748
*/
736-
function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
749+
function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
737750
// First, prepare the current
738751
const { hookName } = meta;
739752
const {
740753
fn: hook,
741754
url: hookFilePath,
742-
} = chain[meta.hookIndex];
755+
next,
756+
} = current;
743757

744758
// ex 'nextResolve'
745759
const nextHookName = `next${
746760
StringPrototypeToUpperCase(hookName[0]) +
747761
StringPrototypeSlice(hookName, 1)
748762
}`;
749763

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;
753764
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 });
765+
if (next) {
766+
nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput });
760767
} else {
761768
// eslint-disable-next-line func-name-matching
762769
nextNextHook = function chainAdvancedTooFar() {
@@ -773,17 +780,16 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
773780

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

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

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

781788
if (context) { // `context` has already been validated, so no fancy check needed.
782789
ObjectAssign(meta.context, context);
783790
}
784791

785792
const output = await hook(arg0, meta.context, nextNextHook);
786-
787793
validateOutput(outputErrIdentifier, output);
788794

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

lib/internal/modules/esm/initialize_import_meta.js

+2-2
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)