Skip to content

Commit c9eb494

Browse files
committed
esm: unflag Module.register and allow nested loader
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/node#48515 Closes nodejs/node#48439 PR-URL: nodejs/node#48559 Backport-PR-URL: nodejs/node#50669 Reviewed-By: Jacob Smith <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent 29f9d1c commit c9eb494

11 files changed

+265
-157
lines changed

graal-nodejs/doc/api/errors.md

-17
Original file line numberDiff line numberDiff line change
@@ -1234,23 +1234,6 @@ provided.
12341234
Encoding provided to `TextDecoder()` API was not one of the
12351235
[WHATWG Supported Encodings][].
12361236

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

12561239
### `ERR_EVAL_ESM_CANNOT_PRINT`

graal-nodejs/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) {

graal-nodejs/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;
@@ -64,7 +65,7 @@ const {
6465
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
6566
debug = fn;
6667
});
67-
68+
let importMetaInitializer;
6869

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

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

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

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

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

153153
if (globalPreload) {
154-
ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url });
154+
ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url });
155155
}
156156
if (resolve) {
157-
ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url });
157+
const next = this.#chains.resolve[this.#chains.resolve.length - 1];
158+
ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next });
158159
}
159160
if (load) {
160-
ArrayPrototypePush(this.#chains.load, { fn: load, url });
161+
const next = this.#chains.load[this.#chains.load.length - 1];
162+
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
161163
}
162164
}
163165

@@ -234,7 +236,6 @@ class Hooks {
234236
chainFinished: null,
235237
context,
236238
hookErrIdentifier: '',
237-
hookIndex: chain.length - 1,
238239
hookName: 'resolve',
239240
shortCircuited: false,
240241
};
@@ -257,7 +258,7 @@ class Hooks {
257258
}
258259
};
259260

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

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

338+
resolveSync(_originalSpecifier, _parentURL, _importAssertions) {
339+
throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()');
340+
}
341+
337342
/**
338343
* Provide source that is understood by one of Node's translators.
339344
*
@@ -350,7 +355,6 @@ class Hooks {
350355
chainFinished: null,
351356
context,
352357
hookErrIdentifier: '',
353-
hookIndex: chain.length - 1,
354358
hookName: 'load',
355359
shortCircuited: false,
356360
};
@@ -392,7 +396,7 @@ class Hooks {
392396
}
393397
};
394398

395-
const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput });
399+
const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
396400

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

@@ -716,46 +730,39 @@ function pluckHooks({
716730
* A utility function to iterate through a hook chain, track advancement in the
717731
* chain, and generate and supply the `next<HookName>` argument to the custom
718732
* hook.
719-
* @param {KeyedHook[]} chain The whole hook chain.
733+
* @param {Hook} current The (currently) first hook in the chain (this shifts
734+
* on every call).
720735
* @param {object} meta Properties that change as the current hook advances
721736
* along the chain.
722737
* @param {boolean} meta.chainFinished Whether the end of the chain has been
723738
* reached AND invoked.
724739
* @param {string} meta.hookErrIdentifier A user-facing identifier to help
725740
* pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
726-
* @param {number} meta.hookIndex A non-negative integer tracking the current
727-
* position in the hook chain.
728741
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
729742
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
730743
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
731744
* containing all validation of a custom loader hook's intermediary output. Any
732745
* validation within MUST throw.
733746
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
734747
*/
735-
function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
748+
function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
736749
// First, prepare the current
737750
const { hookName } = meta;
738751
const {
739752
fn: hook,
740753
url: hookFilePath,
741-
} = chain[meta.hookIndex];
754+
next,
755+
} = current;
742756

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

749-
// When hookIndex is 0, it's reached the default, which does not call next()
750-
// so feed it a noop that blows up if called, so the problem is obvious.
751-
const generatedHookIndex = meta.hookIndex;
752763
let nextNextHook;
753-
if (meta.hookIndex > 0) {
754-
// Now, prepare the next: decrement the pointer so the next call to the
755-
// factory generates the next link in the chain.
756-
meta.hookIndex--;
757-
758-
nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
764+
if (next) {
765+
nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput });
759766
} else {
760767
// eslint-disable-next-line func-name-matching
761768
nextNextHook = function chainAdvancedTooFar() {
@@ -772,17 +779,16 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
772779

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

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

777784
// Set when next<HookName> is actually called, not just generated.
778-
if (generatedHookIndex === 0) { meta.chainFinished = true; }
785+
if (!next) { meta.chainFinished = true; }
779786

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

784791
const output = await hook(arg0, meta.context, nextNextHook);
785-
786792
validateOutput(outputErrIdentifier, output);
787793

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

graal-nodejs/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)