@ 261105.16
current
'use strict';

const {
  StringPrototypeEndsWith,
  globalThis,
} = primordials;

const { getOptionValue } = require('internal/options');
// Through loaderMethods rather than the `modules` binding directly: a mounted
// VFS overrides these, and a worker entry point can live inside one.
const { loaderMethods } = require('internal/modules/helpers');
const path = require('path');
const { pathToFileURL, URL } = require('internal/url');
const { kEmptyObject, getCWDURL } = require('internal/util');
const {
  hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');
const {
  triggerUncaughtException,
} = internalBinding('errors');
const {
  privateSymbols: {
    entry_point_promise_private_symbol,
  },
} = internalBinding('util');
/**
 * Get the absolute path to the main entry point.
 * @param {string} main - Entry point path
 * @returns {string|undefined}
 */
function resolveMainPath(main) {
  /** @type {string} */
  let mainPath;
  // Extension searching for the main entry point is supported for backward compatibility.
  // Module._findPath is monkey-patchable here.
  const { Module } = require('internal/modules/cjs/loader');
  mainPath = Module._findPath(path.resolve(main), null, true);
  if (!mainPath) { return; }

  const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
  if (!preserveSymlinksMain) {
    mainPath = loaderMethods.realpathSync(mainPath);
  }

  return mainPath;
}

/**
 * Determine whether the main entry point should be loaded through the ESM Loader.
 * @param {string} mainPath - Absolute path to the main entry point
 * @returns {boolean}
 */
function shouldUseESMLoader(mainPath) {
  /**
   * @type {string[]} userLoaders A list of custom loaders registered by the user
   * (or an empty list when none have been registered).
   */
  const userLoaders = getOptionValue('--experimental-loader');
  /**
   * @type {string[]} userImports A list of preloaded modules registered by the user
   * (or an empty list when none have been registered).
   */
  const userImports = getOptionValue('--import');
  if (userLoaders.length > 0 || userImports.length > 0) { return true; }

  // Determine the module format of the entry point.
  if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
  if (mainPath && StringPrototypeEndsWith(mainPath, '.wasm')) { return true; }
  if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }

  if (getOptionValue('--strip-types')) {
    if (!mainPath || StringPrototypeEndsWith(mainPath, '.cts')) { return false; }
    // This will likely change in the future to start with commonjs loader by default
    if (mainPath && StringPrototypeEndsWith(mainPath, '.mts')) { return true; }
  }

  const type = loaderMethods.getNearestParentPackageJSONType(mainPath);

  // No package.json or no `type` field.
  if (type === undefined || type === 'none') {
    return false;
  }

  return type === 'module';
}

/**
 * @param {function(ModuleLoader):ModuleWrap|undefined} callback
 */
async function asyncRunEntryPointWithESMLoader(callback) {
  const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
  try {
    const userImports = getOptionValue('--import');
    if (userImports.length > 0) {
      const parentURL = getCWDURL().href;
      for (let i = 0; i < userImports.length; i++) {
        await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
      }
    } else {
      cascadedLoader.waitForAsyncLoaderHookInitialization();
    }
    // Any --import above may have registered a VFS provider, so mount now (no-op
    // if prepareExecution already mounted in the no-import case).
    require('internal/process/pre_execution').finishVfsMounts();
    await callback(cascadedLoader);
  } catch (err) {
    if (hasUncaughtExceptionCaptureCallback()) {
      process._fatalException(err);
      return;
    }
    triggerUncaughtException(
      err,
      true, /* fromPromise */
    );
  }
}

/**
 * This initializes the ESM loader and runs --import (if any) before executing the
 * callback to run the entry point.
 * If the callback intends to evaluate a ESM module as entry point, it should return
 * the corresponding ModuleWrap so that stalled TLA can be checked a process exit.
 * @param {function(ModuleLoader):ModuleWrap|undefined} callback
 * @returns {Promise}
 */
