88
99import { Listr , ListrRenderer , ListrTaskWrapper , color , figures } from 'listr2' ;
1010import assert from 'node:assert' ;
11+ import { existsSync } from 'node:fs' ;
1112import fs from 'node:fs/promises' ;
1213import { createRequire } from 'node:module' ;
13- import { dirname , join } from 'node:path' ;
14+ import { basename , dirname , join } from 'node:path' ;
1415import npa from 'npm-package-arg' ;
1516import semver , { Range , compare , intersects , prerelease , satisfies , valid } from 'semver' ;
1617import { Argv } from 'yargs' ;
@@ -107,6 +108,7 @@ export default class AddCommandModule
107108 private readonly schematicName = 'ng-add' ;
108109 private rootRequire = createRequire ( this . context . root + '/' ) ;
109110 #projectVersionCache = new Map < string , string | null > ( ) ;
111+ #rootManifestCache: PackageManifest | null = null ;
110112
111113 override async builder ( argv : Argv ) : Promise < Argv < AddCommandArgs > > {
112114 const localYargs = ( await super . builder ( argv ) )
@@ -156,6 +158,7 @@ export default class AddCommandModule
156158
157159 async run ( options : Options < AddCommandArgs > & OtherOptions ) : Promise < number | void > {
158160 this . #projectVersionCache. clear ( ) ;
161+ this . #rootManifestCache = null ;
159162 const { logger } = this . context ;
160163 const { collection, skipConfirmation } = options ;
161164
@@ -665,18 +668,7 @@ export default class AddCommandModule
665668 }
666669
667670 private isPackageInstalled ( name : string ) : boolean {
668- try {
669- this . rootRequire . resolve ( join ( name , 'package.json' ) ) ;
670-
671- return true ;
672- } catch ( e ) {
673- assertIsError ( e ) ;
674- if ( e . code !== 'MODULE_NOT_FOUND' ) {
675- throw e ;
676- }
677- }
678-
679- return false ;
671+ return ! ! this . resolvePackageJson ( name ) ;
680672 }
681673
682674 private executeSchematic (
@@ -715,12 +707,7 @@ export default class AddCommandModule
715707 return cachedVersion ;
716708 }
717709
718- const { root } = this . context ;
719- let installedPackagePath ;
720- try {
721- installedPackagePath = this . rootRequire . resolve ( join ( name , 'package.json' ) ) ;
722- } catch { }
723-
710+ const installedPackagePath = this . resolvePackageJson ( name ) ;
724711 if ( installedPackagePath ) {
725712 try {
726713 const installedPackage = JSON . parse (
@@ -732,13 +719,7 @@ export default class AddCommandModule
732719 } catch { }
733720 }
734721
735- let projectManifest ;
736- try {
737- projectManifest = JSON . parse (
738- await fs . readFile ( join ( root , 'package.json' ) , 'utf-8' ) ,
739- ) as PackageManifest ;
740- } catch { }
741-
722+ const projectManifest = await this . getProjectManifest ( ) ;
742723 if ( projectManifest ) {
743724 const version =
744725 projectManifest . dependencies ?. [ name ] || projectManifest . devDependencies ?. [ name ] ;
@@ -754,6 +735,58 @@ export default class AddCommandModule
754735 return null ;
755736 }
756737
738+ private async getProjectManifest ( ) : Promise < PackageManifest | null > {
739+ if ( this . #rootManifestCache) {
740+ return this . #rootManifestCache;
741+ }
742+
743+ const { root } = this . context ;
744+ try {
745+ this . #rootManifestCache = JSON . parse (
746+ await fs . readFile ( join ( root , 'package.json' ) , 'utf-8' ) ,
747+ ) as PackageManifest ;
748+
749+ return this . #rootManifestCache;
750+ } catch {
751+ return null ;
752+ }
753+ }
754+
755+ private resolvePackageJson ( name : string ) : string | undefined {
756+ try {
757+ return this . rootRequire . resolve ( join ( name , 'package.json' ) ) ;
758+ } catch ( e ) {
759+ assertIsError ( e ) ;
760+ if ( e . code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' ) {
761+ try {
762+ const mainPath = this . rootRequire . resolve ( name ) ;
763+ let directory = dirname ( mainPath ) ;
764+
765+ // Stop at the node_modules boundary or the root of the file system
766+ while ( directory && basename ( directory ) !== 'node_modules' ) {
767+ const packageJsonPath = join ( directory , 'package.json' ) ;
768+ if ( existsSync ( packageJsonPath ) ) {
769+ return packageJsonPath ;
770+ }
771+
772+ const parent = dirname ( directory ) ;
773+ if ( parent === directory ) {
774+ break ;
775+ }
776+ directory = parent ;
777+ }
778+ } catch ( e ) {
779+ assertIsError ( e ) ;
780+ this . context . logger . debug (
781+ `Failed to resolve package '${ name } ' during fallback: ${ e . message } ` ,
782+ ) ;
783+ }
784+ }
785+ }
786+
787+ return undefined ;
788+ }
789+
757790 private async getPeerDependencyConflicts ( manifest : PackageManifest ) : Promise < string [ ] | false > {
758791 if ( ! manifest . peerDependencies ) {
759792 return false ;
0 commit comments