Skip to content

Commit

Permalink
esm: working mock test
Browse files Browse the repository at this point in the history
  • Loading branch information
bmeck committed Oct 27, 2021
1 parent e55ab89 commit 13ab19e
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 33 deletions.
24 changes: 24 additions & 0 deletions doc/api/esm.md
Expand Up @@ -806,6 +806,30 @@ 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 `ref()` and `unref()` the
`MessagePort` to prevent a process from being in a state where it won't close
normally.
```js
/**
* This example causes
* @param {object} utilities
* @param {MessagePort} utilities.port
*/
export function getGlobalPreloadCode({ port }) {
port.onmessage = (evt) => {
// ...
};
return `\
port.postMessage('I went to the Loader and back');
port.onmessage = eval;
`;
}
```
### Examples
The various loader hooks can be used together to accomplish wide-ranging
Expand Down
32 changes: 32 additions & 0 deletions lib/internal/modules/esm/initialize_import_meta.js
@@ -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
};
52 changes: 44 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,15 @@ class ESMLoader {
if (!count) return;

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

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

if (preload == null) return;

Expand All @@ -373,22 +387,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;
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
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
39 changes: 39 additions & 0 deletions test/es-module/test-esm-loader-mock.mjs
@@ -0,0 +1,39 @@
// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs
import '../common/index.mjs';
import assert from 'assert/strict';
import mock from 'node:mock';

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

// This resolves to node:events
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!'
});

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'
}));
136 changes: 136 additions & 0 deletions test/fixtures/es-module-loaders/mock-loader.mjs
@@ -0,0 +1,136 @@
import {receiveMessageOnPort} from 'worker_threads';
const mockedModuleExports = new Map();
let currentMockVersion = 0;

/**
* FIXME: this is a hack to workaround loaders being
* single threaded for now
*/
function doDrainPort() {
let msg;
while (msg = receiveMessageOnPort(preloadPort)) {
onPreloadPortMessage(msg.message);
}
}
function onPreloadPortMessage({
mockVersion, resolved, exports
}) {
currentMockVersion = mockVersion;
mockedModuleExports.set(resolved, exports);
}
let preloadPort;
export function globalPreload({port}) {
preloadPort = port;
port.on('message', onPreloadPortMessage);
port.unref();
return `(${()=>{
let mockedModules = new Map();
let mockVersion = 0;
const doMock = (resolved, replacementProperties) => {
let exports = Object.keys(replacementProperties);
let namespace = Object.create(null);
let listeners = [];
for (const name of exports) {
let currentValue = replacementProperties[name];
Object.defineProperty(namespace, name, {
enumerable: true,
get() {
return currentValue;
},
set(v) {
currentValue = v;
for (let fn of listeners) {
try {
fn(name);
} catch {
}
}
}
});
}
mockedModules.set(resolved, {
namespace,
listeners
});
mockVersion++;
port.postMessage({mockVersion, resolved, exports });
return namespace;
}
setImportMetaCallback((meta, context, parent) => {
if (context.url === 'node:mock') {
meta.doMock = doMock;
return;
}
if (context.url.startsWith('mock:')) {
let [proto, version, encodedTargetURL] = context.url.split(':');
let decodedTargetURL = decodeURIComponent(encodedTargetURL);
if (mockedModules.has(decodedTargetURL)) {
meta.mock = mockedModules.get(decodedTargetURL);
return;
}
}
parent(meta, context);
});
}})()`;
}


// rewrites node: loading to mock: so that it can be intercepted
export function resolve(specifier, context, defaultResolve) {
if (specifier === 'node:mock') {
return {
url: specifier
};
}
doDrainPort();
const def = defaultResolve(specifier, context);
if (context.parentURL?.startsWith('mock:')) {
// do nothing, let it get the "real" module
} else if (mockedModuleExports.has(def.url)) {
return {
url: `mock:${currentMockVersion}:${encodeURIComponent(def.url)}`
};
};
return {
url: `${def.url}`
};
}

export function load(url, context, defaultLoad) {
doDrainPort();
if (url === 'node:mock') {
return {
source: 'export default import.meta.doMock',
format: 'module'
};
}
if (url.startsWith('mock:')) {
let [proto, version, encodedTargetURL] = url.split(':');
let ret = generateModule(mockedModuleExports.get(
decodeURIComponent(encodedTargetURL)
));
return {
source: ret,
format: 'module'
};
}
return defaultLoad(url, context);
}

function generateModule(exports) {
let body = 'export {};let mapping = {__proto__: null};'
for (const [i, name] of Object.entries(exports)) {
let key = JSON.stringify(name);
body += `var _${i} = import.meta.mock.namespace[${key}];`
body += `Object.defineProperty(mapping, ${key}, {enumerable: true,set(v) {_${i} = v;}});`
body += `export {_${i} as ${name}};`;
}
body += `import.meta.mock.listeners.push(${
() => {
for (var k in mapping) {
mapping[k] = import.meta.mock.namespace[k];
}
}
});`
return body;
}

0 comments on commit 13ab19e

Please sign in to comment.