Skip to content

Commit ae65610

Browse files
GeoffreyBoothUlisesGascon
authored andcommittedSep 10, 2023
doc: update module hooks docs
PR-URL: #49265 Reviewed-By: Jacob Smith <jacob@frende.me> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>

File tree

3 files changed

+321
-256
lines changed

3 files changed

+321
-256
lines changed
 

Diff for: ‎doc/api/cli.md

+12-7
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ absolute path, it's resolved as a relative path from the current working
2828
directory. That path is then resolved by [CommonJS][] module loader. If no
2929
corresponding file is found, an error is thrown.
3030

31-
If a file is found, its path will be passed to the [ECMAScript module loader][]
32-
under any of the following conditions:
31+
If a file is found, its path will be passed to the
32+
[ES module loader][Modules loaders] under any of the following conditions:
3333

3434
* The program was started with a command-line flag that forces the entry
3535
point to be loaded with ECMAScript module loader.
@@ -43,9 +43,9 @@ Otherwise, the file is loaded using the CommonJS module loader. See
4343

4444
### ECMAScript modules loader entry point caveat
4545

46-
When loading [ECMAScript module loader][] loads the program entry point, the `node`
47-
command will only accept as input only files with `.js`, `.mjs`, or `.cjs`
48-
extensions; and with `.wasm` extensions when
46+
When loading, the [ES module loader][Modules loaders] loads the program
47+
entry point, the `node` command will accept as input only files with `.js`,
48+
`.mjs`, or `.cjs` extensions; and with `.wasm` extensions when
4949
[`--experimental-wasm-modules`][] is enabled.
5050

5151
## Options
@@ -550,7 +550,11 @@ changes:
550550
`--experimental-loader`.
551551
-->
552552

553-
Specify the `module` of a custom experimental [ECMAScript module loader][].
553+
> This flag is discouraged and may be removed in a future version of Node.js.
554+
> Please use
555+
> [`--import` with `register()`][module customization hooks: enabling] instead.
556+
557+
Specify the `module` containing exported [module customization hooks][].
554558
`module` may be any string accepted as an [`import` specifier][].
555559

556560
### `--experimental-network-imports`
@@ -2640,9 +2644,10 @@ done
26402644
[CommonJS module]: modules.md
26412645
[CustomEvent Web API]: https://dom.spec.whatwg.org/#customevent
26422646
[ECMAScript module]: esm.md#modules-ecmascript-modules
2643-
[ECMAScript module loader]: esm.md#loaders
26442647
[Fetch API]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
26452648
[File System Permissions]: permissions.md#file-system-permissions
2649+
[Module customization hooks]: module.md#customization-hooks
2650+
[Module customization hooks: enabling]: module.md#enabling
26462651
[Modules loaders]: packages.md#modules-loaders
26472652
[Node.js issue tracker]: https://github.com/nodejs/node/issues
26482653
[OSSL_PROVIDER-legacy]: https://www.openssl.org/docs/man3.0/man7/OSSL_PROVIDER-legacy.html

Diff for: ‎doc/api/esm.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1070,7 +1070,7 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
10701070
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
10711071
[cjs-module-lexer]: https://github.com/nodejs/cjs-module-lexer/tree/1.2.2
10721072
[commonjs-extension-resolution-loader]: https://github.com/nodejs/loaders-test/tree/main/commonjs-extension-resolution-loader
1073-
[custom https loader]: module.md#https-loader
1073+
[custom https loader]: module.md#import-from-https
10741074
[import.meta.resolve]: #importmetaresolvespecifier
10751075
[percent-encoded]: url.md#percent-encoding-in-urls
10761076
[special scheme]: https://url.spec.whatwg.org/#special-scheme

Diff for: ‎doc/api/module.md

