diff --git a/doc/api/esm.md b/doc/api/esm.md
index c9bb7473a8e689..da3e981f151060 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -1177,18 +1177,40 @@ node --experimental-top-level-await b.mjs # works
To customize the default module resolution, loader hooks can optionally be
-provided via a `--experimental-loader ./loader-name.mjs` argument to Node.js.
-
+provided via `--experimental-loader ./loader-name.mjs` arguments to Node.js.
When hooks are used they only apply to ES module loading and not to any
CommonJS modules loaded.
+Loaders are modules which export functions and data which can be used by
+Node.js to modify the behavior of loading ES Modules. Loaders may be “chained”
+together by passing multiple `--experimental-loader` arguments to Node.js. A
+loader module will be imported once as a singleton.
+
+Loaders follow an authority model, whereby each loader passed to node, from
+left to right, takes precedence over the next. For the `resolve`, `getFormat`,
+and `getSource` hooks, this means that the leftmost hook is called, and then
+the next, and so on, until one gives an answer. For `transformSource`, this
+means the rightmost hook is called first, and the result is passed to the next,
+and so on, until it reaches the leftmost hook. The `getGlobalPreloadCode` hooks
+run from left to right, so that the loaders to the left have more control over
+the environment than loaders to the right.
+
### Hooks
-#### resolve
hook
+#### resolve(specifier, context, nextResolve)
> Note: The loaders API is being redesigned. This hook may disappear or its
> signature may change. Do not rely on the API described below.
+* `specifier` {string}
+* `context` {Object}
+ * `conditions` {string[]}
+ * `parentURL` {string}
+* `nextResolve` {Function}
+* Returns: {Object} If `null` is returned, Node.js will defer to the next
+ loader.
+ * `url` {string}
+
The `resolve` hook returns the resolved file URL for a given module specifier
and parent URL. The module specifier is the string in an `import` statement or
`import()` expression, and the parent URL is the URL of the module that imported
@@ -1199,51 +1221,47 @@ The `conditions` property on the `context` is an array of conditions for
for looking up conditional mappings elsewhere or to modify the list when calling
the default resolution logic.
-The current [package exports conditions][Conditional Exports] will always be in
-the `context.conditions` array passed into the hook. To guarantee _default
-Node.js module specifier resolution behavior_ when calling `defaultResolve`, the
-`context.conditions` array passed to it _must_ include _all_ elements of the
-`context.conditions` array originally passed into the `resolve` hook.
+The [current set of Node.js default conditions][Conditional exports] will always
+be in the `context.conditions` list passed to the hook. If the hook wants to
+ensure Node.js-compatible resolution logic, all items from this default
+condition list **must** be passed through to the next loader function.
```js
-/**
- * @param {string} specifier
- * @param {{
- * parentURL: !(string | undefined),
- * conditions: !(Array),
- * }} context
- * @param {Function} defaultResolve
- * @returns {!(Promise<{ url: string }>)}
- */
-export async function resolve(specifier, context, defaultResolve) {
- const { parentURL = null } = context;
+export async function resolve(specifier, context, nextResolve) {
if (Math.random() > 0.5) { // Some condition.
// For some or all specifiers, do some custom logic for resolving.
// Always return an object of the form {url: }.
return {
- url: parentURL ?
- new URL(specifier, parentURL).href :
+ url: context.parentURL ?
+ new URL(specifier, context.parentURL).href :
new URL(specifier).href,
};
}
if (Math.random() < 0.5) { // Another condition.
- // When calling `defaultResolve`, the arguments can be modified. In this
- // case it's adding another value for matching conditional exports.
- return defaultResolve(specifier, {
+ // When calling to the next loader, the arguments can be modified. In
+ // this case it's adding another value for matching conditional exports.
+ return nextResolve(specifier, {
...context,
conditions: [...context.conditions, 'another-condition'],
});
}
- // Defer to Node.js for all other specifiers.
- return defaultResolve(specifier, context, defaultResolve);
+ // Defer to the next loader for all other specifiers.
+ return null;
}
```
-#### getFormat
hook
+#### getFormat(url, context, nextGetFormat)
> Note: The loaders API is being redesigned. This hook may disappear or its
> signature may change. Do not rely on the API described below.
+* `url` {string}
+* `context` {Object}
+* `nextGetFormat` {Function}
+* Returns: {Object} If `null` is returned, Node.js will defer to the next
+ loader.
+ * `format` {string}
+
The `getFormat` hook provides a way to define a custom method of determining how
a URL should be interpreted. The `format` returned also affects what the
acceptable forms of source values are for a module when parsing. This can be one
@@ -1255,7 +1273,7 @@ of the following:
| `'commonjs'` | Load a Node.js CommonJS module | Not applicable |
| `'json'` | Load a JSON file | { [ArrayBuffer][], [string][], [TypedArray][] } |
| `'module'` | Load an ES module | { [ArrayBuffer][], [string][], [TypedArray][] } |
-| `'wasm'` | Load a WebAssembly module | { [ArrayBuffer][], [string][], [TypedArray][] } |
+| `'wasm'` | Load a WebAssembly module | { [ArrayBuffer][], [TypedArray][] } |
Note: These types all correspond to classes defined in ECMAScript.
@@ -1267,13 +1285,7 @@ Note: If the source value of a text-based format (i.e., `'json'`, `'module'`) is
not a string, it will be converted to a string using [`util.TextDecoder`][].
```js
-/**
- * @param {string} url
- * @param {Object} context (currently empty)
- * @param {Function} defaultGetFormat
- * @returns {Promise<{ format: string }>}
- */
-export async function getFormat(url, context, defaultGetFormat) {
+export async function getFormat(url, context, nextGetFormat) {
if (Math.random() > 0.5) { // Some condition.
// For some or all URLs, do some custom logic for determining format.
// Always return an object of the form {format: }, where the
@@ -1282,28 +1294,30 @@ export async function getFormat(url, context, defaultGetFormat) {
format: 'module',
};
}
- // Defer to Node.js for all other URLs.
- return defaultGetFormat(url, context, defaultGetFormat);
+ // Defer to the next loader for all other URLs.
+ return null;
}
```
-#### getSource
hook
+#### getSource(url, context, nextGetSource)
> Note: The loaders API is being redesigned. This hook may disappear or its
> signature may change. Do not rely on the API described below.
+* `url` {string}
+* `context` {Object}
+ * `format` {string}
+* `nextgetSource` {Object}
+* Returns: {Object} If `null` is returned, Node.js will defer to the next
+ loader.
+ * `source` {string|TypedArray|ArrayBuffer|Buffer}
+
The `getSource` hook provides a way to define a custom method for retrieving
the source code of an ES module specifier. This would allow a loader to
potentially avoid reading files from disk.
```js
-/**
- * @param {string} url
- * @param {{ format: string }} context
- * @param {Function} defaultGetSource
- * @returns {Promise<{ source: !(SharedArrayBuffer | string | Uint8Array) }>}
- */
-export async function getSource(url, context, defaultGetSource) {
+export async function getSource(url, context, nexGetSource) {
const { format } = context;
if (Math.random() > 0.5) { // Some condition.
// For some or all URLs, do some custom logic for retrieving the source.
@@ -1312,16 +1326,24 @@ export async function getSource(url, context, defaultGetSource) {
source: '...',
};
}
- // Defer to Node.js for all other URLs.
- return defaultGetSource(url, context, defaultGetSource);
+ // Defer to the next loader for all other URLs.
+ return null;
}
```
-#### transformSource
hook
+#### transformSource(source, context)
> Note: The loaders API is being redesigned. This hook may disappear or its
> signature may change. Do not rely on the API described below.
+* `source` {string|Buffer}
+* `context` {Object}
+ * `format` {string}
+ * `originalSource` {string|TypedArray|ArrayBuffer|Buffer}
+ * `url` {string}
+* Returns: {Object}
+ * `source` {string|TypedArray|ArrayBuffer|Buffer}
+
The `transformSource` hook provides a way to modify the source code of a loaded
ES module file after the source string has been loaded but before Node.js has
done anything with it.
@@ -1331,17 +1353,7 @@ JavaScript, a resolve hook is also necessary in order to register any
unknown-to-Node.js file extensions. See the [transpiler loader example][] below.
```js
-/**
- * @param {!(SharedArrayBuffer | string | Uint8Array)} source
- * @param {{
- * url: string,
- * format: string,
- * }} context
- * @param {Function} defaultTransformSource
- * @returns {Promise<{ source: !(SharedArrayBuffer | string | Uint8Array) }>}
- */
-export async function transformSource(source, context, defaultTransformSource) {
- const { url, format } = context;
+export async function transformSource(source, { url, format }) {
if (Math.random() > 0.5) { // Some condition.
// For some or all URLs, do some custom logic for modifying the source.
// Always return an object of the form {source: }.
@@ -1349,16 +1361,17 @@ export async function transformSource(source, context, defaultTransformSource) {
source: '...',
};
}
- // Defer to Node.js for all other sources.
- return defaultTransformSource(source, context, defaultTransformSource);
+ return { source };
}
```
-#### getGlobalPreloadCode
hook
+#### getGlobalPreloadCode()
> Note: The loaders API is being redesigned. This hook may disappear or its
> signature may change. Do not rely on the API described below.
+* Returns: {string}
+
Sometimes it can be necessary to run some code inside of the same global scope
that the application will run in. This hook allows to return a string that will
be ran as sloppy-mode script on startup.
@@ -1371,9 +1384,6 @@ If the code needs more advanced `require` features, it will have to construct
its own `require` using `module.createRequire()`.
```js
-/**
- * @returns {string} Code to run before application startup
- */
export function getGlobalPreloadCode() {
return `\
globalThis.someInjectedProperty = 42;
@@ -1405,9 +1415,7 @@ and there is no security.
// https-loader.mjs
import { get } from 'https';
-export function resolve(specifier, context, defaultResolve) {
- const { parentURL = null } = context;
-
+export function resolve(specifier, context, nextResolve) {
// Normally Node.js would error on specifiers starting with 'https://', so
// this hook intercepts them and converts them into absolute URLs to be
// passed along to the later hooks below.
@@ -1415,17 +1423,17 @@ export function resolve(specifier, context, defaultResolve) {
return {
url: specifier
};
- } else if (parentURL && parentURL.startsWith('https://')) {
+ } else if (context.parentURL?.startsWith('https://')) {
return {
url: new URL(specifier, parentURL).href
};
}
// Let Node.js handle all other specifiers.
- return defaultResolve(specifier, context, defaultResolve);
+ return null;
}
-export function getFormat(url, context, defaultGetFormat) {
+export function getFormat(url, context, nextGetFormat) {
// This loader assumes all network-provided JavaScript is ES module code.
if (url.startsWith('https://')) {
return {
@@ -1434,10 +1442,10 @@ export function getFormat(url, context, defaultGetFormat) {
}
// Let Node.js handle all other URLs.
- return defaultGetFormat(url, context, defaultGetFormat);
+ return null;
}
-export function getSource(url, context, defaultGetSource) {
+export function getSource(url, context, nextGetSource) {
// For JavaScript to be loaded over the network, we need to fetch and
// return it.
if (url.startsWith('https://')) {
@@ -1451,7 +1459,7 @@ export function getSource(url, context, defaultGetSource) {
}
// Let Node.js handle all other URLs.
- return defaultGetSource(url, context, defaultGetSource);
+ return null;
}
```
@@ -1492,9 +1500,7 @@ const baseURL = pathToFileURL(`${process.cwd()}/`).href;
// CoffeeScript files end in .coffee, .litcoffee or .coffee.md.
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
-export function resolve(specifier, context, defaultResolve) {
- const { parentURL = baseURL } = context;
-
+export function resolve(specifier, { parentURL = baseURL }) {
// Node.js normally errors on unknown file extensions, so return a URL for
// specifiers ending in the CoffeeScript file extensions.
if (extensionsRegex.test(specifier)) {
@@ -1504,10 +1510,10 @@ export function resolve(specifier, context, defaultResolve) {
}
// Let Node.js handle all other specifiers.
- return defaultResolve(specifier, context, defaultResolve);
+ return null;
}
-export function getFormat(url, context, defaultGetFormat) {
+export function getFormat(url, context, nextGetFormat) {
// Now that we patched resolve to let CoffeeScript URLs through, we need to
// tell Node.js what format such URLs should be interpreted as. For the
// purposes of this loader, all CoffeeScript URLs are ES modules.
@@ -1518,20 +1524,16 @@ export function getFormat(url, context, defaultGetFormat) {
}
// Let Node.js handle all other URLs.
- return defaultGetFormat(url, context, defaultGetFormat);
+ return null;
}
-export function transformSource(source, context, defaultTransformSource) {
- const { url, format } = context;
-
+export function transformSource(source, { url }) {
if (extensionsRegex.test(url)) {
return {
- source: CoffeeScript.compile(source, { bare: true })
+ source: CoffeeScript.compile(source.toString(), { bare: true })
};
}
-
- // Let Node.js handle all other sources.
- return defaultTransformSource(source, context, defaultTransformSource);
+ return { source };
}
```
@@ -1560,6 +1562,18 @@ loaded from disk but before Node.js executes it; and so on for any `.coffee`,
`.litcoffee` or `.coffee.md` files referenced via `import` statements of any
loaded file.
+You could also chain this loader together with the HTTPS loader from above:
+
+```coffee
+# remote.coffee
+import foo from 'https://example.org/resource.coffee'
+console.log foo
+```
+
+```console
+node --experimental-loader ./https-loader.mjs --experimental-loader ./coffeescript-loader.mjs remote.coffee
+```
+
## Resolution algorithm
### Features
@@ -1909,17 +1923,17 @@ success!
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
[`module.createRequire()`]: modules.html#modules_module_createrequire_filename
[`module.syncBuiltinESMExports()`]: modules.html#modules_module_syncbuiltinesmexports
-[`transformSource` hook]: #esm_code_transformsource_code_hook
-[ArrayBuffer]: https://www.ecma-international.org/ecma-262/6.0/#sec-arraybuffer-constructor
+[`transformSource` hook]: #esm_code_transformsource_source_context_code
+[ArrayBuffer]: https://tc39.es/ecma262/#sec-arraybuffer-constructor
[SharedArrayBuffer]: https://tc39.es/ecma262/#sec-sharedarraybuffer-constructor
-[string]: https://www.ecma-international.org/ecma-262/6.0/#sec-string-constructor
-[TypedArray]: https://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects
-[Uint8Array]: https://www.ecma-international.org/ecma-262/6.0/#sec-uint8array
+[string]: https://tc39.es/ecma262/#sec-string-constructor
+[TypedArray]: https://tc39.es/ecma262/#sec-typedarray-objects
+[Uint8Array]: https://tc39.es/ecma262/#sec-uint8array
[`util.TextDecoder`]: util.html#util_class_util_textdecoder
[import an ES or CommonJS module for its side effects only]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Import_a_module_for_its_side_effects_only
[special scheme]: https://url.spec.whatwg.org/#special-scheme
[the full specifier path]: #esm_mandatory_file_extensions
-[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
+[the official standard format]: https://tc39.es/ecma262/#sec-modules
[the dual CommonJS/ES module packages section]: #esm_dual_commonjs_es_module_packages
[transpiler loader example]: #esm_transpiler_loader
[6.1.7 Array Index]: https://tc39.es/ecma262/#integer-index
diff --git a/lib/internal/main/repl.js b/lib/internal/main/repl.js
index a8356687ccedf5..7c38ad6abfa2c2 100644
--- a/lib/internal/main/repl.js
+++ b/lib/internal/main/repl.js
@@ -9,7 +9,8 @@ const {
const esmLoader = require('internal/process/esm_loader');
const {
- evalScript
+ evalScript,
+ uncaughtException,
} = require('internal/process/execution');
const console = require('internal/console/global');
@@ -33,7 +34,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) {
process.exit(1);
}
- esmLoader.loadESM(() => {
+ esmLoader.getLoader().then(() => {
console.log(`Welcome to Node.js ${process.version}.\n` +
'Type ".help" for more information.');
@@ -61,5 +62,7 @@ if (process.env.NODE_REPL_EXTERNAL_MODULE) {
getOptionValue('--inspect-brk'),
getOptionValue('--print'));
}
+ }).catch((e) => {
+ uncaughtException(e, true /* fromPromise */);
});
}
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index de24f6c409c498..ebadaca824fbe1 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -102,6 +102,13 @@ const {
const { validateString } = require('internal/validators');
const pendingDeprecation = getOptionValue('--pending-deprecation');
+const originalModuleExports = new SafeWeakMap();
+module.exports = {
+ wrapSafe, Module, toRealPath, readPackageScope,
+ originalModuleExports,
+ get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; }
+};
+
const {
CHAR_FORWARD_SLASH,
CHAR_BACKWARD_SLASH,
@@ -113,8 +120,6 @@ const {
} = require('internal/util/types');
const asyncESM = require('internal/process/esm_loader');
-const ModuleJob = require('internal/modules/esm/module_job');
-const { ModuleWrap, kInstantiated } = internalBinding('module_wrap');
const {
encodedSepRegEx,
packageInternalResolve
@@ -1119,30 +1124,7 @@ Module.prototype.load = function(filename) {
Module._extensions[extension](this, filename);
this.loaded = true;
- const ESMLoader = asyncESM.ESMLoader;
- const url = `${pathToFileURL(filename)}`;
- const module = ESMLoader.moduleMap.get(url);
- // Create module entry at load time to snapshot exports correctly
- const exports = this.exports;
- // Called from cjs translator
- if (module !== undefined && module.module !== undefined) {
- if (module.module.getStatus() >= kInstantiated)
- module.module.setExport('default', exports);
- } else {
- // Preemptively cache
- // We use a function to defer promise creation for async hooks.
- ESMLoader.moduleMap.set(
- url,
- // Module job creation will start promises.
- // We make it a function to lazily trigger those promises
- // for async hooks compatibility.
- () => new ModuleJob(ESMLoader, url, () =>
- new ModuleWrap(url, undefined, ['default'], function() {
- this.setExport('default', exports);
- })
- , false /* isMain */, false /* inspectBrk */)
- );
- }
+ originalModuleExports.set(this, this.exports);
};
@@ -1176,7 +1158,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier) => {
- const loader = asyncESM.ESMLoader;
+ const loader = await asyncESM.getLoader();
return loader.import(specifier, normalizeReferrerURL(filename));
},
});
@@ -1209,7 +1191,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
const { callbackMap } = internalBinding('module_wrap');
callbackMap.set(compiled.cacheKey, {
importModuleDynamically: async (specifier) => {
- const loader = asyncESM.ESMLoader;
+ const loader = await asyncESM.getLoader();
return loader.import(specifier, normalizeReferrerURL(filename));
}
});
diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js
index 616b2cf52309ea..697e9c5ab7c9e3 100644
--- a/lib/internal/modules/esm/get_format.js
+++ b/lib/internal/modules/esm/get_format.js
@@ -33,7 +33,7 @@ if (experimentalWasmModules)
if (experimentalJsonModules)
extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
-function defaultGetFormat(url, context, defaultGetFormatUnused) {
+function defaultGetFormat(url, context, nextGetFormat) {
if (StringPrototypeStartsWith(url, 'nodejs:')) {
return { format: 'builtin' };
}
diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js
index 37191f65bf0b7e..4ce30b47baa772 100644
--- a/lib/internal/modules/esm/loader.js
+++ b/lib/internal/modules/esm/loader.js
@@ -6,7 +6,6 @@ require('internal/modules/cjs/loader');
const {
FunctionPrototypeBind,
ObjectSetPrototypeOf,
- SafeMap,
} = primordials;
const {
@@ -14,7 +13,7 @@ const {
ERR_INVALID_RETURN_PROPERTY,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
- ERR_UNKNOWN_MODULE_FORMAT
+ ERR_UNKNOWN_MODULE_FORMAT,
} = require('internal/errors').codes;
const { URL, pathToFileURL } = require('internal/url');
const { validateString } = require('internal/validators');
@@ -28,11 +27,30 @@ const {
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { defaultGetSource } = require(
'internal/modules/esm/get_source');
-const { defaultTransformSource } = require(
- 'internal/modules/esm/transform_source');
const { translators } = require(
'internal/modules/esm/translators');
const { getOptionValue } = require('internal/options');
+const {
+ isArrayBufferView,
+ isAnyArrayBuffer,
+} = require('internal/util/types');
+
+let cwd; // Initialized in importLoader
+
+function validateSource(source, hookName, allowString) {
+ if (allowString && typeof source === 'string') {
+ return;
+ }
+ if (isArrayBufferView(source) || isAnyArrayBuffer(source)) {
+ return;
+ }
+ throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
+ `${allowString ? 'string, ' : ''}array buffer, or typed array`,
+ hookName,
+ 'source',
+ source,
+ );
+}
/* 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
@@ -46,33 +64,16 @@ class Loader {
// Registry of loaded modules, akin to `require.cache`
this.moduleMap = new ModuleMap();
- // Map of already-loaded CJS modules to use
- this.cjsCache = new SafeMap();
-
- // This hook is called before the first root module is imported. It's a
- // function that returns a piece of code that runs as a sloppy-mode script.
- // The script may evaluate to a function that can be called with a
- // `getBuiltin` helper that can be used to retrieve builtins.
- // If the hook returns `null` instead of a source string, it opts out of
- // running any preload code.
- // The preload code runs as soon as the hook module has finished evaluating.
- this._getGlobalPreloadCode = null;
- // The resolver has the signature
- // (specifier : string, parentURL : string, defaultResolve)
- // -> Promise<{ url : string }>
- // where defaultResolve is ModuleRequest.resolve (having the same
- // signature itself).
+ // Preload code is provided by loaders to be run after hook initialization.
+ this.globalPreloadCode = [];
+ // Loader resolve hook.
this._resolve = defaultResolve;
- // This hook is called after the module is resolved but before a translator
- // is chosen to load it; the format returned by this function is the name
- // of a translator.
+ // Loader getFormat hook.
this._getFormat = defaultGetFormat;
- // This hook is called just before the source code of an ES module file
- // is loaded.
+ // Loader getSource hook.
this._getSource = defaultGetSource;
- // This hook is called just after the source code of an ES module file
- // is loaded, but before anything is done with the string.
- this._transformSource = defaultTransformSource;
+ // Transform source hooks.
+ this.transformSourceHooks = [];
// The index for assigning unique URLs to anonymous module evaluation
this.evalIndex = 0;
}
@@ -83,7 +84,7 @@ class Loader {
validateString(parentURL, 'parentURL');
const resolveResponse = await this._resolve(
- specifier, { parentURL, conditions: DEFAULT_CONDITIONS }, defaultResolve);
+ specifier, { parentURL, conditions: DEFAULT_CONDITIONS });
if (typeof resolveResponse !== 'object') {
throw new ERR_INVALID_RETURN_VALUE(
'object', 'loader resolve', resolveResponse);
@@ -98,8 +99,7 @@ class Loader {
}
async getFormat(url) {
- const getFormatResponse = await this._getFormat(
- url, {}, defaultGetFormat);
+ const getFormatResponse = await this._getFormat(url, {});
if (typeof getFormatResponse !== 'object') {
throw new ERR_INVALID_RETURN_VALUE(
'object', 'loader getFormat', getFormatResponse);
@@ -137,6 +137,22 @@ class Loader {
return format;
}
+ async getSource(url, format) {
+ const { source: originalSource } = await this._getSource(url, { format });
+
+ const allowString = format !== 'wasm';
+ validateSource(originalSource, 'getSource', allowString);
+
+ let source = originalSource;
+ for (let i = 0; i < this.transformSourceHooks.length; i += 1) {
+ const hook = this.transformSourceHooks[i];
+ ({ source } = await hook(source, { url, format, originalSource }));
+ validateSource(source, 'transformSource', allowString);
+ }
+
+ return source;
+ }
+
async eval(
source,
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href
@@ -166,72 +182,84 @@ class Loader {
return module.getNamespace();
}
+ async importLoader(specifier) {
+ if (cwd === undefined) {
+ try {
+ // `process.cwd()` can fail.
+ cwd = process.cwd() + '/';
+ } catch {
+ cwd = 'file:///';
+ }
+ cwd = pathToFileURL(cwd).href;
+ }
+
+ const { url } = await defaultResolve(specifier, cwd,
+ { conditions: DEFAULT_CONDITIONS });
+ const { format } = await defaultGetFormat(url, {});
+
+ // !!! CRITICAL SECTION !!!
+ // NO AWAIT OPS BETWEEN HERE AND SETTING JOB IN MODULE MAP!
+ // YIELDING CONTROL COULD RESULT IN MAP BEING OVERRIDDEN!
+ let job = this.moduleMap.get(url);
+ if (job === undefined) {
+ if (!translators.has(format))
+ throw new ERR_UNKNOWN_MODULE_FORMAT(format);
+
+ const loaderInstance = translators.get(format);
+
+ job = new ModuleJob(this, url, loaderInstance, false, false);
+ this.moduleMap.set(url, job);
+ // !!! END CRITICAL SECTION !!!
+ }
+
+ const { module } = await job.run();
+ return module.getNamespace();
+ }
+
hook(hooks) {
const {
resolve,
- dynamicInstantiate,
getFormat,
getSource,
- transformSource,
- getGlobalPreloadCode,
} = hooks;
// Use .bind() to avoid giving access to the Loader instance when called.
if (resolve !== undefined)
this._resolve = FunctionPrototypeBind(resolve, null);
- if (dynamicInstantiate !== undefined) {
- process.emitWarning(
- 'The dynamicInstantiate loader hook has been removed.');
- }
if (getFormat !== undefined) {
this._getFormat = FunctionPrototypeBind(getFormat, null);
}
if (getSource !== undefined) {
this._getSource = FunctionPrototypeBind(getSource, null);
}
- if (transformSource !== undefined) {
- this._transformSource = FunctionPrototypeBind(transformSource, null);
- }
- if (getGlobalPreloadCode !== undefined) {
- this._getGlobalPreloadCode =
- FunctionPrototypeBind(getGlobalPreloadCode, null);
- }
}
runGlobalPreloadCode() {
- if (!this._getGlobalPreloadCode) {
- return;
- }
- const preloadCode = this._getGlobalPreloadCode();
- if (preloadCode === null) {
- return;
- }
+ for (let i = 0; i < this.globalPreloadCode.length; i += 1) {
+ const preloadCode = this.globalPreloadCode[i];
- if (typeof preloadCode !== 'string') {
- throw new ERR_INVALID_RETURN_VALUE(
- 'string', 'loader getGlobalPreloadCode', preloadCode);
+ const { compileFunction } = require('vm');
+ const preloadInit = compileFunction(preloadCode, ['getBuiltin'], {
+ filename: '',
+ });
+ const { NativeModule } = require('internal/bootstrap/loaders');
+
+ preloadInit.call(globalThis, (builtinName) => {
+ if (NativeModule.canBeRequiredByUsers(builtinName)) {
+ return require(builtinName);
+ }
+ throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
+ });
}
- const { compileFunction } = require('vm');
- const preloadInit = compileFunction(preloadCode, ['getBuiltin'], {
- filename: '',
- });
- const { NativeModule } = require('internal/bootstrap/loaders');
-
- preloadInit.call(globalThis, (builtinName) => {
- if (NativeModule.canBeRequiredByUsers(builtinName)) {
- return require(builtinName);
- }
- throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
- });
}
async getModuleJob(specifier, parentURL) {
const url = await this.resolve(specifier, parentURL);
const format = await this.getFormat(url);
+
+ // !!! CRITICAL SECTION !!!
+ // NO AWAIT OPS BETWEEN HERE AND SETTING JOB IN MODULE MAP
let job = this.moduleMap.get(url);
- // CommonJS will set functions for lazy job evaluation.
- if (typeof job === 'function')
- this.moduleMap.set(url, job = job());
if (job !== undefined)
return job;
@@ -245,6 +273,8 @@ class Loader {
job = new ModuleJob(this, url, loaderInstance, parentURL === undefined,
inspectBrk);
this.moduleMap.set(url, job);
+ // !!! END CRITICAL SECTION !!!
+
return job;
}
}
diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js
index 7ea59f30c6894e..b424370f67dfb3 100644
--- a/lib/internal/modules/esm/resolve.js
+++ b/lib/internal/modules/esm/resolve.js
@@ -764,8 +764,7 @@ function resolveAsCommonJS(specifier, parentURL) {
}
}
-function defaultResolve(specifier, context = {}, defaultResolveUnused) {
- let { parentURL, conditions } = context;
+function defaultResolve(specifier, { parentURL, conditions } = {}) {
let parsed;
try {
parsed = new URL(specifier);
diff --git a/lib/internal/modules/esm/transform_source.js b/lib/internal/modules/esm/transform_source.js
deleted file mode 100644
index 2d07dd3607fb66..00000000000000
--- a/lib/internal/modules/esm/transform_source.js
+++ /dev/null
@@ -1,7 +0,0 @@
-'use strict';
-
-function defaultTransformSource(source, { url, format } = {},
- defaultTransformSource) {
- return { source };
-}
-exports.defaultTransformSource = defaultTransformSource;
diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js
index bb3528eddde964..a7085402a17356 100644
--- a/lib/internal/modules/esm/translators.js
+++ b/lib/internal/modules/esm/translators.js
@@ -11,22 +11,15 @@ const {
StringPrototypeReplace,
} = primordials;
-let _TYPES = null;
-function lazyTypes() {
- if (_TYPES !== null) return _TYPES;
- return _TYPES = require('internal/util/types');
-}
-
const {
stripBOM,
loadNativeModule
} = require('internal/modules/cjs/helpers');
-const CJSModule = require('internal/modules/cjs/loader').Module;
+const {
+ Module: CJSModule,
+ originalModuleExports: originalCJSModuleExports,
+} = require('internal/modules/cjs/loader');
const internalURLModule = require('internal/url');
-const { defaultGetSource } = require(
- 'internal/modules/esm/get_source');
-const { defaultTransformSource } = require(
- 'internal/modules/esm/transform_source');
const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');
const { fileURLToPath, URL } = require('url');
@@ -36,7 +29,6 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
const { emitExperimentalWarning } = require('internal/util');
const {
ERR_UNKNOWN_BUILTIN_MODULE,
- ERR_INVALID_RETURN_PROPERTY_VALUE
} = require('internal/errors').codes;
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
const moduleWrap = internalBinding('module_wrap');
@@ -48,30 +40,6 @@ const experimentalImportMetaResolve =
const translators = new SafeMap();
exports.translators = translators;
-let DECODER = null;
-function assertBufferSource(body, allowString, hookName) {
- if (allowString && typeof body === 'string') {
- return;
- }
- const { isArrayBufferView, isAnyArrayBuffer } = lazyTypes();
- if (isArrayBufferView(body) || isAnyArrayBuffer(body)) {
- return;
- }
- throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
- `${allowString ? 'string, ' : ''}array buffer, or typed array`,
- hookName,
- 'source',
- body
- );
-}
-
-function stringify(body) {
- if (typeof body === 'string') return body;
- assertBufferSource(body, false, 'transformSource');
- DECODER = DECODER === null ? new TextDecoder() : DECODER;
- return DECODER.decode(body);
-}
-
function errPath(url) {
const parsed = new URL(url);
if (parsed.protocol === 'file:') {
@@ -80,10 +48,21 @@ function errPath(url) {
return url;
}
+let DECODER;
+function stringify(source) {
+ if (typeof source === 'string') {
+ return source;
+ }
+ if (DECODER === undefined) {
+ DECODER = new TextDecoder();
+ }
+ return DECODER.decode(source);
+}
+
let esmLoader;
async function importModuleDynamically(specifier, { url }) {
if (!esmLoader) {
- esmLoader = require('internal/process/esm_loader').ESMLoader;
+ esmLoader = await require('internal/process/esm_loader').getLoader();
}
return esmLoader.import(specifier, url);
}
@@ -91,7 +70,7 @@ async function importModuleDynamically(specifier, { url }) {
function createImportMetaResolve(defaultParentUrl) {
return async function resolve(specifier, parentUrl = defaultParentUrl) {
if (!esmLoader) {
- esmLoader = require('internal/process/esm_loader').ESMLoader;
+ esmLoader = await require('internal/process/esm_loader').getLoader();
}
return PromisePrototypeCatch(
esmLoader.resolve(specifier, parentUrl),
@@ -109,14 +88,20 @@ function initializeImportMeta(meta, { url }) {
meta.url = url;
}
+function requireOriginal(filename) {
+ const resolved = isWindows ?
+ StringPrototypeReplace(filename, winSepRegEx, '\\') : filename;
+ if (!CJSModule._cache[resolved]) {
+ CJSModule._load(filename, undefined, false);
+ }
+ const module = CJSModule._cache[resolved];
+ return originalCJSModuleExports.get(module);
+}
+
// Strategy for loading a standard JavaScript module
translators.set('module', async function moduleStrategy(url) {
- let { source } = await this._getSource(
- url, { format: 'module' }, defaultGetSource);
- assertBufferSource(source, true, 'getSource');
- ({ source } = await this._transformSource(
- source, { url, format: 'module' }, defaultTransformSource));
- source = stringify(source);
+ const source = stringify(await this.getSource(url, 'module'));
+
maybeCacheSourceMap(url, source);
debug(`Translating StandardModule ${url}`);
const module = new ModuleWrap(url, undefined, source, 0, 0);
@@ -133,26 +118,10 @@ const winSepRegEx = /\//g;
translators.set('commonjs', function commonjsStrategy(url, isMain) {
debug(`Translating CJSModule ${url}`);
const pathname = internalURLModule.fileURLToPath(new URL(url));
- const cached = this.cjsCache.get(url);
- if (cached) {
- this.cjsCache.delete(url);
- return cached;
- }
- const module = CJSModule._cache[
- isWindows ? StringPrototypeReplace(pathname, winSepRegEx, '\\') : pathname
- ];
- if (module && module.loaded) {
- const exports = module.exports;
- return new ModuleWrap(url, undefined, ['default'], function() {
- this.setExport('default', exports);
- });
- }
return new ModuleWrap(url, undefined, ['default'], function() {
debug(`Loading CJSModule ${url}`);
- // We don't care about the return val of _load here because Module#load
- // will handle it for us by checking the loader registry and filling the
- // exports like above
- CJSModule._load(pathname, undefined, isMain);
+ const exports = requireOriginal(pathname);
+ this.setExport('default', exports);
});
});
@@ -189,12 +158,9 @@ translators.set('json', async function jsonStrategy(url) {
});
}
}
- let { source } = await this._getSource(
- url, { format: 'json' }, defaultGetSource);
- assertBufferSource(source, true, 'getSource');
- ({ source } = await this._transformSource(
- source, { url, format: 'json' }, defaultTransformSource));
- source = stringify(source);
+
+ const source = stringify(await this.getSource(url, 'json'));
+
if (pathname) {
// A require call could have been called on the same file during loading and
// that resolves synchronously. To make sure we always return the identical
@@ -207,6 +173,7 @@ translators.set('json', async function jsonStrategy(url) {
});
}
}
+
try {
const exports = JSONParse(stripBOM(source));
module = {
@@ -221,9 +188,11 @@ translators.set('json', async function jsonStrategy(url) {
err.message = errPath(url) + ': ' + err.message;
throw err;
}
+
if (pathname) {
CJSModule._cache[modulePath] = module;
}
+
return new ModuleWrap(url, undefined, ['default'], function() {
debug(`Parsing JSONModule ${url}`);
this.setExport('default', module.exports);
@@ -233,12 +202,7 @@ translators.set('json', async function jsonStrategy(url) {
// Strategy for loading a wasm module
translators.set('wasm', async function(url) {
emitExperimentalWarning('Importing Web Assembly modules');
- let { source } = await this._getSource(
- url, { format: 'wasm' }, defaultGetSource);
- assertBufferSource(source, false, 'getSource');
- ({ source } = await this._transformSource(
- source, { url, format: 'wasm' }, defaultTransformSource));
- assertBufferSource(source, false, 'transformSource');
+ const source = await this.getSource(url, 'wasm');
debug(`Translating WASMModule ${url}`);
let compiled;
try {
diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js
index 0967ef539ca20e..cc494fea523fb3 100644
--- a/lib/internal/modules/run_main.js
+++ b/lib/internal/modules/run_main.js
@@ -21,8 +21,8 @@ function resolveMainPath(main) {
}
function shouldUseESMLoader(mainPath) {
- const userLoader = getOptionValue('--experimental-loader');
- if (userLoader)
+ const userLoaders = getOptionValue('--experimental-loader');
+ if (userLoaders.length > 0)
return true;
const esModuleSpecifierResolution =
getOptionValue('--es-module-specifier-resolution');
@@ -38,12 +38,15 @@ function shouldUseESMLoader(mainPath) {
}
function runMainESM(mainPath) {
- const esmLoader = require('internal/process/esm_loader');
+ const { getLoader } = require('internal/process/esm_loader');
const { pathToFileURL } = require('internal/url');
- esmLoader.loadESM((ESMLoader) => {
+ const { uncaughtException } = require('internal/process/execution');
+ return getLoader().then((loader) => {
const main = path.isAbsolute(mainPath) ?
pathToFileURL(mainPath).href : mainPath;
- return ESMLoader.import(main);
+ return loader.import(main);
+ }).catch((e) => {
+ uncaughtException(e, true /* fromPromise */);
});
}
diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js
index 8f076b3ef32efd..d839dab98729be 100644
--- a/lib/internal/process/esm_loader.js
+++ b/lib/internal/process/esm_loader.js
@@ -1,16 +1,21 @@
'use strict';
+const {
+ ArrayPrototypePush,
+ ReflectApply,
+} = primordials;
+
const {
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
} = require('internal/errors').codes;
const { Loader } = require('internal/modules/esm/loader');
-const {
- hasUncaughtExceptionCaptureCallback,
-} = require('internal/process/execution');
-const { pathToFileURL } = require('internal/url');
const {
getModuleFromWrap,
} = require('internal/vm/module');
+const {
+ validateString,
+ validateFunction,
+} = require('internal/validators');
exports.initializeImportMetaObject = function(wrap, meta) {
const { callbackMap } = internalBinding('module_wrap');
@@ -34,46 +39,93 @@ exports.importModuleDynamicallyCallback = async function(wrap, specifier) {
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
};
-let ESMLoader = new Loader();
-exports.ESMLoader = ESMLoader;
+const createLoader = async () => {
+ const loader = new Loader();
-async function initializeLoader() {
const { getOptionValue } = require('internal/options');
- const userLoader = getOptionValue('--experimental-loader');
- if (!userLoader)
- return;
- let cwd;
- try {
- cwd = process.cwd() + '/';
- } catch {
- cwd = 'file:///';
- }
- // If --experimental-loader is specified, create a loader with user hooks.
- // Otherwise create the default loader.
- const { emitExperimentalWarning } = require('internal/util');
- emitExperimentalWarning('--experimental-loader');
- return (async () => {
- const hooks =
- await ESMLoader.import(userLoader, pathToFileURL(cwd).href);
- ESMLoader = new Loader();
- ESMLoader.hook(hooks);
- ESMLoader.runGlobalPreloadCode();
- return exports.ESMLoader = ESMLoader;
- })();
-}
-
-exports.loadESM = async function loadESM(callback) {
- try {
- await initializeLoader();
- await callback(ESMLoader);
- } catch (err) {
- if (hasUncaughtExceptionCaptureCallback()) {
- process._fatalException(err);
- return;
+ const userLoaders = getOptionValue('--experimental-loader');
+
+ if (userLoaders.length > 0) {
+ const { emitExperimentalWarning } = require('internal/util');
+ emitExperimentalWarning('--experimental-loader');
+
+ const importedLoaders = [];
+ for (let i = 0; i < userLoaders.length; i += 1) {
+ const ns = await loader.importLoader(userLoaders[i]);
+
+ if (ns.resolve !== undefined) {
+ validateFunction(ns.resolve, 'resolve');
+ }
+ if (ns.getFormat !== undefined) {
+ validateFunction(ns.getFormat, 'getFormat');
+ }
+ if (ns.getSource !== undefined) {
+ validateFunction(ns.getSource, 'getSource');
+ }
+ if (ns.transformSource !== undefined) {
+ validateFunction(ns.transformSource, 'transformSource');
+ ArrayPrototypePush(loader.transformSourceHooks, ns.transformSource);
+ }
+
+ if (ns.getGlobalPreloadCode !== undefined) {
+ validateFunction(ns.getGlobalPreloadCode, 'getGlobalPreloadCode');
+ const code = ReflectApply(ns.getGlobalPreloadCode, undefined, []);
+ validateString(code, 'getGlobalPreloadCode return value');
+ ArrayPrototypePush(loader.globalPreloadCode, code);
+ }
+
+ importedLoaders.push(ns);
}
- internalBinding('errors').triggerUncaughtException(
- err,
- true /* fromPromise */
- );
+
+ let cursor = {
+ resolve: loader._resolve,
+ getFormat: loader._getFormat,
+ getSource: loader._getSource,
+ };
+ for (let i = userLoaders.length - 1; i >= 0; i -= 1) {
+ const {
+ resolve,
+ getFormat,
+ getSource,
+ } = importedLoaders[i];
+
+ const nextLoader = cursor;
+ cursor = {
+ resolve: resolve ? async (...args) => {
+ const result = await resolve(...args, nextLoader.resolve);
+ if (result === null) {
+ return nextLoader.resolve(...args);
+ }
+ return result;
+ } : nextLoader.resolve,
+ getFormat: getFormat ? async (...args) => {
+ const result = await getFormat(...args, nextLoader.getFormat);
+ if (result === null) {
+ return nextLoader.getFormat(...args);
+ }
+ return result;
+ } : nextLoader.getFormat,
+ getSource: getSource ? async (...args) => {
+ const result = await getSource(...args, nextLoader.getSource);
+ if (result === null) {
+ return nextLoader.getSource(...args);
+ }
+ return result;
+ } : nextLoader.getSource,
+ };
+ }
+
+ loader.hook(cursor);
+ loader.runGlobalPreloadCode();
+ }
+
+ return loader;
+};
+
+let processLoader;
+exports.getLoader = () => {
+ if (!processLoader) {
+ processLoader = createLoader();
}
+ return processLoader;
};
diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js
index 08d6ec8c6ea906..5d250cf1ed8d47 100644
--- a/lib/internal/process/execution.js
+++ b/lib/internal/process/execution.js
@@ -2,7 +2,6 @@
const {
JSONStringify,
- PromiseResolve,
} = primordials;
const path = require('path');
@@ -46,7 +45,7 @@ function evalModule(source, print) {
const { log, error } = require('internal/console/global');
const { decorateErrorStack } = require('internal/util');
const asyncESM = require('internal/process/esm_loader');
- PromiseResolve(asyncESM.ESMLoader).then(async (loader) => {
+ asyncESM.getLoader().then(async (loader) => {
const { result } = await loader.eval(source);
if (print) {
log(result);
@@ -91,7 +90,7 @@ function evalScript(name, body, breakFirstLine, print) {
displayErrors: true,
[kVmBreakFirstLineSymbol]: ${!!breakFirstLine},
async importModuleDynamically (specifier) {
- const loader = await asyncESM.ESMLoader;
+ const loader = await asyncESM.getLoader();
return loader.import(specifier, ${JSONStringify(baseUrl)});
}
});\n`;
@@ -212,6 +211,14 @@ function readStdin(callback) {
});
}
+function uncaughtException(error, fromPromise = false) {
+ if (hasUncaughtExceptionCaptureCallback()) {
+ process._fatalException(error);
+ return;
+ }
+ internalBinding('errors').triggerUncaughtException(error, fromPromise);
+}
+
module.exports = {
readStdin,
tryGetCwd,
@@ -219,5 +226,6 @@ module.exports = {
evalScript,
onGlobalUncaughtException: createOnGlobalUncaughtException(),
setUncaughtExceptionCaptureCallback,
- hasUncaughtExceptionCaptureCallback
+ hasUncaughtExceptionCaptureCallback,
+ uncaughtException,
};
diff --git a/lib/internal/validators.js b/lib/internal/validators.js
index deab53d4b8221e..68ddf5447a9f7b 100644
--- a/lib/internal/validators.js
+++ b/lib/internal/validators.js
@@ -131,6 +131,11 @@ function validateBoolean(value, name) {
throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value);
}
+function validateFunction(value, name) {
+ if (typeof value !== 'function')
+ throw new ERR_INVALID_ARG_TYPE(name, 'function', value);
+}
+
const validateObject = hideStackFrames(
(value, name, { nullable = false } = {}) => {
if ((!nullable && value === null) ||
@@ -208,6 +213,7 @@ module.exports = {
validateBoolean,
validateBuffer,
validateEncoding,
+ validateFunction,
validateInt32,
validateInteger,
validateNumber,
diff --git a/lib/repl.js b/lib/repl.js
index cc5bf069c31295..9a35e832adf67e 100644
--- a/lib/repl.js
+++ b/lib/repl.js
@@ -399,7 +399,8 @@ function REPLServer(prompt,
filename: file,
displayErrors: true,
importModuleDynamically: async (specifier) => {
- return asyncESM.ESMLoader.import(specifier, parentURL);
+ const loader = await asyncESM.getLoader();
+ return loader.import(specifier, parentURL);
}
});
} catch (e) {
diff --git a/node.gyp b/node.gyp
index 497c06c6bc0a6c..229033b4db6cc9 100644
--- a/node.gyp
+++ b/node.gyp
@@ -176,7 +176,6 @@
'lib/internal/modules/esm/module_job.js',
'lib/internal/modules/esm/module_map.js',
'lib/internal/modules/esm/resolve.js',
- 'lib/internal/modules/esm/transform_source.js',
'lib/internal/modules/esm/translators.js',
'lib/internal/net.js',
'lib/internal/options.js',
diff --git a/src/node_options.cc b/src/node_options.cc
index 93a2268da41363..7900d72733cbcf 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -298,7 +298,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
kAllowedInEnvironment);
AddOption("--experimental-loader",
"use the specified module as a custom loader",
- &EnvironmentOptions::userland_loader,
+ &EnvironmentOptions::userland_loaders,
kAllowedInEnvironment);
AddAlias("--loader", "--experimental-loader");
AddOption("--experimental-modules",
diff --git a/src/node_options.h b/src/node_options.h
index 7f8c223f755a24..db74285ff98f90 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -149,7 +149,7 @@ class EnvironmentOptions : public Options {
bool trace_uncaught = false;
bool trace_warnings = false;
std::string unhandled_rejections;
- std::string userland_loader;
+ std::vector userland_loaders;
bool syntax_check_only = false;
bool has_eval_string = false;
diff --git a/test/es-module/test-loader-chaining.mjs b/test/es-module/test-loader-chaining.mjs
new file mode 100644
index 00000000000000..467d2f7a51443d
--- /dev/null
+++ b/test/es-module/test-loader-chaining.mjs
@@ -0,0 +1,8 @@
+import '../common/index.mjs';
+import { strictEqual } from 'assert';
+
+// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-chain-a.mjs --experimental-loader ./test/fixtures/es-module-loaders/loader-chain-b.mjs
+
+// loader-chain-a.mjs changes AAA to BBB
+// loader-chain-b.mjs changes BBB to CCC
+strictEqual('AAA', 'CCC');
diff --git a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
index f476c676cdea5b..cf5c251b4bdd7f 100644
--- a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
+++ b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
@@ -3,7 +3,8 @@ import module from 'module';
const GET_BUILTIN = `$__get_builtin_hole_${Date.now()}`;
export function getGlobalPreloadCode() {
- return `Object.defineProperty(globalThis, ${JSON.stringify(GET_BUILTIN)}, {
+ return `\
+Object.defineProperty(globalThis, ${JSON.stringify(GET_BUILTIN)}, {
value: (builtinName) => {
return getBuiltin(builtinName);
},
@@ -13,8 +14,8 @@ export function getGlobalPreloadCode() {
`;
}
-export function resolve(specifier, context, defaultResolve) {
- const def = defaultResolve(specifier, context);
+export async function resolve(specifier, context, nextResolve) {
+ const def = await nextResolve(specifier, context);
if (def.url.startsWith('nodejs:')) {
return {
url: `custom-${def.url}`,
@@ -23,7 +24,7 @@ export function resolve(specifier, context, defaultResolve) {
return def;
}
-export function getSource(url, context, defaultGetSource) {
+export function getSource(url) {
if (url.startsWith('custom-nodejs:')) {
const urlObj = new URL(url);
return {
@@ -31,14 +32,14 @@ export function getSource(url, context, defaultGetSource) {
format: 'module',
};
}
- return defaultGetSource(url, context);
+ return null;
}
-export function getFormat(url, context, defaultGetFormat) {
+export function getFormat(url) {
if (url.startsWith('custom-nodejs:')) {
return { format: 'module' };
}
- return defaultGetFormat(url, context, defaultGetFormat);
+ return null;
}
function generateBuiltinModule(builtinName) {
diff --git a/test/fixtures/es-module-loaders/example-loader.mjs b/test/fixtures/es-module-loaders/example-loader.mjs
index 1ed18bda51070d..e5024987154f0c 100644
--- a/test/fixtures/es-module-loaders/example-loader.mjs
+++ b/test/fixtures/es-module-loaders/example-loader.mjs
@@ -8,7 +8,7 @@ const JS_EXTENSIONS = new Set(['.js', '.mjs']);
const baseURL = new URL('file://');
baseURL.pathname = process.cwd() + '/';
-export function resolve(specifier, { parentURL = baseURL }, defaultResolve) {
+export function resolve(specifier, { parentURL = baseURL, ...context }, nextResolve) {
if (builtinModules.includes(specifier)) {
return {
url: 'nodejs:' + specifier
@@ -16,7 +16,7 @@ export function resolve(specifier, { parentURL = baseURL }, defaultResolve) {
}
if (/^\.{1,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
// For node_modules support:
- // return defaultResolve(specifier, {parentURL}, defaultResolve);
+ // return nextResolve(specifier, { parentURL, ...context });
throw new Error(
`imports must be URLs or begin with './', or '../'; '${specifier}' does not`);
}
@@ -26,7 +26,7 @@ export function resolve(specifier, { parentURL = baseURL }, defaultResolve) {
};
}
-export function getFormat(url, context, defaultGetFormat) {
+export function getFormat(url) {
if (url.startsWith('nodejs:') && builtinModules.includes(url.slice(7))) {
return {
format: 'builtin'
diff --git a/test/fixtures/es-module-loaders/get-source.mjs b/test/fixtures/es-module-loaders/get-source.mjs
index e5a9c65201aa28..c1c2f6b7f1b1fc 100644
--- a/test/fixtures/es-module-loaders/get-source.mjs
+++ b/test/fixtures/es-module-loaders/get-source.mjs
@@ -1,10 +1,9 @@
-export async function getSource(url, { format }, defaultGetSource) {
+export async function getSource(url, { format }) {
if (url.endsWith('fixtures/es-modules/message.mjs')) {
// Oh, I’ve got that one in my cache!
return {
source: `export const message = 'Woohoo!'.toUpperCase();`
}
- } else {
- return defaultGetSource(url, {format}, defaultGetSource);
}
+ return null;
}
diff --git a/test/fixtures/es-module-loaders/js-loader.mjs b/test/fixtures/es-module-loaders/js-loader.mjs
index 2f79475e77e269..79e77250f91aae 100644
--- a/test/fixtures/es-module-loaders/js-loader.mjs
+++ b/test/fixtures/es-module-loaders/js-loader.mjs
@@ -1,9 +1,9 @@
-export function getFormat(url, context, defaultGetFormat) {
+export function getFormat(url, context) {
// Load all .js files as ESM, regardless of package scope
if (url.endsWith('.js')) {
return {
format: 'module'
}
}
- return defaultGetFormat(url, context, defaultGetFormat);
+ return null;
}
diff --git a/test/fixtures/es-module-loaders/loader-chain-a.mjs b/test/fixtures/es-module-loaders/loader-chain-a.mjs
new file mode 100644
index 00000000000000..70874b2c11d91e
--- /dev/null
+++ b/test/fixtures/es-module-loaders/loader-chain-a.mjs
@@ -0,0 +1,8 @@
+export async function transformSource(source, { url }) {
+ if (url.endsWith('test-loader-chaining.mjs')) {
+ return {
+ source: source.toString().replace(/A/g, 'B'),
+ };
+ }
+ return { source };
+}
diff --git a/test/fixtures/es-module-loaders/loader-chain-b.mjs b/test/fixtures/es-module-loaders/loader-chain-b.mjs
new file mode 100644
index 00000000000000..b8b9299283732a
--- /dev/null
+++ b/test/fixtures/es-module-loaders/loader-chain-b.mjs
@@ -0,0 +1,8 @@
+export async function transformSource(source, { url }) {
+ if (url.endsWith('test-loader-chaining.mjs')) {
+ return {
+ source: source.toString().replace(/B/g, 'C'),
+ };
+ }
+ return { source };
+}
diff --git a/test/fixtures/es-module-loaders/loader-get-format.mjs b/test/fixtures/es-module-loaders/loader-get-format.mjs
index 7ade70fca0ebe6..4b6c2f3802f382 100644
--- a/test/fixtures/es-module-loaders/loader-get-format.mjs
+++ b/test/fixtures/es-module-loaders/loader-get-format.mjs
@@ -1,4 +1,4 @@
-export async function getFormat(url, context, defaultGetFormat) {
+export async function getFormat(url) {
try {
if (new URL(url).pathname.endsWith('.unknown')) {
return {
@@ -6,5 +6,5 @@ export async function getFormat(url, context, defaultGetFormat) {
};
}
} catch {}
- return defaultGetFormat(url, context, defaultGetFormat);
+ return null;
}
diff --git a/test/fixtures/es-module-loaders/loader-invalid-format.mjs b/test/fixtures/es-module-loaders/loader-invalid-format.mjs
index 55ae1cec8ee926..7e3692ca3c7212 100644
--- a/test/fixtures/es-module-loaders/loader-invalid-format.mjs
+++ b/test/fixtures/es-module-loaders/loader-invalid-format.mjs
@@ -1,17 +1,17 @@
-export async function resolve(specifier, { parentURL }, defaultResolve) {
+export async function resolve(specifier, { parentURL }) {
if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
return {
url: 'file:///asdf'
};
}
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return null;
}
-export function getFormat(url, context, defaultGetFormat) {
+export function getFormat(url) {
if (url === 'file:///asdf') {
return {
format: 'esm'
}
}
- return defaultGetFormat(url, context, defaultGetFormat);
+ return null;
}
diff --git a/test/fixtures/es-module-loaders/loader-invalid-url.mjs b/test/fixtures/es-module-loaders/loader-invalid-url.mjs
index e7de0d4ed92378..ea3b87929cf918 100644
--- a/test/fixtures/es-module-loaders/loader-invalid-url.mjs
+++ b/test/fixtures/es-module-loaders/loader-invalid-url.mjs
@@ -1,9 +1,9 @@
/* eslint-disable node-core/required-modules */
-export async function resolve(specifier, { parentURL }, defaultResolve) {
+export async function resolve(specifier, { parentURL }) {
if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
return {
url: specifier
};
}
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return null;
}
diff --git a/test/fixtures/es-module-loaders/loader-shared-dep.mjs b/test/fixtures/es-module-loaders/loader-shared-dep.mjs
index 3576c074d52cec..45c764c94603b6 100644
--- a/test/fixtures/es-module-loaders/loader-shared-dep.mjs
+++ b/test/fixtures/es-module-loaders/loader-shared-dep.mjs
@@ -5,7 +5,7 @@ import {createRequire} from '../../common/index.mjs';
const require = createRequire(import.meta.url);
const dep = require('./loader-dep.js');
-export function resolve(specifier, { parentURL }, defaultResolve) {
+export function resolve() {
assert.strictEqual(dep.format, 'module');
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return null;
}
diff --git a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs
index e976343e47e9bc..f4a2f9c3697cca 100644
--- a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs
+++ b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs
@@ -1,17 +1,17 @@
-export async function resolve(specifier, { parentURL }, defaultResolve) {
+export async function resolve(specifier) {
if (specifier === 'unknown-builtin-module') {
return {
url: 'nodejs:unknown-builtin-module'
};
}
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return null;
}
-export async function getFormat(url, context, defaultGetFormat) {
+export async function getFormat(url) {
if (url === 'nodejs:unknown-builtin-module') {
return {
format: 'builtin'
};
}
- return defaultGetFormat(url, context, defaultGetFormat);
+ return null;
}
diff --git a/test/fixtures/es-module-loaders/loader-with-custom-condition.mjs b/test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
index 78ffd75e6be27b..2a6d82fab7d1cb 100644
--- a/test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
+++ b/test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
@@ -1,9 +1,9 @@
-import {ok, deepStrictEqual} from 'assert';
+import { ok, deepStrictEqual } from 'assert';
-export async function resolve(specifier, context, defaultResolve) {
+export async function resolve(specifier, context, nextResolve) {
ok(Array.isArray(context.conditions), 'loader receives conditions array');
deepStrictEqual([...context.conditions].sort(), ['import', 'node']);
- return defaultResolve(specifier, {
+ return nextResolve(specifier, {
...context,
conditions: ['custom-condition', ...context.conditions],
});
diff --git a/test/fixtures/es-module-loaders/loader-with-dep.mjs b/test/fixtures/es-module-loaders/loader-with-dep.mjs
index da7d44ae793e22..34e93ef81a5877 100644
--- a/test/fixtures/es-module-loaders/loader-with-dep.mjs
+++ b/test/fixtures/es-module-loaders/loader-with-dep.mjs
@@ -3,9 +3,10 @@ import {createRequire} from '../../common/index.mjs';
const require = createRequire(import.meta.url);
const dep = require('./loader-dep.js');
-export function resolve (specifier, { parentURL }, defaultResolve) {
+export async function resolve(specifier, context, nextResolve) {
+ const { url } = await nextResolve(specifier, context);
return {
- url: defaultResolve(specifier, {parentURL}, defaultResolve).url,
- format: dep.format
+ url,
+ format: dep.format,
};
}
diff --git a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs
index 2130bad5f52698..c28c878f2c91aa 100644
--- a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs
+++ b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs
@@ -3,19 +3,15 @@ import assert from 'assert';
// a loader that asserts that the defaultResolve will throw "not found"
// (skipping the top-level main of course)
let mainLoad = true;
-export async function resolve(specifier, { parentURL }, defaultResolve) {
+export async function resolve(specifier, context, nextResolve) {
if (mainLoad) {
mainLoad = false;
- return defaultResolve(specifier, {parentURL}, defaultResolve);
+ return null;
}
- try {
- await defaultResolve(specifier, {parentURL}, defaultResolve);
- }
- catch (e) {
- assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND');
- return {
- url: 'nodejs:fs'
- };
- }
- assert.fail(`Module resolution for ${specifier} should be throw ERR_MODULE_NOT_FOUND`);
+ await assert.rejects(async () => {
+ await nextResolve(specifier, context);
+ }, {
+ code: 'ERR_MODULE_NOT_FOUND',
+ });
+ return { url: 'nodejs:fs' };
}
diff --git a/test/fixtures/es-module-loaders/string-sources.mjs b/test/fixtures/es-module-loaders/string-sources.mjs
index 3133231208ce1e..f46f175776a02f 100644
--- a/test/fixtures/es-module-loaders/string-sources.mjs
+++ b/test/fixtures/es-module-loaders/string-sources.mjs
@@ -10,21 +10,21 @@ const SOURCES = {
'test:Uint8Array': new Uint8Array(0),
'test:undefined': undefined,
}
-export function resolve(specifier, context, defaultFn) {
+export function resolve(specifier) {
if (specifier.startsWith('test:')) {
return { url: specifier };
}
- return defaultFn(specifier, context);
+ return null;
}
-export function getFormat(href, context, defaultFn) {
+export function getFormat(href) {
if (href.startsWith('test:')) {
return { format: 'module' };
}
- return defaultFn(href, context);
+ return null;
}
-export function getSource(href, context, defaultFn) {
+export function getSource(href) {
if (href.startsWith('test:')) {
return { source: SOURCES[href] };
}
- return defaultFn(href, context);
+ return null;
}
diff --git a/test/fixtures/es-module-loaders/transform-source.mjs b/test/fixtures/es-module-loaders/transform-source.mjs
index 25d983b64e62ca..743cb4d8652aa8 100644
--- a/test/fixtures/es-module-loaders/transform-source.mjs
+++ b/test/fixtures/es-module-loaders/transform-source.mjs
@@ -1,5 +1,4 @@
-export async function transformSource(
- source, { url, format }, defaultTransformSource) {
+export async function transformSource(source, { url, format }) {
if (format === 'module') {
if (typeof source !== 'string') {
source = new TextDecoder().decode(source);
@@ -7,8 +6,6 @@ export async function transformSource(
return {
source: source.replace(`'A message';`, `'A message'.toUpperCase();`)
};
- } else { // source could be a buffer, e.g. for WASM
- return defaultTransformSource(
- source, {url, format}, defaultTransformSource);
}
+ return { source };
}
diff --git a/test/message/esm_display_syntax_error_import.out b/test/message/esm_display_syntax_error_import.out
index fe174d54a5c49f..387a63a734b512 100644
--- a/test/message/esm_display_syntax_error_import.out
+++ b/test/message/esm_display_syntax_error_import.out
@@ -5,4 +5,3 @@ SyntaxError: The requested module '../fixtures/es-module-loaders/module-named-ex
at ModuleJob._instantiate (internal/modules/esm/module_job.js:*:*)
at async ModuleJob.run (internal/modules/esm/module_job.js:*:*)
at async Loader.import (internal/modules/esm/loader.js:*:*)
- at async Object.loadESM (internal/process/esm_loader.js:*:*)
diff --git a/test/message/esm_display_syntax_error_import_module.out b/test/message/esm_display_syntax_error_import_module.out
index d220627bd02654..ae8b99d55fef20 100644
--- a/test/message/esm_display_syntax_error_import_module.out
+++ b/test/message/esm_display_syntax_error_import_module.out
@@ -5,4 +5,3 @@ SyntaxError: The requested module './module-named-exports.mjs' does not provide
at ModuleJob._instantiate (internal/modules/esm/module_job.js:*:*)
at async ModuleJob.run (internal/modules/esm/module_job.js:*:*)
at async Loader.import (internal/modules/esm/loader.js:*:*)
- at async Object.loadESM (internal/process/esm_loader.js:*:*)
diff --git a/test/message/esm_loader_not_found.out b/test/message/esm_loader_not_found.out
index c9429dad40a072..d0b7b878183deb 100644
--- a/test/message/esm_loader_not_found.out
+++ b/test/message/esm_loader_not_found.out
@@ -1,18 +1,18 @@
(node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
-internal/process/esm_loader.js:*
- internalBinding('errors').triggerUncaughtException(
- ^
+internal/process/execution.js:*
+ internalBinding('errors').triggerUncaughtException(error, fromPromise);
+ ^
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'i-dont-exist' imported from *
at new NodeError (internal/errors.js:*:*)
at packageResolve (internal/modules/esm/resolve.js:*:*)
at moduleResolve (internal/modules/esm/resolve.js:*:*)
- at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:*:*)
- at Loader.resolve (internal/modules/esm/loader.js:*:*)
- at Loader.getModuleJob (internal/modules/esm/loader.js:*:*)
- at Loader.import (internal/modules/esm/loader.js:*:*)
- at internal/process/esm_loader.js:*:*
- at initializeLoader (internal/process/esm_loader.js:*:*)
- at Object.loadESM (internal/process/esm_loader.js:*:*) {
+ at defaultResolve (internal/modules/esm/resolve.js:*:*)
+ at Loader.importLoader (internal/modules/esm/loader.js:*:*)
+ at createLoader (internal/process/esm_loader.js:*:*)
+ at exports.getLoader (internal/process/esm_loader.js:*:*)
+ at runMainESM (internal/modules/run_main.js:*:*)
+ at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:*:*)
+ at internal/main/run_main_module.js:*:* {
code: 'ERR_MODULE_NOT_FOUND'
}
diff --git a/test/message/esm_loader_not_found_cjs_hint_bare.out b/test/message/esm_loader_not_found_cjs_hint_bare.out
index 6063709859573b..3ca0f46c18f767 100644
--- a/test/message/esm_loader_not_found_cjs_hint_bare.out
+++ b/test/message/esm_loader_not_found_cjs_hint_bare.out
@@ -1,6 +1,6 @@
-internal/process/esm_loader.js:*
- internalBinding('errors').triggerUncaughtException(
- ^
+internal/process/execution.js:*
+ internalBinding('errors').triggerUncaughtException(error, fromPromise);
+ ^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '*test*fixtures*node_modules*some_module*obj' imported from *test*fixtures*esm_loader_not_found_cjs_hint_bare.mjs
Did you mean to import some_module/obj.js?
diff --git a/test/message/esm_loader_not_found_cjs_hint_relative.out b/test/message/esm_loader_not_found_cjs_hint_relative.out
index 820cb575c4aef1..1a678b9ab50427 100644
--- a/test/message/esm_loader_not_found_cjs_hint_relative.out
+++ b/test/message/esm_loader_not_found_cjs_hint_relative.out
@@ -1,20 +1,20 @@
(node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
-internal/process/esm_loader.js:*
- internalBinding('errors').triggerUncaughtException(
- ^
+internal/process/execution.js:*
+ internalBinding('errors').triggerUncaughtException(error, fromPromise);
+ ^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '*test*common*fixtures' imported from *
Did you mean to import ./test/common/fixtures.js?
at new NodeError (internal/errors.js:*:*)
at finalizeResolution (internal/modules/esm/resolve.js:*:*)
at moduleResolve (internal/modules/esm/resolve.js:*:*)
- at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:*:*)
- at Loader.resolve (internal/modules/esm/loader.js:*:*)
- at Loader.getModuleJob (internal/modules/esm/loader.js:*:*)
- at Loader.import (internal/modules/esm/loader.js:*:*)
- at internal/process/esm_loader.js:*:*
- at initializeLoader (internal/process/esm_loader.js:*:*)
- at Object.loadESM (internal/process/esm_loader.js:*:*) {
+ at defaultResolve (internal/modules/esm/resolve.js:*:*)
+ at Loader.importLoader (internal/modules/esm/loader.js:*:*)
+ at createLoader (internal/process/esm_loader.js:*:*)
+ at exports.getLoader (internal/process/esm_loader.js:*:*)
+ at runMainESM (internal/modules/run_main.js:*:*)
+ at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:*:*)
+ at internal/main/run_main_module.js:*:* {
code: 'ERR_MODULE_NOT_FOUND'
}
diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js
index 735f2e6394bd20..e6677f1504e5ab 100644
--- a/test/parallel/test-bootstrap-modules.js
+++ b/test/parallel/test-bootstrap-modules.js
@@ -61,7 +61,6 @@ const expectedModules = new Set([
'NativeModule internal/modules/esm/module_job',
'NativeModule internal/modules/esm/module_map',
'NativeModule internal/modules/esm/resolve',
- 'NativeModule internal/modules/esm/transform_source',
'NativeModule internal/modules/esm/translators',
'NativeModule internal/process/esm_loader',
'NativeModule internal/options',