@ 261107.7
current
'use strict';
const {
  ArrayPrototypeFilter,
  ArrayPrototypePush,
  ArrayPrototypeSlice,
  Error,
  FunctionPrototypeBind,
  FunctionPrototypeCall,
  ObjectAssign,
  ObjectDefineProperties,
  ObjectDefineProperty,
  ObjectGetOwnPropertyDescriptor,
  ObjectGetOwnPropertyDescriptors,
  ObjectGetPrototypeOf,
  ObjectKeys,
  ObjectSetPrototypeOf,
  ObjectValues,
  PromiseReject,
  PromiseResolve,
  Proxy,
  ReflectApply,
  ReflectConstruct,
  ReflectGet,
  SafeMap,
  StringPrototypeIncludes,
  StringPrototypeSlice,
  StringPrototypeStartsWith,
} = primordials;
const {
  codes: {
    ERR_INVALID_ARG_TYPE,
    ERR_INVALID_ARG_VALUE,
    ERR_INVALID_STATE,
  },
} = require('internal/errors');
const esmLoader = require('internal/modules/esm/loader');
const { getOptionValue } = require('internal/options');
const {
  fileURLToPath,
  isURL,
  pathToFileURL,
  URLParse,
} = require('internal/url');
const {
  deprecateProperty,
  emitExperimentalWarning,
  getStructuredStack,
  kEmptyObject,
} = require('internal/util');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
  debug = fn;
});
const {
  validateBoolean,
  validateFunction,
  validateInteger,
  validateObject,
  validateOneOf,
} = require('internal/validators');
const { MockTimers } = require('internal/test_runner/mock/mock_timers');
const {
  Module,
} = require('internal/modules/cjs/loader');
const {
  _load,
  _nodeModulePaths,
  _resolveFilename,
  isBuiltin,
} = Module;
function kDefaultFunction() {}
const enableModuleMocking = getOptionValue('--experimental-test-module-mocks');
const kSupportedFormats = [
  'builtin',
  'commonjs-typescript',
  'commonjs',
  'json',
  'module-typescript',
  'module',
];
let sharedModuleState;
const deprecateNamedExports = deprecateProperty(
  'namedExports',
  'mock.module(): options.namedExports is deprecated. Use options.exports instead.',
);
const deprecateDefaultExport = deprecateProperty(
  'defaultExport',
  'mock.module(): options.defaultExport is deprecated. Use options.exports.default instead.',
);
const {
  hooks: mockHooks,
  mocks,
  constants: { kBadExportsMessage, kMockSearchParam },
} = require('internal/test_runner/mock/loader');

class MockFunctionContext {
  #calls;
  #mocks;
  #implementation;
  #restore;
  #times;

  constructor(implementation, restore, times) {
    this.#calls = [];
    this.#mocks = new SafeMap();
    this.#implementation = implementation;
    this.#restore = restore;
    this.#times = times;
  }

