Skip to content

Commit 386e826

Browse files
izaakschroederRafaelGSS
authored andcommitted
esm: add initialize hook, integrate with register
Follows @giltayar's proposed API: > `register` can pass any data it wants to the loader, which will be passed to the exported `initialize` function of the loader. Additionally, if the user of `register` wants to communicate with the loader, it can just create a `MessageChannel` and pass the port to the loader as data. The `register` API is now: ```ts interface Options { parentUrl?: string; data?: any; transferList?: any[]; } function register(loader: string, parentUrl?: string): any; function register(loader: string, options?: Options): any; ``` This API is backwards compatible with the old one (new arguments are optional and at the end) and allows for passing data into the new `initialize` hook. If this hook returns data it is passed back to `register`: ```ts function initialize(data: any): Promise<any>; ``` **NOTE**: Currently there is no mechanism for a loader to exchange ownership of something back to the caller. Refs: nodejs/loaders#147 PR-URL: #48842 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent 74a2e1e commit 386e826

File tree

10 files changed

+414
-28
lines changed

10 files changed

+414
-28
lines changed

doc/api/esm.md

+71-2
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,9 @@ of Node.js applications.
685685
<!-- YAML
686686
added: v8.8.0
687687
changes:
688+
- version: REPLACEME
689+
pr-url: https://github.com/nodejs/node/pull/48842
690+
description: Added `initialize` hook to replace `globalPreload`.
688691
- version:
689692
- v18.6.0
690693
- v16.17.0
@@ -739,6 +742,69 @@ different [realm](https://tc39.es/ecma262/#realm). The hooks thread may be
739742
terminated by the main thread at any time, so do not depend on asynchronous
740743
operations (like `console.log`) to complete.
741744
745+
#### `initialize()`
746+
747+
<!-- YAML
748+
added: REPLACEME
749+
-->
750+
751+
> The loaders API is being redesigned. This hook may disappear or its
752+
> signature may change. Do not rely on the API described below.
753+
754+
* `data` {any} The data from `register(loader, import.meta.url, { data })`.
755+
* Returns: {any} The data to be returned to the caller of `register`.
756+
757+
The `initialize` hook provides a way to define a custom function that runs
758+
in the loader's thread when the loader is initialized. Initialization happens
759+
when the loader is registered via [`register`][] or registered via the
760+
`--loader` command line option.
761+
762+
This hook can send and receive data from a [`register`][] invocation, including
763+
ports and other transferrable objects. The return value of `initialize` must be
764+
either:
765+
766+
* `undefined`,
767+
* something that can be posted as a message between threads (e.g. the input to
768+
[`port.postMessage`][]),
769+
* a `Promise` resolving to one of the aforementioned values.
770+
771+
Loader code:
772+
773+
```js
774+
// In the below example this file is referenced as
775+
// '/path-to-my-loader.js'
776+
777+
export async function initialize({ number, port }) {
778+
port.postMessage(`increment: ${number + 1}`);
779+
return 'ok';
780+
}
781+
```
782+
783+
Caller code:
784+
785+
```js
786+
import assert from 'node:assert';
787+
import { register } from 'node:module';
788+
import { MessageChannel } from 'node:worker_threads';
789+
790+
// This example showcases how a message channel can be used to
791+
// communicate between the main (application) thread and the loader
792+
// running on the loaders thread, by sending `port2` to the loader.
793+
const { port1, port2 } = new MessageChannel();
794+
795+
port1.on('message', (msg) => {
796+
assert.strictEqual(msg, 'increment: 2');
797+
});
798+
799+
const result = register('/path-to-my-loader.js', {
800+
parentURL: import.meta.url,
801+
data: { number: 1, port: port2 },
802+
transferList: [port2],
803+
});
804+
805+
assert.strictEqual(result, 'ok');
806+
```
807+
742808
#### `resolve(specifier, context, nextResolve)`
743809
744810
<!-- YAML
@@ -949,8 +1015,8 @@ changes:
9491015
description: Add support for chaining globalPreload hooks.
9501016
-->
9511017
952-
> The loaders API is being redesigned. This hook may disappear or its
953-
> signature may change. Do not rely on the API described below.
1018+
> This hook will be removed in a future version. Use [`initialize`][] instead.
1019+
> When a loader has an `initialize` export, `globalPreload` will be ignored.
9541020
9551021
> In a previous version of this API, this hook was named
9561022
> `getGlobalPreloadCode`.
@@ -1609,13 +1675,16 @@ for ESM specifiers is [commonjs-extension-resolution-loader][].
16091675
[`import.meta.resolve`]: #importmetaresolvespecifier-parent
16101676
[`import.meta.url`]: #importmetaurl
16111677
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
1678+
[`initialize`]: #initialize
16121679
[`module.createRequire()`]: module.md#modulecreaterequirefilename
16131680
[`module.register()`]: module.md#moduleregister
16141681
[`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports
16151682
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
1683+
[`port.postMessage`]: worker_threads.md#portpostmessagevalue-transferlist
16161684
[`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref
16171685
[`port.unref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portunref
16181686
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
1687+
[`register`]: module.md#moduleregister
16191688
[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
16201689
[`util.TextDecoder`]: util.md#class-utiltextdecoder
16211690
[cjs-module-lexer]: https://github.com/nodejs/cjs-module-lexer/tree/1.2.2

doc/api/module.md

+23
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,28 @@ globalPreload: http-to-https
175175
globalPreload: unpkg
176176
```
177177
178+
This function can also be used to pass data to the loader's [`initialize`][]
179+
hook; the data passed to the hook may include transferrable objects like ports.
180+
181+
```mjs
182+
import { register } from 'node:module';
183+
import { MessageChannel } from 'node:worker_threads';
184+
185+
// This example showcases how a message channel can be used to
186+
// communicate to the loader, by sending `port2` to the loader.
187+
const { port1, port2 } = new MessageChannel();
188+
189+
port1.on('message', (msg) => {
190+
console.log(msg);
191+
});
192+
193+
register('./my-programmatic-loader.mjs', {
194+
parentURL: import.meta.url,
195+
data: { number: 1, port: port2 },
196+
transferList: [port2],
197+
});
198+
```
199+
178200
### `module.syncBuiltinESMExports()`
179201
180202
<!-- YAML
@@ -364,6 +386,7 @@ returned object contains the following keys:
364386
[`--enable-source-maps`]: cli.md#--enable-source-maps
365387
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
366388
[`SourceMap`]: #class-modulesourcemap
389+
[`initialize`]: esm.md#initialize
367390
[`module`]: modules.md#the-module-object
368391
[module wrapper]: modules.md#the-module-wrapper
369392
[source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx

lib/internal/modules/esm/hooks.js

+56-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
ArrayPrototypePush,
5+
ArrayPrototypePushApply,
56
FunctionPrototypeCall,
67
Int32Array,
78
ObjectAssign,
@@ -47,8 +48,10 @@ const {
4748
validateObject,
4849
validateString,
4950
} = require('internal/validators');
50-
51-
const { kEmptyObject } = require('internal/util');
51+
const {
52+
emitExperimentalWarning,
53+
kEmptyObject,
54+
} = require('internal/util');
5255

5356
const {
5457
defaultResolve,
@@ -83,6 +86,7 @@ let importMetaInitializer;
8386

8487
// [2] `validate...()`s throw the wrong error
8588

89+
let globalPreloadWarned = false;
8690
class Hooks {
8791
#chains = {
8892
/**
@@ -127,31 +131,43 @@ class Hooks {
127131
* Import and register custom/user-defined module loader hook(s).
128132
* @param {string} urlOrSpecifier
129133
* @param {string} parentURL
134+
* @param {any} [data] Arbitrary data to be passed from the custom
135+
* loader (user-land) to the worker.
130136
*/
131-
async register(urlOrSpecifier, parentURL) {
137+
async register(urlOrSpecifier, parentURL, data) {
132138
const moduleLoader = require('internal/process/esm_loader').esmLoader;
133139
const keyedExports = await moduleLoader.import(
134140
urlOrSpecifier,
135141
parentURL,
136142
kEmptyObject,
137143
);
138-
this.addCustomLoader(urlOrSpecifier, keyedExports);
144+
return this.addCustomLoader(urlOrSpecifier, keyedExports, data);
139145
}
140146

141147
/**
142148
* Collect custom/user-defined module loader hook(s).
143149
* After all hooks have been collected, the global preload hook(s) must be initialized.
144150
* @param {string} url Custom loader specifier
145151
* @param {Record<string, unknown>} exports
152+
* @param {any} [data] Arbitrary data to be passed from the custom loader (user-land)
153+
* to the worker.
154+
* @returns {any} The result of the loader's `initialize` hook, if provided.
146155
*/
147-
addCustomLoader(url, exports) {
156+
addCustomLoader(url, exports, data) {
148157
const {
149158
globalPreload,
159+
initialize,
150160
resolve,
151161
load,
152162
} = pluckHooks(exports);
153163

154-
if (globalPreload) {
164+
if (globalPreload && !initialize) {
165+
if (globalPreloadWarned === false) {
166+
globalPreloadWarned = true;
167+
emitExperimentalWarning(
168+
'`globalPreload` will be removed in a future version. Please use `initialize` instead.',
169+
);
170+
}
155171
ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url });
156172
}
157173
if (resolve) {
@@ -162,6 +178,7 @@ class Hooks {
162178
const next = this.#chains.load[this.#chains.load.length - 1];
163179
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
164180
}
181+
return initialize?.(data);
165182
}
166183

167184
/**
@@ -553,15 +570,30 @@ class HooksProxy {
553570
}
554571
}
555572

556-
async makeAsyncRequest(method, ...args) {
573+
/**
574+
* Invoke a remote method asynchronously.
575+
* @param {string} method Method to invoke
576+
* @param {any[]} [transferList] Objects in `args` to be transferred
577+
* @param {any[]} args Arguments to pass to `method`
578+
* @returns {Promise<any>}
579+
*/
580+
async makeAsyncRequest(method, transferList, ...args) {
557581
this.waitForWorker();
558582

559583
MessageChannel ??= require('internal/worker/io').MessageChannel;
560584
const asyncCommChannel = new MessageChannel();
561585

562586
// Pass work to the worker.
563-
debug('post async message to worker', { method, args });
564-
this.#worker.postMessage({ method, args, port: asyncCommChannel.port2 }, [asyncCommChannel.port2]);
587+
debug('post async message to worker', { method, args, transferList });
588+
const finalTransferList = [asyncCommChannel.port2];
589+
if (transferList) {
590+
ArrayPrototypePushApply(finalTransferList, transferList);
591+
}
592+
this.#worker.postMessage({
593+
__proto__: null,
594+
method, args,
595+
port: asyncCommChannel.port2,
596+
}, finalTransferList);
565597

566598
if (this.#numberOfPendingAsyncResponses++ === 0) {
567599
// On the next lines, the main thread will await a response from the worker thread that might
@@ -593,12 +625,19 @@ class HooksProxy {
593625
return body;
594626
}
595627

596-
makeSyncRequest(method, ...args) {
628+
/**
629+
* Invoke a remote method synchronously.
630+
* @param {string} method Method to invoke
631+
* @param {any[]} [transferList] Objects in `args` to be transferred
632+
* @param {any[]} args Arguments to pass to `method`
633+
* @returns {any}
634+
*/
635+
makeSyncRequest(method, transferList, ...args) {
597636
this.waitForWorker();
598637

599638
// Pass work to the worker.
600-
debug('post sync message to worker', { method, args });
601-
this.#worker.postMessage({ method, args });
639+
debug('post sync message to worker', { method, args, transferList });
640+
this.#worker.postMessage({ __proto__: null, method, args }, transferList);
602641

603642
let response;
604643
do {
@@ -708,6 +747,7 @@ ObjectSetPrototypeOf(HooksProxy.prototype, null);
708747
*/
709748
function pluckHooks({
710749
globalPreload,
750+
initialize,
711751
resolve,
712752
load,
713753
}) {
@@ -723,6 +763,10 @@ function pluckHooks({
723763
acceptedHooks.load = load;
724764
}
725765

766+
if (initialize) {
767+
acceptedHooks.initialize = initialize;
768+
}
769+
726770
return acceptedHooks;
727771
}
728772

0 commit comments

Comments
 (0)