+308-248
Original file line numberDiff line numberDiff line change
@@ -102,114 +102,7 @@ added: v20.6.0
102102
* Returns: {any} returns whatever was returned by the `initialize` hook.
103103
104104
Register a module that exports [hooks][] that customize Node.js module
105-
resolution and loading behavior.
106-
107-
```mjs
108-
import { register } from 'node:module';
109-
110-
register('http-to-https', import.meta.url);
111-
112-
// Because this is a dynamic `import()`, the `http-to-https` hooks will run
113-
// before importing `./my-app.mjs`.
114-
await import('./my-app.mjs');
115-
```
116-
117-
In the example above, we are registering the `http-to-https` loader,
118-
but it will only be available for subsequently imported modules—in
119-
this case, `my-app.mjs`. If the `await import('./my-app.mjs')` had
120-
instead been a static `import './my-app.mjs'`, _the app would already
121-
have been loaded_ before the `http-to-https` hooks were
122-
registered. This is part of the design of ES modules, where static
123-
imports are evaluated from the leaves of the tree first back to the
124-
trunk. There can be static imports _within_ `my-app.mjs`, which
125-
will not be evaluated until `my-app.mjs` is when it's dynamically
126-
imported.
127-
128-
The `--experimental-loader` flag of the CLI can be used together
129-
with the `register` function; the hooks registered with the
130-
function will follow the same evaluation chain of hooks registered
131-
within the CLI:
132-
133-
```console
134-
node \
135-
--experimental-loader unpkg \
136-
--experimental-loader http-to-https \
137-
--experimental-loader cache-buster \
138-
entrypoint.mjs
139-
```
140-
141-
```mjs
142-
// entrypoint.mjs
143-
import { URL } from 'node:url';
144-
import { register } from 'node:module';
145-
146-
const loaderURL = new URL('./my-programmatically-loader.mjs', import.meta.url);
147-
148-
register(loaderURL);
149-
await import('./my-app.mjs');
150-
```
151-
152-
The `my-programmatic-loader.mjs` can leverage `unpkg`,
153-
`http-to-https`, and `cache-buster` loaders.
154-
155-
It's also possible to use `register` more than once:
156-
157-
```mjs
158-
// entrypoint.mjs
159-
import { URL } from 'node:url';
160-
import { register } from 'node:module';
161-
162-
register(new URL('./first-loader.mjs', import.meta.url));
163-
register('./second-loader.mjs', import.meta.url);
164-
await import('./my-app.mjs');
165-
```
166-
167-
Both loaders (`first-loader.mjs` and `second-loader.mjs`) can use
168-
all the resources provided by the loaders registered in the CLI. But
169-
remember that they will only be available in the next imported
170-
module (`my-app.mjs`). The evaluation order of the hooks when
171-
importing `my-app.mjs` and consecutive modules in the example above
172-
will be:
173-
174-
```console
175-
resolve: second-loader.mjs
176-
resolve: first-loader.mjs
177-
resolve: cache-buster
178-
resolve: http-to-https
179-
resolve: unpkg
180-
load: second-loader.mjs
181-
load: first-loader.mjs
182-
load: cache-buster
183-
load: http-to-https
184-
load: unpkg
185-
globalPreload: second-loader.mjs
186-
globalPreload: first-loader.mjs
187-
globalPreload: cache-buster
188-
globalPreload: http-to-https
189-
globalPreload: unpkg
190-
```
191-
192-
This function can also be used to pass data to the loader's [`initialize`][]
193-
hook; the data passed to the hook may include transferrable objects like ports.
194-
195-
```mjs
196-
import { register } from 'node:module';
197-
import { MessageChannel } from 'node:worker_threads';
198-
199-
// This example showcases how a message channel can be used to
200-
// communicate to the loader, by sending `port2` to the loader.
201-
const { port1, port2 } = new MessageChannel();
202-
203-
port1.on('message', (msg) => {
204-
console.log(msg);
205-
});
206-
207-
register('./my-programmatic-loader.mjs', {
208-
parentURL: import.meta.url,
209-
data: { number: 1, port: port2 },
210-
transferList: [port2],
211-
});
212-
```
105+
resolution and loading behavior. See [Customization hooks][].
213106
214107
### `module.syncBuiltinESMExports()`
215108
@@ -250,6 +143,8 @@ import('node:fs').then((esmFS) => {
250143
});
251144
```
252145
146+
<i id="module_customization_hooks"></i>
147+
253148
## Customization Hooks
254149
255150
<!-- YAML
@@ -269,65 +164,233 @@ changes:
269164
`globalPreload`; added `load` hook and `getGlobalPreload` hook.
270165
-->
271166
272-
> Stability: 1 - Experimental
273-
274-
> This API is currently being redesigned and will still change.
167+
> Stability: 1.1 - Active development
275168
276169
<!-- type=misc -->
277170
278-
To customize the default module resolution, loader hooks can optionally be
279-
provided via a `--experimental-loader ./loader-name.mjs` argument to Node.js.
171+
<i id="enabling_module_customization_hooks"></i>
172+
173+
### Enabling
174+
175+
Module resolution and loading can be customized by registering a file which
176+
exports a set of hooks. This can be done using the [`register`][] method
177+
from `node:module`, which you can run before your application code by
178+
using the `--import` flag:
179+
180+
```bash
181+
node --import ./register-hooks.js ./my-app.js
182+
```
183+
184+
```mjs
185+
// register-hooks.js
186+
import { register } from 'node:module';
187+
188+
register('./hooks.mjs', import.meta.url);
189+
```
190+
191+
```cjs
192+
// register-hooks.js
193+
const { register } = require('node:module');
194+
const { pathToFileURL } = require('node:url');
195+
196+
register('./hooks.mjs', pathToFileURL(__filename));
197+
```
198+
199+
The file passed to `--import` can also be an export from a dependency:
200+
201+
```bash
202+
node --import some-package/register ./my-app.js
203+
```
204+
205+
Where `some-package` has an [`"exports"`][] field defining the `/register`
206+
export to map to a file that calls `register()`, like the following `register-hooks.js`
207+
example.
208+
209+
Using `--import` ensures that the hooks are registered before any application
210+
files are imported, including the entry point of the application. Alternatively,
211+
`register` can be called from the entry point, but dynamic `import()` must be
212+
used for any code that should be run after the hooks are registered:
213+
214+
```mjs
215+
import { register } from 'node:module';
216+
217+
register('http-to-https', import.meta.url);
218+
219+
// Because this is a dynamic `import()`, the `http-to-https` hooks will run
220+
// to handle `./my-app.js` and any other files it imports or requires.
221+
await import('./my-app.js');
222+
```
223+
224+
```cjs
225+
const { register } = require('node:module');
226+
const { pathToFileURL } = require('node:url');
227+
228+
register('http-to-https', pathToFileURL(__filename));
229+
230+
// Because this is a dynamic `import()`, the `http-to-https` hooks will run
231+
// to handle `./my-app.js` and any other files it imports or requires.
232+
import('./my-app.js');
233+
```
234+
235+
In this example, we are registering the `http-to-https` hooks, but they will
236+
only be available for subsequently imported modules—in this case, `my-app.js`
237+
and anything it references via `import` (and optionally `require`). If the
238+
`import('./my-app.js')` had instead been a static `import './my-app.js'`, the
239+
app would have _already_ been loaded **before** the `http-to-https` hooks were
240+
registered. This due to the ES modules specification, where static imports are
241+
evaluated from the leaves of the tree first, then back to the trunk. There can
242+
be static imports _within_ `my-app.js`, which will not be evaluated until
243+
`my-app.js` is dynamically imported.
280244
281-
When hooks are used they apply to each subsequent loader, the entry point, and
282-
all `import` calls. They won't apply to `require` calls; those still follow
283-
[CommonJS][] rules.
245+
`my-app.js` can also be CommonJS. Customization hooks will run for any
246+
modules that it references via `import` (and optionally `require`).
284247
285-
Loaders follow the pattern of `--require`:
248+
Finally, if all you want to do is register hooks before your app runs and you
249+
don't want to create a separate file for that purpose, you can pass a `data:`
250+
URL to `--import`:
286251
287252
```bash
288-
node \
289-
--experimental-loader unpkg \
290-
--experimental-loader http-to-https \
291-
--experimental-loader cache-buster
253+
node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("http-to-https", pathToFileURL("./"));' ./my-app.js
292254
```
293255
294-
These are called in the following sequence: `cache-buster` calls
295-
`http-to-https` which calls `unpkg`.
256+
### Chaining
257+
258+
It's possible to call `register` more than once:
259+
260+
```mjs
261+
// entrypoint.mjs
262+
import { register } from 'node:module';
263+
264+
register('./first.mjs', import.meta.url);
265+
register('./second.mjs', import.meta.url);
266+
await import('./my-app.mjs');
267+
```
268+
269+
```cjs
270+
// entrypoint.cjs
271+
const { register } = require('node:module');
272+
const { pathToFileURL } = require('node:url');
273+
274+
const parentURL = pathToFileURL(__filename);
275+
register('./first.mjs', parentURL);
276+
register('./second.mjs', parentURL);
277+
import('./my-app.mjs');
278+
```
279+
280+
In this example, the registered hooks will form chains. If both `first.mjs` and
281+
`second.mjs` define a `resolve` hook, both will be called, in the order they
282+
were registered. The same applies to all the other hooks.
283+
284+
The registered hooks also affect `register` itself. In this example,
285+
`second.mjs` will be resolved and loaded per the hooks registered by
286+
`first.mjs`. This allows for things like writing hooks in non-JavaScript
287+
languages, so long as an earlier registered loader is one that transpiles into
288+
JavaScript.
289+
290+
The `register` method cannot be called from within the module that defines the
291+
hooks.
292+
293+
### Communication with module customization hooks
294+
295+
Module customization hooks run on a dedicated thread, separate from the main
296+
thread that runs application code. This means mutating global variables won't
297+
affect the other thread(s), and message channels must be used to communicate
298+
between the threads.
299+
300+
The `register` method can be used to pass data to an [`initialize`][] hook. The
301+
data passed to the hook may include transferrable objects like ports.
302+
303+
```mjs
304+
import { register } from 'node:module';
305+
import { MessageChannel } from 'node:worker_threads';
306+
307+
// This example demonstrates how a message channel can be used to
308+
// communicate with the hooks, by sending `port2` to the hooks.
309+
const { port1, port2 } = new MessageChannel();
310+
311+
port1.on('message', (msg) => {
312+
console.log(msg);
313+
});
314+
315+
register('./my-hooks.mjs', {
316+
parentURL: import.meta.url,
317+
data: { number: 1, port: port2 },
318+
transferList: [port2],
319+
});
320+
```
321+
322+
```cjs
323+
const { register } = require('node:module');
324+
const { pathToFileURL } = require('node:url');
325+
const { MessageChannel } = require('node:worker_threads');
326+
327+
// This example showcases how a message channel can be used to
328+
// communicate with the hooks, by sending `port2` to the hooks.
329+
const { port1, port2 } = new MessageChannel();
330+
331+
port1.on('message', (msg) => {
332+
console.log(msg);
333+
});
334+
335+
register('./my-hooks.mjs', {
336+
parentURL: pathToFileURL(__filename),
337+
data: { number: 1, port: port2 },
338+
transferList: [port2],
339+
});
340+
```
296341
297342
### Hooks
298343
344+
The [`register`][] method can be used to register a module that exports a set of
345+
hooks. The hooks are functions that are called by Node.js to customize the
346+
module resolution and loading process. The exported functions must have specific
347+
names and signatures, and they must be exported as named exports.
348+
349+
```mjs
350+
export async function initialize({ number, port }) {
351+
// Receive data from `register`, return data to `register`.
352+
}
353+
354+
export async function resolve(specifier, context, nextResolve) {
355+
// Take an `import` or `require` specifier and resolve it to a URL.
356+
}
357+
358+
export async function load(url, context, nextLoad) {
359+
// Take a resolved URL and return the source code to be evaluated.
360+
}
361+
```
362+
299363
Hooks are part of a chain, even if that chain consists of only one custom
300364
(user-provided) hook and the default hook, which is always present. Hook
301365
functions nest: each one must always return a plain object, and chaining happens
302-
as a result of each function calling `next<hookName>()`, which is a reference
303-
to the subsequent loader's hook.
366+
as a result of each function calling `next<hookName>()`, which is a reference to
367+
the subsequent loader's hook.
304368
305-
A hook that returns a value lacking a required property triggers an exception.
306-
A hook that returns without calling `next<hookName>()` _and_ without returning
369+
A hook that returns a value lacking a required property triggers an exception. A
370+
hook that returns without calling `next<hookName>()` _and_ without returning
307371
`shortCircuit: true` also triggers an exception. These errors are to help
308-
prevent unintentional breaks in the chain.
372+
prevent unintentional breaks in the chain. Return `shortCircuit: true` from a
373+
hook to signal that the chain is intentionally ending at your hook.
309374
310-
Hooks are run in a separate thread, isolated from the main. That means it is a
311-
different [realm](https://tc39.es/ecma262/#realm). The hooks thread may be
312-
terminated by the main thread at any time, so do not depend on asynchronous
313-
operations (like `console.log`) to complete.
375+
Hooks are run in a separate thread, isolated from the main thread where
376+
application code runs. That means it is a different [realm][]. The hooks thread
377+
may be terminated by the main thread at any time, so do not depend on
378+
asynchronous operations (like `console.log`) to complete.
314379
315380
#### `initialize()`
316381
317382
<!-- YAML
318383
added: REPLACEME
319384
-->
320385
321-
> The loaders API is being redesigned. This hook may disappear or its
322-
> signature may change. Do not rely on the API described below.
386+
> Stability: 1.1 - Active development
323387
324388
* `data` {any} The data from `register(loader, import.meta.url, { data })`.
325389
* Returns: {any} The data to be returned to the caller of `register`.
326390
327-
The `initialize` hook provides a way to define a custom function that runs
328-
in the loader's thread when the loader is initialized. Initialization happens
329-
when the loader is registered via [`register`][] or registered via the
330-
`--experimental-loader` command line option.
391+
The `initialize` hook provides a way to define a custom function that runs in
392+
the hooks thread when the hooks module is initialized. Initialization happens
393+
when the hooks module is registered via [`register`][].
331394
332395
This hook can send and receive data from a [`register`][] invocation, including
333396
ports and other transferrable objects. The return value of `initialize` must be
@@ -338,11 +401,10 @@ either:
338401
[`port.postMessage`][]),
339402
* a `Promise` resolving to one of the aforementioned values.
340403
341-
Loader code:
404+
Module customization code:
342405
343406
```mjs
344-
// In the below example this file is referenced as
345-
// '/path-to-my-loader.js'
407+
// path-to-my-hooks.js
346408

