From 220bea68a53e822b94b7f3b0184a13d1651c8211 Mon Sep 17 00:00:00 2001 From: Izaak Schroeder Date: Wed, 2 Aug 2023 22:10:59 -0700 Subject: [PATCH] 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; ``` **NOTE**: Currently there is no mechanism for a loader to exchange ownership of something back to the caller. Refs: https://github.com/nodejs/loaders/issues/147 PR-URL: https://github.com/nodejs/node/pull/48842 Reviewed-By: Antoine du Hamel Reviewed-By: Matteo Collina Reviewed-By: Geoffrey Booth --- doc/api/esm.md | 73 +++++++- doc/api/module.md | 23 +++ lib/internal/modules/esm/hooks.js | 68 +++++-- lib/internal/modules/esm/loader.js | 53 ++++-- lib/internal/modules/esm/utils.js | 14 ++ lib/internal/process/pre_execution.js | 6 + test/es-module/test-esm-loader-hooks.mjs | 170 ++++++++++++++++++ .../test-esm-loader-programmatically.mjs | 11 +- .../hooks-initialize-port.mjs | 17 ++ .../es-module-loaders/hooks-initialize.mjs | 7 + 10 files changed, 414 insertions(+), 28 deletions(-) create mode 100644 test/fixtures/es-module-loaders/hooks-initialize-port.mjs create mode 100644 test/fixtures/es-module-loaders/hooks-initialize.mjs diff --git a/doc/api/esm.md b/doc/api/esm.md index 68ecad93b8ad3b..6d510d0d0b00b5 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -685,6 +685,9 @@ of Node.js applications. + +> The loaders API is being redesigned. This hook may disappear or its +> signature may change. Do not rely on the API described below. + +* `data` {any} The data from `register(loader, import.meta.url, { data })`. +* Returns: {any} The data to be returned to the caller of `register`. + +The `initialize` hook provides a way to define a custom function that runs +in the loader's thread when the loader is initialized. Initialization happens +when the loader is registered via [`register`][] or registered via the +`--loader` command line option. + +This hook can send and receive data from a [`register`][] invocation, including +ports and other transferrable objects. The return value of `initialize` must be +either: + +* `undefined`, +* something that can be posted as a message between threads (e.g. the input to + [`port.postMessage`][]), +* a `Promise` resolving to one of the aforementioned values. + +Loader code: + +```js +// In the below example this file is referenced as +// '/path-to-my-loader.js' + +export async function initialize({ number, port }) { + port.postMessage(`increment: ${number + 1}`); + return 'ok'; +} +``` + +Caller code: + +```js +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 loader +// running on the loaders thread, by sending `port2` to the loader. +const { port1, port2 } = new MessageChannel(); + +port1.on('message', (msg) => { + assert.strictEqual(msg, 'increment: 2'); +}); + +const result = register('/path-to-my-loader.js', { + parentURL: import.meta.url, + data: { number: 1, port: port2 }, + transferList: [port2], +}); + +assert.strictEqual(result, 'ok'); +``` + #### `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. +> This hook will be removed in a future version. Use [`initialize`][] instead. +> When a loader has an `initialize` export, `globalPreload` will be ignored. > In a previous version of this API, this hook was named > `getGlobalPreloadCode`. @@ -1609,13 +1675,16 @@ for ESM specifiers is [commonjs-extension-resolution-loader][]. [`import.meta.resolve`]: #importmetaresolvespecifier-parent [`import.meta.url`]: #importmetaurl [`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import +[`initialize`]: #initialize [`module.createRequire()`]: module.md#modulecreaterequirefilename [`module.register()`]: module.md#moduleregister [`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports [`package.json`]: packages.md#nodejs-packagejson-field-definitions +[`port.postMessage`]: worker_threads.md#portpostmessagevalue-transferlist [`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 +[`register`]: module.md#moduleregister [`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 diff --git a/doc/api/module.md b/doc/api/module.md index 8dd5fd4fa59f6c..04b4d00c04b372 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -175,6 +175,28 @@ globalPreload: http-to-https globalPreload: unpkg ``` +This function can also be used to pass data to the loader's [`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 showcases how a message channel can be used to +// communicate to the loader, by sending `port2` to the loader. +const { port1, port2 } = new MessageChannel(); + +port1.on('message', (msg) => { + console.log(msg); +}); + +register('./my-programmatic-loader.mjs', { + parentURL: import.meta.url, + data: { number: 1, port: port2 }, + transferList: [port2], +}); +``` + ### `module.syncBuiltinESMExports()`