1import { register } from 'node:module'; 2import { MessageChannel } from 'node:worker_threads'; 3 4 5const { port1, port2 } = new MessageChannel(); 6 7register('./mock-loader.mjs', import.meta.url, { 8 data: { 9 port: port2, 10 mainImportURL: import.meta.url, 11 }, 12 transferList: [port2], 13}); 14 15/** 16 * This is the Map that saves *all* the mocked URL -> replacement Module 17 * mappings 18 * @type {Map<string, {namespace, listeners}>} 19 */ 20export const mockedModules = new Map(); 21let mockVersion = 0; 22 23/** 24 * @param {string} resolved an absolute URL HREF string 25 * @param {object} replacementProperties an object to pick properties from 26 * to act as a module namespace 27 * @returns {object} a mutator object that can update the module namespace 28 * since we can't do something like old Object.observe 29 */ 30export function mock(resolved, replacementProperties) { 31 const exportNames = Object.keys(replacementProperties); 32 const namespace = { __proto__: null }; 33 /** 34 * @type {Array<(name: string)=>void>} functions to call whenever an 35 * export name is updated 36 */ 37 const listeners = []; 38 for (const name of exportNames) { 39 let currentValueForPropertyName = replacementProperties[name]; 40 Object.defineProperty(namespace, name, { 41 __proto__: null, 42 enumerable: true, 43 get() { 44 return currentValueForPropertyName; 45 }, 46 set(v) { 47 currentValueForPropertyName = v; 48 for (const fn of listeners) { 49 try { 50 fn(name); 51 } catch { 52 /* noop */ 53 } 54 } 55 }, 56 }); 57 } 58 mockedModules.set(encodeURIComponent(resolved), { 59 namespace, 60 listeners, 61 }); 62 mockVersion++; 63 // Inform the loader that the `resolved` URL should now use the specific 64 // `mockVersion` and has export names of `exportNames` 65 // 66 // This allows the loader to generate a fake module for that version 67 // and names the next time it resolves a specifier to equal `resolved` 68 port1.postMessage({ mockVersion, resolved, exports: exportNames }); 69 return namespace; 70} 71