Skip to content

Commit a63799b

Browse files
committed
refactor(@angular/cli): optimize and harden list_projects tool
This commit significantly improves the `list_projects` tool's performance, robustness, and code quality. The key changes include: - Parallelizing Workspace Processing: The `createListProjectsHandler` now collects all `angular.json` file paths first, then processes them concurrently using `Promise.all`. This speeds up the tool for repositories containing multiple workspaces. - Robust Duplicate Handling: A Map-based deduplication strategy ensures each `angular.json` is processed only once. It selects the "best" (shortest) search root for each unique workspace, guaranteeing optimal framework version detection regardless of the input `searchRoots` order. - Enhanced package.json Parsing Safety: `findAngularCoreVersion` now specifically catches and reports `SyntaxError` for malformed `package.json` files, providing clearer error messages. - Improved Type Safety and Readability: `loadAndParseWorkspace` now returns a discriminated union, ensuring `workspace` is non-null when no `error` is present. This removed a redundant null check and `seenPaths` arguments from helper functions, simplifying the codebase.
1 parent 3480966 commit a63799b

File tree

1 file changed

+39
-31
lines changed

1 file changed

+39
-31
lines changed

packages/angular/cli/src/commands/mcp/tools/projects.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,14 @@ async function findAngularCoreVersion(
287287
const pkgPath = join(currentDir, 'package.json');
288288
try {
289289
const pkgContent = await readFile(pkgPath, 'utf-8');
290-
const pkg = JSON.parse(pkgContent);
290+
let pkg;
291+
try {
292+
pkg = JSON.parse(pkgContent);
293+
} catch (e) {
294+
assertIsError(e);
295+
throw new Error(`Malformed package.json at ${pkgPath}: ${e.message}`);
296+
}
297+
291298
const versionSpecifier =
292299
pkg.dependencies?.['@angular/core'] ?? pkg.devDependencies?.['@angular/core'];
293300

@@ -454,15 +461,8 @@ async function getProjectStyleLanguage(
454461
*/
455462
async function loadAndParseWorkspace(
456463
configFile: string,
457-
seenPaths: Set<string>,
458-
): Promise<{ workspace: WorkspaceData | null; error: ParsingError | null }> {
464+
): Promise<{ workspace: WorkspaceData; error: null } | { workspace: null; error: ParsingError }> {
459465
try {
460-
const resolvedPath = resolve(configFile);
461-
if (seenPaths.has(resolvedPath)) {
462-
return { workspace: null, error: null }; // Already processed, skip.
463-
}
464-
seenPaths.add(resolvedPath);
465-
466466
const ws = await AngularWorkspace.load(configFile);
467467
const projects = [];
468468
const workspaceRoot = dirname(configFile);
@@ -508,22 +508,17 @@ async function loadAndParseWorkspace(
508508
async function processConfigFile(
509509
configFile: string,
510510
searchRoot: string,
511-
seenPaths: Set<string>,
512511
versionCache: Map<string, string | undefined>,
513512
): Promise<{
514513
workspace?: WorkspaceData;
515514
parsingError?: ParsingError;
516515
versioningError?: VersioningError;
517516
}> {
518-
const { workspace, error } = await loadAndParseWorkspace(configFile, seenPaths);
517+
const { workspace, error } = await loadAndParseWorkspace(configFile);
519518
if (error) {
520519
return { parsingError: error };
521520
}
522521

523-
if (!workspace) {
524-
return {}; // Skipped as it was already seen.
525-
}
526-
527522
try {
528523
const workspaceDir = dirname(configFile);
529524
workspace.frameworkVersion = await findAngularCoreVersion(
@@ -549,7 +544,6 @@ async function createListProjectsHandler({ server }: McpToolContext) {
549544
const workspaces: WorkspaceData[] = [];
550545
const parsingErrors: ParsingError[] = [];
551546
const versioningErrors: z.infer<typeof listProjectsOutputSchema.versioningErrors> = [];
552-
const seenPaths = new Set<string>();
553547
const versionCache = new Map<string, string | undefined>();
554548

555549
let searchRoots: string[];
@@ -574,27 +568,41 @@ async function createListProjectsHandler({ server }: McpToolContext) {
574568
})
575569
.filter((r): r is string => r !== null);
576570

571+
// Collect all unique angular.json files and their best matching search root.
572+
// We prefer the shortest search root (closest to the filesystem root) to maximize
573+
// the upward search range for package.json version detection.
574+
const configFilesToRoot = new Map<string, string>();
575+
577576
for (const root of searchRoots) {
578577
for await (const configFile of findAngularJsonFiles(root, realAllowedRoots)) {
579-
const { workspace, parsingError, versioningError } = await processConfigFile(
580-
configFile,
581-
root,
582-
seenPaths,
583-
versionCache,
584-
);
585-
586-
if (workspace) {
587-
workspaces.push(workspace);
588-
}
589-
if (parsingError) {
590-
parsingErrors.push(parsingError);
591-
}
592-
if (versioningError) {
593-
versioningErrors.push(versioningError);
578+
const resolvedPath = resolve(configFile);
579+
const existingRoot = configFilesToRoot.get(resolvedPath);
580+
581+
if (!existingRoot || root.length < existingRoot.length) {
582+
configFilesToRoot.set(resolvedPath, root);
594583
}
595584
}
596585
}
597586

587+
// Process all unique workspaces in parallel.
588+
const results = await Promise.all(
589+
Array.from(configFilesToRoot).map(([configFile, searchRoot]) =>
590+
processConfigFile(configFile, searchRoot, versionCache),
591+
),
592+
);
593+
594+
for (const { workspace, parsingError, versioningError } of results) {
595+
if (workspace) {
596+
workspaces.push(workspace);
597+
}
598+
if (parsingError) {
599+
parsingErrors.push(parsingError);
600+
}
601+
if (versioningError) {
602+
versioningErrors.push(versioningError);
603+
}
604+
}
605+
598606
if (workspaces.length === 0 && parsingErrors.length === 0) {
599607
return {
600608
content: [

0 commit comments

Comments
 (0)