Skip to content

Commit 6379f19

Browse files
esm: refactor DefaultModuleLoader
Fixes nodejs#48515 Fixes nodejs#48439
1 parent 951da52 commit 6379f19

10 files changed

+191
-152
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
@@ -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

+32-33
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
8080

8181
// [2] `validate...()`s throw the wrong error
8282

83-
8483
class Hooks {
8584
#chains = {
8685
/**
@@ -126,16 +125,18 @@ class Hooks {
126125
*/
127126
async register(urlOrSpecifier, parentURL) {
128127
const moduleLoader = require('internal/process/esm_loader').esmLoader;
129-
130128
const keyedExports = await moduleLoader.import(
131129
urlOrSpecifier,
132130
parentURL,
133131
kEmptyObject,
134132
);
135-
136133
this.addCustomLoader(urlOrSpecifier, keyedExports);
137134
}
138135

136+
allowImportMetaResolve() {
137+
return false;
138+
}
139+
139140
/**
140141
* Collect custom/user-defined module loader hook(s).
141142
* After all hooks have been collected, the global preload hook(s) must be initialized.
@@ -150,13 +151,16 @@ class Hooks {
150151
} = pluckHooks(exports);
151152

152153
if (globalPreload) {
153-
ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url });
154+
const next = this.#chains.globalPreload[this.#chains.globalPreload.length - 1];
155+
ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url, next });
154156
}
155157
if (resolve) {
156-
ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url });
158+
const next = this.#chains.resolve[this.#chains.resolve.length - 1];
159+
ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url, next });
157160
}
158161
if (load) {
159-
ArrayPrototypePush(this.#chains.load, { fn: load, url });
162+
const next = this.#chains.load[this.#chains.load.length - 1];
163+
ArrayPrototypePush(this.#chains.load, { fn: load, url, next });
160164
}
161165
}
162166

@@ -233,7 +237,6 @@ class Hooks {
233237
chainFinished: null,
234238
context,
235239
hookErrIdentifier: '',
236-
hookIndex: chain.length - 1,
237240
hookName: 'resolve',
238241
shortCircuited: false,
239242
};
@@ -256,7 +259,7 @@ class Hooks {
256259
}
257260
};
258261

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

261264
const resolution = await nextResolve(originalSpecifier, context);
262265
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -349,7 +352,6 @@ class Hooks {
349352
chainFinished: null,
350353
context,
351354
hookErrIdentifier: '',
352-
hookIndex: chain.length - 1,
353355
hookName: 'load',
354356
shortCircuited: false,
355357
};
@@ -391,7 +393,7 @@ class Hooks {
391393
}
392394
};
393395

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

396398
const loaded = await nextLoad(url, context);
397399
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -528,7 +530,17 @@ class HooksProxy {
528530
debug('wait for signal from worker');
529531
AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0);
530532
const response = this.#worker.receiveMessageSync();
531-
if (response.message.status === 'exit') { return; }
533+
if (response.message.status === 'exit') {
534+
// TODO: I do not understand why this is necessary.
535+
// node \
536+
// --no-warnings --experimental-loader 'data:text/javascript,process.exit(42)'
537+
// ./test/fixtures/empty.js
538+
// Does not trigger `this.#worker.on('exit', process.exit);`.
539+
// I think it is because `makeSyncRequest` keeps waiting to see another
540+
// message and blocks the thread from ANY other activity including the exit.
541+
process.exit(response.message.body);
542+
return;
543+
}
532544
const { preloadScripts } = this.#unwrapMessage(response);
533545
this.#executePreloadScripts(preloadScripts);
534546
}
@@ -684,46 +696,34 @@ function pluckHooks({
684696
* A utility function to iterate through a hook chain, track advancement in the
685697
* chain, and generate and supply the `next<HookName>` argument to the custom
686698
* hook.
687-
* @param {KeyedHook[]} chain The whole hook chain.
699+
* @param {Hook} hook The first hook in the chain.
688700
* @param {object} meta Properties that change as the current hook advances
689701
* along the chain.
690702
* @param {boolean} meta.chainFinished Whether the end of the chain has been
691703
* reached AND invoked.
692704
* @param {string} meta.hookErrIdentifier A user-facing identifier to help
693705
* pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
694-
* @param {number} meta.hookIndex A non-negative integer tracking the current
695-
* position in the hook chain.
696706
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
697707
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
698708
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
699709
* containing all validation of a custom loader hook's intermediary output. Any
700710
* validation within MUST throw.
701711
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
702712
*/
703-
function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
713+
function nextHookFactory(hook, meta, { validateArgs, validateOutput }) {
704714
// First, prepare the current
705715
const { hookName } = meta;
706-
const {
707-
fn: hook,
708-
url: hookFilePath,
709-
} = chain[meta.hookIndex];
716+
const { fn, url, next } = hook;
710717

711718
// ex 'nextResolve'
712719
const nextHookName = `next${
713720
StringPrototypeToUpperCase(hookName[0]) +
714721
StringPrototypeSlice(hookName, 1)
715722
}`;
716723

717-
// When hookIndex is 0, it's reached the default, which does not call next()
718-
// so feed it a noop that blows up if called, so the problem is obvious.
719-
const generatedHookIndex = meta.hookIndex;
720724
let nextNextHook;
721-
if (meta.hookIndex > 0) {
722-
// Now, prepare the next: decrement the pointer so the next call to the
723-
// factory generates the next link in the chain.
724-
meta.hookIndex--;
725-
726-
nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
725+
if (next) {
726+
nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput });
727727
} else {
728728
// eslint-disable-next-line func-name-matching
729729
nextNextHook = function chainAdvancedTooFar() {
@@ -736,21 +736,20 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
736736
return ObjectDefineProperty(
737737
async (arg0 = undefined, context) => {
738738
// Update only when hook is invoked to avoid fingering the wrong filePath
739-
meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`;
739+
meta.hookErrIdentifier = `${url} '${hookName}'`;
740740

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

743-
const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`;
743+
const outputErrIdentifier = `${url} '${hookName}' hook's ${nextHookName}()`;
744744

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

748748
if (context) { // `context` has already been validated, so no fancy check needed.
749749
ObjectAssign(meta.context, context);
750750
}
751751

752-
const output = await hook(arg0, meta.context, nextNextHook);
753-
752+
const output = await fn(arg0, meta.context, nextNextHook);
754753
validateOutput(outputErrIdentifier, output);
755754

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

lib/internal/modules/esm/initialize_import_meta.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
'use strict';
22

3+
const { Symbol } = primordials;
4+
35
const { getOptionValue } = require('internal/options');
46
const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve');
7+
const kResolveSync = Symbol('sync');
8+
9+
const importAssertions = {
10+
[kResolveSync]: true,
11+
};
512

613
/**
714
* Generate a function to be used as import.meta.resolve for a particular module.
@@ -14,7 +21,7 @@ function createImportMetaResolve(defaultParentUrl, loader) {
1421
let url;
1522

1623
try {
17-
({ url } = loader.resolve(specifier, parentUrl));
24+
({ url } = loader.resolve(specifier, parentUrl, importAssertions));
1825
} catch (error) {
1926
if (error?.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
2027
({ url } = error);
@@ -38,7 +45,7 @@ function initializeImportMeta(meta, context, loader) {
3845
const { url } = context;
3946

4047
// Alphabetical
41-
if (experimentalImportMetaResolve && loader.loaderType !== 'internal') {
48+
if (experimentalImportMetaResolve && loader.allowImportMetaResolve()) {
4249
meta.resolve = createImportMetaResolve(url, loader);
4350
}
4451

@@ -49,4 +56,5 @@ function initializeImportMeta(meta, context, loader) {
4956

5057
module.exports = {
5158
initializeImportMeta,
59+
kResolveSync,
5260
};

0 commit comments

Comments
 (0)