Skip to content

Commit e6af970

Browse files
bmeckdanielleadams
authored andcommitted
esm: working mock test
PR-URL: #39240 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
1 parent d19bb65 commit e6af970

File tree

8 files changed

+425
-36
lines changed

8 files changed

+425
-36
lines changed

doc/api/esm.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,9 @@ its own `require` using `module.createRequire()`.
784784

785785
```js
786786
/**
787+
* @param {{
788+
port: MessagePort,
789+
}} utilities Things that preload code might find useful
787790
* @returns {string} Code to run before application startup
788791
*/
789792
export function globalPreload() {
@@ -800,6 +803,35 @@ const require = createRequire(cwd() + '/<preload>');
800803
}
801804
```
802805
806+
In order to allow communication between the application and the loader, another
807+
argument is provided to the preload code: `port`. This is available as a
808+
parameter to the loader hook and inside of the source text returned by the hook.
809+
Some care must be taken in order to properly call [`port.ref()`][] and
810+
[`port.unref()`][] to prevent a process from being in a state where it won't
811+
close normally.
812+
813+
```js
814+
/**
815+
* This example has the application context send a message to the loader
816+
* and sends the message back to the application context
817+
* @param {{
818+
port: MessagePort,
819+
}} utilities Things that preload code might find useful
820+
* @returns {string} Code to run before application startup
821+
*/
822+
export function globalPreload({ port }) {
823+
port.onmessage = (evt) => {
824+
port.postMessage(evt.data);
825+
};
826+
return `\
827+
port.postMessage('console.log("I went to the Loader and back");');
828+
port.onmessage = (evt) => {
829+
eval(evt.data);
830+
};
831+
`;
832+
}
833+
```
834+
803835
### Examples
804836
805837
The various loader hooks can be used together to accomplish wide-ranging
@@ -1392,6 +1424,8 @@ success!
13921424
[`module.createRequire()`]: module.md#modulecreaterequirefilename
13931425
[`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports
13941426
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
1427+
[`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref
1428+
[`port.unref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portunref
13951429
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
13961430
[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
13971431
[`util.TextDecoder`]: util.md#class-utiltextdecoder
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
3+
const { getOptionValue } = require('internal/options');
4+
const experimentalImportMetaResolve =
5+
getOptionValue('--experimental-import-meta-resolve');
6+
const { PromisePrototypeThen, PromiseReject } = primordials;
7+
const asyncESM = require('internal/process/esm_loader');
8+
9+
function createImportMetaResolve(defaultParentUrl) {
10+
return async function resolve(specifier, parentUrl = defaultParentUrl) {
11+
return PromisePrototypeThen(
12+
asyncESM.esmLoader.resolve(specifier, parentUrl),
13+
({ url }) => url,
14+
(error) => (
15+
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ?
16+
error.url : PromiseReject(error))
17+
);
18+
};
19+
}
20+
21+
function initializeImportMeta(meta, context) {
22+
const url = context.url;
23+
24+
// Alphabetical
25+
if (experimentalImportMetaResolve)
26+
meta.resolve = createImportMetaResolve(url);
27+
meta.url = url;
28+
}
29+
30+
module.exports = {
31+
initializeImportMeta
32+
};

lib/internal/modules/esm/loader.js

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const {
1818
SafeWeakMap,
1919
globalThis,
2020
} = primordials;
21+
const { MessageChannel } = require('internal/worker/io');
2122

2223
const {
2324
ERR_INVALID_ARG_TYPE,
@@ -39,6 +40,9 @@ const {
3940
defaultResolve,
4041
DEFAULT_CONDITIONS,
4142
} = require('internal/modules/esm/resolve');
43+
const {
44+
initializeImportMeta
45+
} = require('internal/modules/esm/initialize_import_meta');
4246
const { defaultLoad } = require('internal/modules/esm/load');
4347
const { translators } = require(
4448
'internal/modules/esm/translators');
@@ -76,6 +80,8 @@ class ESMLoader {
7680
defaultResolve,
7781
];
7882

83+
#importMetaInitializer = initializeImportMeta;
84+
7985
/**
8086
* Map of already-loaded CJS modules to use
8187
*/
@@ -359,7 +365,18 @@ class ESMLoader {
359365
if (!count) return;
360366

361367
for (let i = 0; i < count; i++) {
362-
const preload = this.#globalPreloaders[i]();
368+
const channel = new MessageChannel();
369+
const {
370+
port1: insidePreload,
371+
port2: insideLoader,
372+
} = channel;
373+
374+
insidePreload.unref();
375+
insideLoader.unref();
376+
377+
const preload = this.#globalPreloaders[i]({
378+
port: insideLoader
379+
});
363380

364381
if (preload == null) return;
365382

@@ -373,22 +390,60 @@ class ESMLoader {
373390
const { compileFunction } = require('vm');
374391
const preloadInit = compileFunction(
375392
preload,
376-
['getBuiltin'],
393+
['getBuiltin', 'port', 'setImportMetaCallback'],
377394
{
378395
filename: '<preload>',
379396
}
380397
);
381398
const { NativeModule } = require('internal/bootstrap/loaders');
382-
383-
FunctionPrototypeCall(preloadInit, globalThis, (builtinName) => {
384-
if (NativeModule.canBeRequiredByUsers(builtinName)) {
385-
return require(builtinName);
399+
// We only allow replacing the importMetaInitializer during preload,
400+
// after preload is finished, we disable the ability to replace it
401+
//
402+
// This exposes accidentally setting the initializer too late by
403+
// throwing an error.
404+
let finished = false;
405+
let replacedImportMetaInitializer = false;
406+
let next = this.#importMetaInitializer;
407+
try {
408+
// Calls the compiled preload source text gotten from the hook
409+
// Since the parameters are named we use positional parameters
410+
// see compileFunction above to cross reference the names
411+
FunctionPrototypeCall(
412+
preloadInit,
413+
globalThis,
414+
// Param getBuiltin
415+
(builtinName) => {
416+
if (NativeModule.canBeRequiredByUsers(builtinName)) {
417+
return require(builtinName);
418+
}
419+
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
420+
},
421+
// Param port
422+
insidePreload,
423+
// Param setImportMetaCallback
424+
(fn) => {
425+
if (finished || typeof fn !== 'function') {
426+
throw new ERR_INVALID_ARG_TYPE('fn', fn);
427+
}
428+
replacedImportMetaInitializer = true;
429+
const parent = next;
430+
next = (meta, context) => {
431+
return fn(meta, context, parent);
432+
};
433+
});
434+
} finally {
435+
finished = true;
436+
if (replacedImportMetaInitializer) {
437+
this.#importMetaInitializer = next;
386438
}
387-
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
388-
});
439+
}
389440
}
390441
}
391442

443+
importMetaInitialize(meta, context) {
444+
this.#importMetaInitializer(meta, context);
445+
}
446+
392447
/**
393448
* Resolve the location of the module.
394449
*

lib/internal/modules/esm/translators.js

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ const {
88
ObjectGetPrototypeOf,
99
ObjectPrototypeHasOwnProperty,
1010
ObjectKeys,
11-
PromisePrototypeThen,
12-
PromiseReject,
1311
SafeArrayIterator,
1412
SafeMap,
1513
SafeSet,
@@ -52,9 +50,6 @@ const {
5250
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
5351
const moduleWrap = internalBinding('module_wrap');
5452
const { ModuleWrap } = moduleWrap;
55-
const { getOptionValue } = require('internal/options');
56-
const experimentalImportMetaResolve =
57-
getOptionValue('--experimental-import-meta-resolve');
5853
const asyncESM = require('internal/process/esm_loader');
5954
const { emitWarningSync } = require('internal/process/warning');
6055
const { TextDecoder } = require('internal/encoding');
@@ -111,25 +106,6 @@ async function importModuleDynamically(specifier, { url }) {
111106
return asyncESM.esmLoader.import(specifier, url);
112107
}
113108

114-
function createImportMetaResolve(defaultParentUrl) {
115-
return async function resolve(specifier, parentUrl = defaultParentUrl) {
116-
return PromisePrototypeThen(
117-
asyncESM.esmLoader.resolve(specifier, parentUrl),
118-
({ url }) => url,
119-
(error) => (
120-
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ?
121-
error.url : PromiseReject(error))
122-
);
123-
};
124-
}
125-
126-
function initializeImportMeta(meta, { url }) {
127-
// Alphabetical
128-
if (experimentalImportMetaResolve)
129-
meta.resolve = createImportMetaResolve(url);
130-
meta.url = url;
131-
}
132-
133109
// Strategy for loading a standard JavaScript module.
134110
translators.set('module', async function moduleStrategy(url, source, isMain) {
135111
assertBufferSource(source, true, 'load');
@@ -138,7 +114,9 @@ translators.set('module', async function moduleStrategy(url, source, isMain) {
138114
debug(`Translating StandardModule ${url}`);
139115
const module = new ModuleWrap(url, undefined, source, 0, 0);
140116
moduleWrap.callbackMap.set(module, {
141-
initializeImportMeta,
117+
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, {
118+
url: wrap.url
119+
}),
142120
importModuleDynamically,
143121
});
144122
return module;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs
2+
import '../common/index.mjs';
3+
import assert from 'assert/strict';
4+
5+
// This is provided by test/fixtures/es-module-loaders/mock-loader.mjs
6+
import mock from 'node:mock';
7+
8+
mock('node:events', {
9+
EventEmitter: 'This is mocked!'
10+
});
11+
12+
// This resolves to node:events
13+
// It is intercepted by mock-loader and doesn't return the normal value
14+
assert.deepStrictEqual(await import('events'), Object.defineProperty({
15+
__proto__: null,
16+
EventEmitter: 'This is mocked!'
17+
}, Symbol.toStringTag, {
18+
enumerable: false,
19+
value: 'Module'
20+
}));
21+
22+
const mutator = mock('node:events', {
23+
EventEmitter: 'This is mocked v2!'
24+
});
25+
26+
// It is intercepted by mock-loader and doesn't return the normal value.
27+
// This is resolved separately from the import above since the specifiers
28+
// are different.
29+
const mockedV2 = await import('node:events');
30+
assert.deepStrictEqual(mockedV2, Object.defineProperty({
31+
__proto__: null,
32+
EventEmitter: 'This is mocked v2!'
33+
}, Symbol.toStringTag, {
34+
enumerable: false,
35+
value: 'Module'
36+
}));
37+
38+
mutator.EventEmitter = 'This is mocked v3!';
39+
assert.deepStrictEqual(mockedV2, Object.defineProperty({
40+
__proto__: null,
41+
EventEmitter: 'This is mocked v3!'
42+
}, Symbol.toStringTag, {
43+
enumerable: false,
44+
value: 'Module'
45+
}));

test/fixtures/es-module-loaders/loader-side-effect.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Arrow function so it closes over the this-value of the preload scope.
2-
const globalPreload = () => {
2+
const globalPreloadSrc = () => {
33
/* global getBuiltin */
44
const assert = getBuiltin('assert');
55
const vm = getBuiltin('vm');
@@ -24,9 +24,9 @@ const implicitGlobalConst = 42 * 42;
2424
globalThis.explicitGlobalProperty = 42 * 42 * 42;
2525
}
2626

27-
export function getGlobalPreloadCode() {
27+
export function globalPreload() {
2828
return `\
2929
<!-- assert: inside of script goal -->
30-
(${globalPreload.toString()})();
30+
(${globalPreloadSrc.toString()})();
3131
`;
3232
}

0 commit comments

Comments
 (0)