function runEntryPointWithESMLoader(callback) {
  const promise = asyncRunEntryPointWithESMLoader(callback);
  // Register the promise - if by the time the event loop finishes running, this is
  // still unsettled, we'll search the graph from the entry point module and print
  // the location of any unsettled top-level await found.
  globalThis[entry_point_promise_private_symbol] = promise;
  return promise;
}

/**
 * Parse the CLI main entry point string and run it.
 * For backwards compatibility, we have to run a bunch of monkey-patchable code that belongs to the CJS loader (exposed
 * by `require('module')`) even when the entry point is ESM.
 * Because of backwards compatibility, this function is exposed publicly via `import { runMain } from 'node:module'`.
 * Because of module detection, this function will attempt to run ambiguous (no explicit extension, no
 * `package.json` type field) entry points as CommonJS first; under certain conditions, it will retry running as ESM.
 * @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js`
 * @returns {Promise|undefined} A promise when the entry point is still being evaluated
 *   asynchronously because its graph contains top-level await, otherwise `undefined`.
 */
function executeUserEntryPoint(main = process.argv[1]) {
  if (getOptionValue('[vfs_load_set]')) {
    // The entry is a directory mount whose reserved root only exists after
    // finishVfsMounts() runs (inside runEntryPointWithESMLoader, once --import
    // preloads have had a chance to registerProvider()). Load it through the CJS
    // main loader so the reserved directory resolves to its index the same way
    // any require() of a directory does, sidestepping ESM directory-import.
    runEntryPointWithESMLoader(() => {
      const { getVfsLoadRoot } = require('internal/process/pre_execution');
      const { wrapModuleLoad } = require('internal/modules/cjs/loader');
      return wrapModuleLoad(getVfsLoadRoot(), null, true);
    });
    return;
  }
  let useESMLoader;
  let resolvedMain;
  if (getOptionValue('--entry-url')) {
    useESMLoader = true;
  } else {
    resolvedMain = resolveMainPath(main);
    useESMLoader = shouldUseESMLoader(resolvedMain);
  }
  // Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first
  // try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM.
  if (!useESMLoader) {
    const cjsLoader = require('internal/modules/cjs/loader');
    const { wrapModuleLoad } = cjsLoader;
    wrapModuleLoad(main, null, true);
  } else {
    const mainPath = resolvedMain || main;
    const mainURL = getOptionValue('--entry-url') ? new URL(mainPath, getCWDURL()) : pathToFileURL(mainPath);

    // When no async loader hooks or --import are registered, load the entry point
    // directly. The graph is loaded and linked synchronously, and evaluated
    // synchronously too unless it contains top-level await.
    if (getOptionValue('--experimental-loader').length === 0 &&
        getOptionValue('--import').length === 0) {
      const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
      const job = cascadedLoader.importForEntryPoint(mainURL.href);
      if (job === undefined) {
        // Evaluated synchronously; no promise was created at any point.
        return;
      }
      // Top-level await: evaluate through the usual asynchronous entry point path,
      // reusing the job that was already resolved, linked and instantiated above.
      return runEntryPointWithESMLoader(async () => {
        // Begin evaluation inside a microtask, as `import()` does by awaiting the
        // module job before running it, so that a top-level await in the entry point
        // resumes in the same microtask drain instead of unwinding to the main stack
        // and letting the `process.nextTick` queue run first.
        await null;
        // Await rather than returning the job's promise: resolving a promise *with* a
        // thenable performs a `then` lookup on it, which user code can observe.
        await job.run(true);
      });
    }

    runEntryPointWithESMLoader((cascadedLoader) => {
      // Note that if the graph contains unsettled TLA, this may never resolve
      // even after the event loop stops running.
      return cascadedLoader.import(mainURL, undefined, { __proto__: null }, undefined, true);
    });
  }
}

module.exports = {
  executeUserEntryPoint,
  runEntryPointWithESMLoader,
};