diff --git a/doc/api/errors.md b/doc/api/errors.md
index 3e338a4e8f8cd6..5a8637a0ecc8b0 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1233,23 +1233,6 @@ provided.
Encoding provided to `TextDecoder()` API was not one of the
[WHATWG Supported Encodings][].
-
-
-### `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,`;
-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.
-
### `ERR_EVAL_ESM_CANNOT_PRINT`
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index a632f7dbd870cc..ac2e62147e41d1 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1039,11 +1039,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) {
diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js
index fa34e4b39a5950..85811ad6c0f11c 100644
--- a/lib/internal/modules/esm/hooks.js
+++ b/lib/internal/modules/esm/hooks.js
@@ -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;
@@ -65,7 +66,7 @@ const {
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
-
+let importMetaInitializer;
/**
* @typedef {object} ExportedHooks
@@ -82,7 +83,6 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
// [2] `validate...()`s throw the wrong error
-
class Hooks {
#chains = {
/**
@@ -121,6 +121,8 @@ 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
@@ -128,13 +130,11 @@ 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);
}
@@ -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 });
}
}
@@ -235,7 +237,6 @@ class Hooks {
chainFinished: null,
context,
hookErrIdentifier: '',
- hookIndex: chain.length - 1,
hookName: 'resolve',
shortCircuited: false,
};
@@ -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
@@ -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.
*
@@ -351,7 +356,6 @@ class Hooks {
chainFinished: null,
context,
hookErrIdentifier: '',
- hookIndex: chain.length - 1,
hookName: 'load',
shortCircuited: false,
};
@@ -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
@@ -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);
@@ -717,15 +731,14 @@ function pluckHooks({
* A utility function to iterate through a hook chain, track advancement in the
* chain, and generate and supply the `next` 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
@@ -733,13 +746,14 @@ function pluckHooks({
* validation within MUST throw.
* @returns {function next(...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${
@@ -747,16 +761,9 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
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() {
@@ -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 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; }
diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js
index c548f71bef837a..631231dd9ba7d5 100644
--- a/lib/internal/modules/esm/initialize_import_meta.js
+++ b/lib/internal/modules/esm/initialize_import_meta.js
@@ -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);
@@ -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);
}
diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js
index 9dd032b4227fa2..d1cc9da3c74baf 100644
--- a/lib/internal/modules/esm/loader.js
+++ b/lib/internal/modules/esm/loader.js
@@ -6,12 +6,10 @@ require('internal/modules/cjs/loader');
const {
FunctionPrototypeCall,
ObjectSetPrototypeOf,
- PromisePrototypeThen,
SafeWeakMap,
} = primordials;
const {
- ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE,
ERR_UNKNOWN_MODULE_FORMAT,
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
@@ -52,12 +50,12 @@ let hooksProxy;
* @typedef {ArrayBuffer|TypedArray|string} ModuleSource
*/
-
/**
- * This class covers the default case of an module loader instance where no custom user loaders are used.
- * The below CustomizedModuleLoader class extends this one to support custom user loader hooks.
+ * This class covers the base machinery of module loading. To add custom
+ * behavior you can pass a customizations object and this object will be
+ * used to do the loading/resolving/registration process.
*/
-class DefaultModuleLoader {
+class ModuleLoader {
/**
* The conditions for resolving packages if `--conditions` is not used.
*/
@@ -84,15 +82,85 @@ class DefaultModuleLoader {
translators = getTranslators();
/**
- * Type of loader.
- * @type {'default' | 'internal'}
+ * Truthy to allow the use of `import.meta.resolve`. This is needed
+ * currently because the `Hooks` class does not have `resolveSync`
+ * implemented and `import.meta.resolve` requires it.
*/
- loaderType = 'default';
+ allowImportMetaResolve;
- constructor() {
+ /**
+ * Customizations to pass requests to.
+ *
+ * Note that this value _MUST_ be set with `setCustomizations`
+ * because it needs to copy `customizations.allowImportMetaResolve`
+ * to this property and failure to do so will cause undefined
+ * behavior when invoking `import.meta.resolve`.
+ * @see {ModuleLoader.setCustomizations}
+ */
+ #customizations;
+
+ constructor(customizations) {
if (getOptionValue('--experimental-network-imports')) {
emitExperimentalWarning('Network Imports');
}
+ this.setCustomizations(customizations);
+ }
+
+ /**
+ * Change the currently activate customizations for this module
+ * loader to be the provided `customizations`.
+ *
+ * If present, this class customizes its core functionality to the
+ * `customizations` object, including registration, loading, and resolving.
+ * There are some responsibilities that this class _always_ takes
+ * care of, like validating outputs, so that the customizations object
+ * does not have to do so.
+ *
+ * The customizations object has the shape:
+ *
+ * ```ts
+ * interface LoadResult {
+ * format: ModuleFormat;
+ * source: ModuleSource;
+ * }
+ *
+ * interface ResolveResult {
+ * format: string;
+ * url: URL['href'];
+ * }
+ *
+ * interface Customizations {
+ * allowImportMetaResolve: boolean;
+ * load(url: string, context: object): Promise
+ * resolve(
+ * originalSpecifier:
+ * string, parentURL: string,
+ * importAssertions: Record
+ * ): Promise
+ * resolveSync(
+ * originalSpecifier:
+ * string, parentURL: string,
+ * importAssertions: Record
+ * ) ResolveResult;
+ * register(specifier: string, parentUrl: string): any;
+ * forceLoadHooks(): void;
+ * }
+ * ```
+ *
+ * Note that this class _also_ implements the `Customizations`
+ * interface, as does `CustomizedModuleLoader` and `Hooks`.
+ *
+ * Calling this function alters how modules are loaded and should be
+ * invoked with care.
+ * @param {object} customizations
+ */
+ setCustomizations(customizations) {
+ this.#customizations = customizations;
+ if (customizations) {
+ this.allowImportMetaResolve = customizations.allowImportMetaResolve;
+ } else {
+ this.allowImportMetaResolve = true;
+ }
}
async eval(
@@ -135,17 +203,16 @@ class DefaultModuleLoader {
* point.
* @param {Record} importAssertions Validations for the
* module import.
- * @returns {ModuleJob} The (possibly pending) module job
+ * @returns {Promise} The (possibly pending) module job
*/
- getModuleJob(specifier, parentURL, importAssertions) {
- const resolveResult = this.resolve(specifier, parentURL, importAssertions);
+ async getModuleJob(specifier, parentURL, importAssertions) {
+ const resolveResult = await this.resolve(specifier, parentURL, importAssertions);
return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions);
}
getJobFromResolveResult(resolveResult, parentURL, importAssertions) {
const { url, format } = resolveResult;
const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions;
-
let job = this.moduleMap.get(url, resolvedImportAssertions.type);
// CommonJS will set functions for lazy job evaluation.
@@ -225,11 +292,22 @@ class DefaultModuleLoader {
* @returns {Promise}
*/
async import(specifier, parentURL, importAssertions) {
- const moduleJob = this.getModuleJob(specifier, parentURL, importAssertions);
+ const moduleJob = await this.getModuleJob(specifier, parentURL, importAssertions);
const { module } = await moduleJob.run();
return module.getNamespace();
}
+ register(specifier, parentUrl) {
+ if (!this.#customizations) {
+ // `CustomizedModuleLoader` is defined at the bottom of this file and
+ // available well before this line is ever invoked. This is here in
+ // order to preserve the git diff instead of moving the class.
+ // eslint-disable-next-line no-use-before-define
+ this.setCustomizations(new CustomizedModuleLoader());
+ }
+ return this.#customizations.register(specifier, parentUrl);
+ }
+
/**
* Resolve the location of the module.
* @param {string} originalSpecifier The specified URL path of the module to
@@ -237,9 +315,32 @@ class DefaultModuleLoader {
* @param {string} [parentURL] The URL path of the module's parent.
* @param {ImportAssertions} importAssertions Assertions from the import
* statement or expression.
- * @returns {{ format: string, url: URL['href'] }}
+ * @returns {Promise<{ format: string, url: URL['href'] }>}
*/
resolve(originalSpecifier, parentURL, importAssertions) {
+ if (this.#customizations) {
+ return this.#customizations.resolve(originalSpecifier, parentURL, importAssertions);
+ }
+ return this.defaultResolve(originalSpecifier, parentURL, importAssertions);
+ }
+
+ /**
+ * Just like `resolve` except synchronous. This is here specifically to support
+ * `import.meta.resolve` which must happen synchronously.
+ */
+ resolveSync(originalSpecifier, parentURL, importAssertions) {
+ if (this.#customizations) {
+ return this.#customizations.resolveSync(originalSpecifier, parentURL, importAssertions);
+ }
+ return this.defaultResolve(originalSpecifier, parentURL, importAssertions);
+ }
+
+ /**
+ * Our `defaultResolve` is synchronous and can be used in both
+ * `resolve` and `resolveSync`. This function is here just to avoid
+ * repeating the same code block twice in those functions.
+ */
+ defaultResolve(originalSpecifier, parentURL, importAssertions) {
defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;
const context = {
@@ -260,8 +361,9 @@ class DefaultModuleLoader {
*/
async load(url, context) {
defaultLoad ??= require('internal/modules/esm/load').defaultLoad;
-
- const result = await defaultLoad(url, context);
+ const result = this.#customizations ?
+ await this.#customizations.load(url, context) :
+ await defaultLoad(url, context);
this.validateLoadResult(url, result?.format);
return result;
}
@@ -273,6 +375,9 @@ class DefaultModuleLoader {
}
importMetaInitialize(meta, context) {
+ if (this.#customizations) {
+ return this.#customizations.importMetaInitialize(meta, context, this);
+ }
importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
meta = importMetaInitializer(meta, context, this);
return meta;
@@ -281,18 +386,20 @@ class DefaultModuleLoader {
/**
* No-op when no hooks have been supplied.
*/
- forceLoadHooks() {}
+ forceLoadHooks() {
+ this.#customizations?.forceLoadHooks();
+ }
}
-ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null);
+ObjectSetPrototypeOf(ModuleLoader.prototype, null);
+
+class CustomizedModuleLoader {
+ allowImportMetaResolve = true;
-class CustomizedModuleLoader extends DefaultModuleLoader {
/**
* Instantiate a module loader that uses user-provided custom loader hooks.
*/
constructor() {
- super();
-
getHooksProxy();
}
@@ -318,28 +425,12 @@ class CustomizedModuleLoader extends DefaultModuleLoader {
* @returns {{ format: string, url: URL['href'] }}
*/
resolve(originalSpecifier, parentURL, importAssertions) {
- return hooksProxy.makeSyncRequest('resolve', originalSpecifier, parentURL, importAssertions);
- }
-
- async #getModuleJob(specifier, parentURL, importAssertions) {
- const resolveResult = await hooksProxy.makeAsyncRequest('resolve', specifier, parentURL, importAssertions);
-
- return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions);
+ return hooksProxy.makeAsyncRequest('resolve', originalSpecifier, parentURL, importAssertions);
}
- getModuleJob(specifier, parentURL, importAssertions) {
- const jobPromise = this.#getModuleJob(specifier, parentURL, importAssertions);
- return {
- run() {
- return PromisePrototypeThen(jobPromise, (job) => job.run());
- },
- get modulePromise() {
- return PromisePrototypeThen(jobPromise, (job) => job.modulePromise);
- },
- get linked() {
- return PromisePrototypeThen(jobPromise, (job) => job.linked);
- },
- };
+ resolveSync(originalSpecifier, parentURL, importAssertions) {
+ // This happens only as a result of `import.meta.resolve` calls, which must be sync per spec.
+ return hooksProxy.makeSyncRequest('resolve', originalSpecifier, parentURL, importAssertions);
}
/**
@@ -348,15 +439,12 @@ class CustomizedModuleLoader extends DefaultModuleLoader {
* @param {object} [context] Metadata about the module
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
*/
- async load(url, context) {
- const result = await hooksProxy.makeAsyncRequest('load', url, context);
- this.validateLoadResult(url, result?.format);
-
- return result;
+ load(url, context) {
+ return hooksProxy.makeAsyncRequest('load', url, context);
}
- importMetaInitialize(meta, context) {
- hooksProxy.importMetaInitialize(meta, context, this);
+ importMetaInitialize(meta, context, loader) {
+ hooksProxy.importMetaInitialize(meta, context, loader);
}
forceLoadHooks() {
@@ -364,16 +452,16 @@ class CustomizedModuleLoader extends DefaultModuleLoader {
}
}
-
let emittedExperimentalWarning = false;
/**
* A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is
* only one used for loading the main module and everything in its dependency graph, though separate instances of this
* class might be instantiated as part of bootstrap for other purposes.
* @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them.
- * @returns {DefaultModuleLoader | CustomizedModuleLoader}
+ * @returns {ModuleLoader}
*/
function createModuleLoader(useCustomLoadersIfPresent = true) {
+ let customizations = null;
if (useCustomLoadersIfPresent &&
// Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader;
// doing so would cause an infinite loop.
@@ -384,13 +472,14 @@ function createModuleLoader(useCustomLoadersIfPresent = true) {
emitExperimentalWarning('Custom ESM Loaders');
emittedExperimentalWarning = true;
}
- return new CustomizedModuleLoader();
+ customizations = new CustomizedModuleLoader();
}
}
- return new DefaultModuleLoader();
+ return new ModuleLoader(customizations);
}
+
/**
* Get the HooksProxy instance. If it is not defined, then create a new one.
* @returns {HooksProxy}
@@ -418,18 +507,11 @@ function getHooksProxy() {
* ```
*/
function register(specifier, parentURL = 'data:') {
- // TODO: Remove this limitation in a follow-up before `register` is released publicly
- if (getOptionValue('--experimental-loader').length < 1) {
- throw new ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE();
- }
-
const moduleLoader = require('internal/process/esm_loader').esmLoader;
-
moduleLoader.register(`${specifier}`, parentURL);
}
module.exports = {
- DefaultModuleLoader,
createModuleLoader,
getHooksProxy,
register,
diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js
index 2cf2813a6dcf7f..cb1654812f9d82 100644
--- a/lib/internal/modules/esm/module_job.js
+++ b/lib/internal/modules/esm/module_job.js
@@ -72,8 +72,8 @@ class ModuleJob {
// so that circular dependencies can't cause a deadlock by two of
// these `link` callbacks depending on each other.
const dependencyJobs = [];
- const promises = this.module.link((specifier, assertions) => {
- const job = this.loader.getModuleJob(specifier, url, assertions);
+ const promises = this.module.link(async (specifier, assertions) => {
+ const job = await this.loader.getModuleJob(specifier, url, assertions);
ArrayPrototypePush(dependencyJobs, job);
return job.modulePromise;
});
diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js
index 4e919cd833011c..3da53dd25574e8 100644
--- a/lib/internal/modules/esm/utils.js
+++ b/lib/internal/modules/esm/utils.js
@@ -2,7 +2,6 @@
const {
ArrayIsArray,
- PromisePrototypeThen,
SafeSet,
SafeWeakMap,
ObjectFreeze,
@@ -14,7 +13,6 @@ const {
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const { pathToFileURL } = require('internal/url');
-const { kEmptyObject } = require('internal/util');
const {
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback,
@@ -120,46 +118,17 @@ async function initializeHooks() {
const { Hooks } = require('internal/modules/esm/hooks');
+ const esmLoader = require('internal/process/esm_loader').esmLoader;
+
const hooks = new Hooks();
+ esmLoader.setCustomizations(hooks);
- const { DefaultModuleLoader } = require('internal/modules/esm/loader');
- class ModuleLoader extends DefaultModuleLoader {
- loaderType = 'internal';
- async #getModuleJob(specifier, parentURL, importAssertions) {
- const resolveResult = await hooks.resolve(specifier, parentURL, importAssertions);
- return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions);
- }
- getModuleJob(specifier, parentURL, importAssertions) {
- const jobPromise = this.#getModuleJob(specifier, parentURL, importAssertions);
- return {
- run() {
- return PromisePrototypeThen(jobPromise, (job) => job.run());
- },
- get modulePromise() {
- return PromisePrototypeThen(jobPromise, (job) => job.modulePromise);
- },
- get linked() {
- return PromisePrototypeThen(jobPromise, (job) => job.linked);
- },
- };
- }
- load(url, context) { return hooks.load(url, context); }
- }
- const privateModuleLoader = new ModuleLoader();
const parentURL = pathToFileURL(cwd).href;
-
- // TODO(jlenon7): reuse the `Hooks.register()` method for registering loaders.
for (let i = 0; i < customLoaderURLs.length; i++) {
- const customLoaderURL = customLoaderURLs[i];
-
- // Importation must be handled by internal loader to avoid polluting user-land
- const keyedExports = await privateModuleLoader.import(
- customLoaderURL,
+ await hooks.register(
+ customLoaderURLs[i],
parentURL,
- kEmptyObject,
);
-
- hooks.addCustomLoader(customLoaderURL, keyedExports);
}
const preloadScripts = hooks.initializeGlobalPreload();
diff --git a/test/es-module/test-esm-loader-chaining.mjs b/test/es-module/test-esm-loader-chaining.mjs
index 0f67d71ece0aa4..b43ac740500cd8 100644
--- a/test/es-module/test-esm-loader-chaining.mjs
+++ b/test/es-module/test-esm-loader-chaining.mjs
@@ -470,4 +470,38 @@ describe('ESM: loader chaining', { concurrency: true }, () => {
assert.match(stderr, /'load' hook's nextLoad\(\) context/);
assert.strictEqual(code, 1);
});
+
+ it('should allow loaders to influence subsequent loader `import()` calls in `resolve`', async () => {
+ const { code, stderr, stdout } = await spawnPromisified(
+ execPath,
+ [
+ '--loader',
+ fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
+ '--loader',
+ fixtures.fileURL('es-module-loaders', 'loader-resolve-dynamic-import.mjs'),
+ ...commonArgs,
+ ],
+ { encoding: 'utf8' },
+ );
+ assert.strictEqual(stderr, '');
+ assert.match(stdout, /resolve dynamic import/); // It did go thru resolve-dynamic
+ assert.strictEqual(code, 0);
+ });
+
+ it('should allow loaders to influence subsequent loader `import()` calls in `load`', async () => {
+ const { code, stderr, stdout } = await spawnPromisified(
+ execPath,
+ [
+ '--loader',
+ fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
+ '--loader',
+ fixtures.fileURL('es-module-loaders', 'loader-load-dynamic-import.mjs'),
+ ...commonArgs,
+ ],
+ { encoding: 'utf8' },
+ );
+ assert.strictEqual(stderr, '');
+ assert.match(stdout, /load dynamic import/); // It did go thru load-dynamic
+ assert.strictEqual(code, 0);
+ });
});
diff --git a/test/es-module/test-esm-loader-programmatically.mjs b/test/es-module/test-esm-loader-programmatically.mjs
index 0c20bbcb7519f8..43569e3366d458 100644
--- a/test/es-module/test-esm-loader-programmatically.mjs
+++ b/test/es-module/test-esm-loader-programmatically.mjs
@@ -182,15 +182,19 @@ describe('ESM: programmatically register loaders', { concurrency: true }, () =>
const lines = stdout.split('\n');
+ // Resolve occurs twice because it is first used to resolve the `load` loader
+ // _AND THEN_ the `register` module.
assert.match(lines[0], /resolve passthru/);
- assert.match(lines[1], /load passthru/);
- assert.match(lines[2], /Hello from dynamic import/);
+ assert.match(lines[1], /resolve passthru/);
+ assert.match(lines[2], /load passthru/);
+ assert.match(lines[3], /Hello from dynamic import/);
- assert.strictEqual(lines[3], '');
+ assert.strictEqual(lines[4], '');
});
- it('does not work without dummy CLI loader', async () => {
+ it('works without a CLI flag', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
+ '--no-warnings',
'--input-type=module',
'--eval',
"import { register } from 'node:module';" +
@@ -198,10 +202,16 @@ describe('ESM: programmatically register loaders', { concurrency: true }, () =>
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
- assert.strictEqual(stdout, '');
- assert.strictEqual(code, 1);
+ assert.strictEqual(stderr, '');
+ assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
- assert.match(stderr, /ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE/);
+
+ const lines = stdout.split('\n');
+
+ assert.match(lines[0], /load passthru/);
+ assert.match(lines[1], /Hello from dynamic import/);
+
+ assert.strictEqual(lines[2], '');
});
it('does not work with a loader specifier that does not exist', async () => {
diff --git a/test/fixtures/es-module-loaders/loader-load-dynamic-import.mjs b/test/fixtures/es-module-loaders/loader-load-dynamic-import.mjs
new file mode 100644
index 00000000000000..96af5507d17212
--- /dev/null
+++ b/test/fixtures/es-module-loaders/loader-load-dynamic-import.mjs
@@ -0,0 +1,14 @@
+import { writeSync } from 'node:fs';
+
+
+export async function load(url, context, next) {
+ if (url === 'node:fs' || url.includes('loader')) {
+ return next(url);
+ }
+
+ // Here for asserting dynamic import
+ await import('xxx/loader-load-passthru.mjs');
+
+ writeSync(1, 'load dynamic import' + '\n'); // Signal that this specific hook ran
+ return next(url, context);
+}
diff --git a/test/fixtures/es-module-loaders/loader-resolve-dynamic-import.mjs b/test/fixtures/es-module-loaders/loader-resolve-dynamic-import.mjs
new file mode 100644
index 00000000000000..edc2303ed9aa9e
--- /dev/null
+++ b/test/fixtures/es-module-loaders/loader-resolve-dynamic-import.mjs
@@ -0,0 +1,14 @@
+import { writeSync } from 'node:fs';
+
+
+export async function resolve(specifier, context, next) {
+ if (specifier === 'node:fs' || specifier.includes('loader')) {
+ return next(specifier);
+ }
+
+ // Here for asserting dynamic import
+ await import('xxx/loader-resolve-passthru.mjs');
+
+ writeSync(1, 'resolve dynamic import' + '\n'); // Signal that this specific hook ran
+ return next(specifier);
+}