  /**
   * Gets an array of recorded calls made to the mock function.
   * @returns {Array} An array of recorded calls.
   */
  get calls() {
    return ArrayPrototypeSlice(this.#calls, 0);
  }

  /**
   * Retrieves the number of times the mock function has been called.
   * @returns {number} The call count.
   */
  callCount() {
    return this.#calls.length;
  }

  /**
   * Sets a new implementation for the mock function.
   * @param {Function} implementation - The new implementation for the mock function.
   */
  mockImplementation(implementation) {
    validateFunction(implementation, 'implementation');
    this.#implementation = implementation;
  }

  /**
   * Replaces the implementation of the function only once.
   * @param {Function} implementation - The substitute function.
   * @param {number} [onCall] - The call index to be replaced.
   */
  mockImplementationOnce(implementation, onCall) {
    validateFunction(implementation, 'implementation');
    const nextCall = this.#calls.length;
    const call = onCall ?? nextCall;
    validateInteger(call, 'onCall', nextCall);
    this.#mocks.set(call, implementation);
  }

  /**
   * Sets a new implementation that always returns the given value.
   * @param {any} value - The value to return.
   */
  mockReturnValue(value) {
    this.#implementation = function() { return value; };
  }

  /**
   * Sets a new implementation that returns a promise resolved with the given value.
   * @param {any} value - The value to resolve with.
   */
  mockResolvedValue(value) {
    this.#implementation = function() { return PromiseResolve(value); };
  }

  /**
   * Sets a new implementation that returns a promise rejected with the given reason.
   * @param {any} reason - The reason to reject with.
   */
  mockRejectedValue(reason) {
    this.#implementation = function() { return PromiseReject(reason); };
  }

  /**
   * Sets a new implementation that always throws the given value.
   * @param {any} value - The value to throw.
   */
  mockThrownValue(value) {
    this.#implementation = function() { throw value; };
  }

  /**
   * Restores the original function that was mocked.
   */
  restore() {
    const { descriptor, object, original, methodName } = this.#restore;

    if (typeof methodName === 'string') {
      // This is an object method spy.
      ObjectDefineProperty(object, methodName, { __proto__: null, ...descriptor });
    } else {
      // This is a bare function spy. There isn't much to do here but make
      // the mock call the original function.
      this.#implementation = original;
    }
  }

  /**
   * Resets the recorded calls to the mock function
   */
  resetCalls() {
    this.#calls = [];
  }

  /**
   * Tracks a call made to the mock function.
   * @param {object} call - The call details.
   */
  trackCall(call) {
    ArrayPrototypePush(this.#calls, call);
  }

  /**
   * Gets the next implementation to use for the mock function.
   * @returns {Function} The next implementation.
   */
  nextImpl() {
    const nextCall = this.#calls.length;
    const mock = this.#mocks.get(nextCall);
    const impl = mock ?? this.#implementation;

    if (nextCall + 1 === this.#times) {
      this.restore();
    }

    this.#mocks.delete(nextCall);
    return impl;
  }
}

const {
  nextImpl,
  restore: restoreFn,
  trackCall,
} = MockFunctionContext.prototype;
delete MockFunctionContext.prototype.trackCall;
delete MockFunctionContext.prototype.nextImpl;

class MockModuleContext {
  #restore;
  #sharedState;

  constructor({
    baseURL,
    cache,
    caller,
    cjsPath,
    format,
    fullPath,
    moduleExports,
    sharedState,
    specifier,
  }) {
    const config = {
      __proto__: null,
      cache,
      moduleExports,
      caller,
    };

    sharedState.mockMap.set(baseURL, config);
    sharedState.mockMap.set(fullPath, config);
    // For dual packages (e.g., a package with a "exports" field that exposes
    // both ESM and CJS entry points), the file selected by the ESM resolver
    // (used to compute fullPath) may differ from the one selected by CJS
    // require(). Register the CJS-resolved path so that require() also picks
    // up the mock. See https://github.com/nodejs/node/issues/58231.
    if (cjsPath !== null && cjsPath !== fullPath) {
      sharedState.mockMap.set(cjsPath, config);
    }

    this.#sharedState = sharedState;
    this.#restore = {
      __proto__: null,
      baseURL,
      cached: fullPath in Module._cache,
      cjsPath,
      cjsCached: cjsPath !== null && cjsPath !== fullPath &&
        cjsPath in Module._cache,
      cjsValue: cjsPath !== null && cjsPath !== fullPath ?
        Module._cache[cjsPath] : undefined,
      format,
      fullPath,
      value: Module._cache[fullPath],
    };

    const mock = mocks.get(baseURL);

    if (mock?.active) {
      debug('already mocking "%s"', baseURL);
      throw new ERR_INVALID_STATE(
        `Cannot mock '${specifier}'. The module is already mocked.`,
      );
    } else {
      const localVersion = mock?.localVersion ?? 0;

      debug('new mock version %d for "%s"', localVersion, baseURL);
      mocks.set(baseURL, {
        __proto__: null,
        url: baseURL,
        cache,
        exportNames: ArrayPrototypeFilter(ObjectKeys(moduleExports), (k) => k !== 'default'),
        hasDefaultExport: 'default' in moduleExports,
        format,
        localVersion,
        active: true,
      });
    }

    delete Module._cache[fullPath];
    if (cjsPath !== null && cjsPath !== fullPath) {
      delete Module._cache[cjsPath];
    }
    sharedState.mockExports.set(baseURL, {
      __proto__: null,
      moduleExports,
    });
  }

  restore() {
    if (this.#restore === undefined) {
      return;
    }

    // Delete the mock CJS cache entry. If the module was previously in the
    // cache then restore the old value.
    delete Module._cache[this.#restore.fullPath];

    if (this.#restore.cached) {
      Module._cache[this.#restore.fullPath] = this.#restore.value;
    }

    if (this.#restore.cjsPath !== null &&
        this.#restore.cjsPath !== this.#restore.fullPath) {
      delete Module._cache[this.#restore.cjsPath];
      if (this.#restore.cjsCached) {
        Module._cache[this.#restore.cjsPath] = this.#restore.cjsValue;
      }
    }

    const mock = mocks.get(this.#restore.baseURL);

    if (mock !== undefined) {
      mock.active = false;
      mock.localVersion++;
    }

    this.#sharedState.mockMap.delete(this.#restore.baseURL);
    this.#sharedState.mockMap.delete(this.#restore.fullPath);
    if (this.#restore.cjsPath !== null &&
        this.#restore.cjsPath !== this.#restore.fullPath) {
      this.#sharedState.mockMap.delete(this.#restore.cjsPath);
    }
    this.#restore = undefined;
  }
}

const { restore: restoreModule } = MockModuleContext.prototype;

class MockPropertyContext {
  #object;
  #propertyName;
  #value;
  #originalValue;
  #descriptor;
  #accesses;
  #onceValues;

  constructor(object, propertyName, value) {
    this.#onceValues = new SafeMap();
    this.#accesses = [];
    this.#object = object;
    this.#propertyName = propertyName;
    this.#originalValue = object[propertyName];
    this.#value = arguments.length > 2 ? value : this.#originalValue;
    this.#descriptor = ObjectGetOwnPropertyDescriptor(object, propertyName);
    if (!this.#descriptor) {
      throw new ERR_INVALID_ARG_VALUE(
        'propertyName', propertyName, 'is not a property of the object',
      );
    }

    const { configurable, enumerable } = this.#descriptor;
    ObjectDefineProperty(object, propertyName, {
      __proto__: null,
      configurable,
      enumerable,
      get: () => {
        const nextValue = this.#getAccessValue(this.#value);
        const access = {
          __proto__: null,
          type: 'get',
          value: nextValue,
          // eslint-disable-next-line no-restricted-syntax
          stack: new Error(),
        };
        ArrayPrototypePush(this.#accesses, access);
        return nextValue;
      },
      set: this.mockImplementation.bind(this),
    });
  }

  /**
   * Gets an array of recorded accesses (get/set) to the property.
   * @returns {Array} An array of access records.
   */
  get accesses() {
    return ArrayPrototypeSlice(this.#accesses, 0);
  }

  /**
   * Retrieves the number of times the property was accessed (get or set).
   * @returns {number} The total number of accesses.
   */
  accessCount() {
    return this.#accesses.length;
  }

  /**
   * Sets a new value for the property.
   * @param {any} value - The new value to be set.
   * @throws {Error} If the property is not writable.
   */
  mockImplementation(value) {
    if (!this.#descriptor.writable) {
      throw new ERR_INVALID_ARG_VALUE(
        'propertyName', this.#propertyName, 'cannot be set',
      );
    }
    const nextValue = this.#getAccessValue(value);
    const access = {
      __proto__: null,
      type: 'set',
      value: nextValue,
      // eslint-disable-next-line no-restricted-syntax
      stack: new Error(),
    };
    ArrayPrototypePush(this.#accesses, access);
    this.#value = nextValue;
  }

  #getAccessValue(value) {
    const accessIndex = this.#accesses.length;
    let accessValue;
    if (this.#onceValues.has(accessIndex)) {
      accessValue = this.#onceValues.get(accessIndex);
      this.#onceValues.delete(accessIndex);
    } else {
      accessValue = value;
    }
    return accessValue;
  }

  /**
   * Sets a value to be used only for the next access (get or set), or a specific access index.
   * @param {any} value - The value to be used once.
   * @param {number} [onAccess] - The access index to be replaced.
   */
  mockImplementationOnce(value, onAccess) {
    const nextAccess = this.#accesses.length;
    const accessIndex = onAccess ?? nextAccess;
    validateInteger(accessIndex, 'onAccess', nextAccess);
    this.#onceValues.set(accessIndex, value);
  }

  /**
   * Resets the recorded accesses to the property.
   */
  resetAccesses() {
    this.#accesses = [];
  }

  /**
   * Restores the original value of the property that was mocked.
   */
  restore() {
    ObjectDefineProperty(this.#object, this.#propertyName, {
      __proto__: null,
      ...this.#descriptor,
      value: this.#originalValue,
    });
  }
}

const { restore: restoreProperty } = MockPropertyContext.prototype;

class MockTracker {
  #mocks = [];
  #timers;

  /**
   * Returns the mock timers of this MockTracker instance.
   * @returns {MockTimers} The mock timers instance.
   */
  get timers() {
    this.#timers ??= new MockTimers();
    return this.#timers;
  }

  /**
   * Creates a mock function tracker.
   * @param {Function} [original] - The original function to be tracked.
   * @param {Function} [implementation] - An optional replacement function for the original one.
   * @param {object} [options] - Additional tracking options.
   * @param {number} [options.times] - The maximum number of times the mock function can be called.
   * @returns {ProxyConstructor} The mock function tracker.
   */
  fn(
    original = function() {},
    implementation = original,
    options = kEmptyObject,
  ) {
    if (original !== null && typeof original === 'object') {
      options = original;
      original = function() {};
      implementation = original;
    } else if (implementation !== null && typeof implementation === 'object') {
      options = implementation;
      implementation = original;
    }

    validateFunction(original, 'original');
    validateFunction(implementation, 'implementation');
    validateObject(options, 'options');
    const { times = Infinity } = options;
    validateTimes(times, 'options.times');
    const ctx = new MockFunctionContext(implementation, { __proto__: null, original }, times);
    return this.#setupMock(ctx, original);
  }

  /**
   * Creates a method tracker for a specified object or function.
   * @param {(object | Function)} objectOrFunction - The object or function containing the method to be tracked.
   * @param {string} methodName - The name of the method to be tracked.
   * @param {Function} [implementation] - An optional replacement function for the original method.
   * @param {object} [options] - Additional tracking options.
   * @param {boolean} [options.getter] - Indicates whether this is a getter method.
   * @param {boolean} [options.setter] - Indicates whether this is a setter method.
   * @param {number} [options.times] - The maximum number of times the mock method can be called.
   * @returns {ProxyConstructor} The mock method tracker.
   */
  method(
    objectOrFunction,
    methodName,
    implementation = kDefaultFunction,
    options = kEmptyObject,
  ) {
    validateStringOrSymbol(methodName, 'methodName');
    if (typeof objectOrFunction !== 'function') {
      validateObject(objectOrFunction, 'object');
    }

    if (implementation !== null && typeof implementation === 'object') {
      options = implementation;
      implementation = kDefaultFunction;
    }

    validateFunction(implementation, 'implementation');
    validateObject(options, 'options');

    const {
      getter = false,
      setter = false,
      times = Infinity,
    } = options;

    validateBoolean(getter, 'options.getter');
    validateBoolean(setter, 'options.setter');
    validateTimes(times, 'options.times');

    if (setter && getter) {
      throw new ERR_INVALID_ARG_VALUE(
        'options.setter', setter, "cannot be used with 'options.getter'",
      );
    }
    const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName);

    let original;

    if (getter) {
      original = descriptor?.get;
    } else if (setter) {
      original = descriptor?.set;
    } else {
      original = descriptor?.value;
    }

    if (typeof original !== 'function') {
      throw new ERR_INVALID_ARG_VALUE(
        'methodName', original, 'must be a method',
      );
    }

    const restore = { __proto__: null, descriptor, object: objectOrFunction, methodName };
    const impl = implementation === kDefaultFunction ?
      original : implementation;
    const ctx = new MockFunctionContext(impl, restore, times);
    const mock = this.#setupMock(ctx, original);
    const mockDescriptor = {
      __proto__: null,
      configurable: descriptor.configurable,
      enumerable: descriptor.enumerable,
    };

    if (getter) {
      mockDescriptor.get = mock;
      mockDescriptor.set = descriptor.set;
    } else if (setter) {
      mockDescriptor.get = descriptor.get;
      mockDescriptor.set = mock;
    } else {
      mockDescriptor.writable = descriptor.writable;
      mockDescriptor.value = mock;
    }

    ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor);

    return mock;
  }

  /**
   * Mocks a getter method of an object.
   * This is a syntax sugar for the MockTracker.method with options.getter set to true
   * @param {object} object - The target object.
   * @param {string} methodName - The name of the getter method to be mocked.
   * @param {Function} [implementation] - An optional replacement function for the targeted method.
   * @param {object} [options] - Additional tracking options.
   * @param {boolean} [options.getter] - Indicates whether this is a getter method.
   * @param {boolean} [options.setter] - Indicates whether this is a setter method.
   * @param {number} [options.times] - The maximum number of times the mock method can be called.
   * @returns {ProxyConstructor} The mock method tracker.
   */
  getter(
    object,
    methodName,
    implementation = kDefaultFunction,
    options = kEmptyObject,
  ) {
    if (implementation !== null && typeof implementation === 'object') {
      options = implementation;
      implementation = kDefaultFunction;
    } else {
      validateObject(options, 'options');
    }

    const { getter = true } = options;

    if (getter === false) {
      throw new ERR_INVALID_ARG_VALUE(
        'options.getter', getter, 'cannot be false',
      );
    }

    return this.method(object, methodName, implementation, {
      __proto__: null,
      ...options,
      getter,
    });
  }

  /**
   * Mocks a setter method of an object.
   * This function is a syntax sugar for MockTracker.method with options.setter set to true.
   * @param {object} object - The target object.
   * @param {string} methodName - The setter method to be mocked.
   * @param {Function} [implementation] - An optional replacement function for the targeted method.
   * @param {object} [options] - Additional tracking options.
   * @param {boolean} [options.getter] - Indicates whether this is a getter method.
   * @param {boolean} [options.setter] - Indicates whether this is a setter method.
   * @param {number} [options.times] - The maximum number of times the mock method can be called.
   * @returns {ProxyConstructor} The mock method tracker.
   */
  setter(
    object,
    methodName,
    implementation = kDefaultFunction,
    options = kEmptyObject,
  ) {
    if (implementation !== null && typeof implementation === 'object') {
      options = implementation;
      implementation = kDefaultFunction;
    } else {
      validateObject(options, 'options');
    }

    const { setter = true } = options;

    if (setter === false) {
      throw new ERR_INVALID_ARG_VALUE(
        'options.setter', setter, 'cannot be false',
      );
    }

    return this.method(object, methodName, implementation, {
      __proto__: null,
      ...options,
      setter,
    });
  }

  module(specifier, options = kEmptyObject) {
    emitExperimentalWarning('Module mocking');
    if (typeof specifier !== 'string') {
      if (!isURL(specifier))
        throw new ERR_INVALID_ARG_TYPE('specifier', ['string', 'URL'], specifier);
      specifier = `${specifier}`;
    }
    validateObject(options, 'options');
    debug('module mock entry, specifier = "%s", options = %o', specifier, options);

    const {
      cache,
      moduleExports,
    } = normalizeModuleMockOptions(options);

    const sharedState = setupSharedModuleState();
    const mockSpecifier = StringPrototypeStartsWith(specifier, 'node:') ?
      StringPrototypeSlice(specifier, 5) : specifier;

    // Get the file that called this function. We need four stack frames:
    // vm context -> getStructuredStack() -> this function -> actual caller.
    const filename = getStructuredStack()[3]?.getFileName();
    // If the caller is already a file URL, use it as is. Otherwise, convert it.
    const hasFileProtocol = StringPrototypeStartsWith(filename, 'file://');
    const caller = hasFileProtocol ? filename : pathToFileURL(filename).href;
    const request = { __proto__: null, specifier: mockSpecifier, attributes: kEmptyObject };
    const { format, url } = sharedState.moduleLoader.resolveSync(caller, request);
    debug('module mock, url = "%s", format = "%s", caller = "%s"', url, format, caller);
    if (format) { // Format is not yet known for ambiguous files when detection is enabled.
      validateOneOf(format, 'format', kSupportedFormats);
    }
    const baseURL = URLParse(url);

    if (!baseURL) {
      throw new ERR_INVALID_ARG_VALUE(
        'specifier', specifier, 'cannot compute URL',
      );
    }

    if (baseURL.searchParams.has(kMockSearchParam)) {
      throw new ERR_INVALID_STATE(
        `Cannot mock '${specifier}'. The module is already mocked.`,
      );
    }

    const fullPath = StringPrototypeStartsWith(url, 'file://') ?
      fileURLToPath(url) : null;
    // For dual packages, the ESM resolver may return a different file than
    // CJS require() would for the same specifier (e.g., when a package's
    // "exports" field points to different files for the "import" and
    // "require" conditions). Compute the CJS-resolved path so that
    // require() of a mocked module also picks up the mock.
    // See https://github.com/nodejs/node/issues/58231.
    const cjsPath = resolveAsCJS(mockSpecifier, caller, fullPath);
    const ctx = new MockModuleContext({
      __proto__: null,
      baseURL: baseURL.href,
      cache,
      caller,
      cjsPath,
      format,
      fullPath,
      moduleExports,
      sharedState,
      specifier: mockSpecifier,
    });

    ArrayPrototypePush(this.#mocks, {
      __proto__: null,
      ctx,
      restore: restoreModule,
    });
    return ctx;
  }

  /**
   * Creates a property tracker for a specified object.
   * @param {(object)} object - The object whose value is being tracked.
   * @param {string} propertyName - The identifier of the property on object to be tracked.
   * @param {any} value - An optional replacement value used as the mock value for object[valueName].
   * @returns {ProxyConstructor} The mock property tracker.
   */
  property(
    object,
    propertyName,
    value,
  ) {
    validateObject(object, 'object');
    validateStringOrSymbol(propertyName, 'propertyName');

    const ctx = arguments.length > 2 ?
      new MockPropertyContext(object, propertyName, value) :
      new MockPropertyContext(object, propertyName);
    ArrayPrototypePush(this.#mocks, {
      __proto__: null,
      ctx,
      restore: restoreProperty,
    });

    return new Proxy(object, {
      __proto__: null,
      get(target, property, receiver) {
        if (property === 'mock') {
          return ctx;
        }
        return ReflectGet(target, property, receiver);
      },
    });
  }

  /**
   * Resets the mock tracker, restoring all mocks and clearing timers.
   */
  reset() {
    this.restoreAll();
    this.#timers?.reset();
    this.#mocks = [];
  }

  /**
   * Restore all mocks created by this MockTracker instance.
   */
  restoreAll() {
    for (let i = 0; i < this.#mocks.length; i++) {
      const { ctx, restore } = this.#mocks[i];

      FunctionPrototypeCall(restore, ctx);
    }
  }

  #setupMock(ctx, fnToMatch) {
    const mock = new Proxy(fnToMatch, {
      __proto__: null,
      apply(_fn, thisArg, argList) {
        const fn = FunctionPrototypeCall(nextImpl, ctx);
        let result;
        let error;

        try {
          result = ReflectApply(fn, thisArg, argList);
        } catch (err) {
          error = err;
          throw err;
        } finally {
          FunctionPrototypeCall(trackCall, ctx, {
            __proto__: null,
            arguments: argList,
            error,
            result,
            // eslint-disable-next-line no-restricted-syntax
            stack: new Error(),
            target: undefined,
            this: thisArg,
          });
        }

        return result;
      },
      construct(target, argList, newTarget) {
        const realTarget = FunctionPrototypeCall(nextImpl, ctx);
        let result;
        let error;

        try {
          result = ReflectConstruct(realTarget, argList, newTarget);
        } catch (err) {
          error = err;
          throw err;
        } finally {
          FunctionPrototypeCall(trackCall, ctx, {
            __proto__: null,
            arguments: argList,
            error,
            result,
            // eslint-disable-next-line no-restricted-syntax
            stack: new Error(),
            target,
            this: result,
          });
        }

        return result;
      },
      get(target, property, receiver) {
        if (property === 'mock') {
          return ctx;
        }

        return ReflectGet(target, property, receiver);
      },
    });

    ArrayPrototypePush(this.#mocks, {
      __proto__: null,
      ctx,
      restore: restoreFn,
    });
    return mock;
  }
}

function normalizeModuleMockOptions(options) {
  const { cache = false } = options;
  validateBoolean(cache, 'options.cache');

  const hasExports = 'exports' in options;
  const hasNamedExports = 'namedExports' in options;
  const hasDefaultExport = 'defaultExport' in options;

  deprecateNamedExports(options);
  deprecateDefaultExport(options);

  const moduleExports = { __proto__: null };

  if (hasExports) {
    validateObject(options.exports, 'options.exports');
  }

  if (hasNamedExports) {
    validateObject(options.namedExports, 'options.namedExports');
  }

  if (hasExports && (hasNamedExports || hasDefaultExport)) {
    let reason = "cannot be used with 'options.namedExports'";

    if (hasDefaultExport) {
      reason = hasNamedExports ?
        "cannot be used with 'options.namedExports' or 'options.defaultExport'" :
        "cannot be used with 'options.defaultExport'";
    }

    throw new ERR_INVALID_ARG_VALUE('options.exports', options.exports, reason);
  }

  if (hasExports) {
    copyOwnProperties(options.exports, moduleExports);
  }

  if (hasNamedExports) {
    copyOwnProperties(options.namedExports, moduleExports);
  }

  if (hasDefaultExport) {
    ObjectDefineProperty(
      moduleExports,
      'default',
      ObjectAssign({ __proto__: null }, ObjectGetOwnPropertyDescriptor(options, 'defaultExport')),
    );
  }

  return {
    __proto__: null,
    cache,
    moduleExports,
  };
}


function copyOwnProperties(from, to) {
  const descriptors = ObjectGetOwnPropertyDescriptors(from);

  const descValues = ObjectValues(descriptors);
  for (let i = 0; i < descValues.length; ++i) {
    ObjectSetPrototypeOf(descValues[i], null);
  }

  ObjectDefineProperties(to, descriptors);
}

function setupSharedModuleState() {
  if (sharedModuleState === undefined) {
    const { mock } = require('test');
    const mockExports = new SafeMap();
    const { registerHooks } = require('internal/modules/customization_hooks');
    const moduleLoader = esmLoader.getOrInitializeCascadedLoader();

    registerHooks(mockHooks);

    sharedModuleState = {
      __proto__: null,
      mockExports,
      mockMap: new SafeMap(),
      moduleLoader,
    };
    mock._mockExports = mockExports;
    Module._load = FunctionPrototypeBind(cjsMockModuleLoad, sharedModuleState);
  }

  return sharedModuleState;
}

function cjsMockModuleLoad(request, parent, isMain) {
  // Imported mocked URLs may re-enter Module._load with the mock query attached.
  // Strip it to pass into methods that expect a normal request.
  // TODO(joyeecheung): it might be better to strip the search params from the filename in
  // the translator but that might have a bigger blast radius as other mocker might have also
  // come to rely on this to create multiple cache identities for the same module.
  try {
    const parsedRequest = URLParse(request);
    if (parsedRequest?.searchParams.has(kMockSearchParam)) {
      parsedRequest.searchParams.delete(kMockSearchParam);
      request = parsedRequest.href;
    }
  } catch {
    // Not a valid URL, treat as a normal request.
  }

  let resolved;

  if (isBuiltin(request)) {
    resolved = ensureNodeScheme(request);
  } else {
    resolved = _resolveFilename(request, parent, isMain);
  }

  const config = this.mockMap.get(resolved);
  if (config === undefined) {
    return _load(request, parent, isMain);
  }

  const {
    cache,
    caller,
    moduleExports,
  } = config;

  if (cache && Module._cache[resolved]) {
    // The CJS cache entry is deleted when the mock is configured. If it has
    // been repopulated, return the exports from that entry.
    return Module._cache[resolved].exports;
  }

  const hasDefaultExport = 'default' in moduleExports;
  // eslint-disable-next-line node-core/set-proto-to-null-in-object
  const modExports = hasDefaultExport ? moduleExports.default : {};
  const exportNames = ArrayPrototypeFilter(ObjectKeys(moduleExports), (k) => k !== 'default');

  if ((typeof modExports !== 'object' || modExports === null) &&
      exportNames.length > 0) {
    // eslint-disable-next-line no-restricted-syntax
    throw new Error(kBadExportsMessage);
  }

  for (let i = 0; i < exportNames.length; ++i) {
    const name = exportNames[i];
    const descriptor = ObjectGetOwnPropertyDescriptor(moduleExports, name);
    ObjectDefineProperty(modExports, name, descriptor);
  }

  if (cache) {
    const entry = new Module(resolved, caller);

    entry.exports = modExports;
    entry.filename = resolved;
    entry.loaded = true;
    entry.paths = _nodeModulePaths(entry.path);
    Module._cache[resolved] = entry;
  }

  return modExports;
}

// Resolve `specifier` using CJS resolution rules so that mocks for dual
// packages (e.g., a package whose "exports" field points to different files
// for the "import" and "require" conditions) also intercept require().
// Returns an absolute file path on success, or null when the specifier cannot
// be resolved as CJS (for example, when the package is ESM-only or when it is
// a non-file URL such as data: or node:).
function resolveAsCJS(specifier, callerURL, esmFullPath) {
  if (isBuiltin(specifier) ||
      StringPrototypeStartsWith(specifier, 'node:') ||
      StringPrototypeStartsWith(specifier, 'data:')) {
    return null;
  }

  let parentPath;
  if (StringPrototypeStartsWith(callerURL, 'file://')) {
    try {
      parentPath = fileURLToPath(callerURL);
    } catch {
      return null;
    }
  } else {
    return null;
  }

  try {
    const tmpModule = new Module(parentPath, null);
    tmpModule.paths = _nodeModulePaths(parentPath);
    const resolved = _resolveFilename(specifier, tmpModule, false);
    if (typeof resolved !== 'string') {
      return null;
    }
    // If the resolution matches what the ESM resolver picked, there is
    // nothing additional to register.
    if (resolved === esmFullPath) {
      return esmFullPath;
    }
    // If the resolution returned something that is not a filesystem path
    // (e.g., a builtin id without a slash or backslash), ignore it.
    if (!StringPrototypeIncludes(resolved, '/') &&
        !StringPrototypeIncludes(resolved, '\\')) {
      return null;
    }
    return resolved;
  } catch {
    return null;
  }
}

function validateStringOrSymbol(value, name) {
  if (typeof value !== 'string' && typeof value !== 'symbol') {
    throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value);
  }
}

function validateTimes(value, name) {
  if (value === Infinity) {
    return;
  }

  validateInteger(value, name, 1);
}

function findMethodOnPrototypeChain(instance, methodName) {
  let host = instance;
  let descriptor;

  while (host !== null) {
    descriptor = ObjectGetOwnPropertyDescriptor(host, methodName);

    if (descriptor) {
      break;
    }

    host = ObjectGetPrototypeOf(host);
  }

  return descriptor;
}

function ensureNodeScheme(specifier) {
  if (!StringPrototypeStartsWith(specifier, 'node:')) {
    return `node:${specifier}`;
  }

  return specifier;
}

if (!enableModuleMocking) {
  delete MockTracker.prototype.module;
}

module.exports = {
  ensureNodeScheme,
  MockFunctionContext,
  MockTracker,
};