/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/modules/package_map.js
311 строк
9 KB
Maël Nison
loader: enforce path normalization before lookup
26 июл 2026, 03:59
Не верифицирован
26 июл 2026, 03:59
c8da4ec
Код
Авторство
О чём код?
'use strict'; const { JSONParse, ObjectEntries, SafeMap, StringPrototypeIndexOf, StringPrototypeSlice, } = primordials; const assert = require('internal/assert'); const { emitExperimentalWarning, getLazy, setOwnProperty } = require('internal/util'); const { dirname, resolve: pathResolve } = require('path'); const { fileURLToPath, pathToFileURL, URL } = require('internal/url'); const getPackageMapPath = getLazy(() => require('internal/options').getOptionValue('--experimental-package-map'), ); const fs = require('fs'); const { ERR_PACKAGE_MAP_EXTERNAL_FILE, ERR_PACKAGE_MAP_INVALID, ERR_PACKAGE_MAP_KEY_NOT_FOUND, } = require('internal/errors').codes; // Singleton - initialized once on first call let packageMap; let packageMapPath; /** * @typedef {object} PackageMapEntry * @property {string} path - Absolute decoded path to package on disk * @property {Map<string, string>} dependencies - Map from bare specifier to package key */ class PackageMap { /** @type {string} */ #configPath; /** @type {URL} */ #baseURL; /** @type {Map<string, PackageMapEntry>} */ #packages; /** @type {Map<string, string>} */ #pathToKey; /** @type {Map<string, string|null>} */ #pathToKeyCache; /** * @param {string} configPath * @param {object} data */ constructor(configPath, data) { this.#configPath = configPath; this.#baseURL = new URL('./', pathToFileURL(configPath)); this.#packages = new SafeMap(); this.#pathToKey = new SafeMap(); this.#pathToKeyCache = new SafeMap(); this.#parse(data); } /** * @param {object} data */ #parse(data) { if (!data.packages || typeof data.packages !== 'object') { throw new ERR_PACKAGE_MAP_INVALID(this.#configPath, 'missing "packages" object'); } const entries = ObjectEntries(data.packages); for (let i = 0; i < entries.length; i++) { const { 0: key, 1: entry } = entries[i]; const { url } = entry; switch (typeof url) { case 'string': if (url === '') { throw new ERR_PACKAGE_MAP_INVALID(this.#configPath, `package "${key}" has an empty "url" field`); } break; case 'undefined': throw new ERR_PACKAGE_MAP_INVALID(this.#configPath, `package "${key}" is missing "url" field`); default: throw new ERR_PACKAGE_MAP_INVALID(this.#configPath, `package "${key}" has invalid "url" field: expected string, got ${typeof url}`); } let packageURL; try { packageURL = new URL(entry.url, this.#baseURL); } catch (err) { const error = new ERR_PACKAGE_MAP_INVALID( this.#configPath, `package "${key}" has invalid url "${entry.url}"`, ); setOwnProperty(error, 'cause', err); throw error; } if (packageURL.protocol !== 'file:') { throw new ERR_PACKAGE_MAP_INVALID( this.#configPath, `package "${key}" has unsupported URL scheme in url "${entry.url}"; only file: URLs and relative URLs are currently supported`, ); } let absolutePath; try { absolutePath = fileURLToPath(packageURL); } catch (err) { const error = new ERR_PACKAGE_MAP_INVALID( this.#configPath, `package "${key}" has invalid file url "${entry.url}"`, ); setOwnProperty(error, 'cause', err); throw error; } this.#packages.set(key, { path: absolutePath, dependencies: new SafeMap(ObjectEntries(entry.dependencies ?? {})), }); // TODO(arcanis): Duplicates should be tolerated, but it requires changing module IDs // to include the package ID whenever a package map is used. const existingKey = this.#pathToKey.get(absolutePath); if (existingKey != null) { throw new ERR_PACKAGE_MAP_INVALID( this.#configPath, `packages "${existingKey}" and "${key}" have the same url "${entry.url}"; this will be supported in the future`, ); } this.#pathToKey.set(absolutePath, key); } } /** * Find which package key contains the given file path. * @param {string} filePath * @returns {string|null} */ #getKeyForPath(filePath) { filePath = pathResolve(filePath); const cached = this.#pathToKeyCache.get(filePath); if (cached !== undefined) { return cached; } // Walk up to find containing package let checkPath = filePath; while (true) { const key = this.#pathToKey.get(checkPath); if (key !== undefined) { this.#pathToKeyCache.set(filePath, key); return key; } const parent = dirname(checkPath); if (parent === checkPath) { break; } checkPath = parent; } this.#pathToKeyCache.set(filePath, null); return null; } /** * Main resolution method. * Returns the package path and subpath for the specifier, or undefined if * the specifier is not in the parent package's dependencies. * Throws ERR_PACKAGE_MAP_EXTERNAL_FILE if parentPath is not within any * mapped package. * @param {string} specifier * @param {string} parentPath - File path of the importing module * @returns {{packagePath: string, subpath: string}|undefined} */ resolve(specifier, parentPath) { const parentKey = this.#getKeyForPath(parentPath); if (parentKey === null) { throw new ERR_PACKAGE_MAP_EXTERNAL_FILE(specifier, parentPath, this.#configPath); } const { packageName, subpath } = parsePackageName(specifier); const parentEntry = this.#packages.get(parentKey); const targetKey = parentEntry.dependencies.get(packageName); if (targetKey === undefined) { // Package not in parent's dependencies - let the caller throw the appropriate error return undefined; } const targetEntry = this.#packages.get(targetKey); if (!targetEntry) { throw new ERR_PACKAGE_MAP_KEY_NOT_FOUND(targetKey, this.#configPath); } return { packagePath: targetEntry.path, subpath }; } } /** * Parse a package specifier into name and subpath. * @param {string} specifier * @returns {{packageName: string, subpath: string}} */ function parsePackageName(specifier) { const isScoped = specifier[0] === '@'; let slashIndex = StringPrototypeIndexOf(specifier, '/'); if (isScoped) { if (slashIndex === -1) { // Invalid: @scope without package name, treat whole thing as name return { packageName: specifier, subpath: '.' }; } slashIndex = StringPrototypeIndexOf(specifier, '/', slashIndex + 1); } if (slashIndex === -1) { return { packageName: specifier, subpath: '.' }; } return { packageName: StringPrototypeSlice(specifier, 0, slashIndex), subpath: '.' + StringPrototypeSlice(specifier, slashIndex), }; } /** * Load and parse the package map from a config path. * @param {string} configPath * @returns {PackageMap} */ function loadPackageMap(configPath) { let content; try { content = fs.readFileSync(configPath); } catch (err) { const error = new ERR_PACKAGE_MAP_INVALID(configPath, 'failed to read'); setOwnProperty(error, 'cause', err); throw error; } // Resolve symlinks so that stored paths match the realpath'd module paths // that Node.js uses during resolution. configPath = fs.realpathSync(configPath); let data; try { data = JSONParse(content); } catch (err) { const error = new ERR_PACKAGE_MAP_INVALID(configPath, `invalid JSON`); setOwnProperty(error, 'cause', err); throw error; } return new PackageMap(configPath, data); } /** * Get the singleton package map, initializing on first call. * @returns {PackageMap|null} */ function getPackageMap() { if (packageMap !== undefined) { return packageMap; } packageMapPath = getPackageMapPath(); if (!packageMapPath) { packageMap = null; return null; } emitExperimentalWarning('Package maps'); try { packageMap = loadPackageMap(pathResolve(packageMapPath)); } catch (err) { // Fallback to an empty package map to avoid repeatedly trying // to load the broken package map file. Subsequent resolutions // will still fail to resolve due to the package map being empty. packageMap = new PackageMap(packageMapPath, { packages: {} }); throw err; } return packageMap; } /** * Check if the package map is enabled. * @returns {boolean} */ function hasPackageMap() { return !!getPackageMapPath(); } /** * Resolve a package specifier using the package map. * Returns the package path and subpath, or undefined if resolution should * fall back to standard resolution. * @param {string} specifier - The bare specifier (e.g., "lodash", "react/jsx-runtime") * @param {string} parentPath - File path of the importing module * @returns {{packagePath: string, subpath: string}|undefined} */ function packageMapResolve(specifier, parentPath) { const map = getPackageMap(); assert(map !== null, 'Package map is not enabled'); return map.resolve(specifier, parentPath); } module.exports = { hasPackageMap, packageMapResolve, };