Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esm: unflag Module.register and allow nested loader import() #48559

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1233,23 +1233,6 @@ provided.
Encoding provided to `TextDecoder()` API was not one of the
[WHATWG Supported Encodings][].

<a id="ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE"></a>

### `ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE`

<!-- YAML
added: REPLACEME
-->

Programmatically registering custom ESM loaders
currently requires at least one custom loader to have been
registered via the `--experimental-loader` flag. A no-op
loader registered via CLI is sufficient
(for example: `--experimental-loader data:text/javascript,`;
do not omit the necessary trailing comma).
A future version of Node.js will support the programmatic
registration of loaders without needing to also use the flag.

<a id="ERR_EVAL_ESM_CANNOT_PRINT"></a>

### `ERR_EVAL_ESM_CANNOT_PRINT`
Expand Down
5 changes: 0 additions & 5 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1036,11 +1036,6 @@ E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) {
}, TypeError);
E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
RangeError);
E('ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE', 'Programmatically registering custom ESM loaders ' +
'currently requires at least one custom loader to have been registered via the --experimental-loader ' +
'flag. A no-op loader registered via CLI is sufficient (for example: `--experimental-loader ' +
'"data:text/javascript,"` with the necessary trailing comma). A future version of Node.js ' +
'will remove this requirement.', Error);
E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error);
E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error);
E('ERR_FALSY_VALUE_REJECTION', function(reason) {
Expand Down
62 changes: 34 additions & 28 deletions lib/internal/modules/esm/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const {
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
ERR_LOADER_CHAIN_INCOMPLETE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_UNKNOWN_BUILTIN_MODULE,
ERR_WORKER_UNSERIALIZABLE_ERROR,
} = require('internal/errors').codes;
Expand Down Expand Up @@ -65,7 +66,7 @@ const {
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});

let importMetaInitializer;

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

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


class Hooks {
#chains = {
/**
Expand Down Expand Up @@ -121,20 +121,20 @@ class Hooks {
// Cache URLs we've already validated to avoid repeated validation
#validatedUrls = new SafeSet();

allowImportMetaResolve = false;

/**
* Import and register custom/user-defined module loader hook(s).
* @param {string} urlOrSpecifier
* @param {string} parentURL
*/
async register(urlOrSpecifier, parentURL) {
const moduleLoader = require('internal/process/esm_loader').esmLoader;

const keyedExports = await moduleLoader.import(
urlOrSpecifier,
parentURL,
kEmptyObject,
);

this.addCustomLoader(urlOrSpecifier, keyedExports);
}

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

if (globalPreload) {
ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url });
ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url });
}
if (resolve) {
ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url });
const next = this.#chains.resolve[this.#chains.resolve.length - 1];
ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next });
}
if (load) {
ArrayPrototypePush(this.#chains.load, { fn: load, url });
const next = this.#chains.load[this.#chains.load.length - 1];
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
}
}

Expand Down Expand Up @@ -235,7 +237,6 @@ class Hooks {
chainFinished: null,
context,
hookErrIdentifier: '',
hookIndex: chain.length - 1,
hookName: 'resolve',
shortCircuited: false,
};
Expand All @@ -258,7 +259,7 @@ class Hooks {
}
};

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

const resolution = await nextResolve(originalSpecifier, context);
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
Expand Down Expand Up @@ -335,6 +336,10 @@ class Hooks {
};
}

resolveSync(_originalSpecifier, _parentURL, _importAssertions) {
throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()');
}

/**
* Provide source that is understood by one of Node's translators.
*
Expand All @@ -351,7 +356,6 @@ class Hooks {
chainFinished: null,
context,
hookErrIdentifier: '',
hookIndex: chain.length - 1,
hookName: 'load',
shortCircuited: false,
};
Expand Down Expand Up @@ -393,7 +397,7 @@ class Hooks {
}
};

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

const loaded = await nextLoad(url, context);
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
Expand Down Expand Up @@ -468,6 +472,16 @@ class Hooks {
source,
};
}

forceLoadHooks() {
// No-op
}

importMetaInitialize(meta, context, loader) {
importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
meta = importMetaInitializer(meta, context, loader);
return meta;
}
}
ObjectSetPrototypeOf(Hooks.prototype, null);

Expand Down Expand Up @@ -717,46 +731,39 @@ function pluckHooks({
* A utility function to iterate through a hook chain, track advancement in the
* chain, and generate and supply the `next<HookName>` argument to the custom
* hook.
* @param {KeyedHook[]} chain The whole hook chain.
* @param {Hook} current The (currently) first hook in the chain (this shifts
* on every call).
* @param {object} meta Properties that change as the current hook advances
* along the chain.
* @param {boolean} meta.chainFinished Whether the end of the chain has been
* reached AND invoked.
* @param {string} meta.hookErrIdentifier A user-facing identifier to help
* pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
* @param {number} meta.hookIndex A non-negative integer tracking the current
* position in the hook chain.
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
* containing all validation of a custom loader hook's intermediary output. Any
* validation within MUST throw.
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
*/
function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
// First, prepare the current
const { hookName } = meta;
const {
fn: hook,
url: hookFilePath,
} = chain[meta.hookIndex];
next,
} = current;

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

// When hookIndex is 0, it's reached the default, which does not call next()
// so feed it a noop that blows up if called, so the problem is obvious.
const generatedHookIndex = meta.hookIndex;
let nextNextHook;
if (meta.hookIndex > 0) {
// Now, prepare the next: decrement the pointer so the next call to the
// factory generates the next link in the chain.
meta.hookIndex--;

nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
if (next) {
nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput });
} else {
// eslint-disable-next-line func-name-matching
nextNextHook = function chainAdvancedTooFar() {
Expand All @@ -773,17 +780,16 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {

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

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

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

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

const output = await hook(arg0, meta.context, nextNextHook);

validateOutput(outputErrIdentifier, output);

if (output?.shortCircuit === true) { meta.shortCircuited = true; }
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/modules/esm/initialize_import_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function createImportMetaResolve(defaultParentUrl, loader) {
let url;

try {
({ url } = loader.resolve(specifier, parentUrl));
({ url } = loader.resolveSync(specifier, parentUrl));
} catch (error) {
if (error?.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
({ url } = error);
Expand All @@ -38,7 +38,7 @@ function initializeImportMeta(meta, context, loader) {
const { url } = context;

// Alphabetical
if (experimentalImportMetaResolve && loader.loaderType !== 'internal') {
if (experimentalImportMetaResolve && loader.allowImportMetaResolve) {
meta.resolve = createImportMetaResolve(url, loader);
}

Expand Down
Loading