nx 22.2.0-canary.20251203-2e442d5 → 22.2.0-canary.20251204-0f24bf5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ exports.BUN_LOCK_FILE = 'bun.lockb';
22
22
  exports.BUN_TEXT_LOCK_FILE = 'bun.lock';
23
23
  let currentLockFileHash;
24
24
  let cachedParsedLockFile;
25
+ let cachedPackageIndex;
25
26
  const keyMap = new Map();
26
27
  const packageVersions = new Map();
27
28
  const specParseCache = new Map();
@@ -46,16 +47,13 @@ function readBunLockFile(lockFilePath) {
46
47
  }
47
48
  function getBunTextLockfileDependencies(lockFileContent, lockFileHash, ctx) {
48
49
  try {
49
- const lockFile = parseLockFile(lockFileContent, lockFileHash);
50
+ const { lockFile, index } = parseLockFile(lockFileContent, lockFileHash);
50
51
  const dependencies = [];
51
52
  const workspacePackages = new Set(Object.keys(ctx.projects));
52
53
  if (!lockFile.workspaces || Object.keys(lockFile.workspaces).length === 0) {
53
54
  return dependencies;
54
55
  }
55
- // Pre-compute workspace collections for performance
56
- const workspacePackageNames = getWorkspacePackageNames(lockFile);
57
- const workspacePaths = getWorkspacePaths(lockFile);
58
- const packageDeps = processPackageToPackageDependencies(lockFile, ctx, workspacePackages, workspacePackageNames, workspacePaths);
56
+ const packageDeps = processPackageToPackageDependencies(lockFile, index, ctx, workspacePackages);
59
57
  dependencies.push(...packageDeps);
60
58
  return dependencies;
61
59
  }
@@ -70,18 +68,21 @@ function getBunTextLockfileDependencies(lockFileContent, lockFileHash, ctx) {
70
68
  function clearCache() {
71
69
  currentLockFileHash = undefined;
72
70
  cachedParsedLockFile = undefined;
71
+ cachedPackageIndex = undefined;
73
72
  keyMap.clear();
74
73
  packageVersions.clear();
75
74
  specParseCache.clear();
76
75
  }
77
76
  // ===== UTILITY FUNCTIONS =====
78
77
  function getCachedSpecInfo(resolvedSpec) {
79
- if (!specParseCache.has(resolvedSpec)) {
78
+ let cached = specParseCache.get(resolvedSpec);
79
+ if (!cached) {
80
80
  const { name, version } = parseResolvedSpec(resolvedSpec);
81
81
  const protocol = getProtocolFromResolvedSpec(resolvedSpec);
82
- specParseCache.set(resolvedSpec, { name, version, protocol });
82
+ cached = { name, version, protocol };
83
+ specParseCache.set(resolvedSpec, cached);
83
84
  }
84
- return specParseCache.get(resolvedSpec);
85
+ return cached;
85
86
  }
86
87
  function getProtocolFromResolvedSpec(resolvedSpec) {
87
88
  // Handle scoped packages properly
@@ -228,9 +229,112 @@ function calculatePackageHash(packageData, lockFile, name, version) {
228
229
  function isAliasPackage(packageKey, resolvedPackageName) {
229
230
  return packageKey !== resolvedPackageName;
230
231
  }
232
+ /**
233
+ * Build a pre-computed index for O(1) lookups
234
+ * This is the key optimization - we do one pass through all packages
235
+ * and build indexes that can be queried in O(1) time later
236
+ */
237
+ function buildPackageIndex(lockFile) {
238
+ const byName = new Map();
239
+ const workspaceNames = new Set();
240
+ const workspacePaths = new Set();
241
+ const packagesWithWorkspaceVariants = new Set();
242
+ const patchedPackages = new Set();
243
+ // Collect workspace paths
244
+ if (lockFile.workspaces) {
245
+ for (const workspacePath of Object.keys(lockFile.workspaces)) {
246
+ if (workspacePath !== '') {
247
+ workspacePaths.add(workspacePath);
248
+ }
249
+ }
250
+ }
251
+ // Collect workspace package names from workspacePackages field
252
+ if (lockFile.workspacePackages) {
253
+ for (const packageInfo of Object.values(lockFile.workspacePackages)) {
254
+ workspaceNames.add(packageInfo.name);
255
+ }
256
+ }
257
+ // Collect patched package names
258
+ if (lockFile.patches) {
259
+ for (const packageName of Object.keys(lockFile.patches)) {
260
+ patchedPackages.add(packageName);
261
+ }
262
+ }
263
+ // Collect workspace package names from workspace dependencies
264
+ if (lockFile.workspaces) {
265
+ for (const workspace of Object.values(lockFile.workspaces)) {
266
+ const allDeps = {
267
+ ...workspace.dependencies,
268
+ ...workspace.devDependencies,
269
+ ...workspace.optionalDependencies,
270
+ ...workspace.peerDependencies,
271
+ };
272
+ for (const [depName, depVersion] of Object.entries(allDeps)) {
273
+ if (depVersion.startsWith('workspace:') ||
274
+ depVersion.startsWith('file:')) {
275
+ workspaceNames.add(depName);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ // Single pass through all packages to build indexes
281
+ if (lockFile.packages) {
282
+ for (const [packageKey, packageData] of Object.entries(lockFile.packages)) {
283
+ if (!Array.isArray(packageData) || packageData.length < 1) {
284
+ continue;
285
+ }
286
+ const resolvedSpec = packageData[0];
287
+ if (typeof resolvedSpec !== 'string') {
288
+ continue;
289
+ }
290
+ const { name, version, protocol } = getCachedSpecInfo(resolvedSpec);
291
+ if (!name || !version) {
292
+ continue;
293
+ }
294
+ // Track workspace packages from packages section
295
+ if (protocol === 'workspace' || protocol === 'file') {
296
+ workspaceNames.add(name);
297
+ }
298
+ // Check if this is a workspace-specific variant (e.g., "@quz/pkg1/lodash")
299
+ if (packageKey.includes('/') && packageKey !== name) {
300
+ // Check if it ends with a package name pattern
301
+ const lastSlash = packageKey.lastIndexOf('/');
302
+ if (lastSlash > 0) {
303
+ const possiblePackageName = packageKey.substring(lastSlash + 1);
304
+ const prefix = packageKey.substring(0, lastSlash);
305
+ // If the prefix is a workspace path or scoped package pattern
306
+ // For scoped packages, require prefix to contain '/' (e.g., "@scope/pkg")
307
+ // to avoid incorrectly matching just "@scope"
308
+ if (workspacePaths.has(prefix) ||
309
+ (prefix.startsWith('@') && prefix.includes('/'))) {
310
+ packagesWithWorkspaceVariants.add(possiblePackageName);
311
+ }
312
+ }
313
+ }
314
+ // Build the byName index
315
+ let entries = byName.get(name);
316
+ if (!entries) {
317
+ entries = [];
318
+ byName.set(name, entries);
319
+ }
320
+ // Calculate hash once
321
+ const hash = calculatePackageHash(packageData, lockFile, name, version);
322
+ entries.push({ version, packageKey, hash });
323
+ }
324
+ }
325
+ return {
326
+ byName,
327
+ workspaceNames,
328
+ workspacePaths,
329
+ packagesWithWorkspaceVariants,
330
+ patchedPackages,
331
+ };
332
+ }
231
333
  function parseLockFile(lockFileContent, lockFileHash) {
232
- if (currentLockFileHash === lockFileHash) {
233
- return cachedParsedLockFile;
334
+ if (currentLockFileHash === lockFileHash &&
335
+ cachedParsedLockFile &&
336
+ cachedPackageIndex) {
337
+ return { lockFile: cachedParsedLockFile, index: cachedPackageIndex };
234
338
  }
235
339
  clearCache();
236
340
  try {
@@ -296,7 +400,9 @@ function parseLockFile(lockFileContent, lockFileHash) {
296
400
  }
297
401
  cachedParsedLockFile = result;
298
402
  currentLockFileHash = lockFileHash;
299
- return result;
403
+ // Build the optimized index
404
+ cachedPackageIndex = buildPackageIndex(result);
405
+ return { lockFile: result, index: cachedPackageIndex };
300
406
  }
301
407
  catch (error) {
302
408
  // Handle JSON parsing errors
@@ -315,23 +421,20 @@ function parseLockFile(lockFileContent, lockFileHash) {
315
421
  // ===== MAIN EXPORT FUNCTIONS =====
316
422
  function getBunTextLockfileNodes(lockFileContent, lockFileHash) {
317
423
  try {
318
- const lockFile = parseLockFile(lockFileContent, lockFileHash);
424
+ const { lockFile, index } = parseLockFile(lockFileContent, lockFileHash);
319
425
  const nodes = {};
320
- const packageVersions = new Map();
426
+ const localPackageVersions = new Map();
321
427
  if (!lockFile.packages || Object.keys(lockFile.packages).length === 0) {
322
428
  return nodes;
323
429
  }
324
- // Pre-compute workspace collections for performance
325
- const workspacePaths = getWorkspacePaths(lockFile);
326
- const workspacePackageNames = getWorkspacePackageNames(lockFile);
327
430
  const packageEntries = Object.entries(lockFile.packages);
328
431
  for (const [packageKey, packageData] of packageEntries) {
329
- const result = processPackageEntry(packageKey, packageData, lockFile, keyMap, nodes, packageVersions);
432
+ const result = processPackageEntry(packageKey, packageData, lockFile, index, keyMap, nodes, localPackageVersions);
330
433
  if (result.shouldContinue) {
331
434
  continue;
332
435
  }
333
436
  }
334
- createHoistedNodes(packageVersions, lockFile, keyMap, nodes, workspacePaths, workspacePackageNames);
437
+ createHoistedNodes(localPackageVersions, lockFile, index, keyMap, nodes);
335
438
  return nodes;
336
439
  }
337
440
  catch (error) {
@@ -341,7 +444,7 @@ function getBunTextLockfileNodes(lockFileContent, lockFileHash) {
341
444
  throw new Error(`Failed to get Bun lockfile nodes: ${error.message}`);
342
445
  }
343
446
  }
344
- function processPackageEntry(packageKey, packageData, lockFile, keyMap, nodes, packageVersions) {
447
+ function processPackageEntry(packageKey, packageData, lockFile, index, keyMap, nodes, packageVersions) {
345
448
  try {
346
449
  if (!Array.isArray(packageData) ||
347
450
  packageData.length < 1 ||
@@ -359,16 +462,17 @@ function processPackageEntry(packageKey, packageData, lockFile, keyMap, nodes, p
359
462
  console.warn(`Lockfile contains unrecognized package format. Try regenerating the lockfile with 'bun install --force'.\nDebug: could not parse resolved spec '${resolvedSpec}'`);
360
463
  return { shouldContinue: true };
361
464
  }
362
- if (isWorkspacePackage(name, lockFile)) {
465
+ // O(1) lookups using index
466
+ if (index.workspaceNames.has(name)) {
363
467
  return { shouldContinue: true };
364
468
  }
365
- if (lockFile.patches && lockFile.patches[name]) {
469
+ if (index.patchedPackages.has(name)) {
366
470
  return { shouldContinue: true };
367
471
  }
368
472
  if (protocol === 'workspace') {
369
473
  return { shouldContinue: true };
370
474
  }
371
- const isWorkspaceSpecific = isNestedPackageKey(packageKey, lockFile);
475
+ const isWorkspaceSpecific = isNestedPackageKey(packageKey, index);
372
476
  if (!isWorkspaceSpecific && isAliasPackage(packageKey, name)) {
373
477
  const aliasName = packageKey;
374
478
  const actualPackageName = name;
@@ -442,11 +546,10 @@ function processPackageEntry(packageKey, packageData, lockFile, keyMap, nodes, p
442
546
  return { shouldContinue: true };
443
547
  }
444
548
  }
445
- function isWorkspaceOrPatchedPackage(packageName, lockFile, workspacePackages, workspacePackageNames) {
549
+ function isWorkspaceOrPatchedPackage(packageName, index, workspacePackages) {
446
550
  return (workspacePackages.has(packageName) ||
447
- workspacePackageNames.has(packageName) ||
448
- isWorkspacePackage(packageName, lockFile) ||
449
- (lockFile.patches && !!lockFile.patches[packageName]));
551
+ index.workspaceNames.has(packageName) ||
552
+ index.patchedPackages.has(packageName));
450
553
  }
451
554
  function resolveAliasTarget(versionSpec) {
452
555
  if (!versionSpec.startsWith('npm:'))
@@ -458,15 +561,7 @@ function resolveAliasTarget(versionSpec) {
458
561
  version: actualSpec.substring(actualAtIndex + 1),
459
562
  };
460
563
  }
461
- function getAllWorkspaceDependencies(workspace) {
462
- return {
463
- ...workspace.dependencies,
464
- ...workspace.devDependencies,
465
- ...workspace.optionalDependencies,
466
- ...workspace.peerDependencies,
467
- };
468
- }
469
- function processPackageToPackageDependencies(lockFile, ctx, workspacePackages, workspacePackageNames, workspacePaths) {
564
+ function processPackageToPackageDependencies(lockFile, index, ctx, workspacePackages) {
470
565
  const dependencies = [];
471
566
  if (!lockFile.packages || Object.keys(lockFile.packages).length === 0) {
472
567
  return dependencies;
@@ -474,7 +569,7 @@ function processPackageToPackageDependencies(lockFile, ctx, workspacePackages, w
474
569
  const packageEntries = Object.entries(lockFile.packages);
475
570
  for (const [packageKey, packageData] of packageEntries) {
476
571
  try {
477
- const packageDeps = processPackageForDependencies(packageKey, packageData, lockFile, ctx, workspacePackages, workspacePackageNames, workspacePaths);
572
+ const packageDeps = processPackageForDependencies(packageKey, packageData, lockFile, index, ctx, workspacePackages);
478
573
  dependencies.push(...packageDeps);
479
574
  }
480
575
  catch (error) {
@@ -483,9 +578,10 @@ function processPackageToPackageDependencies(lockFile, ctx, workspacePackages, w
483
578
  }
484
579
  return dependencies;
485
580
  }
486
- function processPackageForDependencies(packageKey, packageData, lockFile, ctx, workspacePackages, workspacePackageNames, workspacePaths) {
487
- if (isWorkspacePackage(packageKey, lockFile) ||
488
- isNestedPackageKey(packageKey, lockFile, workspacePaths, workspacePackageNames)) {
581
+ function processPackageForDependencies(packageKey, packageData, lockFile, index, ctx, workspacePackages) {
582
+ // O(1) checks using index
583
+ if (index.workspaceNames.has(packageKey) ||
584
+ isNestedPackageKey(packageKey, index)) {
489
585
  return [];
490
586
  }
491
587
  if (!Array.isArray(packageData) || packageData.length < 1) {
@@ -499,7 +595,7 @@ function processPackageForDependencies(packageKey, packageData, lockFile, ctx, w
499
595
  if (!sourcePackageName || !sourceVersion) {
500
596
  return [];
501
597
  }
502
- if (lockFile.patches && lockFile.patches[sourcePackageName]) {
598
+ if (index.patchedPackages.has(sourcePackageName)) {
503
599
  return [];
504
600
  }
505
601
  const sourceNodeName = `npm:${sourcePackageName}@${sourceVersion}`;
@@ -515,7 +611,7 @@ function processPackageForDependencies(packageKey, packageData, lockFile, ctx, w
515
611
  const deps = packageDependencies[depType];
516
612
  if (!deps || typeof deps !== 'object')
517
613
  continue;
518
- const depDependencies = processDependencyEntries(deps, sourceNodeName, lockFile, ctx, workspacePackages, workspacePackageNames);
614
+ const depDependencies = processDependencyEntries(deps, sourceNodeName, index, ctx, workspacePackages, lockFile.manifests);
519
615
  dependencies.push(...depDependencies);
520
616
  }
521
617
  return dependencies;
@@ -533,12 +629,12 @@ function extractPackageDependencies(packageData) {
533
629
  }
534
630
  return undefined;
535
631
  }
536
- function processDependencyEntries(deps, sourceNodeName, lockFile, ctx, workspacePackages, workspacePackageNames) {
632
+ function processDependencyEntries(deps, sourceNodeName, index, ctx, workspacePackages, manifests) {
537
633
  const dependencies = [];
538
634
  const depsEntries = Object.entries(deps);
539
635
  for (const [packageName, versionSpec] of depsEntries) {
540
636
  try {
541
- const dependency = processSingleDependency(packageName, versionSpec, sourceNodeName, lockFile, ctx, workspacePackages, workspacePackageNames);
637
+ const dependency = processSingleDependency(packageName, versionSpec, sourceNodeName, index, ctx, workspacePackages, manifests);
542
638
  if (dependency) {
543
639
  dependencies.push(dependency);
544
640
  }
@@ -549,11 +645,12 @@ function processDependencyEntries(deps, sourceNodeName, lockFile, ctx, workspace
549
645
  }
550
646
  return dependencies;
551
647
  }
552
- function processSingleDependency(packageName, versionSpec, sourceNodeName, lockFile, ctx, workspacePackages, workspacePackageNames) {
648
+ function processSingleDependency(packageName, versionSpec, sourceNodeName, index, ctx, workspacePackages, manifests) {
553
649
  if (typeof packageName !== 'string' || typeof versionSpec !== 'string') {
554
650
  return null;
555
651
  }
556
- if (isWorkspaceOrPatchedPackage(packageName, lockFile, workspacePackages, workspacePackageNames)) {
652
+ // O(1) check using index
653
+ if (isWorkspaceOrPatchedPackage(packageName, index, workspacePackages)) {
557
654
  return null;
558
655
  }
559
656
  if (versionSpec.startsWith('workspace:')) {
@@ -567,7 +664,8 @@ function processSingleDependency(packageName, versionSpec, sourceNodeName, lockF
567
664
  targetVersion = aliasTarget.version;
568
665
  }
569
666
  else {
570
- const resolvedVersion = findResolvedVersion(packageName, versionSpec, lockFile.packages, lockFile.manifests);
667
+ // O(1) lookup using index instead of O(n) scan
668
+ const resolvedVersion = findResolvedVersion(packageName, versionSpec, index, manifests);
571
669
  if (!resolvedVersion) {
572
670
  return null;
573
671
  }
@@ -601,70 +699,12 @@ function resolveTargetNodeName(targetPackageName, targetVersion, ctx) {
601
699
  }
602
700
  return null;
603
701
  }
604
- // ===== WORKSPACE-RELATED FUNCTIONS =====
605
- function getWorkspacePackageNames(lockFile) {
606
- const workspacePackageNames = new Set();
607
- if (lockFile.workspacePackages) {
608
- for (const packageInfo of Object.values(lockFile.workspacePackages)) {
609
- workspacePackageNames.add(packageInfo.name);
610
- }
611
- }
612
- return workspacePackageNames;
613
- }
614
- function getWorkspacePaths(lockFile) {
615
- const workspacePaths = new Set();
616
- if (lockFile.workspaces) {
617
- for (const workspacePath of Object.keys(lockFile.workspaces)) {
618
- if (workspacePath !== '') {
619
- workspacePaths.add(workspacePath);
620
- }
621
- }
622
- }
623
- return workspacePaths;
624
- }
625
- function isWorkspacePackage(packageName, lockFile) {
626
- // Check if package is in workspacePackages field
627
- if (lockFile.workspacePackages && lockFile.workspacePackages[packageName]) {
628
- return true;
629
- }
630
- // Check if package is defined in any workspace dependencies with workspace: prefix
631
- // or if it's a file dependency in workspace dependencies
632
- if (lockFile.workspaces) {
633
- for (const workspace of Object.values(lockFile.workspaces)) {
634
- const allDeps = getAllWorkspaceDependencies(workspace);
635
- if (allDeps[packageName]?.startsWith('workspace:')) {
636
- return true;
637
- }
638
- // Check if this is a file dependency defined in workspace dependencies
639
- // Always filter out file dependencies as they represent workspace packages
640
- if (allDeps[packageName]?.startsWith('file:')) {
641
- return true;
642
- }
643
- }
644
- }
645
- // Check if package appears in packages section with workspace: or file: protocol
646
- if (lockFile.packages) {
647
- for (const packageData of Object.values(lockFile.packages)) {
648
- if (Array.isArray(packageData) && packageData.length > 0) {
649
- const resolvedSpec = packageData[0];
650
- if (typeof resolvedSpec === 'string') {
651
- const { name, protocol } = getCachedSpecInfo(resolvedSpec);
652
- if (name === packageName &&
653
- (protocol === 'workspace' || protocol === 'file')) {
654
- return true;
655
- }
656
- }
657
- }
658
- }
659
- }
660
- return false;
661
- }
662
702
  // ===== HOISTING-RELATED FUNCTIONS =====
663
- function createHoistedNodes(packageVersions, lockFile, keyMap, nodes, workspacePaths, workspacePackageNames) {
703
+ function createHoistedNodes(packageVersions, lockFile, index, keyMap, nodes) {
664
704
  for (const [packageName, versions] of packageVersions.entries()) {
665
705
  const hoistedNodeKey = `npm:${packageName}`;
666
- if (shouldCreateHoistedNode(packageName, lockFile, workspacePaths, workspacePackageNames)) {
667
- const hoistedVersion = getHoistedVersion(packageName, versions, lockFile);
706
+ if (shouldCreateHoistedNode(packageName, lockFile, index)) {
707
+ const hoistedVersion = getHoistedVersion(packageName, versions, index);
668
708
  if (hoistedVersion) {
669
709
  const versionedNodeKey = `npm:${packageName}@${hoistedVersion}`;
670
710
  const versionedNode = keyMap.get(versionedNodeKey);
@@ -689,86 +729,49 @@ function createHoistedNodes(packageVersions, lockFile, keyMap, nodes, workspaceP
689
729
  * Checks if a package key represents a workspace-specific or nested dependency entry
690
730
  * These entries should not become external nodes, they are used only for resolution
691
731
  *
692
- * Examples of workspace-specific/nested entries:
693
- * - "@quz/pkg1/lodash" (workspace-specific)
694
- * - "is-even/is-odd" (dependency nesting)
695
- * - "@quz/pkg2/is-even/is-odd" (workspace-specific nested)
732
+ * O(1) lookup using pre-computed index
696
733
  */
697
- function isNestedPackageKey(packageKey, lockFile, workspacePaths, workspacePackageNames) {
734
+ function isNestedPackageKey(packageKey, index) {
698
735
  // If the key doesn't contain '/', it's a direct package entry
699
736
  if (!packageKey.includes('/')) {
700
737
  return false;
701
738
  }
702
- // Get workspace paths and package names for comparison
703
- const computedWorkspacePaths = workspacePaths || getWorkspacePaths(lockFile);
704
- const computedWorkspacePackageNames = workspacePackageNames || getWorkspacePackageNames(lockFile);
705
739
  // Check if this looks like a workspace-specific or nested entry
706
740
  const parts = packageKey.split('/');
707
741
  // For multi-part keys, check if the prefix is a workspace path or package name
708
742
  if (parts.length >= 2) {
709
743
  const prefix = parts.slice(0, -1).join('/');
710
- // Check against known workspace paths
711
- if (computedWorkspacePaths.has(prefix)) {
744
+ // O(1) check against known workspace paths
745
+ if (index.workspacePaths.has(prefix)) {
712
746
  return true;
713
747
  }
714
- // Check against workspace package names (scoped packages)
715
- if (computedWorkspacePackageNames.has(prefix)) {
748
+ // O(1) check against workspace package names (scoped packages)
749
+ if (index.workspaceNames.has(prefix)) {
716
750
  return true;
717
751
  }
718
- // Check for scoped workspace packages (e.g., "@quz/pkg1")
752
+ // Check for scoped workspace packages (e.g., "@quz/pkg1/lodash")
753
+ // The prefix must contain '/' to be a scoped package (e.g., "@scope/pkg")
754
+ // A prefix like just "@scope" without '/' is not a scoped package
719
755
  if (prefix.startsWith('@') && prefix.includes('/')) {
720
756
  return true;
721
757
  }
758
+ // If the key looks like a simple scoped package (e.g., "@custom/lodash")
759
+ // where parts.length === 2 and first part starts with '@', it's likely
760
+ // a scoped package alias, not a nested dependency
761
+ if (parts.length === 2 && parts[0].startsWith('@')) {
762
+ return false;
763
+ }
722
764
  // This could be dependency nesting (e.g., "is-even/is-odd")
723
- // These should also be filtered out as they're not direct packages
765
+ // These should be filtered out as they're not direct packages
724
766
  return true;
725
767
  }
726
768
  return false;
727
769
  }
728
- /**
729
- * Checks if a package has workspace-specific variants in the lockfile
730
- * Workspace-specific variants indicate the package should NOT be hoisted
731
- * Example: "@quz/pkg1/lodash" indicates lodash should not be hoisted for the @quz/pkg1 workspace
732
- *
733
- * This should NOT match dependency nesting like "is-even/is-odd" which represents
734
- * is-odd as a dependency of is-even, not a workspace-specific variant.
735
- */
736
- function hasWorkspaceSpecificVariant(packageName, lockFile, workspacePaths, workspacePackageNames) {
737
- if (!lockFile.packages)
738
- return false;
739
- // Get list of known workspace paths to distinguish workspace-specific variants
740
- // from dependency nesting
741
- const computedWorkspacePaths = workspacePaths || getWorkspacePaths(lockFile);
742
- const computedWorkspacePackageNames = workspacePackageNames || getWorkspacePackageNames(lockFile);
743
- // Check if any package key follows pattern: "workspace/packageName"
744
- for (const packageKey of Object.keys(lockFile.packages)) {
745
- if (packageKey.includes('/') && packageKey.endsWith(`/${packageName}`)) {
746
- const prefix = packageKey.substring(0, packageKey.lastIndexOf(`/${packageName}`));
747
- // Check if prefix is a known workspace path or workspace package name
748
- if (computedWorkspacePaths.has(prefix) ||
749
- computedWorkspacePackageNames.has(prefix)) {
750
- return true;
751
- }
752
- // Also check for scoped workspace packages (e.g., "@quz/pkg1/lodash")
753
- if (prefix.startsWith('@') && prefix.includes('/')) {
754
- return true;
755
- }
756
- }
757
- }
758
- return false;
759
- }
760
770
  /**
761
771
  * Determines if a package should have a hoisted node created
762
- * A package should be hoisted if:
763
- * 1. It has a direct entry in the packages section (key matches package name exactly), OR
764
- * 2. It appears as a direct dependency in any workspace AND no workspace-specific variants exist
765
- *
766
- * This handles both cases:
767
- * - Packages with direct entries (like transitive deps) should be hoisted
768
- * - Packages in workspace deps without conflicts should be hoisted
769
- * - Packages with both direct entries and workspace-specific variants get both
772
+ * O(1) lookup using pre-computed index
770
773
  */
771
- function shouldCreateHoistedNode(packageName, lockFile, workspacePaths, workspacePackageNames) {
774
+ function shouldCreateHoistedNode(packageName, lockFile, index) {
772
775
  if (!lockFile.workspaces || !lockFile.packages)
773
776
  return false;
774
777
  // First check if the package has a direct entry in the packages section
@@ -780,154 +783,126 @@ function shouldCreateHoistedNode(packageName, lockFile, workspacePaths, workspac
780
783
  // and don't have workspace-specific variants (which would cause conflicts)
781
784
  let appearsInWorkspace = false;
782
785
  for (const workspace of Object.values(lockFile.workspaces)) {
783
- const allDeps = getAllWorkspaceDependencies(workspace);
786
+ const allDeps = {
787
+ ...workspace.dependencies,
788
+ ...workspace.devDependencies,
789
+ ...workspace.optionalDependencies,
790
+ ...workspace.peerDependencies,
791
+ };
784
792
  if (allDeps[packageName]) {
785
793
  appearsInWorkspace = true;
786
794
  break;
787
795
  }
788
796
  }
797
+ // O(1) check using pre-computed index
789
798
  if (appearsInWorkspace &&
790
- !hasWorkspaceSpecificVariant(packageName, lockFile, workspacePaths, workspacePackageNames)) {
791
- return true; // Found in workspace deps and no conflicts
799
+ !index.packagesWithWorkspaceVariants.has(packageName)) {
800
+ return true;
792
801
  }
793
802
  return false;
794
803
  }
795
804
  /**
796
805
  * Gets the version that should be used for a hoisted package
797
- * For truly hoisted packages, we look up the version from the main package entry
806
+ * O(1) lookup using pre-computed index
798
807
  */
799
- function getHoistedVersion(packageName, availableVersions, lockFile) {
800
- if (!lockFile.packages)
801
- return null;
802
- // Look for the main package entry (not workspace-specific)
803
- const mainPackageData = lockFile.packages[packageName];
804
- if (mainPackageData &&
805
- Array.isArray(mainPackageData) &&
806
- mainPackageData.length > 0) {
807
- const resolvedSpec = mainPackageData[0];
808
- if (typeof resolvedSpec === 'string') {
809
- const { version } = getCachedSpecInfo(resolvedSpec);
810
- if (version && availableVersions.has(version)) {
811
- return version;
812
- }
813
- }
808
+ function getHoistedVersion(packageName, availableVersions, index) {
809
+ // Use the index to find the main package version
810
+ const candidates = index.byName.get(packageName);
811
+ if (!candidates || candidates.length === 0) {
812
+ return availableVersions.size > 0
813
+ ? availableVersions.values().next().value
814
+ : null;
815
+ }
816
+ // Look for the main package entry (the one where packageKey === packageName)
817
+ const mainEntry = candidates.find((c) => c.packageKey === packageName);
818
+ if (mainEntry && availableVersions.has(mainEntry.version)) {
819
+ return mainEntry.version;
814
820
  }
815
821
  // Fallback: return the first available version
816
- return availableVersions.size > 0 ? Array.from(availableVersions)[0] : null;
822
+ return availableVersions.size > 0
823
+ ? availableVersions.values().next().value
824
+ : null;
817
825
  }
818
826
  /**
819
827
  * Finds the resolved version for a package given its version specification
820
828
  *
821
- * 1. Fast path: Check manifests for exact version match
822
- * 2. Scan all packages to find candidates with matching names
823
- * 3. Include alias packages where the target matches our package name
824
- * 4. Fallback: Search manifests for any matching package entries
825
- * 5. Use findBestVersionMatch to select the optimal version from candidates
829
+ * O(1) lookup using pre-computed index instead of O(n) scan through all packages
826
830
  */
827
- function findResolvedVersion(packageName, versionSpec, packages, manifests) {
828
- // Look for matching packages and collect all versions
829
- const candidateVersions = [];
830
- const packageEntries = Object.entries(packages);
831
- // Early manifest lookup for exact matches
832
- // Avoids expensive package scanning when exact version is available
833
- if (manifests) {
834
- const exactManifestKey = `${packageName}@${versionSpec}`;
835
- if (manifests[exactManifestKey]) {
836
- return versionSpec;
837
- }
838
- }
839
- for (const [packageKey, packageData] of packageEntries) {
840
- const [resolvedSpec] = packageData;
841
- // Skip non-string specs early
842
- if (typeof resolvedSpec !== 'string') {
843
- continue;
844
- }
845
- // Use cached spec parsing to avoid repeated string operations
846
- const { name, version } = getCachedSpecInfo(resolvedSpec);
847
- if (name === packageName) {
848
- // Include manifest information if available
849
- const manifest = manifests?.[`${name}@${version}`];
850
- candidateVersions.push({ version, packageKey, manifest });
851
- // Early termination if we find an exact version match
852
- if (version === versionSpec) {
853
- return version;
854
- }
855
- }
856
- // Check for alias packages where this package might be the target
857
- if (isAliasPackage(packageKey, name) && name === packageName) {
858
- // This alias points to the package we're looking for
859
- const manifest = manifests?.[`${name}@${version}`];
860
- candidateVersions.push({ version, packageKey, manifest });
861
- // Early termination if we find an exact version match
862
- if (version === versionSpec) {
863
- return version;
864
- }
865
- }
866
- }
867
- if (candidateVersions.length === 0) {
831
+ function findResolvedVersion(packageName, versionSpec, index, manifests) {
832
+ // O(1) lookup
833
+ const candidates = index.byName.get(packageName);
834
+ if (!candidates || candidates.length === 0) {
868
835
  // Try to find in manifests as fallback
869
836
  if (manifests) {
837
+ const exactManifestKey = `${packageName}@${versionSpec}`;
838
+ if (manifests[exactManifestKey]) {
839
+ return versionSpec;
840
+ }
870
841
  const manifestKey = Object.keys(manifests).find((key) => key.startsWith(`${packageName}@`));
871
842
  if (manifestKey) {
872
843
  const manifest = manifests[manifestKey];
873
- const version = manifest.version;
874
- if (version) {
875
- candidateVersions.push({
876
- version,
877
- packageKey: manifestKey,
878
- manifest,
879
- });
844
+ if (manifest.version) {
845
+ return manifest.version;
880
846
  }
881
847
  }
882
848
  }
883
- if (candidateVersions.length === 0) {
884
- return null;
885
- }
849
+ return null;
850
+ }
851
+ // Check for exact version match first (most common case)
852
+ const exactMatch = candidates.find((c) => c.version === versionSpec);
853
+ if (exactMatch) {
854
+ return exactMatch.version;
886
855
  }
887
- // Handle different version specification patterns with enhanced logic
888
- const bestMatch = findBestVersionMatch(packageName, versionSpec, candidateVersions);
889
- return bestMatch ? bestMatch.version : null;
856
+ // Handle different version specification patterns
857
+ return findBestVersionMatch(versionSpec, candidates);
890
858
  }
891
859
  /**
892
860
  * Find the best version match for a given version specification
893
- *
894
- * 1. Check for exact version matches first (highest priority)
895
- * 2. Handle union ranges (||) by recursively checking each range
896
- * 3. For non-semver versions (git, file, etc.), prefer exact matches or return first candidate
897
- * 4. For semver versions, use semver.satisfies() to find compatible versions
861
+ * Only operates on the pre-filtered candidates array (usually 1-3 items)
898
862
  */
899
- function findBestVersionMatch(packageName, versionSpec, candidates) {
900
- // For exact matches, return immediately
863
+ function findBestVersionMatch(versionSpec, candidates) {
864
+ if (candidates.length === 0) {
865
+ return null;
866
+ }
867
+ // For exact matches, return immediately (already checked above but defensive)
901
868
  const exactMatch = candidates.find((c) => c.version === versionSpec);
902
869
  if (exactMatch) {
903
- return exactMatch;
870
+ return exactMatch.version;
904
871
  }
905
872
  // Handle union ranges (||)
906
873
  if (versionSpec.includes('||')) {
907
874
  const ranges = versionSpec.split('||').map((r) => r.trim());
908
875
  for (const range of ranges) {
909
- const match = findBestVersionMatch(packageName, range, candidates);
876
+ const match = findBestVersionMatch(range, candidates);
910
877
  if (match) {
911
878
  return match;
912
879
  }
913
880
  }
914
881
  return null;
915
882
  }
883
+ // Separate semver and non-semver versions
884
+ const semverVersions = [];
885
+ const nonSemverVersions = [];
886
+ for (const c of candidates) {
887
+ if (/^\d+\.\d+\.\d+/.test(c.version)) {
888
+ semverVersions.push(c);
889
+ }
890
+ else {
891
+ nonSemverVersions.push(c);
892
+ }
893
+ }
916
894
  // Handle non-semver versions (git, file, etc.)
917
- const nonSemverVersions = candidates.filter((c) => !c.version.match(/^\d+\.\d+\.\d+/));
918
895
  if (nonSemverVersions.length > 0) {
919
- // For non-semver versions, use the first match or exact match
920
896
  const nonSemverMatch = nonSemverVersions.find((c) => c.version === versionSpec);
921
897
  if (nonSemverMatch) {
922
- return nonSemverMatch;
898
+ return nonSemverMatch.version;
923
899
  }
924
- // If no exact match, return the first non-semver candidate
925
- return nonSemverVersions[0];
900
+ // If no exact match for non-semver, continue to semver matching
926
901
  }
927
902
  // Handle semver versions
928
- const semverVersions = candidates.filter((c) => c.version.match(/^\d+\.\d+\.\d+/));
929
903
  if (semverVersions.length === 0) {
930
- return candidates[0]; // Fallback to any available version
904
+ // No semver versions, return first non-semver if available
905
+ return nonSemverVersions.length > 0 ? nonSemverVersions[0].version : null;
931
906
  }
932
907
  // Find all versions that satisfy the spec
933
908
  const satisfyingVersions = semverVersions.filter((candidate) => {
@@ -940,17 +915,14 @@ function findBestVersionMatch(packageName, versionSpec, candidates) {
940
915
  }
941
916
  });
942
917
  if (satisfyingVersions.length === 0) {
943
- // No satisfying versions found, return the first candidate as fallback
944
- return semverVersions[0];
918
+ // No satisfying versions found, return the first semver candidate as fallback
919
+ return semverVersions[0].version;
945
920
  }
946
921
  // Return the highest satisfying version (similar to npm behavior)
947
922
  // Sort versions in descending order and return the first one
948
923
  const sortedVersions = satisfyingVersions.sort((a, b) => {
949
924
  try {
950
- // Use semver comparison if possible
951
- const aVersion = a.version.match(/^\d+\.\d+\.\d+/) ? a.version : '0.0.0';
952
- const bVersion = b.version.match(/^\d+\.\d+\.\d+/) ? b.version : '0.0.0';
953
- return aVersion.localeCompare(bVersion, undefined, {
925
+ return b.version.localeCompare(a.version, undefined, {
954
926
  numeric: true,
955
927
  sensitivity: 'base',
956
928
  });
@@ -960,5 +932,5 @@ function findBestVersionMatch(packageName, versionSpec, candidates) {
960
932
  return b.version.localeCompare(a.version);
961
933
  }
962
934
  });
963
- return sortedVersions[0];
935
+ return sortedVersions[0].version;
964
936
  }