From 11b88c2a56247bc545f90280e195113c1c943a69 Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Sat, 28 Jan 2023 20:19:04 -0800 Subject: [PATCH] esm: delete preload mock test --- test/es-module/test-esm-loader-mock.mjs | 45 ---- .../es-module-loaders/mock-loader.mjs | 249 ------------------ 2 files changed, 294 deletions(-) delete mode 100644 test/es-module/test-esm-loader-mock.mjs delete mode 100644 test/fixtures/es-module-loaders/mock-loader.mjs diff --git a/test/es-module/test-esm-loader-mock.mjs b/test/es-module/test-esm-loader-mock.mjs deleted file mode 100644 index 2783bf694d239a..00000000000000 --- a/test/es-module/test-esm-loader-mock.mjs +++ /dev/null @@ -1,45 +0,0 @@ -// 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' -})); diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs deleted file mode 100644 index 062be39603e851..00000000000000 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ /dev/null @@ -1,249 +0,0 @@ -import { receiveMessageOnPort } from 'node:worker_threads'; -const mockedModuleExports = new Map(); -let currentMockVersion = 0; - -// This loader causes a new module `node:mock` to become available as a way to -// swap module resolution results for mocking purposes. It uses this instead -// of import.meta so that CommonJS can still use the functionality. -// -// It does so by allowing non-mocked modules to live in normal URL cache -// locations but creates 'mock-facade:' URL cache location for every time a -// module location is mocked. Since a single URL can be mocked multiple -// times but it cannot be removed from the cache, `mock-facade:` URLs have a -// form of mock-facade:$VERSION:$REPLACING_URL with the parameters being URL -// percent encoded every time a module is resolved. So if a module for -// 'file:///app.js' is mocked it might look like -// 'mock-facade:12:file%3A%2F%2F%2Fapp.js'. This encoding is done to prevent -// problems like mocking URLs with special URL characters like '#' or '?' from -// accidentally being picked up as part of the 'mock-facade:' URL containing -// the mocked URL. -// -// NOTE: due to ESM spec, once a specifier has been resolved in a source text -// it cannot be changed. So things like the following DO NOT WORK: -// -// ```mjs -// import mock from 'node:mock'; -// mock('file:///app.js', {x:1}); -// const namespace1 = await import('file:///app.js'); -// namespace1.x; // 1 -// mock('file:///app.js', {x:2}); -// const namespace2 = await import('file:///app.js'); -// namespace2.x; // STILL 1, because this source text already set the specifier -// // for 'file:///app.js', a different specifier that resolves -// // to that could still get a new namespace though -// assert(namespace1 === namespace2); -// ``` - -/** - * FIXME: this is a hack to workaround loaders being - * single threaded for now, just ensures that the MessagePort drains - */ -function doDrainPort() { - let msg; - while (msg = receiveMessageOnPort(preloadPort)) { - onPreloadPortMessage(msg.message); - } -} - -/** - * @param param0 message from the application context - */ -function onPreloadPortMessage({ - mockVersion, resolved, exports -}) { - currentMockVersion = mockVersion; - mockedModuleExports.set(resolved, exports); -} -let preloadPort; -export function globalPreload({port}) { - // Save the communication port to the application context to send messages - // to it later - preloadPort = port; - // Every time the application context sends a message over the port - port.on('message', onPreloadPortMessage); - // This prevents the port that the Loader/application talk over - // from keeping the process alive, without this, an application would be kept - // alive just because a loader is waiting for messages - port.unref(); - - const insideAppContext = (getBuiltin, port, setImportMetaCallback) => { - /** - * This is the Map that saves *all* the mocked URL -> replacement Module - * mappings - * @type {Map} - */ - let mockedModules = new Map(); - let mockVersion = 0; - /** - * This is the value that is placed into the `node:mock` default export - * - * @example - * ```mjs - * import mock from 'node:mock'; - * const mutator = mock('file:///app.js', {x:1}); - * const namespace = await import('file:///app.js'); - * namespace.x; // 1; - * mutator.x = 2; - * namespace.x; // 2; - * ``` - * - * @param {string} resolved an absolute URL HREF string - * @param {object} replacementProperties an object to pick properties from - * to act as a module namespace - * @returns {object} a mutator object that can update the module namespace - * since we can't do something like old Object.observe - */ - const doMock = (resolved, replacementProperties) => { - let exportNames = Object.keys(replacementProperties); - let namespace = Object.create(null); - /** - * @type {Array<(name: string)=>void>} functions to call whenever an - * export name is updated - */ - let listeners = []; - for (const name of exportNames) { - let currentValueForPropertyName = replacementProperties[name]; - Object.defineProperty(namespace, name, { - enumerable: true, - get() { - return currentValueForPropertyName; - }, - set(v) { - currentValueForPropertyName = v; - for (let fn of listeners) { - try { - fn(name); - } catch { - } - } - } - }); - } - mockedModules.set(resolved, { - namespace, - listeners - }); - mockVersion++; - // Inform the loader that the `resolved` URL should now use the specific - // `mockVersion` and has export names of `exportNames` - // - // This allows the loader to generate a fake module for that version - // and names the next time it resolves a specifier to equal `resolved` - port.postMessage({ mockVersion, resolved, exports: exportNames }); - return namespace; - } - // Sets the import.meta properties up - // has the normal chaining workflow with `defaultImportMetaInitializer` - setImportMetaCallback((meta, context, defaultImportMetaInitializer) => { - /** - * 'node:mock' creates its default export by plucking off of import.meta - * and must do so in order to get the communications channel from inside - * preloadCode - */ - if (context.url === 'node:mock') { - meta.doMock = doMock; - return; - } - /** - * Fake modules created by `node:mock` get their meta.mock utility set - * to the corresponding value keyed off `mockedModules` and use this - * to setup their exports/listeners properly - */ - if (context.url.startsWith('mock-facade:')) { - let [proto, version, encodedTargetURL] = context.url.split(':'); - let decodedTargetURL = decodeURIComponent(encodedTargetURL); - if (mockedModules.has(decodedTargetURL)) { - meta.mock = mockedModules.get(decodedTargetURL); - return; - } - } - /** - * Ensure we still get things like `import.meta.url` - */ - defaultImportMetaInitializer(meta, context); - }); - }; - return `(${insideAppContext})(getBuiltin, port, setImportMetaCallback)` -} - - -// Rewrites node: loading to mock-facade: so that it can be intercepted -export async function resolve(specifier, context, defaultResolve) { - if (specifier === 'node:mock') { - return { - shortCircuit: true, - url: specifier - }; - } - doDrainPort(); - const def = await defaultResolve(specifier, context); - if (context.parentURL?.startsWith('mock-facade:')) { - // Do nothing, let it get the "real" module - } else if (mockedModuleExports.has(def.url)) { - return { - shortCircuit: true, - url: `mock-facade:${currentMockVersion}:${encodeURIComponent(def.url)}` - }; - }; - return { - shortCircuit: true, - url: def.url, - }; -} - -export async function load(url, context, defaultLoad) { - doDrainPort(); - if (url === 'node:mock') { - /** - * Simply grab the import.meta.doMock to establish the communication - * channel with preloadCode - */ - return { - shortCircuit: true, - source: 'export default import.meta.doMock', - format: 'module' - }; - } - /** - * Mocked fake module, not going to be handled in default way so it - * generates the source text, then short circuits - */ - if (url.startsWith('mock-facade:')) { - let [proto, version, encodedTargetURL] = url.split(':'); - let ret = generateModule(mockedModuleExports.get( - decodeURIComponent(encodedTargetURL) - )); - return { - shortCircuit: true, - source: ret, - format: 'module' - }; - } - return defaultLoad(url, context); -} - -/** - * - * @param {Array} exports name of the exports of the module - * @returns {string} - */ -function generateModule(exports) { - let body = [ - 'export {};', - 'let mapping = {__proto__: null};' - ]; - for (const [i, name] of Object.entries(exports)) { - let key = JSON.stringify(name); - body.push(`var _${i} = import.meta.mock.namespace[${key}];`); - body.push(`Object.defineProperty(mapping, ${key}, { enumerable: true, set(v) {_${i} = v;}, get() {return _${i};} });`); - body.push(`export {_${i} as ${name}};`); - } - body.push(`import.meta.mock.listeners.push(${ - () => { - for (var k in mapping) { - mapping[k] = import.meta.mock.namespace[k]; - } - } - });`); - return body.join('\n'); -}