Skip to content

Commit ebf81d6

Browse files
committed
perf(@angular/cli): cache root manifest and resolve restricted package exports in ng add
This change enhances the `ng add` command's performance by caching the root project manifest (`package.json`) to avoid redundant disk reads and JSON parsing during peer dependency conflict checks. Additionally, it improves the robustness of `package.json` resolution for installed packages. Previously, resolving `package.json` could fail if a third-party package used the Node.js `"exports"` field without explicitly exporting its `package.json`. The CLI now correctly handles this by falling back to resolving the package's entry point and traversing upwards to find the manifest.
1 parent 8adfc22 commit ebf81d6

File tree

1 file changed

+55
-26
lines changed
  • packages/angular/cli/src/commands/add

1 file changed

+55
-26
lines changed

packages/angular/cli/src/commands/add/cli.ts

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
import { Listr, ListrRenderer, ListrTaskWrapper, color, figures } from 'listr2';
1010
import assert from 'node:assert';
11+
import { existsSync } from 'node:fs';
1112
import fs from 'node:fs/promises';
1213
import { createRequire } from 'node:module';
13-
import { dirname, join } from 'node:path';
14+
import { basename, dirname, join } from 'node:path';
1415
import npa from 'npm-package-arg';
1516
import semver, { Range, compare, intersects, prerelease, satisfies, valid } from 'semver';
1617
import { Argv } from 'yargs';
@@ -30,6 +31,7 @@ import {
3031
PackageMetadata,
3132
} from '../../package-managers';
3233
import { assertIsError } from '../../utilities/error';
34+
import { findUpSync } from '../../utilities/find-up';
3335
import { isTTY } from '../../utilities/tty';
3436
import { VERSION } from '../../utilities/version';
3537

@@ -107,6 +109,7 @@ export default class AddCommandModule
107109
private readonly schematicName = 'ng-add';
108110
private rootRequire = createRequire(this.context.root + '/');
109111
#projectVersionCache = new Map<string, string | null>();
112+
#rootManifestCache: PackageManifest | null = null;
110113

111114
override async builder(argv: Argv): Promise<Argv<AddCommandArgs>> {
112115
const localYargs = (await super.builder(argv))
@@ -156,6 +159,7 @@ export default class AddCommandModule
156159

157160
async run(options: Options<AddCommandArgs> & OtherOptions): Promise<number | void> {
158161
this.#projectVersionCache.clear();
162+
this.#rootManifestCache = null;
159163
const { logger } = this.context;
160164
const { collection, skipConfirmation } = options;
161165

@@ -657,18 +661,7 @@ export default class AddCommandModule
657661
}
658662

659663
private isPackageInstalled(name: string): boolean {
660-
try {
661-
this.rootRequire.resolve(join(name, 'package.json'));
662-
663-
return true;
664-
} catch (e) {
665-
assertIsError(e);
666-
if (e.code !== 'MODULE_NOT_FOUND') {
667-
throw e;
668-
}
669-
}
670-
671-
return false;
664+
return !!this.resolvePackageJson(name);
672665
}
673666

674667
private executeSchematic(
@@ -707,12 +700,7 @@ export default class AddCommandModule
707700
return cachedVersion;
708701
}
709702

710-
const { root } = this.context;
711-
let installedPackagePath;
712-
try {
713-
installedPackagePath = this.rootRequire.resolve(join(name, 'package.json'));
714-
} catch {}
715-
703+
const installedPackagePath = this.resolvePackageJson(name);
716704
if (installedPackagePath) {
717705
try {
718706
const installedPackage = JSON.parse(
@@ -724,13 +712,7 @@ export default class AddCommandModule
724712
} catch {}
725713
}
726714

727-
let projectManifest;
728-
try {
729-
projectManifest = JSON.parse(
730-
await fs.readFile(join(root, 'package.json'), 'utf-8'),
731-
) as PackageManifest;
732-
} catch {}
733-
715+
const projectManifest = await this.getProjectManifest();
734716
if (projectManifest) {
735717
const version =
736718
projectManifest.dependencies?.[name] || projectManifest.devDependencies?.[name];
@@ -746,6 +728,53 @@ export default class AddCommandModule
746728
return null;
747729
}
748730

731+
private async getProjectManifest(): Promise<PackageManifest | null> {
732+
if (this.#rootManifestCache) {
733+
return this.#rootManifestCache;
734+
}
735+
736+
const { root } = this.context;
737+
try {
738+
this.#rootManifestCache = JSON.parse(
739+
await fs.readFile(join(root, 'package.json'), 'utf-8'),
740+
) as PackageManifest;
741+
742+
return this.#rootManifestCache;
743+
} catch {
744+
return null;
745+
}
746+
}
747+
748+
private resolvePackageJson(name: string): string | undefined {
749+
try {
750+
return this.rootRequire.resolve(join(name, 'package.json'));
751+
} catch (e) {
752+
assertIsError(e);
753+
if (e.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
754+
try {
755+
const mainPath = this.rootRequire.resolve(name);
756+
let directory = dirname(mainPath);
757+
758+
// Stop at the node_modules boundary or the root of the file system
759+
while (directory && basename(directory) !== 'node_modules') {
760+
const packageJsonPath = join(directory, 'package.json');
761+
if (existsSync(packageJsonPath)) {
762+
return packageJsonPath;
763+
}
764+
765+
const parent = dirname(directory);
766+
if (parent === directory) {
767+
break;
768+
}
769+
directory = parent;
770+
}
771+
} catch {}
772+
}
773+
}
774+
775+
return undefined;
776+
}
777+
749778
private async getPeerDependencyConflicts(manifest: PackageManifest): Promise<string[] | false> {
750779
if (!manifest.peerDependencies) {
751780
return false;

0 commit comments

Comments
 (0)