current commit
261006.10 · module: synchronously load most ES modules
'use strict';

const {
  Array,
  ArrayIsArray,
  ArrayPrototypeFind,
  ArrayPrototypeJoin,
  ArrayPrototypePop,
  ArrayPrototypePush,
  ArrayPrototypeSort,
  FunctionPrototype,
  ObjectAssign,
  ObjectSetPrototypeOf,
  PromisePrototypeThen,
  PromiseResolve,
  RegExpPrototypeExec,
  RegExpPrototypeSymbolReplace,
  RegExpPrototypeSymbolSplit,
  SafePromiseAllReturnArrayLike,
  SafePromiseAllReturnVoid,
  SafeSet,
  StringPrototypeIncludes,
  StringPrototypeSplit,
  StringPrototypeStartsWith,
  globalThis,
} = primordials;
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
  debug = fn;
});

const {
  ModuleWrap,
  kErrored,
  kEvaluated,
  kEvaluating,
  kDeferPhase,
  kEvaluationPhase,
  kInstantiated,
  kUninstantiated,
} = internalBinding('module_wrap');
const {
  getPromiseDetails,
  privateSymbols: {
    entry_point_module_private_symbol,
    module_source_private_symbol: kModuleSource,
  },
} = internalBinding('util');
/**
 * @typedef {import('./utils.js').ModuleRequestType} ModuleRequestType
 */
const { decorateErrorStack } = require('internal/util');
const { isPromise } = require('internal/util/types');
const {
  getSourceMapsSupport,
} = require('internal/source_map/source_map_cache');
const assert = require('internal/assert');
let resolvedPromise;
const {
  setHasStartedUserESMExecution,
  urlToFilename,
} = require('internal/modules/helpers');
const { getOptionValue } = require('internal/options');
const noop = FunctionPrototype;
const {
  ERR_REQUIRE_ASYNC_MODULE,
  ERR_REQUIRE_ESM_RACE_CONDITION,
} = require('internal/errors').codes;

const CJSGlobalLike = [
  'require',
  'module',
  'exports',
  '__filename',
  '__dirname',
];
const findCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
  ArrayPrototypeFind(
    CJSGlobalLike,
    (globalLike) => errorMessage === `${globalLike} is not defined`,
  );


/**
 *
 * @param {Error} e
 * @param {string} url
 * @returns {void}
 */
