Skip to content

Commit a554821

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 341a999 commit a554821

File tree

8 files changed

+425
-36
lines changed

8 files changed

+425
-36
lines changed

doc/api/esm.md

+34
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,9 @@ its own `require` using `module.createRequire()`.
822822

823823
```js
824824
/**
825+
* @param {{
826+
port: MessagePort,
827+
}} utilities Things that preload code might find useful
825828
* @returns {string} Code to run before application startup
826829
*/
827830
export function globalPreload() {
@@ -838,6 +841,35 @@ const require = createRequire(cwd() + '/<preload>');
838841
}
839842
```
840843
844+
In order to allow communication between the application and the loader, another
845+
argument is provided to the preload code: `port`. This is available as a
846+
parameter to the loader hook and inside of the source text returned by the hook.
847+
Some care must be taken in order to properly call [`port.ref()`][] and
848+
[`port.unref()`][] to prevent a process from being in a state where it won't
849+
close normally.
850+
851+
```js
852+
/**
853+
* This example has the application context send a message to the loader
854+
* and sends the message back to the application context
855+
* @param {{
856+
port: MessagePort,
857+
}} utilities Things that preload code might find useful
858+
* @returns {string} Code to run before application startup
859+
*/
860+
export function globalPreload({ port }) {
861+
port.onmessage = (evt) => {
862+
port.postMessage(evt.data);
863+
};
864+
return `\
865+
port.postMessage('console.log("I went to the Loader and back");');
866+
port.onmessage = (evt) => {
867+
eval(evt.data);
868+
};
869+
`;
870+
}
871+
```
872+
841873
### Examples
842874
843875
The various loader hooks can be used together to accomplish wide-ranging
@@ -1433,6 +1465,8 @@ success!
14331465
[`module.createRequire()`]: module.md#modulecreaterequirefilename
14341466
[`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports
14351467
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
1468+
[`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref
1469+
[`port.unref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portunref
14361470
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
14371471
[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
14381472
[`util.TextDecoder`]: util.md#class-utiltextdecoder
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

+63-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const {
1919
SafeWeakMap,
2020
globalThis,
2121
} = primordials;
22+
const { MessageChannel } = require('internal/worker/io');
2223

2324
const {
2425
ERR_INVALID_ARG_TYPE,
@@ -40,6 +41,9 @@ const {
4041
defaultResolve,
4142
DEFAULT_CONDITIONS,
4243
} = require('internal/modules/esm/resolve');
44+
const {
45+
initializeImportMeta
46+
} = require('internal/modules/esm/initialize_import_meta');
4347
const { defaultLoad } = require('internal/modules/esm/load');
4448
const { translators } = require(
4549
'internal/modules/esm/translators');
@@ -77,6 +81,8 @@ class ESMLoader {
7781
defaultResolve,
7882
];
7983

84+
#importMetaInitializer = initializeImportMeta;
85+
8086
/**
8187
* Map of already-loaded CJS modules to use
8288
*/
@@ -409,7 +415,18 @@ class ESMLoader {
409415
if (!count) return;
410416

411417
for (let i = 0; i < count; i++) {
412-
const preload = this.#globalPreloaders[i]();
418+
const channel = new MessageChannel();
419+
const {
420+
port1: insidePreload,
421+
port2: insideLoader,
422+
} = channel;
423+
424+
insidePreload.unref();
425+
insideLoader.unref();
426+
427+
const preload = this.#globalPreloaders[i]({
428+
port: insideLoader
429+
});
413430

414431
if (preload == null) return;
415432

@@ -423,22 +440,60 @@ class ESMLoader {
423440
const { compileFunction } = require('vm');
424441
const preloadInit = compileFunction(
425442
preload,
426-
['getBuiltin'],
443+
['getBuiltin', 'port', 'setImportMetaCallback'],
427444
{
428445
filename: '<preload>',
429446
}
430447
);
431448
const { NativeModule } = require('internal/bootstrap/loaders');
432-
433-
FunctionPrototypeCall(preloadInit, globalThis, (builtinName) => {
434-
if (NativeModule.canBeRequiredByUsers(builtinName)) {
435-
return require(builtinName);
449+
// We only allow replacing the importMetaInitializer during preload,
450+
// after preload is finished, we disable the ability to replace it
451+
//
452+
// This exposes accidentally setting the initializer too late by
453+
// throwing an error.
454+
let finished = false;
455+
let replacedImportMetaInitializer = false;
456+
let next = this.#importMetaInitializer;
457+
try {
458+
// Calls the compiled preload source text gotten from the hook
459+
// Since the parameters are named we use positional parameters
460+
// see compileFunction above to cross reference the names
461+
FunctionPrototypeCall(
462+
preloadInit,
463+
globalThis,
464+
// Param getBuiltin
465+
(builtinName) => {
466+
if (NativeModule.canBeRequiredByUsers(builtinName)) {
467+
return require(builtinName);
468+
}
469+
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
470+
},
471+
// Param port
472+
insidePreload,
473+
// Param setImportMetaCallback
474+
(fn) => {
475+
if (finished || typeof fn !== 'function') {
476+
throw new ERR_INVALID_ARG_TYPE('fn', fn);
477+
}
478+
replacedImportMetaInitializer = true;
479+
const parent = next;
480+
next = (meta, context) => {
481+
return fn(meta, context, parent);
482+
};
483+
});
484+
} finally {
485+
finished = true;
486+
if (replacedImportMetaInitializer) {
487+
this.#importMetaInitializer = next;
436488
}
437-
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
438-
});
489+
}
439490
}
440491
}
441492

493+
importMetaInitialize(meta, context) {
494+
this.#importMetaInitializer(meta, context);
495+
}
496+
442497
/**
443498
* Resolve the location of the module.
444499
*

lib/internal/modules/esm/translators.js

+3-25
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 }, assertions) {
111106
return asyncESM.esmLoader.import(specifier, url, assertions);
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;
+45
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

+3-3
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)