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