diff --git a/.eslintignore b/.eslintignore
index 153ac6e24f731e..6f3d86e6bfac8a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -8,5 +8,7 @@ tools/lint-md/lint-md.mjs
benchmark/tmp
benchmark/fixtures
doc/**/*.js
+doc/changelogs/CHANGELOG_v1*.md
+!doc/changelogs/CHANGELOG_v18.md
!doc/api_assets/*.js
!.eslintrc.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 71c772b3be9e5c..9c31bf3da17dbf 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -18,7 +18,7 @@ const hacks = [
'eslint-plugin-jsdoc',
'eslint-plugin-markdown',
'@babel/eslint-parser',
- '@babel/plugin-syntax-import-assertions',
+ '@babel/plugin-syntax-import-attributes',
];
Module._findPath = (request, paths, isMain) => {
const r = ModuleFindPath(request, paths, isMain);
@@ -44,7 +44,10 @@ module.exports = {
parserOptions: {
babelOptions: {
plugins: [
- Module._findPath('@babel/plugin-syntax-import-assertions'),
+ [
+ Module._findPath('@babel/plugin-syntax-import-attributes'),
+ { deprecatedAssertSyntax: true },
+ ],
],
},
requireConfigFile: false,
@@ -53,10 +56,10 @@ module.exports = {
overrides: [
{
files: [
- 'test/es-module/test-esm-type-flag.js',
- 'test/es-module/test-esm-type-flag-alias.js',
'*.mjs',
'test/es-module/test-esm-example-loader.js',
+ 'test/es-module/test-esm-type-flag.js',
+ 'test/es-module/test-esm-type-flag-alias.js',
],
parserOptions: { sourceType: 'module' },
},
@@ -111,6 +114,14 @@ module.exports = {
},
] },
},
+ {
+ files: [
+ 'lib/internal/modules/**/*.js',
+ ],
+ rules: {
+ 'curly': 'error',
+ },
+ },
],
rules: {
// ESLint built-in rules
diff --git a/benchmark/esm/esm-loader-import.js b/benchmark/esm/esm-loader-import.js
new file mode 100644
index 00000000000000..9967cd95275469
--- /dev/null
+++ b/benchmark/esm/esm-loader-import.js
@@ -0,0 +1,45 @@
+// Tests the impact on eager operations required for policies affecting
+// general startup, does not test lazy operations
+'use strict';
+const fs = require('node:fs');
+const path = require('node:path');
+const common = require('../common.js');
+
+const tmpdir = require('../../test/common/tmpdir.js');
+const { pathToFileURL } = require('node:url');
+
+const benchmarkDirectory = pathToFileURL(path.resolve(tmpdir.path, 'benchmark-import'));
+
+const configs = {
+ n: [1e3],
+ specifier: [
+ 'data:text/javascript,{i}',
+ './relative-existing.js',
+ './relative-nonexistent.js',
+ 'node:prefixed-nonexistent',
+ 'node:os',
+ ],
+};
+
+const options = {
+ flags: ['--expose-internals'],
+};
+
+const bench = common.createBenchmark(main, configs, options);
+
+async function main(conf) {
+ tmpdir.refresh();
+
+ fs.mkdirSync(benchmarkDirectory, { recursive: true });
+ fs.writeFileSync(new URL('./relative-existing.js', benchmarkDirectory), '\n');
+
+ bench.start();
+
+ for (let i = 0; i < conf.n; i++) {
+ try {
+ await import(new URL(conf.specifier.replace('{i}', i), benchmarkDirectory));
+ } catch { /* empty */ }
+ }
+
+ bench.end(conf.n);
+}
diff --git a/doc/api/cli.md b/doc/api/cli.md
index f1124ab9cdaddd..54f747d8aaa18e 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -25,14 +25,16 @@ For more info about `node inspect`, see the [debugger][] documentation.
The program entry point is a specifier-like string. If the string is not an
absolute path, it's resolved as a relative path from the current working
-directory. That path is then resolved by [CommonJS][] module loader. If no
-corresponding file is found, an error is thrown.
+directory. That path is then resolved by [CommonJS][] module loader, or by the
+[ES module loader][Modules loaders] if [`--experimental-default-type=module`][]
+is passed. If no corresponding file is found, an error is thrown.
-If a file is found, its path will be passed to the [ECMAScript module loader][]
-under any of the following conditions:
+If a file is found, its path will be passed to the
+[ES module loader][Modules loaders] under any of the following conditions:
* The program was started with a command-line flag that forces the entry
- point to be loaded with ECMAScript module loader.
+ point to be loaded with ECMAScript module loader, such as `--import` or
+ [`--experimental-default-type=module`][].
* The file has an `.mjs` extension.
* The file does not have a `.cjs` extension, and the nearest parent
`package.json` file contains a top-level [`"type"`][] field with a value of
@@ -43,10 +45,11 @@ Otherwise, the file is loaded using the CommonJS module loader. See
### ECMAScript modules loader entry point caveat
-When loading [ECMAScript module loader][] loads the program entry point, the `node`
-command will only accept as input only files with `.js`, `.mjs`, or `.cjs`
-extensions; and with `.wasm` extensions when
-[`--experimental-wasm-modules`][] is enabled.
+When loading, the [ES module loader][Modules loaders] loads the program
+entry point, the `node` command will accept as input only files with `.js`,
+`.mjs`, or `.cjs` extensions; with `.wasm` extensions when
+[`--experimental-wasm-modules`][] is enabled; and with no extension when
+[`--experimental-default-type=module`][] is passed.
## Options
@@ -366,15 +369,54 @@ added: v17.6.0
Expose the [Web Crypto API][] on the global scope.
+### `--experimental-default-type=type`
+
+
+
+> Stability: 1.0 - Early development
+
+Define which module system, `module` or `commonjs`, to use for the following:
+
+* String input provided via `--eval` or STDIN, if `--input-type` is unspecified.
+
+* Files ending in `.js` or with no extension, if there is no `package.json` file
+ present in the same folder or any parent folder.
+
+* Files ending in `.js` or with no extension, if the nearest parent
+ `package.json` field lacks a `"type"` field; unless the `package.json` folder
+ or any parent folder is inside a `node_modules` folder.
+
+In other words, `--experimental-default-type=module` flips all the places where
+Node.js currently defaults to CommonJS to instead default to ECMAScript modules,
+with the exception of folders and subfolders below `node_modules`, for backward
+compatibility.
+
+Under `--experimental-default-type=module` and `--experimental-wasm-modules`,
+files with no extension will be treated as WebAssembly if they begin with the
+WebAssembly magic number (`\0asm`); otherwise they will be treated as ES module
+JavaScript.
+
### `--experimental-import-meta-resolve`
-Enable experimental `import.meta.resolve()` support.
+Enable experimental `import.meta.resolve()` parent URL support, which allows
+passing a second `parentURL` argument for contextual resolution.
+
+Previously gated the entire `import.meta.resolve` feature.
### `--experimental-loader=module`
@@ -387,7 +429,11 @@ changes:
`--experimental-loader`.
-->
-Specify the `module` of a custom experimental [ECMAScript module loader][].
+> This flag is discouraged and may be removed in a future version of Node.js.
+> Please use
+> [`--import` with `register()`][module customization hooks: enabling] instead.
+
+Specify the `module` containing exported [module customization hooks][].
`module` may be any string accepted as an [`import` specifier][].
### `--experimental-network-imports`
@@ -1910,6 +1956,7 @@ Node.js options that are allowed are:
* `--enable-network-family-autoselection`
* `--enable-source-maps`
* `--experimental-abortcontroller`
+* `--experimental-default-type`
* `--experimental-global-customevent`
* `--experimental-global-webcrypto`
* `--experimental-import-meta-resolve`
@@ -2357,8 +2404,9 @@ done
[CommonJS module]: modules.md
[CustomEvent Web API]: https://dom.spec.whatwg.org/#customevent
[ECMAScript module]: esm.md#modules-ecmascript-modules
-[ECMAScript module loader]: esm.md#loaders
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
+[Module customization hooks]: module.md#customization-hooks
+[Module customization hooks: enabling]: module.md#enabling
[Modules loaders]: packages.md#modules-loaders
[Node.js issue tracker]: https://github.com/nodejs/node/issues
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html
@@ -2372,6 +2420,7 @@ done
[`"type"`]: packages.md#type
[`--cpu-prof-dir`]: #--cpu-prof-dir
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
+[`--experimental-default-type=module`]: #--experimental-default-typetype
[`--experimental-wasm-modules`]: #--experimental-wasm-modules
[`--heap-prof-dir`]: #--heap-prof-dir
[`--import`]: #--importmodule
diff --git a/doc/api/errors.md b/doc/api/errors.md
index c589fcd375136d..ecfb20864c37d1 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1762,7 +1762,8 @@ added:
- v16.14.0
-->
-An import assertion has failed, preventing the specified module to be imported.
+An import `type` attribute was provided, but the specified module is of a
+different type.
@@ -1774,7 +1775,7 @@ added:
- v16.14.0
-->
-An import assertion is missing, preventing the specified module to be imported.
+An import attribute is missing, preventing the specified module to be imported.
@@ -1786,7 +1787,17 @@ added:
- v16.14.0
-->
-An import assertion is not supported by this version of Node.js.
+An import attribute is not supported by this version of Node.js.
+
+
+
+### `ERR_IMPORT_ATTRIBUTE_UNSUPPORTED`
+
+
+
+An import attribute is not supported by this version of Node.js.
diff --git a/doc/api/esm.md b/doc/api/esm.md
index 0dd17d1950dd94..88d58735707123 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -7,21 +7,24 @@
@@ -230,17 +234,28 @@ absolute URL strings.
import fs from 'node:fs/promises';
```
-## Import assertions
+
+
+## Import attributes
-> Stability: 1 - Experimental
+> Stability: 1.1 - Active development
-The [Import Assertions proposal][] adds an inline syntax for module import
+> This feature was previously named "Import assertions", and using the `assert`
+> keyword instead of `with`. Because the version of V8 on this release line does
+> not support the `with` keyword, you need to keep using `assert` to support
+> this version of Node.js.
+
+The [Import Attributes proposal][] adds an inline syntax for module import
statements to pass on more information alongside the module specifier.
```js
@@ -250,10 +265,10 @@ const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
```
-Node.js supports the following `type` values, for which the assertion is
+Node.js supports the following `type` values, for which the attribute is
mandatory:
-| Assertion `type` | Needed for |
+| Attribute `type` | Needed for |
| ---------------- | ---------------- |
| `'json'` | [JSON modules][] |
@@ -318,13 +333,24 @@ import { readFileSync } from 'node:fs';
const buffer = readFileSync(new URL('./data.proto', import.meta.url));
```
-### `import.meta.resolve(specifier[, parent])`
+### `import.meta.resolve(specifier)`
-
-> Stability: 1 - Experimental
-
-This feature is only available with the `--experimental-import-meta-resolve`
-command flag enabled.
+> Stability: 1.2 - Release candidate
-* `specifier` {string} The module specifier to resolve relative to `parent`.
-* `parent` {string|URL} The absolute parent module URL to resolve from. If none
- is specified, the value of `import.meta.url` is used as the default.
-* Returns: {Promise}
+* `specifier` {string} The module specifier to resolve relative to the
+ current module.
+* Returns: {string} The absolute URL string that the specifier would resolve to.
-Provides a module-relative resolution function scoped to each module, returning
-the URL string.
-
-
+[`import.meta.resolve`][] is a module-relative resolution function scoped to
+each module, returning the URL string.
```js
-const dependencyAsset = await import.meta.resolve('component-lib/asset.css');
+const dependencyAsset = import.meta.resolve('component-lib/asset.css');
+// file:///app/node_modules/component-lib/asset.css
+import.meta.resolve('./dep.js');
+// file:///app/dep.js
```
-`import.meta.resolve` also accepts a second argument which is the parent module
-from which to resolve from:
+All features of the Node.js module resolution are supported. Dependency
+resolutions are subject to the permitted exports resolutions within the package.
-
+**Caveats**:
-```js
-await import.meta.resolve('./dep', import.meta.url);
-```
+* This can result in synchronous file-system operations, which
+ can impact performance similarly to `require.resolve`.
+* This feature is not available within custom loaders (it would
+ create a deadlock).
+
+**Non-standard API**:
-This function is asynchronous because the ES module resolver in Node.js is
-allowed to be asynchronous.
+When using the `--experimental-import-meta-resolve` flag, that function accepts
+a second argument:
+
+* `parent` {string|URL} An optional absolute parent module URL to resolve from.
+ **Default:** `import.meta.url`
## Interoperability with CommonJS
@@ -497,8 +526,8 @@ They can instead be loaded with [`module.createRequire()`][] or
Relative resolution can be handled via `new URL('./local', import.meta.url)`.
-For a complete `require.resolve` replacement, there is a flagged experimental
-[`import.meta.resolve`][] API.
+For a complete `require.resolve` replacement, there is the
+[import.meta.resolve][] API.
Alternatively `module.createRequire()` can be used.
@@ -509,8 +538,8 @@ if this behavior is desired.
#### No `require.extensions`
-`require.extensions` is not used by `import`. The expectation is that loader
-hooks can provide this workflow in the future.
+`require.extensions` is not used by `import`. Module customization hooks can
+provide a replacement.
#### No `require.cache`
@@ -529,7 +558,7 @@ JSON files can be referenced by `import`:
import packageConfig from './package.json' assert { type: 'json' };
```
-The `assert { type: 'json' }` syntax is mandatory; see [Import Assertions][].
+The `assert { type: 'json' }` syntax is mandatory; see [Import Attributes][].
The imported JSON only exposes a `default` export. There is no support for named
exports. A cache entry is created in the CommonJS cache to avoid duplication.
@@ -678,533 +707,8 @@ of Node.js applications.
## Loaders
-
-
-> Stability: 1 - Experimental
-
-> This API is currently being redesigned and will still change.
-
-
-
-To customize the default module resolution, loader hooks can optionally be
-provided via a `--experimental-loader ./loader-name.mjs` argument to Node.js.
-
-When hooks are used they apply to each subsequent loader, the entry point, and
-all `import` calls. They won't apply to `require` calls; those still follow
-[CommonJS][] rules.
-
-Loaders follow the pattern of `--require`:
-
-```console
-node \
- --experimental-loader unpkg \
- --experimental-loader http-to-https \
- --experimental-loader cache-buster
-```
-
-These are called in the following sequence: `cache-buster` calls
-`http-to-https` which calls `unpkg`.
-
-### Hooks
-
-Hooks are part of a chain, even if that chain consists of only one custom
-(user-provided) hook and the default hook, which is always present. Hook
-functions nest: each one must always return a plain object, and chaining happens
-as a result of each function calling `next()`, which is a reference
-to the subsequent loader’s hook.
-
-A hook that returns a value lacking a required property triggers an exception.
-A hook that returns without calling `next()` _and_ without returning
-`shortCircuit: true` also triggers an exception. These errors are to help
-prevent unintentional breaks in the chain.
-
-#### `resolve(specifier, context, nextResolve)`
-
-
-
-> 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\[]} Export conditions of the relevant `package.json`
- * `importAssertions` {Object} An object whose key-value pairs represent the
- assertions for the module to import
- * `parentURL` {string|undefined} The module importing this one, or undefined
- if this is the Node.js entry point
-* `nextResolve` {Function} The subsequent `resolve` hook in the chain, or the
- Node.js default `resolve` hook after the last user-supplied `resolve` hook
- * `specifier` {string}
- * `context` {Object}
-* Returns: {Object}
- * `format` {string|null|undefined} A hint to the load hook (it might be
- ignored)
- `'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'`
- * `importAssertions` {Object|undefined} The import assertions to use when
- caching the module (optional; if excluded the input will be used)
- * `shortCircuit` {undefined|boolean} A signal that this hook intends to
- terminate the chain of `resolve` hooks. **Default:** `false`
- * `url` {string} The absolute URL to which this input resolves
-
-The `resolve` hook chain is responsible for telling Node.js where to find and
-how to cache a given `import` statement or expression. It can optionally return
-its format (such as `'module'`) as a hint to the `load` hook. If a format is
-specified, the `load` hook is ultimately responsible for providing the final
-`format` value (and it is free to ignore the hint provided by `resolve`); if
-`resolve` provides a `format`, a custom `load` hook is required even if only to
-pass the value to the Node.js default `load` hook.
-
-Import type assertions are part of the cache key for saving loaded modules into
-the internal module cache. The `resolve` hook is responsible for
-returning an `importAssertions` object if the module should be cached with
-different assertions than were present in the source code.
-
-The `conditions` property in `context` is an array of conditions for
-[package exports conditions][Conditional Exports] that apply to this resolution
-request. They can be used for looking up conditional mappings elsewhere or to
-modify the list when calling the default resolution logic.
-
-The current [package exports conditions][Conditional Exports] are always 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.
-
-```js
-export async function resolve(specifier, context, nextResolve) {
- const { parentURL = null } = context;
-
- 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 {
- shortCircuit: true,
- url: parentURL ?
- new URL(specifier, 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 nextResolve(specifier, {
- ...context,
- conditions: [...context.conditions, 'another-condition'],
- });
- }
-
- // Defer to the next hook in the chain, which would be the
- // Node.js default resolve if this is the last user-specified loader.
- return nextResolve(specifier);
-}
-```
-
-#### `load(url, context, nextLoad)`
-
-
-
-> The loaders API is being redesigned. This hook may disappear or its
-> signature may change. Do not rely on the API described below.
-
-> In a previous version of this API, this was split across 3 separate, now
-> deprecated, hooks (`getFormat`, `getSource`, and `transformSource`).
-
-* `url` {string} The URL returned by the `resolve` chain
-* `context` {Object}
- * `conditions` {string\[]} Export conditions of the relevant `package.json`
- * `format` {string|null|undefined} The format optionally supplied by the
- `resolve` hook chain
- * `importAssertions` {Object}
-* `nextLoad` {Function} The subsequent `load` hook in the chain, or the
- Node.js default `load` hook after the last user-supplied `load` hook
- * `specifier` {string}
- * `context` {Object}
-* Returns: {Object}
- * `format` {string}
- * `shortCircuit` {undefined|boolean} A signal that this hook intends to
- terminate the chain of `resolve` hooks. **Default:** `false`
- * `source` {string|ArrayBuffer|TypedArray} The source for Node.js to evaluate
-
-The `load` hook provides a way to define a custom method of determining how
-a URL should be interpreted, retrieved, and parsed. It is also in charge of
-validating the import assertion.
-
-The final value of `format` must be one of the following:
-
-| `format` | Description | Acceptable types for `source` returned by `load` |
-| ------------ | ------------------------------ | ----------------------------------------------------- |
-| `'builtin'` | Load a Node.js builtin module | Not applicable |
-| `'commonjs'` | Load a Node.js CommonJS module | Not applicable |
-| `'json'` | Load a JSON file | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][] } |
-| `'module'` | Load an ES module | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][] } |
-| `'wasm'` | Load a WebAssembly module | { [`ArrayBuffer`][], [`TypedArray`][] } |
-
-The value of `source` is ignored for type `'builtin'` because currently it is
-not possible to replace the value of a Node.js builtin (core) module. The value
-of `source` is ignored for type `'commonjs'` because the CommonJS module loader
-does not provide a mechanism for the ES module loader to override the
-[CommonJS module return value](#commonjs-namespaces). This limitation might be
-overcome in the future.
-
-> **Caveat**: The ESM `load` hook and namespaced exports from CommonJS modules
-> are incompatible. Attempting to use them together will result in an empty
-> object from the import. This may be addressed in the future.
-
-> These types all correspond to classes defined in ECMAScript.
-
-* The specific [`ArrayBuffer`][] object is a [`SharedArrayBuffer`][].
-* The specific [`TypedArray`][] object is a [`Uint8Array`][].
-
-If the source value of a text-based format (i.e., `'json'`, `'module'`)
-is not a string, it is converted to a string using [`util.TextDecoder`][].
-
-The `load` 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. It could also be used to map an unrecognized
-format to a supported one, for example `yaml` to `module`.
-
-```js
-export async function load(url, context, nextLoad) {
- const { format } = context;
-
- if (Math.random() > 0.5) { // Some condition
- /*
- For some or all URLs, do some custom logic for retrieving the source.
- Always return an object of the form {
- format: ,
- source: ,
- }.
- */
- return {
- format,
- shortCircuit: true,
- source: '...',
- };
- }
-
- // Defer to the next hook in the chain.
- return nextLoad(url);
-}
-```
-
-In a more advanced scenario, this can also be used to transform an unsupported
-source to a supported one (see [Examples](#examples) below).
-
-#### `globalPreload()`
-
-
-
-> The loaders API is being redesigned. This hook may disappear or its
-> signature may change. Do not rely on the API described below.
-
-> In a previous version of this API, this hook was named
-> `getGlobalPreloadCode`.
-
-* `context` {Object} Information to assist the preload code
- * `port` {MessagePort}
-* Returns: {string} Code to run before application startup
-
-Sometimes it might be necessary to run some code inside of the same global
-scope that the application runs in. This hook allows the return of a string
-that is run as a sloppy-mode script on startup.
-
-Similar to how CommonJS wrappers work, the code runs in an implicit function
-scope. The only argument is a `require`-like function that can be used to load
-builtins like "fs": `getBuiltin(request: string)`.
-
-If the code needs more advanced `require` features, it has to construct
-its own `require` using `module.createRequire()`.
-
-```js
-export function globalPreload(context) {
- return `\
-globalThis.someInjectedProperty = 42;
-console.log('I just set some globals!');
-
-const { createRequire } = getBuiltin('module');
-const { cwd } = getBuiltin('process');
-
-const require = createRequire(cwd() + '/');
-// [...]
-`;
-}
-```
-
-In order to allow communication between the application and the loader, another
-argument is provided to the preload code: `port`. This is available as a
-parameter to the loader hook and inside of the source text returned by the hook.
-Some care must be taken in order to properly call [`port.ref()`][] and
-[`port.unref()`][] to prevent a process from being in a state where it won't
-close normally.
-
-```js
-/**
- * This example has the application context send a message to the loader
- * and sends the message back to the application context
- */
-export function globalPreload({ port }) {
- port.on('message', (msg) => {
- port.postMessage(msg);
- });
- return `\
- port.postMessage('console.log("I went to the Loader and back");');
- port.on('message', (data) => {
- eval(data);
- });
- `;
-}
-```
-
-### Examples
-
-The various loader hooks can be used together to accomplish wide-ranging
-customizations of the Node.js code loading and evaluation behaviors.
-
-#### HTTPS loader
-
-In current Node.js, specifiers starting with `https://` are experimental (see
-[HTTPS and HTTP imports][]).
-
-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 'node:https';
-
-export function load(url, context, nextLoad) {
- // 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.setEncoding('utf8');
- res.on('data', (chunk) => data += chunk);
- res.on('end', () => resolve({
- // This example assumes all network-provided JavaScript is ES module
- // code.
- format: 'module',
- shortCircuit: true,
- source: data,
- }));
- }).on('error', (err) => reject(err));
- });
- }
-
- // Let Node.js handle all other URLs.
- return nextLoad(url);
-}
-```
-
-```js
-// main.mjs
-import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
-
-console.log(VERSION);
-```
-
-With the preceding loader, running
-`node --experimental-loader ./https-loader.mjs ./main.mjs`
-prints 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 [`load` hook][load hook].
-
-This is 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 { readFile } from 'node:fs/promises';
-import { dirname, extname, resolve as resolvePath } from 'node:path';
-import { cwd } from 'node:process';
-import { fileURLToPath, pathToFileURL } from 'node:url';
-import CoffeeScript from 'coffeescript';
-
-const baseURL = pathToFileURL(`${cwd()}/`).href;
-
-export async function load(url, context, nextLoad) {
- if (extensionsRegex.test(url)) {
- // Now that we patched resolve to let CoffeeScript URLs through, we need to
- // tell Node.js what format such URLs should be interpreted as. Because
- // CoffeeScript transpiles into JavaScript, it should be one of the two
- // JavaScript formats: 'commonjs' or 'module'.
-
- // CoffeeScript files can be either CommonJS or ES modules, so we want any
- // CoffeeScript file to be treated by Node.js the same as a .js file at the
- // same location. To determine how Node.js would interpret an arbitrary .js
- // file, search up the file system for the nearest parent package.json file
- // and read its "type" field.
- const format = await getPackageType(url);
- // When a hook returns a format of 'commonjs', `source` is ignored.
- // To handle CommonJS files, a handler needs to be registered with
- // `require.extensions` in order to process the files with the CommonJS
- // loader. Avoiding the need for a separate CommonJS handler is a future
- // enhancement planned for ES module loaders.
- if (format === 'commonjs') {
- return {
- format,
- shortCircuit: true,
- };
- }
-
- const { source: rawSource } = await nextLoad(url, { ...context, format });
- // This hook converts CoffeeScript source code into JavaScript source code
- // for all imported CoffeeScript files.
- const transformedSource = coffeeCompile(rawSource.toString(), url);
-
- return {
- format,
- shortCircuit: true,
- source: transformedSource,
- };
- }
-
- // Let Node.js handle all other URLs.
- return nextLoad(url);
-}
-
-async function getPackageType(url) {
- // `url` is only a file path during the first iteration when passed the
- // resolved url from the load() hook
- // an actual file path from load() will contain a file extension as it's
- // required by the spec
- // this simple truthy check for whether `url` contains a file extension will
- // work for most projects but does not cover some edge-cases (such as
- // extensionless files or a url ending in a trailing space)
- const isFilePath = !!extname(url);
- // If it is a file path, get the directory it's in
- const dir = isFilePath ?
- dirname(fileURLToPath(url)) :
- url;
- // Compose a file path to a package.json in the same directory,
- // which may or may not exist
- const packagePath = resolvePath(dir, 'package.json');
- // Try to read the possibly nonexistent package.json
- const type = await readFile(packagePath, { encoding: 'utf8' })
- .then((filestring) => JSON.parse(filestring).type)
- .catch((err) => {
- if (err?.code !== 'ENOENT') console.error(err);
- });
- // Ff package.json existed and contained a `type` field with a value, voila
- if (type) return type;
- // Otherwise, (if not at the root) continue checking the next directory up
- // If at the root, stop and return false
- return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
-}
-```
-
-```coffee
-# main.coffee
-import { scream } from './scream.coffee'
-console.log scream 'hello, world'
-
-import { version } from 'node:process'
-console.log "Brought to you by Node.js version #{version}"
-```
-
-```coffee
-# scream.coffee
-export scream = (str) -> str.toUpperCase()
-```
-
-With the preceding loader, running
-`node --experimental-loader ./coffeescript-loader.mjs main.coffee`
-causes `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.
-
-#### "import map" loader
-
-The previous two loaders defined `load` hooks. This is an example of a loader
-that does its work via the `resolve` hook. This loader reads an
-`import-map.json` file that specifies which specifiers to override to another
-URL (this is a very simplistic implemenation of a small subset of the
-"import maps" specification).
-
-```js
-// import-map-loader.js
-import fs from 'node:fs/promises';
-
-const { imports } = JSON.parse(await fs.readFile('import-map.json'));
-
-export async function resolve(specifier, context, nextResolve) {
- if (Object.hasOwn(imports, specifier)) {
- return nextResolve(imports[specifier], context);
- }
-
- return nextResolve(specifier, context);
-}
-```
-
-Let's assume we have these files:
-
-```js
-// main.js
-import 'a-module';
-```
-
-```json
-// import-map.json
-{
- "imports": {
- "a-module": "./some-module.js"
- }
-}
-```
-
-```js
-// some-module.js
-console.log('some module!');
-```
-
-If you run `node --experimental-loader ./import-map-loader.js main.js`
-the output will be `some module!`.
+The former Loaders documentation is now at
+[Modules: Customization hooks][Module customization hooks].
## Resolution and loading algorithm
@@ -1515,8 +1019,12 @@ _isImports_, _conditions_)
> 5. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
> 6. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
> 7. If _pjson?.type_ exists and is _"module"_, then
-> 1. If _url_ ends in _".js"_, then
-> 1. Return _"module"_.
+> 1. If _url_ ends in _".js"_ or has no file extension, then
+> 1. If `--experimental-wasm-modules` is enabled and the file at _url_
+> contains the header for a WebAssembly module, then
+> 1. Return _"wasm"_.
+> 2. Otherwise,
+> 1. Return _"module"_.
> 2. Return **undefined**.
> 8. Otherwise,
> 1. Return **undefined**.
@@ -1547,8 +1055,8 @@ _isImports_, _conditions_)
> Stability: 1 - Experimental
> Do not rely on this flag. We plan to remove it once the
-> [Loaders API][] has advanced to the point that equivalent functionality can
-> be achieved via custom loaders.
+> [Module customization hooks][] have advanced to the point that equivalent
+> functionality can be achieved via custom hooks.
The current specifier resolution does not support all default behavior of
the CommonJS loader. One of the behavior differences is automatic resolution
@@ -1575,43 +1083,34 @@ success!
[6.1.7 Array Index]: https://tc39.es/ecma262/#integer-index
[Addons]: addons.md
[CommonJS]: modules.md
-[Conditional exports]: packages.md#conditional-exports
[Core modules]: modules.md#core-modules
[Determining module system]: packages.md#determining-module-system
[Dynamic `import()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
[ES Module Integration Proposal for WebAssembly]: https://github.com/webassembly/esm-integration
-[HTTPS and HTTP imports]: #https-and-http-imports
-[Import Assertions]: #import-assertions
-[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
+[Import Attributes]: #import-attributes
+[Import Attributes proposal]: https://github.com/tc39/proposal-import-attributes
[JSON modules]: #json-modules
-[Loaders API]: #loaders
+[Module customization hooks]: module.md#customization-hooks
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
[Terminology]: #terminology
[URL]: https://url.spec.whatwg.org/
[`"exports"`]: packages.md#exports
[`"type"`]: packages.md#type
+[`--experimental-default-type`]: cli.md#--experimental-default-typetype
[`--input-type`]: cli.md#--input-typetype
-[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
-[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
-[`TypedArray`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
-[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
[`data:` URLs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
[`export`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
[`import()`]: #import-expressions
-[`import.meta.resolve`]: #importmetaresolvespecifier-parent
+[`import.meta.resolve`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve
[`import.meta.url`]: #importmetaurl
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
[`module.createRequire()`]: module.md#modulecreaterequirefilename
[`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
-[`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref
-[`port.unref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portunref
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
-[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
-[`util.TextDecoder`]: util.md#class-utiltextdecoder
[cjs-module-lexer]: https://github.com/nodejs/cjs-module-lexer/tree/1.2.2
-[custom https loader]: #https-loader
-[load hook]: #loadurl-context-nextload
+[custom https loader]: module.md#import-from-https
+[import.meta.resolve]: #importmetaresolvespecifier
[percent-encoded]: url.md#percent-encoding-in-urls
[special scheme]: https://url.spec.whatwg.org/#special-scheme
[status code]: process.md#exit-codes
diff --git a/doc/api/fs.md b/doc/api/fs.md
index 893eb7befb7fe5..711ed4a86da6ed 100644
--- a/doc/api/fs.md
+++ b/doc/api/fs.md
@@ -1153,6 +1153,9 @@ makeDirectory().catch(console.error);
-* `prefix` {string}
+* `prefix` {string|Buffer|URL}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* Returns: {Promise} Fulfills with a string containing the file system path
@@ -3225,6 +3228,9 @@ See the POSIX mkdir(2) documentation for more details.
-* `prefix` {string}
+* `prefix` {string|Buffer|URL}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* `callback` {Function}
@@ -5478,6 +5484,9 @@ See the POSIX mkdir(2) documentation for more details.
-* `prefix` {string}
+* `prefix` {string|Buffer|URL}
* `options` {string|Object}
* `encoding` {string} **Default:** `'utf8'`
* Returns: {string}
diff --git a/doc/api/module.md b/doc/api/module.md
index cb0b27cb612be7..61776aefbebeef 100644
--- a/doc/api/module.md
+++ b/doc/api/module.md
@@ -78,6 +78,33 @@ isBuiltin('fs'); // true
isBuiltin('wss'); // false
```
+### `module.register(specifier[, parentURL][, options])`
+
+
+
+> Stability: 1.1 - Active development
+
+* `specifier` {string|URL} Customization hooks to be registered; this should be
+ the same string that would be passed to `import()`, except that if it is
+ relative, it is resolved relative to `parentURL`.
+* `parentURL` {string|URL} If you want to resolve `specifier` relative to a base
+ URL, such as `import.meta.url`, you can pass that URL here. **Default:**
+ `'data:'`
+* `options` {Object}
+ * `data` {any} Any arbitrary, cloneable JavaScript value to pass into the
+ [`initialize`][] hook.
+ * `transferList` {Object\[]} [transferrable objects][] to be passed into the
+ `initialize` hook.
+
+Register a module that exports [hooks][] that customize Node.js module
+resolution and loading behavior. See [Customization hooks][].
+
### `module.syncBuiltinESMExports()`
+
+> Stability: 1.1 - Active development
+
+
+
+
+
+### Enabling
+
+Module resolution and loading can be customized by registering a file which
+exports a set of hooks. This can be done using the [`register`][] method
+from `node:module`, which you can run before your application code by
+using the `--import` flag:
+
+```bash
+node --import ./register-hooks.js ./my-app.js
+```
+
+```mjs
+// register-hooks.js
+import { register } from 'node:module';
+
+register('./hooks.mjs', import.meta.url);
+```
+
+```cjs
+// register-hooks.js
+const { register } = require('node:module');
+const { pathToFileURL } = require('node:url');
+
+register('./hooks.mjs', pathToFileURL(__filename));
+```
+
+The file passed to `--import` can also be an export from a dependency:
+
+```bash
+node --import some-package/register ./my-app.js
+```
+
+Where `some-package` has an [`"exports"`][] field defining the `/register`
+export to map to a file that calls `register()`, like the following `register-hooks.js`
+example.
+
+Using `--import` ensures that the hooks are registered before any application
+files are imported, including the entry point of the application. Alternatively,
+`register` can be called from the entry point, but dynamic `import()` must be
+used for any code that should be run after the hooks are registered:
+
+```mjs
+import { register } from 'node:module';
+
+register('http-to-https', import.meta.url);
+
+// Because this is a dynamic `import()`, the `http-to-https` hooks will run
+// to handle `./my-app.js` and any other files it imports or requires.
+await import('./my-app.js');
+```
+
+```cjs
+const { register } = require('node:module');
+const { pathToFileURL } = require('node:url');
+
+register('http-to-https', pathToFileURL(__filename));
+
+// Because this is a dynamic `import()`, the `http-to-https` hooks will run
+// to handle `./my-app.js` and any other files it imports or requires.
+import('./my-app.js');
+```
+
+In this example, we are registering the `http-to-https` hooks, but they will
+only be available for subsequently imported modules—in this case, `my-app.js`
+and anything it references via `import` (and optionally `require`). If the
+`import('./my-app.js')` had instead been a static `import './my-app.js'`, the
+app would have _already_ been loaded **before** the `http-to-https` hooks were
+registered. This due to the ES modules specification, where static imports are
+evaluated from the leaves of the tree first, then back to the trunk. There can
+be static imports _within_ `my-app.js`, which will not be evaluated until
+`my-app.js` is dynamically imported.
+
+`my-app.js` can also be CommonJS. Customization hooks will run for any
+modules that it references via `import` (and optionally `require`).
+
+Finally, if all you want to do is register hooks before your app runs and you
+don't want to create a separate file for that purpose, you can pass a `data:`
+URL to `--import`:
+
+```bash
+node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("http-to-https", pathToFileURL("./"));' ./my-app.js
+```
+
+### Chaining
+
+It's possible to call `register` more than once:
+
+```mjs
+// entrypoint.mjs
+import { register } from 'node:module';
+
+register('./first.mjs', import.meta.url);
+register('./second.mjs', import.meta.url);
+await import('./my-app.mjs');
+```
+
+```cjs
+// entrypoint.cjs
+const { register } = require('node:module');
+const { pathToFileURL } = require('node:url');
+
+const parentURL = pathToFileURL(__filename);
+register('./first.mjs', parentURL);
+register('./second.mjs', parentURL);
+import('./my-app.mjs');
+```
+
+In this example, the registered hooks will form chains. If both `first.mjs` and
+`second.mjs` define a `resolve` hook, both will be called, in the order they
+were registered. The same applies to all the other hooks.
+
+The registered hooks also affect `register` itself. In this example,
+`second.mjs` will be resolved and loaded per the hooks registered by
+`first.mjs`. This allows for things like writing hooks in non-JavaScript
+languages, so long as an earlier registered loader is one that transpiles into
+JavaScript.
+
+The `register` method cannot be called from within the module that defines the
+hooks.
+
+### Communication with module customization hooks
+
+Module customization hooks run on a dedicated thread, separate from the main
+thread that runs application code. This means mutating global variables won't
+affect the other thread(s), and message channels must be used to communicate
+between the threads.
+
+The `register` method can be used to pass data to an [`initialize`][] hook. The
+data passed to the hook may include transferrable objects like ports.
+
+```mjs
+import { register } from 'node:module';
+import { MessageChannel } from 'node:worker_threads';
+
+// This example demonstrates how a message channel can be used to
+// communicate with the hooks, by sending `port2` to the hooks.
+const { port1, port2 } = new MessageChannel();
+
+port1.on('message', (msg) => {
+ console.log(msg);
+});
+
+register('./my-hooks.mjs', {
+ parentURL: import.meta.url,
+ data: { number: 1, port: port2 },
+ transferList: [port2],
+});
+```
+
+```cjs
+const { register } = require('node:module');
+const { pathToFileURL } = require('node:url');
+const { MessageChannel } = require('node:worker_threads');
+
+// This example showcases how a message channel can be used to
+// communicate with the hooks, by sending `port2` to the hooks.
+const { port1, port2 } = new MessageChannel();
+
+port1.on('message', (msg) => {
+ console.log(msg);
+});
+
+register('./my-hooks.mjs', {
+ parentURL: pathToFileURL(__filename),
+ data: { number: 1, port: port2 },
+ transferList: [port2],
+});
+```
+
+### Hooks
+
+The [`register`][] method can be used to register a module that exports a set of
+hooks. The hooks are functions that are called by Node.js to customize the
+module resolution and loading process. The exported functions must have specific
+names and signatures, and they must be exported as named exports.
+
+```mjs
+export async function initialize({ number, port }) {
+ // Receives data from `register`.
+}
+
+export async function resolve(specifier, context, nextResolve) {
+ // Take an `import` or `require` specifier and resolve it to a URL.
+}
+
+export async function load(url, context, nextLoad) {
+ // Take a resolved URL and return the source code to be evaluated.
+}
+```
+
+Hooks are part of a chain, even if that chain consists of only one custom
+(user-provided) hook and the default hook, which is always present. Hook
+functions nest: each one must always return a plain object, and chaining happens
+as a result of each function calling `next()`, which is a reference to
+the subsequent loader's hook.
+
+A hook that returns a value lacking a required property triggers an exception. A
+hook that returns without calling `next()` _and_ without returning
+`shortCircuit: true` also triggers an exception. These errors are to help
+prevent unintentional breaks in the chain. Return `shortCircuit: true` from a
+hook to signal that the chain is intentionally ending at your hook.
+
+Hooks are run in a separate thread, isolated from the main thread where
+application code runs. That means it is a different [realm][]. The hooks thread
+may be terminated by the main thread at any time, so do not depend on
+asynchronous operations (like `console.log`) to complete.
+
+#### `initialize()`
+
+
+
+> Stability: 1.1 - Active development
+
+* `data` {any} The data from `register(loader, import.meta.url, { data })`.
+
+The `initialize` hook provides a way to define a custom function that runs in
+the hooks thread when the hooks module is initialized. Initialization happens
+when the hooks module is registered via [`register`][].
+
+This hook can receive data from a [`register`][] invocation, including
+ports and other transferrable objects. The return value of `initialize` can be a
+{Promise}, in which case it will be awaited before the main application thread
+execution resumes.
+
+Module customization code:
+
+```mjs
+// path-to-my-hooks.js
+
+export async function initialize({ number, port }) {
+ port.postMessage(`increment: ${number + 1}`);
+}
+```
+
+Caller code:
+
+```mjs
+import assert from 'node:assert';
+import { register } from 'node:module';
+import { MessageChannel } from 'node:worker_threads';
+
+// This example showcases how a message channel can be used to communicate
+// between the main (application) thread and the hooks running on the hooks
+// thread, by sending `port2` to the `initialize` hook.
+const { port1, port2 } = new MessageChannel();
+
+port1.on('message', (msg) => {
+ assert.strictEqual(msg, 'increment: 2');
+});
+
+register('./path-to-my-hooks.js', {
+ parentURL: import.meta.url,
+ data: { number: 1, port: port2 },
+ transferList: [port2],
+});
+```
+
+```cjs
+const assert = require('node:assert');
+const { register } = require('node:module');
+const { pathToFileURL } = require('node:url');
+const { MessageChannel } = require('node:worker_threads');
+
+// This example showcases how a message channel can be used to communicate
+// between the main (application) thread and the hooks running on the hooks
+// thread, by sending `port2` to the `initialize` hook.
+const { port1, port2 } = new MessageChannel();
+
+port1.on('message', (msg) => {
+ assert.strictEqual(msg, 'increment: 2');
+});
+
+register('./path-to-my-hooks.js', {
+ parentURL: pathToFileURL(__filename),
+ data: { number: 1, port: port2 },
+ transferList: [port2],
+});
+```
+
+#### `resolve(specifier, context, nextResolve)`
+
+
+
+> Stability: 1.2 - Release candidate
+
+* `specifier` {string}
+* `context` {Object}
+ * `conditions` {string\[]} Export conditions of the relevant `package.json`
+ * `importAttributes` {Object} An object whose key-value pairs represent the
+ attributes for the module to import
+ * `parentURL` {string|undefined} The module importing this one, or undefined
+ if this is the Node.js entry point
+* `nextResolve` {Function} The subsequent `resolve` hook in the chain, or the
+ Node.js default `resolve` hook after the last user-supplied `resolve` hook
+ * `specifier` {string}
+ * `context` {Object}
+* Returns: {Object|Promise}
+ * `format` {string|null|undefined} A hint to the load hook (it might be
+ ignored)
+ `'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'`
+ * `importAttributes` {Object|undefined} The import attributes to use when
+ caching the module (optional; if excluded the input will be used)
+ * `shortCircuit` {undefined|boolean} A signal that this hook intends to
+ terminate the chain of `resolve` hooks. **Default:** `false`
+ * `url` {string} The absolute URL to which this input resolves
+
+> **Warning** Despite support for returning promises and async functions, calls
+> to `resolve` may block the main thread which can impact performance.
+
+The `resolve` hook chain is responsible for telling Node.js where to find and
+how to cache a given `import` statement or expression, or `require` call. It can
+optionally return a format (such as `'module'`) as a hint to the `load` hook. If
+a format is specified, the `load` hook is ultimately responsible for providing
+the final `format` value (and it is free to ignore the hint provided by
+`resolve`); if `resolve` provides a `format`, a custom `load` hook is required
+even if only to pass the value to the Node.js default `load` hook.
+
+Import type attributes are part of the cache key for saving loaded modules into
+the internal module cache. The `resolve` hook is responsible for returning an
+`importAttributes` object if the module should be cached with different
+attributes than were present in the source code.
+
+The `conditions` property in `context` is an array of conditions for
+[package exports conditions][Conditional exports] that apply to this resolution
+request. They can be used for looking up conditional mappings elsewhere or to
+modify the list when calling the default resolution logic.
+
+The current [package exports conditions][Conditional exports] are always 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.
+
+```mjs
+export async function resolve(specifier, context, nextResolve) {
+ const { parentURL = null } = context;
+
+ 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 {
+ shortCircuit: true,
+ url: parentURL ?
+ new URL(specifier, 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 nextResolve(specifier, {
+ ...context,
+ conditions: [...context.conditions, 'another-condition'],
+ });
+ }
+
+ // Defer to the next hook in the chain, which would be the
+ // Node.js default resolve if this is the last user-specified loader.
+ return nextResolve(specifier);
+}
+```
+
+#### `load(url, context, nextLoad)`
+
+
+
+> Stability: 1.2 - Release candidate
+
+* `url` {string} The URL returned by the `resolve` chain
+* `context` {Object}
+ * `conditions` {string\[]} Export conditions of the relevant `package.json`
+ * `format` {string|null|undefined} The format optionally supplied by the
+ `resolve` hook chain
+ * `importAttributes` {Object}
+* `nextLoad` {Function} The subsequent `load` hook in the chain, or the
+ Node.js default `load` hook after the last user-supplied `load` hook
+ * `specifier` {string}
+ * `context` {Object}
+* Returns: {Object}
+ * `format` {string}
+ * `shortCircuit` {undefined|boolean} A signal that this hook intends to
+ terminate the chain of `resolve` hooks. **Default:** `false`
+ * `source` {string|ArrayBuffer|TypedArray} The source for Node.js to evaluate
+
+The `load` hook provides a way to define a custom method of determining how a
+URL should be interpreted, retrieved, and parsed. It is also in charge of
+validating the import assertion.
+
+The final value of `format` must be one of the following:
+
+| `format` | Description | Acceptable types for `source` returned by `load` |
+| ------------ | ------------------------------ | ----------------------------------------------------- |
+| `'builtin'` | Load a Node.js builtin module | Not applicable |
+| `'commonjs'` | Load a Node.js CommonJS module | Not applicable |
+| `'json'` | Load a JSON file | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][] } |
+| `'module'` | Load an ES module | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][] } |
+| `'wasm'` | Load a WebAssembly module | { [`ArrayBuffer`][], [`TypedArray`][] } |
+
+The value of `source` is ignored for type `'builtin'` because currently it is
+not possible to replace the value of a Node.js builtin (core) module. The value
+of `source` is ignored for type `'commonjs'` because the CommonJS module loader
+does not provide a mechanism for the ES module loader to override the
+[CommonJS module return value](esm.md#commonjs-namespaces). This limitation
+might be overcome in the future.
+
+> **Warning**: The ESM `load` hook and namespaced exports from CommonJS modules
+> are incompatible. Attempting to use them together will result in an empty
+> object from the import. This may be addressed in the future.
+
+> These types all correspond to classes defined in ECMAScript.
+
+* The specific [`ArrayBuffer`][] object is a [`SharedArrayBuffer`][].
+* The specific [`TypedArray`][] object is a [`Uint8Array`][].
+
+If the source value of a text-based format (i.e., `'json'`, `'module'`)
+is not a string, it is converted to a string using [`util.TextDecoder`][].
+
+The `load` hook provides a way to define a custom method for retrieving the
+source code of a resolved URL. This would allow a loader to potentially avoid
+reading files from disk. It could also be used to map an unrecognized format to
+a supported one, for example `yaml` to `module`.
+
+```mjs
+export async function load(url, context, nextLoad) {
+ const { format } = context;
+
+ if (Math.random() > 0.5) { // Some condition
+ /*
+ For some or all URLs, do some custom logic for retrieving the source.
+ Always return an object of the form {
+ format: ,
+ source: ,
+ }.
+ */
+ return {
+ format,
+ shortCircuit: true,
+ source: '...',
+ };
+ }
+
+ // Defer to the next hook in the chain.
+ return nextLoad(url);
+}
+```
+
+In a more advanced scenario, this can also be used to transform an unsupported
+source to a supported one (see [Examples](#examples) below).
+
+#### `globalPreload()`
+
+
+
+> Stability: 1.0 - Early development
+
+> **Warning:** This hook will be removed in a future version. Use
+> [`initialize`][] instead. When a hooks module has an `initialize` export,
+> `globalPreload` will be ignored.
+
+* `context` {Object} Information to assist the preload code
+ * `port` {MessagePort}
+* Returns: {string} Code to run before application startup
+
+Sometimes it might be necessary to run some code inside of the same global
+scope that the application runs in. This hook allows the return of a string
+that is run as a sloppy-mode script on startup.
+
+Similar to how CommonJS wrappers work, the code runs in an implicit function
+scope. The only argument is a `require`-like function that can be used to load
+builtins like "fs": `getBuiltin(request: string)`.
+
+If the code needs more advanced `require` features, it has to construct
+its own `require` using `module.createRequire()`.
+
+```mjs
+export function globalPreload(context) {
+ return `\
+globalThis.someInjectedProperty = 42;
+console.log('I just set some globals!');
+
+const { createRequire } = getBuiltin('module');
+const { cwd } = getBuiltin('process');
+
+const require = createRequire(cwd() + '/');
+// [...]
+`;
+}
+```
+
+Another argument is provided to the preload code: `port`. This is available as a
+parameter to the hook and inside of the source text returned by the hook. This
+functionality has been moved to the `initialize` hook.
+
+Care must be taken in order to properly call [`port.ref()`][] and
+[`port.unref()`][] to prevent a process from being in a state where it won't
+close normally.
+
+```mjs
+/**
+ * This example has the application context send a message to the hook
+ * and sends the message back to the application context
+ */
+export function globalPreload({ port }) {
+ port.on('message', (msg) => {
+ port.postMessage(msg);
+ });
+ return `\
+ port.postMessage('console.log("I went to the hook and back");');
+ port.on('message', (msg) => {
+ eval(msg);
+ });
+ `;
+}
+```
+
+### Examples
+
+The various module customization hooks can be used together to accomplish
+wide-ranging customizations of the Node.js code loading and evaluation
+behaviors.
+
+#### Import from HTTPS
+
+In current Node.js, specifiers starting with `https://` are experimental (see
+[HTTPS and HTTP imports][]).
+
+The hook 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 these hooks:
+performance is much slower than loading files from disk, there is no caching,
+and there is no security.
+
+```mjs
+// https-hooks.mjs
+import { get } from 'node:https';
+
+export function load(url, context, nextLoad) {
+ // 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.setEncoding('utf8');
+ res.on('data', (chunk) => data += chunk);
+ res.on('end', () => resolve({
+ // This example assumes all network-provided JavaScript is ES module
+ // code.
+ format: 'module',
+ shortCircuit: true,
+ source: data,
+ }));
+ }).on('error', (err) => reject(err));
+ });
+ }
+
+ // Let Node.js handle all other URLs.
+ return nextLoad(url);
+}
+```
+
+```mjs
+// main.mjs
+import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffeescript.js';
+
+console.log(VERSION);
+```
+
+With the preceding hooks module, running
+`node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./https-hooks.mjs"));' ./main.mjs`
+prints the current version of CoffeeScript per the module at the URL in
+`main.mjs`.
+
+#### Transpilation
+
+Sources that are in formats Node.js doesn't understand can be converted into
+JavaScript using the [`load` hook][load hook].
+
+This is less performant than transpiling source files before running Node.js;
+transpiler hooks should only be used for development and testing purposes.
+
+```mjs
+// coffeescript-hooks.mjs
+import { readFile } from 'node:fs/promises';
+import { dirname, extname, resolve as resolvePath } from 'node:path';
+import { cwd } from 'node:process';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import coffeescript from 'coffeescript';
+
+const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
+
+export async function load(url, context, nextLoad) {
+ if (extensionsRegex.test(url)) {
+ // CoffeeScript files can be either CommonJS or ES modules, so we want any
+ // CoffeeScript file to be treated by Node.js the same as a .js file at the
+ // same location. To determine how Node.js would interpret an arbitrary .js
+ // file, search up the file system for the nearest parent package.json file
+ // and read its "type" field.
+ const format = await getPackageType(url);
+
+ const { source: rawSource } = await nextLoad(url, { ...context, format });
+ // This hook converts CoffeeScript source code into JavaScript source code
+ // for all imported CoffeeScript files.
+ const transformedSource = coffeescript.compile(rawSource.toString(), url);
+
+ return {
+ format,
+ shortCircuit: true,
+ source: transformedSource,
+ };
+ }
+
+ // Let Node.js handle all other URLs.
+ return nextLoad(url);
+}
+
+async function getPackageType(url) {
+ // `url` is only a file path during the first iteration when passed the
+ // resolved url from the load() hook
+ // an actual file path from load() will contain a file extension as it's
+ // required by the spec
+ // this simple truthy check for whether `url` contains a file extension will
+ // work for most projects but does not cover some edge-cases (such as
+ // extensionless files or a url ending in a trailing space)
+ const isFilePath = !!extname(url);
+ // If it is a file path, get the directory it's in
+ const dir = isFilePath ?
+ dirname(fileURLToPath(url)) :
+ url;
+ // Compose a file path to a package.json in the same directory,
+ // which may or may not exist
+ const packagePath = resolvePath(dir, 'package.json');
+ // Try to read the possibly nonexistent package.json
+ const type = await readFile(packagePath, { encoding: 'utf8' })
+ .then((filestring) => JSON.parse(filestring).type)
+ .catch((err) => {
+ if (err?.code !== 'ENOENT') console.error(err);
+ });
+ // Ff package.json existed and contained a `type` field with a value, voila
+ if (type) return type;
+ // Otherwise, (if not at the root) continue checking the next directory up
+ // If at the root, stop and return false
+ return dir.length > 1 && getPackageType(resolvePath(dir, '..'));
+}
+```
+
+```coffee
+# main.coffee
+import { scream } from './scream.coffee'
+console.log scream 'hello, world'
+
+import { version } from 'node:process'
+console.log "Brought to you by Node.js version #{version}"
+```
+
+```coffee
+# scream.coffee
+export scream = (str) -> str.toUpperCase()
+```
+
+With the preceding hooks module, running
+`node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee`
+causes `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.
+
+#### Import maps
+
+The previous two examples defined `load` hooks. This is an example of a
+`resolve` hook. This hooks module reads an `import-map.json` file that defines
+which specifiers to override to other URLs (this is a very simplistic
+implementation of a small subset of the "import maps" specification).
+
+```mjs
+// import-map-hooks.js
+import fs from 'node:fs/promises';
+
+const { imports } = JSON.parse(await fs.readFile('import-map.json'));
+
+export async function resolve(specifier, context, nextResolve) {
+ if (Object.hasOwn(imports, specifier)) {
+ return nextResolve(imports[specifier], context);
+ }
+
+ return nextResolve(specifier, context);
+}
+```
+
+With these files:
+
+```mjs
+// main.js
+import 'a-module';
+```
+
+```json
+// import-map.json
+{
+ "imports": {
+ "a-module": "./some-module.js"
+ }
+}
+```
+
+```mjs
+// some-module.js
+console.log('some module!');
+```
+
+Running `node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./import-map-hooks.js"));' main.js`
+should print `some module!`.
+
## Source map v3 support
+
* `linker` {Function}
* `specifier` {string} The specifier of the requested module:
```mjs
@@ -623,15 +631,14 @@ The identifier of the current module, as set in the constructor.
* `referencingModule` {vm.Module} The `Module` object `link()` is called on.
* `extra` {Object}
- * `assert` {Object} The data from the assertion:
-
- ```js
+ * `attributes` {Object} The data from the attribute:
+ ```mjs
import foo from 'foo' assert { name: 'value' };
- // ^^^^^^^^^^^^^^^^^ the assertion
+ // ^^^^^^^^^^^^^^^^^ the attribute
```
- Per ECMA-262, hosts are expected to ignore assertions that they do not
- support, as opposed to, for example, triggering an error if an
- unsupported assertion is present.
+ Per ECMA-262, hosts are expected to trigger an error if an
+ unsupported attribute is present.
+ * `assert` {Object} Alias for `extra.attributes`.
* Returns: {vm.Module|Promise}
* Returns: {Promise}
@@ -730,7 +737,7 @@ changes:
- v17.0.0
- v16.12.0
pr-url: https://github.com/nodejs/node/pull/40249
- description: Added support for import assertions to the
+ description: Added support for import attributes to the
`importModuleDynamically` parameter.
-->
@@ -760,7 +767,7 @@ changes:
`import()` will reject with [`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`][].
* `specifier` {string} specifier passed to `import()`
* `module` {vm.Module}
- * `importAssertions` {Object} The `"assert"` value passed to the
+ * `importAttributes` {Object} The `"assert"` value passed to the
[`optionsExpression`][] optional parameter, or an empty object if no value
was provided.
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
@@ -974,7 +981,7 @@ changes:
- v17.0.0
- v16.12.0
pr-url: https://github.com/nodejs/node/pull/40249
- description: Added support for import assertions to the
+ description: Added support for import attributes to the
`importModuleDynamically` parameter.
- version: v15.9.0
pr-url: https://github.com/nodejs/node/pull/35431
@@ -1018,7 +1025,7 @@ changes:
considered stable.
* `specifier` {string} specifier passed to `import()`
* `function` {Function}
- * `importAssertions` {Object} The `"assert"` value passed to the
+ * `importAttributes` {Object} The `"assert"` value passed to the
[`optionsExpression`][] optional parameter, or an empty object if no value
was provided.
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
@@ -1204,7 +1211,7 @@ changes:
- v17.0.0
- v16.12.0
pr-url: https://github.com/nodejs/node/pull/40249
- description: Added support for import assertions to the
+ description: Added support for import attributes to the
`importModuleDynamically` parameter.
- version: v6.3.0
pr-url: https://github.com/nodejs/node/pull/6635
@@ -1242,7 +1249,7 @@ changes:
using it in a production environment.
* `specifier` {string} specifier passed to `import()`
* `script` {vm.Script}
- * `importAssertions` {Object} The `"assert"` value passed to the
+ * `importAttributes` {Object} The `"assert"` value passed to the
[`optionsExpression`][] optional parameter, or an empty object if no value
was provided.
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
@@ -1282,7 +1289,7 @@ changes:
- v17.0.0
- v16.12.0
pr-url: https://github.com/nodejs/node/pull/40249
- description: Added support for import assertions to the
+ description: Added support for import attributes to the
`importModuleDynamically` parameter.
- version: v14.6.0
pr-url: https://github.com/nodejs/node/pull/34023
@@ -1341,7 +1348,7 @@ changes:
using it in a production environment.
* `specifier` {string} specifier passed to `import()`
* `script` {vm.Script}
- * `importAssertions` {Object} The `"assert"` value passed to the
+ * `importAttributes` {Object} The `"assert"` value passed to the
[`optionsExpression`][] optional parameter, or an empty object if no value
was provided.
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
@@ -1385,7 +1392,7 @@ changes:
- v17.0.0
- v16.12.0
pr-url: https://github.com/nodejs/node/pull/40249
- description: Added support for import assertions to the
+ description: Added support for import attributes to the
`importModuleDynamically` parameter.
- version: v6.3.0
pr-url: https://github.com/nodejs/node/pull/6635
@@ -1421,7 +1428,7 @@ changes:
using it in a production environment.
* `specifier` {string} specifier passed to `import()`
* `script` {vm.Script}
- * `importAssertions` {Object} The `"assert"` value passed to the
+ * `importAttributes` {Object} The `"assert"` value passed to the
[`optionsExpression`][] optional parameter, or an empty object if no value
was provided.
* Returns: {Module Namespace Object|vm.Module} Returning a `vm.Module` is
diff --git a/doc/node.1 b/doc/node.1
index b7e69550b2fea8..362e9bfdc29cb8 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -140,6 +140,11 @@ Requires Node.js to be built with
.It Fl -enable-source-maps
Enable Source Map V3 support for stack traces.
.
+.It Fl -experimental-default-type Ns = Ns Ar type
+Interpret as either ES modules or CommonJS modules input via --eval or STDIN, when --input-type is unspecified;
+.js or extensionless files with no sibling or parent package.json;
+.js or extensionless files whose nearest parent package.json lacks a "type" field, unless under node_modules.
+.
.It Fl -experimental-global-customevent
Expose the CustomEvent on the global scope.
.
diff --git a/lib/fs.js b/lib/fs.js
index 8b6af16e5e56b8..09cace93b542b2 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -139,7 +139,6 @@ const {
validateFunction,
validateInteger,
validateObject,
- validateString,
} = require('internal/validators');
let truncateWarn = true;
@@ -2884,7 +2883,7 @@ realpath.native = (path, options, callback) => {
/**
* Creates a unique temporary directory.
- * @param {string} prefix
+ * @param {string | Buffer | URL} prefix
* @param {string | { encoding?: string; }} [options]
* @param {(
* err?: Error,
@@ -2896,27 +2895,40 @@ function mkdtemp(prefix, options, callback) {
callback = makeCallback(typeof options === 'function' ? options : callback);
options = getOptions(options);
- validateString(prefix, 'prefix');
- nullCheck(prefix, 'prefix');
+ prefix = getValidatedPath(prefix, 'prefix');
warnOnNonPortableTemplate(prefix);
+
+ let path;
+ if (typeof prefix === 'string') {
+ path = `${prefix}XXXXXX`;
+ } else {
+ path = Buffer.concat([prefix, Buffer.from('XXXXXX')]);
+ }
+
const req = new FSReqCallback();
req.oncomplete = callback;
- binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, req);
+ binding.mkdtemp(path, options.encoding, req);
}
/**
* Synchronously creates a unique temporary directory.
- * @param {string} prefix
+ * @param {string | Buffer | URL} prefix
* @param {string | { encoding?: string; }} [options]
* @returns {string}
*/
function mkdtempSync(prefix, options) {
options = getOptions(options);
- validateString(prefix, 'prefix');
- nullCheck(prefix, 'prefix');
+ prefix = getValidatedPath(prefix, 'prefix');
warnOnNonPortableTemplate(prefix);
- const path = `${prefix}XXXXXX`;
+
+ let path;
+ if (typeof prefix === 'string') {
+ path = `${prefix}XXXXXX`;
+ } else {
+ path = Buffer.concat([prefix, Buffer.from('XXXXXX')]);
+ }
+
const ctx = { path };
const result = binding.mkdtemp(path, options.encoding,
undefined, ctx);
diff --git a/lib/internal/dns/promises.js b/lib/internal/dns/promises.js
index 79be8591bbcad2..1169b2735d4efe 100644
--- a/lib/internal/dns/promises.js
+++ b/lib/internal/dns/promises.js
@@ -113,6 +113,19 @@ function onlookupall(err, addresses) {
}
}
+/**
+ * Creates a promise that resolves with the IP address of the given hostname.
+ * @param {0 | 4 | 6} family - The IP address family (4 or 6, or 0 for both).
+ * @param {string} hostname - The hostname to resolve.
+ * @param {boolean} all - Whether to resolve with all IP addresses for the hostname.
+ * @param {number} hints - One or more supported getaddrinfo flags (supply multiple via
+ * bitwise OR).
+ * @param {boolean} verbatim - Whether to use the hostname verbatim.
+ * @returns {Promise} The IP address(es) of the hostname.
+ * @typedef {object} DNSLookupResult
+ * @property {string} address - The IP address.
+ * @property {0 | 4 | 6} family - The IP address type. 4 for IPv4 or 6 for IPv6, or 0 (for both).
+ */
function createLookupPromise(family, hostname, all, hints, verbatim) {
return new Promise((resolve, reject) => {
if (!hostname) {
@@ -154,6 +167,17 @@ function createLookupPromise(family, hostname, all, hints, verbatim) {
}
const validFamilies = [0, 4, 6];
+/**
+ * Get the IP address for a given hostname.
+ * @param {string} hostname - The hostname to resolve (ex. 'nodejs.org').
+ * @param {object} [options] - Optional settings.
+ * @param {boolean} [options.all=false] - Whether to return all or just the first resolved address.
+ * @param {0 | 4 | 6} [options.family=0] - The record family. Must be 4, 6, or 0 (for both).
+ * @param {number} [options.hints] - One or more supported getaddrinfo flags (supply multiple via
+ * bitwise OR).
+ * @param {boolean} [options.verbatim=false] - Return results in same order DNS resolved them;
+ * otherwise IPv4 then IPv6. New code should supply `true`.
+ */
function lookup(hostname, options) {
let hints = 0;
let family = 0;
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 7bc7998f918ac6..5d488843f60ab8 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1177,12 +1177,17 @@ E('ERR_HTTP_SOCKET_ENCODING',
E('ERR_HTTP_TRAILER_INVALID',
'Trailers are invalid with this transfer encoding', Error);
E('ERR_ILLEGAL_CONSTRUCTOR', 'Illegal constructor', TypeError);
+// TODO(aduh95): change the error to mention import attributes instead of import assertions.
E('ERR_IMPORT_ASSERTION_TYPE_FAILED',
'Module "%s" is not of type "%s"', TypeError);
+// TODO(aduh95): change the error to mention import attributes instead of import assertions.
E('ERR_IMPORT_ASSERTION_TYPE_MISSING',
- 'Module "%s" needs an import assertion of type "%s"', TypeError);
+ 'Module "%s" needs an import attribute of type "%s"', TypeError);
+// TODO(aduh95): change the error to mention import attributes instead of import assertions.
E('ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED',
- 'Import assertion type "%s" is unsupported', TypeError);
+ 'Import attribute type "%s" is unsupported', TypeError);
+E('ERR_IMPORT_ATTRIBUTE_UNSUPPORTED',
+ 'Import attribute "%s" with value "%s" is not supported', TypeError);
E('ERR_INCOMPATIBLE_OPTION_PAIR',
'Option "%s" cannot be used in combination with option "%s"', TypeError);
E('ERR_INPUT_TYPE_NOT_ALLOWED', '--input-type can only be used with string ' +
@@ -1453,8 +1458,12 @@ E('ERR_MISSING_ARGS',
return `${msg} must be specified`;
}, TypeError);
E('ERR_MISSING_OPTION', '%s is required', TypeError);
-E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => {
- return `Cannot find ${type} '${path}' imported from ${base}`;
+E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) {
+ if (exactUrl) {
+ lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`);
+ }
+ return `Cannot find ${
+ exactUrl ? 'module' : 'package'} '${path}' imported from ${base}`;
}, Error);
E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error);
E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError);
@@ -1542,7 +1551,7 @@ E('ERR_REQUIRE_ESM',
msg += `\n${basename} is treated as an ES module file as it is a .js ` +
'file whose nearest parent package.json contains "type": "module" ' +
'which declares all .js files in that package scope as ES modules.' +
- `\nInstead rename ${basename} to end in .cjs, change the requiring ` +
+ `\nInstead either rename ${basename} to end in .cjs, change the requiring ` +
'code to use dynamic import() which is available in all CommonJS ' +
'modules, or change "type": "module" to "type": "commonjs" in ' +
`${packageJsonPath} to treat all .js files as CommonJS (using .mjs for ` +
@@ -1682,18 +1691,15 @@ E('ERR_UNHANDLED_ERROR',
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', (ext, path, suggestion) => {
- let msg = `Unknown file extension "${ext}" for ${path}`;
- if (suggestion) {
- msg += `. ${suggestion}`;
- }
- return msg;
-}, TypeError);
+E('ERR_UNKNOWN_FILE_EXTENSION', 'Unknown file extension "%s" for %s', TypeError);
E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s for URL %s',
RangeError);
E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError);
-E('ERR_UNSUPPORTED_DIR_IMPORT', "Directory import '%s' is not supported " +
-'resolving ES modules imported from %s', Error);
+E('ERR_UNSUPPORTED_DIR_IMPORT', function(path, base, exactUrl) {
+ lazyInternalUtil().setOwnProperty(this, 'url', exactUrl);
+ return `Directory import '${path}' is not supported ` +
+ `resolving ES modules imported from ${base}`;
+}, Error);
E('ERR_UNSUPPORTED_ESM_URL_SCHEME', (url, supported) => {
let msg = `Only URLs with a scheme in: ${formatList(supported)} are supported by the default ESM loader`;
if (isWindows && url.protocol.length === 2) {
diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js
index 4cf97f2253aa7d..c4409d51b9dac6 100644
--- a/lib/internal/fs/promises.js
+++ b/lib/internal/fs/promises.js
@@ -59,7 +59,6 @@ const {
getStatsFromBinding,
getValidatedPath,
getValidMode,
- nullCheck,
preprocessSymlinkDestination,
stringToFlags,
stringToSymlinkType,
@@ -976,10 +975,17 @@ async function realpath(path, options) {
async function mkdtemp(prefix, options) {
options = getOptions(options);
- validateString(prefix, 'prefix');
- nullCheck(prefix);
+ prefix = getValidatedPath(prefix, 'prefix');
warnOnNonPortableTemplate(prefix);
- return binding.mkdtemp(`${prefix}XXXXXX`, options.encoding, kUsePromises);
+
+ let path;
+ if (typeof prefix === 'string') {
+ path = `${prefix}XXXXXX`;
+ } else {
+ path = Buffer.concat([prefix, Buffer.from('XXXXXX')]);
+ }
+
+ return binding.mkdtemp(path, options.encoding, kUsePromises);
}
async function writeFile(path, data, options) {
diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js
index 23865845bac59e..00889ffccc4b99 100644
--- a/lib/internal/fs/utils.js
+++ b/lib/internal/fs/utils.js
@@ -21,6 +21,7 @@ const {
StringPrototypeEndsWith,
StringPrototypeIncludes,
Symbol,
+ TypedArrayPrototypeAt,
TypedArrayPrototypeIncludes,
} = primordials;
@@ -736,7 +737,9 @@ let nonPortableTemplateWarn = true;
function warnOnNonPortableTemplate(template) {
// Template strings passed to the mkdtemp() family of functions should not
// end with 'X' because they are handled inconsistently across platforms.
- if (nonPortableTemplateWarn && StringPrototypeEndsWith(template, 'X')) {
+ if (nonPortableTemplateWarn &&
+ ((typeof template === 'string' && StringPrototypeEndsWith(template, 'X')) ||
+ (typeof template !== 'string' && TypedArrayPrototypeAt(template, -1) === 0x58))) {
process.emitWarning('mkdtemp() templates ending with X are not portable. ' +
'For details see: https://nodejs.org/api/fs.html');
nonPortableTemplateWarn = false;
diff --git a/lib/internal/main/check_syntax.js b/lib/internal/main/check_syntax.js
index 52c83be33287c0..b6ee64de499c2f 100644
--- a/lib/internal/main/check_syntax.js
+++ b/lib/internal/main/check_syntax.js
@@ -63,7 +63,8 @@ function loadESMIfNeeded(cb) {
async function checkSyntax(source, filename) {
let isModule = true;
if (filename === '[stdin]' || filename === '[eval]') {
- isModule = getOptionValue('--input-type') === 'module';
+ isModule = getOptionValue('--input-type') === 'module' ||
+ (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs');
} else {
const { defaultResolve } = require('internal/modules/esm/resolve');
const { defaultGetFormat } = require('internal/modules/esm/get_format');
diff --git a/lib/internal/main/eval_stdin.js b/lib/internal/main/eval_stdin.js
index d947af49a6a942..d71751e781b9b5 100644
--- a/lib/internal/main/eval_stdin.js
+++ b/lib/internal/main/eval_stdin.js
@@ -25,12 +25,14 @@ readStdin((code) => {
const print = getOptionValue('--print');
const loadESM = getOptionValue('--import').length > 0;
- if (getOptionValue('--input-type') === 'module')
+ if (getOptionValue('--input-type') === 'module' ||
+ (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) {
evalModule(code, print);
- else
+ } else {
evalScript('[stdin]',
code,
getOptionValue('--inspect-brk'),
print,
loadESM);
+ }
});
diff --git a/lib/internal/main/eval_string.js b/lib/internal/main/eval_string.js
index 2b9c99e1944fdc..4e2edf9ce62499 100644
--- a/lib/internal/main/eval_string.js
+++ b/lib/internal/main/eval_string.js
@@ -22,12 +22,14 @@ markBootstrapComplete();
const source = getOptionValue('--eval');
const print = getOptionValue('--print');
-const loadESM = getOptionValue('--import').length > 0;
-if (getOptionValue('--input-type') === 'module')
+const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0;
+if (getOptionValue('--input-type') === 'module' ||
+ (getOptionValue('--experimental-default-type') === 'module' && getOptionValue('--input-type') !== 'commonjs')) {
evalModule(source, print);
-else
+} else {
evalScript('[eval]',
source,
getOptionValue('--inspect-brk'),
print,
loadESM);
+}
diff --git a/lib/internal/main/run_main_module.js b/lib/internal/main/run_main_module.js
index 51331270a2161f..5d09203b8c27ee 100644
--- a/lib/internal/main/run_main_module.js
+++ b/lib/internal/main/run_main_module.js
@@ -6,18 +6,24 @@ const {
prepareMainThreadExecution,
markBootstrapComplete,
} = require('internal/process/pre_execution');
+const { getOptionValue } = require('internal/options');
-prepareMainThreadExecution(true);
+const mainEntry = prepareMainThreadExecution(true);
markBootstrapComplete();
// Necessary to reset RegExp statics before user code runs.
RegExpPrototypeExec(/^/, '');
-// Note: this loads the module through the ESM loader if the module is
-// determined to be an ES module. This hangs from the CJS module loader
-// because we currently allow monkey-patching of the module loaders
-// in the preloaded scripts through require('module').
-// runMain here might be monkey-patched by users in --require.
-// XXX: the monkey-patchability here should probably be deprecated.
-require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);
+if (getOptionValue('--experimental-default-type') === 'module') {
+ require('internal/modules/run_main').executeUserEntryPoint(mainEntry);
+} else {
+ /**
+ * To support legacy monkey-patching of `Module.runMain`, we call `runMain` here to have the CommonJS loader begin
+ * the execution of the main entry point, even if the ESM loader immediately takes over because the main entry is an
+ * ES module or one of the other opt-in conditions (such as the use of `--import`) are met. Users can monkey-patch
+ * before the main entry point is loaded by doing so via scripts loaded through `--require`. This monkey-patchability
+ * is undesirable and is removed in `--experimental-default-type=module` mode.
+ */
+ require('internal/modules/cjs/loader').Module.runMain(mainEntry);
+}
diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js
index 7fbfb64984c290..b905de0da20a20 100644
--- a/lib/internal/main/worker_thread.js
+++ b/lib/internal/main/worker_thread.js
@@ -10,6 +10,7 @@ const {
ObjectDefineProperty,
PromisePrototypeThen,
RegExpPrototypeExec,
+ SafeWeakMap,
globalThis: {
Atomics,
SharedArrayBuffer,
@@ -88,23 +89,25 @@ port.on('message', (message) => {
const {
argv,
cwdCounter,
- filename,
doEval,
- workerData,
environmentData,
- publicPort,
+ filename,
+ hasStdin,
manifestSrc,
manifestURL,
- hasStdin,
+ publicPort,
+ workerData,
} = message;
- if (argv !== undefined) {
- ArrayPrototypePushApply(process.argv, argv);
- }
+ if (doEval !== 'internal') {
+ if (argv !== undefined) {
+ ArrayPrototypePushApply(process.argv, argv);
+ }
- const publicWorker = require('worker_threads');
- publicWorker.parentPort = publicPort;
- publicWorker.workerData = workerData;
+ const publicWorker = require('worker_threads');
+ publicWorker.parentPort = publicPort;
+ publicWorker.workerData = workerData;
+ }
require('internal/worker').assignEnvironmentData(environmentData);
@@ -129,7 +132,10 @@ port.on('message', (message) => {
if (manifestSrc) {
require('internal/process/policy').setup(manifestSrc, manifestURL);
}
- setupUserModules();
+ const isLoaderWorker =
+ doEval === 'internal' &&
+ filename === require('internal/modules/esm/utils').loaderWorkerId;
+ setupUserModules(isLoaderWorker);
if (!hasStdin)
process.stdin.push(null);
@@ -137,31 +143,47 @@ port.on('message', (message) => {
debug(`[${threadId}] starts worker script ${filename} ` +
`(eval = ${doEval}) at cwd = ${process.cwd()}`);
port.postMessage({ type: UP_AND_RUNNING });
- if (doEval === 'classic') {
- const { evalScript } = require('internal/process/execution');
- const name = '[worker eval]';
- // This is necessary for CJS module compilation.
- // TODO: pass this with something really internal.
- ObjectDefineProperty(process, '_eval', {
- __proto__: null,
- configurable: true,
- enumerable: true,
- value: filename,
- });
- ArrayPrototypeSplice(process.argv, 1, 0, name);
- evalScript(name, filename);
- } else if (doEval === 'module') {
- const { evalModule } = require('internal/process/execution');
- PromisePrototypeThen(evalModule(filename), undefined, (e) => {
- workerOnGlobalUncaughtException(e, true);
- });
- } else {
- // script filename
- // runMain here might be monkey-patched by users in --require.
- // XXX: the monkey-patchability here should probably be deprecated.
- ArrayPrototypeSplice(process.argv, 1, 0, filename);
- const CJSLoader = require('internal/modules/cjs/loader');
- CJSLoader.Module.runMain(filename);
+ switch (doEval) {
+ case 'internal': {
+ // Create this WeakMap in js-land because V8 has no C++ API for WeakMap.
+ internalBinding('module_wrap').callbackMap = new SafeWeakMap();
+ require(filename)(workerData, publicPort);
+ break;
+ }
+
+ case 'classic': {
+ const { evalScript } = require('internal/process/execution');
+ const name = '[worker eval]';
+ // This is necessary for CJS module compilation.
+ // TODO: pass this with something really internal.
+ ObjectDefineProperty(process, '_eval', {
+ __proto__: null,
+ configurable: true,
+ enumerable: true,
+ value: filename,
+ });
+ ArrayPrototypeSplice(process.argv, 1, 0, name);
+ evalScript(name, filename);
+ break;
+ }
+
+ case 'module': {
+ const { evalModule } = require('internal/process/execution');
+ PromisePrototypeThen(evalModule(filename), undefined, (e) => {
+ workerOnGlobalUncaughtException(e, true);
+ });
+ break;
+ }
+
+ default: {
+ // script filename
+ // runMain here might be monkey-patched by users in --require.
+ // XXX: the monkey-patchability here should probably be deprecated.
+ ArrayPrototypeSplice(process.argv, 1, 0, filename);
+ const CJSLoader = require('internal/modules/cjs/loader');
+ CJSLoader.Module.runMain(filename);
+ break;
+ }
}
} else if (message.type === STDIO_PAYLOAD) {
const { stream, chunks } = message;
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index 44fab4de4c0823..316996a8c329a1 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -56,7 +56,6 @@ const {
StringPrototypeCharAt,
StringPrototypeCharCodeAt,
StringPrototypeEndsWith,
- StringPrototypeLastIndexOf,
StringPrototypeIndexOf,
StringPrototypeRepeat,
StringPrototypeSlice,
@@ -69,7 +68,7 @@ const cjsParseCache = new SafeWeakMap();
// Set first due to cycle with ESM loader functions.
module.exports = {
- wrapSafe, Module, toRealPath, readPackageScope, cjsParseCache,
+ wrapSafe, Module, cjsParseCache,
get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; },
initializeCJS,
};
@@ -83,16 +82,13 @@ const {
pendingDeprecate,
emitExperimentalWarning,
kEmptyObject,
- filterOwnProperties,
setOwnProperty,
getLazy,
} = require('internal/util');
const { internalCompileFunction } = require('internal/vm');
const assert = require('internal/assert');
const fs = require('fs');
-const internalFS = require('internal/fs/utils');
const path = require('path');
-const { sep } = path;
const { internalModuleStat } = internalBinding('fs');
const { safeGetenv } = internalBinding('credentials');
const {
@@ -108,6 +104,7 @@ const {
makeRequireFunction,
normalizeReferrerURL,
stripBOM,
+ toRealPath,
} = require('internal/modules/helpers');
const packageJsonReader = require('internal/modules/package_json_reader');
const { getOptionValue, getEmbedderOptions } = require('internal/options');
@@ -155,6 +152,11 @@ let requireDepth = 0;
let isPreloading = false;
let statCache = null;
+/**
+ * Our internal implementation of `require`.
+ * @param {Module} module Parent module of what is being required
+ * @param {string} id Specifier of the child module being imported
+ */
function internalRequire(module, id) {
validateString(id, 'id');
if (id === '') {
@@ -169,11 +171,15 @@ function internalRequire(module, id) {
}
}
+/**
+ * Get a path's properties, using an in-memory cache to minimize lookups.
+ * @param {string} filename Absolute path to the file
+ */
function stat(filename) {
filename = path.toNamespacedPath(filename);
if (statCache !== null) {
const result = statCache.get(filename);
- if (result !== undefined) return result;
+ if (result !== undefined) { return result; }
}
const result = internalModuleStat(filename);
if (statCache !== null && result >= 0) {
@@ -195,25 +201,47 @@ ObjectDefineProperty(Module, '_stat', {
configurable: true,
});
+/**
+ * Update the parent's children array with the child module.
+ * @param {Module} parent Module requiring the children
+ * @param {Module} child Module being required
+ * @param {boolean} scan Add the child to the parent's children if not already present
+ */
function updateChildren(parent, child, scan) {
const children = parent?.children;
- if (children && !(scan && ArrayPrototypeIncludes(children, child)))
+ if (children && !(scan && ArrayPrototypeIncludes(children, child))) {
ArrayPrototypePush(children, child);
+ }
}
+/**
+ * Tell the watch mode that a module was required.
+ * @param {string} filename Absolute path of the module
+ */
function reportModuleToWatchMode(filename) {
if (shouldReportRequiredModules() && process.send) {
process.send({ 'watch:require': [filename] });
}
}
+/**
+ * Tell the watch mode that a module was not found.
+ * @param {string} basePath The absolute path that errored
+ * @param {string[]} extensions The extensions that were tried
+ */
function reportModuleNotFoundToWatchMode(basePath, extensions) {
if (shouldReportRequiredModules() && process.send) {
process.send({ 'watch:require': ArrayPrototypeMap(extensions, (ext) => path.resolve(`${basePath}${ext}`)) });
}
}
+/** @type {Map} */
const moduleParentCache = new SafeWeakMap();
+/**
+ * Create a new module instance.
+ * @param {string} id
+ * @param {Module} parent
+ */
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
@@ -236,16 +264,24 @@ function Module(id = '', parent) {
this[require_private_symbol] = internalRequire;
}
-Module._cache = ObjectCreate(null);
-Module._pathCache = ObjectCreate(null);
-Module._extensions = ObjectCreate(null);
+/** @type {Record} */
+Module._cache = { __proto__: null };
+/** @type {Record} */
+Module._pathCache = { __proto__: null };
+/** @type {Record void>} */
+Module._extensions = { __proto__: null };
+/** @type {string[]} */
let modulePaths = [];
+/** @type {string[]} */
Module.globalPaths = [];
let patched = false;
-// eslint-disable-next-line func-style
-let wrap = function(script) {
+/**
+ * Add the CommonJS wrapper around a module's source code.
+ * @param {string} script Module source code
+ */
+let wrap = function(script) { // eslint-disable-line func-style
return Module.wrapper[0] + script + Module.wrapper[1];
};
@@ -296,10 +332,17 @@ const isPreloadingDesc = { get() { return isPreloading; } };
ObjectDefineProperty(Module.prototype, 'isPreloading', isPreloadingDesc);
ObjectDefineProperty(BuiltinModule.prototype, 'isPreloading', isPreloadingDesc);
+/**
+ * Get the parent of the current module from our cache.
+ */
function getModuleParent() {
return moduleParentCache.get(this);
}
+/**
+ * Set the parent of the current module in our cache.
+ * @param {Module} value
+ */
function setModuleParent(value) {
moduleParentCache.set(this, value);
}
@@ -326,7 +369,10 @@ ObjectDefineProperty(Module.prototype, 'parent', {
Module._debug = pendingDeprecate(debug, 'Module._debug is deprecated.', 'DEP0077');
Module.isBuiltin = BuiltinModule.isBuiltin;
-// This function is called during pre-execution, before any user code is run.
+/**
+ * Prepare to run CommonJS code.
+ * This function is called during pre-execution, before any user code is run.
+ */
function initializeCJS() {
// This need to be done at runtime in case --expose-internals is set.
const builtinModules = BuiltinModule.getCanBeRequiredByUsersWithoutSchemeList();
@@ -354,39 +400,7 @@ function initializeCJS() {
// -> a.
// -> a/index.
-const packageJsonCache = new SafeMap();
-
-function readPackage(requestPath) {
- const jsonPath = path.resolve(requestPath, 'package.json');
-
- const existing = packageJsonCache.get(jsonPath);
- if (existing !== undefined) return existing;
-
- const result = packageJsonReader.read(jsonPath);
- const json = result.containsKeys === false ? '{}' : result.string;
- if (json === undefined) {
- packageJsonCache.set(jsonPath, false);
- return false;
- }
-
- try {
- const filtered = filterOwnProperties(JSONParse(json), [
- 'name',
- 'main',
- 'exports',
- 'imports',
- 'type',
- ]);
- packageJsonCache.set(jsonPath, filtered);
- return filtered;
- } catch (e) {
- e.path = jsonPath;
- e.message = 'Error parsing ' + jsonPath + ': ' + e.message;
- throw e;
- }
-}
-
-let _readPackage = readPackage;
+let _readPackage = packageJsonReader.readPackage;
ObjectDefineProperty(Module, '_readPackage', {
__proto__: null,
get() { return _readPackage; },
@@ -398,25 +412,15 @@ ObjectDefineProperty(Module, '_readPackage', {
configurable: true,
});
-function readPackageScope(checkPath) {
- const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep);
- let separatorIndex;
- do {
- separatorIndex = StringPrototypeLastIndexOf(checkPath, sep);
- checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex);
- if (StringPrototypeEndsWith(checkPath, sep + 'node_modules'))
- return false;
- const pjson = _readPackage(checkPath + sep);
- if (pjson) return {
- data: pjson,
- path: checkPath,
- };
- } while (separatorIndex > rootSeparatorIndex);
- return false;
-}
-
+/**
+ * Try to load a specifier as a package.
+ * @param {string} requestPath The path to what we are trying to load
+ * @param {string[]} exts File extensions to try appending in order to resolve the file
+ * @param {boolean} isMain Whether the file is the main entry point of the app
+ * @param {string} originalPath The specifier passed to `require`
+ */
function tryPackage(requestPath, exts, isMain, originalPath) {
- const pkg = _readPackage(requestPath)?.main;
+ const pkg = _readPackage(requestPath).main;
if (!pkg) {
return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain);
@@ -452,34 +456,30 @@ function tryPackage(requestPath, exts, isMain, originalPath) {
return actual;
}
-// In order to minimize unnecessary lstat() calls,
-// this cache is a list of known-real paths.
-// Set to an empty Map to reset.
-const realpathCache = new SafeMap();
-
-// Check if the file exists and is not a directory
-// if using --preserve-symlinks and isMain is false,
-// keep symlinks intact, otherwise resolve to the
-// absolute realpath.
+/**
+ * Check if the file exists and is not a directory if using `--preserve-symlinks` and `isMain` is false, keep symlinks
+ * intact, otherwise resolve to the absolute realpath.
+ * @param {string} requestPath The path to the file to load.
+ * @param {boolean} isMain Whether the file is the main module.
+ */
function tryFile(requestPath, isMain) {
const rc = _stat(requestPath);
- if (rc !== 0) return;
+ if (rc !== 0) { return; }
if (getOptionValue('--preserve-symlinks') && !isMain) {
return path.resolve(requestPath);
}
return toRealPath(requestPath);
}
-function toRealPath(requestPath) {
- return fs.realpathSync(requestPath, {
- [internalFS.realpathCacheKey]: realpathCache,
- });
-}
-
-// Given a path, check if the file exists with any of the set extensions
-function tryExtensions(p, exts, isMain) {
+/**
+ * Given a path, check if the file exists with any of the set extensions.
+ * @param {string} basePath The path and filename without extension
+ * @param {string[]} exts The extensions to try
+ * @param {boolean} isMain Whether the module is the main module
+ */
+function tryExtensions(basePath, exts, isMain) {
for (let i = 0; i < exts.length; i++) {
- const filename = tryFile(p + exts[i], isMain);
+ const filename = tryFile(basePath + exts[i], isMain);
if (filename) {
return filename;
@@ -488,8 +488,10 @@ function tryExtensions(p, exts, isMain) {
return false;
}
-// Find the longest (possibly multi-dot) extension registered in
-// Module._extensions
+/**
+ * Find the longest (possibly multi-dot) extension registered in `Module._extensions`.
+ * @param {string} filename The filename to find the longest registered extension for.
+ */
function findLongestRegisteredExtension(filename) {
const name = path.basename(filename);
let currentExtension;
@@ -497,15 +499,19 @@ function findLongestRegisteredExtension(filename) {
let startIndex = 0;
while ((index = StringPrototypeIndexOf(name, '.', startIndex)) !== -1) {
startIndex = index + 1;
- if (index === 0) continue; // Skip dotfiles like .gitignore
+ if (index === 0) { continue; } // Skip dotfiles like .gitignore
currentExtension = StringPrototypeSlice(name, index);
- if (Module._extensions[currentExtension]) return currentExtension;
+ if (Module._extensions[currentExtension]) { return currentExtension; }
}
return '.js';
}
+/**
+ * Tries to get the absolute file path of the parent module.
+ * @param {Module} parent The parent module object.
+ */
function trySelfParentPath(parent) {
- if (!parent) return false;
+ if (!parent) { return false; }
if (parent.filename) {
return parent.filename;
@@ -518,12 +524,18 @@ function trySelfParentPath(parent) {
}
}
+/**
+ * Attempt to resolve a module request using the parent module package metadata.
+ * @param {string} parentPath The path of the parent module
+ * @param {string} request The module request to resolve
+ */
function trySelf(parentPath, request) {
- if (!parentPath) return false;
+ if (!parentPath) { return false; }
- const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {};
- if (!pkg || pkg.exports === undefined) return false;
- if (typeof pkg.name !== 'string') return false;
+ const { data: pkg, path: pkgPath } = packageJsonReader.readPackageScope(parentPath);
+ if (!pkg || pkg.exports == null || pkg.name === undefined) {
+ return false;
+ }
let expansion;
if (request === pkg.name) {
@@ -540,42 +552,52 @@ function trySelf(parentPath, request) {
pathToFileURL(pkgPath + '/package.json'), expansion, pkg,
pathToFileURL(parentPath), getCjsConditions()), parentPath, pkgPath);
} catch (e) {
- if (e.code === 'ERR_MODULE_NOT_FOUND')
+ if (e.code === 'ERR_MODULE_NOT_FOUND') {
throw createEsmNotFoundErr(request, pkgPath + '/package.json');
+ }
throw e;
}
}
-// This only applies to requests of a specific form:
-// 1. name/.*
-// 2. @scope/name/.*
+/**
+ * This only applies to requests of a specific form:
+ * 1. `name/.*`
+ * 2. `@scope/name/.*`
+ */
const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/;
+
+/**
+ * Resolves the exports for a given module path and request.
+ * @param {string} nmPath The path to the module.
+ * @param {string} request The request for the module.
+ */
function resolveExports(nmPath, request) {
// The implementation's behavior is meant to mirror resolution in ESM.
const { 1: name, 2: expansion = '' } =
RegExpPrototypeExec(EXPORTS_PATTERN, request) || kEmptyObject;
- if (!name)
- return;
+ if (!name) { return; }
const pkgPath = path.resolve(nmPath, name);
const pkg = _readPackage(pkgPath);
- if (pkg?.exports != null) {
+ if (pkg.exists && pkg.exports != null) {
try {
const { packageExportsResolve } = require('internal/modules/esm/resolve');
return finalizeEsmResolution(packageExportsResolve(
pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null,
getCjsConditions()), null, pkgPath);
} catch (e) {
- if (e.code === 'ERR_MODULE_NOT_FOUND')
+ if (e.code === 'ERR_MODULE_NOT_FOUND') {
throw createEsmNotFoundErr(request, pkgPath + '/package.json');
+ }
throw e;
}
}
}
/**
- * @param {string} request a relative or absolute file path
- * @param {Array} paths file system directories to search as file paths
- * @param {boolean} isMain if the request is the main app entry point
+ * Get the absolute path to a module.
+ * @param {string} request Relative or absolute file path
+ * @param {Array} paths Folders to search as file paths
+ * @param {boolean} isMain Whether the request is the main app entry point
* @returns {string | false}
*/
Module._findPath = function(request, paths, isMain) {
@@ -588,8 +610,9 @@ Module._findPath = function(request, paths, isMain) {
const cacheKey = request + '\x00' + ArrayPrototypeJoin(paths, '\x00');
const entry = Module._pathCache[cacheKey];
- if (entry)
+ if (entry) {
return entry;
+ }
let exts;
const trailingSlash = request.length > 0 &&
@@ -627,12 +650,15 @@ Module._findPath = function(request, paths, isMain) {
for (let i = 0; i < paths.length; i++) {
// Don't search further if path doesn't exist and request is inside the path
const curPath = paths[i];
- if (insidePath && curPath && _stat(curPath) < 1) continue;
+ if (insidePath && curPath && _stat(curPath) < 1) {
+ continue;
+ }
if (!absoluteRequest) {
const exportsResolved = resolveExports(curPath, request);
- if (exportsResolved)
+ if (exportsResolved) {
return exportsResolved;
+ }
}
const basePath = path.resolve(curPath, request);
@@ -664,16 +690,18 @@ Module._findPath = function(request, paths, isMain) {
if (!filename) {
// Try it with each of the extensions
- if (exts === undefined)
+ if (exts === undefined) {
exts = ObjectKeys(Module._extensions);
+ }
filename = tryExtensions(basePath, exts, isMain);
}
}
if (!filename && rc === 1) { // Directory.
// try it with each of the extensions at "index"
- if (exts === undefined)
+ if (exts === undefined) {
exts = ObjectKeys(Module._extensions);
+ }
filename = tryPackage(basePath, exts, isMain, request);
}
@@ -692,11 +720,14 @@ Module._findPath = function(request, paths, isMain) {
return false;
};
-// 'node_modules' character codes reversed
+/** `node_modules` character codes reversed */
const nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ];
const nmLen = nmChars.length;
if (isWindows) {
- // 'from' is the __dirname of the module.
+ /**
+ * Get the paths to the `node_modules` folder for a given path.
+ * @param {string} from `__dirname` of the module
+ */
Module._nodeModulePaths = function(from) {
// Guarantee that 'from' is absolute.
from = path.resolve(from);
@@ -709,9 +740,11 @@ if (isWindows) {
// path.resolve will make sure from.length >=3 in Windows.
if (StringPrototypeCharCodeAt(from, from.length - 1) ===
CHAR_BACKWARD_SLASH &&
- StringPrototypeCharCodeAt(from, from.length - 2) === CHAR_COLON)
+ StringPrototypeCharCodeAt(from, from.length - 2) === CHAR_COLON) {
return [from + 'node_modules'];
+ }
+ /** @type {string[]} */
const paths = [];
for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) {
const code = StringPrototypeCharCodeAt(from, i);
@@ -723,11 +756,12 @@ if (isWindows) {
if (code === CHAR_BACKWARD_SLASH ||
code === CHAR_FORWARD_SLASH ||
code === CHAR_COLON) {
- if (p !== nmLen)
+ if (p !== nmLen) {
ArrayPrototypePush(
paths,
StringPrototypeSlice(from, 0, last) + '\\node_modules',
);
+ }
last = i;
p = 0;
} else if (p !== -1) {
@@ -742,27 +776,33 @@ if (isWindows) {
return paths;
};
} else { // posix
- // 'from' is the __dirname of the module.
+ /**
+ * Get the paths to the `node_modules` folder for a given path.
+ * @param {string} from `__dirname` of the module
+ */
Module._nodeModulePaths = function(from) {
// Guarantee that 'from' is absolute.
from = path.resolve(from);
// Return early not only to avoid unnecessary work, but to *avoid* returning
// an array of two items for a root: [ '//node_modules', '/node_modules' ]
- if (from === '/')
+ if (from === '/') {
return ['/node_modules'];
+ }
// note: this approach *only* works when the path is guaranteed
// to be absolute. Doing a fully-edge-case-correct path.split
// that works on both Windows and Posix is non-trivial.
+ /** @type {string[]} */
const paths = [];
for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) {
const code = StringPrototypeCharCodeAt(from, i);
if (code === CHAR_FORWARD_SLASH) {
- if (p !== nmLen)
+ if (p !== nmLen) {
ArrayPrototypePush(
paths,
StringPrototypeSlice(from, 0, last) + '/node_modules',
);
+ }
last = i;
p = 0;
} else if (p !== -1) {
@@ -781,6 +821,11 @@ if (isWindows) {
};
}
+/**
+ * Get the paths for module resolution.
+ * @param {string} request
+ * @param {Module} parent
+ */
Module._resolveLookupPaths = function(request, parent) {
if (BuiltinModule.normalizeRequirableId(request)) {
debug('looking for %j in []', request);
@@ -794,6 +839,7 @@ Module._resolveLookupPaths = function(request, parent) {
StringPrototypeCharAt(request, 1) !== '/' &&
(!isWindows || StringPrototypeCharAt(request, 1) !== '\\'))) {
+ /** @type {string[]} */
let paths;
if (parent?.paths?.length) {
paths = ArrayPrototypeSlice(modulePaths);
@@ -823,6 +869,10 @@ Module._resolveLookupPaths = function(request, parent) {
return parentDir;
};
+/**
+ * Emits a warning when a non-existent property of module exports is accessed inside a circular dependency.
+ * @param {string} prop The name of the non-existent property.
+ */
function emitCircularRequireWarning(prop) {
process.emitWarning(
`Accessing non-existent property '${String(prop)}' of module exports ` +
@@ -839,19 +889,26 @@ const CircularRequirePrototypeWarningProxy = new Proxy({}, {
// Allow __esModule access in any case because it is used in the output
// of transpiled code to determine whether something comes from an
// ES module, and is not used as a regular key of `module.exports`.
- if (prop in target || prop === '__esModule') return target[prop];
+ if (prop in target || prop === '__esModule') { return target[prop]; }
emitCircularRequireWarning(prop);
return undefined;
},
getOwnPropertyDescriptor(target, prop) {
- if (ObjectPrototypeHasOwnProperty(target, prop) || prop === '__esModule')
+ if (ObjectPrototypeHasOwnProperty(target, prop) || prop === '__esModule') {
return ObjectGetOwnPropertyDescriptor(target, prop);
+ }
emitCircularRequireWarning(prop);
return undefined;
},
});
+/**
+ * Returns the exports object for a module that has a circular `require`.
+ * If the exports object is a plain object, it is wrapped in a proxy that warns
+ * about circular dependencies.
+ * @param {Module} module The module instance
+ */
function getExportsForCircularRequire(module) {
if (module.exports &&
!isProxy(module.exports) &&
@@ -869,13 +926,17 @@ function getExportsForCircularRequire(module) {
return module.exports;
}
-// Check the cache for the requested file.
-// 1. If a module already exists in the cache: return its exports object.
-// 2. If the module is native: call
-// `BuiltinModule.prototype.compileForPublicLoader()` and return the exports.
-// 3. Otherwise, create a new module for the file and save it to the cache.
-// Then have it load the file contents before returning its exports
-// object.
+/**
+ * Load a module from cache if it exists, otherwise create a new module instance.
+ * 1. If a module already exists in the cache: return its exports object.
+ * 2. If the module is native: call
+ * `BuiltinModule.prototype.compileForPublicLoader()` and return the exports.
+ * 3. Otherwise, create a new module for the file and save it to the cache.
+ * Then have it load the file contents before returning its exports object.
+ * @param {string} request Specifier of module to load via `require`
+ * @param {string} parent Absolute path of the module importing the child
+ * @param {boolean} isMain Whether the module is the main entry point
+ */
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
@@ -890,8 +951,9 @@ Module._load = function(request, parent, isMain) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
- if (!cachedModule.loaded)
+ if (!cachedModule.loaded) {
return getExportsForCircularRequire(cachedModule);
+ }
return cachedModule.exports;
}
delete relativeResolveCache[relResolveCacheIdentifier];
@@ -916,8 +978,9 @@ Module._load = function(request, parent, isMain) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded) {
const parseCachedModule = cjsParseCache.get(cachedModule);
- if (!parseCachedModule || parseCachedModule.loaded)
+ if (!parseCachedModule || parseCachedModule.loaded) {
return getExportsForCircularRequire(cachedModule);
+ }
parseCachedModule.loaded = true;
} else {
return cachedModule.exports;
@@ -973,6 +1036,15 @@ Module._load = function(request, parent, isMain) {
return module.exports;
};
+/**
+ * Given a `require` string and its context, get its absolute file path.
+ * @param {string} request The specifier to resolve
+ * @param {Module} parent The module containing the `require` call
+ * @param {boolean} isMain Whether the module is the main entry point
+ * @param {ResolveFilenameOptions} options Options object
+ * @typedef {object} ResolveFilenameOptions
+ * @property {string[]} paths Paths to search for modules in
+ */
Module._resolveFilename = function(request, parent, isMain, options) {
if (BuiltinModule.normalizeRequirableId(request)) {
return request;
@@ -1000,8 +1072,9 @@ Module._resolveFilename = function(request, parent, isMain, options) {
const lookupPaths = Module._resolveLookupPaths(request, fakeParent);
for (let j = 0; j < lookupPaths.length; j++) {
- if (!ArrayPrototypeIncludes(paths, lookupPaths[j]))
+ if (!ArrayPrototypeIncludes(paths, lookupPaths[j])) {
ArrayPrototypePush(paths, lookupPaths[j]);
+ }
}
}
}
@@ -1016,7 +1089,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
if (request[0] === '#' && (parent?.filename || parent?.id === '')) {
const parentPath = parent?.filename ?? process.cwd() + path.sep;
- const pkg = readPackageScope(parentPath) || {};
+ const pkg = packageJsonReader.readPackageScope(parentPath) || { __proto__: null };
if (pkg.data?.imports != null) {
try {
const { packageImportsResolve } = require('internal/modules/esm/resolve');
@@ -1025,8 +1098,9 @@ Module._resolveFilename = function(request, parent, isMain, options) {
getCjsConditions()), parentPath,
pkg.path);
} catch (e) {
- if (e.code === 'ERR_MODULE_NOT_FOUND')
+ if (e.code === 'ERR_MODULE_NOT_FOUND') {
throw createEsmNotFoundErr(request);
+ }
throw e;
}
}
@@ -1044,7 +1118,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
// Look up the filename first, since that's the cache key.
const filename = Module._findPath(request, paths, isMain);
- if (filename) return filename;
+ if (filename) { return filename; }
const requireStack = [];
for (let cursor = parent;
cursor;
@@ -1063,31 +1137,50 @@ Module._resolveFilename = function(request, parent, isMain, options) {
throw err;
};
+/**
+ * Finishes resolving an ES module specifier into an absolute file path.
+ * @param {string} resolved The resolved module specifier
+ * @param {string} parentPath The path of the parent module
+ * @param {string} pkgPath The path of the package.json file
+ * @throws {ERR_INVALID_MODULE_SPECIFIER} If the resolved module specifier contains encoded `/` or `\\` characters
+ * @throws {Error} If the module cannot be found
+ */
function finalizeEsmResolution(resolved, parentPath, pkgPath) {
const { encodedSepRegEx } = require('internal/modules/esm/resolve');
- if (RegExpPrototypeExec(encodedSepRegEx, resolved) !== null)
+ if (RegExpPrototypeExec(encodedSepRegEx, resolved) !== null) {
throw new ERR_INVALID_MODULE_SPECIFIER(
resolved, 'must not include encoded "/" or "\\" characters', parentPath);
+ }
const filename = fileURLToPath(resolved);
const actual = tryFile(filename);
- if (actual)
+ if (actual) {
return actual;
+ }
const err = createEsmNotFoundErr(filename,
path.resolve(pkgPath, 'package.json'));
throw err;
}
+/**
+ * Creates an error object for when a requested ES module cannot be found.
+ * @param {string} request The name of the requested module
+ * @param {string} [path] The path to the requested module
+ */
function createEsmNotFoundErr(request, path) {
// eslint-disable-next-line no-restricted-syntax
const err = new Error(`Cannot find module '${request}'`);
err.code = 'MODULE_NOT_FOUND';
- if (path)
+ if (path) {
err.path = path;
+ }
// TODO(BridgeAR): Add the requireStack as well.
return err;
}
-// Given a file name, pass it to the proper extension handler.
+/**
+ * Given a file name, pass it to the proper extension handler.
+ * @param {string} filename The `require` specifier
+ */
Module.prototype.load = function(filename) {
debug('load %j for module %j', filename, this.id);
@@ -1097,8 +1190,9 @@ Module.prototype.load = function(filename) {
const extension = findLongestRegisteredExtension(filename);
// allow .mjs to be overridden
- if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs'])
+ if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) {
throw new ERR_REQUIRE_ESM(filename, true);
+ }
Module._extensions[extension](this, filename);
this.loaded = true;
@@ -1109,13 +1203,17 @@ Module.prototype.load = function(filename) {
// Preemptively cache
if ((module?.module === undefined ||
module.module.getStatus() < kEvaluated) &&
- !cascadedLoader.cjsCache.has(this))
+ !cascadedLoader.cjsCache.has(this)) {
cascadedLoader.cjsCache.set(this, exports);
+ }
};
-// Loads a module at the given file path. Returns that module's
-// `exports` property.
-// Note: when using the experimental policy mechanism this function is overridden
+/**
+ * Loads a module at the given file path. Returns that module's `exports` property.
+ * Note: when using the experimental policy mechanism this function is overridden.
+ * @param {string} id
+ * @throws {ERR_INVALID_ARG_TYPE} When `id` is not a string
+ */
Module.prototype.require = function(id) {
validateString(id, 'id');
if (id === '') {
@@ -1130,11 +1228,22 @@ Module.prototype.require = function(id) {
}
};
-// Resolved path to process.argv[1] will be lazily placed here
-// (needed for setting breakpoint when called with --inspect-brk)
+/**
+ * Resolved path to `process.argv[1]` will be lazily placed here
+ * (needed for setting breakpoint when called with `--inspect-brk`).
+ * @type {string | undefined}
+ */
let resolvedArgv;
let hasPausedEntry = false;
+/** @type {import('vm').Script} */
let Script;
+
+/**
+ * Wraps the given content in a script and runs it in a new context.
+ * @param {string} filename The name of the file being loaded
+ * @param {string} content The content of the file being loaded
+ * @param {Module} cjsModuleInstance The CommonJS loader instance
+ */
function wrapSafe(filename, content, cjsModuleInstance) {
if (patched) {
const wrapper = Module.wrap(content);
@@ -1144,10 +1253,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
const script = new Script(wrapper, {
filename,
lineOffset: 0,
- importModuleDynamically: async (specifier, _, importAssertions) => {
+ importModuleDynamically: async (specifier, _, importAttributes) => {
const cascadedLoader = getCascadedLoader();
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
- importAssertions);
+ importAttributes);
},
});
@@ -1170,10 +1279,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
'__dirname',
], {
filename,
- importModuleDynamically(specifier, _, importAssertions) {
+ importModuleDynamically(specifier, _, importAttributes) {
const cascadedLoader = getCascadedLoader();
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
- importAssertions);
+ importAttributes);
},
});
@@ -1192,10 +1301,12 @@ function wrapSafe(filename, content, cjsModuleInstance) {
}
}
-// Run the file contents in the correct scope or sandbox. Expose
-// the correct helper variables (require, module, exports) to
-// the file.
-// Returns exception, if any.
+/**
+ * Run the file contents in the correct scope or sandbox. Expose the correct helper variables (`require`, `module`,
+ * `exports`) to the file. Returns exception, if any.
+ * @param {string} content The source code of the module
+ * @param {string} filename The file path of the module
+ */
Module.prototype._compile = function(content, filename) {
let moduleURL;
let redirects;
@@ -1237,7 +1348,7 @@ Module.prototype._compile = function(content, filename) {
const exports = this.exports;
const thisValue = exports;
const module = this;
- if (requireDepth === 0) statCache = new SafeMap();
+ if (requireDepth === 0) { statCache = new SafeMap(); }
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, thisValue, exports,
require, module, filename, dirname);
@@ -1246,11 +1357,15 @@ Module.prototype._compile = function(content, filename) {
[exports, require, module, filename, dirname]);
}
hasLoadedAnyUserCJSModule = true;
- if (requireDepth === 0) statCache = null;
+ if (requireDepth === 0) { statCache = null; }
return result;
};
-// Native extension for .js
+/**
+ * Native handler for `.js` files.
+ * @param {Module} module The module to compile
+ * @param {string} filename The file path of the module
+ */
Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
const cached = cjsParseCache.get(module);
@@ -1262,9 +1377,9 @@ Module._extensions['.js'] = function(module, filename) {
content = fs.readFileSync(filename, 'utf8');
}
if (StringPrototypeEndsWith(filename, '.js')) {
- const pkg = readPackageScope(filename);
+ const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
// Function require shouldn't be used in ES modules.
- if (pkg?.data?.type === 'module') {
+ if (pkg.data?.type === 'module') {
const parent = moduleParentCache.get(module);
const parentPath = parent?.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
@@ -1299,8 +1414,11 @@ Module._extensions['.js'] = function(module, filename) {
module._compile(content, filename);
};
-
-// Native extension for .json
+/**
+ * Native handler for `.json` files.
+ * @param {Module} module The module to compile
+ * @param {string} filename The file path of the module
+ */
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
@@ -1318,8 +1436,11 @@ Module._extensions['.json'] = function(module, filename) {
}
};
-
-// Native extension for .node
+/**
+ * Native handler for `.node` files.
+ * @param {Module} module The module to compile
+ * @param {string} filename The file path of the module
+ */
Module._extensions['.node'] = function(module, filename) {
const manifest = policy()?.manifest;
if (manifest) {
@@ -1331,6 +1452,10 @@ Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path.toNamespacedPath(filename));
};
+/**
+ * Creates a `require` function that can be used to load modules from the specified path.
+ * @param {string} filename The path to the module
+ */
function createRequireFromPath(filename) {
// Allow a directory to be passed as the filename
const trailingSlash =
@@ -1351,6 +1476,12 @@ function createRequireFromPath(filename) {
const createRequireError = 'must be a file URL object, file URL string, or ' +
'absolute path string';
+/**
+ * Creates a new `require` function that can be used to load modules.
+ * @param {string | URL} filename The path or URL to the module context for this `require`
+ * @throws {ERR_INVALID_ARG_VALUE} If `filename` is not a string or URL, or if it is a relative path that cannot be
+ * resolved to an absolute path.
+ */
function createRequire(filename) {
let filepath;
@@ -1372,6 +1503,9 @@ function createRequire(filename) {
Module.createRequire = createRequire;
+/**
+ * Define the paths to use for resolving a module.
+ */
Module._initPaths = function() {
const homeDir = isWindows ? process.env.USERPROFILE : safeGetenv('HOME');
const nodePath = isWindows ? process.env.NODE_PATH : safeGetenv('NODE_PATH');
@@ -1402,9 +1536,12 @@ Module._initPaths = function() {
Module.globalPaths = ArrayPrototypeSlice(modulePaths);
};
+/**
+ * Handle modules loaded via `--require`.
+ * @param {string[]} requests The values of `--require`
+ */
Module._preloadModules = function(requests) {
- if (!ArrayIsArray(requests))
- return;
+ if (!ArrayIsArray(requests)) { return; }
isPreloading = true;
@@ -1420,11 +1557,16 @@ Module._preloadModules = function(requests) {
throw e;
}
}
- for (let n = 0; n < requests.length; n++)
+ for (let n = 0; n < requests.length; n++) {
internalRequire(parent, requests[n]);
+ }
isPreloading = false;
};
+/**
+ * If the user has overridden an export from a builtin module, this function can ensure that the override is used in
+ * both CommonJS and ES module contexts.
+ */
Module.syncBuiltinESMExports = function syncBuiltinESMExports() {
for (const mod of BuiltinModule.map.values()) {
if (BuiltinModule.canBeRequiredWithoutScheme(mod.id)) {
diff --git a/lib/internal/modules/esm/assert.js b/lib/internal/modules/esm/assert.js
index b1267a10a7a6b2..ce3280de84bf4d 100644
--- a/lib/internal/modules/esm/assert.js
+++ b/lib/internal/modules/esm/assert.js
@@ -3,7 +3,6 @@
const {
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
- ObjectCreate,
ObjectKeys,
ObjectValues,
ObjectPrototypeHasOwnProperty,
@@ -14,16 +13,15 @@ const {
ERR_IMPORT_ASSERTION_TYPE_FAILED,
ERR_IMPORT_ASSERTION_TYPE_MISSING,
ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED,
+ ERR_IMPORT_ATTRIBUTE_UNSUPPORTED,
} = require('internal/errors').codes;
// The HTML spec has an implied default type of `'javascript'`.
const kImplicitAssertType = 'javascript';
-let alreadyWarned = false;
-
/**
- * Define a map of module formats to import assertion types (the value of
- * `type` in `assert { type: 'json' }`).
+ * Define a map of module formats to import attributes types (the value of
+ * `type` in `with { type: 'json' }`).
* @type {Map}
*/
const formatTypeMap = {
@@ -32,13 +30,13 @@ const formatTypeMap = {
'commonjs': kImplicitAssertType,
'json': 'json',
'module': kImplicitAssertType,
- 'wasm': kImplicitAssertType, // It's unclear whether the HTML spec will require an assertion type or not for Wasm; see https://github.com/WebAssembly/esm-integration/issues/42
+ 'wasm': kImplicitAssertType, // It's unclear whether the HTML spec will require an attribute type or not for Wasm; see https://github.com/WebAssembly/esm-integration/issues/42
};
/**
* The HTML spec disallows the default type to be explicitly specified
* (for now); so `import './file.js'` is okay but
- * `import './file.js' assert { type: 'javascript' }` throws.
+ * `import './file.js' with { type: 'javascript' }` throws.
* @type {Array}
*/
const supportedAssertionTypes = ArrayPrototypeFilter(
@@ -47,54 +45,50 @@ const supportedAssertionTypes = ArrayPrototypeFilter(
/**
- * Test a module's import assertions.
+ * Test a module's import attributes.
* @param {string} url The URL of the imported module, for error reporting.
* @param {string} format One of Node's supported translators
- * @param {Record} importAssertions Validations for the
+ * @param {Record} importAttributes Validations for the
* module import.
* @returns {true}
* @throws {TypeError} If the format and assertion type are incompatible.
*/
-function validateAssertions(url, format,
- importAssertions = ObjectCreate(null)) {
- const validType = formatTypeMap[format];
-
- if (!alreadyWarned && ObjectKeys(importAssertions).length !== 0) {
- alreadyWarned = true;
- process.emitWarning(
- 'Import assertions are not a stable feature of the JavaScript language. ' +
- 'Avoid relying on their current behavior and syntax as those might change ' +
- 'in a future version of Node.js.',
- 'ExperimentalWarning',
- );
+function validateAttributes(url, format,
+ importAttributes = { __proto__: null }) {
+ const keys = ObjectKeys(importAttributes);
+ for (let i = 0; i < keys.length; i++) {
+ if (keys[i] !== 'type') {
+ throw new ERR_IMPORT_ATTRIBUTE_UNSUPPORTED(keys[i], importAttributes[keys[i]]);
+ }
}
+ const validType = formatTypeMap[format];
switch (validType) {
case undefined:
- // Ignore assertions for module formats we don't recognize, to allow new
+ // Ignore attributes for module formats we don't recognize, to allow new
// formats in the future.
return true;
case kImplicitAssertType:
// This format doesn't allow an import assertion type, so the property
- // must not be set on the import assertions object.
- if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
+ // must not be set on the import attributes object.
+ if (!ObjectPrototypeHasOwnProperty(importAttributes, 'type')) {
return true;
}
- return handleInvalidType(url, importAssertions.type);
+ return handleInvalidType(url, importAttributes.type);
- case importAssertions.type:
+ case importAttributes.type:
// The asserted type is the valid type for this format.
return true;
default:
// There is an expected type for this format, but the value of
- // `importAssertions.type` might not have been it.
- if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
+ // `importAttributes.type` might not have been it.
+ if (!ObjectPrototypeHasOwnProperty(importAttributes, 'type')) {
// `type` wasn't specified at all.
throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType);
}
- return handleInvalidType(url, importAssertions.type);
+ return handleInvalidType(url, importAttributes.type);
}
}
@@ -119,5 +113,5 @@ function handleInvalidType(url, type) {
module.exports = {
kImplicitAssertType,
- validateAssertions,
+ validateAttributes,
};
diff --git a/lib/internal/modules/esm/create_dynamic_module.js b/lib/internal/modules/esm/create_dynamic_module.js
index 32f1c82a7a20c2..c0060c47e93b5a 100644
--- a/lib/internal/modules/esm/create_dynamic_module.js
+++ b/lib/internal/modules/esm/create_dynamic_module.js
@@ -12,12 +12,22 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
+/**
+ * Creates an import statement for a given module path and index.
+ * @param {string} impt - The module path to import.
+ * @param {number} index - The index of the import statement.
+ */
function createImport(impt, index) {
const imptPath = JSONStringify(impt);
return `import * as $import_${index} from ${imptPath};
import.meta.imports[${imptPath}] = $import_${index};`;
}
+/**
+ * Creates an export for a given module.
+ * @param {string} expt - The name of the export.
+ * @param {number} index - The index of the export statement.
+ */
function createExport(expt, index) {
const nameStringLit = JSONStringify(expt);
return `let $export_${index};
@@ -28,6 +38,17 @@ import.meta.exports[${nameStringLit}] = {
};`;
}
+/**
+ * Creates a dynamic module with the given imports, exports, URL, and evaluate function.
+ * @param {string[]} imports - An array of imports.
+ * @param {string[]} exports - An array of exports.
+ * @param {string} [url=''] - The URL of the module.
+ * @param {(reflect: DynamicModuleReflect) => void} evaluate - The function to evaluate the module.
+ * @typedef {object} DynamicModuleReflect
+ * @property {string[]} imports - The imports of the module.
+ * @property {string[]} exports - The exports of the module.
+ * @property {(cb: (reflect: DynamicModuleReflect) => void) => void} onReady - Callback to evaluate the module.
+ */
const createDynamicModule = (imports, exports, url = '', evaluate) => {
debug('creating ESM facade for %s with exports: %j', url, exports);
const source = `
@@ -39,19 +60,22 @@ import.meta.done();
const m = new ModuleWrap(`${url}`, undefined, source, 0, 0);
const readyfns = new SafeSet();
+ /** @type {DynamicModuleReflect} */
const reflect = {
exports: ObjectCreate(null),
onReady: (cb) => { readyfns.add(cb); },
};
- if (imports.length)
- reflect.imports = ObjectCreate(null);
+ if (imports.length) {
+ reflect.imports = { __proto__: null };
+ }
const { setCallbackForWrap } = require('internal/modules/esm/utils');
setCallbackForWrap(m, {
initializeImportMeta: (meta, wrap) => {
meta.exports = reflect.exports;
- if (reflect.imports)
+ if (reflect.imports) {
meta.imports = reflect.imports;
+ }
meta.done = () => {
evaluate(reflect);
reflect.onReady = (cb) => cb(reflect);
diff --git a/lib/internal/modules/esm/fetch_module.js b/lib/internal/modules/esm/fetch_module.js
index 74d2d2599dbd45..21b7456899604f 100644
--- a/lib/internal/modules/esm/fetch_module.js
+++ b/lib/internal/modules/esm/fetch_module.js
@@ -44,37 +44,56 @@ const cacheForGET = new SafeMap();
// [2] Creating a new agent instead of using the gloabl agent improves
// performance and precludes the agent becoming tainted.
+/** @type {import('https').Agent} The Cached HTTP Agent for **secure** HTTP requests. */
let HTTPSAgent;
-function HTTPSGet(url, opts) {
+/**
+ * Make a HTTPs GET request (handling agent setup if needed, caching the agent to avoid
+ * redudant instantiations).
+ * @param {Parameters[0]} input - The URI to fetch.
+ * @param {Parameters[1]} options - See https.get() options.
+ */
+function HTTPSGet(input, options) {
const https = require('https'); // [1]
HTTPSAgent ??= new https.Agent({ // [2]
keepAlive: true,
});
- return https.get(url, {
+ return https.get(input, {
agent: HTTPSAgent,
- ...opts,
+ ...options,
});
}
+/** @type {import('https').Agent} The Cached HTTP Agent for **insecure** HTTP requests. */
let HTTPAgent;
-function HTTPGet(url, opts) {
+/**
+ * Make a HTTP GET request (handling agent setup if needed, caching the agent to avoid
+ * redudant instantiations).
+ * @param {Parameters[0]} input - The URI to fetch.
+ * @param {Parameters[1]} options - See http.get() options.
+ */
+function HTTPGet(input, options) {
const http = require('http'); // [1]
HTTPAgent ??= new http.Agent({ // [2]
keepAlive: true,
});
- return http.get(url, {
+ return http.get(input, {
agent: HTTPAgent,
- ...opts,
+ ...options,
});
}
-function dnsLookup(name, opts) {
+/** @type {import('../../dns/promises.js').lookup} */
+function dnsLookup(hostname, options) {
// eslint-disable-next-line no-func-assign
dnsLookup = require('dns/promises').lookup;
- return dnsLookup(name, opts);
+ return dnsLookup(hostname, options);
}
let zlib;
+/**
+ * Create a decompressor for the Brotli format.
+ * @returns {import('zlib').BrotliDecompress}
+ */
function createBrotliDecompress() {
zlib ??= require('zlib'); // [1]
// eslint-disable-next-line no-func-assign
@@ -82,6 +101,10 @@ function createBrotliDecompress() {
return createBrotliDecompress();
}
+/**
+ * Create an unzip handler.
+ * @returns {import('zlib').Unzip}
+ */
function createUnzip() {
zlib ??= require('zlib'); // [1]
// eslint-disable-next-line no-func-assign
@@ -144,7 +167,7 @@ function fetchWithRedirects(parsed) {
return entry;
}
if (res.statusCode === 404) {
- const err = new ERR_MODULE_NOT_FOUND(parsed.href, null);
+ const err = new ERR_MODULE_NOT_FOUND(parsed.href, null, parsed);
err.message = `Cannot find module '${parsed.href}', HTTP 404`;
throw err;
}
diff --git a/lib/internal/modules/esm/formats.js b/lib/internal/modules/esm/formats.js
index b52a31ff54080e..b081cbe8dd54d5 100644
--- a/lib/internal/modules/esm/formats.js
+++ b/lib/internal/modules/esm/formats.js
@@ -2,9 +2,11 @@
const {
RegExpPrototypeExec,
+ Uint8Array,
} = primordials;
const { getOptionValue } = require('internal/options');
+const { closeSync, openSync, readSync } = require('fs');
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
@@ -39,9 +41,9 @@ function mimeToFormat(mime) {
/\s*(text|application)\/javascript\s*(;\s*charset=utf-?8\s*)?/i,
mime,
) !== null
- ) return 'module';
- if (mime === 'application/json') return 'json';
- if (experimentalWasmModules && mime === 'application/wasm') return 'wasm';
+ ) { return 'module'; }
+ if (mime === 'application/json') { return 'json'; }
+ if (experimentalWasmModules && mime === 'application/wasm') { return 'wasm'; }
return null;
}
@@ -49,8 +51,34 @@ function getLegacyExtensionFormat(ext) {
return legacyExtensionFormatMap[ext];
}
+/**
+ * For extensionless files in a `module` package scope, or a default `module` scope enabled by the
+ * `--experimental-default-type` flag, we check the file contents to disambiguate between ES module JavaScript and Wasm.
+ * We do this by taking advantage of the fact that all Wasm files start with the header `0x00 0x61 0x73 0x6d` (`_asm`).
+ * @param {URL} url
+ */
+function getFormatOfExtensionlessFile(url) {
+ if (!experimentalWasmModules) { return 'module'; }
+
+ const magic = new Uint8Array(4);
+ let fd;
+ try {
+ // TODO(@anonrig): Optimize the following by having a single C++ call
+ fd = openSync(url);
+ readSync(fd, magic, 0, 4); // Only read the first four bytes
+ if (magic[0] === 0x00 && magic[1] === 0x61 && magic[2] === 0x73 && magic[3] === 0x6d) {
+ return 'wasm';
+ }
+ } finally {
+ if (fd !== undefined) { closeSync(fd); }
+ }
+
+ return 'module';
+}
+
module.exports = {
extensionFormatMap,
+ getFormatOfExtensionlessFile,
getLegacyExtensionFormat,
legacyExtensionFormatMap,
mimeToFormat,
diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js
index 0f600b9cdcfc68..9ad0110b0c3716 100644
--- a/lib/internal/modules/esm/get_format.js
+++ b/lib/internal/modules/esm/get_format.js
@@ -1,16 +1,18 @@
'use strict';
+
const {
RegExpPrototypeExec,
ObjectPrototypeHasOwnProperty,
PromisePrototypeThen,
PromiseResolve,
+ StringPrototypeIncludes,
StringPrototypeCharCodeAt,
StringPrototypeSlice,
} = primordials;
-const { basename, relative } = require('path');
const { getOptionValue } = require('internal/options');
const {
extensionFormatMap,
+ getFormatOfExtensionlessFile,
getLegacyExtensionFormat,
mimeToFormat,
} = require('internal/modules/esm/formats');
@@ -19,7 +21,10 @@ const experimentalNetworkImports =
getOptionValue('--experimental-network-imports');
const experimentalSpecifierResolution =
getOptionValue('--experimental-specifier-resolution');
-const { getPackageType, getPackageScopeConfig } = require('internal/modules/esm/resolve');
+const defaultTypeFlag = getOptionValue('--experimental-default-type');
+// The next line is where we flip the default to ES modules someday.
+const defaultType = defaultTypeFlag === 'module' ? 'module' : 'commonjs';
+const { getPackageType } = require('internal/modules/esm/resolve');
const { fileURLToPath } = require('internal/url');
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
@@ -69,6 +74,18 @@ function extname(url) {
return '';
}
+/**
+ * Determine whether the given file URL is under a `node_modules` folder.
+ * This function assumes that the input has already been verified to be a `file:` URL,
+ * and is a file rather than a folder.
+ * @param {URL} url
+ */
+function underNodeModules(url) {
+ if (url.protocol !== 'file:') { return false; } // We determine module types for other protocols based on MIME header
+
+ return StringPrototypeIncludes(url.pathname, '/node_modules/');
+}
+
/**
* @param {URL} url
* @param {{parentURL: string}} context
@@ -77,30 +94,46 @@ function extname(url) {
*/
function getFileProtocolModuleFormat(url, context, ignoreErrors) {
const ext = extname(url);
+
if (ext === '.js') {
- return getPackageType(url) === 'module' ? 'module' : 'commonjs';
+ const packageType = getPackageType(url);
+ if (packageType !== 'none') {
+ return packageType;
+ }
+ // The controlling `package.json` file has no `type` field.
+ if (defaultType === 'module') {
+ // An exception to the type flag making ESM the default everywhere is that package scopes under `node_modules`
+ // should retain the assumption that a lack of a `type` field means CommonJS.
+ return underNodeModules(url) ? 'commonjs' : 'module';
+ }
+ return 'commonjs';
+ }
+
+ if (ext === '') {
+ const packageType = getPackageType(url);
+ if (defaultType === 'commonjs') { // Legacy behavior
+ if (packageType === 'none' || packageType === 'commonjs') {
+ return 'commonjs';
+ } // Else packageType === 'module'
+ return getFormatOfExtensionlessFile(url);
+ } // Else defaultType === 'module'
+ if (underNodeModules(url)) { // Exception for package scopes under `node_modules`
+ return packageType === 'module' ? getFormatOfExtensionlessFile(url) : 'commonjs';
+ }
+ if (packageType === 'none' || packageType === 'module') {
+ return getFormatOfExtensionlessFile(url);
+ } // Else packageType === 'commonjs'
+ return 'commonjs';
}
const format = extensionFormatMap[ext];
- if (format) return format;
+ if (format) { return format; }
if (experimentalSpecifierResolution !== 'node') {
// Explicit undefined return indicates load hook should rerun format check
- if (ignoreErrors) return undefined;
+ if (ignoreErrors) { return undefined; }
const filepath = fileURLToPath(url);
- let suggestion = '';
- if (getPackageType(url) === 'module' && ext === '') {
- const config = getPackageScopeConfig(url);
- const fileBasename = basename(filepath);
- const relativePath = StringPrototypeSlice(relative(config.pjsonPath, filepath), 1);
- suggestion = 'Loading extensionless files is not supported inside of ' +
- '"type":"module" package.json contexts. The package.json file ' +
- `${config.pjsonPath} caused this "type":"module" context. Try ` +
- `changing ${filepath} to have a file extension. Note the "bin" ` +
- 'field of package.json can point to a file with an extension, for example ' +
- `{"type":"module","bin":{"${fileBasename}":"${relativePath}.js"}}`;
- }
- throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath, suggestion);
+ throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath);
}
return getLegacyExtensionFormat(ext) ?? null;
diff --git a/lib/internal/modules/esm/handle_process_exit.js b/lib/internal/modules/esm/handle_process_exit.js
index db830900bd3154..4febbcce54dd94 100644
--- a/lib/internal/modules/esm/handle_process_exit.js
+++ b/lib/internal/modules/esm/handle_process_exit.js
@@ -1,8 +1,10 @@
'use strict';
-// Handle a Promise from running code that potentially does Top-Level Await.
-// In that case, it makes sense to set the exit code to a specific non-zero
-// value if the main code never finishes running.
+/**
+ * Handle a Promise from running code that potentially does Top-Level Await.
+ * In that case, it makes sense to set the exit code to a specific non-zero value
+ * if the main code never finishes running.
+ */
function handleProcessExit() {
process.exitCode ??= 13;
}
diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js
index 11e85326324f2e..d6f7e04923bff2 100644
--- a/lib/internal/modules/esm/hooks.js
+++ b/lib/internal/modules/esm/hooks.js
@@ -1,27 +1,45 @@
'use strict';
const {
- ArrayPrototypeJoin,
ArrayPrototypePush,
+ ArrayPrototypePushApply,
FunctionPrototypeCall,
+ Int32Array,
ObjectAssign,
ObjectDefineProperty,
ObjectSetPrototypeOf,
+ Promise,
+ ReflectSet,
SafeSet,
StringPrototypeSlice,
+ StringPrototypeStartsWith,
StringPrototypeToUpperCase,
globalThis,
} = primordials;
const {
- ERR_LOADER_CHAIN_INCOMPLETE,
+ Atomics: {
+ load: AtomicsLoad,
+ wait: AtomicsWait,
+ waitAsync: AtomicsWaitAsync,
+ },
+ SharedArrayBuffer,
+} = globalThis;
+
+const {
ERR_INTERNAL_ASSERTION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
+ ERR_LOADER_CHAIN_INCOMPLETE,
+ ERR_METHOD_NOT_IMPLEMENTED,
+ ERR_UNKNOWN_BUILTIN_MODULE,
+ ERR_WORKER_UNSERIALIZABLE_ERROR,
} = require('internal/errors').codes;
-const { isURL, URL } = require('internal/url');
+const { URL } = require('internal/url');
+const { canParse: URLCanParse } = internalBinding('url');
+const { receiveMessageOnPort } = require('worker_threads');
const {
isAnyArrayBuffer,
isArrayBufferView,
@@ -30,14 +48,60 @@ const {
validateObject,
validateString,
} = require('internal/validators');
+const {
+ emitExperimentalWarning,
+ kEmptyObject,
+} = require('internal/util');
const {
defaultResolve,
+ throwIfInvalidParentURL,
} = require('internal/modules/esm/resolve');
const {
getDefaultConditions,
+ loaderWorkerId,
} = require('internal/modules/esm/utils');
+const { deserializeError } = require('internal/error_serdes');
+const {
+ SHARED_MEMORY_BYTE_LENGTH,
+ WORKER_TO_MAIN_THREAD_NOTIFICATION,
+} = require('internal/modules/esm/shared_constants');
+let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
+ debug = fn;
+});
+let importMetaInitializer;
+
+let importAssertionAlreadyWarned = false;
+
+function emitImportAssertionWarning() {
+ if (!importAssertionAlreadyWarned) {
+ importAssertionAlreadyWarned = true;
+ process.emitWarning('Use `importAttributes` instead of `importAssertions`', 'ExperimentalWarning');
+ }
+}
+
+function defineImportAssertionAlias(context) {
+ return ObjectDefineProperty(context, 'importAssertions', {
+ __proto__: null,
+ configurable: true,
+ get() {
+ emitImportAssertionWarning();
+ return this.importAttributes;
+ },
+ set(value) {
+ emitImportAssertionWarning();
+ return ReflectSet(this, 'importAttributes', value);
+ },
+ });
+}
+/**
+ * @typedef {object} ExportedHooks
+ * @property {Function} initialize Customizations setup hook.
+ * @property {Function} globalPreload Global preload hook.
+ * @property {Function} resolve Resolve hook.
+ * @property {Function} load Load hook.
+ */
/**
* @typedef {object} KeyedHook
@@ -47,9 +111,8 @@ const {
// [2] `validate...()`s throw the wrong error
-
class Hooks {
- #hooks = {
+ #chains = {
/**
* Prior to ESM loading. These are called once before any modules are started.
* @private
@@ -83,75 +146,68 @@ class Hooks {
],
};
- // Enable an optimization in ESMLoader.getModuleJob
- hasCustomResolveOrLoadHooks = false;
-
// Cache URLs we've already validated to avoid repeated validation
#validatedUrls = new SafeSet();
- #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
+ allowImportMetaResolve = false;
- constructor(userLoaders) {
- this.#addCustomLoaders(userLoaders);
+ /**
+ * Import and register custom/user-defined module loader hook(s).
+ * @param {string} urlOrSpecifier
+ * @param {string} parentURL
+ * @param {any} [data] Arbitrary data to be passed from the custom
+ * loader (user-land) to the worker.
+ */
+ async register(urlOrSpecifier, parentURL, data) {
+ const moduleLoader = require('internal/process/esm_loader').esmLoader;
+ const keyedExports = await moduleLoader.import(
+ urlOrSpecifier,
+ parentURL,
+ kEmptyObject,
+ );
+ await this.addCustomLoader(urlOrSpecifier, keyedExports, data);
}
/**
* Collect custom/user-defined module loader hook(s).
* After all hooks have been collected, the global preload hook(s) must be initialized.
- * @param {import('./loader.js).KeyedExports} customLoaders Exports from user-defined loaders
- * (as returned by `ESMLoader.import()`).
+ * @param {string} url Custom loader specifier
+ * @param {Record} exports
+ * @param {any} [data] Arbitrary data to be passed from the custom loader (user-land)
+ * to the worker.
+ * @returns {any | Promise} User data, ignored unless it's a promise, in which case it will be awaited.
*/
- #addCustomLoaders(
- customLoaders = [],
- ) {
- for (let i = 0; i < customLoaders.length; i++) {
- const {
- exports,
- url,
- } = customLoaders[i];
- const {
- globalPreload,
- resolve,
- load,
- } = pluckHooks(exports);
-
- if (globalPreload) {
- ArrayPrototypePush(
- this.#hooks.globalPreload,
- {
- fn: globalPreload,
- url,
- },
- );
- }
- if (resolve) {
- this.hasCustomResolveOrLoadHooks = true;
- ArrayPrototypePush(
- this.#hooks.resolve,
- {
- fn: resolve,
- url,
- },
- );
- }
- if (load) {
- this.hasCustomResolveOrLoadHooks = true;
- ArrayPrototypePush(
- this.#hooks.load,
- {
- fn: load,
- url,
- },
- );
- }
+ addCustomLoader(url, exports, data) {
+ const {
+ globalPreload,
+ initialize,
+ resolve,
+ load,
+ } = pluckHooks(exports);
+
+ if (globalPreload && !initialize) {
+ emitExperimentalWarning(
+ '`globalPreload` is planned for removal in favor of `initialize`. `globalPreload`',
+ );
+ ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url });
}
+ if (resolve) {
+ const next = this.#chains.resolve[this.#chains.resolve.length - 1];
+ ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next });
+ }
+ if (load) {
+ const next = this.#chains.load[this.#chains.load.length - 1];
+ ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
+ }
+ return initialize?.(data);
}
/**
* Initialize `globalPreload` hooks.
*/
- preload() {
- for (let i = this.#hooks.globalPreload.length - 1; i >= 0; i--) {
+ initializeGlobalPreload() {
+ const preloadScripts = [];
+ for (let i = this.#chains.globalPreload.length - 1; i >= 0; i--) {
const { MessageChannel } = require('internal/worker/io');
const channel = new MessageChannel();
const {
@@ -165,77 +221,28 @@ class Hooks {
const {
fn: preload,
url: specifier,
- } = this.#hooks.globalPreload[i];
+ } = this.#chains.globalPreload[i];
const preloaded = preload({
port: insideLoader,
});
- if (preloaded == null) { return; }
-
- const hookErrIdentifier = `${specifier} globalPreload`;
+ if (preloaded == null) { continue; }
if (typeof preloaded !== 'string') { // [2]
throw new ERR_INVALID_RETURN_VALUE(
'a string',
- hookErrIdentifier,
+ `${specifier} globalPreload`,
preload,
);
}
- const { compileFunction } = require('vm');
- const preloadInit = compileFunction(
- preloaded,
- ['getBuiltin', 'port', 'setImportMetaCallback'],
- {
- filename: '',
- },
- );
- const { BuiltinModule } = require('internal/bootstrap/realm');
- // We only allow replacing the importMetaInitializer during preload;
- // after preload is finished, we disable the ability to replace it.
- //
- // This exposes accidentally setting the initializer too late by throwing an error.
- let finished = false;
- let replacedImportMetaInitializer = false;
- let next = this.#importMetaInitializer;
- try {
- // Calls the compiled preload source text gotten from the hook
- // Since the parameters are named we use positional parameters
- // see compileFunction above to cross reference the names
- FunctionPrototypeCall(
- preloadInit,
- globalThis,
- // Param getBuiltin
- (builtinName) => {
- if (BuiltinModule.canBeRequiredWithoutScheme(builtinName)) {
- return require(builtinName);
- }
- throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
- },
- // Param port
- insidePreload,
- // Param setImportMetaCallback
- (fn) => {
- if (finished || typeof fn !== 'function') {
- throw new ERR_INVALID_ARG_TYPE('fn', fn);
- }
- replacedImportMetaInitializer = true;
- const parent = next;
- next = (meta, context) => {
- return fn(meta, context, parent);
- };
- });
- } finally {
- finished = true;
- if (replacedImportMetaInitializer) {
- this.#importMetaInitializer = next;
- }
- }
- }
- }
- importMetaInitialize(meta, context) {
- this.#importMetaInitializer(meta, context);
+ ArrayPrototypePush(preloadScripts, {
+ code: preloaded,
+ port: insidePreload,
+ });
+ }
+ return preloadScripts;
}
/**
@@ -247,39 +254,27 @@ class Hooks {
* @param {string} originalSpecifier The specified URL path of the module to
* be resolved.
* @param {string} [parentURL] The URL path of the module's parent.
- * @param {ImportAssertions} [importAssertions] Assertions from the import
+ * @param {ImportAttributes} [importAttributes] Attributes from the import
* statement or expression.
* @returns {Promise<{ format: string, url: URL['href'] }>}
*/
async resolve(
originalSpecifier,
parentURL,
- importAssertions = { __proto__: null },
+ importAttributes = { __proto__: null },
) {
- const isMain = parentURL === undefined;
+ throwIfInvalidParentURL(parentURL);
- if (
- !isMain &&
- typeof parentURL !== 'string' &&
- !isURL(parentURL)
- ) {
- throw new ERR_INVALID_ARG_TYPE(
- 'parentURL',
- ['string', 'URL'],
- parentURL,
- );
- }
- const chain = this.#hooks.resolve;
+ const chain = this.#chains.resolve;
const context = {
conditions: getDefaultConditions(),
- importAssertions,
+ importAttributes,
parentURL,
};
const meta = {
chainFinished: null,
context,
hookErrIdentifier: '',
- hookIndex: chain.length - 1,
hookName: 'resolve',
shortCircuited: false,
};
@@ -290,7 +285,7 @@ class Hooks {
`${hookErrIdentifier} specifier`,
); // non-strings can be coerced to a URL string
- if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
+ if (ctx) { validateObject(ctx, `${hookErrIdentifier} context`); }
};
const validateOutput = (hookErrIdentifier, output) => {
if (typeof output !== 'object' || output === null) { // [2]
@@ -302,7 +297,7 @@ class Hooks {
}
};
- const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput });
+ const nextResolve = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
const resolution = await nextResolve(originalSpecifier, context);
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
@@ -315,9 +310,9 @@ class Hooks {
throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
}
+ let resolvedImportAttributes;
const {
format,
- importAssertions: resolvedImportAssertions,
url,
} = resolution;
@@ -334,10 +329,8 @@ class Hooks {
// Avoid expensive URL instantiation for known-good URLs
if (!this.#validatedUrls.has(url)) {
- try {
- new URL(url);
- this.#validatedUrls.add(url);
- } catch {
+ // No need to convert to string, since the type is already validated
+ if (!URLCanParse(url)) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a URL string',
hookErrIdentifier,
@@ -345,17 +338,26 @@ class Hooks {
url,
);
}
+
+ this.#validatedUrls.add(url);
+ }
+
+ if (!('importAttributes' in resolution) && ('importAssertions' in resolution)) {
+ emitImportAssertionWarning();
+ resolvedImportAttributes = resolution.importAssertions;
+ } else {
+ resolvedImportAttributes = resolution.importAttributes;
}
if (
- resolvedImportAssertions != null &&
- typeof resolvedImportAssertions !== 'object'
+ resolvedImportAttributes != null &&
+ typeof resolvedImportAttributes !== 'object'
) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'an object',
hookErrIdentifier,
- 'importAssertions',
- resolvedImportAssertions,
+ 'importAttributes',
+ resolvedImportAttributes,
);
}
@@ -374,11 +376,15 @@ class Hooks {
return {
__proto__: null,
format,
- importAssertions: resolvedImportAssertions,
+ importAttributes: resolvedImportAttributes,
url,
};
}
+ resolveSync(_originalSpecifier, _parentURL, _importAttributes) {
+ throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()');
+ }
+
/**
* Provide source that is understood by one of Node's translators.
*
@@ -390,12 +396,11 @@ class Hooks {
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
*/
async load(url, context = {}) {
- const chain = this.#hooks.load;
+ const chain = this.#chains.load;
const meta = {
chainFinished: null,
context,
hookErrIdentifier: '',
- hookIndex: chain.length - 1,
hookName: 'load',
shortCircuited: false,
};
@@ -413,16 +418,16 @@ class Hooks {
// Avoid expensive URL instantiation for known-good URLs
if (!this.#validatedUrls.has(nextUrl)) {
- try {
- new URL(nextUrl);
- this.#validatedUrls.add(nextUrl);
- } catch {
+ // No need to convert to string, since the type is already validated
+ if (!URLCanParse(nextUrl)) {
throw new ERR_INVALID_ARG_VALUE(
`${hookErrIdentifier} url`,
nextUrl,
'should be a URL string',
);
}
+
+ this.#validatedUrls.add(nextUrl);
}
if (ctx) { validateObject(ctx, `${hookErrIdentifier} context`); }
@@ -437,9 +442,9 @@ class Hooks {
}
};
- const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput });
+ const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
- const loaded = await nextLoad(url, context);
+ const loaded = await nextLoad(url, defineImportAssertionAlias(context));
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
validateOutput(hookErrIdentifier, loaded);
@@ -512,58 +517,269 @@ class Hooks {
source,
};
}
-}
-ObjectSetPrototypeOf(Hooks.prototype, null);
+ forceLoadHooks() {
+ // No-op
+ }
+ importMetaInitialize(meta, context, loader) {
+ importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
+ meta = importMetaInitializer(meta, context, loader);
+ return meta;
+ }
+}
+ObjectSetPrototypeOf(Hooks.prototype, null);
/**
- * A utility function to pluck the hooks from a user-defined loader.
- * @param {import('./loader.js).ModuleExports} exports
- * @returns {import('./loader.js).ExportedHooks}
+ * There may be multiple instances of Hooks/HooksProxy, but there is only 1 Internal worker, so
+ * there is only 1 MessageChannel.
*/
-function pluckHooks({
- globalPreload,
- resolve,
- load,
- // obsolete hooks:
- dynamicInstantiate,
- getFormat,
- getGlobalPreloadCode,
- getSource,
- transformSource,
-}) {
- const obsoleteHooks = [];
- const acceptedHooks = { __proto__: null };
+let MessageChannel;
+class HooksProxy {
+ /**
+ * Shared memory. Always use Atomics method to read or write to it.
+ * @type {Int32Array}
+ */
+ #lock;
+ /**
+ * The InternalWorker instance, which lets us communicate with the loader thread.
+ */
+ #worker;
- if (getGlobalPreloadCode) {
- globalPreload ??= getGlobalPreloadCode;
+ /**
+ * The last notification ID received from the worker. This is used to detect
+ * if the worker has already sent a notification before putting the main
+ * thread to sleep, to avoid a race condition.
+ * @type {number}
+ */
+ #workerNotificationLastId = 0;
- process.emitWarning(
- 'Loader hook "getGlobalPreloadCode" has been renamed to "globalPreload"',
- );
+ /**
+ * Track how many async responses the main thread should expect.
+ * @type {number}
+ */
+ #numberOfPendingAsyncResponses = 0;
+
+ #isReady = false;
+
+ constructor() {
+ const { InternalWorker } = require('internal/worker');
+ MessageChannel ??= require('internal/worker/io').MessageChannel;
+
+ const lock = new SharedArrayBuffer(SHARED_MEMORY_BYTE_LENGTH);
+ this.#lock = new Int32Array(lock);
+
+ this.#worker = new InternalWorker(loaderWorkerId, {
+ stderr: false,
+ stdin: false,
+ stdout: false,
+ trackUnmanagedFds: false,
+ workerData: {
+ lock,
+ },
+ });
+ this.#worker.unref(); // ! Allows the process to eventually exit.
+ this.#worker.on('exit', process.exit);
}
- if (dynamicInstantiate) {
- ArrayPrototypePush(obsoleteHooks, 'dynamicInstantiate');
+
+ waitForWorker() {
+ if (!this.#isReady) {
+ const { kIsOnline } = require('internal/worker');
+ if (!this.#worker[kIsOnline]) {
+ debug('wait for signal from worker');
+ AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0);
+ const response = this.#worker.receiveMessageSync();
+ if (response == null || response.message.status === 'exit') { return; }
+ const { preloadScripts } = this.#unwrapMessage(response);
+ this.#executePreloadScripts(preloadScripts);
+ }
+
+ this.#isReady = true;
+ }
+ }
+
+ /**
+ * Invoke a remote method asynchronously.
+ * @param {string} method Method to invoke
+ * @param {any[]} [transferList] Objects in `args` to be transferred
+ * @param {any[]} args Arguments to pass to `method`
+ * @returns {Promise}
+ */
+ async makeAsyncRequest(method, transferList, ...args) {
+ this.waitForWorker();
+
+ MessageChannel ??= require('internal/worker/io').MessageChannel;
+ const asyncCommChannel = new MessageChannel();
+
+ // Pass work to the worker.
+ debug('post async message to worker', { method, args, transferList });
+ const finalTransferList = [asyncCommChannel.port2];
+ if (transferList) {
+ ArrayPrototypePushApply(finalTransferList, transferList);
+ }
+ this.#worker.postMessage({
+ __proto__: null,
+ method, args,
+ port: asyncCommChannel.port2,
+ }, finalTransferList);
+
+ if (this.#numberOfPendingAsyncResponses++ === 0) {
+ // On the next lines, the main thread will await a response from the worker thread that might
+ // come AFTER the last task in the event loop has run its course and there would be nothing
+ // left keeping the thread alive (and once the main thread dies, the whole process stops).
+ // However we want to keep the process alive until the worker thread responds (or until the
+ // event loop of the worker thread is also empty), so we ref the worker until we get all the
+ // responses back.
+ this.#worker.ref();
+ }
+
+ let response;
+ do {
+ debug('wait for async response from worker', { method, args });
+ await AtomicsWaitAsync(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, this.#workerNotificationLastId).value;
+ this.#workerNotificationLastId = AtomicsLoad(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
+
+ response = receiveMessageOnPort(asyncCommChannel.port1);
+ } while (response == null);
+ debug('got async response from worker', { method, args }, this.#lock);
+
+ if (--this.#numberOfPendingAsyncResponses === 0) {
+ // We got all the responses from the worker, its job is done (until next time).
+ this.#worker.unref();
+ }
+
+ const body = this.#unwrapMessage(response);
+ asyncCommChannel.port1.close();
+ return body;
}
- if (getFormat) {
- ArrayPrototypePush(obsoleteHooks, 'getFormat');
+
+ /**
+ * Invoke a remote method synchronously.
+ * @param {string} method Method to invoke
+ * @param {any[]} [transferList] Objects in `args` to be transferred
+ * @param {any[]} args Arguments to pass to `method`
+ * @returns {any}
+ */
+ makeSyncRequest(method, transferList, ...args) {
+ this.waitForWorker();
+
+ // Pass work to the worker.
+ debug('post sync message to worker', { method, args, transferList });
+ this.#worker.postMessage({ __proto__: null, method, args }, transferList);
+
+ let response;
+ do {
+ debug('wait for sync response from worker', { method, args });
+ // Sleep until worker responds.
+ AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, this.#workerNotificationLastId);
+ this.#workerNotificationLastId = AtomicsLoad(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
+
+ response = this.#worker.receiveMessageSync();
+ } while (response == null);
+ debug('got sync response from worker', { method, args });
+ if (response.message.status === 'never-settle') {
+ process.exit(13);
+ } else if (response.message.status === 'exit') {
+ process.exit(response.message.body);
+ }
+ return this.#unwrapMessage(response);
}
- if (getSource) {
- ArrayPrototypePush(obsoleteHooks, 'getSource');
+
+ #unwrapMessage(response) {
+ if (response.message.status === 'never-settle') {
+ return new Promise(() => {});
+ }
+ const { status, body } = response.message;
+ if (status === 'error') {
+ if (body == null || typeof body !== 'object') { throw body; }
+ if (body.serializationFailed || body.serialized == null) {
+ throw ERR_WORKER_UNSERIALIZABLE_ERROR();
+ }
+
+ // eslint-disable-next-line no-restricted-syntax
+ throw deserializeError(body.serialized);
+ } else {
+ return body;
+ }
}
- if (transformSource) {
- ArrayPrototypePush(obsoleteHooks, 'transformSource');
+
+ #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
+
+ importMetaInitialize(meta, context, loader) {
+ this.#importMetaInitializer(meta, context, loader);
}
- if (obsoleteHooks.length) {
- process.emitWarning(
- `Obsolete loader hook(s) supplied and will be ignored: ${
- ArrayPrototypeJoin(obsoleteHooks, ', ')
- }`,
- 'DeprecationWarning',
- );
+ #executePreloadScripts(preloadScripts) {
+ for (let i = 0; i < preloadScripts.length; i++) {
+ const { code, port } = preloadScripts[i];
+ const { compileFunction } = require('vm');
+ const preloadInit = compileFunction(
+ code,
+ ['getBuiltin', 'port', 'setImportMetaCallback'],
+ {
+ filename: '',
+ },
+ );
+ let finished = false;
+ let replacedImportMetaInitializer = false;
+ let next = this.#importMetaInitializer;
+ const { BuiltinModule } = require('internal/bootstrap/realm');
+ // Calls the compiled preload source text gotten from the hook
+ // Since the parameters are named we use positional parameters
+ // see compileFunction above to cross reference the names
+ try {
+ FunctionPrototypeCall(
+ preloadInit,
+ globalThis,
+ // Param getBuiltin
+ (builtinName) => {
+ if (StringPrototypeStartsWith(builtinName, 'node:')) {
+ builtinName = StringPrototypeSlice(builtinName, 5);
+ } else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) {
+ throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName);
+ }
+ if (BuiltinModule.canBeRequiredByUsers(builtinName)) {
+ return require(builtinName);
+ }
+ throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName);
+ },
+ // Param port
+ port,
+ // setImportMetaCallback
+ (fn) => {
+ if (finished || typeof fn !== 'function') {
+ throw new ERR_INVALID_ARG_TYPE('fn', fn);
+ }
+ replacedImportMetaInitializer = true;
+ const parent = next;
+ next = (meta, context) => {
+ return fn(meta, context, parent);
+ };
+ },
+ );
+ } finally {
+ finished = true;
+ if (replacedImportMetaInitializer) {
+ this.#importMetaInitializer = next;
+ }
+ }
+ }
}
+}
+ObjectSetPrototypeOf(HooksProxy.prototype, null);
+
+/**
+ * A utility function to pluck the hooks from a user-defined loader.
+ * @param {import('./loader.js).ModuleExports} exports
+ * @returns {ExportedHooks}
+ */
+function pluckHooks({
+ globalPreload,
+ initialize,
+ resolve,
+ load,
+}) {
+ const acceptedHooks = { __proto__: null };
if (globalPreload) {
acceptedHooks.globalPreload = globalPreload;
@@ -575,6 +791,10 @@ function pluckHooks({
acceptedHooks.load = load;
}
+ if (initialize) {
+ acceptedHooks.initialize = initialize;
+ }
+
return acceptedHooks;
}
@@ -583,15 +803,14 @@ function pluckHooks({
* A utility function to iterate through a hook chain, track advancement in the
* chain, and generate and supply the `next` argument to the custom
* hook.
- * @param {KeyedHook[]} chain The whole hook chain.
+ * @param {Hook} current The (currently) first hook in the chain (this shifts
+ * on every call).
* @param {object} meta Properties that change as the current hook advances
* along the chain.
* @param {boolean} meta.chainFinished Whether the end of the chain has been
* reached AND invoked.
* @param {string} meta.hookErrIdentifier A user-facing identifier to help
* pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
- * @param {number} meta.hookIndex A non-negative integer tracking the current
- * position in the hook chain.
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
@@ -599,13 +818,14 @@ function pluckHooks({
* validation within MUST throw.
* @returns {function next(...hookArgs)} The next hook in the chain.
*/
-function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
+function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
// First, prepare the current
const { hookName } = meta;
const {
fn: hook,
url: hookFilePath,
- } = chain[meta.hookIndex];
+ next,
+ } = current;
// ex 'nextResolve'
const nextHookName = `next${
@@ -613,16 +833,9 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
StringPrototypeSlice(hookName, 1)
}`;
- // When hookIndex is 0, it's reached the default, which does not call next()
- // so feed it a noop that blows up if called, so the problem is obvious.
- const generatedHookIndex = meta.hookIndex;
let nextNextHook;
- if (meta.hookIndex > 0) {
- // Now, prepare the next: decrement the pointer so the next call to the
- // factory generates the next link in the chain.
- meta.hookIndex--;
-
- nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
+ if (next) {
+ nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput });
} else {
// eslint-disable-next-line func-name-matching
nextNextHook = function chainAdvancedTooFar() {
@@ -639,17 +852,16 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context);
- const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`;
+ const outputErrIdentifier = `${hookFilePath} '${hookName}' hook's ${nextHookName}()`;
// Set when next is actually called, not just generated.
- if (generatedHookIndex === 0) { meta.chainFinished = true; }
+ if (!next) { meta.chainFinished = true; }
if (context) { // `context` has already been validated, so no fancy check needed.
ObjectAssign(meta.context, context);
}
const output = await hook(arg0, meta.context, nextNextHook);
-
validateOutput(outputErrIdentifier, output);
if (output?.shortCircuit === true) { meta.shortCircuited = true; }
@@ -663,3 +875,4 @@ function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
exports.Hooks = Hooks;
+exports.HooksProxy = HooksProxy;
diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js
index fe5ba4a3cc1248..f55f60a5b7647a 100644
--- a/lib/internal/modules/esm/initialize_import_meta.js
+++ b/lib/internal/modules/esm/initialize_import_meta.js
@@ -1,39 +1,63 @@
'use strict';
const { getOptionValue } = require('internal/options');
-const experimentalImportMetaResolve =
- getOptionValue('--experimental-import-meta-resolve');
-const {
- PromisePrototypeThen,
- PromiseReject,
-} = primordials;
-const asyncESM = require('internal/process/esm_loader');
-
-function createImportMetaResolve(defaultParentUrl) {
- return async function resolve(specifier, parentUrl = defaultParentUrl) {
- return PromisePrototypeThen(
- asyncESM.esmLoader.resolve(specifier, parentUrl),
- ({ url }) => url,
- (error) => (
- error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ?
- error.url : PromiseReject(error)),
- );
+const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve');
+
+/**
+ * Generate a function to be used as import.meta.resolve for a particular module.
+ * @param {string} defaultParentURL The default base to use for resolution
+ * @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader
+ * @param {bool} allowParentURL Whether to permit parentURL second argument for contextual resolution
+ * @returns {(specifier: string) => string} Function to assign to import.meta.resolve
+ */
+function createImportMetaResolve(defaultParentURL, loader, allowParentURL) {
+ /**
+ * @param {string} specifier
+ * @param {URL['href']} [parentURL] When `--experimental-import-meta-resolve` is specified, a
+ * second argument can be provided.
+ */
+ return function resolve(specifier, parentURL = defaultParentURL) {
+ let url;
+
+ if (!allowParentURL) {
+ parentURL = defaultParentURL;
+ }
+
+ try {
+ ({ url } = loader.resolveSync(specifier, parentURL));
+ return url;
+ } catch (error) {
+ switch (error?.code) {
+ case 'ERR_UNSUPPORTED_DIR_IMPORT':
+ case 'ERR_MODULE_NOT_FOUND':
+ ({ url } = error);
+ if (url) {
+ return url;
+ }
+ }
+ throw error;
+ }
};
}
/**
+ * Create the `import.meta` object for a module.
* @param {object} meta
* @param {{url: string}} context
+ * @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader
+ * @returns {{url: string, resolve?: Function}}
*/
-function initializeImportMeta(meta, context) {
+function initializeImportMeta(meta, context, loader) {
const { url } = context;
// Alphabetical
- if (experimentalImportMetaResolve) {
- meta.resolve = createImportMetaResolve(url);
+ if (!loader || loader.allowImportMetaResolve) {
+ meta.resolve = createImportMetaResolve(url, loader, experimentalImportMetaResolve);
}
meta.url = url;
+
+ return meta;
}
module.exports = {
diff --git a/lib/internal/modules/esm/load.js b/lib/internal/modules/esm/load.js
index 29135cd08103f2..4b5ff362ed0c4a 100644
--- a/lib/internal/modules/esm/load.js
+++ b/lib/internal/modules/esm/load.js
@@ -8,7 +8,7 @@ const {
const { kEmptyObject } = require('internal/util');
const { defaultGetFormat } = require('internal/modules/esm/get_format');
-const { validateAssertions } = require('internal/modules/esm/assert');
+const { validateAttributes, emitImportAssertionWarning } = require('internal/modules/esm/assert');
const { getOptionValue } = require('internal/options');
// Do not eagerly grab .manifest, it may be in TDZ
@@ -78,19 +78,29 @@ async function getSource(url, context) {
*/
async function defaultLoad(url, context = kEmptyObject) {
let responseURL = url;
- const { importAssertions } = context;
let {
+ importAttributes,
format,
source,
} = context;
+ if (importAttributes == null && !('importAttributes' in context) && 'importAssertions' in context) {
+ emitImportAssertionWarning();
+ importAttributes = context.importAssertions;
+ // Alias `importAssertions` to `importAttributes`
+ context = {
+ ...context,
+ importAttributes,
+ };
+ }
+
const urlInstance = new URL(url);
throwIfUnsupportedURLScheme(urlInstance, experimentalNetworkImports);
format ??= await defaultGetFormat(urlInstance, context);
- validateAssertions(url, format, importAssertions);
+ validateAttributes(url, format, importAttributes);
if (
format === 'builtin' ||
diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js
index 125cebd19866d1..6e42f1a5db5a8a 100644
--- a/lib/internal/modules/esm/loader.js
+++ b/lib/internal/modules/esm/loader.js
@@ -4,53 +4,68 @@
require('internal/modules/cjs/loader');
const {
- Array,
- ArrayIsArray,
+ ArrayPrototypeJoin,
+ ArrayPrototypeMap,
+ ArrayPrototypeReduce,
FunctionPrototypeCall,
- ObjectCreate,
+ JSONStringify,
ObjectSetPrototypeOf,
- SafePromiseAllReturnArrayLike,
+ RegExpPrototypeSymbolReplace,
SafeWeakMap,
+ encodeURIComponent,
+ hardenRegExp,
} = primordials;
const {
ERR_UNKNOWN_MODULE_FORMAT,
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
-const { pathToFileURL } = require('internal/url');
+const { pathToFileURL, isURL } = require('internal/url');
const { emitExperimentalWarning } = require('internal/util');
-
const {
getDefaultConditions,
} = require('internal/modules/esm/utils');
+let defaultResolve, defaultLoad, importMetaInitializer;
+
+/**
+ * Lazy loads the module_map module and returns a new instance of ResolveCache.
+ * @returns {import('./module_map.js').ResolveCache')}
+ */
+function newResolveCache() {
+ const { ResolveCache } = require('internal/modules/esm/module_map');
+ return new ResolveCache();
+}
-function newModuleMap() {
- const ModuleMap = require('internal/modules/esm/module_map');
- return new ModuleMap();
+/**
+ * Generate a load cache (to store the final result of a load-chain for a particular module).
+ * @returns {import('./module_map.js').LoadCache')}
+ */
+function newLoadCache() {
+ const { LoadCache } = require('internal/modules/esm/module_map');
+ return new LoadCache();
}
+/**
+ * Lazy-load translators to avoid potentially unnecessary work at startup (ex if ESM is not used).
+ * @returns {import('./translators.js').Translators}
+ */
function getTranslators() {
const { translators } = require('internal/modules/esm/translators');
return translators;
}
/**
- * @typedef {object} ExportedHooks
- * @property {Function} globalPreload Global preload hook.
- * @property {Function} resolve Resolve hook.
- * @property {Function} load Load hook.
+ * @type {HooksProxy}
+ * Multiple loader instances exist for various, specific reasons (see code comments at site).
+ * In order to maintain consistency, we use a single worker (sandbox), which must sit apart of an
+ * individual loader instance.
*/
+let hooksProxy;
/**
* @typedef {Record} ModuleExports
*/
-/**
- * @typedef {object} KeyedExports
- * @property {ModuleExports} exports The contents of the module.
- * @property {URL['href']} url The URL of the module.
- */
-
/**
* @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat
*/
@@ -62,17 +77,11 @@ function getTranslators() {
let emittedSpecifierResolutionWarning = false;
/**
- * An ESMLoader instance is used as the main entry point for loading ES modules.
- * Currently, this is a singleton -- there is only one used for loading
- * the main module and everything in its dependency graph.
+ * This class covers the base machinery of module loading. To add custom
+ * behavior you can pass a customizations object and this object will be
+ * used to do the loading/resolving/registration process.
*/
-
-class ESMLoader {
- #hooks;
- #defaultResolve;
- #defaultLoad;
- #importMetaInitializer;
-
+class ModuleLoader {
/**
* The conditions for resolving packages if `--conditions` is not used.
*/
@@ -88,20 +97,40 @@ class ESMLoader {
*/
evalIndex = 0;
+ /**
+ * Registry of resolved specifiers
+ */
+ #resolveCache = newResolveCache();
+
/**
* Registry of loaded modules, akin to `require.cache`
*/
- moduleMap = newModuleMap();
+ loadCache = newLoadCache();
/**
* Methods which translate input code or other information into ES modules
*/
translators = getTranslators();
- constructor() {
- if (getOptionValue('--experimental-loader').length > 0) {
- emitExperimentalWarning('Custom ESM Loaders');
- }
+ /**
+ * Truthy to allow the use of `import.meta.resolve`. This is needed
+ * currently because the `Hooks` class does not have `resolveSync`
+ * implemented and `import.meta.resolve` requires it.
+ */
+ allowImportMetaResolve;
+
+ /**
+ * Customizations to pass requests to.
+ *
+ * Note that this value _MUST_ be set with `setCustomizations`
+ * because it needs to copy `customizations.allowImportMetaResolve`
+ * to this property and failure to do so will cause undefined
+ * behavior when invoking `import.meta.resolve`.
+ * @see {ModuleLoader.setCustomizations}
+ */
+ #customizations;
+
+ constructor(customizations) {
if (getOptionValue('--experimental-network-imports')) {
emitExperimentalWarning('Network Imports');
}
@@ -115,15 +144,64 @@ class ESMLoader {
);
emittedSpecifierResolutionWarning = true;
}
+ this.setCustomizations(customizations);
}
- addCustomLoaders(userLoaders) {
- const { Hooks } = require('internal/modules/esm/hooks');
- this.#hooks = new Hooks(userLoaders);
- }
-
- preload() {
- this.#hooks?.preload();
+ /**
+ * Change the currently activate customizations for this module
+ * loader to be the provided `customizations`.
+ *
+ * If present, this class customizes its core functionality to the
+ * `customizations` object, including registration, loading, and resolving.
+ * There are some responsibilities that this class _always_ takes
+ * care of, like validating outputs, so that the customizations object
+ * does not have to do so.
+ *
+ * The customizations object has the shape:
+ *
+ * ```ts
+ * interface LoadResult {
+ * format: ModuleFormat;
+ * source: ModuleSource;
+ * }
+ *
+ * interface ResolveResult {
+ * format: string;
+ * url: URL['href'];
+ * }
+ *
+ * interface Customizations {
+ * allowImportMetaResolve: boolean;
+ * load(url: string, context: object): Promise
+ * resolve(
+ * originalSpecifier:
+ * string, parentURL: string,
+ * importAttributes: Record
+ * ): Promise
+ * resolveSync(
+ * originalSpecifier:
+ * string, parentURL: string,
+ * importAttributes: Record
+ * ) ResolveResult;
+ * register(specifier: string, parentURL: string): any;
+ * forceLoadHooks(): void;
+ * }
+ * ```
+ *
+ * Note that this class _also_ implements the `Customizations`
+ * interface, as does `CustomizedModuleLoader` and `Hooks`.
+ *
+ * Calling this function alters how modules are loaded and should be
+ * invoked with care.
+ * @param {object} customizations
+ */
+ setCustomizations(customizations) {
+ this.#customizations = customizations;
+ if (customizations) {
+ this.allowImportMetaResolve = customizations.allowImportMetaResolve;
+ } else {
+ this.allowImportMetaResolve = true;
+ }
}
async eval(
@@ -135,8 +213,9 @@ class ESMLoader {
const { setCallbackForWrap } = require('internal/modules/esm/utils');
const module = new ModuleWrap(url, undefined, source, 0, 0);
setCallbackForWrap(module, {
- importModuleDynamically: (specifier, { url }, importAssertions) => {
- return this.import(specifier, url, importAssertions);
+ initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
+ importModuleDynamically: (specifier, { url }, importAttributes) => {
+ return this.import(specifier, url, importAttributes);
},
});
@@ -145,7 +224,7 @@ class ESMLoader {
const ModuleJob = require('internal/modules/esm/module_job');
const job = new ModuleJob(
this, url, undefined, evalInstance, false, false);
- this.moduleMap.set(url, undefined, job);
+ this.loadCache.set(url, undefined, job);
const { module } = await job.run();
return {
@@ -162,37 +241,27 @@ class ESMLoader {
* @param {string | undefined} parentURL The URL of the module importing this
* one, unless this is the Node.js entry
* point.
- * @param {Record} importAssertions Validations for the
+ * @param {Record} importAttributes Validations for the
* module import.
* @returns {Promise} The (possibly pending) module job
*/
- async getModuleJob(specifier, parentURL, importAssertions) {
- let importAssertionsForResolve;
-
- // We can skip cloning if there are no user-provided loaders because
- // the Node.js default resolve hook does not use import assertions.
- if (this.#hooks?.hasCustomResolveOrLoadHooks) {
- // This method of cloning only works so long as import assertions cannot contain objects as values,
- // which they currently cannot per spec.
- importAssertionsForResolve = {
- __proto__: null,
- ...importAssertions,
- };
- }
+ async getModuleJob(specifier, parentURL, importAttributes) {
+ const resolveResult = await this.resolve(specifier, parentURL, importAttributes);
+ return this.getJobFromResolveResult(resolveResult, parentURL, importAttributes);
+ }
- const resolveResult = await this.resolve(specifier, parentURL, importAssertionsForResolve);
+ getJobFromResolveResult(resolveResult, parentURL, importAttributes) {
const { url, format } = resolveResult;
- const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions;
-
- let job = this.moduleMap.get(url, resolvedImportAssertions.type);
+ const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes;
+ let job = this.loadCache.get(url, resolvedImportAttributes.type);
// CommonJS will set functions for lazy job evaluation.
if (typeof job === 'function') {
- this.moduleMap.set(url, undefined, job = job());
+ this.loadCache.set(url, undefined, job = job());
}
if (job === undefined) {
- job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format);
+ job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format);
}
return job;
@@ -201,7 +270,7 @@ class ESMLoader {
/**
* Create and cache an object representing a loaded module.
* @param {string} url The absolute URL that was resolved for this module
- * @param {Record} importAssertions Validations for the
+ * @param {Record} importAttributes Validations for the
* module import.
* @param {string} [parentURL] The absolute URL of the module importing this
* one, unless this is the Node.js entry point
@@ -209,7 +278,7 @@ class ESMLoader {
* `resolve` hook
* @returns {Promise} The (possibly pending) module job
*/
- #createModuleJob(url, importAssertions, parentURL, format) {
+ #createModuleJob(url, importAttributes, parentURL, format) {
const moduleProvider = async (url, isMain) => {
const {
format: finalFormat,
@@ -217,7 +286,7 @@ class ESMLoader {
source,
} = await this.load(url, {
format,
- importAssertions,
+ importAttributes,
});
const translator = getTranslators().get(finalFormat);
@@ -242,65 +311,44 @@ class ESMLoader {
const job = new ModuleJob(
this,
url,
- importAssertions,
+ importAttributes,
moduleProvider,
parentURL === undefined,
inspectBrk,
);
- this.moduleMap.set(url, importAssertions.type, job);
+ this.loadCache.set(url, importAttributes.type, job);
return job;
}
/**
* This method is usually called indirectly as part of the loading processes.
- * Internally, it is used directly to add loaders. Use directly with caution.
- *
- * This method must NOT be renamed: it functions as a dynamic import on a
- * loader module.
- * @param {string | string[]} specifiers Path(s) to the module.
+ * Use directly with caution.
+ * @param {string} specifier The first parameter of an `import()` expression.
* @param {string} parentURL Path of the parent importing the module.
- * @param {Record} importAssertions Validations for the
+ * @param {Record} importAttributes Validations for the
* module import.
- * @returns {Promise}
- * A collection of module export(s) or a list of collections of module
- * export(s).
+ * @returns {Promise}
*/
- async import(specifiers, parentURL, importAssertions) {
- // For loaders, `import` is passed multiple things to process, it returns a
- // list pairing the url and exports collected. This is especially useful for
- // error messaging, to identity from where an export came. But, in most
- // cases, only a single url is being "imported" (ex `import()`), so there is
- // only 1 possible url from which the exports were collected and it is
- // already known to the caller. Nesting that in a list would only ever
- // create redundant work for the caller, so it is later popped off the
- // internal list.
- const wasArr = ArrayIsArray(specifiers);
- if (!wasArr) { specifiers = [specifiers]; }
-
- const count = specifiers.length;
- const jobs = new Array(count);
-
- for (let i = 0; i < count; i++) {
- jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions)
- .then((job) => job.run())
- .then(({ module }) => module.getNamespace());
- }
-
- const namespaces = await SafePromiseAllReturnArrayLike(jobs);
-
- if (!wasArr) { return namespaces[0]; } // We can skip the pairing below
+ async import(specifier, parentURL, importAttributes) {
+ const moduleJob = await this.getModuleJob(specifier, parentURL, importAttributes);
+ const { module } = await moduleJob.run();
+ return module.getNamespace();
+ }
- for (let i = 0; i < count; i++) {
- namespaces[i] = {
- __proto__: null,
- url: specifiers[i],
- exports: namespaces[i],
- };
+ /**
+ * @see {@link CustomizedModuleLoader.register}
+ */
+ register(specifier, parentURL, data, transferList) {
+ if (!this.#customizations) {
+ // `CustomizedModuleLoader` is defined at the bottom of this file and
+ // available well before this line is ever invoked. This is here in
+ // order to preserve the git diff instead of moving the class.
+ // eslint-disable-next-line no-use-before-define
+ this.setCustomizations(new CustomizedModuleLoader());
}
-
- return namespaces;
+ return this.#customizations.register(`${specifier}`, `${parentURL}`, data, transferList);
}
/**
@@ -308,29 +356,51 @@ class ESMLoader {
* @param {string} originalSpecifier The specified URL path of the module to
* be resolved.
* @param {string} [parentURL] The URL path of the module's parent.
- * @param {ImportAssertions} importAssertions Assertions from the import
- * statement or expression.
- * @returns {Promise<{ format: string, url: URL['href'] }>}
+ * @param {ImportAttributes} importAttributes Attributes from the import
+ * statement or expression.
+ * @returns {{ format: string, url: URL['href'] }}
*/
- async resolve(
- originalSpecifier,
- parentURL,
- importAssertions = ObjectCreate(null),
- ) {
- if (this.#hooks) {
- return this.#hooks.resolve(originalSpecifier, parentURL, importAssertions);
+ resolve(originalSpecifier, parentURL, importAttributes) {
+ if (this.#customizations) {
+ return this.#customizations.resolve(originalSpecifier, parentURL, importAttributes);
}
- if (!this.#defaultResolve) {
- this.#defaultResolve = require('internal/modules/esm/resolve').defaultResolve;
+ const requestKey = this.#resolveCache.serializeKey(originalSpecifier, importAttributes);
+ const cachedResult = this.#resolveCache.get(requestKey, parentURL);
+ if (cachedResult != null) {
+ return cachedResult;
}
+ const result = this.defaultResolve(originalSpecifier, parentURL, importAttributes);
+ this.#resolveCache.set(requestKey, parentURL, result);
+ return result;
+ }
+
+ /**
+ * Just like `resolve` except synchronous. This is here specifically to support
+ * `import.meta.resolve` which must happen synchronously.
+ */
+ resolveSync(originalSpecifier, parentURL, importAttributes) {
+ if (this.#customizations) {
+ return this.#customizations.resolveSync(originalSpecifier, parentURL, importAttributes);
+ }
+ return this.defaultResolve(originalSpecifier, parentURL, importAttributes);
+ }
+
+ /**
+ * Our `defaultResolve` is synchronous and can be used in both
+ * `resolve` and `resolveSync`. This function is here just to avoid
+ * repeating the same code block twice in those functions.
+ */
+ defaultResolve(originalSpecifier, parentURL, importAttributes) {
+ defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;
+
const context = {
__proto__: null,
conditions: this.#defaultConditions,
- importAssertions,
+ importAttributes,
parentURL,
};
- return this.#defaultResolve(originalSpecifier, context);
+ return defaultResolve(originalSpecifier, context);
}
/**
@@ -340,36 +410,202 @@ class ESMLoader {
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
*/
async load(url, context) {
- let loadResult;
- if (this.#hooks) {
- loadResult = await this.#hooks.load(url, context);
- } else {
- if (!this.#defaultLoad) {
- this.#defaultLoad = require('internal/modules/esm/load').defaultLoad;
- }
- loadResult = await this.#defaultLoad(url, context);
- }
+ defaultLoad ??= require('internal/modules/esm/load').defaultLoad;
+ const result = this.#customizations ?
+ await this.#customizations.load(url, context) :
+ await defaultLoad(url, context);
+ this.validateLoadResult(url, result?.format);
+ return result;
+ }
- const { format } = loadResult;
+ validateLoadResult(url, format) {
if (format == null) {
require('internal/modules/esm/load').throwUnknownModuleFormat(url, format);
}
-
- return loadResult;
}
importMetaInitialize(meta, context) {
- if (this.#hooks) {
- this.#hooks.importMetaInitialize(meta, context);
- } else {
- if (!this.#importMetaInitializer) {
- this.#importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
+ if (this.#customizations) {
+ return this.#customizations.importMetaInitialize(meta, context, this);
+ }
+ importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
+ meta = importMetaInitializer(meta, context, this);
+ return meta;
+ }
+
+ /**
+ * No-op when no hooks have been supplied.
+ */
+ forceLoadHooks() {
+ this.#customizations?.forceLoadHooks();
+ }
+}
+ObjectSetPrototypeOf(ModuleLoader.prototype, null);
+
+class CustomizedModuleLoader {
+
+ allowImportMetaResolve = true;
+
+ /**
+ * Instantiate a module loader that uses user-provided custom loader hooks.
+ */
+ constructor() {
+ getHooksProxy();
+ }
+
+ /**
+ * Register some loader specifier.
+ * @param {string} originalSpecifier The specified URL path of the loader to
+ * be registered.
+ * @param {string} parentURL The parent URL from where the loader will be
+ * registered if using it package name as specifier
+ * @param {any} [data] Arbitrary data to be passed from the custom loader
+ * (user-land) to the worker.
+ * @param {any[]} [transferList] Objects in `data` that are changing ownership
+ * @returns {{ format: string, url: URL['href'] }}
+ */
+ register(originalSpecifier, parentURL, data, transferList) {
+ return hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data);
+ }
+
+ /**
+ * Resolve the location of the module.
+ * @param {string} originalSpecifier The specified URL path of the module to
+ * be resolved.
+ * @param {string} [parentURL] The URL path of the module's parent.
+ * @param {ImportAttributes} importAttributes Attributes from the import
+ * statement or expression.
+ * @returns {{ format: string, url: URL['href'] }}
+ */
+ resolve(originalSpecifier, parentURL, importAttributes) {
+ return hooksProxy.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes);
+ }
+
+ resolveSync(originalSpecifier, parentURL, importAttributes) {
+ // This happens only as a result of `import.meta.resolve` calls, which must be sync per spec.
+ return hooksProxy.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes);
+ }
+
+ /**
+ * Provide source that is understood by one of Node's translators.
+ * @param {URL['href']} url The URL/path of the module to be loaded
+ * @param {object} [context] Metadata about the module
+ * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
+ */
+ load(url, context) {
+ return hooksProxy.makeAsyncRequest('load', undefined, url, context);
+ }
+
+ importMetaInitialize(meta, context, loader) {
+ hooksProxy.importMetaInitialize(meta, context, loader);
+ }
+
+ forceLoadHooks() {
+ hooksProxy.waitForWorker();
+ }
+}
+
+let emittedLoaderFlagWarning = false;
+/**
+ * A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is
+ * only one used for loading the main module and everything in its dependency graph, though separate instances of this
+ * class might be instantiated as part of bootstrap for other purposes.
+ * @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them.
+ * @returns {ModuleLoader}
+ */
+function createModuleLoader(useCustomLoadersIfPresent = true) {
+ let customizations = null;
+ if (useCustomLoadersIfPresent &&
+ // Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader;
+ // doing so would cause an infinite loop.
+ !require('internal/modules/esm/utils').isLoaderWorker()) {
+ const userLoaderPaths = getOptionValue('--experimental-loader');
+ if (userLoaderPaths.length > 0) {
+ if (!emittedLoaderFlagWarning) {
+ const readableURIEncode = (string) => ArrayPrototypeReduce(
+ [
+ [/'/g, '%27'], // We need to URL-encode the single quote as it's the delimiter for the --import flag.
+ [/%22/g, '"'], // We can decode the double quotes to improve readability.
+ [/%2F/ig, '/'], // We can decode the slashes to improve readability.
+ ],
+ (str, { 0: regex, 1: replacement }) => RegExpPrototypeSymbolReplace(hardenRegExp(regex), str, replacement),
+ encodeURIComponent(string));
+ process.emitWarning(
+ '`--experimental-loader` may be removed in the future; instead use `register()`:\n' +
+ `--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; ${ArrayPrototypeJoin(
+ ArrayPrototypeMap(userLoaderPaths, (loader) => `register(${readableURIEncode(JSONStringify(loader))}, pathToFileURL("./"))`),
+ '; ',
+ )};'`,
+ 'ExperimentalWarning',
+ );
+ emittedLoaderFlagWarning = true;
}
- this.#importMetaInitializer(meta, context);
+ customizations = new CustomizedModuleLoader();
}
}
+
+ return new ModuleLoader(customizations);
}
-ObjectSetPrototypeOf(ESMLoader.prototype, null);
-exports.ESMLoader = ESMLoader;
+/**
+ * Get the HooksProxy instance. If it is not defined, then create a new one.
+ * @returns {HooksProxy}
+ */
+function getHooksProxy() {
+ if (!hooksProxy) {
+ const { HooksProxy } = require('internal/modules/esm/hooks');
+ hooksProxy = new HooksProxy();
+ }
+
+ return hooksProxy;
+}
+
+/**
+ * Register a single loader programmatically.
+ * @param {string|import('url').URL} specifier
+ * @param {string|import('url').URL} [parentURL] Base to use when resolving `specifier`; optional if
+ * `specifier` is absolute. Same as `options.parentUrl`, just inline
+ * @param {object} [options] Additional options to apply, described below.
+ * @param {string|import('url').URL} [options.parentURL] Base to use when resolving `specifier`
+ * @param {any} [options.data] Arbitrary data passed to the loader's `initialize` hook
+ * @param {any[]} [options.transferList] Objects in `data` that are changing ownership
+ * @returns {void} We want to reserve the return value for potential future extension of the API.
+ * @example
+ * ```js
+ * register('./myLoader.js');
+ * register('ts-node/esm', { parentURL: import.meta.url });
+ * register('./myLoader.js', { parentURL: import.meta.url });
+ * register('ts-node/esm', import.meta.url);
+ * register('./myLoader.js', import.meta.url);
+ * register(new URL('./myLoader.js', import.meta.url));
+ * register('./myLoader.js', {
+ * parentURL: import.meta.url,
+ * data: { banana: 'tasty' },
+ * });
+ * register('./myLoader.js', {
+ * parentURL: import.meta.url,
+ * data: someArrayBuffer,
+ * transferList: [someArrayBuffer],
+ * });
+ * ```
+ */
+function register(specifier, parentURL = undefined, options) {
+ const moduleLoader = require('internal/process/esm_loader').esmLoader;
+ if (parentURL != null && typeof parentURL === 'object' && !isURL(parentURL)) {
+ options = parentURL;
+ parentURL = options.parentURL;
+ }
+ moduleLoader.register(
+ specifier,
+ parentURL ?? 'data:',
+ options?.data,
+ options?.transferList,
+ );
+}
+
+module.exports = {
+ createModuleLoader,
+ getHooksProxy,
+ register,
+};
diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js
index f1fe73eec6edb6..3bc327a39c7e67 100644
--- a/lib/internal/modules/esm/module_job.js
+++ b/lib/internal/modules/esm/module_job.js
@@ -5,7 +5,6 @@ const {
ArrayPrototypePush,
ArrayPrototypeSome,
FunctionPrototype,
- ObjectCreate,
ObjectSetPrototypeOf,
PromiseResolve,
PromisePrototypeThen,
@@ -22,7 +21,7 @@ const {
const { ModuleWrap } = internalBinding('module_wrap');
-const { decorateErrorStack } = require('internal/util');
+const { decorateErrorStack, kEmptyObject } = require('internal/util');
const {
getSourceMapsEnabled,
} = require('internal/source_map/source_map_cache');
@@ -51,10 +50,10 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
class ModuleJob {
// `loader` is the Loader instance used for loading dependencies.
// `moduleProvider` is a function
- constructor(loader, url, importAssertions = ObjectCreate(null),
+ constructor(loader, url, importAttributes = { __proto__: null },
moduleProvider, isMain, inspectBrk) {
this.loader = loader;
- this.importAssertions = importAssertions;
+ this.importAttributes = importAttributes;
this.isMain = isMain;
this.inspectBrk = inspectBrk;
@@ -73,15 +72,15 @@ class ModuleJob {
// so that circular dependencies can't cause a deadlock by two of
// these `link` callbacks depending on each other.
const dependencyJobs = [];
- const promises = this.module.link(async (specifier, assertions) => {
- const jobPromise = this.loader.getModuleJob(specifier, url, assertions);
- ArrayPrototypePush(dependencyJobs, jobPromise);
- const job = await jobPromise;
+ const promises = this.module.link(async (specifier, attributes) => {
+ const job = await this.loader.getModuleJob(specifier, url, attributes);
+ ArrayPrototypePush(dependencyJobs, job);
return job.modulePromise;
});
- if (promises !== undefined)
+ if (promises !== undefined) {
await SafePromiseAllReturnVoid(promises);
+ }
return SafePromiseAllReturnArrayLike(dependencyJobs);
};
@@ -142,12 +141,14 @@ class ModuleJob {
/module '(.*)' does not provide an export named '(.+)'/,
e.message);
const { url: childFileURL } = await this.loader.resolve(
- childSpecifier, parentFileUrl,
+ childSpecifier,
+ parentFileUrl,
+ kEmptyObject,
);
let format;
try {
// This might throw for non-CommonJS modules because we aren't passing
- // in the import assertions and some formats require them; but we only
+ // in the import attributes and some formats require them; but we only
// care about CommonJS for the purposes of this error message.
({ format } =
await this.loader.load(childFileURL));
diff --git a/lib/internal/modules/esm/module_map.js b/lib/internal/modules/esm/module_map.js
index 7280f052fef59a..595e251048b900 100644
--- a/lib/internal/modules/esm/module_map.js
+++ b/lib/internal/modules/esm/module_map.js
@@ -1,18 +1,93 @@
'use strict';
-const { kImplicitAssertType } = require('internal/modules/esm/assert');
const {
+ ArrayPrototypeJoin,
+ ArrayPrototypeMap,
+ ArrayPrototypeSort,
+ JSONStringify,
ObjectCreate,
+ ObjectKeys,
SafeMap,
} = primordials;
+const { kImplicitAssertType } = require('internal/modules/esm/assert');
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { validateString } = require('internal/validators');
-// Tracks the state of the loader-level module cache
-class ModuleMap extends SafeMap {
+/**
+ * Cache the results of the `resolve` step of the module resolution and loading process.
+ * Future resolutions of the same input (specifier, parent URL and import attributes)
+ * must return the same result if the first attempt was successful, per
+ * https://tc39.es/ecma262/#sec-HostLoadImportedModule.
+ * This cache is *not* used when custom loaders are registered.
+ */
+class ResolveCache extends SafeMap {
+ constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
+
+ /**
+ * Generates the internal serialized cache key and returns it along the actual cache object.
+ *
+ * It is exposed to allow more efficient read and overwrite a cache entry.
+ * @param {string} specifier
+ * @param {Record} importAttributes
+ * @returns {string}
+ */
+ serializeKey(specifier, importAttributes) {
+ // To serialize the ModuleRequest (specifier + list of import attributes),
+ // we need to sort the attributes by key, then stringifying,
+ // so that different import statements with the same attributes are always treated
+ // as identical.
+ const keys = ObjectKeys(importAttributes);
+
+ if (keys.length === 0) {
+ return specifier + '::';
+ }
+
+ return specifier + '::' + ArrayPrototypeJoin(
+ ArrayPrototypeMap(
+ ArrayPrototypeSort(keys),
+ (key) => JSONStringify(key) + JSONStringify(importAttributes[key])),
+ ',');
+ }
+
+ #getModuleCachedImports(parentURL) {
+ let internalCache = super.get(parentURL);
+ if (internalCache == null) {
+ super.set(parentURL, internalCache = { __proto__: null });
+ }
+ return internalCache;
+ }
+
+ /**
+ * @param {string} serializedKey
+ * @param {string} parentURL
+ * @returns {import('./loader').ModuleExports | Promise}
+ */
+ get(serializedKey, parentURL) {
+ return this.#getModuleCachedImports(parentURL)[serializedKey];
+ }
+
+ /**
+ * @param {string} serializedKey
+ * @param {string} parentURL
+ * @param {{ format: string, url: URL['href'] }} result
+ */
+ set(serializedKey, parentURL, result) {
+ this.#getModuleCachedImports(parentURL)[serializedKey] = result;
+ return this;
+ }
+
+ has(serializedKey, parentURL) {
+ return serializedKey in this.#getModuleCachedImports(parentURL);
+ }
+}
+
+/**
+ * Cache the results of the `load` step of the module resolution and loading process.
+ */
+class LoadCache extends SafeMap {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
get(url, type = kImplicitAssertType) {
validateString(url, 'url');
@@ -30,7 +105,7 @@ class ModuleMap extends SafeMap {
}
debug(`Storing ${url} (${
type === kImplicitAssertType ? 'implicit type' : type
- }) in ModuleMap`);
+ }) in ModuleLoadMap`);
const cachedJobsForUrl = super.get(url) ?? ObjectCreate(null);
cachedJobsForUrl[type] = job;
return super.set(url, cachedJobsForUrl);
@@ -41,4 +116,8 @@ class ModuleMap extends SafeMap {
return super.get(url)?.[type] !== undefined;
}
}
-module.exports = ModuleMap;
+
+module.exports = {
+ LoadCache,
+ ResolveCache,
+};
diff --git a/lib/internal/modules/esm/package_config.js b/lib/internal/modules/esm/package_config.js
index dc3c37f6042333..5da47764c9de2c 100644
--- a/lib/internal/modules/esm/package_config.js
+++ b/lib/internal/modules/esm/package_config.js
@@ -1,106 +1,29 @@
'use strict';
const {
- JSONParse,
- ObjectPrototypeHasOwnProperty,
- SafeMap,
StringPrototypeEndsWith,
} = primordials;
const { URL, fileURLToPath } = require('internal/url');
-const {
- ERR_INVALID_PACKAGE_CONFIG,
-} = require('internal/errors').codes;
-
-const { filterOwnProperties } = require('internal/util');
-
+const packageJsonReader = require('internal/modules/package_json_reader');
/**
- * @typedef {string | string[] | Record} Exports
- * @typedef {'module' | 'commonjs'} PackageType
- * @typedef {{
- * pjsonPath: string,
- * exports?: ExportConfig,
- * name?: string,
- * main?: string,
- * type?: PackageType,
- * }} PackageConfig
+ * @typedef {object} PackageConfig
+ * @property {string} pjsonPath - The path to the package.json file.
+ * @property {boolean} exists - Whether the package.json file exists.
+ * @property {'none' | 'commonjs' | 'module'} type - The type of the package.
+ * @property {string} [name] - The name of the package.
+ * @property {string} [main] - The main entry point of the package.
+ * @property {PackageTarget} [exports] - The exports configuration of the package.
+ * @property {Record>} [imports] - The imports configuration of the package.
*/
-
-/** @type {Map} */
-const packageJSONCache = new SafeMap();
-
-
/**
- * @param {string} path
- * @param {string} specifier
- * @param {string | URL | undefined} base
- * @returns {PackageConfig}
+ * @typedef {string | string[] | Record>} PackageTarget
*/
-function getPackageConfig(path, specifier, base) {
- const existing = packageJSONCache.get(path);
- if (existing !== undefined) {
- return existing;
- }
- const packageJsonReader = require('internal/modules/package_json_reader');
- const source = packageJsonReader.read(path).string;
- if (source === undefined) {
- const packageConfig = {
- pjsonPath: path,
- exists: false,
- main: undefined,
- name: undefined,
- type: 'none',
- exports: undefined,
- imports: undefined,
- };
- packageJSONCache.set(path, packageConfig);
- return packageConfig;
- }
-
- let packageJSON;
- try {
- packageJSON = JSONParse(source);
- } catch (error) {
- throw new ERR_INVALID_PACKAGE_CONFIG(
- path,
- (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier),
- error.message,
- );
- }
-
- let { imports, main, name, type } = filterOwnProperties(packageJSON, ['imports', 'main', 'name', 'type']);
- const exports = ObjectPrototypeHasOwnProperty(packageJSON, 'exports') ? packageJSON.exports : undefined;
- if (typeof imports !== 'object' || imports === null) {
- imports = undefined;
- }
- if (typeof main !== 'string') {
- main = undefined;
- }
- if (typeof name !== 'string') {
- name = undefined;
- }
- // Ignore unknown types for forwards compatibility
- if (type !== 'module' && type !== 'commonjs') {
- type = 'none';
- }
-
- const packageConfig = {
- pjsonPath: path,
- exists: true,
- main,
- name,
- type,
- exports,
- imports,
- };
- packageJSONCache.set(path, packageConfig);
- return packageConfig;
-}
-
/**
- * @param {URL | string} resolved
- * @returns {PackageConfig}
+ * Returns the package configuration for the given resolved URL.
+ * @param {URL | string} resolved - The resolved URL.
+ * @returns {PackageConfig} - The package configuration.
*/
function getPackageScopeConfig(resolved) {
let packageJSONUrl = new URL('./package.json', resolved);
@@ -109,7 +32,11 @@ function getPackageScopeConfig(resolved) {
if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) {
break;
}
- const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), resolved);
+ const packageConfig = packageJsonReader.read(fileURLToPath(packageJSONUrl), {
+ __proto__: null,
+ specifier: resolved,
+ isESM: true,
+ });
if (packageConfig.exists) {
return packageConfig;
}
@@ -124,7 +51,8 @@ function getPackageScopeConfig(resolved) {
}
}
const packageJSONPath = fileURLToPath(packageJSONUrl);
- const packageConfig = {
+ return {
+ __proto__: null,
pjsonPath: packageJSONPath,
exists: false,
main: undefined,
@@ -133,12 +61,9 @@ function getPackageScopeConfig(resolved) {
exports: undefined,
imports: undefined,
};
- packageJSONCache.set(packageJSONPath, packageConfig);
- return packageConfig;
}
module.exports = {
- getPackageConfig,
getPackageScopeConfig,
};
diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js
index 7c6dd5cd59c4bf..96dd20a86b076e 100644
--- a/lib/internal/modules/esm/resolve.js
+++ b/lib/internal/modules/esm/resolve.js
@@ -4,7 +4,6 @@ const {
ArrayIsArray,
ArrayPrototypeJoin,
ArrayPrototypeShift,
- JSONParse,
JSONStringify,
ObjectGetOwnPropertyNames,
ObjectPrototypeHasOwnProperty,
@@ -37,10 +36,13 @@ const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const experimentalNetworkImports =
getOptionValue('--experimental-network-imports');
-const typeFlag = getOptionValue('--input-type');
-const { URL, pathToFileURL, fileURLToPath, toPathIfFileURL } = require('internal/url');
+const inputTypeFlag = getOptionValue('--input-type');
+const { URL, pathToFileURL, fileURLToPath, isURL, toPathIfFileURL } = require('internal/url');
+const { getCWDURL } = require('internal/util');
+const { canParse: URLCanParse } = internalBinding('url');
const {
ERR_INPUT_TYPE_NOT_ALLOWED,
+ ERR_INVALID_ARG_TYPE,
ERR_INVALID_MODULE_SPECIFIER,
ERR_INVALID_PACKAGE_CONFIG,
ERR_INVALID_PACKAGE_TARGET,
@@ -53,9 +55,9 @@ const {
} = require('internal/errors').codes;
const { Module: CJSModule } = require('internal/modules/cjs/loader');
-const packageJsonReader = require('internal/modules/package_json_reader');
-const { getPackageConfig, getPackageScopeConfig } = require('internal/modules/esm/package_config');
+const { getPackageScopeConfig } = require('internal/modules/esm/package_config');
const { getConditionsSet } = require('internal/modules/esm/utils');
+const packageJsonReader = require('internal/modules/package_json_reader');
const { internalModuleStat } = internalBinding('fs');
/**
@@ -65,10 +67,16 @@ const { internalModuleStat } = internalBinding('fs');
const emittedPackageWarnings = new SafeSet();
+/**
+ * Emits a deprecation warning for the use of a deprecated trailing slash pattern mapping in the "exports" field
+ * module resolution of a package.
+ * @param {string} match - The deprecated trailing slash pattern mapping.
+ * @param {string} pjsonUrl - The URL of the package.json file.
+ * @param {string} base - The URL of the module that imported the package.
+ */
function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) {
const pjsonPath = fileURLToPath(pjsonUrl);
- if (emittedPackageWarnings.has(pjsonPath + '|' + match))
- return;
+ if (emittedPackageWarnings.has(pjsonPath + '|' + match)) { return; }
emittedPackageWarnings.add(pjsonPath + '|' + match);
process.emitWarning(
`Use of deprecated trailing slash pattern mapping "${match}" in the ` +
@@ -82,6 +90,16 @@ function emitTrailingSlashPatternDeprecation(match, pjsonUrl, base) {
const doubleSlashRegEx = /[/\\][/\\]/;
+/**
+ * Emits a deprecation warning for invalid segment in module resolution.
+ * @param {string} target - The target module.
+ * @param {string} request - The requested module.
+ * @param {string} match - The matched module.
+ * @param {string} pjsonUrl - The package.json URL.
+ * @param {boolean} internal - Whether the module is in the "imports" or "exports" field.
+ * @param {string} base - The base URL.
+ * @param {boolean} isTarget - Whether the target is a module.
+ */
function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, internal, base, isTarget) {
if (!pendingDeprecation) { return; }
const pjsonPath = fileURLToPath(pjsonUrl);
@@ -98,16 +116,16 @@ function emitInvalidSegmentDeprecation(target, request, match, pjsonUrl, interna
}
/**
- * @param {URL} url
- * @param {URL} packageJSONUrl
- * @param {string | URL | undefined} base
- * @param {string} [main]
- * @returns {void}
+ * Emits a deprecation warning if the given URL is a module and
+ * the package.json file does not define a "main" or "exports" field.
+ * @param {URL} url - The URL of the module being resolved.
+ * @param {URL} packageJSONUrl - The URL of the package.json file for the module.
+ * @param {string | URL} [base] - The base URL for the module being resolved.
+ * @param {string} [main] - The "main" field from the package.json file.
*/
function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) {
const format = defaultGetFormatWithoutErrors(url);
- if (format !== 'module')
- return;
+ if (format !== 'module') { return; }
const path = fileURLToPath(url);
const pkgPath = fileURLToPath(new URL('.', packageJSONUrl));
const basePath = fileURLToPath(base);
@@ -159,22 +177,23 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) {
let guess;
if (packageConfig.main !== undefined) {
// Note: fs check redundances will be handled by Descriptor cache here.
- if (fileExists(guess = new URL(`./${packageConfig.main}`,
- packageJSONUrl))) {
+ if (fileExists(guess = new URL(`./${packageConfig.main}`, packageJSONUrl))) {
return guess;
- } else if (fileExists(guess = new URL(`./${packageConfig.main}.js`,
- packageJSONUrl)));
- else if (fileExists(guess = new URL(`./${packageConfig.main}.json`,
- packageJSONUrl)));
- else if (fileExists(guess = new URL(`./${packageConfig.main}.node`,
- packageJSONUrl)));
- else if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`,
- packageJSONUrl)));
- else if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`,
- packageJSONUrl)));
- else if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`,
- packageJSONUrl)));
- else guess = undefined;
+ } else if (fileExists(guess = new URL(`./${packageConfig.main}.js`, packageJSONUrl))) {
+ // Handled below.
+ } else if (fileExists(guess = new URL(`./${packageConfig.main}.json`, packageJSONUrl))) {
+ // Handled below.
+ } else if (fileExists(guess = new URL(`./${packageConfig.main}.node`, packageJSONUrl))) {
+ // Handled below.
+ } else if (fileExists(guess = new URL(`./${packageConfig.main}/index.js`, packageJSONUrl))) {
+ // Handled below.
+ } else if (fileExists(guess = new URL(`./${packageConfig.main}/index.json`, packageJSONUrl))) {
+ // Handled below.
+ } else if (fileExists(guess = new URL(`./${packageConfig.main}/index.node`, packageJSONUrl))) {
+ // Handled below.
+ } else {
+ guess = undefined;
+ }
if (guess) {
emitLegacyIndexDeprecation(guess, packageJSONUrl, base,
packageConfig.main);
@@ -182,11 +201,15 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) {
}
// Fallthrough.
}
- if (fileExists(guess = new URL('./index.js', packageJSONUrl)));
- // So fs.
- else if (fileExists(guess = new URL('./index.json', packageJSONUrl)));
- else if (fileExists(guess = new URL('./index.node', packageJSONUrl)));
- else guess = undefined;
+ if (fileExists(guess = new URL('./index.js', packageJSONUrl))) {
+ // Handled below.
+ } else if (fileExists(guess = new URL('./index.json', packageJSONUrl))) {
+ // Handled below.
+ } else if (fileExists(guess = new URL('./index.node', packageJSONUrl))) {
+ // Handled below.
+ } else {
+ guess = undefined;
+ }
if (guess) {
emitLegacyIndexDeprecation(guess, packageJSONUrl, base, packageConfig.main);
return guess;
@@ -201,7 +224,7 @@ function legacyMainResolve(packageJSONUrl, packageConfig, base) {
* @returns {URL | undefined}
*/
function resolveExtensionsWithTryExactName(search) {
- if (fileExists(search)) return search;
+ if (fileExists(search)) { return search; }
return resolveExtensions(search);
}
@@ -215,7 +238,7 @@ function resolveExtensions(search) {
for (let i = 0; i < extensions.length; i++) {
const extension = extensions[i];
const guess = new URL(`${search.pathname}${extension}`, search);
- if (fileExists(guess)) return guess;
+ if (fileExists(guess)) { return guess; }
}
return undefined;
}
@@ -229,8 +252,8 @@ function resolveDirectoryEntry(search) {
const pkgJsonPath = resolve(dirPath, 'package.json');
if (fileExists(pkgJsonPath)) {
const pkgJson = packageJsonReader.read(pkgJsonPath);
- if (pkgJson.containsKeys) {
- const { main } = JSONParse(pkgJson.string);
+ if (pkgJson.exists) {
+ const { main } = pkgJson;
if (main != null) {
const mainUrl = pathToFileURL(resolve(dirPath, main));
return resolveExtensionsWithTryExactName(mainUrl);
@@ -242,18 +265,33 @@ function resolveDirectoryEntry(search) {
const encodedSepRegEx = /%2F|%5C/i;
/**
- * @param {URL} resolved
- * @param {string | URL | undefined} base
- * @param {boolean} preserveSymlinks
- * @returns {URL | undefined}
+ * Finalizes the resolution of a module specifier by checking if the resolved pathname contains encoded "/" or "\\"
+ * characters, checking if the resolved pathname is a directory or file, and resolving any symlinks if necessary.
+ * @param {URL} resolved - The resolved URL object.
+ * @param {string | URL | undefined} base - The base URL object.
+ * @param {boolean} preserveSymlinks - Whether to preserve symlinks or not.
+ * @returns {URL} - The finalized URL object.
+ * @throws {ERR_INVALID_MODULE_SPECIFIER} - If the resolved pathname contains encoded "/" or "\\" characters.
+ * @throws {ERR_UNSUPPORTED_DIR_IMPORT} - If the resolved pathname is a directory.
+ * @throws {ERR_MODULE_NOT_FOUND} - If the resolved pathname is not a file.
*/
function finalizeResolution(resolved, base, preserveSymlinks) {
- if (RegExpPrototypeExec(encodedSepRegEx, resolved.pathname) !== null)
+ if (RegExpPrototypeExec(encodedSepRegEx, resolved.pathname) !== null) {
throw new ERR_INVALID_MODULE_SPECIFIER(
resolved.pathname, 'must not include encoded "/" or "\\" characters',
fileURLToPath(base));
+ }
+
+ let path;
+ try {
+ path = fileURLToPath(resolved);
+ } catch (err) {
+ const { setOwnProperty } = require('internal/util');
+ setOwnProperty(err, 'input', `${resolved}`);
+ setOwnProperty(err, 'module', `${base}`);
+ throw err;
+ }
- let path = fileURLToPath(resolved);
if (getOptionValue('--experimental-specifier-resolution') === 'node') {
let file = resolveExtensionsWithTryExactName(resolved);
@@ -262,7 +300,7 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
file = StringPrototypeEndsWith(path, '/') ?
(resolveDirectoryEntry(resolved) || resolved) : resolveDirectoryEntry(new URL(`${resolved}/`));
- if (file === resolved) return file;
+ if (file === resolved) { return file; }
if (file === undefined) {
throw new ERR_MODULE_NOT_FOUND(
@@ -280,16 +318,14 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
// Check for stats.isDirectory()
if (stats === 1) {
- const err = new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base));
- err.url = String(resolved);
- throw err;
+ throw new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base), String(resolved));
} else if (stats !== 0) {
// Check for !stats.isFile()
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
process.send({ 'watch:require': [path || resolved.pathname] });
}
throw new ERR_MODULE_NOT_FOUND(
- path || resolved.pathname, base && fileURLToPath(base), 'module');
+ path || resolved.pathname, base && fileURLToPath(base), resolved);
}
if (!preserveSymlinks) {
@@ -307,9 +343,11 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
}
/**
- * @param {string} specifier
- * @param {URL} packageJSONUrl
- * @param {string | URL | undefined} base
+ * Returns an error object indicating that the specified import is not defined.
+ * @param {string} specifier - The import specifier that is not defined.
+ * @param {URL} packageJSONUrl - The URL of the package.json file, or null if not available.
+ * @param {string | URL | undefined} base - The base URL to use for resolving relative URLs.
+ * @returns {ERR_PACKAGE_IMPORT_NOT_DEFINED} - The error object.
*/
function importNotDefined(specifier, packageJSONUrl, base) {
return new ERR_PACKAGE_IMPORT_NOT_DEFINED(
@@ -318,9 +356,11 @@ function importNotDefined(specifier, packageJSONUrl, base) {
}
/**
- * @param {string} subpath
- * @param {URL} packageJSONUrl
- * @param {string | URL | undefined} base
+ * Returns an error object indicating that the specified subpath was not exported by the package.
+ * @param {string} subpath - The subpath that was not exported.
+ * @param {URL} packageJSONUrl - The URL of the package.json file.
+ * @param {string | URL | undefined} [base] - The base URL to use for resolving the subpath.
+ * @returns {ERR_PACKAGE_PATH_NOT_EXPORTED} - The error object.
*/
function exportsNotFound(subpath, packageJSONUrl, base) {
return new ERR_PACKAGE_PATH_NOT_EXPORTED(
@@ -329,12 +369,13 @@ function exportsNotFound(subpath, packageJSONUrl, base) {
}
/**
- *
- * @param {string} request
- * @param {string} match
- * @param {URL} packageJSONUrl
- * @param {boolean} internal
- * @param {string | URL | undefined} base
+ * Throws an error indicating that the given request is not a valid subpath match for the specified pattern.
+ * @param {string} request - The request that failed to match the pattern.
+ * @param {string} match - The pattern that the request was compared against.
+ * @param {URL} packageJSONUrl - The URL of the package.json file being resolved.
+ * @param {boolean} internal - Whether the resolution is for an "imports" or "exports" field in package.json.
+ * @param {string | URL | undefined} base - The base URL for the resolution.
+ * @throws {ERR_INVALID_MODULE_SPECIFIER} When the request is not a valid match for the pattern.
*/
function throwInvalidSubpath(request, match, packageJSONUrl, internal, base) {
const reason = `request is not a valid match in pattern "${match}" for the "${
@@ -344,6 +385,15 @@ function throwInvalidSubpath(request, match, packageJSONUrl, internal, base) {
base && fileURLToPath(base));
}
+/**
+ * Creates an error object for an invalid package target.
+ * @param {string} subpath - The subpath.
+ * @param {import('internal/modules/esm/package_config.js').PackageTarget} target - The target.
+ * @param {URL} packageJSONUrl - The URL of the package.json file.
+ * @param {boolean} internal - Whether the package is internal.
+ * @param {string | URL | undefined} base - The base URL.
+ * @returns {ERR_INVALID_PACKAGE_TARGET} - The error object.
+ */
function invalidPackageTarget(
subpath, target, packageJSONUrl, internal, base) {
if (typeof target === 'object' && target !== null) {
@@ -362,17 +412,19 @@ const invalidPackageNameRegEx = /^\.|%|\\/;
const patternRegEx = /\*/g;
/**
- *
- * @param {string} target
- * @param {*} subpath
- * @param {*} match
- * @param {*} packageJSONUrl
- * @param {*} base
- * @param {*} pattern
- * @param {*} internal
- * @param {*} isPathMap
- * @param {*} conditions
- * @returns {URL}
+ * Resolves the package target string to a URL object.
+ * @param {string} target - The target string to resolve.
+ * @param {string} subpath - The subpath to append to the resolved URL.
+ * @param {RegExpMatchArray} match - The matched string array from the import statement.
+ * @param {string} packageJSONUrl - The URL of the package.json file.
+ * @param {string} base - The base URL to resolve the target against.
+ * @param {RegExp} pattern - The pattern to replace in the target string.
+ * @param {boolean} internal - Whether the target is internal to the package.
+ * @param {boolean} isPathMap - Whether the target is a path map.
+ * @param {string[]} conditions - The import conditions.
+ * @returns {URL} - The resolved URL object.
+ * @throws {ERR_INVALID_PACKAGE_TARGET} - If the target is invalid.
+ * @throws {ERR_INVALID_SUBPATH} - If the subpath is invalid.
*/
function resolvePackageTargetString(
target,
@@ -386,20 +438,15 @@ function resolvePackageTargetString(
conditions,
) {
- if (subpath !== '' && !pattern && target[target.length - 1] !== '/')
+ if (subpath !== '' && !pattern && target[target.length - 1] !== '/') {
throw invalidPackageTarget(match, target, packageJSONUrl, internal, base);
+ }
if (!StringPrototypeStartsWith(target, './')) {
if (internal && !StringPrototypeStartsWith(target, '../') &&
!StringPrototypeStartsWith(target, '/')) {
- let isURL = false;
- try {
- new URL(target);
- isURL = true;
- } catch {
- // Continue regardless of error.
- }
- if (!isURL) {
+ // No need to convert target to string, since it's already presumed to be
+ if (!URLCanParse(target)) {
const exportTarget = pattern ?
RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) :
target + subpath;
@@ -430,10 +477,11 @@ function resolvePackageTargetString(
const resolvedPath = resolved.pathname;
const packagePath = new URL('.', packageJSONUrl).pathname;
- if (!StringPrototypeStartsWith(resolvedPath, packagePath))
+ if (!StringPrototypeStartsWith(resolvedPath, packagePath)) {
throw invalidPackageTarget(match, target, packageJSONUrl, internal, base);
+ }
- if (subpath === '') return resolved;
+ if (subpath === '') { return resolved; }
if (RegExpPrototypeExec(invalidSegmentRegEx, subpath) !== null) {
const request = pattern ? StringPrototypeReplace(match, '*', () => subpath) : match + subpath;
@@ -459,27 +507,28 @@ function resolvePackageTargetString(
}
/**
- * @param {string} key
- * @returns {boolean}
+ * Checks if the given key is a valid array index.
+ * @param {string} key - The key to check.
+ * @returns {boolean} - Returns `true` if the key is a valid array index, else `false`.
*/
function isArrayIndex(key) {
const keyNum = +key;
- if (`${keyNum}` !== key) return false;
+ if (`${keyNum}` !== key) { return false; }
return keyNum >= 0 && keyNum < 0xFFFF_FFFF;
}
/**
- *
- * @param {*} packageJSONUrl
- * @param {string|[string]} target
- * @param {*} subpath
- * @param {*} packageSubpath
- * @param {*} base
- * @param {*} pattern
- * @param {*} internal
- * @param {*} isPathMap
- * @param {*} conditions
- * @returns {URL|null}
+ * Resolves the target of a package based on the provided parameters.
+ * @param {string} packageJSONUrl - The URL of the package.json file.
+ * @param {import('internal/modules/esm/package_config.js').PackageTarget} target - The target to resolve.
+ * @param {string} subpath - The subpath to resolve.
+ * @param {string} packageSubpath - The subpath of the package to resolve.
+ * @param {string} base - The base path to resolve.
+ * @param {RegExp} pattern - The pattern to match.
+ * @param {boolean} internal - Whether the package is internal.
+ * @param {boolean} isPathMap - Whether the package is a path map.
+ * @param {Set} conditions - The conditions to match.
+ * @returns {URL | null | undefined} - The resolved target, or null if not found, or undefined if not resolvable.
*/
function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
base, pattern, internal, isPathMap, conditions) {
@@ -516,8 +565,9 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
}
return resolveResult;
}
- if (lastException === undefined || lastException === null)
+ if (lastException === undefined || lastException === null) {
return lastException;
+ }
throw lastException;
} else if (typeof target === 'object' && target !== null) {
const keys = ObjectGetOwnPropertyNames(target);
@@ -536,8 +586,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
const resolveResult = resolvePackageTarget(
packageJSONUrl, conditionalTarget, subpath, packageSubpath, base,
pattern, internal, isPathMap, conditions);
- if (resolveResult === undefined)
- continue;
+ if (resolveResult === undefined) { continue; }
return resolveResult;
}
}
@@ -550,15 +599,14 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
}
/**
- *
- * @param {import('internal/modules/esm/package_config.js').Exports} exports
- * @param {URL} packageJSONUrl
- * @param {string | URL | undefined} base
- * @returns {boolean}
+ * Is the given exports object using the shorthand syntax?
+ * @param {import('internal/modules/esm/package_config.js').PackageConfig['exports']} exports
+ * @param {URL} packageJSONUrl The URL of the package.json file.
+ * @param {string | URL | undefined} base The base URL.
*/
function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {
- if (typeof exports === 'string' || ArrayIsArray(exports)) return true;
- if (typeof exports !== 'object' || exports === null) return false;
+ if (typeof exports === 'string' || ArrayIsArray(exports)) { return true; }
+ if (typeof exports !== 'object' || exports === null) { return false; }
const keys = ObjectGetOwnPropertyNames(exports);
let isConditionalSugar = false;
@@ -580,18 +628,20 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {
}
/**
- * @param {URL} packageJSONUrl
- * @param {string} packageSubpath
- * @param {PackageConfig} packageConfig
- * @param {string | URL | undefined} base
- * @param {Set} conditions
- * @returns {URL}
+ * Resolves the exports of a package.
+ * @param {URL} packageJSONUrl - The URL of the package.json file.
+ * @param {string} packageSubpath - The subpath of the package to resolve.
+ * @param {import('internal/modules/esm/package_config.js').PackageConfig} packageConfig - The package metadata.
+ * @param {string | URL | undefined} base - The base path to resolve from.
+ * @param {Set} conditions - An array of conditions to match.
+ * @returns {URL} - The resolved package target.
*/
function packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions) {
let exports = packageConfig.exports;
- if (isConditionalExportsMainSugar(exports, packageJSONUrl, base))
+ if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
exports = { '.': exports };
+ }
if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) &&
!StringPrototypeIncludes(packageSubpath, '*') &&
@@ -624,9 +674,10 @@ function packageExportsResolve(
// throwInvalidSubpath(packageSubpath)
//
// To match "imports" and the spec.
- if (StringPrototypeEndsWith(packageSubpath, '/'))
+ if (StringPrototypeEndsWith(packageSubpath, '/')) {
emitTrailingSlashPatternDeprecation(packageSubpath, packageJSONUrl,
base);
+ }
const patternTrailer = StringPrototypeSlice(key, patternIndex + 1);
if (packageSubpath.length >= key.length &&
StringPrototypeEndsWith(packageSubpath, patternTrailer) &&
@@ -662,25 +713,35 @@ function packageExportsResolve(
throw exportsNotFound(packageSubpath, packageJSONUrl, base);
}
+/**
+ * Compares two strings that may contain a wildcard character ('*') and returns a value indicating their order.
+ * @param {string} a - The first string to compare.
+ * @param {string} b - The second string to compare.
+ * @returns {number} - A negative number if `a` should come before `b`, a positive number if `a` should come after `b`,
+ * or 0 if they are equal.
+ */
function patternKeyCompare(a, b) {
const aPatternIndex = StringPrototypeIndexOf(a, '*');
const bPatternIndex = StringPrototypeIndexOf(b, '*');
const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1;
const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1;
- if (baseLenA > baseLenB) return -1;
- if (baseLenB > baseLenA) return 1;
- if (aPatternIndex === -1) return 1;
- if (bPatternIndex === -1) return -1;
- if (a.length > b.length) return -1;
- if (b.length > a.length) return 1;
+ if (baseLenA > baseLenB) { return -1; }
+ if (baseLenB > baseLenA) { return 1; }
+ if (aPatternIndex === -1) { return 1; }
+ if (bPatternIndex === -1) { return -1; }
+ if (a.length > b.length) { return -1; }
+ if (b.length > a.length) { return 1; }
return 0;
}
/**
- * @param {string} name
- * @param {string | URL | undefined} base
- * @param {Set} conditions
- * @returns {URL}
+ * Resolves the given import name for a package.
+ * @param {string} name - The name of the import to resolve.
+ * @param {string | URL | undefined} base - The base URL to resolve the import from.
+ * @param {Set} conditions - An object containing the import conditions.
+ * @throws {ERR_INVALID_MODULE_SPECIFIER} If the import name is not valid.
+ * @throws {ERR_PACKAGE_IMPORT_NOT_DEFINED} If the import name cannot be resolved.
+ * @returns {URL} The resolved import URL.
*/
function packageImportsResolve(name, base, conditions) {
if (name === '#' || StringPrototypeStartsWith(name, '#/') ||
@@ -743,8 +804,8 @@ function packageImportsResolve(name, base, conditions) {
}
/**
- * @param {URL} url
- * @returns {import('internal/modules/esm/package_config.js').PackageType}
+ * Returns the package type for a given URL.
+ * @param {URL} url - The URL to get the package type for.
*/
function getPackageType(url) {
const packageConfig = getPackageScopeConfig(url);
@@ -752,9 +813,9 @@ function getPackageType(url) {
}
/**
- * @param {string} specifier
- * @param {string | URL | undefined} base
- * @returns {{ packageName: string, packageSubpath: string, isScoped: boolean }}
+ * Parse a package name from a specifier.
+ * @param {string} specifier - The import specifier.
+ * @param {string | URL | undefined} base - The parent URL.
*/
function parsePackageName(specifier, base) {
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
@@ -775,8 +836,9 @@ function parsePackageName(specifier, base) {
// Package name cannot have leading . and cannot have percent-encoding or
// \\ separators.
- if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null)
+ if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null) {
validPackageName = false;
+ }
if (!validPackageName) {
throw new ERR_INVALID_MODULE_SPECIFIER(
@@ -790,10 +852,11 @@ function parsePackageName(specifier, base) {
}
/**
- * @param {string} specifier
- * @param {string | URL | undefined} base
- * @param {Set} conditions
- * @returns {resolved: URL, format? : string}
+ * Resolves a package specifier to a URL.
+ * @param {string} specifier - The package specifier to resolve.
+ * @param {string | URL | undefined} base - The base URL to use for resolution.
+ * @param {Set} conditions - An object containing the conditions for resolution.
+ * @returns {URL} - The resolved URL.
*/
function packageResolve(specifier, base, conditions) {
if (BuiltinModule.canBeRequiredWithoutScheme(specifier)) {
@@ -807,8 +870,7 @@ function packageResolve(specifier, base, conditions) {
const packageConfig = getPackageScopeConfig(base);
if (packageConfig.exists) {
const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
- if (packageConfig.name === packageName &&
- packageConfig.exports !== undefined && packageConfig.exports !== null) {
+ if (packageConfig.exports != null && packageConfig.name === packageName) {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
@@ -832,8 +894,8 @@ function packageResolve(specifier, base, conditions) {
}
// Package match.
- const packageConfig = getPackageConfig(packageJSONPath, specifier, base);
- if (packageConfig.exports !== undefined && packageConfig.exports !== null) {
+ const packageConfig = packageJsonReader.read(packageJSONPath, { __proto__: null, specifier, base, isESM: true });
+ if (packageConfig.exports != null) {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
@@ -851,39 +913,47 @@ function packageResolve(specifier, base, conditions) {
// eslint can't handle the above code.
// eslint-disable-next-line no-unreachable
- throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base));
+ throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
}
/**
- * @param {string} specifier
- * @returns {boolean}
+ * Checks if a specifier is a bare specifier.
+ * @param {string} specifier - The specifier to check.
*/
function isBareSpecifier(specifier) {
return specifier[0] && specifier[0] !== '/' && specifier[0] !== '.';
}
+/**
+ * Determines whether a specifier is a relative path.
+ * @param {string} specifier - The specifier to check.
+ */
function isRelativeSpecifier(specifier) {
if (specifier[0] === '.') {
- if (specifier.length === 1 || specifier[1] === '/') return true;
+ if (specifier.length === 1 || specifier[1] === '/') { return true; }
if (specifier[1] === '.') {
- if (specifier.length === 2 || specifier[2] === '/') return true;
+ if (specifier.length === 2 || specifier[2] === '/') { return true; }
}
}
return false;
}
+/**
+ * Determines whether a specifier should be treated as a relative or absolute path.
+ * @param {string} specifier - The specifier to check.
+ */
function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
- if (specifier === '') return false;
- if (specifier[0] === '/') return true;
+ if (specifier === '') { return false; }
+ if (specifier[0] === '/') { return true; }
return isRelativeSpecifier(specifier);
}
/**
- * @param {string} specifier
- * @param {string | URL | undefined} base
- * @param {Set} conditions
- * @param {boolean} preserveSymlinks
- * @returns {url: URL, format?: string}
+ * Resolves a module specifier to a URL.
+ * @param {string} specifier - The module specifier to resolve.
+ * @param {string | URL | undefined} base - The base URL to resolve against.
+ * @param {Set} conditions - An object containing environment conditions.
+ * @param {boolean} preserveSymlinks - Whether to preserve symlinks in the resolved URL.
*/
function moduleResolve(specifier, base, conditions, preserveSymlinks) {
const isRemote = base.protocol === 'http:' ||
@@ -911,10 +981,9 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) {
}
/**
- * Try to resolve an import as a CommonJS module
- * @param {string} specifier
- * @param {string} parentURL
- * @returns {boolean|string}
+ * Try to resolve an import as a CommonJS module.
+ * @param {string} specifier - The specifier to resolve.
+ * @param {string} parentURL - The base URL.
*/
function resolveAsCommonJS(specifier, parentURL) {
try {
@@ -956,7 +1025,14 @@ function resolveAsCommonJS(specifier, parentURL) {
}
}
-// TODO(@JakobJingleheimer): de-dupe `specifier` & `parsed`
+/**
+ * Throw an error if an import is not allowed.
+ * TODO(@JakobJingleheimer): de-dupe `specifier` & `parsed`
+ * @param {string} specifier - The import specifier.
+ * @param {URL} parsed - The parsed URL of the import specifier.
+ * @param {URL} parsedParentURL - The parsed URL of the parent module.
+ * @throws {ERR_NETWORK_IMPORT_DISALLOWED} - If the import is disallowed.
+ */
function checkIfDisallowedImport(specifier, parsed, parsedParentURL) {
if (parsedParentURL) {
// Avoid accessing the `protocol` property due to the lazy getters.
@@ -1000,9 +1076,31 @@ function checkIfDisallowedImport(specifier, parsed, parsedParentURL) {
}
}
+/**
+ * Validate user-input in `context` supplied by a custom loader.
+ * @param {string | URL | undefined} parentURL - The parent URL.
+ */
+function throwIfInvalidParentURL(parentURL) {
+ if (parentURL === undefined) {
+ return; // Main entry point, so no parent
+ }
+ if (typeof parentURL !== 'string' && !isURL(parentURL)) {
+ throw new ERR_INVALID_ARG_TYPE('parentURL', ['string', 'URL'], parentURL);
+ }
+}
-async function defaultResolve(specifier, context = {}) {
+/**
+ * Resolves the given specifier using the provided context, which includes the parent URL and conditions.
+ * Throws an error if the parent URL is invalid or if the resolution is disallowed by the policy manifest.
+ * Otherwise, attempts to resolve the specifier and returns the resulting URL and format.
+ * @param {string} specifier - The specifier to resolve.
+ * @param {object} [context={}] - The context object containing the parent URL and conditions.
+ * @param {string} [context.parentURL] - The URL of the parent module.
+ * @param {string[]} [context.conditions] - The conditions for resolving the specifier.
+ */
+function defaultResolve(specifier, context = {}) {
let { parentURL, conditions } = context;
+ throwIfInvalidParentURL(parentURL);
if (parentURL && policy?.manifest) {
const redirects = policy.manifest.getDependencyMapper(parentURL);
if (redirects) {
@@ -1070,15 +1168,15 @@ async function defaultResolve(specifier, context = {}) {
parsedParentURL,
);
- if (maybeReturn) return maybeReturn;
+ if (maybeReturn) { return maybeReturn; }
// This must come after checkIfDisallowedImport
- if (parsed && parsed.protocol === 'node:') return { url: specifier };
+ if (parsed && parsed.protocol === 'node:') { return { __proto__: null, url: specifier }; }
const isMain = parentURL === undefined;
if (isMain) {
- parentURL = pathToFileURL(`${process.cwd()}/`).href;
+ parentURL = getCWDURL().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
@@ -1086,7 +1184,7 @@ async function defaultResolve(specifier, context = {}) {
// 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();
+ if (inputTypeFlag) { throw new ERR_INPUT_TYPE_NOT_ALLOWED(); }
}
conditions = getConditionsSet(conditions);
@@ -1106,17 +1204,7 @@ async function defaultResolve(specifier, context = {}) {
if (StringPrototypeStartsWith(specifier, 'file://')) {
specifier = fileURLToPath(specifier);
}
- const found = resolveAsCommonJS(specifier, parentURL);
- if (found) {
- // Modify the stack and message string to include the hint
- const lines = StringPrototypeSplit(error.stack, '\n');
- const hint = `Did you mean to import ${found}?`;
- error.stack =
- ArrayPrototypeShift(lines) + '\n' +
- hint + '\n' +
- ArrayPrototypeJoin(lines, '\n');
- error.message += `\n${hint}`;
- }
+ decorateErrorWithCommonJSHints(error, specifier, parentURL);
}
throw error;
}
@@ -1129,13 +1217,35 @@ async function defaultResolve(specifier, context = {}) {
};
}
+/**
+ * Decorates the given error with a hint for CommonJS modules.
+ * @param {Error} error - The error to decorate.
+ * @param {string} specifier - The specifier that was attempted to be imported.
+ * @param {string} parentURL - The URL of the parent module.
+ */
+function decorateErrorWithCommonJSHints(error, specifier, parentURL) {
+ const found = resolveAsCommonJS(specifier, parentURL);
+ if (found) {
+ // Modify the stack and message string to include the hint
+ const lines = StringPrototypeSplit(error.stack, '\n');
+ const hint = `Did you mean to import ${found}?`;
+ error.stack =
+ ArrayPrototypeShift(lines) + '\n' +
+ hint + '\n' +
+ ArrayPrototypeJoin(lines, '\n');
+ error.message += `\n${hint}`;
+ }
+}
+
module.exports = {
+ decorateErrorWithCommonJSHints,
defaultResolve,
encodedSepRegEx,
getPackageScopeConfig,
getPackageType,
packageExportsResolve,
packageImportsResolve,
+ throwIfInvalidParentURL,
};
// cycle
@@ -1145,11 +1255,11 @@ const {
if (policy) {
const $defaultResolve = defaultResolve;
- module.exports.defaultResolve = async function defaultResolve(
+ module.exports.defaultResolve = function defaultResolve(
specifier,
context,
) {
- const ret = await $defaultResolve(specifier, context);
+ const ret = $defaultResolve(specifier, context);
// This is a preflight check to avoid data exfiltration by query params etc.
policy.manifest.mightAllow(ret.url, () =>
new ERR_MANIFEST_DEPENDENCY_MISSING(
diff --git a/lib/internal/modules/esm/shared_constants.js b/lib/internal/modules/esm/shared_constants.js
new file mode 100644
index 00000000000000..4200bc87367d14
--- /dev/null
+++ b/lib/internal/modules/esm/shared_constants.js
@@ -0,0 +1,25 @@
+// This file contains the definition for the constant values that must be
+// available to both the main thread and the loader thread.
+
+'use strict';
+
+/*
+The shared memory area is divided in 1 32-bit long section. It has to be 32-bit long as
+`Atomics.notify` only works with `Int32Array` objects.
+
+--32-bits--
+ ^
+ |
+ |
+WORKER_TO_MAIN_THREAD_NOTIFICATION
+
+WORKER_TO_MAIN_THREAD_NOTIFICATION is only used to send notifications, its value is going to
+increase every time the worker sends a notification to the main thread.
+
+*/
+
+module.exports = {
+ WORKER_TO_MAIN_THREAD_NOTIFICATION: 0,
+
+ SHARED_MEMORY_BYTE_LENGTH: 1 * 4,
+};
diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js
index 267d89f1d44730..39af99c6c3b5b7 100644
--- a/lib/internal/modules/esm/translators.js
+++ b/lib/internal/modules/esm/translators.js
@@ -11,6 +11,7 @@ const {
SafeArrayIterator,
SafeMap,
SafeSet,
+ StringPrototypeIncludes,
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeStartsWith,
@@ -18,9 +19,13 @@ const {
globalThis: { WebAssembly },
} = primordials;
+/** @type {import('internal/util/types')} */
let _TYPES = null;
+/**
+ * Lazily loads and returns the internal/util/types module.
+ */
function lazyTypes() {
- if (_TYPES !== null) return _TYPES;
+ if (_TYPES !== null) { return _TYPES; }
return _TYPES = require('internal/util/types');
}
@@ -50,7 +55,13 @@ const { ModuleWrap } = moduleWrap;
const asyncESM = require('internal/process/esm_loader');
const { emitWarningSync } = require('internal/process/warning');
+/** @type {import('deps/cjs-module-lexer/lexer.js').parse} */
let cjsParse;
+/**
+ * Initializes the CommonJS module lexer parser.
+ * If WebAssembly is available, it uses the optimized version from the dist folder.
+ * Otherwise, it falls back to the JavaScript version from the lexer folder.
+ */
async function initCJSParse() {
if (typeof WebAssembly === 'undefined') {
cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
@@ -71,6 +82,14 @@ exports.translators = translators;
exports.enrichCJSError = enrichCJSError;
let DECODER = null;
+/**
+ * Asserts that the given body is a buffer source (either a string, array buffer, or typed array).
+ * Throws an error if the body is not a buffer source.
+ * @param {string | ArrayBufferView | ArrayBuffer} body - The body to check.
+ * @param {boolean} allowString - Whether or not to allow a string as a valid buffer source.
+ * @param {string} hookName - The name of the hook being called.
+ * @throws {ERR_INVALID_RETURN_PROPERTY_VALUE} If the body is not a buffer source.
+ */
function assertBufferSource(body, allowString, hookName) {
if (allowString && typeof body === 'string') {
return;
@@ -87,14 +106,23 @@ function assertBufferSource(body, allowString, hookName) {
);
}
+/**
+ * Converts a buffer or buffer-like object to a string.
+ * @param {string | ArrayBuffer | ArrayBufferView} body - The buffer or buffer-like object to convert to a string.
+ * @returns {string} The resulting string.
+ */
function stringify(body) {
- if (typeof body === 'string') return body;
+ if (typeof body === 'string') { return body; }
assertBufferSource(body, false, 'transformSource');
const { TextDecoder } = require('internal/encoding');
DECODER = DECODER === null ? new TextDecoder() : DECODER;
return DECODER.decode(body);
}
+/**
+ * Converts a URL to a file path if the URL protocol is 'file:'.
+ * @param {string} url - The URL to convert.
+ */
function errPath(url) {
const parsed = new URL(url);
if (parsed.protocol === 'file:') {
@@ -103,8 +131,16 @@ function errPath(url) {
return url;
}
-async function importModuleDynamically(specifier, { url }, assertions) {
- return asyncESM.esmLoader.import(specifier, url, assertions);
+/**
+ * Dynamically imports a module using the ESM loader.
+ * @param {string} specifier - The module specifier to import.
+ * @param {object} options - An object containing options for the import.
+ * @param {string} options.url - The URL of the module requesting the import.
+ * @param {Record} [attributes] - An object containing attributes for the import.
+ * @returns {Promise} The imported module.
+ */
+async function importModuleDynamically(specifier, { url }, attributes) {
+ return asyncESM.esmLoader.import(specifier, url, attributes);
}
// Strategy for loading a standard JavaScript module.
@@ -123,6 +159,7 @@ translators.set('module', async function moduleStrategy(url, source, isMain) {
});
/**
+ * Provide a more informative error for CommonJS imports.
* @param {Error | any} err
* @param {string} [content] Content of the file, if known.
* @param {string} [filename] Useful only if `content` is unknown.
@@ -148,7 +185,7 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
const filename = fileURLToPath(new URL(url));
- if (!cjsParse) await initCJSParse();
+ if (!cjsParse) { await initCJSParse(); }
const { module, exportNames } = cjsPreparseModuleExports(filename);
const namesWithDefault = exportNames.has('default') ?
[...exportNames] : ['default', ...exportNames];
@@ -171,8 +208,9 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
for (const exportName of exportNames) {
if (!ObjectPrototypeHasOwnProperty(exports, exportName) ||
- exportName === 'default')
+ exportName === 'default') {
continue;
+ }
// We might trigger a getter -> dont fail.
let value;
try {
@@ -186,12 +224,17 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
});
});
+/**
+ * Pre-parses a CommonJS module's exports and re-exports.
+ * @param {string} filename - The filename of the module.
+ */
function cjsPreparseModuleExports(filename) {
let module = CJSModule._cache[filename];
if (module) {
const cached = cjsParseCache.get(module);
- if (cached)
+ if (cached) {
return { module, exportNames: cached.exportNames };
+ }
}
const loaded = Boolean(module);
if (!loaded) {
@@ -236,8 +279,9 @@ function cjsPreparseModuleExports(filename) {
if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) &&
isAbsolute(resolved)) {
const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved);
- for (const name of reexportNames)
+ for (const name of reexportNames) {
exportNames.add(name);
+ }
}
});
@@ -265,9 +309,12 @@ translators.set('json', async function jsonStrategy(url, source) {
debug(`Loading JSONModule ${url}`);
const pathname = StringPrototypeStartsWith(url, 'file:') ?
fileURLToPath(url) : null;
+ const shouldCheckAndPopulateCJSModuleCache =
+ // We want to involve the CJS loader cache only for `file:` URL with no search query and no hash.
+ pathname && !StringPrototypeIncludes(url, '?') && !StringPrototypeIncludes(url, '#');
let modulePath;
let module;
- if (pathname) {
+ if (shouldCheckAndPopulateCJSModuleCache) {
modulePath = isWindows ?
StringPrototypeReplaceAll(pathname, '/', '\\') : pathname;
module = CJSModule._cache[modulePath];
@@ -279,7 +326,7 @@ translators.set('json', async function jsonStrategy(url, source) {
}
}
source = stringify(source);
- if (pathname) {
+ if (shouldCheckAndPopulateCJSModuleCache) {
// 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
// export, we have to check again if the module already exists or not.
@@ -305,7 +352,7 @@ translators.set('json', async function jsonStrategy(url, source) {
err.message = errPath(url) + ': ' + err.message;
throw err;
}
- if (pathname) {
+ if (shouldCheckAndPopulateCJSModuleCache) {
CJSModule._cache[modulePath] = module;
}
return new ModuleWrap(url, undefined, ['default'], function() {
@@ -341,7 +388,8 @@ translators.set('wasm', async function(url, source) {
'internal/modules/esm/create_dynamic_module');
return createDynamicModule(imports, exports, url, (reflect) => {
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
- for (const expt of ObjectKeys(exports))
+ for (const expt of ObjectKeys(exports)) {
reflect.exports[expt].set(exports[expt]);
+ }
}).module;
});
diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js
index bf3edc86518b4c..df729ef96c7172 100644
--- a/lib/internal/modules/esm/utils.js
+++ b/lib/internal/modules/esm/utils.js
@@ -1,4 +1,5 @@
'use strict';
+
const {
ArrayIsArray,
SafeSet,
@@ -10,9 +11,12 @@ const {
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
ERR_INVALID_ARG_VALUE,
} = require('internal/errors').codes;
-
const { getOptionValue } = require('internal/options');
-
+const {
+ loadPreloadModules,
+ initializeFrozenIntrinsics,
+} = require('internal/process/pre_execution');
+const { getCWDURL } = require('internal/util');
const {
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback,
@@ -28,18 +32,28 @@ function setCallbackForWrap(wrap, data) {
}
let defaultConditions;
+/**
+ * Returns the default conditions for ES module loading.
+ */
function getDefaultConditions() {
assert(defaultConditions !== undefined);
return defaultConditions;
}
+/** @type {Set} */
let defaultConditionsSet;
+/**
+ * Returns the default conditions for ES module loading, as a Set.
+ */
function getDefaultConditionsSet() {
assert(defaultConditionsSet !== undefined);
return defaultConditionsSet;
}
-// This function is called during pre-execution, before any user code is run.
+/**
+ * Initializes the default conditions for ESM module loading.
+ * This function is called during pre-execution, before any user code is run.
+ */
function initializeDefaultConditions() {
const userConditions = getOptionValue('--conditions');
const noAddons = getOptionValue('--no-addons');
@@ -69,27 +83,48 @@ function getConditionsSet(conditions) {
return getDefaultConditionsSet();
}
+/**
+ * Defines the `import.meta` object for a given module.
+ * @param {object} wrap - Reference to the module.
+ * @param {Record} meta - The import.meta object to initialize.
+ */
function initializeImportMetaObject(wrap, meta) {
if (callbackMap.has(wrap)) {
const { initializeImportMeta } = callbackMap.get(wrap);
if (initializeImportMeta !== undefined) {
- initializeImportMeta(meta, getModuleFromWrap(wrap) || wrap);
+ meta = initializeImportMeta(meta, getModuleFromWrap(wrap) || wrap);
}
}
}
-async function importModuleDynamicallyCallback(wrap, specifier, assertions) {
+/**
+ * Asynchronously imports a module dynamically using a callback function. The native callback.
+ * @param {object} wrap - Reference to the module.
+ * @param {string} specifier - The module specifier string.
+ * @param {Record} attributes - The import attributes object.
+ * @returns {Promise} - The imported module object.
+ * @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
+ */
+async function importModuleDynamicallyCallback(wrap, specifier, attributes) {
if (callbackMap.has(wrap)) {
const { importModuleDynamically } = callbackMap.get(wrap);
if (importModuleDynamically !== undefined) {
return importModuleDynamically(
- specifier, getModuleFromWrap(wrap) || wrap, assertions);
+ specifier, getModuleFromWrap(wrap) || wrap, attributes);
}
}
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
}
-function initializeESM() {
+let _isLoaderWorker = false;
+/**
+ * Initializes handling of ES modules.
+ * This is configured during pre-execution. Specifically it's set to true for
+ * the loader worker in internal/main/worker_thread.js.
+ * @param {boolean} [isLoaderWorker=false] - A boolean indicating whether the loader is a worker or not.
+ */
+function initializeESM(isLoaderWorker = false) {
+ _isLoaderWorker = isLoaderWorker;
initializeDefaultConditions();
// Setup per-isolate callbacks that locate data or callbacks that we keep
// track of for different ESM modules.
@@ -97,9 +132,55 @@ function initializeESM() {
setImportModuleDynamicallyCallback(importModuleDynamicallyCallback);
}
+/**
+ * Determine whether the current process is a loader worker.
+ * @returns {boolean} Whether the current process is a loader worker.
+ */
+function isLoaderWorker() {
+ return _isLoaderWorker;
+}
+
+/**
+ * Register module customization hooks.
+ */
+async function initializeHooks() {
+ const customLoaderURLs = getOptionValue('--experimental-loader');
+
+ const { Hooks } = require('internal/modules/esm/hooks');
+ const esmLoader = require('internal/process/esm_loader').esmLoader;
+
+ const hooks = new Hooks();
+ esmLoader.setCustomizations(hooks);
+
+ // We need the loader customizations to be set _before_ we start invoking
+ // `--require`, otherwise loops can happen because a `--require` script
+ // might call `register(...)` before we've installed ourselves. These
+ // global values are magically set in `setupUserModules` just for us and
+ // we call them in the correct order.
+ // N.B. This block appears here specifically in order to ensure that
+ // `--require` calls occur before `--loader` ones do.
+ loadPreloadModules();
+ initializeFrozenIntrinsics();
+
+ const parentURL = getCWDURL().href;
+ for (let i = 0; i < customLoaderURLs.length; i++) {
+ await hooks.register(
+ customLoaderURLs[i],
+ parentURL,
+ );
+ }
+
+ const preloadScripts = hooks.initializeGlobalPreload();
+
+ return { __proto__: null, hooks, preloadScripts };
+}
+
module.exports = {
setCallbackForWrap,
initializeESM,
+ initializeHooks,
getDefaultConditions,
getConditionsSet,
+ loaderWorkerId: 'internal/modules/esm/worker',
+ isLoaderWorker,
};
diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js
new file mode 100644
index 00000000000000..7b295973abe7a4
--- /dev/null
+++ b/lib/internal/modules/esm/worker.js
@@ -0,0 +1,261 @@
+'use strict';
+
+const {
+ DataViewPrototypeGetBuffer,
+ Int32Array,
+ PromisePrototypeThen,
+ ReflectApply,
+ SafeSet,
+ TypedArrayPrototypeGetBuffer,
+ globalThis: {
+ Atomics: {
+ add: AtomicsAdd,
+ notify: AtomicsNotify,
+ },
+ },
+} = primordials;
+const assert = require('internal/assert');
+const { clearImmediate, setImmediate } = require('timers');
+const {
+ hasUncaughtExceptionCaptureCallback,
+} = require('internal/process/execution');
+const {
+ isArrayBuffer,
+ isDataView,
+ isTypedArray,
+} = require('util/types');
+
+const { receiveMessageOnPort } = require('internal/worker/io');
+const {
+ WORKER_TO_MAIN_THREAD_NOTIFICATION,
+} = require('internal/modules/esm/shared_constants');
+const { initializeHooks } = require('internal/modules/esm/utils');
+
+
+/**
+ * Transfers an ArrayBuffer, TypedArray, or DataView to a worker thread.
+ * @param {boolean} hasError - Whether an error occurred during transfer.
+ * @param {ArrayBuffer | TypedArray | DataView} source - The data to transfer.
+ */
+function transferArrayBuffer(hasError, source) {
+ if (hasError || source == null) { return; }
+ if (isArrayBuffer(source)) { return [source]; }
+ if (isTypedArray(source)) { return [TypedArrayPrototypeGetBuffer(source)]; }
+ if (isDataView(source)) { return [DataViewPrototypeGetBuffer(source)]; }
+}
+
+/**
+ * Wraps a message with a status and body, and serializes the body if necessary.
+ * @param {string} status - The status of the message.
+ * @param {unknown} body - The body of the message.
+ */
+function wrapMessage(status, body) {
+ if (status === 'success' || body === null ||
+ (typeof body !== 'object' &&
+ typeof body !== 'function' &&
+ typeof body !== 'symbol')) {
+ return { status, body };
+ }
+
+ let serialized;
+ let serializationFailed;
+ try {
+ const { serializeError } = require('internal/error_serdes');
+ serialized = serializeError(body);
+ } catch {
+ serializationFailed = true;
+ }
+
+ return {
+ status,
+ body: {
+ serialized,
+ serializationFailed,
+ },
+ };
+}
+
+/**
+ * Initializes a worker thread for a customized module loader.
+ * @param {SharedArrayBuffer} lock - The lock used to synchronize communication between the worker and the main thread.
+ * @param {MessagePort} syncCommPort - The message port used for synchronous communication between the worker and the
+ * main thread.
+ * @param {(err: Error, origin?: string) => void} errorHandler - The function to use for uncaught exceptions.
+ * @returns {Promise} A promise that resolves when the worker thread has been initialized.
+ */
+async function customizedModuleWorker(lock, syncCommPort, errorHandler) {
+ let hooks, preloadScripts, initializationError;
+ let hasInitializationError = false;
+
+ {
+ // If a custom hook is calling `process.exit`, we should wake up the main thread
+ // so it can detect the exit event.
+ const { exit } = process;
+ process.exit = function(code) {
+ syncCommPort.postMessage(wrapMessage('exit', code ?? process.exitCode));
+ AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
+ AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
+ return ReflectApply(exit, this, arguments);
+ };
+ }
+
+
+ try {
+ const initResult = await initializeHooks();
+ hooks = initResult.hooks;
+ preloadScripts = initResult.preloadScripts;
+ } catch (exception) {
+ // If there was an error while parsing and executing a user loader, for example if because a
+ // loader contained a syntax error, then we need to send the error to the main thread so it can
+ // be thrown and printed.
+ hasInitializationError = true;
+ initializationError = exception;
+ }
+
+ syncCommPort.on('message', handleMessage);
+
+ if (hasInitializationError) {
+ syncCommPort.postMessage(wrapMessage('error', initializationError));
+ } else {
+ syncCommPort.postMessage(wrapMessage('success', { preloadScripts }), preloadScripts.map(({ port }) => port));
+ }
+
+ // We're ready, so unlock the main thread.
+ AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
+ AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
+
+ let immediate;
+ /**
+ * Checks for messages on the syncCommPort and handles them asynchronously.
+ */
+ function checkForMessages() {
+ immediate = setImmediate(checkForMessages).unref();
+ // We need to let the event loop tick a few times to give the main thread a chance to send
+ // follow-up messages.
+ const response = receiveMessageOnPort(syncCommPort);
+
+ if (response !== undefined) {
+ PromisePrototypeThen(handleMessage(response.message), undefined, errorHandler);
+ }
+ }
+
+ const unsettledResponsePorts = new SafeSet();
+
+ process.on('beforeExit', () => {
+ for (const port of unsettledResponsePorts) {
+ port.postMessage(wrapMessage('never-settle'));
+ }
+ unsettledResponsePorts.clear();
+
+ AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
+ AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
+
+ // Attach back the event handler.
+ syncCommPort.on('message', handleMessage);
+ // Also check synchronously for a message, in case it's already there.
+ clearImmediate(immediate);
+ checkForMessages();
+ // We don't need the sync check after this tick, as we already have added the event handler.
+ clearImmediate(immediate);
+ // Add some work for next tick so the worker cannot exit.
+ setImmediate(() => {});
+ });
+
+ /**
+ * Handles incoming messages from the main thread or other workers.
+ * @param {object} options - The options object.
+ * @param {string} options.method - The name of the hook.
+ * @param {Array} options.args - The arguments to pass to the method.
+ * @param {MessagePort} options.port - The message port to use for communication.
+ */
+ async function handleMessage({ method, args, port }) {
+ // Each potential exception needs to be caught individually so that the correct error is sent to
+ // the main thread.
+ let hasError = false;
+ let shouldRemoveGlobalErrorHandler = false;
+ assert(typeof hooks[method] === 'function');
+ if (port == null && !hasUncaughtExceptionCaptureCallback()) {
+ // When receiving sync messages, we want to unlock the main thread when there's an exception.
+ process.on('uncaughtException', errorHandler);
+ shouldRemoveGlobalErrorHandler = true;
+ }
+
+ // We are about to yield the execution with `await ReflectApply` below. In case the code
+ // following the `await` never runs, we remove the message handler so the `beforeExit` event
+ // can be triggered.
+ syncCommPort.off('message', handleMessage);
+
+ // We keep checking for new messages to not miss any.
+ clearImmediate(immediate);
+ immediate = setImmediate(checkForMessages).unref();
+
+ unsettledResponsePorts.add(port ?? syncCommPort);
+
+ let response;
+ try {
+ response = await ReflectApply(hooks[method], hooks, args);
+ } catch (exception) {
+ hasError = true;
+ response = exception;
+ }
+
+ unsettledResponsePorts.delete(port ?? syncCommPort);
+
+ // Send the method response (or exception) to the main thread.
+ try {
+ (port ?? syncCommPort).postMessage(
+ wrapMessage(hasError ? 'error' : 'success', response),
+ transferArrayBuffer(hasError, response?.source),
+ );
+ } catch (exception) {
+ // Or send the exception thrown when trying to send the response.
+ (port ?? syncCommPort).postMessage(wrapMessage('error', exception));
+ }
+
+ AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
+ AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
+ if (shouldRemoveGlobalErrorHandler) {
+ process.off('uncaughtException', errorHandler);
+ }
+
+ syncCommPort.off('message', handleMessage);
+ // We keep checking for new messages to not miss any.
+ clearImmediate(immediate);
+ immediate = setImmediate(checkForMessages).unref();
+ }
+}
+
+/**
+ * Initializes a worker thread for a module with customized hooks.
+ * ! Run everything possible within this function so errors get reported.
+ * @param {{lock: SharedArrayBuffer}} workerData - The lock used to synchronize with the main thread.
+ * @param {MessagePort} syncCommPort - The communication port used to communicate with the main thread.
+ */
+module.exports = function setupModuleWorker(workerData, syncCommPort) {
+ const lock = new Int32Array(workerData.lock);
+
+ /**
+ * Handles errors that occur in the worker thread.
+ * @param {Error} err - The error that occurred.
+ * @param {string} [origin='unhandledRejection'] - The origin of the error.
+ */
+ function errorHandler(err, origin = 'unhandledRejection') {
+ AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
+ AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
+ process.off('uncaughtException', errorHandler);
+ if (hasUncaughtExceptionCaptureCallback()) {
+ process._fatalException(err);
+ return;
+ }
+ internalBinding('errors').triggerUncaughtException(
+ err,
+ origin === 'unhandledRejection',
+ );
+ }
+
+ return PromisePrototypeThen(
+ customizedModuleWorker(lock, syncCommPort, errorHandler),
+ undefined,
+ errorHandler,
+ );
+};
diff --git a/lib/internal/modules/helpers.js b/lib/internal/modules/helpers.js
index 307a34cb09b512..7f2959cc469dc1 100644
--- a/lib/internal/modules/helpers.js
+++ b/lib/internal/modules/helpers.js
@@ -21,6 +21,8 @@ const {
const { BuiltinModule } = require('internal/bootstrap/realm');
const { validateString } = require('internal/validators');
+const fs = require('fs'); // Import all of `fs` so that it can be monkey-patched.
+const internalFS = require('internal/fs/utils');
const path = require('path');
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
@@ -37,7 +39,30 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
debug = fn;
});
+/** @typedef {import('internal/modules/cjs/loader.js').Module} Module */
+
+/**
+ * Cache for storing resolved real paths of modules.
+ * In order to minimize unnecessary lstat() calls, this cache is a list of known-real paths.
+ * Set to an empty Map to reset.
+ * @type {Map}
+ */
+const realpathCache = new SafeMap();
+/**
+ * Resolves the path of a given `require` specifier, following symlinks.
+ * @param {string} requestPath The `require` specifier
+ */
+function toRealPath(requestPath) {
+ return fs.realpathSync(requestPath, {
+ [internalFS.realpathCacheKey]: realpathCache,
+ });
+}
+
+/** @type {Set} */
let cjsConditions;
+/**
+ * Define the conditions that apply to the CommonJS loader.
+ */
function initializeCjsConditions() {
const userConditions = getOptionValue('--conditions');
const noAddons = getOptionValue('--no-addons');
@@ -51,6 +76,9 @@ function initializeCjsConditions() {
]);
}
+/**
+ * Get the conditions that apply to the CommonJS loader.
+ */
function getCjsConditions() {
if (cjsConditions === undefined) {
initializeCjsConditions();
@@ -58,27 +86,45 @@ function getCjsConditions() {
return cjsConditions;
}
-function loadBuiltinModule(filename, request) {
- if (!BuiltinModule.canBeRequiredByUsers(filename)) {
+/**
+ * Provide one of Node.js' public modules to user code.
+ * @param {string} id - The identifier/specifier of the builtin module to load
+ * @param {string} request - The module requiring or importing the builtin module
+ */
+function loadBuiltinModule(id, request) {
+ if (!BuiltinModule.canBeRequiredByUsers(id)) {
return;
}
- const mod = BuiltinModule.map.get(filename);
+ /** @type {import('internal/bootstrap/realm.js').BuiltinModule} */
+ const mod = BuiltinModule.map.get(id);
debug('load built-in module %s', request);
// compileForPublicLoader() throws if canBeRequiredByUsers is false:
mod.compileForPublicLoader();
return mod;
}
+/** @type {Module} */
let $Module = null;
+/**
+ * Import the Module class on first use.
+ */
function lazyModule() {
$Module = $Module || require('internal/modules/cjs/loader').Module;
return $Module;
}
-// Invoke with makeRequireFunction(module) where |module| is the Module object
-// to use as the context for the require() function.
-// Use redirects to set up a mapping from a policy and restrict dependencies
+/**
+ * Invoke with `makeRequireFunction(module)` where `module` is the `Module` object to use as the context for the
+ * `require()` function.
+ * Use redirects to set up a mapping from a policy and restrict dependencies.
+ */
const urlToFileCache = new SafeMap();
+/**
+ * Create the module-scoped `require` function to pass into CommonJS modules.
+ * @param {Module} mod - The module to create the `require` function for.
+ * @param {ReturnType} redirects
+ * @typedef {(specifier: string) => unknown} RequireFunction
+ */
function makeRequireFunction(mod, redirects) {
// lazy due to cycle
const Module = lazyModule();
@@ -86,6 +132,7 @@ function makeRequireFunction(mod, redirects) {
throw new ERR_INVALID_ARG_TYPE('mod', 'Module', mod);
}
+ /** @type {RequireFunction} */
let require;
if (redirects) {
const id = mod.filename || mod.id;
@@ -131,6 +178,11 @@ function makeRequireFunction(mod, redirects) {
};
}
+ /**
+ * The `resolve` method that gets attached to module-scope `require`.
+ * @param {string} request
+ * @param {Parameters[3]} options
+ */
function resolve(request, options) {
validateString(request, 'request');
return Module._resolveFilename(request, mod, false, options);
@@ -138,6 +190,10 @@ function makeRequireFunction(mod, redirects) {
require.resolve = resolve;
+ /**
+ * The `paths` method that gets attached to module-scope `require`.
+ * @param {string} request
+ */
function paths(request) {
validateString(request, 'request');
return Module._resolveLookupPaths(request, mod);
@@ -159,6 +215,7 @@ function makeRequireFunction(mod, redirects) {
* Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
* because the buffer-to-string conversion in `fs.readFileSync()`
* translates it to FEFF, the UTF-16 BOM.
+ * @param {string} content
*/
function stripBOM(content) {
if (StringPrototypeCharCodeAt(content) === 0xFEFF) {
@@ -167,6 +224,11 @@ function stripBOM(content) {
return content;
}
+/**
+ * Add built-in modules to a global or REPL scope object.
+ * @param {Record} object - The object such as `globalThis` to add the built-in modules to.
+ * @param {string} dummyModuleName - The label representing the set of built-in modules to add.
+ */
function addBuiltinLibsToObject(object, dummyModuleName) {
// Make built-in modules available directly (loaded lazily).
const Module = require('internal/modules/cjs/loader').Module;
@@ -227,9 +289,8 @@ function addBuiltinLibsToObject(object, dummyModuleName) {
}
/**
- *
+ * If a referrer is an URL instance or absolute path, convert it into an URL string.
* @param {string | URL} referrer
- * @returns {string}
*/
function normalizeReferrerURL(referrer) {
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
@@ -238,7 +299,10 @@ function normalizeReferrerURL(referrer) {
return new URL(referrer).href;
}
-// For error messages only - used to check if ESM syntax is in use.
+/**
+ * For error messages only, check if ESM syntax is in use.
+ * @param {string} code
+ */
function hasEsmSyntax(code) {
debug('Checking for ESM syntax');
const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
@@ -265,4 +329,5 @@ module.exports = {
makeRequireFunction,
normalizeReferrerURL,
stripBOM,
+ toRealPath,
};
diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js
index bb175d0df54c04..1968960576013c 100644
--- a/lib/internal/modules/package_json_reader.js
+++ b/lib/internal/modules/package_json_reader.js
@@ -1,30 +1,124 @@
'use strict';
-const { SafeMap } = primordials;
+const {
+ JSONParse,
+ ObjectPrototypeHasOwnProperty,
+ SafeMap,
+ StringPrototypeEndsWith,
+ StringPrototypeIndexOf,
+ StringPrototypeLastIndexOf,
+ StringPrototypeSlice,
+} = primordials;
+const {
+ ERR_INVALID_PACKAGE_CONFIG,
+} = require('internal/errors').codes;
const { internalModuleReadJSON } = internalBinding('fs');
-const { pathToFileURL } = require('url');
-const { toNamespacedPath } = require('path');
+const { resolve, sep, toNamespacedPath } = require('path');
+const { kEmptyObject } = require('internal/util');
+
+const { fileURLToPath, pathToFileURL } = require('internal/url');
const cache = new SafeMap();
+const isAIX = process.platform === 'aix';
let manifest;
/**
- *
+ * @typedef {{
+ * exists: boolean,
+ * pjsonPath: string,
+ * exports?: string | string[] | Record,
+ * imports?: string | string[] | Record,
+ * name?: string,
+ * main?: string,
+ * type: 'commonjs' | 'module' | 'none',
+ * }} PackageConfig
+ */
+
+/**
* @param {string} jsonPath
+ * @param {{
+ * base?: string,
+ * specifier: string,
+ * isESM: boolean,
+ * }} options
+ * @returns {PackageConfig}
*/
-function read(jsonPath) {
+function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
if (cache.has(jsonPath)) {
return cache.get(jsonPath);
}
- const { 0: string, 1: containsKeys } = internalModuleReadJSON(
+ const {
+ 0: string,
+ 1: containsKeys,
+ } = internalModuleReadJSON(
toNamespacedPath(jsonPath),
);
- const result = { string, containsKeys };
- const { getOptionValue } = require('internal/options');
- if (string !== undefined) {
+ const result = {
+ __proto__: null,
+ exists: false,
+ pjsonPath: jsonPath,
+ main: undefined,
+ name: undefined,
+ type: 'none', // Ignore unknown types for forwards compatibility
+ exports: undefined,
+ imports: undefined,
+ };
+
+ // Folder read operation succeeds in AIX.
+ // For libuv change, see https://github.com/libuv/libuv/pull/2025.
+ // https://github.com/nodejs/node/pull/48477#issuecomment-1604586650
+ // TODO(anonrig): Follow-up on this change and remove it since it is a
+ // semver-major change.
+ const isResultValid = isAIX && !isESM ? containsKeys : string !== undefined;
+
+ if (isResultValid) {
+ let parsed;
+ try {
+ parsed = JSONParse(string);
+ } catch (error) {
+ if (isESM) {
+ throw new ERR_INVALID_PACKAGE_CONFIG(
+ jsonPath,
+ (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier),
+ error.message,
+ );
+ } else {
+ // For backward compat, we modify the error returned by JSON.parse rather than creating a new one.
+ // TODO(aduh95): make it throw ERR_INVALID_PACKAGE_CONFIG in a semver-major with original error as cause
+ error.message = 'Error parsing ' + jsonPath + ': ' + error.message;
+ error.path = jsonPath;
+ throw error;
+ }
+ }
+
+ result.exists = true;
+
+ // ObjectPrototypeHasOwnProperty is used to avoid prototype pollution.
+ if (ObjectPrototypeHasOwnProperty(parsed, 'name') && typeof parsed.name === 'string') {
+ result.name = parsed.name;
+ }
+
+ if (ObjectPrototypeHasOwnProperty(parsed, 'main') && typeof parsed.main === 'string') {
+ result.main = parsed.main;
+ }
+
+ if (ObjectPrototypeHasOwnProperty(parsed, 'exports')) {
+ result.exports = parsed.exports;
+ }
+
+ if (ObjectPrototypeHasOwnProperty(parsed, 'imports')) {
+ result.imports = parsed.imports;
+ }
+
+ // Ignore unknown types for forwards compatibility
+ if (ObjectPrototypeHasOwnProperty(parsed, 'type') && (parsed.type === 'commonjs' || parsed.type === 'module')) {
+ result.type = parsed.type;
+ }
+
if (manifest === undefined) {
+ const { getOptionValue } = require('internal/options');
manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
@@ -38,4 +132,41 @@ function read(jsonPath) {
return result;
}
-module.exports = { read };
+/**
+ * @param {string} requestPath
+ * @return {PackageConfig}
+ */
+function readPackage(requestPath) {
+ return read(resolve(requestPath, 'package.json'));
+}
+
+/**
+ * Get the nearest parent package.json file from a given path.
+ * Return the package.json data and the path to the package.json file, or false.
+ * @param {string} checkPath The path to start searching from.
+ */
+function readPackageScope(checkPath) {
+ const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep);
+ let separatorIndex;
+ do {
+ separatorIndex = StringPrototypeLastIndexOf(checkPath, sep);
+ checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex);
+ if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) {
+ return false;
+ }
+ const pjson = readPackage(checkPath + sep);
+ if (pjson.exists) {
+ return {
+ data: pjson,
+ path: checkPath,
+ };
+ }
+ } while (separatorIndex > rootSeparatorIndex);
+ return false;
+}
+
+module.exports = {
+ read,
+ readPackage,
+ readPackageScope,
+};
diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js
index 42a0b6af0626ec..287f3ed91b1d7b 100644
--- a/lib/internal/modules/run_main.js
+++ b/lib/internal/modules/run_main.js
@@ -1,30 +1,56 @@
'use strict';
const {
- ObjectCreate,
StringPrototypeEndsWith,
} = primordials;
const { getOptionValue } = require('internal/options');
const path = require('path');
+/**
+ * Get the absolute path to the main entry point.
+ * @param {string} main - Entry point path
+ */
function resolveMainPath(main) {
- // Note extension resolution for the main entry point can be deprecated in a
- // future major.
- // Module._findPath is monkey-patchable here.
- const { Module, toRealPath } = require('internal/modules/cjs/loader');
- let mainPath = Module._findPath(path.resolve(main), null, true);
- if (!mainPath)
- return;
+ const defaultType = getOptionValue('--experimental-default-type');
+ /** @type {string} */
+ let mainPath;
+ if (defaultType === 'module') {
+ if (getOptionValue('--preserve-symlinks-main')) { return; }
+ mainPath = path.resolve(main);
+ } else {
+ // Extension searching for the main entry point is supported only in legacy mode.
+ // Module._findPath is monkey-patchable here.
+ const { Module } = require('internal/modules/cjs/loader');
+ mainPath = Module._findPath(path.resolve(main), null, true);
+ }
+ if (!mainPath) { return; }
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
- if (!preserveSymlinksMain)
- mainPath = toRealPath(mainPath);
+ if (!preserveSymlinksMain) {
+ const { toRealPath } = require('internal/modules/helpers');
+ try {
+ mainPath = toRealPath(mainPath);
+ } catch (err) {
+ if (defaultType === 'module' && err?.code === 'ENOENT') {
+ const { decorateErrorWithCommonJSHints } = require('internal/modules/esm/resolve');
+ const { getCWDURL } = require('internal/util');
+ decorateErrorWithCommonJSHints(err, mainPath, getCWDURL());
+ }
+ throw err;
+ }
+ }
return mainPath;
}
+/**
+ * Determine whether the main entry point should be loaded through the ESM Loader.
+ * @param {string} mainPath - Absolute path to the main entry point
+ */
function shouldUseESMLoader(mainPath) {
+ if (getOptionValue('--experimental-default-type') === 'module') { return true; }
+
/**
* @type {string[]} userLoaders A list of custom loaders registered by the user
* (or an empty list when none have been registered).
@@ -35,33 +61,42 @@ function shouldUseESMLoader(mainPath) {
* (or an empty list when none have been registered).
*/
const userImports = getOptionValue('--import');
- if (userLoaders.length > 0 || userImports.length > 0)
+ if (userLoaders.length > 0 || userImports.length > 0) {
return true;
+ }
const esModuleSpecifierResolution =
getOptionValue('--experimental-specifier-resolution');
- if (esModuleSpecifierResolution === 'node')
- return true;
- const { readPackageScope } = require('internal/modules/cjs/loader');
- // Determine the module format of the main
- if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs'))
+ if (esModuleSpecifierResolution === 'node') {
return true;
- if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs'))
- return false;
+ }
+ // Determine the module format of the entry point.
+ if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
+ if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }
+
+ const { readPackageScope } = require('internal/modules/package_json_reader');
const pkg = readPackageScope(mainPath);
- return pkg && pkg.data.type === 'module';
+ // No need to guard `pkg` as it can only be an object or `false`.
+ return pkg.data?.type === 'module' || getOptionValue('--experimental-default-type') === 'module';
}
+/**
+ * Run the main entry point through the ESM Loader.
+ * @param {string} mainPath - Absolute path for the main entry point
+ */
function runMainESM(mainPath) {
const { loadESM } = require('internal/process/esm_loader');
const { pathToFileURL } = require('internal/url');
+ const main = pathToFileURL(mainPath).href;
handleMainPromise(loadESM((esmLoader) => {
- const main = path.isAbsolute(mainPath) ?
- pathToFileURL(mainPath).href : mainPath;
- return esmLoader.import(main, undefined, ObjectCreate(null));
+ return esmLoader.import(main, undefined, { __proto__: null });
}));
}
+/**
+ * Handle process exit events around the main entry point promise.
+ * @param {Promise} promise - Main entry point promise
+ */
async function handleMainPromise(promise) {
const {
handleProcessExit,
@@ -74,9 +109,14 @@ async function handleMainPromise(promise) {
}
}
-// For backwards compatibility, we have to run a bunch of
-// monkey-patchable code that belongs to the CJS loader (exposed by
-// `require('module')`) even when the entry point is ESM.
+/**
+ * Parse the CLI main entry point string and run it.
+ * For backwards compatibility, we have to run a bunch of monkey-patchable code that belongs to the CJS loader (exposed
+ * by `require('module')`) even when the entry point is ESM.
+ * This monkey-patchable code is bypassed under `--experimental-default-type=module`.
+ * Because of backwards compatibility, this function is exposed publicly via `import { runMain } from 'node:module'`.
+ * @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js`
+ */
function executeUserEntryPoint(main = process.argv[1]) {
const resolvedMain = resolveMainPath(main);
const useESMLoader = shouldUseESMLoader(resolvedMain);
diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js
index 9a04e094e001c4..a3451ddab307f2 100644
--- a/lib/internal/process/esm_loader.js
+++ b/lib/internal/process/esm_loader.js
@@ -1,105 +1,46 @@
'use strict';
const {
- ArrayIsArray,
- ArrayPrototypePushApply,
+ SafePromiseAllReturnVoid,
} = primordials;
-const { ESMLoader } = require('internal/modules/esm/loader');
+const { createModuleLoader } = require('internal/modules/esm/loader');
+const { getOptionValue } = require('internal/options');
const {
hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');
-const { pathToFileURL } = require('internal/url');
-const { kEmptyObject } = require('internal/util');
-
-const esmLoader = new ESMLoader();
-exports.esmLoader = esmLoader;
-
-// Module.runMain() causes loadESM() to re-run (which it should do); however, this should NOT cause
-// ESM to be re-initialized; doing so causes duplicate custom loaders to be added to the public
-// esmLoader.
-let isESMInitialized = false;
-
-/**
- * Causes side-effects: user-defined loader hooks are added to esmLoader.
- * @returns {void}
- */
-async function initializeLoader() {
- if (isESMInitialized) { return; }
-
- const { getOptionValue } = require('internal/options');
- const customLoaders = getOptionValue('--experimental-loader');
- const preloadModules = getOptionValue('--import');
-
- let cwd;
- try {
- cwd = process.cwd() + '/';
- } catch {
- cwd = '/';
- }
-
- const internalEsmLoader = new ESMLoader();
- const allLoaders = [];
-
- const parentURL = pathToFileURL(cwd).href;
-
- for (let i = 0; i < customLoaders.length; i++) {
- const customLoader = customLoaders[i];
-
- // Importation must be handled by internal loader to avoid polluting user-land
- const keyedExportsSublist = await internalEsmLoader.import(
- [customLoader],
- parentURL,
- kEmptyObject,
- );
-
- internalEsmLoader.addCustomLoaders(keyedExportsSublist);
- ArrayPrototypePushApply(allLoaders, keyedExportsSublist);
- }
-
- // Hooks must then be added to external/public loader
- // (so they're triggered in userland)
- esmLoader.addCustomLoaders(allLoaders);
- esmLoader.preload();
-
- // Preload after loaders are added so they can be used
- if (preloadModules?.length) {
- await loadModulesInIsolation(parentURL, preloadModules, allLoaders);
- }
-
- isESMInitialized = true;
-}
-
-function loadModulesInIsolation(parentURL, specifiers, loaders = []) {
- if (!ArrayIsArray(specifiers) || specifiers.length === 0) { return; }
-
- // A separate loader instance is necessary to avoid cross-contamination
- // between internal Node.js and userland. For example, a module with internal
- // state (such as a counter) should be independent.
- const internalEsmLoader = new ESMLoader();
- internalEsmLoader.addCustomLoaders(loaders);
- internalEsmLoader.preload();
-
- // Importation must be handled by internal loader to avoid polluting userland
- return internalEsmLoader.import(
- specifiers,
- parentURL,
- kEmptyObject,
- );
-}
-
-exports.loadESM = async function loadESM(callback) {
- try {
- await initializeLoader();
- await callback(esmLoader);
- } catch (err) {
- if (hasUncaughtExceptionCaptureCallback()) {
- process._fatalException(err);
- return;
+const { kEmptyObject, getCWDURL } = require('internal/util');
+
+let esmLoader;
+
+module.exports = {
+ get esmLoader() {
+ return esmLoader ??= createModuleLoader(true);
+ },
+ async loadESM(callback) {
+ esmLoader ??= createModuleLoader(true);
+ try {
+ const userImports = getOptionValue('--import');
+ if (userImports.length > 0) {
+ const parentURL = getCWDURL().href;
+ await SafePromiseAllReturnVoid(userImports, (specifier) => esmLoader.import(
+ specifier,
+ parentURL,
+ kEmptyObject,
+ ));
+ } else {
+ esmLoader.forceLoadHooks();
+ }
+ await callback(esmLoader);
+ } catch (err) {
+ if (hasUncaughtExceptionCaptureCallback()) {
+ process._fatalException(err);
+ return;
+ }
+ internalBinding('errors').triggerUncaughtException(
+ err,
+ true, /* fromPromise */
+ );
}
- internalBinding('errors').triggerUncaughtException(
- err,
- true, /* fromPromise */
- );
- }
+ },
};
diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js
index 7379db357bdcc4..c3a63f63d1d613 100644
--- a/lib/internal/process/execution.js
+++ b/lib/internal/process/execution.js
@@ -83,9 +83,9 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
filename: name,
displayErrors: true,
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
- importModuleDynamically(specifier, _, importAssertions) {
+ importModuleDynamically(specifier, _, importAttributes) {
const loader = asyncESM.esmLoader;
- return loader.import(specifier, baseUrl, importAssertions);
+ return loader.import(specifier, baseUrl, importAttributes);
},
}));
if (print) {
diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js
index d948b2b933eba2..f8f34f25f0c163 100644
--- a/lib/internal/process/pre_execution.js
+++ b/lib/internal/process/pre_execution.js
@@ -37,7 +37,7 @@ const {
} = require('internal/v8/startup_snapshot');
function prepareMainThreadExecution(expandArgv1 = false, initializeModules = true) {
- prepareExecution({
+ return prepareExecution({
expandArgv1,
initializeModules,
isMainThread: true,
@@ -58,8 +58,8 @@ function prepareExecution(options) {
refreshRuntimeOptions();
reconnectZeroFillToggle();
- // Patch the process object with legacy properties and normalizations
- patchProcessObject(expandArgv1);
+ // Patch the process object and get the resolved main entry point.
+ const mainEntry = patchProcessObject(expandArgv1);
setupTraceCategoryState();
setupPerfHooks();
setupInspectorHooks();
@@ -113,6 +113,8 @@ function prepareExecution(options) {
if (initializeModules) {
setupUserModules();
}
+
+ return mainEntry;
}
function setupSymbolDisposePolyfill() {
@@ -123,11 +125,15 @@ function setupSymbolDisposePolyfill() {
Symbol.asyncDispose ??= SymbolAsyncDispose;
}
-function setupUserModules() {
+function setupUserModules(isLoaderWorker = false) {
initializeCJSLoader();
- initializeESMLoader();
+ initializeESMLoader(isLoaderWorker);
const CJSLoader = require('internal/modules/cjs/loader');
assert(!CJSLoader.hasLoadedAnyUserCJSModule);
+ // Loader workers are responsible for doing this themselves.
+ if (isLoaderWorker) {
+ return;
+ }
loadPreloadModules();
// Need to be done after --require setup.
initializeFrozenIntrinsics();
@@ -137,12 +143,20 @@ function refreshRuntimeOptions() {
refreshOptions();
}
+/**
+ * Patch the process object with legacy properties and normalizations.
+ * Replace `process.argv[0]` with `process.execPath`, preserving the original `argv[0]` value as `process.argv0`.
+ * Replace `process.argv[1]` with the resolved absolute file path of the entry point, if found.
+ * @param {boolean} expandArgv1 - Whether to replace `process.argv[1]` with the resolved absolute file path of
+ * the main entry point.
+ */
function patchProcessObject(expandArgv1) {
const binding = internalBinding('process_methods');
binding.patchProcessObject(process);
require('internal/process/per_thread').refreshHrtimeBuffer();
+ // Since we replace process.argv[0] below, preserve the original value in case the user needs it.
ObjectDefineProperty(process, 'argv0', {
__proto__: null,
enumerable: true,
@@ -155,12 +169,17 @@ function patchProcessObject(expandArgv1) {
process._exiting = false;
process.argv[0] = process.execPath;
+ /** @type {string} */
+ let mainEntry;
+ // If requested, update process.argv[1] to replace whatever the user provided with the resolved absolute file path of
+ // the entry point.
if (expandArgv1 && process.argv[1] &&
!StringPrototypeStartsWith(process.argv[1], '-')) {
// Expand process.argv[1] into a full path.
const path = require('path');
try {
- process.argv[1] = path.resolve(process.argv[1]);
+ mainEntry = path.resolve(process.argv[1]);
+ process.argv[1] = mainEntry;
} catch {
// Continue regardless of error.
}
@@ -187,6 +206,8 @@ function patchProcessObject(expandArgv1) {
addReadOnlyProcessAlias('traceDeprecation', '--trace-deprecation');
addReadOnlyProcessAlias('_breakFirstLine', '--inspect-brk', false);
addReadOnlyProcessAlias('_breakNodeFirstLine', '--inspect-brk-node', false);
+
+ return mainEntry;
}
function addReadOnlyProcessAlias(name, option, enumerable = true) {
@@ -546,9 +567,9 @@ function initializeCJSLoader() {
initializeCJS();
}
-function initializeESMLoader() {
+function initializeESMLoader(isLoaderWorker) {
const { initializeESM } = require('internal/modules/esm/utils');
- initializeESM();
+ initializeESM(isLoaderWorker);
// Patch the vm module when --experimental-vm-modules is on.
// Please update the comments in vm.js when this block changes.
@@ -600,4 +621,6 @@ module.exports = {
prepareMainThreadExecution,
prepareWorkerThreadExecution,
markBootstrapComplete,
+ loadPreloadModules,
+ initializeFrozenIntrinsics,
};
diff --git a/lib/internal/url.js b/lib/internal/url.js
index 9e26e42b27a229..8e4a5e97b5d531 100644
--- a/lib/internal/url.js
+++ b/lib/internal/url.js
@@ -22,6 +22,7 @@ const {
ReflectOwnKeys,
RegExpPrototypeSymbolReplace,
SafeMap,
+ SafeSet,
SafeWeakMap,
StringPrototypeCharAt,
StringPrototypeCharCodeAt,
@@ -106,6 +107,40 @@ const searchParams = Symbol('query');
*/
const internalSearchParams = new SafeWeakMap();
+// `unsafeProtocol`, `hostlessProtocol` and `slashedProtocol` is
+// deliberately moved to `internal/url` rather than `url`.
+// Workers does not bootstrap URL module. Therefore, `SafeSet`
+// is not initialized on bootstrap. This case breaks the
+// test-require-delete-array-iterator test.
+
+// Protocols that can allow "unsafe" and "unwise" chars.
+const unsafeProtocol = new SafeSet([
+ 'javascript',
+ 'javascript:',
+]);
+// Protocols that never have a hostname.
+const hostlessProtocol = new SafeSet([
+ 'javascript',
+ 'javascript:',
+]);
+// Protocols that always contain a // bit.
+const slashedProtocol = new SafeSet([
+ 'http',
+ 'http:',
+ 'https',
+ 'https:',
+ 'ftp',
+ 'ftp:',
+ 'gopher',
+ 'gopher:',
+ 'file',
+ 'file:',
+ 'ws',
+ 'ws:',
+ 'wss',
+ 'wss:',
+]);
+
const updateActions = {
kProtocol: 0,
kHost: 1,
@@ -1534,4 +1569,7 @@ module.exports = {
isURL,
urlUpdateActions: updateActions,
+ unsafeProtocol,
+ hostlessProtocol,
+ slashedProtocol,
};
diff --git a/lib/internal/util.js b/lib/internal/util.js
index d9c292c99a6710..aedfa2e98896e7 100644
--- a/lib/internal/util.js
+++ b/lib/internal/util.js
@@ -357,6 +357,36 @@ function getConstructorOf(obj) {
return null;
}
+let cachedURL;
+let cachedCWD;
+
+/**
+ * Get the current working directory while accounting for the possibility that it has been deleted.
+ * `process.cwd()` can fail if the parent directory is deleted while the process runs.
+ * @returns {URL} The current working directory or the volume root if it cannot be determined.
+ */
+function getCWDURL() {
+ const { sep } = require('path');
+ const { pathToFileURL } = require('internal/url');
+
+ let cwd;
+
+ try {
+ // The implementation of `process.cwd()` already uses proper cache when it can.
+ // It's a relatively cheap call performance-wise for the most common use case.
+ cwd = process.cwd();
+ } catch {
+ cachedURL ??= pathToFileURL(sep);
+ }
+
+ if (cwd != null && cwd !== cachedCWD) {
+ cachedURL = pathToFileURL(cwd + sep);
+ cachedCWD = cwd;
+ }
+
+ return cachedURL;
+}
+
function getSystemErrorName(err) {
const entry = uvErrmapGet(err);
return entry ? entry[0] : `Unknown system error ${err}`;
@@ -784,6 +814,7 @@ module.exports = {
filterDuplicateStrings,
filterOwnProperties,
getConstructorOf,
+ getCWDURL,
getInternalGlobal,
getSystemErrorMap,
getSystemErrorName,
diff --git a/lib/internal/vm/module.js b/lib/internal/vm/module.js
index ec9618139b5dc2..19d93e1abfbd42 100644
--- a/lib/internal/vm/module.js
+++ b/lib/internal/vm/module.js
@@ -302,8 +302,8 @@ class SourceTextModule extends Module {
this[kLink] = async (linker) => {
this.#statusOverride = 'linking';
- const promises = this[kWrap].link(async (identifier, assert) => {
- const module = await linker(identifier, this, { assert });
+ const promises = this[kWrap].link(async (identifier, attributes) => {
+ const module = await linker(identifier, this, { attributes, assert: attributes });
if (module[kWrap] === undefined) {
throw new ERR_VM_MODULE_NOT_MODULE();
}
diff --git a/lib/internal/worker.js b/lib/internal/worker.js
index 3d828d2f6f2b19..98067387249627 100644
--- a/lib/internal/worker.js
+++ b/lib/internal/worker.js
@@ -44,6 +44,7 @@ const { getOptionValue } = require('internal/options');
const workerIo = require('internal/worker/io');
const {
drainMessagePort,
+ receiveMessageOnPort,
MessageChannel,
messageTypes,
kPort,
@@ -81,6 +82,7 @@ const kOnCouldNotSerializeErr = Symbol('kOnCouldNotSerializeErr');
const kOnErrorMessage = Symbol('kOnErrorMessage');
const kParentSideStdio = Symbol('kParentSideStdio');
const kLoopStartTime = Symbol('kLoopStartTime');
+const kIsInternal = Symbol('kIsInternal');
const kIsOnline = Symbol('kIsOnline');
const SHARE_ENV = SymbolFor('nodejs.worker_threads.SHARE_ENV');
@@ -124,7 +126,13 @@ function assignEnvironmentData(data) {
class Worker extends EventEmitter {
constructor(filename, options = kEmptyObject) {
super();
- debug(`[${threadId}] create new worker`, filename, options);
+ const isInternal = arguments[2] === kIsInternal;
+ debug(
+ `[${threadId}] create new worker`,
+ filename,
+ options,
+ `isInternal: ${isInternal}`,
+ );
if (options.execArgv)
validateArray(options.execArgv, 'options.execArgv');
@@ -135,7 +143,10 @@ class Worker extends EventEmitter {
}
let url, doEval;
- if (options.eval) {
+ if (isInternal) {
+ doEval = 'internal';
+ url = `node:${filename}`;
+ } else if (options.eval) {
if (typeof filename !== 'string') {
throw new ERR_INVALID_ARG_VALUE(
'options.eval',
@@ -191,12 +202,14 @@ class Worker extends EventEmitter {
name = StringPrototypeTrim(options.name);
}
+ debug('instantiating Worker.', `url: ${url}`, `doEval: ${doEval}`);
// Set up the C++ handle for the worker, as well as some internal wiring.
this[kHandle] = new WorkerImpl(url,
env === process.env ? null : env,
options.execArgv,
parseResourceLimits(options.resourceLimits),
!!(options.trackUnmanagedFds ?? true),
+ isInternal,
name);
if (this[kHandle].invalidExecArgv) {
throw new ERR_WORKER_INVALID_EXEC_ARGV(this[kHandle].invalidExecArgv);
@@ -248,6 +261,7 @@ class Worker extends EventEmitter {
type: messageTypes.LOAD_SCRIPT,
filename,
doEval,
+ isInternal,
cwdCounter: cwdCounter || workerIo.sharedCwdCounter,
workerData: options.workerData,
environmentData,
@@ -428,6 +442,20 @@ class Worker extends EventEmitter {
}
}
+/**
+ * A worker which has an internal module for entry point (e.g. internal/module/esm/worker).
+ * Internal workers bypass the permission model.
+ */
+class InternalWorker extends Worker {
+ constructor(filename, options) {
+ super(filename, options, kIsInternal);
+ }
+
+ receiveMessageSync() {
+ return receiveMessageOnPort(this[kPublicPort]);
+ }
+}
+
function pipeWithoutWarning(source, dest) {
const sourceMaxListeners = source._maxListeners;
const destMaxListeners = dest._maxListeners;
@@ -508,6 +536,7 @@ function eventLoopUtilization(util1, util2) {
module.exports = {
ownsProcessState,
+ kIsOnline,
isMainThread,
SHARE_ENV,
resourceLimits:
@@ -516,5 +545,6 @@ module.exports = {
getEnvironmentData,
assignEnvironmentData,
threadId,
+ InternalWorker,
Worker,
};
diff --git a/lib/module.js b/lib/module.js
index b4a6dd7d18de56..ee90e92f53093c 100644
--- a/lib/module.js
+++ b/lib/module.js
@@ -2,8 +2,10 @@
const { findSourceMap } = require('internal/source_map/source_map_cache');
const { Module } = require('internal/modules/cjs/loader');
+const { register } = require('internal/modules/esm/loader');
const { SourceMap } = require('internal/source_map/source_map');
Module.findSourceMap = findSourceMap;
+Module.register = register;
Module.SourceMap = SourceMap;
module.exports = Module;
diff --git a/lib/repl.js b/lib/repl.js
index aae269712f3d3f..b2d143619ae093 100644
--- a/lib/repl.js
+++ b/lib/repl.js
@@ -483,9 +483,9 @@ function REPLServer(prompt,
vm.createScript(fallbackCode, {
filename: file,
displayErrors: true,
- importModuleDynamically: (specifier, _, importAssertions) => {
+ importModuleDynamically: (specifier, _, importAttributes) => {
return asyncESM.esmLoader.import(specifier, parentURL,
- importAssertions);
+ importAttributes);
},
});
} catch (fallbackError) {
@@ -527,9 +527,9 @@ function REPLServer(prompt,
script = vm.createScript(code, {
filename: file,
displayErrors: true,
- importModuleDynamically: (specifier, _, importAssertions) => {
+ importModuleDynamically: (specifier, _, importAttributes) => {
return asyncESM.esmLoader.import(specifier, parentURL,
- importAssertions);
+ importAttributes);
},
});
} catch (e) {
diff --git a/lib/url.js b/lib/url.js
index 2b32a7e8bc8843..c99bf384962f90 100644
--- a/lib/url.js
+++ b/lib/url.js
@@ -26,7 +26,6 @@ const {
Int8Array,
ObjectCreate,
ObjectKeys,
- SafeSet,
StringPrototypeCharCodeAt,
decodeURIComponent,
} = primordials;
@@ -57,6 +56,9 @@ const {
fileURLToPath,
pathToFileURL,
urlToHttpOptions,
+ unsafeProtocol,
+ hostlessProtocol,
+ slashedProtocol,
} = require('internal/url');
const bindingUrl = internalBinding('url');
@@ -92,33 +94,6 @@ const hostPattern = /^\/\/[^@/]+@[^@/]+/;
const simplePathPattern = /^(\/\/?(?!\/)[^?\s]*)(\?[^\s]*)?$/;
const hostnameMaxLen = 255;
-// Protocols that can allow "unsafe" and "unwise" chars.
-const unsafeProtocol = new SafeSet([
- 'javascript',
- 'javascript:',
-]);
-// Protocols that never have a hostname.
-const hostlessProtocol = new SafeSet([
- 'javascript',
- 'javascript:',
-]);
-// Protocols that always contain a // bit.
-const slashedProtocol = new SafeSet([
- 'http',
- 'http:',
- 'https',
- 'https:',
- 'ftp',
- 'ftp:',
- 'gopher',
- 'gopher:',
- 'file',
- 'file:',
- 'ws',
- 'ws:',
- 'wss',
- 'wss:',
-]);
const {
CHAR_SPACE,
CHAR_TAB,
diff --git a/pyproject.toml b/pyproject.toml
index 6b51197ad66c2e..d0c3a056f2e92c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,6 +20,7 @@ select = [
exclude = [
"deps",
"tools/inspector_protocol",
+ "tools/node_modules",
]
ignore = [
"E401",
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index 9b2b0b8334d102..21b9a702aebaf4 100644
--- a/src/module_wrap.cc
+++ b/src/module_wrap.cc
@@ -250,19 +250,19 @@ void ModuleWrap::New(const FunctionCallbackInfo& args) {
args.GetReturnValue().Set(that);
}
-static Local