const explainCommonJSGlobalLikeNotDefinedError = (e, url, hasTopLevelAwait) => {
  const notDefinedGlobalLike = e?.name === 'ReferenceError' && findCommonJSGlobalLikeNotDefinedError(e.message);

  if (notDefinedGlobalLike) {
    if (hasTopLevelAwait) {
      let advice;
      switch (notDefinedGlobalLike) {
        case 'require':
          advice = 'replace require() with import';
          break;
        case 'module':
        case 'exports':
          advice = 'use export instead of module.exports/exports';
          break;
        case '__filename':
          advice = 'use import.meta.filename instead';
          break;
        case '__dirname':
          advice = 'use import.meta.dirname instead';
          break;
      }

      e.message = `Cannot determine intended module format because both '${notDefinedGlobalLike}' and top-level await are present. If the code is intended to be CommonJS, wrap await in an async function. If the code is intended to be an ES module, ${advice}.`;
      e.code = 'ERR_AMBIGUOUS_MODULE_SYNTAX';
      return;
    }

    e.message += ' in ES module scope';

    if (StringPrototypeStartsWith(e.message, 'require ')) {
      e.message += ', you can use import instead';
    }

    const packageConfig =
      StringPrototypeStartsWith(url, 'file://') &&
        RegExpPrototypeExec(/\.js(\?[^#]*)?(#.*)?$/, url) !== null &&
        require('internal/modules/package_json_reader')
          .getPackageScopeConfig(url);
    if (packageConfig.type === 'module') {
      e.message +=
        '\nThis file is being treated as an ES module because it has a ' +
        `'.js' file extension and '${packageConfig.pjsonPath}' contains ` +
        '"type": "module". To treat it as a CommonJS script, rename it ' +
        'to use the \'.cjs\' file extension.';
    }
  }
};

/**
 * @typedef {object} TopLevelAwaitLocation
 * @property {string} url URL of the module containing the top-level await.
 * @property {number} line 1-based line number of the top-level await.
 * @property {number} column 1-based column number of the top-level await.
 * @property {string} sourceLine The source line containing the top-level await.
 */

/**
 * Locate the top-level awaits in the given module by parsing the source with acorn.
 * @param {string} source Module source code.
 * @returns {object[]} The acorn AST nodes of the top-level awaits, in source order.
 */
function findTopLevelAwait(source) {
  const { Parser } = require('internal/deps/acorn/acorn/dist/acorn');
  const walk = require('internal/deps/acorn/acorn-walk/dist/walk');
  let ast;
  try {
    ast = Parser.parse(source, {
      __proto__: null, ecmaVersion: 'latest', sourceType: 'module', locations: true,
    });
  } catch {
    return [];  // The source is not parsable, skip.
  }
  // We are looking for _top-level_ await, so we don't traverse into function bodies.
  const baseVisitor = ObjectAssign({ __proto__: null }, walk.base, { Function: noop });
  const found = [];
  walk.simple(ast, {
    __proto__: null,
    AwaitExpression(node) { ArrayPrototypePush(found, node); },
    // `for await (...)` is a ForOfStatement with `await: true`, not an AwaitExpression.
    ForOfStatement(node) {
      if (node.await) { ArrayPrototypePush(found, node); }
    },
    // `await using x = ...` is a VariableDeclaration, not an AwaitExpression.
    VariableDeclaration(node) {
      if (node.kind === 'await using') { ArrayPrototypePush(found, node); }
    },
  }, baseVisitor);
  ArrayPrototypeSort(found, (a, b) => a.start - b.start);
  return found;
}

/**
 * Collect the modules that contain top-level await in the linked graph of a job.
 * @param {ModuleJobBase} root The root of the module graph to search.
 * @returns {ModuleWrap[]} Modules that contain top-level await.
 */
function findModulesWithTopLevelAwait(root) {
  const found = [];
  const seen = new SafeSet();
  const stack = [root];
  while (stack.length > 0) {
    const job = ArrayPrototypePop(stack);
    if (seen.has(job)) { continue; }
    seen.add(job);
    if (job.module?.hasTopLevelAwait) {
      ArrayPrototypePush(found, job.module);
    }
    let linked = job.linked;
    if (isPromise(linked)) {
      linked = getPromiseDetails(linked)?.[1];
    }
    // If `require(esm)` comes from the deprecated async loader hook worker thread,
    // linked may be pending at this point. In that case, this branch would be skipped -
    // we just allow lossy reporting of TLA locations in an edge case when a deprecated
    // feature is used in combination with another experimental flag.
    if (ArrayIsArray(linked)) {
      for (let i = 0; i < linked.length; i++) {
        ArrayPrototypePush(stack, linked[i]);
      }
    }
  }
  return found;
}

/**
 * Locate the top-level awaits in the given modules.
 * @param {ModuleJobBase} root The root of the module graph to search.
 * @returns {TopLevelAwaitLocation[]} The locations of the top-level awaits.
 */
function getTopLevelAwaitLocations(root) {
  const modules = findModulesWithTopLevelAwait(root);
  const locations = [];
  for (let i = 0; i < modules.length; i++) {
    const module = modules[i];
    const source = module[kModuleSource];
    if (typeof source !== 'string') { continue; }  // Not retained during compilation. Skip.
    const found = findTopLevelAwait(source);
    if (found.length === 0) { continue; }
    const lines = RegExpPrototypeSymbolSplit(/\r?\n/, source);
    for (let j = 0; j < found.length; j++) {
      const { start } = found[j].loc;
      ArrayPrototypePush(locations, {
        __proto__: null,
        url: module.url,
        line: start.line,
        // Acorn reports 0-based columns, convert them to 1-based to match `line`.
        column: start.column + 1,
        sourceLine: lines[start.line - 1],
      });
    }
  }
  return locations;
}

/**
 * If `error` is a SyntaxError from V8 for a missing named export on a CJS module, rewrite its message to the friendlier
 * "Named export '...' not found..." form. Must be called after `decorateErrorStack(error)` so that the arrow (source
 * context with the import statement text) has been prepended to `error.stack`.
 * @param {Error} error
 * @param {ModuleWrap} module The parent module that triggered the instantiation.
 * @param {boolean[]} commonJsDeps Per-request array indicating whether each dependency is a CJS module, aligned with
 *   `module.getModuleRequests()`.
 */
const handleCJSNamedExportError = (error, module, commonJsDeps) => {
  // TODO(@bcoe): Add source map support to exception that occurs as result
  // of missing named export. This is currently not possible because
  // stack trace originates in module_job, not the file itself. A hidden
  // symbol with filename could be set in node_errors.cc to facilitate this.
  if (!getSourceMapsSupport().enabled &&
      StringPrototypeIncludes(error.message,
                              ' does not provide an export named')) {
    const splitStack = StringPrototypeSplit(error.stack, '\n', 2);
    const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(
      /module '(.*)' does not provide an export named '(.+)'/,
      error.message);
    const moduleRequests = module.getModuleRequests();
    let isCommonJS = false;
    for (let i = 0; i < moduleRequests.length; ++i) {
      if (moduleRequests[i].specifier === childSpecifier) {
        isCommonJS = commonJsDeps[i];
        break;
      }
    }
    if (isCommonJS) {
      const importStatement = splitStack[1];
      // TODO(@ctavan): The original error stack only provides the single
      // line which causes the error. For multi-line import statements we
      // cannot generate an equivalent object destructuring assignment by
      // just parsing the error stack.
      const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement);
      const destructuringAssignment = oneLineNamedImports &&
        RegExpPrototypeSymbolReplace(/\s+as\s+/g, oneLineNamedImports, ': ');
      error.message = `Named export '${name}' not found. The requested module` +
        ` '${childSpecifier}' is a CommonJS module, which may not support` +
        ' all module.exports as named exports.\nCommonJS modules can ' +
        'always be imported via the default export, for example using:' +
        `\n\nimport pkg from '${childSpecifier}';\n${
          destructuringAssignment ?
            `const ${destructuringAssignment} = pkg;\n` : ''}`;
      const newStack = StringPrototypeSplit(error.stack, '\n');
      newStack[3] = `SyntaxError: ${error.message}`;
      error.stack = ArrayPrototypeJoin(newStack, '\n');
    }
  }
};

class ModuleJobBase {
  constructor(loader, url, importAttributes, phase, isMain, inspectBrk) {
    assert(typeof phase === 'number');
    this.loader = loader;
    this.importAttributes = importAttributes;
    this.phase = phase;
    this.isMain = isMain;
    this.inspectBrk = inspectBrk;

    this.url = url;
  }

  /**
   * Synchronously link the module and its dependencies.
   * @param {ModuleRequestType} requestType Type of the module request.
   * @returns {ModuleJobBase[]}
   */
  syncLink(requestType) {
    // Store itself into the cache first before linking in case there are circular
    // references in the linking. Track whether we're overwriting an existing entry
    // so we know whether to remove the temporary entry in the finally block.
    const hadPreviousEntry = this.loader.loadCache.get(this.url, this.type) !== undefined;
    this.loader.loadCache.set(this.url, this.type, this);
    const moduleRequests = this.module.getModuleRequests();
    // Modules should be aligned with the moduleRequests array in order.
    const modules = Array(moduleRequests.length);
    const evaluationDepJobs = [];
    this.commonJsDeps = Array(moduleRequests.length);
    try {
      for (let idx = 0; idx < moduleRequests.length; idx++) {
        const request = moduleRequests[idx];
        // TODO(joyeecheung): split this into two iterators, one for resolving and one for loading so
        // that hooks can pre-fetch sources off-thread.
        const job = this.loader.getOrCreateModuleJob(this.url, request, requestType);
        debug(`ModuleJobBase.syncLink() ${this.url} -> ${request.specifier}`, job);
        assert(!isPromise(job));
        assert(job.module instanceof ModuleWrap);
        if (this.shouldRunModule(request.phase)) {
          ArrayPrototypePush(evaluationDepJobs, job);
        }
        modules[idx] = job.module;
        this.commonJsDeps[idx] = job.module.isCommonJS;
      }
      this.module.link(modules);
    } finally {
      if (!hadPreviousEntry) {
        // Remove the temporary entry. On failure this ensures subsequent attempts
        // don't return a broken job. On success the caller
        // (#getOrCreateModuleJobAfterResolve) will re-insert under the correct key.
        this.loader.loadCache.delete(this.url, this.type);
      }
      // If there was a previous entry (ensurePhase() path), leave this in cache -
      // it is the upgraded job and the caller will not re-insert.
    }

    return evaluationDepJobs;
  }

  /**
   * If the require()'d graph contains top-level await, collect the source locations
   * of the top-level awaits using source code retained during compilation and throw
   * ERR_REQUIRE_ASYNC_MODULE. The module must be at least instantiated.
   * @param {Module|undefined} parent CommonJS module that require()'d this, if any.
   */
  throwIfAsyncGraph(parent) {
    if (!this.module.hasAsyncGraph) {
      return;
    }
    const locations = getOptionValue('--experimental-print-required-tla') ? getTopLevelAwaitLocations(this) : [];
    const filename = urlToFilename(this.url);
    throw new ERR_REQUIRE_ASYNC_MODULE(filename, parent, locations);
  }

  /**
   * Ensure that this ModuleJob is moving towards the required phase
   * (does not necessarily mean it is ready at that phase - run does that)
   * @param {number} phase
   */
  ensurePhase(phase, requestType) {
    if (this.phase < phase) {
      this.phase = phase;
      this.linked = this.link(requestType);
      if (isPromise(this.linked)) {
        PromisePrototypeThen(this.linked, undefined, noop);
      }
    }
  }

  shouldLinkModule(phase) {
    return phase >= kDeferPhase;
  }
  shouldRunModule(phase) {
    return phase === kEvaluationPhase;
  }
}

/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of
 * its dependencies, over time. */
class ModuleJob extends ModuleJobBase {
  /**
   * @param {ModuleLoader} loader The ESM loader.
   * @param {string} url URL of the module to be wrapped in ModuleJob.
   * @param {ImportAttributes} importAttributes Import attributes from the import statement.
   * @param {ModuleWrap|Promise<ModuleWrap>} moduleOrModulePromise Translated ModuleWrap for the module.
   * @param {number} phase The phase to load the module to.
   * @param {boolean} isMain Whether the module is the entry point.
   * @param {boolean} inspectBrk Whether this module should be evaluated with the
   *   first line paused in the debugger (because --inspect-brk is passed).
   * @param {ModuleRequestType} requestType Type of the module request.
   */
  constructor(loader, url, importAttributes = { __proto__: null }, moduleOrModulePromise,
              phase = kEvaluationPhase, isMain, inspectBrk, requestType) {
    super(loader, url, importAttributes, phase, isMain, inspectBrk);

    // Expose the promise to the ModuleWrap directly for linking below.
    if (isPromise(moduleOrModulePromise)) {
      this.modulePromise = moduleOrModulePromise;
    } else {
      this.module = moduleOrModulePromise;
      this.modulePromise = PromiseResolve(moduleOrModulePromise);
    }

    if (this.shouldLinkModule(this.phase)) {
      // Promise for the list of all dependencyJobs.
      this.linked = this.link(requestType);
      // This promise is awaited later anyway, so silence
      // 'unhandled rejection' warnings.
      if (isPromise(this.linked)) {
        PromisePrototypeThen(this.linked, undefined, noop);
      }
    }

    // instantiated == deep dependency jobs wrappers are instantiated,
    // and module wrapper is instantiated.
    this.instantiated = undefined;
  }

  /**
   * @param {ModuleRequestType} requestType Type of the module request.
   * @returns {ModuleJobBase[]|Promise<ModuleJobBase[]>}
   */
  link(requestType) {
    if (this.loader.isForAsyncLoaderHookWorker) {
      return this.#asyncLink(requestType);
    }
    return this.syncLink(requestType);
  }

  /**
   * @param {ModuleRequestType} requestType Type of the module request.
   * @returns {Promise<ModuleJobBase[]>}
   */
  async #asyncLink(requestType) {
    assert(this.loader.isForAsyncLoaderHookWorker);
    this.module = await this.modulePromise;
    assert(this.module instanceof ModuleWrap);
    const moduleRequests = this.module.getModuleRequests();
    // Create an ArrayLike to avoid calling into userspace with `.then`
    // when returned from the async function.
    // Modules should be aligned with the moduleRequests array in order.
    const modulePromises = Array(moduleRequests.length);
    const evaluationDepJobs = [];
    this.commonJsDeps = Array(moduleRequests.length);
    for (let idx = 0; idx < moduleRequests.length; idx++) {
      const request = moduleRequests[idx];
      // Explicitly keeping track of dependency jobs is needed in order
      // to flatten out the dependency graph below in `asyncInstantiate()`,
      // so that circular dependencies can't cause a deadlock by two of
      // these `link` callbacks depending on each other.
      // TODO(joyeecheung): split this into two iterators, one for resolving and one for loading so
      // that hooks can pre-fetch sources off-thread.
      const dependencyJobPromise = this.loader.getOrCreateModuleJob(this.url, request, requestType);
      const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => {
        debug(`ModuleJob.asyncLink() ${this.url} -> ${request.specifier}`, job);
        if (this.shouldRunModule(request.phase)) {
          ArrayPrototypePush(evaluationDepJobs, job);
        }
        return job.modulePromise;
      });
      modulePromises[idx] = modulePromise;
    }
    const modules = await SafePromiseAllReturnArrayLike(modulePromises);
    for (let idx = 0; idx < moduleRequests.length; idx++) {
      this.commonJsDeps[idx] = modules[idx].isCommonJS;
    }

    this.module.link(modules);
    return evaluationDepJobs;
  }

  #instantiate() {
    if (this.instantiated === undefined) {
      this.instantiated = this.#asyncInstantiate();
    }
    return this.instantiated;
  }

  async #asyncInstantiate() {
    const jobsInGraph = new SafeSet();
    // TODO(joyeecheung): if it's not on the async loader thread, consider this already
    // linked.
    const addJobsToDependencyGraph = async (moduleJob) => {
      debug(`async addJobsToDependencyGraph() ${this.url}`, moduleJob);

      if (jobsInGraph.has(moduleJob)) {
        return;
      }
      jobsInGraph.add(moduleJob);
      const dependencyJobs = isPromise(moduleJob.linked) ? await moduleJob.linked : moduleJob.linked;
      return SafePromiseAllReturnVoid(dependencyJobs, addJobsToDependencyGraph);
    };
    await addJobsToDependencyGraph(this);

    try {
      this.module.instantiate();
    } catch (error) {
      decorateErrorStack(error);
      handleCJSNamedExportError(error, this.module, this.commonJsDeps);
      throw error;
    }

    for (const dependencyJob of jobsInGraph) {
      // Calling `this.module.instantiate()` instantiates not only the
      // ModuleWrap in this module, but all modules in the graph.
      dependencyJob.instantiated = resolvedPromise ??= PromiseResolve();
    }
  }

  runSync(parent) {
    assert(this.shouldRunModule(this.phase));
    assert(this.module instanceof ModuleWrap);
    let status = this.module.getStatus();

    debug('ModuleJob.runSync()', status, this.module);
    if (status === kUninstantiated) {
      // TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that
      // the async graph error supersedes instantiation (mismatch export) errors in the graph.
      // FIXME(joyeecheung): this cannot fully handle < kInstantiated. Make the linking
      // fully synchronous instead.
      if (this.module.getModuleRequests().length === 0) {
        this.module.link([]);
      }
      this.module.instantiate();
      status = this.module.getStatus();
    }
    if (status === kInstantiated || status === kErrored) {
      this.throwIfAsyncGraph(parent);
      if (status === kInstantiated) {
        setHasStartedUserESMExecution();
        const namespace = this.module.evaluateSync();
        return { __proto__: null, module: this.module, namespace };
      }
      throw this.module.getError();
    } else if (status === kEvaluating || status === kEvaluated) {
      this.throwIfAsyncGraph(parent);
      // kEvaluating can show up when this is being used to deal with CJS <-> CJS cycles.
      // Allow it for now, since we only need to ban ESM <-> CJS cycles which would be
      // detected earlier during the linking phase, though the CJS handling in the ESM
      // loader won't be able to emit warnings on pending circular exports like what
      // the CJS loader does.
      // TODO(joyeecheung): remove the re-invented require() in the ESM loader and
      // always handle CJS using the CJS loader to eliminate the quirks.
      return { __proto__: null, module: this.module, namespace: this.module.getNamespace() };
    }
    assert(status === kUninstantiated, `Unexpected module status ${status}.`);
    throw new ERR_REQUIRE_ESM_RACE_CONDITION();
  }

  async run(isEntryPoint = false) {
    debug('ModuleJob.run()', this.module);
    assert(this.shouldRunModule(this.phase));
    await this.#instantiate();
    if (isEntryPoint) {
      globalThis[entry_point_module_private_symbol] = this.module;
    }
    const timeout = -1;
    const breakOnSigint = false;
    setHasStartedUserESMExecution();
    try {
      await this.module.evaluate(timeout, breakOnSigint);
    } catch (e) {
      explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait);
      throw e;
    }
    return { __proto__: null, module: this.module };
  }
}

