@ 261105.16
current
'use strict';

const {
  ArrayIsArray,
  JSONParse,
  ObjectDefineProperty,
  RegExpPrototypeExec,
  SafeMap,
  StringPrototypeIndexOf,
  StringPrototypeSlice,
  StringPrototypeStartsWith,
} = primordials;
const {
  fileURLToPath,
  isURL,
  pathToFileURL,
  URL,
} = require('internal/url');
const { canParse: URLCanParse } = internalBinding('url');
const {
  codes: {
    ERR_INVALID_MODULE_SPECIFIER,
    ERR_MISSING_ARGS,
    ERR_MODULE_NOT_FOUND,
  },
} = require('internal/errors');
const { kEmptyObject, isWindows } = require('internal/util');
const permission = require('internal/process/permission');
const path = require('path');
const { validateString } = require('internal/validators');
const {
  cleanForVfsPrefix,
  loaderMethods,
} = require('internal/modules/helpers');


/**
 * @typedef {import('../../../typings/internalBinding/modules').DeserializedPackageConfig} DeserializedPackageConfig
 * @typedef {import('../../../typings/internalBinding/modules').PackageConfig} PackageConfig
 * @typedef {import('../../../typings/internalBinding/modules').SerializedPackageConfig} SerializedPackageConfig
 */

/**
 * @param {URL['pathname']} path
 * @param {SerializedPackageConfig} contents
 * @returns {DeserializedPackageConfig}
 */
function deserializePackageJSON(path, contents) {
  if (contents === undefined) {
    return {
      data: {
        __proto__: null,
        type: 'none', // Ignore unknown types for forwards compatibility
      },
      exists: false,
      path,
    };
  }

  const {
    0: name,
    1: main,
    2: type,
    3: plainImports,
    4: plainExports,
    5: optionalFilePath,
    6: isPrivate,
  } = contents;

  const pjsonPath = optionalFilePath ?? path;

  const data = {
    __proto__: null,
    ...(name != null && { name }),
    ...(main != null && { main }),
    ...(type != null && { type }),
    ...(isPrivate != null && { private: isPrivate }),
  };

  if (plainExports !== null) {
    ObjectDefineProperty(data, 'exports', {
      __proto__: null,
      configurable: true,
      enumerable: true,
      get() {
        const value = requiresJSONParse(plainExports) ? JSONParse(plainExports) : plainExports;
        ObjectDefineProperty(data, 'exports', { __proto__: null, enumerable: true, value });
        return value;
      },
    });
  }

  if (plainImports !== null) {
    ObjectDefineProperty(data, 'imports', {
      __proto__: null,
      configurable: true,
      enumerable: true,
      get() {
        const value = requiresJSONParse(plainImports) ? JSONParse(plainImports) : plainImports;
        ObjectDefineProperty(data, 'imports', { __proto__: null, enumerable: true, value });
        return value;
      },
    });
  }

  return {
    data,
    exists: true,
    path: pjsonPath,
  };
}

// The imports and exports fields can be either undefined or a string.
// - If it's a string, it's either plain string or a stringified JSON string.
// - If it's a stringified JSON string, it starts with either '[' or '{'.
const requiresJSONParse = (value) => (value !== undefined && (value[0] === '[' || value[0] === '{'));

/**
 * Reads a package.json file and returns the parsed contents.
 * @param {string} jsonPath
 * @param {{
 *   base?: URL | string,
 *   specifier?: URL | string,
 *   isESM?: boolean,
 * }} options
 * @returns {PackageConfig}
 */
function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
  // This function will be called by both CJS and ESM, so we need to make sure
  // non-null attributes are converted to strings.
  const parsed = loaderMethods.readPackageJSON(
    jsonPath,
    isESM,
    base == null ? undefined : `${base}`,
    specifier == null ? undefined : `${specifier}`,
  );

  const result = deserializePackageJSON(jsonPath, parsed);

  return {
    __proto__: null,
    ...result.data,
    exists: result.exists,
    pjsonPath: result.path,
  };
}

/**
 * A cache mapping a directory to the path of the nearest `package.json` at or
 * above it (`null` when there is none). The native traversal for a module
 * starts at the module's directory, so every module in a directory shares one
 * entry and one native call. Used in concert with
 * `deserializedPackageJSONCache`.
 */
const directoryToParentPackageJSONPathCache = new SafeMap();

/**
 * When the permission model is enabled the native traversal also depends on
 * the read permissions in effect at the time of the call, so results are only
 * remembered per exact module path, as before.
 */
const moduleToParentPackageJSONCache = new SafeMap();

/**
 * A cache mapping the path of a `package.json` file to its
 * {@link DeserializedPackageConfig deserialized representation},
 * as produced by {@link deserializedPackageJSONCache}. The purpose of this
 * cache is to ensure that we always return the same
 * {@link DeserializedPackageConfig} instance for a given `package.json`,
 * which is necessary to ensure that we don't re-parse `imports` and
 * `exports` redundantly.
 */
const deserializedPackageJSONCache = new SafeMap();

