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,30 +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 } ` , {
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+ } ) ;
372+
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 , {
368378 registry,
379+ verbose,
380+ rejectionReasons,
369381 } ) ;
370- if ( ! manifest ) {
371- continue ;
372- }
373-
374- const conflicts = await this . getPeerDependencyConflicts ( manifest ) ;
375- if ( conflicts ) {
376- if ( verbose || rejectionReasons . length < DEFAULT_CONFLICT_DISPLAY_LIMIT ) {
377- rejectionReasons . push ( ...conflicts ) ;
378- }
379- continue ;
380- }
381-
382- context . packageIdentifier = npa . resolve ( manifest . name , manifest . version ) ;
383- found = manifest ;
384- break ;
385382 }
386383
387384 if ( ! found ) {
@@ -403,10 +400,54 @@ export default class AddCommandModule
403400 }
404401 }
405402
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 } ` , {
419+ registry,
420+ } ) ;
421+ if ( ! manifest ) {
422+ continue ;
423+ }
424+
425+ const conflicts = await this . getPeerDependencyConflicts ( manifest ) ;
426+ if ( conflicts ) {
427+ if ( verbose || rejectionReasons . length < DEFAULT_CONFLICT_DISPLAY_LIMIT ) {
428+ rejectionReasons . push ( ...conflicts ) ;
429+ }
430+ continue ;
431+ }
432+
433+ context . packageIdentifier = npa . resolve ( manifest . name , manifest . version ) ;
434+
435+ return manifest ;
436+ }
437+
438+ return null ;
439+ }
440+
406441 #getPotentialVersions( packageMetadata : PackageMetadata , allowPrereleases : boolean ) : string [ ] {
407442 const versionExclusions = packageVersionExclusions [ packageMetadata . name ] ;
443+ const latestVersion = packageMetadata [ 'dist-tags' ] [ 'latest' ] ;
408444
409445 const versions = Object . values ( packageMetadata . versions ) . filter ( ( version ) => {
446+ // Latest tag has already been checked
447+ if ( latestVersion && version === latestVersion ) {
448+ return false ;
449+ }
450+
410451 // Prerelease versions are not stable and should not be considered by default
411452 if ( ! allowPrereleases && prerelease ( version ) ) {
412453 return false ;
@@ -424,6 +465,19 @@ export default class AddCommandModule
424465 return versions . sort ( ( a , b ) => compare ( b , a , true ) ) ;
425466 }
426467
468+ #getMajorVersions( versions : string [ ] ) : string [ ] {
469+ const majorVersions = new Map < number , string > ( ) ;
470+ for ( const version of versions ) {
471+ const major = semver . major ( version ) ;
472+ const existing = majorVersions . get ( major ) ;
473+ if ( ! existing || semver . gt ( version , existing ) ) {
474+ majorVersions . set ( major , version ) ;
475+ }
476+ }
477+
478+ return [ ...majorVersions . values ( ) ] . sort ( ( a , b ) => compare ( b , a , true ) ) ;
479+ }
480+
427481 private async loadPackageInfoTask (
428482 context : AddCommandTaskContext ,
429483 task : AddCommandTaskWrapper ,
0 commit comments