11cb0ef41Sopenharmony_ciimport { receiveMessageOnPort } from 'node:worker_threads'; 21cb0ef41Sopenharmony_ciconst mockedModuleExports = new Map(); 31cb0ef41Sopenharmony_cilet currentMockVersion = 0; 41cb0ef41Sopenharmony_ci 51cb0ef41Sopenharmony_ci// These hooks enable code running on the application thread to 61cb0ef41Sopenharmony_ci// swap module resolution results for mocking purposes. It uses this instead 71cb0ef41Sopenharmony_ci// of import.meta so that CommonJS can still use the functionality. 81cb0ef41Sopenharmony_ci// 91cb0ef41Sopenharmony_ci// It does so by allowing non-mocked modules to live in normal URL cache 101cb0ef41Sopenharmony_ci// locations but creates 'mock-facade:' URL cache location for every time a 111cb0ef41Sopenharmony_ci// module location is mocked. Since a single URL can be mocked multiple 121cb0ef41Sopenharmony_ci// times but it cannot be removed from the cache, `mock-facade:` URLs have a 131cb0ef41Sopenharmony_ci// form of mock-facade:$VERSION:$REPLACING_URL with the parameters being URL 141cb0ef41Sopenharmony_ci// percent encoded every time a module is resolved. So if a module for 151cb0ef41Sopenharmony_ci// 'file:///app.js' is mocked it might look like 161cb0ef41Sopenharmony_ci// 'mock-facade:12:file%3A%2F%2F%2Fapp.js'. This encoding is done to prevent 171cb0ef41Sopenharmony_ci// problems like mocking URLs with special URL characters like '#' or '?' from 181cb0ef41Sopenharmony_ci// accidentally being picked up as part of the 'mock-facade:' URL containing 191cb0ef41Sopenharmony_ci// the mocked URL. 201cb0ef41Sopenharmony_ci// 211cb0ef41Sopenharmony_ci// NOTE: due to ESM spec, once a specifier has been resolved in a source text 221cb0ef41Sopenharmony_ci// it cannot be changed. So things like the following DO NOT WORK: 231cb0ef41Sopenharmony_ci// 241cb0ef41Sopenharmony_ci// ```mjs 251cb0ef41Sopenharmony_ci// import mock from 'test-esm-loader-mock'; // See test-esm-loader-mock.mjs 261cb0ef41Sopenharmony_ci// mock('file:///app.js', {x:1}); 271cb0ef41Sopenharmony_ci// const namespace1 = await import('file:///app.js'); 281cb0ef41Sopenharmony_ci// namespace1.x; // 1 291cb0ef41Sopenharmony_ci// mock('file:///app.js', {x:2}); 301cb0ef41Sopenharmony_ci// const namespace2 = await import('file:///app.js'); 311cb0ef41Sopenharmony_ci// namespace2.x; // STILL 1, because this source text already set the specifier 321cb0ef41Sopenharmony_ci// // for 'file:///app.js', a different specifier that resolves 331cb0ef41Sopenharmony_ci// // to that could still get a new namespace though 341cb0ef41Sopenharmony_ci// assert(namespace1 === namespace2); 351cb0ef41Sopenharmony_ci// ``` 361cb0ef41Sopenharmony_ci 371cb0ef41Sopenharmony_ci/** 381cb0ef41Sopenharmony_ci * @param param0 message from the application context 391cb0ef41Sopenharmony_ci */ 401cb0ef41Sopenharmony_cifunction onPreloadPortMessage({ 411cb0ef41Sopenharmony_ci mockVersion, resolved, exports 421cb0ef41Sopenharmony_ci}) { 431cb0ef41Sopenharmony_ci currentMockVersion = mockVersion; 441cb0ef41Sopenharmony_ci mockedModuleExports.set(resolved, exports); 451cb0ef41Sopenharmony_ci} 461cb0ef41Sopenharmony_ci 471cb0ef41Sopenharmony_ci/** @type {URL['href']} */ 481cb0ef41Sopenharmony_cilet mainImportURL; 491cb0ef41Sopenharmony_ci/** @type {MessagePort} */ 501cb0ef41Sopenharmony_cilet preloadPort; 511cb0ef41Sopenharmony_ciexport async function initialize(data) { 521cb0ef41Sopenharmony_ci ({ mainImportURL, port: preloadPort } = data); 531cb0ef41Sopenharmony_ci 541cb0ef41Sopenharmony_ci data.port.on('message', onPreloadPortMessage); 551cb0ef41Sopenharmony_ci} 561cb0ef41Sopenharmony_ci 571cb0ef41Sopenharmony_ci/** 581cb0ef41Sopenharmony_ci * Because Node.js internals use a separate MessagePort for cross-thread 591cb0ef41Sopenharmony_ci * communication, there could be some messages pending that we should handle 601cb0ef41Sopenharmony_ci * before continuing. 611cb0ef41Sopenharmony_ci */ 621cb0ef41Sopenharmony_cifunction doDrainPort() { 631cb0ef41Sopenharmony_ci let msg; 641cb0ef41Sopenharmony_ci while (msg = receiveMessageOnPort(preloadPort)) { 651cb0ef41Sopenharmony_ci onPreloadPortMessage(msg.message); 661cb0ef41Sopenharmony_ci } 671cb0ef41Sopenharmony_ci} 681cb0ef41Sopenharmony_ci 691cb0ef41Sopenharmony_ci// Rewrites node: loading to mock-facade: so that it can be intercepted 701cb0ef41Sopenharmony_ciexport async function resolve(specifier, context, defaultResolve) { 711cb0ef41Sopenharmony_ci doDrainPort(); 721cb0ef41Sopenharmony_ci const def = await defaultResolve(specifier, context); 731cb0ef41Sopenharmony_ci if (context.parentURL?.startsWith('mock-facade:')) { 741cb0ef41Sopenharmony_ci // Do nothing, let it get the "real" module 751cb0ef41Sopenharmony_ci } else if (mockedModuleExports.has(def.url)) { 761cb0ef41Sopenharmony_ci return { 771cb0ef41Sopenharmony_ci shortCircuit: true, 781cb0ef41Sopenharmony_ci url: `mock-facade:${currentMockVersion}:${encodeURIComponent(def.url)}` 791cb0ef41Sopenharmony_ci }; 801cb0ef41Sopenharmony_ci }; 811cb0ef41Sopenharmony_ci return { 821cb0ef41Sopenharmony_ci shortCircuit: true, 831cb0ef41Sopenharmony_ci url: def.url, 841cb0ef41Sopenharmony_ci }; 851cb0ef41Sopenharmony_ci} 861cb0ef41Sopenharmony_ci 871cb0ef41Sopenharmony_ciexport async function load(url, context, defaultLoad) { 881cb0ef41Sopenharmony_ci doDrainPort(); 891cb0ef41Sopenharmony_ci /** 901cb0ef41Sopenharmony_ci * Mocked fake module, not going to be handled in default way so it 911cb0ef41Sopenharmony_ci * generates the source text, then short circuits 921cb0ef41Sopenharmony_ci */ 931cb0ef41Sopenharmony_ci if (url.startsWith('mock-facade:')) { 941cb0ef41Sopenharmony_ci const encodedTargetURL = url.slice(url.lastIndexOf(':') + 1); 951cb0ef41Sopenharmony_ci return { 961cb0ef41Sopenharmony_ci shortCircuit: true, 971cb0ef41Sopenharmony_ci source: generateModule(encodedTargetURL), 981cb0ef41Sopenharmony_ci format: 'module', 991cb0ef41Sopenharmony_ci }; 1001cb0ef41Sopenharmony_ci } 1011cb0ef41Sopenharmony_ci return defaultLoad(url, context); 1021cb0ef41Sopenharmony_ci} 1031cb0ef41Sopenharmony_ci 1041cb0ef41Sopenharmony_ci/** 1051cb0ef41Sopenharmony_ci * Generate the source code for a mocked module. 1061cb0ef41Sopenharmony_ci * @param {string} encodedTargetURL the module being mocked 1071cb0ef41Sopenharmony_ci * @returns {string} 1081cb0ef41Sopenharmony_ci */ 1091cb0ef41Sopenharmony_cifunction generateModule(encodedTargetURL) { 1101cb0ef41Sopenharmony_ci const exports = mockedModuleExports.get( 1111cb0ef41Sopenharmony_ci decodeURIComponent(encodedTargetURL) 1121cb0ef41Sopenharmony_ci ); 1131cb0ef41Sopenharmony_ci let body = [ 1141cb0ef41Sopenharmony_ci `import { mockedModules } from ${JSON.stringify(mainImportURL)};`, 1151cb0ef41Sopenharmony_ci 'export {};', 1161cb0ef41Sopenharmony_ci 'let mapping = {__proto__: null};', 1171cb0ef41Sopenharmony_ci `const mock = mockedModules.get(${JSON.stringify(encodedTargetURL)});`, 1181cb0ef41Sopenharmony_ci ]; 1191cb0ef41Sopenharmony_ci for (const [i, name] of Object.entries(exports)) { 1201cb0ef41Sopenharmony_ci let key = JSON.stringify(name); 1211cb0ef41Sopenharmony_ci body.push(`var _${i} = mock.namespace[${key}];`); 1221cb0ef41Sopenharmony_ci body.push(`Object.defineProperty(mapping, ${key}, { enumerable: true, set(v) {_${i} = v;}, get() {return _${i};} });`); 1231cb0ef41Sopenharmony_ci body.push(`export {_${i} as ${name}};`); 1241cb0ef41Sopenharmony_ci } 1251cb0ef41Sopenharmony_ci body.push(`mock.listeners.push(${ 1261cb0ef41Sopenharmony_ci () => { 1271cb0ef41Sopenharmony_ci for (var k in mapping) { 1281cb0ef41Sopenharmony_ci mapping[k] = mock.namespace[k]; 1291cb0ef41Sopenharmony_ci } 1301cb0ef41Sopenharmony_ci } 1311cb0ef41Sopenharmony_ci });`); 1321cb0ef41Sopenharmony_ci return body.join('\n'); 1331cb0ef41Sopenharmony_ci} 134