347409
export async function initialize({ number, port }) {
348410
port.postMessage(`increment: ${number + 1}`);
@@ -357,16 +419,16 @@ import assert from 'node:assert';
357419
import { register } from 'node:module';
358420
import { MessageChannel } from 'node:worker_threads';
359421

360-
// This example showcases how a message channel can be used to
361-
// communicate between the main (application) thread and the loader
362-
// running on the loaders thread, by sending `port2` to the loader.
422+
// This example showcases how a message channel can be used to communicate
423+
// between the main (application) thread and the hooks running on the hooks
424+
// thread, by sending `port2` to the `initialize` hook.
363425
const { port1, port2 } = new MessageChannel();
364426

365427
port1.on('message', (msg) => {
366428
assert.strictEqual(msg, 'increment: 2');
367429
});
368430

369-
const result = register('/path-to-my-loader.js', {
431+
const result = register('./path-to-my-hooks.js', {
370432
parentURL: import.meta.url,
371433
data: { number: 1, port: port2 },
372434
transferList: [port2],
@@ -375,6 +437,30 @@ const result = register('/path-to-my-loader.js', {
375437
assert.strictEqual(result, 'ok');
376438
```
377439
440+
```cjs
441+
const assert = require('node:assert');
442+
const { register } = require('node:module');
443+
const { pathToFileURL } = require('node:url');
444+
const { MessageChannel } = require('node:worker_threads');
445+
446+
// This example showcases how a message channel can be used to communicate
447+
// between the main (application) thread and the hooks running on the hooks
448+
// thread, by sending `port2` to the `initialize` hook.
449+
const { port1, port2 } = new MessageChannel();
450+
451+
port1.on('message', (msg) => {
452+
assert.strictEqual(msg, 'increment: 2');
453+
});
454+
455+
const result = register('./path-to-my-hooks.js', {
456+
parentURL: pathToFileURL(__filename),
457+
data: { number: 1, port: port2 },
458+
transferList: [port2],
459+
});
460+
461+
assert.strictEqual(result, 'ok');
462+
```
463+
378464
#### `resolve(specifier, context, nextResolve)`
379465
380466
<!-- YAML
@@ -393,8 +479,7 @@ changes:
393479
description: Add support for import assertions.
394480
-->
395481
396-
> The loaders API is being redesigned. This hook may disappear or its
397-
> signature may change. Do not rely on the API described below.
482+
> Stability: 1.2 - Release candidate
398483
399484
* `specifier` {string}
400485
* `context` {Object}
@@ -417,21 +502,21 @@ changes:
417502
terminate the chain of `resolve` hooks. **Default:** `false`
418503
* `url` {string} The absolute URL to which this input resolves
419504
420-
> **Caveat** Despite support for returning promises and async functions, calls
505+
> **Warning** Despite support for returning promises and async functions, calls
421506
> to `resolve` may block the main thread which can impact performance.
422507
423508
The `resolve` hook chain is responsible for telling Node.js where to find and
424-
how to cache a given `import` statement or expression. It can optionally return
425-
its format (such as `'module'`) as a hint to the `load` hook. If a format is
426-
specified, the `load` hook is ultimately responsible for providing the final
427-
`format` value (and it is free to ignore the hint provided by `resolve`); if
428-
`resolve` provides a `format`, a custom `load` hook is required even if only to
429-
pass the value to the Node.js default `load` hook.
509+
how to cache a given `import` statement or expression, or `require` call. It can
510+
optionally return a format (such as `'module'`) as a hint to the `load` hook. If
511+
a format is specified, the `load` hook is ultimately responsible for providing
512+
the final `format` value (and it is free to ignore the hint provided by
513+
`resolve`); if `resolve` provides a `format`, a custom `load` hook is required
514+
even if only to pass the value to the Node.js default `load` hook.
430515
431516
Import type assertions are part of the cache key for saving loaded modules into
432-
the internal module cache. The `resolve` hook is responsible for
433-
returning an `importAssertions` object if the module should be cached with
434-
different assertions than were present in the source code.
517+
the internal module cache. The `resolve` hook is responsible for returning an
518+
`importAssertions` object if the module should be cached with different
519+
assertions than were present in the source code.
435520
436521
The `conditions` property in `context` is an array of conditions for
437522
[package exports conditions][Conditional exports] that apply to this resolution
@@ -445,7 +530,7 @@ Node.js module specifier resolution behavior_ when calling `defaultResolve`, the
445530
`context.conditions` array originally passed into the `resolve` hook.
446531
447532
```mjs
448-
export function resolve(specifier, context, nextResolve) {
533+
export async function resolve(specifier, context, nextResolve) {
449534
const { parentURL = null } = context;
450535

451536
if (Math.random() > 0.5) { // Some condition.
@@ -490,11 +575,7 @@ changes:
490575
its return.
491576
-->
492577
493-
> The loaders API is being redesigned. This hook may disappear or its
494-
> signature may change. Do not rely on the API described below.
495-
496-
> In a previous version of this API, this was split across 3 separate, now
497-
> deprecated, hooks (`getFormat`, `getSource`, and `transformSource`).
578+
> Stability: 1.2 - Release candidate
498579
499580
* `url` {string} The URL returned by the `resolve` chain
500581
* `context` {Object}
@@ -512,8 +593,8 @@ changes:
512593
terminate the chain of `resolve` hooks. **Default:** `false`
513594
* `source` {string|ArrayBuffer|TypedArray} The source for Node.js to evaluate
514595
515-
The `load` hook provides a way to define a custom method of determining how
516-
a URL should be interpreted, retrieved, and parsed. It is also in charge of
596+
The `load` hook provides a way to define a custom method of determining how a
597+
URL should be interpreted, retrieved, and parsed. It is also in charge of
517598
validating the import assertion.
518599
519600
The final value of `format` must be one of the following:
@@ -529,19 +610,23 @@ The final value of `format` must be one of the following:
529610
The value of `source` is ignored for type `'builtin'` because currently it is
530611
not possible to replace the value of a Node.js builtin (core) module.
531612
532-
The value of `source` can be omitted for type `'commonjs'`. When a `source` is
533-
provided, all `require` calls from this module will be processed by the ESM
534-
loader with registered `resolve` and `load` hooks; all `require.resolve` calls
535-
from this module will be processed by the ESM loader with registered `resolve`
536-
hooks; `require.extensions` and monkey-patching on the CommonJS module loader
537-
will not apply. If `source` is undefined or `null`, it will be handled by the
538-
CommonJS module loader and `require`/`require.resolve` calls will not go through
539-
the registered hooks. This behavior for nullish `source` is temporary — in the
540-
future, nullish `source` will not be supported.
541-
542-
The Node.js own `load` implementation, which is the value of `next` for the last
543-
loader in the `load` chain, returns `null` for `source` when `format` is
544-
`'commonjs'` for backward compatibility. Here is an example loader that would
613+
Omitting vs providing a `source` for `'commonjs'` has very different effects:
614+
615+
* When a `source` is provided, all `require` calls from this module will be
616+
processed by the ESM loader with registered `resolve` and `load` hooks; all
617+
`require.resolve` calls from this module will be processed by the ESM loader
618+
with registered `resolve` hooks; only a subset of the CommonJS API will be
619+
available (e.g. no `require.extensions`, no `require.cache`, no
620+
`require.resolve.paths`) and monkey-patching on the CommonJS module loader
621+
will not apply.
622+
* If `source` is undefined or `null`, it will be handled by the CommonJS module
623+
loader and `require`/`require.resolve` calls will not go through the
624+
registered hooks. This behavior for nullish `source` is temporary — in the
625+
future, nullish `source` will not be supported.
626+
627+
The Node.js internal `load` implementation, which is the value of `next` for the
628+
last hook in the `load` chain, returns `null` for `source` when `format` is
629+
`'commonjs'` for backward compatibility. Here is an example hook that would
545630
opt-in to using the non-default behavior:
546631
547632
```mjs
@@ -556,7 +641,7 @@ export async function load(url, context, nextLoad) {
556641
}
557642
```
558643
559-
> **Caveat**: The ESM `load` hook and namespaced exports from CommonJS modules
644+
> **Warning**: The ESM `load` hook and namespaced exports from CommonJS modules
560645
> are incompatible. Attempting to use them together will result in an empty
561646
> object from the import. This may be addressed in the future.
562647
@@ -569,9 +654,9 @@ If the source value of a text-based format (i.e., `'json'`, `'module'`)
569654
is not a string, it is converted to a string using [`util.TextDecoder`][].
570655
571656
The `load` hook provides a way to define a custom method for retrieving the
572-
source code of an ES module specifier. This would allow a loader to potentially
573-
avoid reading files from disk. It could also be used to map an unrecognized
574-
format to a supported one, for example `yaml` to `module`.
657+
source code of a resolved URL. This would allow a loader to potentially avoid
658+
reading files from disk. It could also be used to map an unrecognized format to
659+
a supported one, for example `yaml` to `module`.
575660
576661
```mjs
577662
export async function load(url, context, nextLoad) {
@@ -611,11 +696,11 @@ changes:
611696
description: Add support for chaining globalPreload hooks.
612697
-->
613698
614-
> This hook will be removed in a future version. Use [`initialize`][] instead.
615-
> When a loader has an `initialize` export, `globalPreload` will be ignored.
699+
> Stability: 1.0 - Early development
616700
617-
> In a previous version of this API, this hook was named
618-
> `getGlobalPreloadCode`.
701+
> **Warning:** This hook will be removed in a future version. Use
702+
> [`initialize`][] instead. When a hooks module has an `initialize` export,
703+
> `globalPreload` will be ignored.
619704
620705
* `context` {Object} Information to assist the preload code
621706
* `port` {MessagePort}
@@ -647,24 +732,25 @@ const require = createRequire(cwd() + '/<preload>');
647732
}
648733
```
649734
650-
In order to allow communication between the application and the loader, another
651-
argument is provided to the preload code: `port`. This is available as a
652-
parameter to the loader hook and inside of the source text returned by the hook.
653-
Some care must be taken in order to properly call [`port.ref()`][] and
735+
Another argument is provided to the preload code: `port`. This is available as a
736+
parameter to the hook and inside of the source text returned by the hook. This
737+
functionality has been moved to the `initialize` hook.
738+
739+
Care must be taken in order to properly call [`port.ref()`][] and
654740
[`port.unref()`][] to prevent a process from being in a state where it won't
655741
close normally.
656742
657743
```mjs
658744
/**
659-
* This example has the application context send a message to the loader
745+
* This example has the application context send a message to the hook
660746
* and sends the message back to the application context
661747
*/
662748
export function globalPreload({ port }) {
663749
port.onmessage = (evt) => {
664750
port.postMessage(evt.data);
665751
};
666752
return `\
667-
port.postMessage('console.log("I went to the Loader and back");');
753+
port.postMessage('console.log("I went to the hook and back");');
668754
port.onmessage = (evt) => {
669755
eval(evt.data);
670756
};
@@ -674,22 +760,23 @@ export function globalPreload({ port }) {
674760
675761
### Examples
676762
677-
The various loader hooks can be used together to accomplish wide-ranging
678-
customizations of the Node.js code loading and evaluation behaviors.
763+
The various module customization hooks can be used together to accomplish
764+
wide-ranging customizations of the Node.js code loading and evaluation
765+
behaviors.
679766
680-
#### HTTPS loader
767+
#### Import from HTTPS
681768
682769
In current Node.js, specifiers starting with `https://` are experimental (see
683770
[HTTPS and HTTP imports][]).
684771

685-
The loader below registers hooks to enable rudimentary support for such
772+
The hook below registers hooks to enable rudimentary support for such
686773
specifiers. While this may seem like a significant improvement to Node.js core
687-
functionality, there are substantial downsides to actually using this loader:
774+
functionality, there are substantial downsides to actually using these hooks:
688775
performance is much slower than loading files from disk, there is no caching,
689776
and there is no security.
690777

691778
```mjs
692-
// https-loader.mjs
779+
// https-hooks.mjs
693780
import { get } from 'node:https';
694781
695782
export function load(url, context, nextLoad) {
@@ -724,59 +811,42 @@ import { VERSION } from 'https://coffeescript.org/browser-compiler-modern/coffee
724811
console.log(VERSION);
725812
```
726813

727-
With the preceding loader, running
728-
`node --experimental-loader ./https-loader.mjs ./main.mjs`
814+
With the preceding hooks module, running
815+
`node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./https-hooks.mjs"));' ./main.mjs`
729816
prints the current version of CoffeeScript per the module at the URL in
730817
`main.mjs`.
731818

732-
#### Transpiler loader
819+
#### Transpilation
733820

734821
Sources that are in formats Node.js doesn't understand can be converted into
735822
JavaScript using the [`load` hook][load hook].
736823
737-
This is less performant than transpiling source files before running
738-
Node.js; a transpiler loader should only be used for development and testing
739-
purposes.
824+
This is less performant than transpiling source files before running Node.js;
825+
transpiler hooks should only be used for development and testing purposes.
740826
741827
```mjs
742-
// coffeescript-loader.mjs
828+
// coffeescript-hooks.mjs
743829
import { readFile } from 'node:fs/promises';
744830
import { dirname, extname, resolve as resolvePath } from 'node:path';
745831
import { cwd } from 'node:process';
746832
import { fileURLToPath, pathToFileURL } from 'node:url';
747-
import CoffeeScript from 'coffeescript';
833+
import coffeescript from 'coffeescript';
748834
749-
const baseURL = pathToFileURL(`${cwd()}/`).href;
835+
const extensionsRegex = /\.(coffee|litcoffee|coffee\.md)$/;
750836
751837
export async function load(url, context, nextLoad) {
752838
if (extensionsRegex.test(url)) {
753-
// Now that we patched resolve to let CoffeeScript URLs through, we need to
754-
// tell Node.js what format such URLs should be interpreted as. Because
755-
// CoffeeScript transpiles into JavaScript, it should be one of the two
756-
// JavaScript formats: 'commonjs' or 'module'.
757-
758839
// CoffeeScript files can be either CommonJS or ES modules, so we want any
759840
// CoffeeScript file to be treated by Node.js the same as a .js file at the
760841
// same location. To determine how Node.js would interpret an arbitrary .js
761842
// file, search up the file system for the nearest parent package.json file
762843
// and read its "type" field.
763844
const format = await getPackageType(url);
764-
// When a hook returns a format of 'commonjs', `source` is ignored.
765-
// To handle CommonJS files, a handler needs to be registered with
766-
// `require.extensions` in order to process the files with the CommonJS
767-
// loader. Avoiding the need for a separate CommonJS handler is a future
768-
// enhancement planned for ES module loaders.
769-
if (format === 'commonjs') {
770-
return {
771-
format,
772-
shortCircuit: true,
773-
};
774-
}
775845
776846
const { source: rawSource } = await nextLoad(url, { ...context, format });
777847
// This hook converts CoffeeScript source code into JavaScript source code
778848
// for all imported CoffeeScript files.
779-
const transformedSource = coffeeCompile(rawSource.toString(), url);
849+
const transformedSource = coffeescript.compile(rawSource.toString(), url);
780850
781851
return {
782852
format,
@@ -833,23 +903,22 @@ console.log "Brought to you by Node.js version #{version}"
833903
export scream = (str) -> str.toUpperCase()
834904
```
835905
836-
With the preceding loader, running
837-
`node --experimental-loader ./coffeescript-loader.mjs main.coffee`
906+
With the preceding hooks module, running
907+
`node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./coffeescript-hooks.mjs"));' ./main.coffee`
838908
causes `main.coffee` to be turned into JavaScript after its source code is
839909
loaded from disk but before Node.js executes it; and so on for any `.coffee`,
840910
`.litcoffee` or `.coffee.md` files referenced via `import` statements of any
841911
loaded file.
842912
843-
#### "import map" loader
913+
#### Import maps
844914
845-
The previous two loaders defined `load` hooks. This is an example of a loader
846-
that does its work via the `resolve` hook. This loader reads an
847-
`import-map.json` file that specifies which specifiers to override to another
848-
URL (this is a very simplistic implemenation of a small subset of the
849-
"import maps" specification).
915+
The previous two examples defined `load` hooks. This is an example of a
916+
`resolve` hook. This hooks module reads an `import-map.json` file that defines
917+
which specifiers to override to other URLs (this is a very simplistic
918+
implementation of a small subset of the "import maps" specification).
850919
851920
```mjs
852-
// import-map-loader.js
921+
// import-map-hooks.js
853922
import fs from 'node:fs/promises';
854923

855924
const { imports } = JSON.parse(await fs.readFile('import-map.json'));
@@ -863,7 +932,7 @@ export async function resolve(specifier, context, nextResolve) {
863932
}
864933
```
865934
866-
Let's assume we have these files:
935+
With these files:
867936
868937
```mjs
869938
// main.js
@@ -884,19 +953,8 @@ import 'a-module';
884953
console.log('some module!');
885954
```
886955
887-
If you run `node --experimental-loader ./import-map-loader.js main.js`
888-
the output will be `some module!`.
889-
890-
### Register loaders programmatically
891-
892-
<!-- YAML
893-
added: REPLACEME
894-
-->
895-
896-
In addition to using the `--experimental-loader` option in the CLI,
897-
loaders can also be registered programmatically. You can find
898-
detailed information about this process in the documentation page
899-
for [`module.register()`][].
956+
Running `node --import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register(pathToFileURL("./import-map-hooks.js"));' main.js`
957+
should print `some module!`.
900958
901959
## Source map v3 support
902960
@@ -1044,9 +1102,11 @@ returned object contains the following keys:
10441102
10451103
[CommonJS]: modules.md
10461104
[Conditional exports]: packages.md#conditional-exports
1105+
[Customization hooks]: #customization-hooks
10471106
[ES Modules]: esm.md
10481107
[HTTPS and HTTP imports]: esm.md#https-and-http-imports
10491108
[Source map v3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej
1109+
[`"exports"`]: packages.md#exports
10501110
[`--enable-source-maps`]: cli.md#--enable-source-maps
10511111
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
10521112
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
@@ -1055,7 +1115,6 @@ returned object contains the following keys:
10551115
[`TypedArray`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
10561116
[`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
10571117
[`initialize`]: #initialize
1058-
[`module.register()`]: #moduleregisterspecifier-parenturl-options
10591118
[`module`]: modules.md#the-module-object
10601119
[`port.postMessage`]: worker_threads.md#portpostmessagevalue-transferlist
10611120
[`port.ref()`]: worker_threads.md#portref
@@ -1066,5 +1125,6 @@ returned object contains the following keys:
10661125
[hooks]: #customization-hooks
10671126
[load hook]: #loadurl-context-nextload
10681127
[module wrapper]: modules.md#the-module-wrapper
1128+
[realm]: https://tc39.es/ecma262/#realm
10691129
[source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx
10701130
[transferrable objects]: worker_threads.md#portpostmessagevalue-transferlist

0 commit comments

Comments
 (0)
Please sign in to comment.