/**
 * The directory the native nearest-parent traversal starts from for `checkPath`
 * (see BindingData::NormalizePath/TraverseParent): the path itself when it has
 * a trailing separator, its dirname otherwise.
 * @param {string} checkPath
 * @returns {string}
 */
function getTraversalStartDirectory(checkPath) {
  const last = checkPath[checkPath.length - 1];
  if (last === '/' || (isWindows && last === '\\')) {
    return StringPrototypeSlice(checkPath, 0, -1);
  }
  return path.dirname(checkPath);
}

/**
 * Get the nearest parent package.json file from a given path.
 * Return the package.json data and the path to the package.json file, or undefined.
 * @param {string} checkPath The path to start searching from.
 * @returns {undefined | DeserializedPackageConfig}
 */
function getNearestParentPackageJSON(checkPath) {
  const permissionEnabled = permission.isEnabled();
  const cache = permissionEnabled ? moduleToParentPackageJSONCache : directoryToParentPackageJSONPathCache;
  const key = permissionEnabled ? checkPath : getTraversalStartDirectory(checkPath);
  let parentPackageJSONPath = cache.get(key);
  if (parentPackageJSONPath === undefined) {
    const result = loaderMethods.getNearestParentPackageJSON(checkPath);
    if (result === undefined) {
      parentPackageJSONPath = null;
    } else {
      const packageConfig = deserializePackageJSON(checkPath, result);
      parentPackageJSONPath = packageConfig.path;
      if (!deserializedPackageJSONCache.has(parentPackageJSONPath)) {
        deserializedPackageJSONCache.set(parentPackageJSONPath, packageConfig);
      }
    }
    cache.set(key, parentPackageJSONPath);
  }

  if (parentPackageJSONPath === null) {
    // No package.json above this path: same shape as before, carrying the queried path.
    return deserializePackageJSON(checkPath, undefined);
  }
  return deserializedPackageJSONCache.get(parentPackageJSONPath);
}

const moduleToRootPackageJSONCache = new SafeMap();

/**
 * This is stricter than {@link getNearestParentPackageJSON}: nested
 * `package.json` files inside a package are ignored, and a non-private package
 * anywhere in the `node_modules` chain (for example a publishable package that
 * bundles a private dependency) disqualifies the path. A directory under
 * `node_modules` with no `package.json` (such as pnpm's `.pnpm` store) is not
 * a package and is skipped.
 *
 * Returns the package.json data and the path to the package.json file, or undefined.
 * @param {string} checkPath The path to start searching from.
 * @returns {undefined | DeserializedPackageConfig}
 */
function getRootPackageJSON(checkPath) {
  const packageJSONPath = moduleToRootPackageJSONCache.get(checkPath);
  if (packageJSONPath !== undefined) {
    return deserializedPackageJSONCache.get(packageJSONPath);
  }

  const result = loaderMethods.getRootPackageJSON(checkPath);
  const packageConfig = deserializePackageJSON(checkPath, result);

  moduleToRootPackageJSONCache.set(checkPath, packageConfig.path);

  const maybeCachedPackageConfig = deserializedPackageJSONCache.get(packageConfig.path);
  if (maybeCachedPackageConfig !== undefined) {
    return maybeCachedPackageConfig;
  }

  deserializedPackageJSONCache.set(packageConfig.path, packageConfig);
  return packageConfig;
}

/**
 * Returns the package configuration for the given resolved URL.
 * @param {URL | string} resolved - The resolved URL.
 * @returns {import('../../../typings/internalBinding/modules').PackageConfig} - The package configuration.
 */
function getPackageScopeConfig(resolved) {
  const result = loaderMethods.getPackageScopeConfig(`${resolved}`);

  if (ArrayIsArray(result)) {
    const { data, exists, path } = deserializePackageJSON(`${resolved}`, result);

    // `data` is not shared, so extend it in place instead of spreading it:
    // spreading would invoke the lazy `imports` and `exports` getters.
    data.exists = exists;
    data.pjsonPath = path;
    return data;
  }

  // This means that the response is a string
  // and it is the path to the package.json file
  return {
    __proto__: null,
    pjsonPath: result,
    exists: false,
    type: 'none',
  };
}

/**
 * Returns the package type for a given URL.
 * @param {URL} url - The URL to get the package type for.
 * @returns {string}
 */
function getPackageType(url) {
  const type = loaderMethods.getPackageType(`${url}`);
  return type ?? 'none';
}

const invalidPackageNameRegEx = /^\.|%|\\/;
/**
 * Parse a package name from a specifier.
 * @param {string} specifier - The import specifier.
 * @param {string | URL | undefined} base - The parent URL.
 * @returns {object}
 */
