Skip to content

Commit 10e7c3a

Browse files
izaakschroedertargos
authored andcommittedNov 23, 2023
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 Backport-PR-URL: #50669 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent f96b610 commit 10e7c3a

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
@@ -684,6 +684,9 @@ of Node.js applications.
684684
<!-- YAML
685685
added: v8.8.0
686686
changes:
687+
- version: REPLACEME
688+
pr-url: https://github.com/nodejs/node/pull/48842
689+
description: Added `initialize` hook to replace `globalPreload`.
687690
- version:
688691
- v18.6.0
689692
pr-url: https://github.com/nodejs/node/pull/42623
@@ -737,6 +740,69 @@ different [realm](https://tc39.es/ecma262/#realm). The hooks thread may be
737740
terminated by the main thread at any time, so do not depend on asynchronous
738741
operations to (like `console.log`) complete.
739742
743+
#### `initialize()`
744+
745+
<!-- YAML
746+
added: REPLACEME
747+
-->
748+
749+
> The loaders API is being redesigned. This hook may disappear or its
750+
> signature may change. Do not rely on the API described below.
751+
752+
* `data` {any} The data from `register(loader, import.meta.url, { data })`.
753+
* Returns: {any} The data to be returned to the caller of `register`.
754+
755+
The `initialize` hook provides a way to define a custom function that runs
756+
in the loader's thread when the loader is initialized. Initialization happens
757+
when the loader is registered via [`register`][] or registered via the
758+
`--loader` command line option.
759+
760+
This hook can send and receive data from a [`register`][] invocation, including
761+
ports and other transferrable objects. The return value of `initialize` must be
762+
either:
763+
764+
* `undefined`,
765+
* something that can be posted as a message between threads (e.g. the input to
766+
[`port.postMessage`][]),
767+
* a `Promise` resolving to one of the aforementioned values.
768+
769+
Loader code:
770+
771+
```js
772+
// In the below example this file is referenced as
773+
// '/path-to-my-loader.js'
774+
775+
export async function initialize({ number, port }) {
776+
port.postMessage(`increment: ${number + 1}`);
777+
return 'ok';
778+
}
779+
```
780+
781+
Caller code:
782+
783+
```js
784+
import assert from 'node:assert';
785+
import { register } from 'node:module';
786+
import { MessageChannel } from 'node:worker_threads';
787+
788+
// This example showcases how a message channel can be used to
789+
// communicate between the main (application) thread and the loader
790+
// running on the loaders thread, by sending `port2` to the loader.
791+
const { port1, port2 } = new MessageChannel();
792+
793+
port1.on('message', (msg) => {
794+
assert.strictEqual(msg, 'increment: 2');
795+
});
796+
797+
const result = register('/path-to-my-loader.js', {
798+
parentURL: import.meta.url,
799+
data: { number: 1, port: port2 },
800+
transferList: [port2],
801+
});
802+
803+
assert.strictEqual(result, 'ok');
804+
```
805+
740806
#### `resolve(specifier, context, nextResolve)`
741807
742808
<!-- YAML
@@ -941,8 +1007,8 @@ changes:
9411007
description: Add support for chaining globalPreload hooks.
9421008
-->
9431009
944-
> The loaders API is being redesigned. This hook may disappear or its
945-
> signature may change. Do not rely on the API described below.
1010+
> This hook will be removed in a future version. Use [`initialize`][] instead.
1011+
> When a loader has an `initialize` export, `globalPreload` will be ignored.
9461012
9471013
> In a previous version of this API, this hook was named
9481014
> `getGlobalPreloadCode`.
@@ -1642,13 +1708,16 @@ success!
16421708
[`import.meta.resolve`]: #importmetaresolvespecifier-parent
16431709
[`import.meta.url`]: #importmetaurl
16441710
[`import`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
1711+
[`initialize`]: #initialize
16451712
[`module.createRequire()`]: module.md#modulecreaterequirefilename
16461713
[`module.register()`]: module.md#moduleregister
16471714
[`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports
16481715
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
1716+
[`port.postMessage`]: worker_threads.md#portpostmessagevalue-transferlist
16491717
[`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref
16501718
[`port.unref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portunref
16511719
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
1720+
[`register`]: module.md#moduleregister
16521721
[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
16531722
[`util.TextDecoder`]: util.md#class-utiltextdecoder
16541723
[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
@@ -173,6 +173,28 @@ globalPreload: http-to-https
173173
globalPreload: unpkg
174174
```
175175
176+
This function can also be used to pass data to the loader's [`initialize`][]
177+
hook; the data passed to the hook may include transferrable objects like ports.
178+
179+
```mjs
180+
import { register } from 'node:module';
181+
import { MessageChannel } from 'node:worker_threads';
182+
183+
// This example showcases how a message channel can be used to
184+
// communicate to the loader, by sending `port2` to the loader.
185+
const { port1, port2 } = new MessageChannel();
186+
187+
port1.on('message', (msg) => {
188+
console.log(msg);
189+
});
190+
191+
register('./my-programmatic-loader.mjs', {
192+
parentURL: import.meta.url,
193+
data: { number: 1, port: port2 },
194+
transferList: [port2],
195+
});
196+
```
197+
176198
### `module.syncBuiltinESMExports()`
177199
178200
<!-- YAML
@@ -358,6 +380,7 @@ returned object contains the following keys:
358380
[`--enable-source-maps`]: cli.md#--enable-source-maps
359381
[`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir
360382
[`SourceMap`]: #class-modulesourcemap
383+
[`initialize`]: esm.md#initialize
361384
[`module`]: modules.md#the-module-object
362385
[module wrapper]: modules.md#the-module-wrapper
363386
[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,
@@ -46,8 +47,10 @@ const {
4647
validateObject,
4748
validateString,
4849
} = require('internal/validators');
49-
50-
const { kEmptyObject } = require('internal/util');
50+
const {
51+
emitExperimentalWarning,
52+
kEmptyObject,
53+
} = require('internal/util');
5154

5255
const {
5356
defaultResolve,
@@ -82,6 +85,7 @@ let importMetaInitializer;
8285

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

88+
let globalPreloadWarned = false;
8589
class Hooks {
8690
#chains = {
8791
/**
@@ -126,31 +130,43 @@ class Hooks {
126130
* Import and register custom/user-defined module loader hook(s).
127131
* @param {string} urlOrSpecifier
128132
* @param {string} parentURL
133+
* @param {any} [data] Arbitrary data to be passed from the custom
134+
* loader (user-land) to the worker.
129135
*/
130-
async register(urlOrSpecifier, parentURL) {
136+
async register(urlOrSpecifier, parentURL, data) {
131137
const moduleLoader = require('internal/process/esm_loader').esmLoader;
132138
const keyedExports = await moduleLoader.import(
133139
urlOrSpecifier,
134140
parentURL,
135141
kEmptyObject,
136142
);
137-
this.addCustomLoader(urlOrSpecifier, keyedExports);
143+
return this.addCustomLoader(urlOrSpecifier, keyedExports, data);
138144
}
139145

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

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

166183
/**
@@ -552,15 +569,30 @@ class HooksProxy {
552569
}
553570
}
554571

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

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

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

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

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

598637
// Pass work to the worker.
599-
debug('post sync message to worker', { method, args });
600-
this.#worker.postMessage({ method, args });
638+
debug('post sync message to worker', { method, args, transferList });
639+
this.#worker.postMessage({ __proto__: null, method, args }, transferList);
601640

602641
let response;
603642
do {
@@ -707,6 +746,7 @@ ObjectSetPrototypeOf(HooksProxy.prototype, null);
707746
*/
708747
function pluckHooks({
709748
globalPreload,
749+
initialize,
710750
resolve,
711751
load,
712752
}) {
@@ -722,6 +762,10 @@ function pluckHooks({
722762
acceptedHooks.load = load;
723763
}
724764

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

‎lib/internal/modules/esm/loader.js

+42-11
Original file line numberDiff line numberDiff line change
@@ -318,15 +318,18 @@ class ModuleLoader {
318318
return module.getNamespace();
319319
}
320320

321-
register(specifier, parentURL) {
321+
/**
322+
* @see {@link CustomizedModuleLoader.register}
323+
*/
324+
register(specifier, parentURL, data, transferList) {
322325
if (!this.#customizations) {
323326
// `CustomizedModuleLoader` is defined at the bottom of this file and
324327
// available well before this line is ever invoked. This is here in
325328
// order to preserve the git diff instead of moving the class.
326329
// eslint-disable-next-line no-use-before-define
327330
this.setCustomizations(new CustomizedModuleLoader());
328331
}
329-
return this.#customizations.register(specifier, parentURL);
332+
return this.#customizations.register(specifier, parentURL, data, transferList);
330333
}
331334

332335
/**
@@ -437,10 +440,13 @@ class CustomizedModuleLoader {
437440
* be registered.
438441
* @param {string} parentURL The parent URL from where the loader will be
439442
* registered if using it package name as specifier
443+
* @param {any} [data] Arbitrary data to be passed from the custom loader
444+
* (user-land) to the worker.
445+
* @param {any[]} [transferList] Objects in `data` that are changing ownership
440446
* @returns {{ format: string, url: URL['href'] }}
441447
*/
442-
register(originalSpecifier, parentURL) {
443-
return hooksProxy.makeSyncRequest('register', originalSpecifier, parentURL);
448+
register(originalSpecifier, parentURL, data, transferList) {
449+
return hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data);
444450
}
445451

446452
/**
@@ -453,12 +459,12 @@ class CustomizedModuleLoader {
453459
* @returns {{ format: string, url: URL['href'] }}
454460
*/
455461
resolve(originalSpecifier, parentURL, importAssertions) {
456-
return hooksProxy.makeAsyncRequest('resolve', originalSpecifier, parentURL, importAssertions);
462+
return hooksProxy.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAssertions);
457463
}
458464

459465
resolveSync(originalSpecifier, parentURL, importAssertions) {
460466
// This happens only as a result of `import.meta.resolve` calls, which must be sync per spec.
461-
return hooksProxy.makeSyncRequest('resolve', originalSpecifier, parentURL, importAssertions);
467+
return hooksProxy.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAssertions);
462468
}
463469

464470
/**
@@ -468,7 +474,7 @@ class CustomizedModuleLoader {
468474
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
469475
*/
470476
load(url, context) {
471-
return hooksProxy.makeAsyncRequest('load', url, context);
477+
return hooksProxy.makeAsyncRequest('load', undefined, url, context);
472478
}
473479

474480
importMetaInitialize(meta, context, loader) {
@@ -524,19 +530,44 @@ function getHooksProxy() {
524530
/**
525531
* Register a single loader programmatically.
526532
* @param {string} specifier
527-
* @param {string} [parentURL]
528-
* @returns {void}
533+
* @param {string} [parentURL] Base to use when resolving `specifier`; optional if
534+
* `specifier` is absolute. Same as `options.parentUrl`, just inline
535+
* @param {object} [options] Additional options to apply, described below.
536+
* @param {string} [options.parentURL] Base to use when resolving `specifier`
537+
* @param {any} [options.data] Arbitrary data passed to the loader's `initialize` hook
538+
* @param {any[]} [options.transferList] Objects in `data` that are changing ownership
539+
* @returns {any} The result of the loader's initialize hook, if any
529540
* @example
530541
* ```js
531542
* register('./myLoader.js');
543+
* register('ts-node/esm', { parentURL: import.meta.url });
544+
* register('./myLoader.js', { parentURL: import.meta.url });
532545
* register('ts-node/esm', import.meta.url);
533546
* register('./myLoader.js', import.meta.url);
534547
* register(new URL('./myLoader.js', import.meta.url));
548+
* register('./myLoader.js', {
549+
* parentURL: import.meta.url,
550+
* data: { banana: 'tasty' },
551+
* });
552+
* register('./myLoader.js', {
553+
* parentURL: import.meta.url,
554+
* data: someArrayBuffer,
555+
* transferList: [someArrayBuffer],
556+
* });
535557
* ```
536558
*/
537-
function register(specifier, parentURL = 'data:') {
559+
function register(specifier, parentURL = undefined, options) {
538560
const moduleLoader = require('internal/process/esm_loader').esmLoader;
539-
moduleLoader.register(`${specifier}`, parentURL);
561+
if (parentURL != null && typeof parentURL === 'object') {
562+
options = parentURL;
563+
parentURL = options.parentURL;
564+
}
565+
return moduleLoader.register(
566+
`${specifier}`,
567+
parentURL ?? 'data:',
568+
options?.data,
569+
options?.transferList,
570+
);
540571
}
541572

542573
module.exports = {

‎lib/internal/modules/esm/utils.js

+14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const {
1212
ERR_INVALID_ARG_VALUE,
1313
} = require('internal/errors').codes;
1414
const { getOptionValue } = require('internal/options');
15+
const {
16+
loadPreloadModules,
17+
initializeFrozenIntrinsics,
18+
} = require('internal/process/pre_execution');
1519
const { pathToFileURL } = require('internal/url');
1620
const {
1721
setImportModuleDynamicallyCallback,
@@ -123,6 +127,16 @@ async function initializeHooks() {
123127
const hooks = new Hooks();
124128
esmLoader.setCustomizations(hooks);
125129

130+
// We need the loader customizations to be set _before_ we start invoking
131+
// `--require`, otherwise loops can happen because a `--require` script
132+
// might call `register(...)` before we've installed ourselves. These
133+
// global values are magically set in `setupUserModules` just for us and
134+
// we call them in the correct order.
135+
// N.B. This block appears here specifically in order to ensure that
136+
// `--require` calls occur before `--loader` ones do.
137+
loadPreloadModules();
138+
initializeFrozenIntrinsics();
139+
126140
const parentURL = pathToFileURL(cwd).href;
127141
for (let i = 0; i < customLoaderURLs.length; i++) {
128142
await hooks.register(

‎lib/internal/process/pre_execution.js

+6
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ function setupUserModules(isLoaderWorker = false) {
128128
initializeESMLoader(isLoaderWorker);
129129
const CJSLoader = require('internal/modules/cjs/loader');
130130
assert(!CJSLoader.hasLoadedAnyUserCJSModule);
131+
// Loader workers are responsible for doing this themselves.
132+
if (isLoaderWorker) {
133+
return;
134+
}
131135
loadPreloadModules();
132136
// Need to be done after --require setup.
133137
initializeFrozenIntrinsics();
@@ -600,4 +604,6 @@ module.exports = {
600604
prepareMainThreadExecution,
601605
prepareWorkerThreadExecution,
602606
markBootstrapComplete,
607+
loadPreloadModules,
608+
initializeFrozenIntrinsics,
603609
};

‎test/es-module/test-esm-loader-hooks.mjs

+170
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,16 @@ describe('Loader hooks', { concurrency: true }, () => {
372372
});
373373

374374
describe('globalPreload', () => {
375+
it('should emit deprecation warning', async () => {
376+
const { stderr } = await spawnPromisified(execPath, [
377+
'--experimental-loader',
378+
'data:text/javascript,export function globalPreload(){}',
379+
fixtures.path('empty.js'),
380+
]);
381+
382+
assert.match(stderr, /`globalPreload` will be removed/);
383+
});
384+
375385
it('should handle globalPreload returning undefined', async () => {
376386
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
377387
'--no-warnings',
@@ -501,4 +511,164 @@ describe('Loader hooks', { concurrency: true }, () => {
501511
assert.strictEqual(code, 0);
502512
assert.strictEqual(signal, null);
503513
});
514+
515+
it('should invoke `initialize` correctly', async () => {
516+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
517+
'--no-warnings',
518+
'--experimental-loader',
519+
fixtures.fileURL('/es-module-loaders/hooks-initialize.mjs'),
520+
'--input-type=module',
521+
'--eval',
522+
'import os from "node:os";',
523+
]);
524+
525+
const lines = stdout.trim().split('\n');
526+
527+
assert.strictEqual(lines.length, 1);
528+
assert.strictEqual(lines[0], 'hooks initialize 1');
529+
530+
assert.strictEqual(stderr, '');
531+
532+
assert.strictEqual(code, 0);
533+
assert.strictEqual(signal, null);
534+
});
535+
536+
it('should allow communicating with loader via `register` ports', async () => {
537+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
538+
'--no-warnings',
539+
'--input-type=module',
540+
'--eval',
541+
`
542+
import {MessageChannel} from 'node:worker_threads';
543+
import {register} from 'node:module';
544+
const {port1, port2} = new MessageChannel();
545+
port1.on('message', (msg) => {
546+
console.log('message', msg);
547+
});
548+
const result = register(
549+
${JSON.stringify(fixtures.fileURL('/es-module-loaders/hooks-initialize-port.mjs'))},
550+
{data: port2, transferList: [port2]},
551+
);
552+
console.log('register', result);
553+
554+
await import('node:os');
555+
port1.close();
556+
`,
557+
]);
558+
559+
const lines = stdout.split('\n');
560+
561+
assert.strictEqual(lines[0], 'register ok');
562+
assert.strictEqual(lines[1], 'message initialize');
563+
assert.strictEqual(lines[2], 'message resolve node:os');
564+
565+
assert.strictEqual(stderr, '');
566+
567+
assert.strictEqual(code, 0);
568+
assert.strictEqual(signal, null);
569+
});
570+
571+
it('should have `register` work with cjs', async () => {
572+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
573+
'--no-warnings',
574+
'--input-type=commonjs',
575+
'--eval',
576+
`
577+
const {register} = require('node:module');
578+
register(
579+
${JSON.stringify(fixtures.fileURL('/es-module-loaders/hooks-initialize.mjs'))},
580+
);
581+
register(
582+
${JSON.stringify(fixtures.fileURL('/es-module-loaders/loader-load-foo-or-42.mjs'))},
583+
);
584+
585+
import('node:os').then((result) => {
586+
console.log(result.default);
587+
});
588+
`,
589+
]);
590+
591+
const lines = stdout.split('\n');
592+
593+
assert.strictEqual(lines[0], 'hooks initialize 1');
594+
assert.strictEqual(lines[1], 'foo');
595+
596+
assert.strictEqual(stderr, '');
597+
598+
assert.strictEqual(code, 0);
599+
assert.strictEqual(signal, null);
600+
});
601+
602+
it('`register` should work with `require`', async () => {
603+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
604+
'--no-warnings',
605+
'--require',
606+
fixtures.path('/es-module-loaders/register-loader.cjs'),
607+
'--input-type=module',
608+
'--eval',
609+
'import "node:os";',
610+
]);
611+
612+
const lines = stdout.split('\n');
613+
614+
assert.strictEqual(lines[0], 'resolve passthru');
615+
616+
assert.strictEqual(stderr, '');
617+
618+
assert.strictEqual(code, 0);
619+
assert.strictEqual(signal, null);
620+
});
621+
622+
it('`register` should work with `import`', async () => {
623+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
624+
'--no-warnings',
625+
'--import',
626+
fixtures.fileURL('/es-module-loaders/register-loader.mjs'),
627+
'--input-type=module',
628+
'--eval',
629+
`
630+
import 'node:os';
631+
`,
632+
]);
633+
634+
const lines = stdout.split('\n');
635+
636+
assert.strictEqual(lines[0], 'resolve passthru');
637+
638+
assert.strictEqual(stderr, '');
639+
640+
assert.strictEqual(code, 0);
641+
assert.strictEqual(signal, null);
642+
});
643+
644+
it('should execute `initialize` in sequence', async () => {
645+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
646+
'--no-warnings',
647+
'--input-type=module',
648+
'--eval',
649+
`
650+
import {register} from 'node:module';
651+
console.log('result', register(
652+
${JSON.stringify(fixtures.fileURL('/es-module-loaders/hooks-initialize.mjs'))}
653+
));
654+
console.log('result', register(
655+
${JSON.stringify(fixtures.fileURL('/es-module-loaders/hooks-initialize.mjs'))}
656+
));
657+
658+
await import('node:os');
659+
`,
660+
]);
661+
662+
const lines = stdout.split('\n');
663+
664+
assert.strictEqual(lines[0], 'result 1');
665+
assert.strictEqual(lines[1], 'result 2');
666+
assert.strictEqual(lines[2], 'hooks initialize 1');
667+
assert.strictEqual(lines[3], 'hooks initialize 2');
668+
669+
assert.strictEqual(stderr, '');
670+
671+
assert.strictEqual(code, 0);
672+
assert.strictEqual(signal, null);
673+
});
504674
});

‎test/es-module/test-esm-loader-programmatically.mjs

+8-3
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ const commonEvals = {
2222

2323
describe('ESM: programmatically register loaders', { concurrency: true }, () => {
2424
it('works with only a dummy CLI argument', async () => {
25+
const parentURL = fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs');
2526
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
2627
...commonArgs,
2728
'--eval',
2829
"import { register } from 'node:module';" +
2930
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs')) +
3031
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) +
32+
`register(${JSON.stringify('./loader-resolve-passthru.mjs')}, ${JSON.stringify({ parentURL })});` +
33+
`register(${JSON.stringify('./loader-load-passthru.mjs')}, ${JSON.stringify({ parentURL })});` +
3134
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
3235
]);
3336

@@ -38,10 +41,12 @@ describe('ESM: programmatically register loaders', { concurrency: true }, () =>
3841
const lines = stdout.split('\n');
3942

4043
assert.match(lines[0], /resolve passthru/);
41-
assert.match(lines[1], /load passthru/);
42-
assert.match(lines[2], /Hello from dynamic import/);
44+
assert.match(lines[1], /resolve passthru/);
45+
assert.match(lines[2], /load passthru/);
46+
assert.match(lines[3], /load passthru/);
47+
assert.match(lines[4], /Hello from dynamic import/);
4348

44-
assert.strictEqual(lines[3], '');
49+
assert.strictEqual(lines[5], '');
4550
});
4651

4752
describe('registering via --import', { concurrency: true }, () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
let thePort = null;
2+
3+
export async function initialize(port) {
4+
port.postMessage('initialize');
5+
thePort = port;
6+
return 'ok';
7+
}
8+
9+
export async function resolve(specifier, context, next) {
10+
if (specifier === 'node:fs' || specifier.includes('loader')) {
11+
return next(specifier);
12+
}
13+
14+
thePort.postMessage(`resolve ${specifier}`);
15+
16+
return next(specifier);
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
let counter = 0;
2+
3+
export async function initialize() {
4+
counter += 1;
5+
console.log('hooks initialize', counter);
6+
return counter;
7+
}

0 commit comments

Comments
 (0)
Please sign in to comment.