From 179a5bca315505ea483794e0faa74cbe8161f185 Mon Sep 17 00:00:00 2001 From: Izaak Schroeder Date: Mon, 10 Jul 2023 16:37:14 -0700 Subject: [PATCH] esm: add `initialize` hook Refs: https://github.com/nodejs/loaders/issues/147 --- lib/internal/modules/esm/hooks.js | 30 ++++++--- lib/internal/modules/esm/loader.js | 22 ++++--- test/es-module/test-esm-loader-hooks.mjs | 61 +++++++++++++++++++ .../hooks-initialize-port.mjs | 17 ++++++ .../es-module-loaders/hooks-initialize.mjs | 3 + 5 files changed, 115 insertions(+), 18 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/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index 8ad0263e49d724..109342e1d99c86 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -122,15 +122,16 @@ class Hooks { * Import and register custom/user-defined module loader hook(s). * @param {string} urlOrSpecifier * @param {string} parentURL + * @param {any} data */ - async register(urlOrSpecifier, parentURL) { + async register(urlOrSpecifier, parentURL, data) { const moduleLoader = require('internal/process/esm_loader').esmLoader; const keyedExports = await moduleLoader.import( urlOrSpecifier, parentURL, kEmptyObject, ); - this.addCustomLoader(urlOrSpecifier, keyedExports); + return await this.addCustomLoader(urlOrSpecifier, keyedExports, data); } allowImportMetaResolve() { @@ -142,12 +143,14 @@ class Hooks { * After all hooks have been collected, the global preload hook(s) must be initialized. * @param {string} url Custom loader specifier * @param {Record} exports + * @param {any} data Arbitrary data to pass to loader's `initialize` */ - addCustomLoader(url, exports) { + async addCustomLoader(url, exports, data) { const { globalPreload, resolve, load, + initialize, } = pluckHooks(exports); if (globalPreload) { @@ -162,6 +165,9 @@ class Hooks { const next = this.#chains.load[this.#chains.load.length - 1]; ArrayPrototypePush(this.#chains.load, { fn: load, url, next }); } + if (initialize) { + return await initialize(data); + } } /** @@ -549,7 +555,7 @@ class HooksProxy { } } - async makeAsyncRequest(method, ...args) { + async makeAsyncRequest(method, args, transferList) { this.#waitForWorker(); MessageChannel ??= require('internal/worker/io').MessageChannel; @@ -557,7 +563,10 @@ class HooksProxy { // Pass work to the worker. debug('post async message to worker', { method, args }); - this.#worker.postMessage({ method, args, port: asyncCommChannel.port2 }, [asyncCommChannel.port2]); + const finalTransferList = transferList ? + [asyncCommChannel.port2, ...transferList] : + [asyncCommChannel.port2] + this.#worker.postMessage({ 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 @@ -589,12 +598,12 @@ class HooksProxy { return body; } - makeSyncRequest(method, ...args) { + makeSyncRequest(method, args, transferList) { this.#waitForWorker(); // Pass work to the worker. - debug('post sync message to worker', { method, args }); - this.#worker.postMessage({ method, args }); + debug('post sync message to worker', { method, args, transferList }); + this.#worker.postMessage({ method, args }, transferList); let response; do { @@ -675,6 +684,7 @@ function pluckHooks({ globalPreload, resolve, load, + initialize, }) { const acceptedHooks = { __proto__: null }; @@ -688,6 +698,10 @@ function pluckHooks({ acceptedHooks.load = load; } + if (initialize) { + acceptedHooks.initialize = initialize; + } + return acceptedHooks; } diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 5f8824a1bce647..b6faf6065b50de 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -66,10 +66,11 @@ class HooksProxyLoaderDelegate { * 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 to the loader * @returns {{ format: string, url: URL['href'] }} */ - register(originalSpecifier, parentURL) { - return hooksProxy.makeSyncRequest('register', originalSpecifier, parentURL); + register(originalSpecifier, parentURL, data, transferList) { + return hooksProxy.makeSyncRequest('register', [originalSpecifier, parentURL, data], transferList); } /** @@ -83,9 +84,9 @@ class HooksProxyLoaderDelegate { */ resolve(originalSpecifier, parentURL, importAssertions) { if (importAssertions && importAssertions[kResolveSync]) { - return hooksProxy.makeSyncRequest('resolve', originalSpecifier, parentURL, importAssertions); + return hooksProxy.makeSyncRequest('resolve', [originalSpecifier, parentURL, importAssertions]); } - return hooksProxy.makeAsyncRequest('resolve', originalSpecifier, parentURL, importAssertions); + return hooksProxy.makeAsyncRequest('resolve', [originalSpecifier, parentURL, importAssertions]); } /** @@ -95,7 +96,7 @@ class HooksProxyLoaderDelegate { * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>} */ load(url, context) { - return hooksProxy.makeAsyncRequest('load', url, context); + return hooksProxy.makeAsyncRequest('load', [url, context]); } } @@ -301,15 +302,15 @@ class ModuleLoader { return module.getNamespace(); } - register(specifier, parentUrl) { + register(specifier, parentUrl, data, transferList) { if (this.#delegate) { - return this.#delegate.register(specifier, parentUrl); + return this.#delegate.register(specifier, parentUrl, data, transferList); } // TODO: Consider flagging this. const flag = true; // getOptionValue('--experimental-loader-register'); if (flag) { this.#delegate = new HooksProxyLoaderDelegate(); - return this.register(specifier, parentUrl); + return this.#delegate.register(specifier, parentUrl, data, transferList); } throw new ERR_ESM_LOADER_REGISTRATION_UNAVAILABLE(); } @@ -420,6 +421,7 @@ function getHooksProxy() { * Register a single loader programmatically. * @param {string} specifier * @param {string} [parentURL] + * @param {any} [data] * @returns {void} * @example * ```js @@ -429,9 +431,9 @@ function getHooksProxy() { * register(new URL('./myLoader.js', import.meta.url)); * ``` */ -function register(specifier, parentURL = 'data:') { +function register(specifier, parentURL = 'data:', data = undefined, transferList = []) { const moduleLoader = require('internal/process/esm_loader').esmLoader; - moduleLoader.register(`${specifier}`, parentURL); + return moduleLoader.register(`${specifier}`, parentURL, data, transferList); } module.exports = { diff --git a/test/es-module/test-esm-loader-hooks.mjs b/test/es-module/test-esm-loader-hooks.mjs index 4cd1e7297c08d7..ec0c519fe5bff9 100644 --- a/test/es-module/test-esm-loader-hooks.mjs +++ b/test/es-module/test-esm-loader-hooks.mjs @@ -451,4 +451,65 @@ describe('Loader hooks', { concurrency: true }, () => { assert.strictEqual(code, 0); assert.strictEqual(signal, null); }); + + it('should invoke `initialize` correctly', async () => { + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('/es-module-loaders/hooks-initialize.mjs'), + '--input-type=module', + '--eval', + ` + import os from 'node:os'; + import fs from 'node:fs'; + `, + ]); + + const lines = stdout.trim().split('\n'); + + assert.strictEqual(lines.length, 1); + assert.strictEqual(lines[0], 'hooks initialize'); + + assert.strictEqual(stderr, ''); + + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + }); + + it('should allow communicating with loader via `register` ports', async () => { + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--no-warnings', + '--input-type=module', + '--eval', + ` + import {MessageChannel} from 'node:worker_threads'; + import {register} from 'node:module'; + const {port1, port2} = new MessageChannel(); + port1.on('message', (msg) => { + console.log('message', msg); + }); + const result = register( + ${JSON.stringify(fixtures.fileURL('/es-module-loaders/hooks-initialize-port.mjs'))}, + 'data:', + port2, + [port2], + ); + console.log('register', result); + + await import('node:os'); + port1.close(); + `, + ]); + + const lines = stdout.split('\n'); + + assert.strictEqual(lines[0], 'register ok'); + assert.strictEqual(lines[1], 'message initialize'); + assert.strictEqual(lines[2], 'message resolve node:os'); + + assert.strictEqual(stderr, ''); + + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + }); }); diff --git a/test/fixtures/es-module-loaders/hooks-initialize-port.mjs b/test/fixtures/es-module-loaders/hooks-initialize-port.mjs new file mode 100644 index 00000000000000..c522e3fa8bfd98 --- /dev/null +++ b/test/fixtures/es-module-loaders/hooks-initialize-port.mjs @@ -0,0 +1,17 @@ +let thePort = null; + +export async function initialize(port) { + port.postMessage('initialize'); + thePort = port; + return 'ok'; +} + +export async function resolve(specifier, context, next) { + if (specifier === 'node:fs' || specifier.includes('loader')) { + return next(specifier); + } + + thePort.postMessage(`resolve ${specifier}`); + + return next(specifier); +} diff --git a/test/fixtures/es-module-loaders/hooks-initialize.mjs b/test/fixtures/es-module-loaders/hooks-initialize.mjs new file mode 100644 index 00000000000000..bacf95a05700c0 --- /dev/null +++ b/test/fixtures/es-module-loaders/hooks-initialize.mjs @@ -0,0 +1,3 @@ +export async function initialize() { + console.log('hooks initialize'); +} \ No newline at end of file