88
99import { Listr , ListrRenderer , ListrTaskWrapper , color , figures } from 'listr2' ;
1010import assert from 'node:assert' ;
11- import { promises as fs } from 'node:fs' ;
11+ import fs from 'node:fs/promises ' ;
1212import { createRequire } from 'node:module' ;
1313import { dirname , join } from 'node:path' ;
1414import npa from 'npm-package-arg' ;
15- import { Range , compare , intersects , prerelease , satisfies , valid } from 'semver' ;
15+ import semver , { Range , compare , intersects , prerelease , satisfies , valid } from 'semver' ;
1616import { Argv } from 'yargs' ;
1717import {
1818 CommandModuleImplementation ,
@@ -358,28 +358,27 @@ export default class AddCommandModule
358358 throw new CommandError ( 'Unable to load package information from registry.' ) ;
359359 }
360360
361- // Allow prelease versions if the CLI itself is a prerelease.
362- const allowPrereleases = ! ! prerelease ( VERSION . full ) ;
361+ // Allow prelease versions if the CLI itself is a prerelease or locally built .
362+ const allowPrereleases = ! ! prerelease ( VERSION . full ) || VERSION . full === '0.0.0' ;
363363 const potentialVersions = this . #getPotentialVersions( packageMetadata , allowPrereleases ) ;
364364
365- let found ;
366- for ( const version of potentialVersions ) {
367- const manifest = await packageManager . getPackageManifest ( packageName , version , { registry } ) ;
368- if ( ! manifest ) {
369- continue ;
370- }
371-
372- const conflicts = await this . getPeerDependencyConflicts ( manifest ) ;
373- if ( conflicts ) {
374- if ( verbose || rejectionReasons . length < DEFAULT_CONFLICT_DISPLAY_LIMIT ) {
375- rejectionReasons . push ( ...conflicts ) ;
376- }
377- continue ;
378- }
365+ // Heuristic-based search: Check the latest release of each major version first.
366+ const majorVersions = this . #getMajorVersions( potentialVersions ) ;
367+ let found = await this . #findCompatibleVersion( context , majorVersions , {
368+ registry,
369+ verbose,
370+ rejectionReasons,
371+ } ) ;
379372
380- context . packageIdentifier = npa . resolve ( manifest . name , manifest . version ) ;
381- found = manifest ;
382- break ;
373+ // Exhaustive search: If no compatible major version is found, fall back to checking all versions.
374+ if ( ! found ) {
375+ const checkedVersions = new Set ( majorVersions ) ;
376+ const remainingVersions = potentialVersions . filter ( ( v ) => ! checkedVersions . has ( v ) ) ;
377+ found = await this . #findCompatibleVersion( context , remainingVersions , {
378+ registry,
379+ verbose,
380+ rejectionReasons,
381+ } ) ;
383382 }
384383
385384 if ( ! found ) {
@@ -401,10 +400,52 @@ export default class AddCommandModule
401400 }
402401 }
403402
403+ async #findCompatibleVersion(
404+ context : AddCommandTaskContext ,
405+ versions : string [ ] ,
406+ options : {
407+ registry ?: string ;
408+ verbose ?: boolean ;
409+ rejectionReasons : string [ ] ;
410+ } ,
411+ ) : Promise < PackageManifest | null > {
412+ const { packageManager, packageIdentifier } = context ;
413+ const { registry, verbose, rejectionReasons } = options ;
414+ const packageName = packageIdentifier . name ;
415+ assert ( packageName , 'Package name must be defined.' ) ;
416+
417+ for ( const version of versions ) {
418+ const manifest = await packageManager . getPackageManifest ( packageName , version , { registry } ) ;
419+ if ( ! manifest ) {
420+ continue ;
421+ }
422+
423+ const conflicts = await this . getPeerDependencyConflicts ( manifest ) ;
424+ if ( conflicts ) {
425+ if ( verbose || rejectionReasons . length < DEFAULT_CONFLICT_DISPLAY_LIMIT ) {
426+ rejectionReasons . push ( ...conflicts ) ;
427+ }
428+ continue ;
429+ }
430+
431+ context . packageIdentifier = npa . resolve ( manifest . name , manifest . version ) ;
432+
433+ return manifest ;
434+ }
435+
436+ return null ;
437+ }
438+
404439 #getPotentialVersions( packageMetadata : PackageMetadata , allowPrereleases : boolean ) : string [ ] {
405440 const versionExclusions = packageVersionExclusions [ packageMetadata . name ] ;
441+ const latestVersion = packageMetadata [ 'dist-tags' ] [ 'latest' ] ;
406442
407443 const versions = Object . values ( packageMetadata . versions ) . filter ( ( version ) => {
444+ // Latest tag has already been checked
445+ if ( latestVersion && version === latestVersion ) {
446+ return false ;
447+ }
448+
408449 // Prerelease versions are not stable and should not be considered by default
409450 if ( ! allowPrereleases && prerelease ( version ) ) {
410451 return false ;
@@ -422,6 +463,19 @@ export default class AddCommandModule
422463 return versions . sort ( ( a , b ) => compare ( b , a , true ) ) ;
423464 }
424465
466+ #getMajorVersions( versions : string [ ] ) : string [ ] {
467+ const majorVersions = new Map < number , string > ( ) ;
468+ for ( const version of versions ) {
469+ const major = semver . major ( version ) ;
470+ const existing = majorVersions . get ( major ) ;
471+ if ( ! existing || semver . gt ( version , existing ) ) {
472+ majorVersions . set ( major , version ) ;
473+ }
474+ }
475+
476+ return [ ...majorVersions . values ( ) ] . sort ( ( a , b ) => compare ( b , a , true ) ) ;
477+ }
478+
425479 private async loadPackageInfoTask (
426480 context : AddCommandTaskContext ,
427481 task : AddCommandTaskWrapper ,
0 commit comments