Skip to content

Commit

Permalink
esm: working mock test
Browse files Browse the repository at this point in the history
PR-URL: #39240
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
  • Loading branch information
bmeck authored and danielleadams committed Dec 13, 2021
1 parent 7c41f32 commit b0b7943
Show file tree
Hide file tree
Showing 8 changed files with 425 additions and 36 deletions.
34 changes: 34 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,9 @@ its own `require` using `module.createRequire()`.

```js
/**
* @param {{
port: MessagePort,
}} utilities Things that preload code might find useful
* @returns {string} Code to run before application startup
*/
export function globalPreload() {
Expand All @@ -843,6 +846,35 @@ const require = createRequire(cwd() + '/<preload>');
}
```
In order to allow communication between the application and the loader, another
argument is provided to the preload code: `port`. This is available as a
parameter to the loader hook and inside of the source text returned by the hook.
Some care must be taken in order to properly call [`port.ref()`][] and
[`port.unref()`][] to prevent a process from being in a state where it won't
close normally.
```js
/**
* This example has the application context send a message to the loader
* and sends the message back to the application context
* @param {{
port: MessagePort,
}} utilities Things that preload code might find useful
* @returns {string} Code to run before application startup
*/
export function globalPreload({ port }) {
port.onmessage = (evt) => {
port.postMessage(evt.data);
};
return `\
port.postMessage('console.log("I went to the Loader and back");');
port.onmessage = (evt) => {
eval(evt.data);
};
`;
}
```
### Examples
The various loader hooks can be used together to accomplish wide-ranging
Expand Down Expand Up @@ -1417,6 +1449,8 @@ success!
[`module.createRequire()`]: module.md#modulecreaterequirefilename
[`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
[`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref
[`port.unref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portunref
[`process.dlopen`]: process.md#processdlopenmodule-filename-flags
[`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
[`util.TextDecoder`]: util.md#class-utiltextdecoder
Expand Down
32 changes: 32 additions & 0 deletions lib/internal/modules/esm/initialize_import_meta.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict';

const { getOptionValue } = require('internal/options');
const experimentalImportMetaResolve =
getOptionValue('--experimental-import-meta-resolve');
const { PromisePrototypeThen, PromiseReject } = primordials;
const asyncESM = require('internal/process/esm_loader');

function createImportMetaResolve(defaultParentUrl) {
return async function resolve(specifier, parentUrl = defaultParentUrl) {
return PromisePrototypeThen(
asyncESM.esmLoader.resolve(specifier, parentUrl),
({ url }) => url,
(error) => (
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ?
error.url : PromiseReject(error))
);
};
}

function initializeImportMeta(meta, context) {
const url = context.url;

// Alphabetical
if (experimentalImportMetaResolve)
meta.resolve = createImportMetaResolve(url);
meta.url = url;
}

module.exports = {
initializeImportMeta
};
71 changes: 63 additions & 8 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
SafeWeakMap,
globalThis,
} = primordials;
const { MessageChannel } = require('internal/worker/io');

const {
ERR_INVALID_ARG_TYPE,
Expand All @@ -40,6 +41,9 @@ const {
defaultResolve,
DEFAULT_CONDITIONS,
} = require('internal/modules/esm/resolve');
const {
initializeImportMeta
} = require('internal/modules/esm/initialize_import_meta');
const { defaultLoad } = require('internal/modules/esm/load');
const { translators } = require(
'internal/modules/esm/translators');
Expand Down Expand Up @@ -77,6 +81,8 @@ class ESMLoader {
defaultResolve,
];

#importMetaInitializer = initializeImportMeta;

/**
* Map of already-loaded CJS modules to use
*/
Expand Down Expand Up @@ -409,7 +415,18 @@ class ESMLoader {
if (!count) return;

for (let i = 0; i < count; i++) {
const preload = this.#globalPreloaders[i]();
const channel = new MessageChannel();
const {
port1: insidePreload,
port2: insideLoader,
} = channel;

insidePreload.unref();
insideLoader.unref();

const preload = this.#globalPreloaders[i]({
port: insideLoader
});

if (preload == null) return;

Expand All @@ -423,22 +440,60 @@ class ESMLoader {
const { compileFunction } = require('vm');
const preloadInit = compileFunction(
preload,
['getBuiltin'],
['getBuiltin', 'port', 'setImportMetaCallback'],
{
filename: '<preload>',
}
);
const { NativeModule } = require('internal/bootstrap/loaders');

FunctionPrototypeCall(preloadInit, globalThis, (builtinName) => {
if (NativeModule.canBeRequiredByUsers(builtinName)) {
return require(builtinName);
// We only allow replacing the importMetaInitializer during preload,
// after preload is finished, we disable the ability to replace it
//
// This exposes accidentally setting the initializer too late by
// throwing an error.
let finished = false;
let replacedImportMetaInitializer = false;
let next = this.#importMetaInitializer;
try {
// Calls the compiled preload source text gotten from the hook
// Since the parameters are named we use positional parameters
// see compileFunction above to cross reference the names
FunctionPrototypeCall(
preloadInit,
globalThis,
// Param getBuiltin
(builtinName) => {
if (NativeModule.canBeRequiredByUsers(builtinName)) {
return require(builtinName);
}
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
},
// Param port
insidePreload,
// Param setImportMetaCallback
(fn) => {
if (finished || typeof fn !== 'function') {
throw new ERR_INVALID_ARG_TYPE('fn', fn);
}
replacedImportMetaInitializer = true;
const parent = next;
next = (meta, context) => {
return fn(meta, context, parent);
};
});
} finally {
finished = true;
if (replacedImportMetaInitializer) {
this.#importMetaInitializer = next;
}
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
});
}
}
}

importMetaInitialize(meta, context) {
this.#importMetaInitializer(meta, context);
}

/**
* Resolve the location of the module.
*
Expand Down
28 changes: 3 additions & 25 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const {
ObjectGetPrototypeOf,
ObjectPrototypeHasOwnProperty,
ObjectKeys,
PromisePrototypeThen,
PromiseReject,
SafeArrayIterator,
SafeMap,
SafeSet,
Expand Down Expand Up @@ -52,9 +50,6 @@ const {
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
const moduleWrap = internalBinding('module_wrap');
const { ModuleWrap } = moduleWrap;
const { getOptionValue } = require('internal/options');
const experimentalImportMetaResolve =
getOptionValue('--experimental-import-meta-resolve');
const asyncESM = require('internal/process/esm_loader');
const { emitWarningSync } = require('internal/process/warning');
const { TextDecoder } = require('internal/encoding');
Expand Down Expand Up @@ -111,25 +106,6 @@ async function importModuleDynamically(specifier, { url }, assertions) {
return asyncESM.esmLoader.import(specifier, url, assertions);
}

function createImportMetaResolve(defaultParentUrl) {
return async function resolve(specifier, parentUrl = defaultParentUrl) {
return PromisePrototypeThen(
asyncESM.esmLoader.resolve(specifier, parentUrl),
({ url }) => url,
(error) => (
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ?
error.url : PromiseReject(error))
);
};
}

function initializeImportMeta(meta, { url }) {
// Alphabetical
if (experimentalImportMetaResolve)
meta.resolve = createImportMetaResolve(url);
meta.url = url;
}

// Strategy for loading a standard JavaScript module.
translators.set('module', async function moduleStrategy(url, source, isMain) {
assertBufferSource(source, true, 'load');
Expand All @@ -138,7 +114,9 @@ translators.set('module', async function moduleStrategy(url, source, isMain) {
debug(`Translating StandardModule ${url}`);
const module = new ModuleWrap(url, undefined, source, 0, 0);
moduleWrap.callbackMap.set(module, {
initializeImportMeta,
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, {
url: wrap.url
}),
importModuleDynamically,
});
return module;
Expand Down
45 changes: 45 additions & 0 deletions test/es-module/test-esm-loader-mock.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs
import '../common/index.mjs';
import assert from 'assert/strict';

// This is provided by test/fixtures/es-module-loaders/mock-loader.mjs
import mock from 'node:mock';

mock('node:events', {
EventEmitter: 'This is mocked!'
});

// This resolves to node:events
// It is intercepted by mock-loader and doesn't return the normal value
assert.deepStrictEqual(await import('events'), Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));

const mutator = mock('node:events', {
EventEmitter: 'This is mocked v2!'
});

// It is intercepted by mock-loader and doesn't return the normal value.
// This is resolved separately from the import above since the specifiers
// are different.
const mockedV2 = await import('node:events');
assert.deepStrictEqual(mockedV2, Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked v2!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));

mutator.EventEmitter = 'This is mocked v3!';
assert.deepStrictEqual(mockedV2, Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked v3!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));
6 changes: 3 additions & 3 deletions test/fixtures/es-module-loaders/loader-side-effect.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Arrow function so it closes over the this-value of the preload scope.
const globalPreload = () => {
const globalPreloadSrc = () => {
/* global getBuiltin */
const assert = getBuiltin('assert');
const vm = getBuiltin('vm');
Expand All @@ -24,9 +24,9 @@ const implicitGlobalConst = 42 * 42;
globalThis.explicitGlobalProperty = 42 * 42 * 42;
}

export function getGlobalPreloadCode() {
export function globalPreload() {
return `\
<!-- assert: inside of script goal -->
(${globalPreload.toString()})();
(${globalPreloadSrc.toString()})();
`;
}
Loading

0 comments on commit b0b7943

Please sign in to comment.