diff --git a/doc/api/cli.md b/doc/api/cli.md
index dfb7ec875689ea..e6dcb3e5016844 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -1383,6 +1383,6 @@ greater than `4` (its current default value). For more information, see the
[debugger]: debugger.html
[debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications
[emit_warning]: process.html#process_process_emitwarning_warning_type_code_ctor
-[experimental ECMAScript Module loader]: esm.html#esm_resolve_hook
+[experimental ECMAScript Module loader]: esm.html#esm_experimental_loaders
[libuv threadpool documentation]: http://docs.libuv.org/en/latest/threadpool.html
[remote code execution]: https://www.owasp.org/index.php/Code_Injection
diff --git a/doc/api/esm.md b/doc/api/esm.md
index 897fadb09ead9e..381bdc8a053443 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -994,7 +994,7 @@ node --experimental-wasm-modules index.mjs
would provide the exports interface for the instantiation of `module.wasm`.
-## Experimental Loader hooks
+## Experimental Loaders
**Note: This API is currently being redesigned and will still change.**
@@ -1006,39 +1006,49 @@ provided via a `--experimental-loader ./loader-name.mjs` argument to Node.js.
When hooks are used they only apply to ES module loading and not to any
CommonJS modules loaded.
-### Resolve hook
+### Hooks
-The resolve hook returns the resolved file URL and module format for a
-given module specifier and parent file URL:
+#### resolve
hook
-```js
-import { URL, pathToFileURL } from 'url';
-const baseURL = pathToFileURL(process.cwd()).href;
+> Note: The loaders API is being redesigned. This hook may disappear or its
+> signature may change. Do not rely on the API described below.
+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
+this one, or `undefined` if this is the main entry point for the application.
+
+```js
/**
* @param {string} specifier
- * @param {string} parentModuleURL
- * @param {function} defaultResolver
+ * @param {object} context
+ * @param {string} context.parentURL
+ * @param {function} defaultResolve
+ * @returns {object} response
+ * @returns {string} response.url
*/
-export async function resolve(specifier,
- parentModuleURL = baseURL,
- defaultResolver) {
- return {
- url: new URL(specifier, parentModuleURL).href,
- format: 'module'
- };
+export async function resolve(specifier, context, defaultResolve) {
+ const { parentURL = null } = context;
+ if (someCondition) {
+ // 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 : new URL(specifier).href
+ };
+ }
+ // Defer to Node.js for all other specifiers.
+ return defaultResolve(specifier, context, defaultResolve);
}
```
-The `parentModuleURL` is provided as `undefined` when performing main Node.js
-load itself.
+#### getFormat
hook
-The default Node.js ES module resolution function is provided as a third
-argument to the resolver for easy compatibility workflows.
+> Note: The loaders API is being redesigned. This hook may disappear or its
+> signature may change. Do not rely on the API described below.
-In addition to returning the resolved file URL value, the resolve hook also
-returns a `format` property specifying the module format of the resolved
-module. This can be one of the following:
+The `getFormat` hook provides a way to define a custom method of determining how
+a URL should be interpreted. This can be one of the following:
| `format` | Description |
| --- | --- |
@@ -1046,74 +1056,120 @@ module. This can be one of the following:
| `'commonjs'` | Load a Node.js CommonJS module |
| `'dynamic'` | Use a [dynamic instantiate hook][] |
| `'json'` | Load a JSON file |
-| `'module'` | Load a standard JavaScript module |
+| `'module'` | Load a standard JavaScript module (ES module) |
| `'wasm'` | Load a WebAssembly module |
-For example, a dummy loader to load JavaScript restricted to browser resolution
-rules with only JS file extension and Node.js builtin modules support could
-be written:
-
```js
-import path from 'path';
-import process from 'process';
-import Module from 'module';
-import { URL, pathToFileURL } from 'url';
+/**
+ * @param {string} url
+ * @param {object} context (currently empty)
+ * @param {function} defaultGetFormat
+ * @returns {object} response
+ * @returns {string} response.format
+ */
+export async function getFormat(url, context, defaultGetFormat) {
+ if (someCondition) {
+ // For some or all URLs, do some custom logic for determining format.
+ // Always return an object of the form {format: }, where the
+ // format is one of the strings in the table above.
+ return {
+ format: 'module'
+ };
+ }
+ // Defer to Node.js for all other URLs.
+ return defaultGetFormat(url, context, defaultGetFormat);
+}
+```
+
+#### getSource
hook
-const builtins = Module.builtinModules;
-const JS_EXTENSIONS = new Set(['.js', '.mjs']);
+> Note: The loaders API is being redesigned. This hook may disappear or its
+> signature may change. Do not rely on the API described below.
-const baseURL = pathToFileURL(process.cwd()).href;
+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} specifier
- * @param {string} parentModuleURL
- * @param {function} defaultResolver
+ * @param {string} url
+ * @param {object} context
+ * @param {string} context.format
+ * @param {function} defaultGetSource
+ * @returns {object} response
+ * @returns {string|buffer} response.source
*/
-export async function resolve(specifier,
- parentModuleURL = baseURL,
- defaultResolver) {
- if (builtins.includes(specifier)) {
+export async function getSource(url, context, defaultGetSource) {
+ const { format } = context;
+ if (someCondition) {
+ // For some or all URLs, do some custom logic for retrieving the source.
+ // Always return an object of the form {source: }.
return {
- url: specifier,
- format: 'builtin'
+ source: '...'
};
}
- if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
- // For node_modules support:
- // return defaultResolver(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: 'module'
- };
+ // Defer to Node.js for all other URLs.
+ return defaultGetSource(url, context, defaultGetSource);
}
```
-With this loader, running:
+#### transformSource
hook
-```console
-NODE_OPTIONS='--experimental-loader ./custom-loader.mjs' node x.js
+> Note: The loaders API is being redesigned. This hook may disappear or its
+> signature may change. Do not rely on the API described below.
+
+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.
+
+If this hook is used to convert unknown-to-Node.js file types into executable
+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 {string|buffer} source
+ * @param {object} context
+ * @param {string} context.url
+ * @param {string} context.format
+ * @param {function} defaultTransformSource
+ * @returns {object} response
+ * @returns {string|buffer} response.source
+ */
+export async function transformSource(source,
+ context,
+ defaultTransformSource) {
+ const { url, format } = context;
+ if (someCondition) {
+ // For some or all URLs, do some custom logic for modifying the source.
+ // Always return an object of the form {source: }.
+ return {
+ source: '...'
+ };
+ }
+ // Defer to Node.js for all other sources.
+ return defaultTransformSource(
+ source, context, defaultTransformSource);
+}
```
-would load the module `x.js` as an ES module with relative resolution support
-(with `node_modules` loading skipped in this example).
+#### dynamicInstantiate
hook
-### Dynamic instantiate hook
+> Note: The loaders API is being redesigned. This hook may disappear or its
+> signature may change. Do not rely on the API described below.
To create a custom dynamic module that doesn't correspond to one of the
existing `format` interpretations, the `dynamicInstantiate` hook can be used.
This hook is called only for modules that return `format: 'dynamic'` from
-the `resolve` hook.
+the [`getFormat` hook][].
```js
+/**
+ * @param {string} url
+ * @returns {object} response
+ * @returns {array} response.exports
+ * @returns {function} response.execute
+ */
export async function dynamicInstantiate(url) {
return {
exports: ['customExportName'],
@@ -1129,6 +1185,179 @@ With the list of module exports provided upfront, the `execute` function will
then be called at the exact point of module evaluation order for that module
in the import tree.
+### Examples
+
+The various loader hooks can be used together to accomplish wide-ranging
+customizations of Node.js’ code loading and evaluation behaviors.
+
+#### HTTPS loader
+
+In current Node.js, specifiers starting with `https://` are unsupported. The
+loader below registers hooks to enable rudimentary support for such specifiers.
+While this may seem like a significant improvement to Node.js core
+functionality, there are substantial downsides to actually using this loader:
+performance is much slower than loading files from disk, there is no caching,
+and there is no security.
+
+```js
+// https-loader.mjs
+import { get } from 'https';
+
+export function resolve(specifier, context, defaultResolve) {
+ const { parentURL = null } = context;
+
+ // 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.
+ if (specifier.startsWith('https://')) {
+ return {
+ url: specifier
+ };
+ } else if (parentURL && parentURL.startsWith('https://')) {
+ return {
+ url: new URL(specifier, parentURL).href
+ };
+ }
+
+ // Let Node.js handle all other specifiers.
+ return defaultResolve(specifier, context, defaultResolve);
+}
+
+export function getFormat(url, context, defaultGetFormat) {
+ // This loader assumes all network-provided JavaScript is ES module code.
+ if (url.startsWith('https://')) {
+ return {
+ format: 'module'
+ };
+ }
+
+ // Let Node.js handle all other URLs.
+ return defaultGetFormat(url, context, defaultGetFormat);
+}
+
+export function getSource(url, context, defaultGetSource) {
+ // For JavaScript to be loaded over the network, we need to fetch and
+ // return it.
+ if (url.startsWith('https://')) {
+ return new Promise((resolve, reject) => {
+ get(url, (res) => {
+ let data = '';
+ res.on('data', (chunk) => data += chunk);
+ res.on('end', () => resolve({ source: data }));
+ }).on('error', (err) => reject(err));
+ });
+ }
+
+ // Let Node.js handle all other URLs.
+ return defaultGetSource(url, context, defaultGetSource);
+}
+```
+
+```js
+// main.mjs
+import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
+
+console.log(VERSION);
+```
+
+With this loader, running:
+
+```console
+node --experimental-loader ./https-loader.mjs ./main.js
+```
+
+Will print the current version of CoffeeScript per the module at the URL in
+`main.mjs`.
+
+#### Transpiler loader
+
+Sources that are in formats Node.js doesn’t understand can be converted into
+JavaScript using the [`transformSource` hook][]. Before that hook gets called,
+however, other hooks need to tell Node.js not to throw an error on unknown file
+types; and to tell Node.js how to load this new file type.
+
+This is obviously less performant than transpiling source files before running
+Node.js; a transpiler loader should only be used for development and testing
+purposes.
+
+```js
+// coffeescript-loader.mjs
+import { URL, pathToFileURL } from 'url';
+import CoffeeScript from 'coffeescript';
+
+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;
+
+ // Node.js normally errors on unknown file extensions, so return a URL for
+ // specifiers ending in the CoffeeScript file extensions.
+ if (extensionsRegex.test(specifier)) {
+ return {
+ url: new URL(specifier, parentURL).href
+ };
+ }
+
+ // Let Node.js handle all other specifiers.
+ return defaultResolve(specifier, context, defaultResolve);
+}
+
+export function getFormat(url, context, defaultGetFormat) {
+ // 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.
+ if (extensionsRegex.test(url)) {
+ return {
+ format: 'module'
+ };
+ }
+
+ // Let Node.js handle all other URLs.
+ return defaultGetFormat(url, context, defaultGetFormat);
+}
+
+export function transformSource(source, context, defaultTransformSource) {
+ const { url, format } = context;
+
+ if (extensionsRegex.test(url)) {
+ return {
+ source: CoffeeScript.compile(source, { bare: true })
+ };
+ }
+
+ // Let Node.js handle all other sources.
+ return defaultTransformSource(source, context, defaultTransformSource);
+}
+```
+
+```coffee
+# main.coffee
+import { scream } from './scream.coffee'
+console.log scream 'hello, world'
+
+import { version } from 'process'
+console.log "Brought to you by Node.js version #{version}"
+```
+
+```coffee
+# scream.coffee
+export scream = (str) -> str.toUpperCase()
+```
+
+With this loader, running:
+
+```console
+node --experimental-loader ./coffeescript-loader.mjs main.coffee
+```
+
+Will cause `main.coffee` to be turned into JavaScript after its source code is
+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.
+
## Resolution Algorithm
### Features
@@ -1409,11 +1638,14 @@ success!
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
[`esm`]: https://github.com/standard-things/esm#readme
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
+[`getFormat` hook]: #esm_code_getformat_code_hook
[`import()`]: #esm_import-expressions
[`import.meta.url`]: #esm_import_meta
[`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
-[dynamic instantiate hook]: #esm_dynamic_instantiate_hook
+[`transformSource` hook]: #esm_code_transformsource_code_hook
+[dynamic instantiate hook]: #esm_code_dynamicinstantiate_code_hook
[special scheme]: https://url.spec.whatwg.org/#special-scheme
[the official standard format]: https://tc39.github.io/ecma262/#sec-modules
+[transpiler loader example]: #esm_transpiler_loader
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index a0b7ee38cf1963..2166803c9741c2 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1331,7 +1331,7 @@ E('ERR_UNKNOWN_BUILTIN_MODULE', 'No such built-in module: %s', Error);
E('ERR_UNKNOWN_CREDENTIAL', '%s identifier does not exist: %s', Error);
E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s', TypeError);
E('ERR_UNKNOWN_FILE_EXTENSION',
- 'Unknown file extension "%s" for %s imported from %s',
+ 'Unknown file extension "%s" for %s',
TypeError);
E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s', RangeError);
E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError);
diff --git a/lib/internal/main/check_syntax.js b/lib/internal/main/check_syntax.js
index c5d919bd15bfab..a3b9bf2923769e 100644
--- a/lib/internal/main/check_syntax.js
+++ b/lib/internal/main/check_syntax.js
@@ -51,8 +51,10 @@ function checkSyntax(source, filename) {
if (filename === '[stdin]' || filename === '[eval]') {
isModule = getOptionValue('--input-type') === 'module';
} else {
- const resolve = require('internal/modules/esm/default_resolve');
- const { format } = resolve(pathToFileURL(filename).toString());
+ const { defaultResolve } = require('internal/modules/esm/resolve');
+ const { defaultGetFormat } = require('internal/modules/esm/get_format');
+ const { url } = defaultResolve(pathToFileURL(filename).toString());
+ const { format } = defaultGetFormat(url);
isModule = format === 'module';
}
if (isModule) {
diff --git a/lib/internal/modules/esm/default_resolve.js b/lib/internal/modules/esm/default_resolve.js
deleted file mode 100644
index 749f6861cd3ee6..00000000000000
--- a/lib/internal/modules/esm/default_resolve.js
+++ /dev/null
@@ -1,135 +0,0 @@
-'use strict';
-
-const {
- SafeMap,
-} = primordials;
-
-const internalFS = require('internal/fs/utils');
-const { NativeModule } = require('internal/bootstrap/loaders');
-const { extname } = require('path');
-const { realpathSync } = require('fs');
-const { getOptionValue } = require('internal/options');
-
-const preserveSymlinks = getOptionValue('--preserve-symlinks');
-const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
-const experimentalJsonModules = getOptionValue('--experimental-json-modules');
-const experimentalSpeciferResolution =
- getOptionValue('--experimental-specifier-resolution');
-const typeFlag = getOptionValue('--input-type');
-const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
-const { resolve: moduleWrapResolve,
- getPackageType } = internalBinding('module_wrap');
-const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
-const { ERR_INPUT_TYPE_NOT_ALLOWED,
- ERR_UNKNOWN_FILE_EXTENSION,
- ERR_UNSUPPORTED_ESM_URL_SCHEME } = require('internal/errors').codes;
-
-const realpathCache = new SafeMap();
-
-// const TYPE_NONE = 0;
-// const TYPE_COMMONJS = 1;
-const TYPE_MODULE = 2;
-
-const extensionFormatMap = {
- '__proto__': null,
- '.cjs': 'commonjs',
- '.js': 'module',
- '.mjs': 'module'
-};
-
-const legacyExtensionFormatMap = {
- '__proto__': null,
- '.cjs': 'commonjs',
- '.js': 'commonjs',
- '.json': 'commonjs',
- '.mjs': 'module',
- '.node': 'commonjs'
-};
-
-if (experimentalWasmModules)
- extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
-
-if (experimentalJsonModules)
- extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
-
-function resolve(specifier, parentURL) {
- let parsed;
- try {
- parsed = new URL(specifier);
- if (parsed.protocol === 'data:') {
- const [ , mime ] = /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/.exec(parsed.pathname) || [ null, null, null ];
- const format = ({
- '__proto__': null,
- 'text/javascript': 'module',
- 'application/json': 'json',
- 'application/wasm': experimentalWasmModules ? 'wasm' : null
- })[mime] || null;
- return {
- url: specifier,
- format
- };
- }
- } catch {}
- if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:')
- throw new ERR_UNSUPPORTED_ESM_URL_SCHEME();
- if (NativeModule.canBeRequiredByUsers(specifier)) {
- return {
- url: specifier,
- format: 'builtin'
- };
- }
- if (parentURL && parentURL.startsWith('data:')) {
- // This is gonna blow up, we want the error
- new URL(specifier, parentURL);
- }
-
- const isMain = parentURL === undefined;
- if (isMain) {
- parentURL = pathToFileURL(`${process.cwd()}/`).href;
-
- // This is the initial entry point to the program, and --input-type has
- // been passed as an option; but --input-type can only be used with
- // --eval, --print or STDIN string input. It is not allowed with file
- // input, to avoid user confusion over how expansive the effect of the
- // flag should be (i.e. entry point only, package scope surrounding the
- // entry point, etc.).
- if (typeFlag)
- throw new ERR_INPUT_TYPE_NOT_ALLOWED();
- }
-
- let url = moduleWrapResolve(specifier, parentURL);
-
- if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
- const real = realpathSync(fileURLToPath(url), {
- [internalFS.realpathCacheKey]: realpathCache
- });
- const old = url;
- url = pathToFileURL(real);
- url.search = old.search;
- url.hash = old.hash;
- }
-
- const ext = extname(url.pathname);
- let format;
- if (ext === '.js' || ext === '') {
- format = getPackageType(url.href) === TYPE_MODULE ? 'module' : 'commonjs';
- } else {
- format = extensionFormatMap[ext];
- }
- if (!format) {
- if (experimentalSpeciferResolution === 'node') {
- process.emitWarning(
- 'The Node.js specifier resolution in ESM is experimental.',
- 'ExperimentalWarning');
- format = legacyExtensionFormatMap[ext];
- } else {
- throw new ERR_UNKNOWN_FILE_EXTENSION(
- ext,
- fileURLToPath(url),
- fileURLToPath(parentURL));
- }
- }
- return { url: `${url}`, format };
-}
-
-module.exports = resolve;
diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js
new file mode 100644
index 00000000000000..2c215ab5378a40
--- /dev/null
+++ b/lib/internal/modules/esm/get_format.js
@@ -0,0 +1,77 @@
+'use strict';
+
+const { NativeModule } = require('internal/bootstrap/loaders');
+const { extname } = require('path');
+const { getOptionValue } = require('internal/options');
+
+const experimentalJsonModules = getOptionValue('--experimental-json-modules');
+const experimentalSpeciferResolution =
+ getOptionValue('--experimental-specifier-resolution');
+const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
+const { getPackageType } = internalBinding('module_wrap');
+const { URL, fileURLToPath } = require('internal/url');
+const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
+
+// const TYPE_NONE = 0;
+// const TYPE_COMMONJS = 1;
+const TYPE_MODULE = 2;
+
+const extensionFormatMap = {
+ '__proto__': null,
+ '.cjs': 'commonjs',
+ '.js': 'module',
+ '.mjs': 'module'
+};
+
+const legacyExtensionFormatMap = {
+ '__proto__': null,
+ '.cjs': 'commonjs',
+ '.js': 'commonjs',
+ '.json': 'commonjs',
+ '.mjs': 'module',
+ '.node': 'commonjs'
+};
+
+if (experimentalWasmModules)
+ extensionFormatMap['.wasm'] = legacyExtensionFormatMap['.wasm'] = 'wasm';
+
+if (experimentalJsonModules)
+ extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';
+
+function defaultGetFormat(url, context, defaultGetFormat) {
+ if (NativeModule.canBeRequiredByUsers(url)) {
+ return { format: 'builtin' };
+ }
+ const parsed = new URL(url);
+ if (parsed.protocol === 'data:') {
+ const [ , mime ] = /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/.exec(parsed.pathname) || [ null, null, null ];
+ const format = ({
+ '__proto__': null,
+ 'text/javascript': 'module',
+ 'application/json': experimentalJsonModules ? 'json' : null,
+ 'application/wasm': experimentalWasmModules ? 'wasm' : null
+ })[mime] || null;
+ return { format };
+ } else if (parsed.protocol === 'file:') {
+ const ext = extname(parsed.pathname);
+ let format;
+ if (ext === '.js' || ext === '') {
+ format = getPackageType(parsed.href) === TYPE_MODULE ?
+ 'module' : 'commonjs';
+ } else {
+ format = extensionFormatMap[ext];
+ }
+ if (!format) {
+ if (experimentalSpeciferResolution === 'node') {
+ process.emitWarning(
+ 'The Node.js specifier resolution in ESM is experimental.',
+ 'ExperimentalWarning');
+ format = legacyExtensionFormatMap[ext];
+ } else {
+ throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url));
+ }
+ }
+ return { format: format || null };
+ }
+}
+exports.defaultGetFormat = defaultGetFormat;
diff --git a/lib/internal/modules/esm/get_source.js b/lib/internal/modules/esm/get_source.js
new file mode 100644
index 00000000000000..18af566df90ae3
--- /dev/null
+++ b/lib/internal/modules/esm/get_source.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const { Buffer } = require('buffer');
+
+const fs = require('fs');
+const { URL } = require('url');
+const { promisify } = require('internal/util');
+const {
+ ERR_INVALID_URL,
+ ERR_INVALID_URL_SCHEME,
+} = require('internal/errors').codes;
+const readFileAsync = promisify(fs.readFile);
+
+const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;
+
+async function defaultGetSource(url, { format } = {}, defaultGetSource) {
+ const parsed = new URL(url);
+ if (parsed.protocol === 'file:') {
+ return {
+ source: await readFileAsync(parsed)
+ };
+ } else if (parsed.protocol === 'data:') {
+ const match = DATA_URL_PATTERN.exec(parsed.pathname);
+ if (!match) {
+ throw new ERR_INVALID_URL(url);
+ }
+ const [ , base64, body ] = match;
+ return {
+ source: Buffer.from(body, base64 ? 'base64' : 'utf8')
+ };
+ } else {
+ throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
+ }
+}
+exports.defaultGetSource = defaultGetSource;
diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js
index 255e5d2aba7bd8..6d9b267ffe5d67 100644
--- a/lib/internal/modules/esm/loader.js
+++ b/lib/internal/modules/esm/loader.js
@@ -13,18 +13,21 @@ const {
ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK,
ERR_UNKNOWN_MODULE_FORMAT
} = require('internal/errors').codes;
-const {
- URL,
- pathToFileURL
-} = require('url');
+const { URL, pathToFileURL } = require('internal/url');
const { validateString } = require('internal/validators');
const ModuleMap = require('internal/modules/esm/module_map');
const ModuleJob = require('internal/modules/esm/module_job');
-const defaultResolve = require('internal/modules/esm/default_resolve');
+const { defaultResolve } = require('internal/modules/esm/resolve');
+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 createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');
-const { translators } = require('internal/modules/esm/translators');
+const { translators } = require(
+ 'internal/modules/esm/translators');
const { getOptionValue } = require('internal/options');
const debug = require('internal/util/debuglog').debuglog('esm');
@@ -46,13 +49,23 @@ class Loader {
// The resolver has the signature
// (specifier : string, parentURL : string, defaultResolve)
- // -> Promise<{ url : string, format: string }>
+ // -> Promise<{ url : string }>
// where defaultResolve is ModuleRequest.resolve (having the same
// signature itself).
+ 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.
// If `.format` on the returned value is 'dynamic', .dynamicInstantiate
// will be used as described below.
- this._resolve = defaultResolve;
- // This hook is only called when resolve(...).format is 'dynamic' and
+ this._getFormat = defaultGetFormat;
+ // This hook is called just before the source code of an ES module file
+ // is loaded.
+ 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;
+ // This hook is only called when getFormat is 'dynamic' and
// has the signature
// (url : string) -> Promise<{ exports: { ... }, execute: function }>
// Where `exports` is an object whose property names define the exported
@@ -69,27 +82,35 @@ class Loader {
if (!isMain)
validateString(parentURL, 'parentURL');
- const resolved = await this._resolve(specifier, parentURL, defaultResolve);
-
- if (typeof resolved !== 'object')
+ const resolveResponse = await this._resolve(
+ specifier, { parentURL }, defaultResolve);
+ if (typeof resolveResponse !== 'object') {
throw new ERR_INVALID_RETURN_VALUE(
- 'object', 'loader resolve', resolved
- );
-
- const { url, format } = resolved;
+ 'object', 'loader resolve', resolveResponse);
+ }
- if (typeof url !== 'string')
+ const { url } = resolveResponse;
+ if (typeof url !== 'string') {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
- 'string', 'loader resolve', 'url', url
- );
+ 'string', 'loader resolve', 'url', url);
+ }
- if (typeof format !== 'string')
+ const getFormatResponse = await this._getFormat(
+ url, {}, defaultGetFormat);
+ if (typeof getFormatResponse !== 'object') {
+ throw new ERR_INVALID_RETURN_VALUE(
+ 'object', 'loader getFormat', getFormatResponse);
+ }
+
+ const { format } = getFormatResponse;
+ if (typeof format !== 'string') {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
- 'string', 'loader resolve', 'format', format
- );
+ 'string', 'loader getFormat', 'format', format);
+ }
- if (format === 'builtin')
+ if (format === 'builtin') {
return { url: `node:${url}`, format };
+ }
if (this._resolve !== defaultResolve) {
try {
@@ -101,13 +122,15 @@ class Loader {
}
}
- if (format !== 'dynamic' &&
+ if (this._resolve === defaultResolve &&
+ format !== 'dynamic' &&
!url.startsWith('file:') &&
!url.startsWith('data:')
- )
+ ) {
throw new ERR_INVALID_RETURN_PROPERTY(
'file: or data: url', 'loader resolve', 'url', url
);
+ }
return { url, format };
}
@@ -142,7 +165,7 @@ class Loader {
return module.getNamespace();
}
- hook({ resolve, dynamicInstantiate }) {
+ hook({ resolve, dynamicInstantiate, getFormat, getSource, transformSource }) {
// Use .bind() to avoid giving access to the Loader instance when called.
if (resolve !== undefined)
this._resolve = FunctionPrototypeBind(resolve, null);
@@ -150,6 +173,15 @@ class Loader {
this._dynamicInstantiate =
FunctionPrototypeBind(dynamicInstantiate, null);
}
+ if (getFormat !== undefined) {
+ this._getFormat = FunctionPrototypeBind(getFormat, null);
+ }
+ if (getSource !== undefined) {
+ this._getSource = FunctionPrototypeBind(getSource, null);
+ }
+ if (transformSource !== undefined) {
+ this._transformSource = FunctionPrototypeBind(transformSource, null);
+ }
}
async getModuleJob(specifier, parentURL) {
diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js
new file mode 100644
index 00000000000000..f1045871dddb72
--- /dev/null
+++ b/lib/internal/modules/esm/resolve.js
@@ -0,0 +1,72 @@
+'use strict';
+
+const {
+ SafeMap,
+} = primordials;
+
+const internalFS = require('internal/fs/utils');
+const { NativeModule } = require('internal/bootstrap/loaders');
+const { realpathSync } = require('fs');
+const { getOptionValue } = require('internal/options');
+
+const preserveSymlinks = getOptionValue('--preserve-symlinks');
+const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
+const typeFlag = getOptionValue('--input-type');
+const { resolve: moduleWrapResolve } = internalBinding('module_wrap');
+const { URL, pathToFileURL, fileURLToPath } = require('internal/url');
+const { ERR_INPUT_TYPE_NOT_ALLOWED,
+ ERR_UNSUPPORTED_ESM_URL_SCHEME } = require('internal/errors').codes;
+
+const realpathCache = new SafeMap();
+
+function defaultResolve(specifier, { parentURL } = {}, defaultResolve) {
+ let parsed;
+ try {
+ parsed = new URL(specifier);
+ if (parsed.protocol === 'data:') {
+ return {
+ url: specifier
+ };
+ }
+ } catch {}
+ if (parsed && parsed.protocol !== 'file:' && parsed.protocol !== 'data:')
+ throw new ERR_UNSUPPORTED_ESM_URL_SCHEME();
+ if (NativeModule.canBeRequiredByUsers(specifier)) {
+ return {
+ url: specifier
+ };
+ }
+ if (parentURL && parentURL.startsWith('data:')) {
+ // This is gonna blow up, we want the error
+ new URL(specifier, parentURL);
+ }
+
+ const isMain = parentURL === undefined;
+ if (isMain) {
+ parentURL = pathToFileURL(`${process.cwd()}/`).href;
+
+ // This is the initial entry point to the program, and --input-type has
+ // been passed as an option; but --input-type can only be used with
+ // --eval, --print or STDIN string input. It is not allowed with file
+ // input, to avoid user confusion over how expansive the effect of the
+ // flag should be (i.e. entry point only, package scope surrounding the
+ // entry point, etc.).
+ if (typeFlag)
+ throw new ERR_INPUT_TYPE_NOT_ALLOWED();
+ }
+
+ let url = moduleWrapResolve(specifier, parentURL);
+
+ if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
+ const real = realpathSync(fileURLToPath(url), {
+ [internalFS.realpathCacheKey]: realpathCache
+ });
+ const old = url;
+ url = pathToFileURL(real);
+ url.search = old.search;
+ url.hash = old.hash;
+ }
+
+ return { url: `${url}` };
+}
+exports.defaultResolve = defaultResolve;
diff --git a/lib/internal/modules/esm/transform_source.js b/lib/internal/modules/esm/transform_source.js
new file mode 100644
index 00000000000000..2d07dd3607fb66
--- /dev/null
+++ b/lib/internal/modules/esm/transform_source.js
@@ -0,0 +1,7 @@
+'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 99e4c014053202..9f3bcfb8e7db9d 100644
--- a/lib/internal/modules/esm/translators.js
+++ b/lib/internal/modules/esm/translators.js
@@ -9,26 +9,22 @@ const {
StringPrototypeReplace,
} = primordials;
-const { Buffer } = require('buffer');
-
const {
stripBOM,
loadNativeModule
} = require('internal/modules/cjs/helpers');
const CJSModule = require('internal/modules/cjs/loader').Module;
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 fs = require('fs');
const { fileURLToPath, URL } = require('url');
const { debuglog } = require('internal/util/debuglog');
-const { promisify, emitExperimentalWarning } = require('internal/util');
-const {
- ERR_INVALID_URL,
- ERR_INVALID_URL_SCHEME,
- ERR_UNKNOWN_BUILTIN_MODULE
-} = require('internal/errors').codes;
-const readFileAsync = promisify(fs.readFile);
+const { emitExperimentalWarning } = require('internal/util');
+const { ERR_UNKNOWN_BUILTIN_MODULE } = require('internal/errors').codes;
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
const moduleWrap = internalBinding('module_wrap');
const { ModuleWrap } = moduleWrap;
@@ -38,23 +34,6 @@ const debug = debuglog('esm');
const translators = new SafeMap();
exports.translators = translators;
-const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;
-function getSource(url) {
- const parsed = new URL(url);
- if (parsed.protocol === 'file:') {
- return readFileAsync(parsed);
- } else if (parsed.protocol === 'data:') {
- const match = DATA_URL_PATTERN.exec(parsed.pathname);
- if (!match) {
- throw new ERR_INVALID_URL(url);
- }
- const [ , base64, body ] = match;
- return Buffer.from(body, base64 ? 'base64' : 'utf8');
- } else {
- throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
- }
-}
-
function errPath(url) {
const parsed = new URL(url);
if (parsed.protocol === 'file:') {
@@ -77,7 +56,11 @@ async function importModuleDynamically(specifier, { url }) {
// Strategy for loading a standard JavaScript module
translators.set('module', async function moduleStrategy(url) {
- const source = `${await getSource(url)}`;
+ let { source } = await this._getSource(
+ url, { format: 'module' }, defaultGetSource);
+ source = `${source}`;
+ ({ source } = await this._transformSource(
+ source, { url, format: 'module' }, defaultTransformSource));
maybeCacheSourceMap(url, source);
debug(`Translating StandardModule ${url}`);
const module = new ModuleWrap(url, undefined, source, 0, 0);
@@ -150,7 +133,11 @@ translators.set('json', async function jsonStrategy(url) {
});
}
}
- const content = `${await getSource(url)}`;
+ let { source } = await this._getSource(
+ url, { format: 'json' }, defaultGetSource);
+ source = `${source}`;
+ ({ source } = await this._transformSource(
+ source, { url, format: 'json' }, defaultTransformSource));
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
@@ -164,7 +151,7 @@ translators.set('json', async function jsonStrategy(url) {
}
}
try {
- const exports = JSONParse(stripBOM(content));
+ const exports = JSONParse(stripBOM(source));
module = {
exports,
loaded: true
@@ -189,11 +176,14 @@ translators.set('json', async function jsonStrategy(url) {
// Strategy for loading a wasm module
translators.set('wasm', async function(url) {
emitExperimentalWarning('Importing Web Assembly modules');
- const buffer = await getSource(url);
+ let { source } = await this._getSource(
+ url, { format: 'wasm' }, defaultGetSource);
+ ({ source } = await this._transformSource(
+ source, { url, format: 'wasm' }, defaultTransformSource));
debug(`Translating WASMModule ${url}`);
let compiled;
try {
- compiled = await WebAssembly.compile(buffer);
+ compiled = await WebAssembly.compile(source);
} catch (err) {
err.message = errPath(url) + ': ' + err.message;
throw err;
diff --git a/node.gyp b/node.gyp
index 9389aa42464038..c46becb586e7ce 100644
--- a/node.gyp
+++ b/node.gyp
@@ -156,9 +156,12 @@
'lib/internal/modules/cjs/loader.js',
'lib/internal/modules/esm/loader.js',
'lib/internal/modules/esm/create_dynamic_module.js',
- 'lib/internal/modules/esm/default_resolve.js',
+ 'lib/internal/modules/esm/get_format.js',
+ 'lib/internal/modules/esm/get_source.js',
'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/test/es-module/test-esm-data-urls.js b/test/es-module/test-esm-data-urls.js
index 30ae55d01f38b8..61da442c9e081c 100644
--- a/test/es-module/test-esm-data-urls.js
+++ b/test/es-module/test-esm-data-urls.js
@@ -1,3 +1,4 @@
+// Flags: --experimental-json-modules
'use strict';
const common = require('../common');
const assert = require('assert');
diff --git a/test/es-module/test-esm-get-source-loader.mjs b/test/es-module/test-esm-get-source-loader.mjs
new file mode 100644
index 00000000000000..66bac969a47d8f
--- /dev/null
+++ b/test/es-module/test-esm-get-source-loader.mjs
@@ -0,0 +1,6 @@
+// Flags: --experimental-loader ./test/fixtures/es-module-loaders/get-source.mjs
+/* eslint-disable node-core/require-common-first, node-core/required-modules */
+import assert from 'assert';
+import { message } from '../fixtures/es-modules/message.mjs';
+
+assert.strictEqual(message, 'WOOHOO!');
diff --git a/test/es-module/test-esm-invalid-extension.js b/test/es-module/test-esm-invalid-extension.js
index 87b0c6691d3445..cca7704f7994f0 100644
--- a/test/es-module/test-esm-invalid-extension.js
+++ b/test/es-module/test-esm-invalid-extension.js
@@ -6,10 +6,8 @@ const { spawnSync } = require('child_process');
const fixture = fixtures.path('/es-modules/import-invalid-ext.mjs');
const child = spawnSync(process.execPath, [fixture]);
const errMsg = 'TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension';
-const importMsg = `imported from ${fixture}`;
assert.strictEqual(child.status, 1);
assert.strictEqual(child.signal, null);
assert.strictEqual(child.stdout.toString().trim(), '');
-assert(child.stderr.toString().includes(errMsg));
-assert(child.stderr.toString().includes(importMsg));
+assert.ok(child.stderr.toString().includes(errMsg));
diff --git a/test/es-module/test-esm-loader-get-format.mjs b/test/es-module/test-esm-loader-get-format.mjs
new file mode 100644
index 00000000000000..2a252439f91ce4
--- /dev/null
+++ b/test/es-module/test-esm-loader-get-format.mjs
@@ -0,0 +1,12 @@
+// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-get-format.mjs
+import { mustCall, mustNotCall } from '../common/index.mjs';
+import assert from 'assert';
+
+import('../fixtures/es-modules/package-type-module/extension.unknown')
+.then(
+ mustCall((ns) => {
+ assert.strictEqual(ns.default, 'unknown');
+ }),
+ // Do not use .catch; want exclusive or
+ mustNotCall(() => {})
+);
diff --git a/test/es-module/test-esm-loader-invalid-format.mjs b/test/es-module/test-esm-loader-invalid-format.mjs
index 75f5de83805394..64237f7af243af 100644
--- a/test/es-module/test-esm-loader-invalid-format.mjs
+++ b/test/es-module/test-esm-loader-invalid-format.mjs
@@ -4,8 +4,7 @@ import assert from 'assert';
import('../fixtures/es-modules/test-esm-ok.mjs')
.then(assert.fail, expectsError({
- code: 'ERR_INVALID_RETURN_PROPERTY_VALUE',
- message: 'Expected string to be returned for the "format" from the ' +
- '"loader resolve" function but got type undefined.'
+ code: 'ERR_UNKNOWN_MODULE_FORMAT',
+ message: /Unknown module format: esm/
}))
.then(mustCall());
diff --git a/test/es-module/test-esm-loader-invalid-url.mjs b/test/es-module/test-esm-loader-invalid-url.mjs
index 4007be052dd7c7..e9f04d0af4d4fe 100644
--- a/test/es-module/test-esm-loader-invalid-url.mjs
+++ b/test/es-module/test-esm-loader-invalid-url.mjs
@@ -4,9 +4,7 @@ import assert from 'assert';
import('../fixtures/es-modules/test-esm-ok.mjs')
.then(assert.fail, expectsError({
- code: 'ERR_INVALID_RETURN_PROPERTY',
- message: 'Expected a valid url to be returned for the "url" from the ' +
- '"loader resolve" function but got ' +
- '../fixtures/es-modules/test-esm-ok.mjs.'
+ code: 'ERR_INVALID_URL',
+ message: 'Invalid URL: ../fixtures/es-modules/test-esm-ok.mjs'
}))
.then(mustCall());
diff --git a/test/es-module/test-esm-loader-search.js b/test/es-module/test-esm-loader-search.js
index a7e675decca1bc..3c451409b356db 100644
--- a/test/es-module/test-esm-loader-search.js
+++ b/test/es-module/test-esm-loader-search.js
@@ -6,10 +6,12 @@
require('../common');
const assert = require('assert');
-const resolve = require('internal/modules/esm/default_resolve');
+const {
+ defaultResolve: resolve
+} = require('internal/modules/esm/resolve');
assert.throws(
- () => resolve('target', undefined),
+ () => resolve('target'),
{
code: 'ERR_MODULE_NOT_FOUND',
name: 'Error',
diff --git a/test/es-module/test-esm-transform-source-loader.mjs b/test/es-module/test-esm-transform-source-loader.mjs
new file mode 100644
index 00000000000000..e6d4affea495de
--- /dev/null
+++ b/test/es-module/test-esm-transform-source-loader.mjs
@@ -0,0 +1,6 @@
+// Flags: --experimental-loader ./test/fixtures/es-module-loaders/transform-source.mjs
+/* eslint-disable node-core/require-common-first, node-core/required-modules */
+import assert from 'assert';
+import { message } from '../fixtures/es-modules/message.mjs';
+
+assert.strictEqual(message, 'A MESSAGE');
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 a944c4fd5ebc67..9f1bc24560b87a 100644
--- a/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
+++ b/test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
@@ -1,7 +1,16 @@
import module from 'module';
+export function getFormat(url, context, defaultGetFormat) {
+ if (module.builtinModules.includes(url)) {
+ return {
+ format: 'dynamic'
+ };
+ }
+ return defaultGetFormat(url, context, defaultGetFormat);
+}
+
export function dynamicInstantiate(url) {
- const builtinInstance = module._load(url.substr(5));
+ const builtinInstance = module._load(url);
const builtinExports = ['default', ...Object.keys(builtinInstance)];
return {
exports: builtinExports,
@@ -12,13 +21,3 @@ export function dynamicInstantiate(url) {
}
};
}
-
-export function resolve(specifier, base, defaultResolver) {
- if (module.builtinModules.includes(specifier)) {
- return {
- url: `node:${specifier}`,
- format: 'dynamic'
- };
- }
- return defaultResolver(specifier, base);
-}
diff --git a/test/fixtures/es-module-loaders/example-loader.mjs b/test/fixtures/es-module-loaders/example-loader.mjs
index ed5b0d9be5940d..70f9f28f08e742 100644
--- a/test/fixtures/es-module-loaders/example-loader.mjs
+++ b/test/fixtures/es-module-loaders/example-loader.mjs
@@ -1,34 +1,44 @@
-import url from 'url';
+import { URL } from 'url';
import path from 'path';
import process from 'process';
import { builtinModules } from 'module';
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
-const baseURL = new url.URL('file://');
+const baseURL = new URL('file://');
baseURL.pathname = process.cwd() + '/';
-export function resolve(specifier, parentModuleURL = baseURL /*, defaultResolve */) {
+export function resolve(specifier, { parentURL = baseURL }, defaultResolve) {
if (builtinModules.includes(specifier)) {
return {
- url: specifier,
- format: 'builtin'
+ url: specifier
};
}
if (/^\.{1,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
// For node_modules support:
- // return defaultResolve(specifier, parentModuleURL);
+ // return defaultResolve(specifier, {parentURL}, defaultResolve);
throw new Error(
`imports must be URLs or begin with './', or '../'; '${specifier}' does not`);
}
- const resolved = new url.URL(specifier, parentModuleURL);
- const ext = path.extname(resolved.pathname);
+ const resolved = new URL(specifier, parentURL);
+ return {
+ url: resolved.href
+ };
+}
+
+export function getFormat(url, context, defaultGetFormat) {
+ if (builtinModules.includes(url)) {
+ return {
+ format: 'builtin'
+ };
+ }
+ const { pathname } = new URL(url);
+ const ext = path.extname(pathname);
if (!JS_EXTENSIONS.has(ext)) {
throw new Error(
`Cannot load file with non-JavaScript file extension ${ext}.`);
}
return {
- url: resolved.href,
format: 'module'
};
}
diff --git a/test/fixtures/es-module-loaders/get-source.mjs b/test/fixtures/es-module-loaders/get-source.mjs
new file mode 100644
index 00000000000000..e5a9c65201aa28
--- /dev/null
+++ b/test/fixtures/es-module-loaders/get-source.mjs
@@ -0,0 +1,10 @@
+export async function getSource(url, { format }, defaultGetSource) {
+ 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);
+ }
+}
diff --git a/test/fixtures/es-module-loaders/js-loader.mjs b/test/fixtures/es-module-loaders/js-loader.mjs
index 4b8a0fc365f3ac..2f79475e77e269 100644
--- a/test/fixtures/es-module-loaders/js-loader.mjs
+++ b/test/fixtures/es-module-loaders/js-loader.mjs
@@ -1,20 +1,9 @@
-import { URL } from 'url';
-import { builtinModules } from 'module';
-
-const baseURL = new URL('file://');
-baseURL.pathname = process.cwd() + '/';
-
-export function resolve (specifier, base = baseURL) {
- if (builtinModules.includes(specifier)) {
+export function getFormat(url, context, defaultGetFormat) {
+ // Load all .js files as ESM, regardless of package scope
+ if (url.endsWith('.js')) {
return {
- url: specifier,
- format: 'builtin'
- };
+ format: 'module'
+ }
}
- // load all dependencies as esm, regardless of file extension
- const url = new URL(specifier, base).href;
- return {
- url,
- format: 'module'
- };
+ return defaultGetFormat(url, context, defaultGetFormat);
}
diff --git a/test/fixtures/es-module-loaders/loader-get-format.mjs b/test/fixtures/es-module-loaders/loader-get-format.mjs
new file mode 100644
index 00000000000000..7ade70fca0ebe6
--- /dev/null
+++ b/test/fixtures/es-module-loaders/loader-get-format.mjs
@@ -0,0 +1,10 @@
+export async function getFormat(url, context, defaultGetFormat) {
+ try {
+ if (new URL(url).pathname.endsWith('.unknown')) {
+ return {
+ format: 'module'
+ };
+ }
+ } catch {}
+ return defaultGetFormat(url, context, defaultGetFormat);
+}
diff --git a/test/fixtures/es-module-loaders/loader-invalid-format.mjs b/test/fixtures/es-module-loaders/loader-invalid-format.mjs
index 17a0dcd04daad9..55ae1cec8ee926 100644
--- a/test/fixtures/es-module-loaders/loader-invalid-format.mjs
+++ b/test/fixtures/es-module-loaders/loader-invalid-format.mjs
@@ -1,8 +1,17 @@
-export async function resolve(specifier, parentModuleURL, defaultResolve) {
- if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
+export async function resolve(specifier, { parentURL }, defaultResolve) {
+ if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
return {
url: 'file:///asdf'
};
}
- return defaultResolve(specifier, parentModuleURL);
+ return defaultResolve(specifier, {parentURL}, defaultResolve);
+}
+
+export function getFormat(url, context, defaultGetFormat) {
+ if (url === 'file:///asdf') {
+ return {
+ format: 'esm'
+ }
+ }
+ return defaultGetFormat(url, context, defaultGetFormat);
}
diff --git a/test/fixtures/es-module-loaders/loader-invalid-url.mjs b/test/fixtures/es-module-loaders/loader-invalid-url.mjs
index f653155899d6fc..e7de0d4ed92378 100644
--- a/test/fixtures/es-module-loaders/loader-invalid-url.mjs
+++ b/test/fixtures/es-module-loaders/loader-invalid-url.mjs
@@ -1,10 +1,9 @@
/* eslint-disable node-core/required-modules */
-export async function resolve(specifier, parentModuleURL, defaultResolve) {
- if (parentModuleURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
+export async function resolve(specifier, { parentURL }, defaultResolve) {
+ if (parentURL && specifier === '../fixtures/es-modules/test-esm-ok.mjs') {
return {
- url: specifier,
- format: 'esm'
+ url: specifier
};
}
- return defaultResolve(specifier, parentModuleURL);
+ return defaultResolve(specifier, {parentURL}, defaultResolve);
}
diff --git a/test/fixtures/es-module-loaders/loader-shared-dep.mjs b/test/fixtures/es-module-loaders/loader-shared-dep.mjs
index 3acafcce1ecf7a..3576c074d52cec 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, base, defaultResolve) {
+export function resolve(specifier, { parentURL }, defaultResolve) {
assert.strictEqual(dep.format, 'module');
- return defaultResolve(specifier, base);
+ return defaultResolve(specifier, {parentURL}, defaultResolve);
}
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 e7c6c8ff345617..1a48231966ce5b 100644
--- a/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs
+++ b/test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs
@@ -1,6 +1,17 @@
-export async function resolve(specifier, parent, defaultResolve) {
+export async function resolve(specifier, { parentURL }, defaultResolve) {
if (specifier === 'unknown-builtin-module') {
- return { url: 'unknown-builtin-module', format: 'builtin' };
+ return {
+ url: 'unknown-builtin-module'
+ };
}
- return defaultResolve(specifier, parent);
-}
\ No newline at end of file
+ return defaultResolve(specifier, {parentURL}, defaultResolve);
+}
+
+export async function getFormat(url, context, defaultGetFormat) {
+ if (url === 'unknown-builtin-module') {
+ return {
+ format: 'builtin'
+ };
+ }
+ return defaultGetFormat(url, context, defaultGetFormat);
+}
diff --git a/test/fixtures/es-module-loaders/loader-with-dep.mjs b/test/fixtures/es-module-loaders/loader-with-dep.mjs
index 5afd3b2e212322..da7d44ae793e22 100644
--- a/test/fixtures/es-module-loaders/loader-with-dep.mjs
+++ b/test/fixtures/es-module-loaders/loader-with-dep.mjs
@@ -3,9 +3,9 @@ import {createRequire} from '../../common/index.mjs';
const require = createRequire(import.meta.url);
const dep = require('./loader-dep.js');
-export function resolve (specifier, base, defaultResolve) {
+export function resolve (specifier, { parentURL }, defaultResolve) {
return {
- url: defaultResolve(specifier, base).url,
+ url: defaultResolve(specifier, {parentURL}, defaultResolve).url,
format: dep.format
};
}
diff --git a/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs b/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs
index 6993747fcc0142..ec15eb0bb8fc24 100644
--- a/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs
+++ b/test/fixtures/es-module-loaders/missing-dynamic-instantiate-hook.mjs
@@ -1,6 +1,17 @@
-export function resolve(specifier, parentModule, defaultResolver) {
- if (specifier !== 'test') {
- return defaultResolver(specifier, parentModule);
+export function resolve(specifier, { parentURL }, defaultResolve) {
+ if (specifier === 'test') {
+ return {
+ url: 'file://'
+ };
}
- return { url: 'file://', format: 'dynamic' };
+ return defaultResolve(specifier, {parentURL}, defaultResolve);
+}
+
+export function getFormat(url, context, defaultGetFormat) {
+ if (url === 'file://') {
+ return {
+ format: 'dynamic'
+ }
+ }
+ return defaultGetFormat(url, context, defaultGetFormat);
}
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 d3eebcd47ec906..7b1d176e4537f6 100644
--- a/test/fixtures/es-module-loaders/not-found-assert-loader.mjs
+++ b/test/fixtures/es-module-loaders/not-found-assert-loader.mjs
@@ -3,13 +3,13 @@ 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, base, defaultResolve) {
+export async function resolve(specifier, { parentURL }, defaultResolve) {
if (mainLoad) {
mainLoad = false;
- return defaultResolve(specifier, base);
+ return defaultResolve(specifier, {parentURL}, defaultResolve);
}
try {
- await defaultResolve(specifier, base);
+ await defaultResolve(specifier, {parentURL}, defaultResolve);
}
catch (e) {
assert.strictEqual(e.code, 'ERR_MODULE_NOT_FOUND');
diff --git a/test/fixtures/es-module-loaders/transform-source.mjs b/test/fixtures/es-module-loaders/transform-source.mjs
new file mode 100644
index 00000000000000..ab147c34cb34fd
--- /dev/null
+++ b/test/fixtures/es-module-loaders/transform-source.mjs
@@ -0,0 +1,11 @@
+export async function transformSource(
+ source, { url, format }, defaultTransformSource) {
+ if (source && source.replace) {
+ 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);
+ }
+}
diff --git a/test/fixtures/es-modules/package-type-module/extension.unknown b/test/fixtures/es-modules/package-type-module/extension.unknown
index bd2b1aaa1ebece..ff62e978da69e3 100644
--- a/test/fixtures/es-modules/package-type-module/extension.unknown
+++ b/test/fixtures/es-modules/package-type-module/extension.unknown
@@ -1 +1 @@
-throw new Error('NO, NEVER');
+export default 'unknown';
diff --git a/test/message/esm_loader_not_found.out b/test/message/esm_loader_not_found.out
index b03b7641af072b..770ffdc1cb3559 100644
--- a/test/message/esm_loader_not_found.out
+++ b/test/message/esm_loader_not_found.out
@@ -1,11 +1,11 @@
(node:*) ExperimentalWarning: The ESM module loader is experimental.
(node:*) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
-internal/modules/esm/default_resolve.js:*
+internal/modules/esm/resolve.js:*
let url = moduleWrapResolve(specifier, parentURL);
^
Error: Cannot find package 'i-dont-exist' imported from *
- at Loader.resolve [as _resolve] (internal/modules/esm/default_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:*:*)
diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js
index 23d47c5e237ad1..5680017e0c8026 100644
--- a/test/parallel/test-bootstrap-modules.js
+++ b/test/parallel/test-bootstrap-modules.js
@@ -50,10 +50,13 @@ const expectedModules = new Set([
'NativeModule internal/modules/cjs/helpers',
'NativeModule internal/modules/cjs/loader',
'NativeModule internal/modules/esm/create_dynamic_module',
- 'NativeModule internal/modules/esm/default_resolve',
+ 'NativeModule internal/modules/esm/get_format',
+ 'NativeModule internal/modules/esm/get_source',
'NativeModule internal/modules/esm/loader',
'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',