From 667daf9ae2c6d83f1608545d65572c60ef6a63a8 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 24 Nov 2025 18:33:27 -0500 Subject: [PATCH] refactor(@angular/cli): parallelize MCP zoneless migration file discovery and improve error handling This commit enhances the `onpush_zoneless_migration` MCP tool by improving its file discovery and error handling, with a primary goal of increasing performance on large projects. The `discoverAndCategorizeFiles` function is refactored to parallelize both file I/O and analysis. It now gathers file paths and processes them concurrently in batches, preventing system resource exhaustion while speeding up execution. The tool now continues processing even if some files fail to be analyzed. Any errors are collected and reported in the final output, providing the user with a complete summary of unanalyzable files. --- .../zoneless-migration.ts | 120 ++++++++++++------ 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts index 080480742d3b..f85a6f322c60 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts @@ -70,12 +70,10 @@ export async function registerZonelessMigrationTool( fileOrDirPath: string, extras: RequestHandlerExtra, ) { - let filesWithComponents, componentTestFiles, zoneFiles; + let filesWithComponents, componentTestFiles, zoneFiles, categorizationErrors; try { - ({ filesWithComponents, componentTestFiles, zoneFiles } = await discoverAndCategorizeFiles( - fileOrDirPath, - extras, - )); + ({ filesWithComponents, componentTestFiles, zoneFiles, categorizationErrors } = + await discoverAndCategorizeFiles(fileOrDirPath, extras)); } catch (e) { return createResponse( `Error: Could not access the specified path. Please ensure the following path is correct ` + @@ -113,6 +111,14 @@ export async function registerZonelessMigrationTool( } } + if (categorizationErrors.length > 0) { + let errorMessage = + 'Migration analysis is complete for all actionable files. However, the following files could not be analyzed due to errors:\n'; + errorMessage += categorizationErrors.map((e) => `- ${e.filePath}: ${e.message}`).join('\n'); + + return createResponse(errorMessage); + } + return createTestDebuggingGuideForNonActionableInput(fileOrDirPath); } @@ -120,10 +126,11 @@ async function discoverAndCategorizeFiles( fileOrDirPath: string, extras: RequestHandlerExtra, ) { - let files: SourceFile[] = []; + const filePaths: string[] = []; const componentTestFiles = new Set(); const filesWithComponents = new Set(); const zoneFiles = new Set(); + const categorizationErrors: { filePath: string; message: string }[] = []; let isDirectory: boolean; try { @@ -134,52 +141,87 @@ async function discoverAndCategorizeFiles( } if (isDirectory) { - const allFiles = glob(`${fileOrDirPath}/**/*.ts`); - for await (const file of allFiles) { - files.push(await createSourceFile(file)); + for await (const file of glob(`${fileOrDirPath}/**/*.ts`)) { + filePaths.push(file); } } else { - files = [await createSourceFile(fileOrDirPath)]; + filePaths.push(fileOrDirPath); const maybeTestFile = await getTestFilePath(fileOrDirPath); if (maybeTestFile) { - componentTestFiles.add(await createSourceFile(maybeTestFile)); + // Eagerly add the test file path for categorization. + filePaths.push(maybeTestFile); } } - for (const sourceFile of files) { - const content = sourceFile.getFullText(); - const componentSpecifier = await getImportSpecifier(sourceFile, '@angular/core', 'Component'); - const zoneSpecifier = await getImportSpecifier(sourceFile, '@angular/core', 'NgZone'); - const testBedSpecifier = await getImportSpecifier( - sourceFile, - /(@angular\/core)?\/testing/, - 'TestBed', + const CONCURRENCY_LIMIT = 50; + const filesToProcess = [...filePaths]; + while (filesToProcess.length > 0) { + const batch = filesToProcess.splice(0, CONCURRENCY_LIMIT); + const results = await Promise.allSettled( + batch.map(async (filePath) => { + const sourceFile = await createSourceFile(filePath); + await categorizeFile(sourceFile, extras, { + filesWithComponents, + componentTestFiles, + zoneFiles, + }); + }), ); - if (testBedSpecifier) { - componentTestFiles.add(sourceFile); - } else if (componentSpecifier) { - if ( - !content.includes('changeDetectionStrategy: ChangeDetectionStrategy.OnPush') && - !content.includes('changeDetectionStrategy: ChangeDetectionStrategy.Default') - ) { - filesWithComponents.add(sourceFile); - } else { - sendDebugMessage( - `Component file already has change detection strategy: ${sourceFile.fileName}. Skipping migration.`, - extras, - ); - } - const testFilePath = await getTestFilePath(sourceFile.fileName); - if (testFilePath) { - componentTestFiles.add(await createSourceFile(testFilePath)); + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === 'rejected') { + const failedFile = batch[i]; + const reason = result.reason instanceof Error ? result.reason.message : `${result.reason}`; + categorizationErrors.push({ filePath: failedFile, message: reason }); } - } else if (zoneSpecifier) { - zoneFiles.add(sourceFile); } } - return { filesWithComponents, componentTestFiles, zoneFiles }; + return { filesWithComponents, componentTestFiles, zoneFiles, categorizationErrors }; +} + +async function categorizeFile( + sourceFile: SourceFile, + extras: RequestHandlerExtra, + categorizedFiles: { + filesWithComponents: Set; + componentTestFiles: Set; + zoneFiles: Set; + }, +) { + const { filesWithComponents, componentTestFiles, zoneFiles } = categorizedFiles; + const content = sourceFile.getFullText(); + const componentSpecifier = await getImportSpecifier(sourceFile, '@angular/core', 'Component'); + const zoneSpecifier = await getImportSpecifier(sourceFile, '@angular/core', 'NgZone'); + const testBedSpecifier = await getImportSpecifier( + sourceFile, + /(@angular\/core)?\/testing/, + 'TestBed', + ); + + if (testBedSpecifier) { + componentTestFiles.add(sourceFile); + } else if (componentSpecifier) { + if ( + !content.includes('changeDetectionStrategy: ChangeDetectionStrategy.OnPush') && + !content.includes('changeDetectionStrategy: ChangeDetectionStrategy.Default') + ) { + filesWithComponents.add(sourceFile); + } else { + sendDebugMessage( + `Component file already has change detection strategy: ${sourceFile.fileName}. Skipping migration.`, + extras, + ); + } + + const testFilePath = await getTestFilePath(sourceFile.fileName); + if (testFilePath) { + componentTestFiles.add(await createSourceFile(testFilePath)); + } + } else if (zoneSpecifier) { + zoneFiles.add(sourceFile); + } } async function rankComponentFilesForMigration(