diff --git a/doc/api/errors.md b/doc/api/errors.md
index b9c52ad0d9af89..5f0d1648614b3a 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1229,6 +1229,12 @@ An attempt was made to open an IPC communication channel with a synchronously
forked Node.js process. See the documentation for the [`child_process`][] module
for more information.
+
+### ERR_LOADER_HOOK_BAD_TYPE
+
+A Loader defined an invalid value for a hook. See the documentation for
+[ECMAScript Modules][] for more information.
+
### ERR_METHOD_NOT_IMPLEMENTED
@@ -1243,6 +1249,13 @@ strict compliance with the API specification (which in some cases may accept
`func(undefined)` and `func()` are treated identically, and the
[`ERR_INVALID_ARG_TYPE`][] error code may be used instead.
+
+### ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK
+
+A Loader resolved an import to a "dynamic" format, but no hook for
+`dynamicInstantiate` was declared. See Loader hook documentation in
+[ECMAScript Modules][].
+
### ERR_MISSING_MODULE
@@ -1630,6 +1643,7 @@ Creation of a [`zlib`][] object failed due to incorrect configuration.
[`cipher.getAuthTag()`]: crypto.html#crypto_cipher_getauthtag
[`crypto.timingSafeEqual()`]: crypto.html#crypto_crypto_timingsafeequal_a_b
[`dgram.createSocket()`]: dgram.html#dgram_dgram_createsocket_options_callback
+[ECMAScript Modules]: esm.html
[`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE
[`EventEmitter`]: events.html#events_class_eventemitter
[`fs.symlink()`]: fs.html#fs_fs_symlink_target_path_type_callback
diff --git a/doc/api/esm.md b/doc/api/esm.md
index adde6a199fd068..4bcc185925c631 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -106,26 +106,45 @@ fs.readFile('./foo.txt', (err, body) => {
To customize the default module resolution, loader hooks can optionally be
-provided via a `--loader ./loader-name.mjs` argument to Node.
+provided via a `--loader ./loader-name.mjs` argument to Node.js. This argument
+can be passed multiple times to compose loaders like
+`--loader ./loader-coverage.mjs --loader ./loader-mocking.mjs`. The last loader
+must explicitly call to the parent loader in order to provide compose behavior.
When hooks are used they only apply to ES module loading and not to any
CommonJS modules loaded.
+All loaders are created by invoking the default export of their module as a
+function. The parameters given to the function are a single object with
+properties to call the `resolve` and `dynamicInstantiate` hooks of the parent
+loader. The default loader has a `resolve` hook and a function that throws for
+the value of `dynamicInstantiate`.
+
### Resolve hook
The resolve hook returns the resolved file URL and module format for a
given module specifier and parent file URL:
```js
+// example loader that treats all files within the current working directory as
+// ECMAScript Modules
const baseURL = new URL('file://');
baseURL.pathname = `${process.cwd()}/`;
-export async function resolve(specifier,
- parentModuleURL = baseURL,
- defaultResolver) {
+export default function(parent) {
return {
- url: new URL(specifier, parentModuleURL).href,
- format: 'esm'
+ async resolve(specifier,
+ parentModuleURL = baseURL) {
+ const location = new URL(specifier, parentModuleURL);
+ if (locations.host === baseURL.host &&
+ location.pathname.startsWith(baseURL.pathname)) {
+ return {
+ url: location.href,
+ format: 'esm'
+ };
+ }
+ return parent.resolve(specifier, parentModuleURL);
+ }
};
}
```
@@ -164,28 +183,35 @@ const JS_EXTENSIONS = new Set(['.js', '.mjs']);
const baseURL = new URL('file://');
baseURL.pathname = `${process.cwd()}/`;
-export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
- if (builtins.includes(specifier)) {
- return {
- url: specifier,
- format: 'builtin'
- };
- }
- if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
- // For node_modules support:
- // return defaultResolve(specifier, parentModuleURL);
- throw new Error(
- `imports must begin with '/', './', or '../'; '${specifier}' does not`);
- }
- const resolved = new URL(specifier, parentModuleURL);
- const ext = path.extname(resolved.pathname);
- if (!JS_EXTENSIONS.has(ext)) {
- throw new Error(
- `Cannot load file with non-JavaScript file extension ${ext}.`);
- }
+export default function(parent) {
return {
- url: resolved.href,
- format: 'esm'
+ resolve(specifier, parentModuleURL = baseURL) {
+ if (builtins.includes(specifier)) {
+ return {
+ url: specifier,
+ format: 'builtin'
+ };
+ }
+ if (/^\.{0,2}[/]/.test(specifier) !== true &&
+ !specifier.startsWith('file:')) {
+ // For node_modules support:
+ // return parent.resolve(specifier, parentModuleURL);
+ throw new Error(
+ `imports must begin with '/', './', or '../'; '${
+ specifier
+ }' does not`);
+ }
+ const resolved = new URL(specifier, parentModuleURL);
+ const ext = path.extname(resolved.pathname);
+ if (!JS_EXTENSIONS.has(ext)) {
+ throw new Error(
+ `Cannot load file with non-JavaScript file extension ${ext}.`);
+ }
+ return {
+ url: resolved.href,
+ format: 'esm'
+ };
+ }
};
}
```
@@ -207,12 +233,25 @@ This hook is called only for modules that return `format: "dynamic"` from
the `resolve` hook.
```js
-export async function dynamicInstantiate(url) {
+// example loader that can generate modules for .txt files
+// that resolved to a 'dynamic' format
+import fs from 'fs';
+import util from 'util';
+export default function(parent) {
return {
- exports: ['customExportName'],
- execute: (exports) => {
- // get and set functions provided for pre-allocated export names
- exports.customExportName.set('value');
+ async dynamicInstantiate(url) {
+ const location = new URL(url);
+ if (location.pathname.slice(-4) === '.txt') {
+ const text = String(await util.promisify(fs.readFile)(location));
+ return {
+ exports: ['text'],
+ execute: (exports) => {
+ // get and set functions provided for pre-allocated export names
+ exports.text.set(text);
+ }
+ };
+ }
+ return parent.dynamicInstantiate(url);
}
};
}
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index a4a79d671e4938..639f63d3b5460f 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -800,8 +800,13 @@ E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error);
E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error);
E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error);
E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', Error);
+E('ERR_LOADER_HOOK_BAD_TYPE',
+ 'ES Module loader hook %s must be of type %s', TypeError);
E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented', Error);
E('ERR_MISSING_ARGS', missingArgs, TypeError);
+E('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK',
+ 'The ES Module loader may not return a format of \'dynamic\' when no ' +
+ 'dynamicInstantiate function was provided', TypeError);
E('ERR_MISSING_MODULE', 'Cannot find module %s', Error);
E('ERR_MODULE_RESOLUTION_LEGACY',
'%s not found by import in %s.' +
diff --git a/lib/internal/process/modules.js b/lib/internal/process/modules.js
index f89278ddaa2d52..94baa7825eee9c 100644
--- a/lib/internal/process/modules.js
+++ b/lib/internal/process/modules.js
@@ -2,14 +2,28 @@
const {
setImportModuleDynamicallyCallback,
- setInitializeImportMetaObjectCallback
+ setInitializeImportMetaObjectCallback,
} = internalBinding('module_wrap');
+const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
+const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty);
+const apply = Reflect.apply;
+
+const errors = require('internal/errors');
const { getURLFromFilePath } = require('internal/url');
const Loader = require('internal/loader/Loader');
const path = require('path');
const { URL } = require('url');
+// fires a getter or reads the value off a descriptor
+function grabPropertyOffDescriptor(object, descriptor) {
+ if (hasOwnProperty(descriptor, 'value')) {
+ return descriptor.value;
+ } else {
+ return apply(descriptor.get, object, []);
+ }
+}
+
function normalizeReferrerURL(referrer) {
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
return getURLFromFilePath(referrer).href;
@@ -23,7 +37,9 @@ function initializeImportMetaObject(wrap, meta) {
let loaderResolve;
exports.loaderPromise = new Promise((resolve, reject) => {
- loaderResolve = resolve;
+ loaderResolve = (v) => {
+ resolve(v);
+ };
});
exports.ESMLoader = undefined;
@@ -31,17 +47,71 @@ exports.ESMLoader = undefined;
exports.setup = function() {
setInitializeImportMetaObjectCallback(initializeImportMetaObject);
- let ESMLoader = new Loader();
+ const RuntimeLoader = new Loader();
const loaderPromise = (async () => {
- const userLoader = process.binding('config').userLoader;
- if (userLoader) {
- const hooks = await ESMLoader.import(
- userLoader, getURLFromFilePath(`${process.cwd()}/`).href);
- ESMLoader = new Loader();
- ESMLoader.hook(hooks);
- exports.ESMLoader = ESMLoader;
+ const { userLoaders } = process.binding('config');
+ if (userLoaders) {
+ const BootstrapLoader = new Loader();
+ exports.ESMLoader = BootstrapLoader;
+ let resolve = (url, referrer) => {
+ return require('internal/loader/DefaultResolve')(url, referrer);
+ };
+ let dynamicInstantiate = (url) => {
+ throw new errors.Error('ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK');
+ };
+ for (var i = 0; i < userLoaders.length; i++) {
+ const loaderSpecifier = userLoaders[i];
+ const { default: factory } = await BootstrapLoader.import(
+ loaderSpecifier);
+ const cachedResolve = resolve;
+ const cachedDynamicInstantiate = dynamicInstantiate;
+ const next = factory({
+ __proto__: null,
+ resolve: Object.setPrototypeOf(async (url, referrer) => {
+ const ret = await cachedResolve(url, referrer);
+ return {
+ __proto__: null,
+ url: `${ret.url}`,
+ format: `${ret.format}`,
+ };
+ }, null),
+ dynamicInstantiate: Object.setPrototypeOf(async (url) => {
+ const ret = await cachedDynamicInstantiate(url);
+ return {
+ __proto__: null,
+ exports: ret.exports,
+ execute: ret.execute,
+ };
+ }, null),
+ });
+ const resolveDesc = getOwnPropertyDescriptor(next, 'resolve');
+ if (resolveDesc !== undefined) {
+ resolve = grabPropertyOffDescriptor(next, resolveDesc);
+ if (typeof resolve !== 'function') {
+ throw new errors.TypeError('ERR_LOADER_HOOK_BAD_TYPE',
+ 'resolve', 'function');
+ }
+ }
+ const dynamicInstantiateDesc = getOwnPropertyDescriptor(
+ next,
+ 'dynamicInstantiate');
+ if (dynamicInstantiateDesc !== undefined) {
+ dynamicInstantiate = grabPropertyOffDescriptor(
+ next,
+ dynamicInstantiateDesc);
+ if (typeof dynamicInstantiate !== 'function') {
+ throw new errors.TypeError('ERR_LOADER_HOOK_BAD_TYPE',
+ 'dynamicInstantiate', 'function');
+ }
+ }
+ }
+ RuntimeLoader.hook({
+ resolve,
+ dynamicInstantiate
+ });
}
- return ESMLoader;
+ exports.ESMLoader = RuntimeLoader;
+ return RuntimeLoader;
})();
loaderResolve(loaderPromise);
@@ -50,5 +120,5 @@ exports.setup = function() {
return loader.import(specifier, normalizeReferrerURL(referrer));
});
- exports.ESMLoader = ESMLoader;
+ exports.RuntimeLoader = RuntimeLoader;
};
diff --git a/src/node.cc b/src/node.cc
index 64de859bc6a278..6fc1c5f5f7e5d1 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -246,7 +246,7 @@ bool config_experimental_vm_modules = false;
// Set in node.cc by ParseArgs when --loader is used.
// Used in node_config.cc to set a constant on process.binding('config')
// that is used by lib/internal/bootstrap_node.js
-std::string config_userland_loader; // NOLINT(runtime/string)
+std::vector config_userland_loaders;
// Set by ParseArgs when --pending-deprecation or NODE_PENDING_DEPRECATION
// is used.
@@ -3745,7 +3745,7 @@ static void ParseArgs(int* argc,
exit(9);
}
args_consumed += 1;
- config_userland_loader = module;
+ config_userland_loaders.push_back(module);
} else if (strcmp(arg, "--prof-process") == 0) {
prof_process = true;
short_circuit = true;
diff --git a/src/node_config.cc b/src/node_config.cc
index cac551ad2c410a..5bbd01ac0792c5 100644
--- a/src/node_config.cc
+++ b/src/node_config.cc
@@ -71,13 +71,22 @@ static void InitConfig(Local