function parsePackageName(specifier, base) {
  let separatorIndex = StringPrototypeIndexOf(specifier, '/');
  let validPackageName = true;
  let isScoped = false;
  if (specifier[0] === '@') {
    isScoped = true;
    if (separatorIndex === -1 || specifier.length === 0) {
      validPackageName = false;
    } else {
      separatorIndex = StringPrototypeIndexOf(
        specifier, '/', separatorIndex + 1);
    }
  }

  const packageName = separatorIndex === -1 ?
    specifier : StringPrototypeSlice(specifier, 0, separatorIndex);

  // Package name cannot have leading . and cannot have percent-encoding or
  // \\ separators.
  if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null) {
    validPackageName = false;
  }

  if (!validPackageName) {
    throw new ERR_INVALID_MODULE_SPECIFIER(
      specifier, 'is not a valid package name', fileURLToPath(base));
  }

  const packageSubpath = '.' + (separatorIndex === -1 ? '' :
    StringPrototypeSlice(specifier, separatorIndex));

  return { packageName, packageSubpath, isScoped };
}

function getPackageJSONURL(specifier, base) {
  const { packageName, packageSubpath, isScoped } = parsePackageName(specifier, base);

  // ResolveSelf
  const packageConfig = getPackageScopeConfig(base);
  if (packageConfig.exists) {
    if (packageConfig.exports != null && packageConfig.name === packageName) {
      const packageJSONPath = packageConfig.pjsonPath;
      return { packageJSONUrl: pathToFileURL(packageJSONPath), packageJSONPath, packageSubpath };
    }
  }

  let packageJSONUrl = new URL(`./node_modules/${packageName}/package.json`, base);
  let packageJSONPath = fileURLToPath(packageJSONUrl);
  // The walk stops at the resolution root (e.g. a VFS mount point)
  // instead of continuing to the file system root.
  const resolutionRoot = loaderMethods.getResolutionRoot(packageJSONPath);
  const resolutionRootPrefix =
    resolutionRoot === undefined ? undefined : resolutionRoot + path.sep;
  let lastPath;
  do {
    const stat = loaderMethods.internalModuleStat(
      StringPrototypeSlice(packageJSONPath, 0, packageJSONPath.length - 13),
    );
    // Check for !stat.isDirectory()
    if (stat !== 1) {
      lastPath = packageJSONPath;
      packageJSONUrl = new URL(
        `${isScoped ? '../' : ''}../../../node_modules/${packageName}/package.json`,
        packageJSONUrl,
      );
      packageJSONPath = fileURLToPath(packageJSONUrl);
      if (resolutionRootPrefix !== undefined &&
          !StringPrototypeStartsWith(packageJSONPath, resolutionRootPrefix)) {
        break;
      }
      continue;
    }

    // Package match.
    return { packageJSONUrl, packageJSONPath, packageSubpath };
  } while (packageJSONPath.length !== lastPath.length);

  throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
}

/** @type {import('./esm/resolve.js').defaultResolve} */
let defaultResolve;
/**
 * @param {string | URL} specifier The location for which to get the "root" package.json
 * @param {string | URL} [base] The location of the current module (ex file://tmp/foo.js).
 * @returns {string}
 */
function findPackageJSON(specifier, base = 'data:') {
  if (arguments.length === 0) {
    throw new ERR_MISSING_ARGS('specifier');
  }
  try {
    specifier = `${specifier}`;
  } catch {
    validateString(specifier, 'specifier');
  }

  let parentURL = base;
  if (!isURL(base)) {
    validateString(base, 'base');
    parentURL = path.isAbsolute(base) ? pathToFileURL(base) : new URL(base);
  }

  if (specifier && specifier[0] !== '.' && specifier[0] !== '/' && !URLCanParse(specifier)) {
    // If `specifier` is a bare specifier.
    const { packageJSONPath } = getPackageJSONURL(specifier, parentURL);
    return packageJSONPath;
  }

  let resolvedTarget;
  defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;

  try {
    // TODO(@JakobJingleheimer): Detect whether findPackageJSON is being used within a loader
    // (possibly piggyback on `isForAsyncLoaderHookWorker` from the loader?) and if so:
    // - When inside, use the default resolve
    //   - (I think it's impossible to use the chain because of re-entry & a deadlock from atomics).
    // - When outside, use cascadedLoader.resolveSync (not implemented yet, but the pieces exist).
    resolvedTarget = defaultResolve(specifier, { parentURL: `${parentURL}` }).url;
  } catch (err) {
    if (err.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
      resolvedTarget = err.url;
    } else {
      throw err;
    }
  }

  const pkg = getNearestParentPackageJSON(fileURLToPath(resolvedTarget));

  return pkg?.path;
}

/**
 * Drop package.json-cache entries under an unmounted VFS mount point.
 * Called by internal/vfs/setup.js. A module path and its owning
 * package.json path always live in the same mount (the pjson search
 * walks up from the module and stops at the mount root), so a single
 * key test on each map is exact.
 * @param {string} mountPoint Absolute mount-point path
 */
function purgePackageJSONCacheForPrefix(mountPoint) {
  cleanForVfsPrefix(directoryToParentPackageJSONPathCache, mountPoint);
  cleanForVfsPrefix(moduleToParentPackageJSONCache, mountPoint);
  cleanForVfsPrefix(deserializedPackageJSONCache, mountPoint);
}

module.exports = {
  read,
  getNearestParentPackageJSON,
  getRootPackageJSON,
  getPackageScopeConfig,
  getPackageType,
  getPackageJSONURL,
  findPackageJSON,
  purgePackageJSONCacheForPrefix,
};