Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

working mock test #39240

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 24 additions & 12 deletions doc/api/esm.md
Expand Up @@ -773,7 +773,7 @@ source to a supported one (see [Examples](#examples) below).
> signature may change. Do not rely on the API described below.

> Note: In a previous version of this API, this hook was named
> `getGlobalPreloadCode`.
> `globalPreload`.
bmeck marked this conversation as resolved.
Show resolved Hide resolved

* Returns: {string}

Expand All @@ -790,22 +790,32 @@ its own `require` using `module.createRequire()`.

```js
/**
* @returns {string} Code to run before application startup
* This example has the application context send a message to the loader
* and sends the message back to the application context
* @param {object} utilities
* @param {MessagePort} utilities.port
bmeck marked this conversation as resolved.
Show resolved Hide resolved
* @returns {string}
bmeck marked this conversation as resolved.
Show resolved Hide resolved
*/
export function globalPreload() {
export function globalPreload({ port }) {
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved
port.onmessage = (evt) => {
port.postMessage(evt.data);
};
return `\
globalThis.someInjectedProperty = 42;
console.log('I just set some globals!');

const { createRequire } = getBuiltin('module');
const { cwd } = getBuiltin('process');

const require = createRequire(cwd() + '/<preload>');
// [...]
`;
bmeck marked this conversation as resolved.
Show resolved Hide resolved
port.postMessage('console.log("I went to the Loader and back");');
port.onmessage = (evt) => {
eval(evt.data);
};
`;
}
```

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.

### Examples

The various loader hooks can be used together to accomplish wide-ranging
Expand Down Expand Up @@ -1377,6 +1387,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
@@ -0,0 +1,32 @@
'use strict';

bmeck marked this conversation as resolved.
Show resolved Hide resolved
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),
bmeck marked this conversation as resolved.
Show resolved Hide resolved
({ 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);
bmeck marked this conversation as resolved.
Show resolved Hide resolved
meta.url = url;
}

module.exports = {
initializeImportMeta
};
55 changes: 47 additions & 8 deletions lib/internal/modules/esm/loader.js
Expand Up @@ -18,6 +18,7 @@ const {
SafeWeakMap,
globalThis,
} = primordials;
const { MessageChannel } = require('internal/worker/io');

const {
ERR_INVALID_ARG_TYPE,
Expand All @@ -39,6 +40,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 @@ -76,6 +80,8 @@ class ESMLoader {
defaultResolve,
];

#importMetaInitializer = initializeImportMeta;

/**
* Map of already-loaded CJS modules to use
*/
Expand Down Expand Up @@ -359,7 +365,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 @@ -373,22 +390,44 @@ 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);
let finished = false;
let replacedImportMetaInitializer = false;
let next = this.#importMetaInitializer;
try {
FunctionPrototypeCall(preloadInit, globalThis, (builtinName) => {
if (NativeModule.canBeRequiredByUsers(builtinName)) {
return require(builtinName);
}
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
}, insidePreload, (fn) => {
if (finished || typeof fn !== 'function') {
throw new ERR_INVALID_ARG_TYPE('fn', fn);
}
replacedImportMetaInitializer = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m struggling to follow this code. Are we in a callback here? Can this be split out?

I see that by now we’ve apparently replaced import.meta initializer, but where did that happen? Some comments might help.

const parent = next;
next = (meta, context) => {
return fn(meta, context, parent);
};
});
} finally {
finished = true;
if (replacedImportMetaInitializer) {
this.#importMetaInitializer = next;
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved
}
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
});
}
}
}

importMetaInitialize(meta, context) {
this.#importMetaInitializer(meta, context);
}
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved

/**
* Resolve the location of the module.
*
Expand Down
28 changes: 3 additions & 25 deletions lib/internal/modules/esm/translators.js
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 }) {
return asyncESM.esmLoader.import(specifier, url);
}

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
@@ -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';
bmeck marked this conversation as resolved.
Show resolved Hide resolved

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
@@ -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()})();
`;
}