/**
 * This is a fully synchronous job and does not spawn additional threads in any way.
 * Loading and linking are always synchronous. Evaluation via runSync() throws on an
 * asynchronous graph; evaluation via run() falls back to async for top-level await.
 * It also disallows CJS <-> ESM cycles.
 *
 * Used for all ES module imports on the main thread, regardless of how the import was
 * triggered (entry point, import(), require(esm), --import, etc.).
 * Modules loaded by require() in imported CJS are handled by ModuleJob with the
 * isForRequireInImportedCJS set to true instead. The two currently have different caching behaviors.
 * TODO(joyeecheung): consolidate this with the isForRequireInImportedCJS variant of ModuleJob.
 */
class ModuleJobSync extends ModuleJobBase {
  /**
   * @param {ModuleLoader} loader The ESM loader.
   * @param {string} url URL of the module to be wrapped in ModuleJob.
   * @param {ImportAttributes} importAttributes Import attributes from the import statement.
   * @param {ModuleWrap} moduleWrap Translated ModuleWrap for the module.
   * @param {number} phase The phase to load the module to.
   * @param {boolean} isMain Whether the module is the entry point.
   * @param {boolean} inspectBrk Whether this module should be evaluated with the
   *   first line paused in the debugger (because --inspect-brk is passed).
   */
  constructor(loader, url, importAttributes, moduleWrap, phase = kEvaluationPhase, isMain,
              inspectBrk, requestType) {
    super(loader, url, importAttributes, phase, isMain, inspectBrk);

    this.module = moduleWrap;

    assert(this.module instanceof ModuleWrap);
    this.linked = undefined;
    this.type = importAttributes.type;
    if (this.shouldLinkModule(phase)) {
      this.linked = this.link(requestType);
    }
  }

