UNPKG

nx

Version:

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

965 lines (964 loc) • 41.1 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; 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 = parseLockFile(lockFileContent, lockFileHash); const dependencies = []; const workspacePackages = new Set(Object.keys(ctx.projects)); if (!lockFile.workspaces || Object.keys(lockFile.workspaces).length === 0) { return dependencies; } // Pre-compute workspace collections for performance const workspacePackageNames = getWorkspacePackageNames(lockFile); const workspacePaths = getWorkspacePaths(lockFile); const packageDeps = processPackageToPackageDependencies(lockFile, ctx, workspacePackages, workspacePackageNames, workspacePaths); 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; keyMap.clear(); packageVersions.clear(); specParseCache.clear(); } // ===== UTILITY FUNCTIONS ===== function getCachedSpecInfo(resolvedSpec) { if (!specParseCache.has(resolvedSpec)) { const { name, version } = parseResolvedSpec(resolvedSpec); const protocol = getProtocolFromResolvedSpec(resolvedSpec); specParseCache.set(resolvedSpec, { name, version, protocol }); } return specParseCache.get(resolvedSpec); } 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; } function parseLockFile(lockFileContent, lockFileHash) { if (currentLockFileHash === lockFileHash) { return cachedParsedLockFile; } 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; return result; } 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 = parseLockFile(lockFileContent, lockFileHash); const nodes = {}; const packageVersions = new Map(); if (!lockFile.packages || Object.keys(lockFile.packages).length === 0) { return nodes; } // Pre-compute workspace collections for performance const workspacePaths = getWorkspacePaths(lockFile); const workspacePackageNames = getWorkspacePackageNames(lockFile); const packageEntries = Object.entries(lockFile.packages); for (const [packageKey, packageData] of packageEntries) { const result = processPackageEntry(packageKey, packageData, lockFile, keyMap, nodes, packageVersions); if (result.shouldContinue) { continue; } } createHoistedNodes(packageVersions, lockFile, keyMap, nodes, workspacePaths, workspacePackageNames); 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, 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 }; } if (isWorkspacePackage(name, lockFile)) { return { shouldContinue: true }; } if (lockFile.patches && lockFile.patches[name]) { return { shouldContinue: true }; } if (protocol === 'workspace') { return { shouldContinue: true }; } const isWorkspaceSpecific = isNestedPackageKey(packageKey, lockFile); 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, lockFile, workspacePackages, workspacePackageNames) { return (workspacePackages.has(packageName) || workspacePackageNames.has(packageName) || isWorkspacePackage(packageName, lockFile) || (lockFile.patches && !!lockFile.patches[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 getAllWorkspaceDependencies(workspace) { return { ...workspace.dependencies, ...workspace.devDependencies, ...workspace.optionalDependencies, ...workspace.peerDependencies, }; } function processPackageToPackageDependencies(lockFile, ctx, workspacePackages, workspacePackageNames, workspacePaths) { 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, ctx, workspacePackages, workspacePackageNames, workspacePaths); dependencies.push(...packageDeps); } catch (error) { continue; } } return dependencies; } function processPackageForDependencies(packageKey, packageData, lockFile, ctx, workspacePackages, workspacePackageNames, workspacePaths) { if (isWorkspacePackage(packageKey, lockFile) || isNestedPackageKey(packageKey, lockFile, workspacePaths, workspacePackageNames)) { 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 (lockFile.patches && lockFile.patches[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, lockFile, ctx, workspacePackages, workspacePackageNames); 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, lockFile, ctx, workspacePackages, workspacePackageNames) { const dependencies = []; const depsEntries = Object.entries(deps); for (const [packageName, versionSpec] of depsEntries) { try { const dependency = processSingleDependency(packageName, versionSpec, sourceNodeName, lockFile, ctx, workspacePackages, workspacePackageNames); if (dependency) { dependencies.push(dependency); } } catch (error) { continue; } } return dependencies; } function processSingleDependency(packageName, versionSpec, sourceNodeName, lockFile, ctx, workspacePackages, workspacePackageNames) { if (typeof packageName !== 'string' || typeof versionSpec !== 'string') { return null; } if (isWorkspaceOrPatchedPackage(packageName, lockFile, workspacePackages, workspacePackageNames)) { 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 { const resolvedVersion = findResolvedVersion(packageName, versionSpec, lockFile.packages, lockFile.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; } // ===== WORKSPACE-RELATED FUNCTIONS ===== function getWorkspacePackageNames(lockFile) { const workspacePackageNames = new Set(); if (lockFile.workspacePackages) { for (const packageInfo of Object.values(lockFile.workspacePackages)) { workspacePackageNames.add(packageInfo.name); } } return workspacePackageNames; } function getWorkspacePaths(lockFile) { const workspacePaths = new Set(); if (lockFile.workspaces) { for (const workspacePath of Object.keys(lockFile.workspaces)) { if (workspacePath !== '') { workspacePaths.add(workspacePath); } } } return workspacePaths; } function isWorkspacePackage(packageName, lockFile) { // Check if package is in workspacePackages field if (lockFile.workspacePackages && lockFile.workspacePackages[packageName]) { return true; } // Check if package is defined in any workspace dependencies with workspace: prefix // or if it's a file dependency in workspace dependencies if (lockFile.workspaces) { for (const workspace of Object.values(lockFile.workspaces)) { const allDeps = getAllWorkspaceDependencies(workspace); if (allDeps[packageName]?.startsWith('workspace:')) { return true; } // Check if this is a file dependency defined in workspace dependencies // Always filter out file dependencies as they represent workspace packages if (allDeps[packageName]?.startsWith('file:')) { return true; } } } // Check if package appears in packages section with workspace: or file: protocol if (lockFile.packages) { for (const packageData of Object.values(lockFile.packages)) { if (Array.isArray(packageData) && packageData.length > 0) { const resolvedSpec = packageData[0]; if (typeof resolvedSpec === 'string') { const { name, protocol } = getCachedSpecInfo(resolvedSpec); if (name === packageName && (protocol === 'workspace' || protocol === 'file')) { return true; } } } } } return false; } // ===== HOISTING-RELATED FUNCTIONS ===== function createHoistedNodes(packageVersions, lockFile, keyMap, nodes, workspacePaths, workspacePackageNames) { for (const [packageName, versions] of packageVersions.entries()) { const hoistedNodeKey = `npm:${packageName}`; if (shouldCreateHoistedNode(packageName, lockFile, workspacePaths, workspacePackageNames)) { const hoistedVersion = getHoistedVersion(packageName, versions, lockFile); 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 * * Examples of workspace-specific/nested entries: * - "@quz/pkg1/lodash" (workspace-specific) * - "is-even/is-odd" (dependency nesting) * - "@quz/pkg2/is-even/is-odd" (workspace-specific nested) */ function isNestedPackageKey(packageKey, lockFile, workspacePaths, workspacePackageNames) { // If the key doesn't contain '/', it's a direct package entry if (!packageKey.includes('/')) { return false; } // Get workspace paths and package names for comparison const computedWorkspacePaths = workspacePaths || getWorkspacePaths(lockFile); const computedWorkspacePackageNames = workspacePackageNames || getWorkspacePackageNames(lockFile); // 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('/'); // Check against known workspace paths if (computedWorkspacePaths.has(prefix)) { return true; } // Check against workspace package names (scoped packages) if (computedWorkspacePackageNames.has(prefix)) { return true; } // Check for scoped workspace packages (e.g., "@quz/pkg1") if (prefix.startsWith('@') && prefix.includes('/')) { return true; } // This could be dependency nesting (e.g., "is-even/is-odd") // These should also be filtered out as they're not direct packages return true; } return false; } /** * Checks if a package has workspace-specific variants in the lockfile * Workspace-specific variants indicate the package should NOT be hoisted * Example: "@quz/pkg1/lodash" indicates lodash should not be hoisted for the @quz/pkg1 workspace * * This should NOT match dependency nesting like "is-even/is-odd" which represents * is-odd as a dependency of is-even, not a workspace-specific variant. */ function hasWorkspaceSpecificVariant(packageName, lockFile, workspacePaths, workspacePackageNames) { if (!lockFile.packages) return false; // Get list of known workspace paths to distinguish workspace-specific variants // from dependency nesting const computedWorkspacePaths = workspacePaths || getWorkspacePaths(lockFile); const computedWorkspacePackageNames = workspacePackageNames || getWorkspacePackageNames(lockFile); // Check if any package key follows pattern: "workspace/packageName" for (const packageKey of Object.keys(lockFile.packages)) { if (packageKey.includes('/') && packageKey.endsWith(`/${packageName}`)) { const prefix = packageKey.substring(0, packageKey.lastIndexOf(`/${packageName}`)); // Check if prefix is a known workspace path or workspace package name if (computedWorkspacePaths.has(prefix) || computedWorkspacePackageNames.has(prefix)) { return true; } // Also check for scoped workspace packages (e.g., "@quz/pkg1/lodash") if (prefix.startsWith('@') && prefix.includes('/')) { return true; } } } return false; } /** * Determines if a package should have a hoisted node created * A package should be hoisted if: * 1. It has a direct entry in the packages section (key matches package name exactly), OR * 2. It appears as a direct dependency in any workspace AND no workspace-specific variants exist * * This handles both cases: * - Packages with direct entries (like transitive deps) should be hoisted * - Packages in workspace deps without conflicts should be hoisted * - Packages with both direct entries and workspace-specific variants get both */ function shouldCreateHoistedNode(packageName, lockFile, workspacePaths, workspacePackageNames) { 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 = getAllWorkspaceDependencies(workspace); if (allDeps[packageName]) { appearsInWorkspace = true; break; } } if (appearsInWorkspace && !hasWorkspaceSpecificVariant(packageName, lockFile, workspacePaths, workspacePackageNames)) { return true; // Found in workspace deps and no conflicts } return false; } /** * Gets the version that should be used for a hoisted package * For truly hoisted packages, we look up the version from the main package entry */ function getHoistedVersion(packageName, availableVersions, lockFile) { if (!lockFile.packages) return null; // Look for the main package entry (not workspace-specific) const mainPackageData = lockFile.packages[packageName]; if (mainPackageData && Array.isArray(mainPackageData) && mainPackageData.length > 0) { const resolvedSpec = mainPackageData[0]; if (typeof resolvedSpec === 'string') { const { version } = getCachedSpecInfo(resolvedSpec); if (version && availableVersions.has(version)) { return version; } } } // Fallback: return the first available version return availableVersions.size > 0 ? Array.from(availableVersions)[0] : null; } /** * Finds the resolved version for a package given its version specification * * 1. Fast path: Check manifests for exact version match * 2. Scan all packages to find candidates with matching names * 3. Include alias packages where the target matches our package name * 4. Fallback: Search manifests for any matching package entries * 5. Use findBestVersionMatch to select the optimal version from candidates */ function findResolvedVersion(packageName, versionSpec, packages, manifests) { // Look for matching packages and collect all versions const candidateVersions = []; const packageEntries = Object.entries(packages); // Early manifest lookup for exact matches // Avoids expensive package scanning when exact version is available if (manifests) { const exactManifestKey = `${packageName}@${versionSpec}`; if (manifests[exactManifestKey]) { return versionSpec; } } for (const [packageKey, packageData] of packageEntries) { const [resolvedSpec] = packageData; // Skip non-string specs early if (typeof resolvedSpec !== 'string') { continue; } // Use cached spec parsing to avoid repeated string operations const { name, version } = getCachedSpecInfo(resolvedSpec); if (name === packageName) { // Include manifest information if available const manifest = manifests?.[`${name}@${version}`]; candidateVersions.push({ version, packageKey, manifest }); // Early termination if we find an exact version match if (version === versionSpec) { return version; } } // Check for alias packages where this package might be the target if (isAliasPackage(packageKey, name) && name === packageName) { // This alias points to the package we're looking for const manifest = manifests?.[`${name}@${version}`]; candidateVersions.push({ version, packageKey, manifest }); // Early termination if we find an exact version match if (version === versionSpec) { return version; } } } if (candidateVersions.length === 0) { // Try to find in manifests as fallback if (manifests) { const manifestKey = Object.keys(manifests).find((key) => key.startsWith(`${packageName}@`)); if (manifestKey) { const manifest = manifests[manifestKey]; const version = manifest.version; if (version) { candidateVersions.push({ version, packageKey: manifestKey, manifest, }); } } } if (candidateVersions.length === 0) { return null; } } // Handle different version specification patterns with enhanced logic const bestMatch = findBestVersionMatch(packageName, versionSpec, candidateVersions); return bestMatch ? bestMatch.version : null; } /** * Find the best version match for a given version specification * * 1. Check for exact version matches first (highest priority) * 2. Handle union ranges (||) by recursively checking each range * 3. For non-semver versions (git, file, etc.), prefer exact matches or return first candidate * 4. For semver versions, use semver.satisfies() to find compatible versions */ function findBestVersionMatch(packageName, versionSpec, candidates) { // For exact matches, return immediately const exactMatch = candidates.find((c) => c.version === versionSpec); if (exactMatch) { return exactMatch; } // Handle union ranges (||) if (versionSpec.includes('||')) { const ranges = versionSpec.split('||').map((r) => r.trim()); for (const range of ranges) { const match = findBestVersionMatch(packageName, range, candidates); if (match) { return match; } } return null; } // Handle non-semver versions (git, file, etc.) const nonSemverVersions = candidates.filter((c) => !c.version.match(/^\d+\.\d+\.\d+/)); if (nonSemverVersions.length > 0) { // For non-semver versions, use the first match or exact match const nonSemverMatch = nonSemverVersions.find((c) => c.version === versionSpec); if (nonSemverMatch) { return nonSemverMatch; } // If no exact match, return the first non-semver candidate return nonSemverVersions[0]; } // Handle semver versions const semverVersions = candidates.filter((c) => c.version.match(/^\d+\.\d+\.\d+/)); if (semverVersions.length === 0) { return candidates[0]; // Fallback to any available version } // 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 candidate as fallback return semverVersions[0]; } // 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 { // Use semver comparison if possible const aVersion = a.version.match(/^\d+\.\d+\.\d+/) ? a.version : '0.0.0'; const bVersion = b.version.match(/^\d+\.\d+\.\d+/) ? b.version : '0.0.0'; return aVersion.localeCompare(bVersion, undefined, { numeric: true, sensitivity: 'base', }); } catch { // Fallback to string comparison return b.version.localeCompare(a.version); } }); return sortedVersions[0]; }