Skip to content

Commit

Permalink
esm: refactor DefaultModuleLoader
Browse files Browse the repository at this point in the history
  • Loading branch information
izaakschroeder committed Jul 14, 2023
1 parent 951da52 commit 9881863
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 149 deletions.
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
56 changes: 28 additions & 28 deletions lib/internal/modules/esm/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {

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


class Hooks {
#chains = {
/**
Expand Down Expand Up @@ -126,16 +125,18 @@ class Hooks {
*/
async register(urlOrSpecifier, parentURL) {
const moduleLoader = require('internal/process/esm_loader').esmLoader;

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

this.addCustomLoader(urlOrSpecifier, keyedExports);
}

allowImportMetaResolve() {
return false;
}

/**
* Collect custom/user-defined module loader hook(s).
* After all hooks have been collected, the global preload hook(s) must be initialized.
Expand All @@ -150,13 +151,16 @@ class Hooks {
} = pluckHooks(exports);

if (globalPreload) {
ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url });
const next = this.#chains.globalPreload[this.#chains.globalPreload.length - 1];
ArrayPrototypePush(this.#chains.globalPreload, { fn: globalPreload, url, next });
}
if (resolve) {
ArrayPrototypePush(this.#chains.resolve, { fn: resolve, url });
const next = this.#chains.resolve[this.#chains.resolve.length - 1];
ArrayPrototypePush(this.#chains.resolve, { 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, { fn: load, url, next });
}
}

Expand Down Expand Up @@ -233,7 +237,6 @@ class Hooks {
chainFinished: null,
context,
hookErrIdentifier: '',
hookIndex: chain.length - 1,
hookName: 'resolve',
shortCircuited: false,
};
Expand All @@ -256,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 @@ -333,6 +336,10 @@ class Hooks {
};
}

resolveSync(_originalSpecifier, _parentURL, _importAssertions) {
throw new Error('`resolveSync` is not supported.');
}

/**
* Provide source that is understood by one of Node's translators.
*
Expand All @@ -349,7 +356,6 @@ class Hooks {
chainFinished: null,
context,
hookErrIdentifier: '',
hookIndex: chain.length - 1,
hookName: 'load',
shortCircuited: false,
};
Expand Down Expand Up @@ -391,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 @@ -528,7 +534,10 @@ class HooksProxy {
debug('wait for signal from worker');
AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0);
const response = this.#worker.receiveMessageSync();
if (response.message.status === 'exit') { return; }
if (response.message.status === 'exit') {
process.exit(response.message.body);
return;
}
const { preloadScripts } = this.#unwrapMessage(response);
this.#executePreloadScripts(preloadScripts);
}
Expand Down Expand Up @@ -684,46 +693,38 @@ 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 first hook in the chain.
* @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 @@ -740,17 +741,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
6 changes: 4 additions & 2 deletions lib/internal/modules/esm/initialize_import_meta.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const { Symbol } = primordials;

const { getOptionValue } = require('internal/options');
const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve');

Expand All @@ -14,7 +16,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 +40,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

0 comments on commit 9881863

Please sign in to comment.