  /**
   * @param {ModuleRequestType} requestType Type of the module request.
   * @returns {ModuleJobBase[]}
   */
  link(requestType) {
    // Synchronous linking is always used for ModuleJobSync.
    return this.syncLink(requestType);
  }

  get modulePromise() {
    return PromiseResolve(this.module);
  }

  instantiateSync() {
    try {
      this.module.instantiate();
    } catch (error) {
      decorateErrorStack(error);
      handleCJSNamedExportError(error, this.module, this.commonJsDeps);
      throw error;
    }
  }

  async run(isEntryPoint = false) {
    assert(this.shouldRunModule(this.phase));
    const status = this.module.getStatus();
    debug('ModuleJobSync.run()', status, this.module);
    // If the module was previously required and errored, reject from import() again.
    if (status === kErrored) {
      throw this.module.getError();
    }
    if (status > kInstantiated) {
      // Already evaluated (e.g. previously require()'d and now import()'d again).
      if (this.evaluationPromise) {
        await this.evaluationPromise;
      }
      return { __proto__: null, module: this.module };
    }
    if (status < kInstantiated) {
      // Either a fresh module - links were already resolved synchronously in the constructor -
      // or one that was initially require()'d and is now being imported after its instantiation
      // failed (e.g. a missing named export). Try finishing the instantiation: if it succeeds,
      // proceed to evaluation, otherwise re-throw the instantiation error.
      this.instantiateSync();
    }
    // `status === kInstantiated`: either just instantiated above, or previously instantiated
    // but evaluation was deferred (e.g. TLA detected by a prior `runSync()` call)
    if (isEntryPoint) {
      globalThis[entry_point_module_private_symbol] = this.module;
    }
    setHasStartedUserESMExecution();
    if (this.module.hasAsyncGraph) {
      // Has top-level `await`: fall back to async evaluation
      const timeout = -1;
      const breakOnSigint = false;
      if (this.inspectBrk) {
        const { callAndPauseOnStart } = internalBinding('inspector');
        this.evaluationPromise = callAndPauseOnStart(this.module.evaluate, this.module, timeout, breakOnSigint);
      } else {
        this.evaluationPromise = this.module.evaluate(timeout, breakOnSigint);
      }
      await this.evaluationPromise;
      this.evaluationPromise = undefined;
      return { __proto__: null, module: this.module };
    }
    // No top-level `await`: evaluate synchronously
    try {
      this.#evaluateSync();
    } catch (evaluateError) {
      explainCommonJSGlobalLikeNotDefinedError(evaluateError, this.module.url, this.module.hasTopLevelAwait);
      throw evaluateError;
    }
    return { __proto__: null, module: this.module };
  }

