UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

937 lines (936 loc) • 38.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BunLockfileParseError = exports.BUN_TEXT_LOCK_FILE = exports.BUN_LOCK_FILE = void 0; exports.readBunLockFile = readBunLockFile; exports.getBunTextLockfileDependencies = getBunTextLockfileDependencies; exports.clearCache = clearCache; exports.getBunTextLockfileNodes = getBunTextLockfileNodes; const node_child_process_1 = require("node:child_process"); const node_fs_1 = require("node:fs"); const semver_1 = require("semver"); const project_graph_1 = require("../../../config/project-graph"); const file_hasher_1 = require("../../../hasher/file-hasher"); const project_graph_builder_1 = require("../../../project-graph/project-graph-builder"); const json_1 = require("../../../utils/json"); const DEPENDENCY_TYPES = [ 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', ]; exports.BUN_LOCK_FILE = 'bun.lockb'; exports.BUN_TEXT_LOCK_FILE = 'bun.lock'; let currentLockFileHash; let cachedParsedLockFile; let cachedPackageIndex; const keyMap = new Map(); const packageVersions = new Map(); const specParseCache = new Map(); // Structured error types for better error handling class BunLockfileParseError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = 'BunLockfileParseError'; } } exports.BunLockfileParseError = BunLockfileParseError; function readBunLockFile(lockFilePath) { if (lockFilePath.endsWith(exports.BUN_TEXT_LOCK_FILE)) { return (0, node_fs_1.readFileSync)(lockFilePath, { encoding: 'utf-8' }); } return (0, node_child_process_1.execSync)(`bun ${lockFilePath}`, { encoding: 'utf-8', maxBuffer: 1024 * 1024 * 10, windowsHide: false, }); } function getBunTextLockfileDependencies(lockFileContent, lockFileHash, ctx) { try { const { lockFile, index } = parseLockFile(lockFileContent, lockFileHash); const dependencies = []; const workspacePackages = new Set(Object.keys(ctx.projects)); if (!lockFile.workspaces || Object.keys(lockFile.workspaces).length === 0) { return dependencies; } const packageDeps = processPackageToPackageDependencies(lockFile, index, ctx, workspacePackages); dependencies.push(...packageDeps); return dependencies; } catch (error) { if (error instanceof Error) { throw error; } throw new Error(`Failed to get Bun lockfile dependencies: ${error.message}`); } } /** @internal */ function clearCache() { currentLockFileHash = undefined; cachedParsedLockFile = undefined; cachedPackageIndex = undefined; keyMap.clear(); packageVersions.clear(); specParseCache.clear(); } // ===== UTILITY FUNCTIONS ===== function getCachedSpecInfo(resolvedSpec) { let cached = specParseCache.get(resolvedSpec); if (!cached) { const { name, version } = parseResolvedSpec(resolvedSpec); const protocol = getProtocolFromResolvedSpec(resolvedSpec); cached = { name, version, protocol }; specParseCache.set(resolvedSpec, cached); } return cached; } function getProtocolFromResolvedSpec(resolvedSpec) { // Handle scoped packages properly let protocolAndSpec; if (resolvedSpec.startsWith('@')) { // For scoped packages, find the second @ which separates name from protocol const secondAtIndex = resolvedSpec.indexOf('@', 1); if (secondAtIndex === -1) { return 'npm'; // Default fallback } protocolAndSpec = resolvedSpec.substring(secondAtIndex + 1); } else { // For non-scoped packages, find the first @ which separates name from protocol const firstAtIndex = resolvedSpec.indexOf('@'); if (firstAtIndex === -1) { return 'npm'; // Default fallback } protocolAndSpec = resolvedSpec.substring(firstAtIndex + 1); } const colonIndex = protocolAndSpec.indexOf(':'); if (colonIndex === -1) { return 'npm'; // No protocol specified, default to npm } return protocolAndSpec.substring(0, colonIndex); } function parseResolvedSpec(resolvedSpec) { // Handle different resolution formats: // - "package-name@npm:1.0.0" // - "@scope/package-name@npm:1.0.0" // - "package-name@github:user/repo#commit" // - "package-name@file:./path" // - "package-name@https://example.com/package.tgz" // - "alias-name@npm:actual-package@version" (ALIAS FORMAT) // Handle scoped packages properly - they have an extra @ at the beginning // Format: @scope/package@protocol:spec let name; let protocolAndSpec; if (resolvedSpec.startsWith('@')) { // For scoped packages, find the second @ which separates name from protocol const secondAtIndex = resolvedSpec.indexOf('@', 1); if (secondAtIndex === -1) { return { name: '', version: '' }; } name = resolvedSpec.substring(0, secondAtIndex); protocolAndSpec = resolvedSpec.substring(secondAtIndex + 1); } else { // For non-scoped packages, find the first @ which separates name from protocol const firstAtIndex = resolvedSpec.indexOf('@'); if (firstAtIndex === -1) { return { name: '', version: '' }; } name = resolvedSpec.substring(0, firstAtIndex); protocolAndSpec = resolvedSpec.substring(firstAtIndex + 1); } // Parse protocol and spec const colonIndex = protocolAndSpec.indexOf(':'); // Handle specs without protocol prefix (e.g., "package@1.0.0" instead of "package@npm:1.0.0") if (colonIndex === -1) { // No protocol specified, treat as npm package with direct version return { name, version: protocolAndSpec }; } const protocol = protocolAndSpec.substring(0, colonIndex); const spec = protocolAndSpec.substring(colonIndex + 1); if (protocol === 'npm') { // For npm protocol, spec should always be in format: package@version // Examples: // - Regular: "package@npm:package@1.0.0" -> version: "1.0.0" // - Alias: "alias@npm:real-package@1.0.0" -> version: "npm:real-package@1.0.0" // Extract the package name and version from the spec const atIndex = spec.lastIndexOf('@'); if (atIndex === -1) { // Malformed spec, return as-is return { name, version: spec }; } const specPackageName = spec.substring(0, atIndex); const specVersion = spec.substring(atIndex + 1); if (specPackageName === name) { // Regular npm package: "package@npm:package@1.0.0" -> version: "1.0.0" return { name, version: specVersion }; } else { // Alias package: "alias@npm:real-package@1.0.0" -> version: "npm:real-package@1.0.0" return { name, version: `npm:${spec}` }; } } else if (protocol === 'workspace') { // Workspace dependencies use the workspace path as version return { name, version: `workspace:${spec}` }; } else if (protocol === 'github' || protocol === 'git') { // Extract commit hash from GitHub/Git reference // Format: user/repo#commit-hash or repo-url#commit-hash const gitMatch = spec.match(/^(.+?)#(.+)$/); if (gitMatch) { const [, repo, commit] = gitMatch; return { name, version: `${protocol}:${repo}#${commit}` }; } else { return { name, version: `${protocol}:${spec}` }; } } else if (protocol === 'file' || protocol === 'link') { // File/Link dependencies use the file path as version return { name, version: `${protocol}:${spec}` }; } else if (protocol === 'https' || protocol === 'http') { // Tarball dependencies use the full URL as version return { name, version: `${protocol}:${spec}` }; } else { // For any other protocols, use the original spec as version return { name, version: resolvedSpec }; } } function calculatePackageHash(packageData, lockFile, name, version) { const [resolvedSpec, tarballUrl, metadata, hash] = packageData; // For NPM packages (4 elements), use the integrity hash if (packageData.length === 4 && hash && typeof hash === 'string') { // Use better hash from manifests if available if (lockFile.manifests && lockFile.manifests[`${name}@${version}`]) { const manifest = lockFile.manifests[`${name}@${version}`]; if (manifest.dist && manifest.dist.shasum) { return manifest.dist.shasum; } } return hash; } // For other package types, calculate hash from available data const hashData = [resolvedSpec]; if (tarballUrl && typeof tarballUrl === 'string') hashData.push(tarballUrl); if (metadata) hashData.push(JSON.stringify(metadata)); if (hash && typeof hash === 'string') hashData.push(hash); return (0, file_hasher_1.hashArray)(hashData); } /** * Determines if a package is an alias by comparing the package key with the resolved spec name * In Bun lockfiles, aliases are identified when the package key differs from the resolved package name */ function isAliasPackage(packageKey, resolvedPackageName) { return packageKey !== resolvedPackageName; } /** * Build a pre-computed index for O(1) lookups * This is the key optimization - we do one pass through all packages * and build indexes that can be queried in O(1) time later */ function buildPackageIndex(lockFile) { const byName = new Map(); const workspaceNames = new Set(); const workspacePaths = new Set(); const packagesWithWorkspaceVariants = new Set(); const patchedPackages = new Set(); // Collect workspace paths if (lockFile.workspaces) { for (const workspacePath of Object.keys(lockFile.workspaces)) { if (workspacePath !== '') { workspacePaths.add(workspacePath); } } } // Collect workspace package names from workspacePackages field if (lockFile.workspacePackages) { for (const packageInfo of Object.values(lockFile.workspacePackages)) { workspaceNames.add(packageInfo.name); } } // Collect patched package names if (lockFile.patches) { for (const packageName of Object.keys(lockFile.patches)) { patchedPackages.add(packageName); } } // Collect workspace package names from workspace dependencies if (lockFile.workspaces) { for (const workspace of Object.values(lockFile.workspaces)) { const allDeps = { ...workspace.dependencies, ...workspace.devDependencies, ...workspace.optionalDependencies, ...workspace.peerDependencies, }; for (const [depName, depVersion] of Object.entries(allDeps)) { if (depVersion.startsWith('workspace:') || depVersion.startsWith('file:')) { workspaceNames.add(depName); } } } } // Single pass through all packages to build indexes if (lockFile.packages) { for (const [packageKey, packageData] of Object.entries(lockFile.packages)) { if (!Array.isArray(packageData) || packageData.length < 1) { continue; } const resolvedSpec = packageData[0]; if (typeof resolvedSpec !== 'string') { continue; } const { name, version, protocol } = getCachedSpecInfo(resolvedSpec); if (!name || !version) { continue; } // Track workspace packages from packages section if (protocol === 'workspace' || protocol === 'file') { workspaceNames.add(name); } // Check if this is a workspace-specific variant (e.g., "@quz/pkg1/lodash") if (packageKey.includes('/') && packageKey !== name) { // Check if it ends with a package name pattern const lastSlash = packageKey.lastIndexOf('/'); if (lastSlash > 0) { const possiblePackageName = packageKey.substring(lastSlash + 1); const prefix = packageKey.substring(0, lastSlash); // If the prefix is a workspace path or scoped package pattern // For scoped packages, require prefix to contain '/' (e.g., "@scope/pkg") // to avoid incorrectly matching just "@scope" if (workspacePaths.has(prefix) || (prefix.startsWith('@') && prefix.includes('/'))) { packagesWithWorkspaceVariants.add(possiblePackageName); } } } // Build the byName index let entries = byName.get(name); if (!entries) { entries = []; byName.set(name, entries); } // Calculate hash once const hash = calculatePackageHash(packageData, lockFile, name, version); entries.push({ version, packageKey, hash }); } } return { byName, workspaceNames, workspacePaths, packagesWithWorkspaceVariants, patchedPackages, }; } function parseLockFile(lockFileContent, lockFileHash) { if (currentLockFileHash === lockFileHash && cachedParsedLockFile && cachedPackageIndex) { return { lockFile: cachedParsedLockFile, index: cachedPackageIndex }; } clearCache(); try { const result = (0, json_1.parseJson)(lockFileContent, { allowTrailingComma: true, expectComments: true, }); // Validate basic structure if (!result || typeof result !== 'object') { throw new Error('Lockfile root must be an object'); } // Validate lockfile version if (result.lockfileVersion !== undefined) { if (typeof result.lockfileVersion !== 'number') { throw new Error(`Lockfile version must be a number, got ${typeof result.lockfileVersion}`); } const supportedVersions = [0, 1]; if (!supportedVersions.includes(result.lockfileVersion)) { throw new Error(`Unsupported lockfile version ${result.lockfileVersion}. Supported versions: ${supportedVersions.join(', ')}`); } } if (!result.packages || typeof result.packages !== 'object') { throw new Error('Lockfile packages section must be an object'); } if (!result.workspaces || typeof result.workspaces !== 'object') { throw new Error('Lockfile workspaces section must be an object'); } // Validate optional sections if (result.patches && typeof result.patches !== 'object') { throw new Error('Lockfile patches section must be an object'); } if (result.manifests && typeof result.manifests !== 'object') { throw new Error('Lockfile manifests section must be an object'); } if (result.workspacePackages && typeof result.workspacePackages !== 'object') { throw new Error('Lockfile workspacePackages section must be an object'); } // Validate structure of patches entries if (result.patches) { for (const [packageName, patchInfo] of Object.entries(result.patches)) { if (!patchInfo || typeof patchInfo !== 'object') { throw new Error(`Invalid patch entry for package "${packageName}": must be an object`); } if (!patchInfo.path || typeof patchInfo.path !== 'string') { throw new Error(`Invalid patch entry for package "${packageName}": path must be a string`); } } } // Validate structure of workspace packages entries if (result.workspacePackages) { for (const [packageName, packageInfo] of Object.entries(result.workspacePackages)) { if (!packageInfo || typeof packageInfo !== 'object') { throw new Error(`Invalid workspace package entry for "${packageName}": must be an object`); } if (!packageInfo.name || typeof packageInfo.name !== 'string') { throw new Error(`Invalid workspace package entry for "${packageName}": name must be a string`); } if (!packageInfo.version || typeof packageInfo.version !== 'string') { throw new Error(`Invalid workspace package entry for "${packageName}": version must be a string`); } } } cachedParsedLockFile = result; currentLockFileHash = lockFileHash; // Build the optimized index cachedPackageIndex = buildPackageIndex(result); return { lockFile: result, index: cachedPackageIndex }; } catch (error) { // Handle JSON parsing errors if (error.message.includes('JSON') || error.message.includes('InvalidSymbol')) { throw new BunLockfileParseError('Failed to parse Bun lockfile: Invalid JSON syntax. Please check for syntax errors or regenerate the lockfile.', error); } // Re-throw parsing errors as-is (for validation errors) if (error instanceof Error) { throw error; } // Handle unknown errors throw new BunLockfileParseError(`Failed to parse Bun lockfile: ${error.message}`, error); } } // ===== MAIN EXPORT FUNCTIONS ===== function getBunTextLockfileNodes(lockFileContent, lockFileHash) { try { const { lockFile, index } = parseLockFile(lockFileContent, lockFileHash); const nodes = {}; const localPackageVersions = new Map(); if (!lockFile.packages || Object.keys(lockFile.packages).length === 0) { return nodes; } const packageEntries = Object.entries(lockFile.packages); for (const [packageKey, packageData] of packageEntries) { const result = processPackageEntry(packageKey, packageData, lockFile, index, keyMap, nodes, localPackageVersions); if (result.shouldContinue) { continue; } } createHoistedNodes(localPackageVersions, lockFile, index, keyMap, nodes); return nodes; } catch (error) { if (error instanceof Error) { throw error; } throw new Error(`Failed to get Bun lockfile nodes: ${error.message}`); } } function processPackageEntry(packageKey, packageData, lockFile, index, keyMap, nodes, packageVersions) { try { if (!Array.isArray(packageData) || packageData.length < 1 || packageData.length > 4) { console.warn(`Lockfile contains invalid package entry '${packageKey}'. Try regenerating the lockfile with 'bun install --force'.\nDebug: expected 1-4 elements, got ${JSON.stringify(packageData)}`); return { shouldContinue: true }; } const [resolvedSpec] = packageData; if (typeof resolvedSpec !== 'string') { console.warn(`Lockfile contains corrupted package entry '${packageKey}'. Try regenerating the lockfile with 'bun install --force'.\nDebug: expected string, got ${typeof resolvedSpec}`); return { shouldContinue: true }; } const { name, version, protocol } = getCachedSpecInfo(resolvedSpec); if (!name || !version) { console.warn(`Lockfile contains unrecognized package format. Try regenerating the lockfile with 'bun install --force'.\nDebug: could not parse resolved spec '${resolvedSpec}'`); return { shouldContinue: true }; } // O(1) lookups using index if (index.workspaceNames.has(name)) { return { shouldContinue: true }; } if (index.patchedPackages.has(name)) { return { shouldContinue: true }; } if (protocol === 'workspace') { return { shouldContinue: true }; } const isWorkspaceSpecific = isNestedPackageKey(packageKey, index); if (!isWorkspaceSpecific && isAliasPackage(packageKey, name)) { const aliasName = packageKey; const actualPackageName = name; const actualVersion = version; const aliasNodeKey = `npm:${aliasName}`; if (!keyMap.has(aliasNodeKey)) { const aliasNode = { type: 'npm', name: aliasNodeKey, data: { version: `npm:${actualPackageName}@${actualVersion}`, packageName: aliasName, hash: calculatePackageHash(packageData, lockFile, aliasName, `npm:${actualPackageName}@${actualVersion}`), }, }; keyMap.set(aliasNodeKey, aliasNode); nodes[aliasNodeKey] = aliasNode; } const targetNodeKey = `npm:${actualPackageName}@${actualVersion}`; if (!keyMap.has(targetNodeKey)) { const targetNode = { type: 'npm', name: targetNodeKey, data: { version: actualVersion, packageName: actualPackageName, hash: calculatePackageHash(packageData, lockFile, actualPackageName, actualVersion), }, }; keyMap.set(targetNodeKey, targetNode); nodes[targetNodeKey] = targetNode; } if (!packageVersions.has(aliasName)) { packageVersions.set(aliasName, new Set()); } packageVersions .get(aliasName) .add(`npm:${actualPackageName}@${actualVersion}`); if (!packageVersions.has(actualPackageName)) { packageVersions.set(actualPackageName, new Set()); } packageVersions.get(actualPackageName).add(actualVersion); } else { if (!packageVersions.has(name)) { packageVersions.set(name, new Set()); } packageVersions.get(name).add(version); const nodeKey = `npm:${name}@${version}`; if (keyMap.has(nodeKey)) { nodes[nodeKey] = keyMap.get(nodeKey); return { shouldContinue: false }; } const nodeHash = calculatePackageHash(packageData, lockFile, name, version); const node = { type: 'npm', name: nodeKey, data: { version, packageName: name, hash: nodeHash, }, }; keyMap.set(nodeKey, node); nodes[nodeKey] = node; } return { shouldContinue: false }; } catch (error) { console.warn(`Unable to process package '${packageKey}'. The lockfile may be corrupted. Try regenerating with 'bun install --force'.\nDebug: ${error.message}`); return { shouldContinue: true }; } } function isWorkspaceOrPatchedPackage(packageName, index, workspacePackages) { return (workspacePackages.has(packageName) || index.workspaceNames.has(packageName) || index.patchedPackages.has(packageName)); } function resolveAliasTarget(versionSpec) { if (!versionSpec.startsWith('npm:')) return null; const actualSpec = versionSpec.substring(4); const actualAtIndex = actualSpec.lastIndexOf('@'); return { packageName: actualSpec.substring(0, actualAtIndex), version: actualSpec.substring(actualAtIndex + 1), }; } function processPackageToPackageDependencies(lockFile, index, ctx, workspacePackages) { const dependencies = []; if (!lockFile.packages || Object.keys(lockFile.packages).length === 0) { return dependencies; } const packageEntries = Object.entries(lockFile.packages); for (const [packageKey, packageData] of packageEntries) { try { const packageDeps = processPackageForDependencies(packageKey, packageData, lockFile, index, ctx, workspacePackages); dependencies.push(...packageDeps); } catch (error) { continue; } } return dependencies; } function processPackageForDependencies(packageKey, packageData, lockFile, index, ctx, workspacePackages) { // O(1) checks using index if (index.workspaceNames.has(packageKey) || isNestedPackageKey(packageKey, index)) { return []; } if (!Array.isArray(packageData) || packageData.length < 1) { return []; } const [resolvedSpec] = packageData; if (typeof resolvedSpec !== 'string') { return []; } const { name: sourcePackageName, version: sourceVersion } = getCachedSpecInfo(resolvedSpec); if (!sourcePackageName || !sourceVersion) { return []; } if (index.patchedPackages.has(sourcePackageName)) { return []; } const sourceNodeName = `npm:${sourcePackageName}@${sourceVersion}`; if (!ctx.externalNodes[sourceNodeName]) { return []; } const packageDependencies = extractPackageDependencies(packageData); if (!packageDependencies) { return []; } const dependencies = []; for (const depType of DEPENDENCY_TYPES) { const deps = packageDependencies[depType]; if (!deps || typeof deps !== 'object') continue; const depDependencies = processDependencyEntries(deps, sourceNodeName, index, ctx, workspacePackages, lockFile.manifests); dependencies.push(...depDependencies); } return dependencies; } function extractPackageDependencies(packageData) { if (packageData.length >= 3 && packageData[2] && typeof packageData[2] === 'object') { return packageData[2]; } if (packageData.length >= 2 && packageData[1] && typeof packageData[1] === 'object') { return packageData[1]; } return undefined; } function processDependencyEntries(deps, sourceNodeName, index, ctx, workspacePackages, manifests) { const dependencies = []; const depsEntries = Object.entries(deps); for (const [packageName, versionSpec] of depsEntries) { try { const dependency = processSingleDependency(packageName, versionSpec, sourceNodeName, index, ctx, workspacePackages, manifests); if (dependency) { dependencies.push(dependency); } } catch (error) { continue; } } return dependencies; } function processSingleDependency(packageName, versionSpec, sourceNodeName, index, ctx, workspacePackages, manifests) { if (typeof packageName !== 'string' || typeof versionSpec !== 'string') { return null; } // O(1) check using index if (isWorkspaceOrPatchedPackage(packageName, index, workspacePackages)) { return null; } if (versionSpec.startsWith('workspace:')) { return null; } let targetPackageName = packageName; let targetVersion = versionSpec; const aliasTarget = resolveAliasTarget(versionSpec); if (aliasTarget) { targetPackageName = aliasTarget.packageName; targetVersion = aliasTarget.version; } else { // O(1) lookup using index instead of O(n) scan const resolvedVersion = findResolvedVersion(packageName, versionSpec, index, manifests); if (!resolvedVersion) { return null; } targetVersion = resolvedVersion; } const targetNodeName = resolveTargetNodeName(targetPackageName, targetVersion, ctx); if (!targetNodeName) { return null; } const dependency = { source: sourceNodeName, target: targetNodeName, type: project_graph_1.DependencyType.static, }; try { (0, project_graph_builder_1.validateDependency)(dependency, ctx); return dependency; } catch (e) { return null; } } function resolveTargetNodeName(targetPackageName, targetVersion, ctx) { const hoistedNodeName = `npm:${targetPackageName}`; const versionedNodeName = `npm:${targetPackageName}@${targetVersion}`; if (ctx.externalNodes[versionedNodeName]) { return versionedNodeName; } if (ctx.externalNodes[hoistedNodeName]) { return hoistedNodeName; } return null; } // ===== HOISTING-RELATED FUNCTIONS ===== function createHoistedNodes(packageVersions, lockFile, index, keyMap, nodes) { for (const [packageName, versions] of packageVersions.entries()) { const hoistedNodeKey = `npm:${packageName}`; if (shouldCreateHoistedNode(packageName, lockFile, index)) { const hoistedVersion = getHoistedVersion(packageName, versions, index); if (hoistedVersion) { const versionedNodeKey = `npm:${packageName}@${hoistedVersion}`; const versionedNode = keyMap.get(versionedNodeKey); if (versionedNode && !keyMap.has(hoistedNodeKey)) { const hoistedNode = { type: 'npm', name: hoistedNodeKey, data: { version: hoistedVersion, packageName: packageName, hash: versionedNode.data.hash, }, }; keyMap.set(hoistedNodeKey, hoistedNode); nodes[hoistedNodeKey] = hoistedNode; } } } } } /** * Checks if a package key represents a workspace-specific or nested dependency entry * These entries should not become external nodes, they are used only for resolution * * O(1) lookup using pre-computed index */ function isNestedPackageKey(packageKey, index) { // If the key doesn't contain '/', it's a direct package entry if (!packageKey.includes('/')) { return false; } // Check if this looks like a workspace-specific or nested entry const parts = packageKey.split('/'); // For multi-part keys, check if the prefix is a workspace path or package name if (parts.length >= 2) { const prefix = parts.slice(0, -1).join('/'); // O(1) check against known workspace paths if (index.workspacePaths.has(prefix)) { return true; } // O(1) check against workspace package names (scoped packages) if (index.workspaceNames.has(prefix)) { return true; } // Check for scoped workspace packages (e.g., "@quz/pkg1/lodash") // The prefix must contain '/' to be a scoped package (e.g., "@scope/pkg") // A prefix like just "@scope" without '/' is not a scoped package if (prefix.startsWith('@') && prefix.includes('/')) { return true; } // If the key looks like a simple scoped package (e.g., "@custom/lodash") // where parts.length === 2 and first part starts with '@', it's likely // a scoped package alias, not a nested dependency if (parts.length === 2 && parts[0].startsWith('@')) { return false; } // This could be dependency nesting (e.g., "is-even/is-odd") // These should be filtered out as they're not direct packages return true; } return false; } /** * Determines if a package should have a hoisted node created * O(1) lookup using pre-computed index */ function shouldCreateHoistedNode(packageName, lockFile, index) { if (!lockFile.workspaces || !lockFile.packages) return false; // First check if the package has a direct entry in the packages section // Direct entries should always be hoisted (they represent the canonical version) if (lockFile.packages[packageName]) { return true; } // For packages without direct entries, check if they appear in workspace dependencies // and don't have workspace-specific variants (which would cause conflicts) let appearsInWorkspace = false; for (const workspace of Object.values(lockFile.workspaces)) { const allDeps = { ...workspace.dependencies, ...workspace.devDependencies, ...workspace.optionalDependencies, ...workspace.peerDependencies, }; if (allDeps[packageName]) { appearsInWorkspace = true; break; } } // O(1) check using pre-computed index if (appearsInWorkspace && !index.packagesWithWorkspaceVariants.has(packageName)) { return true; } return false; } /** * Gets the version that should be used for a hoisted package * O(1) lookup using pre-computed index */ function getHoistedVersion(packageName, availableVersions, index) { // Use the index to find the main package version const candidates = index.byName.get(packageName); if (!candidates || candidates.length === 0) { return availableVersions.size > 0 ? availableVersions.values().next().value : null; } // Look for the main package entry (the one where packageKey === packageName) const mainEntry = candidates.find((c) => c.packageKey === packageName); if (mainEntry && availableVersions.has(mainEntry.version)) { return mainEntry.version; } // Fallback: return the first available version return availableVersions.size > 0 ? availableVersions.values().next().value : null; } /** * Finds the resolved version for a package given its version specification * * O(1) lookup using pre-computed index instead of O(n) scan through all packages */ function findResolvedVersion(packageName, versionSpec, index, manifests) { // O(1) lookup const candidates = index.byName.get(packageName); if (!candidates || candidates.length === 0) { // Try to find in manifests as fallback if (manifests) { const exactManifestKey = `${packageName}@${versionSpec}`; if (manifests[exactManifestKey]) { return versionSpec; } const manifestKey = Object.keys(manifests).find((key) => key.startsWith(`${packageName}@`)); if (manifestKey) { const manifest = manifests[manifestKey]; if (manifest.version) { return manifest.version; } } } return null; } // Check for exact version match first (most common case) const exactMatch = candidates.find((c) => c.version === versionSpec); if (exactMatch) { return exactMatch.version; } // Handle different version specification patterns return findBestVersionMatch(versionSpec, candidates); } /** * Find the best version match for a given version specification * Only operates on the pre-filtered candidates array (usually 1-3 items) */ function findBestVersionMatch(versionSpec, candidates) { if (candidates.length === 0) { return null; } // For exact matches, return immediately (already checked above but defensive) const exactMatch = candidates.find((c) => c.version === versionSpec); if (exactMatch) { return exactMatch.version; } // Handle union ranges (||) if (versionSpec.includes('||')) { const ranges = versionSpec.split('||').map((r) => r.trim()); for (const range of ranges) { const match = findBestVersionMatch(range, candidates); if (match) { return match; } } return null; } // Separate semver and non-semver versions const semverVersions = []; const nonSemverVersions = []; for (const c of candidates) { if (/^\d+\.\d+\.\d+/.test(c.version)) { semverVersions.push(c); } else { nonSemverVersions.push(c); } } // Handle non-semver versions (git, file, etc.) if (nonSemverVersions.length > 0) { const nonSemverMatch = nonSemverVersions.find((c) => c.version === versionSpec); if (nonSemverMatch) { return nonSemverMatch.version; } // If no exact match for non-semver, continue to semver matching } // Handle semver versions if (semverVersions.length === 0) { // No semver versions, return first non-semver if available return nonSemverVersions.length > 0 ? nonSemverVersions[0].version : null; } // Find all versions that satisfy the spec const satisfyingVersions = semverVersions.filter((candidate) => { try { return (0, semver_1.satisfies)(candidate.version, versionSpec); } catch (error) { // If semver fails, fall back to string comparison return candidate.version === versionSpec; } }); if (satisfyingVersions.length === 0) { // No satisfying versions found, return the first semver candidate as fallback return semverVersions[0].version; } // Return the highest satisfying version (similar to npm behavior) // Sort versions in descending order and return the first one const sortedVersions = satisfyingVersions.sort((a, b) => { try { return b.version.localeCompare(a.version, undefined, { numeric: true, sensitivity: 'base', }); } catch { // Fallback to string comparison return b.version.localeCompare(a.version); } }); return sortedVersions[0].version; }