nx
Version:
937 lines (936 loc) • 38.2 kB
JavaScript
;
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;
}