  // Evaluate this module synchronously, pausing at the first line of user land JS
  // that runs if --inspect-brk is active - that may well be inside a dependency.
  #evaluateSync() {
    if (this.inspectBrk) {
      const { callAndPauseOnStart } = internalBinding('inspector');
      return callAndPauseOnStart(this.module.evaluateSync, this.module);
    }
    return this.module.evaluateSync();
  }

  runSync(parent) {
    debug('ModuleJobSync.runSync()', this.module);
    assert(this.shouldRunModule(this.phase));
    // TODO(joyeecheung): Reject graphs with top-level await _before_ instantiation, so that the
    // async graph error supersedes instantiation (mismatch export) errors in the graph.
    this.instantiateSync();
    // On the deprecated async loader hook worker thread, dependencies linked by an
    // earlier import may not be walkable synchronously, so double-check with
    // V8 now that the graph is instantiated.
    this.throwIfAsyncGraph(parent);
    setHasStartedUserESMExecution();
    try {
      const namespace = this.#evaluateSync();
      return { __proto__: null, module: this.module, namespace };
    } catch (e) {
      explainCommonJSGlobalLikeNotDefinedError(e, this.module.url, this.module.hasTopLevelAwait);
      throw e;
    }
  }
}

ObjectSetPrototypeOf(ModuleJobBase.prototype, null);
module.exports = {
  ModuleJob, ModuleJobSync, ModuleJobBase,
};