Skip to content

Commit

Permalink
esm: add initialize hook
Browse files Browse the repository at this point in the history
  • Loading branch information
izaakschroeder committed Jul 10, 2023
1 parent cb657fb commit 179a5bc
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 18 deletions.
30 changes: 22 additions & 8 deletions lib/internal/modules/esm/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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<string, unknown>} 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) {
Expand All @@ -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);
}
}

/**
Expand Down Expand Up @@ -549,15 +555,18 @@ class HooksProxy {
}
}

async makeAsyncRequest(method, ...args) {
async makeAsyncRequest(method, args, transferList) {
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 });
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
Expand Down Expand Up @@ -589,12 +598,12 @@ class HooksProxy {
return body;
}

makeSyncRequest(method, ...args) {

This comment has been minimized.

Copy link
@izaakschroeder

izaakschroeder Jul 10, 2023

Author Owner

I'm not sure if the signature (method, transferList, ...args) is better… or this? And if there is any performance difference from ... vs just passing an array.

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 {
Expand Down Expand Up @@ -675,6 +684,7 @@ function pluckHooks({
globalPreload,
resolve,
load,
initialize,
}) {
const acceptedHooks = { __proto__: null };

Expand All @@ -688,6 +698,10 @@ function pluckHooks({
acceptedHooks.load = load;
}

if (initialize) {
acceptedHooks.initialize = initialize;
}

return acceptedHooks;
}

Expand Down
22 changes: 12 additions & 10 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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]);
}

/**
Expand All @@ -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]);
}
}

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -420,6 +421,7 @@ function getHooksProxy() {
* Register a single loader programmatically.
* @param {string} specifier
* @param {string} [parentURL]
* @param {any} [data]
* @returns {void}
* @example
* ```js
Expand All @@ -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 = {
Expand Down
61 changes: 61 additions & 0 deletions test/es-module/test-esm-loader-hooks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
17 changes: 17 additions & 0 deletions test/fixtures/es-module-loaders/hooks-initialize-port.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions test/fixtures/es-module-loaders/hooks-initialize.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function initialize() {
console.log('hooks initialize');
}

0 comments on commit 179a5bc

Please sign in to comment.