import fs from "node:fs"; import path from "node:path"; import { NPM_PUBLIC_REGISTRY } from "../constants.js"; import type { NpmLockNode } from "../types.js"; import { bareName, specifierAt } from "../overrides/parsing/package-json.js"; import { isGitSource } from "../utils/advisory.js"; import { isRecord } from "../utils/package-json.js"; import { npmAliasRegistryName } from "../utils/package-identity.js"; import { registryForPackage, resolveNpmAliasRegistryConfig } from "../utils/registry-config.js"; import { isValidRange, satisfiesRange } from "../utils/version.js"; export type RawRootLockMetadata = { dependencies?: Record; devDependencies?: Record; optionalDependencies?: Record; peerDependencies?: Record; overrides?: Record; }; export type NpmLockAliasResolutionContext = { filePath: string; rootMeta: RawRootLockMetadata | null; rawLegacyDependencies: unknown; rootEntryNodeIds: ReadonlySet; rootRanges: Readonly>; peerRangesByChildNodeId: ReadonlyMap; manifestNameByNodeId: ReadonlyMap; resolvedUrlByNodeId: ReadonlyMap; ancestorVersionByNodeId: ReadonlyMap; nodesById: ReadonlyMap; parentNodeIdsByChildNodeId: ReadonlyMap>; rangeByParentNodeId: ReadonlyMap>; peerFallbackEdgeKeys: ReadonlySet; }; type OverrideSelector = { name: string; range?: string; }; type OverrideAliasRule = { parentChain: OverrideSelector[]; target: OverrideSelector; registryName: string; }; type AliasRegistryConfig = ReturnType; /** * Resolve true npm registry identities onto lockfile graph nodes without * re-attributing non-registry git/tarball packages or un-installed peers. */ export function resolveNpmLockAliasIdentities(ctx: NpmLockAliasResolutionContext): void { const overrideRules = loadOverrideAliasRules(ctx.filePath, ctx.rootMeta); const legacyAliasByPath = collectLegacyAliasRegistryNames(ctx.rawLegacyDependencies); const registryConfig = resolveNpmAliasRegistryConfig(path.dirname(ctx.filePath)); for (const node of ctx.nodesById.values()) { const registryName = resolveNodeRegistryName(node, ctx, overrideRules, legacyAliasByPath, registryConfig); if (registryName) { node.registryName = registryName; } } } /** Format the canonical key tracking a fallback edge added from `peerDependencies`. */ export function peerFallbackEdgeKey(parentNodeId: string, childNodeId: string): string { return `${parentNodeId}\u0000${childNodeId}`; } function collectRegularDeclaredRanges(node: NpmLockNode, ctx: NpmLockAliasResolutionContext): string[] { const ranges: string[] = []; if (ctx.rootEntryNodeIds.has(node.id)) { const rootRange = ctx.rootRanges[node.name]; if (rootRange) ranges.push(rootRange); } for (const parentId of ctx.parentNodeIdsByChildNodeId.get(node.id) ?? []) { if (ctx.peerFallbackEdgeKeys.has(peerFallbackEdgeKey(parentId, node.id))) continue; const range = ctx.rangeByParentNodeId.get(parentId)?.get(node.name); if (range) ranges.push(range); } return ranges; } function resolveNodeRegistryName( node: NpmLockNode, ctx: NpmLockAliasResolutionContext, overrideRules: OverrideAliasRule[], legacyAliasByPath: Map, registryConfig: AliasRegistryConfig, ): string | null { const regularDeclaredRanges = collectRegularDeclaredRanges(node, ctx); const manifestName = ctx.manifestNameByNodeId.get(node.id); const resolvedUrl = ctx.resolvedUrlByNodeId.get(node.id); // 1. Explicit `npm:` spec on root or regular parent dependencies. const explicitAlias = regularDeclaredRanges .map(npmAliasRegistryName) .find((name) => name && name !== node.name); if (explicitAlias) return explicitAlias; // 2. Lockfile v2 legacy `dependencies` entry. const legacyAlias = legacyAliasByPath.get(node.packagePath); if (legacyAlias && legacyAlias !== node.name && (!manifestName || manifestName === legacyAlias)) { return legacyAlias; } // Remaining paths rely on a differing installed manifest `name`. if (!manifestName || manifestName === node.name) return null; // 3. Peer-derived `npm:` alias: only trust when the node's lockfile manifest `name` // matches the alias target, so `--legacy-peer-deps` or optional peers cannot // substitute an alias identity onto a different real package installed in that slot. if ( !isGitSource({ resolvedUrl }) && (regularDeclaredRanges.length === 0 || regularDeclaredRanges.every(looksLikeRegistryVersionSpec)) ) { const peerRanges = ctx.peerRangesByChildNodeId.get(node.id) ?? []; const peerAlias = peerRanges .map(npmAliasRegistryName) .find((name) => name === manifestName); if (peerAlias) return peerAlias; } // 4. Configured `overrides` (package.json or root lockfile metadata). if (!isGitSource({ resolvedUrl })) { const matchedRule = overrideRules.find( (rule) => rule.target.name === node.name && rule.registryName === manifestName && matchesAncestorChain(node.id, rule.parentChain, ctx), ); if (matchedRule) return matchedRule.registryName; } // 5. Standalone v3 lockfiles omit `overrides` and replace the child spec only in // node metadata; accept tarballs from npm or the configured registry when // every regular parent spec is a registry range/tag (#1450). if ( !isGitSource({ resolvedUrl }) && regularDeclaredRanges.length > 0 && regularDeclaredRanges.every(looksLikeRegistryVersionSpec) && isRegistryTarballForPackage(resolvedUrl, manifestName, node.version, registryConfig) ) { return manifestName; } return null; } function loadOverrideAliasRules( lockfilePath: string, rootMeta: RawRootLockMetadata | null, ): OverrideAliasRule[] { const pkgJson = readJsonObject(path.join(path.dirname(lockfilePath), "package.json")); const rootManifestDeps = collectRootManifestDeps(rootMeta, pkgJson); const rules: OverrideAliasRule[] = []; walkOverrideAliasRules(pkgJson?.overrides, [], rootManifestDeps, rules); walkOverrideAliasRules(rootMeta?.overrides, [], rootManifestDeps, rules); return rules; } function readJsonObject(filePath: string): Record | null { try { const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")); return isRecord(parsed) ? parsed : null; } catch { return null; } } function collectRootManifestDeps( rootMeta: RawRootLockMetadata | null, pkgJson: Record | null, ): Record { const deps: Record = {}; const sections = ["peerDependencies", "devDependencies", "optionalDependencies", "dependencies"] as const; for (const source of [rootMeta as Record | null, pkgJson]) { if (!source) continue; for (const sectionName of sections) { const section = source[sectionName]; if (!isRecord(section)) continue; for (const [name, spec] of Object.entries(section)) { if (typeof spec === "string" && spec) deps[name] = spec; } } } return deps; } function walkOverrideAliasRules( node: unknown, ancestors: OverrideSelector[], rootManifestDeps: Record, rules: OverrideAliasRule[], ): void { if (!isRecord(node)) return; for (const [key, value] of Object.entries(node)) { if (key === ".") { if (typeof value === "string" && ancestors.length > 0) { const registryName = resolveOverrideAliasRegistryName(value, rootManifestDeps); const target = ancestors[ancestors.length - 1]; if (registryName && target.name) { rules.push({ parentChain: ancestors.slice(0, -1), target, registryName, }); } } continue; } const selector = splitOverrideSelector(key); if (!selector.name) continue; if (typeof value === "string") { const registryName = resolveOverrideAliasRegistryName(value, rootManifestDeps); if (registryName) { rules.push({ parentChain: ancestors, target: selector, registryName }); } } else if (isRecord(value)) { walkOverrideAliasRules(value, [...ancestors, selector], rootManifestDeps, rules); } } } function splitOverrideSelector(key: string): OverrideSelector { const trimmed = key.trim(); const at = specifierAt(trimmed); if (at === -1) return { name: trimmed }; const name = bareName(trimmed); const range = trimmed.slice(at + 1).trim(); return range ? { name, range } : { name }; } function resolveOverrideAliasRegistryName( rawSpec: string, rootManifestDeps: Record, ): string | null { const trimmed = rawSpec.trim(); const resolved = trimmed.startsWith("$") ? rootManifestDeps[trimmed.slice(1)] : trimmed; return npmAliasRegistryName(resolved); } function matchesAncestorChain( childNodeId: string, parentChain: OverrideSelector[], ctx: Pick, ): boolean { if (parentChain.length === 0) return true; const visited = new Set(); const stack: Array<{ currentId: string; chainIndex: number }> = []; for (const parentId of ctx.parentNodeIdsByChildNodeId.get(childNodeId) ?? []) { stack.push({ currentId: parentId, chainIndex: parentChain.length - 1 }); } while (stack.length > 0) { const { currentId, chainIndex } = stack.pop()!; const visitKey = `${currentId}\u0000${chainIndex}`; if (visited.has(visitKey)) continue; visited.add(visitKey); const parentNode = ctx.nodesById.get(currentId); if (!parentNode) continue; const selector = parentChain[chainIndex]; const parentVersion = parentNode.version ?? ctx.ancestorVersionByNodeId.get(currentId) ?? null; const selectorMatches = parentNode.name === selector.name && (!selector.range || !isValidRange(selector.range) || (parentVersion !== null && satisfiesRange(parentVersion, selector.range))); if (selectorMatches) { if (chainIndex === 0) return true; for (const grandParentId of ctx.parentNodeIdsByChildNodeId.get(currentId) ?? []) { stack.push({ currentId: grandParentId, chainIndex: chainIndex - 1 }); } } // Nested npm overrides apply through intermediate transitive dependencies. for (const grandParentId of ctx.parentNodeIdsByChildNodeId.get(currentId) ?? []) { stack.push({ currentId: grandParentId, chainIndex }); } } return false; } function collectLegacyAliasRegistryNames(deps: unknown): Map { const byPath = new Map(); const walk = (entries: unknown, prefix: string): void => { if (!isRecord(entries)) return; for (const [name, meta] of Object.entries(entries)) { if (!isRecord(meta)) continue; const packagePath = prefix ? `${prefix}/node_modules/${name}` : `node_modules/${name}`; const registryName = npmAliasRegistryName(meta.version); if (registryName) byPath.set(packagePath, registryName); if (meta.dependencies) walk(meta.dependencies, packagePath); } }; walk(deps, ""); return byPath; } function looksLikeRegistryVersionSpec(spec: string): boolean { const trimmed = spec.trim(); if (!trimmed) return false; if (trimmed.startsWith("npm:")) return true; if (trimmed.includes(":") || trimmed.includes("/")) return false; return isValidRange(trimmed) || /^[A-Za-z0-9._-]+$/.test(trimmed); } function isRegistryTarballForPackage( resolvedUrl: string | undefined, manifestName: string, version: string | null, registryConfig: AliasRegistryConfig, ): boolean { if (!resolvedUrl || !version) return false; const tarball = URL.parse(resolvedUrl); if (!tarball || !["http:", "https:"].includes(tarball.protocol) || tarball.username || tarball.password) return false; const unscoped = manifestName.startsWith("@") ? manifestName.slice(manifestName.indexOf("/") + 1) : manifestName; if (!unscoped) return false; if (!tarball.pathname.endsWith(`/${manifestName}/-/${unscoped}-${version}.tgz`)) return false; return [NPM_PUBLIC_REGISTRY, registryForPackage(registryConfig, manifestName)].some((registry) => { const base = URL.parse(registry); return base !== null && base.origin === tarball.origin && tarball.pathname.startsWith(`${base.pathname.replace(/\/+$/, "")}/`); }); }