diff --git a/packages/core/schematics/migrations/inject-flags/index.ts b/packages/core/schematics/migrations/inject-flags/index.ts index 977914b5fc32..6afef8b719ac 100644 --- a/packages/core/schematics/migrations/inject-flags/index.ts +++ b/packages/core/schematics/migrations/inject-flags/index.ts @@ -6,65 +6,15 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Rule, SchematicsException} from '@angular-devkit/schematics'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem'; -import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements'; -import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; -import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge'; -import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units'; -import {CompilationUnitData, InjectFlagsMigration} from './inject_flags_migration'; +import {Rule} from '@angular-devkit/schematics'; +import {InjectFlagsMigration} from './inject_flags_migration'; +import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit'; export function migrate(): Rule { return async (tree) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot replace `InjectFlags` usages.', - ); - } - - const fs = new DevkitMigrationFilesystem(tree); - setFileSystem(fs); - - const migration = new InjectFlagsMigration(); - const unitResults: CompilationUnitData[] = []; - const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { - const baseInfo = migration.createProgram(tsconfigPath, fs); - const info = migration.prepareProgram(baseInfo); - return {info, tsconfigPath}; + await runMigrationInDevkit({ + tree, + getMigration: () => new InjectFlagsMigration(), }); - - for (const {info} of programInfos) { - unitResults.push(await migration.analyze(info)); - } - - const combined = await synchronouslyCombineUnitData(migration, unitResults); - if (combined === null) { - return; - } - - const globalMeta = await migration.globalMeta(combined); - const replacementsPerFile: Map = new Map(); - const {replacements} = await migration.migrate(globalMeta); - const changesPerFile = groupReplacementsByFile(replacements); - - for (const [file, changes] of changesPerFile) { - if (!replacementsPerFile.has(file)) { - replacementsPerFile.set(file, changes); - } - } - - for (const [file, changes] of replacementsPerFile) { - const recorder = tree.beginUpdate(file); - for (const c of changes) { - recorder - .remove(c.data.position, c.data.end - c.data.position) - .insertRight(c.data.position, c.data.toInsert); - } - tree.commitUpdate(recorder); - } }; } diff --git a/packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel b/packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel index fbd7ca8113a0..3b057f58ab65 100644 --- a/packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel +++ b/packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel @@ -21,7 +21,6 @@ ts_library( "//packages/compiler-cli", "//packages/compiler-cli/private", "//packages/compiler-cli/src/ngtsc/core:api", - "//packages/compiler-cli/src/ngtsc/file_system", "//packages/core/schematics/utils", "//packages/core/schematics/utils/tsurge", "//packages/core/schematics/utils/tsurge/helpers/angular_devkit", diff --git a/packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts b/packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts index 70cd7898d68e..0518aa3c4b31 100644 --- a/packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts +++ b/packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts @@ -6,86 +6,39 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Rule, SchematicsException} from '@angular-devkit/schematics'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem'; -import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements'; -import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; -import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge'; -import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units'; -import {CompilationUnitData, UnusedImportsMigration} from './unused_imports_migration'; +import {Rule} from '@angular-devkit/schematics'; +import {UnusedImportsMigration} from './unused_imports_migration'; +import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit'; export function migrate(): Rule { return async (tree, context) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot clean up unused imports.', - ); - } - - const fs = new DevkitMigrationFilesystem(tree); - setFileSystem(fs); - - const migration = new UnusedImportsMigration(); - const unitResults: CompilationUnitData[] = []; - const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { - context.logger.info(`Preparing analysis for ${tsconfigPath}`); - - const baseInfo = migration.createProgram(tsconfigPath, fs); - const info = migration.prepareProgram(baseInfo); - - return {info, tsconfigPath}; + await runMigrationInDevkit({ + getMigration: () => new UnusedImportsMigration(), + tree, + beforeProgramCreation: (tsconfigPath) => { + context.logger.info(`Preparing analysis for ${tsconfigPath}`); + }, + beforeUnitAnalysis: (tsconfigPath) => { + context.logger.info(`Scanning for unused imports using ${tsconfigPath}`); + }, + afterAnalysisFailure: () => { + context.logger.error('Schematic failed unexpectedly with no analysis data'); + }, + whenDone: (stats) => { + const {removedImports, changedFiles} = stats.counters; + let statsMessage: string; + + if (removedImports === 0) { + statsMessage = 'Schematic could not find unused imports in the project'; + } else { + statsMessage = + `Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` + + `in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`; + } + + context.logger.info(''); + context.logger.info(statsMessage); + }, }); - - for (const {info, tsconfigPath} of programInfos) { - context.logger.info(`Scanning for unused imports using ${tsconfigPath}`); - unitResults.push(await migration.analyze(info)); - } - - const combined = await synchronouslyCombineUnitData(migration, unitResults); - if (combined === null) { - context.logger.error('Schematic failed unexpectedly with no analysis data'); - return; - } - - const globalMeta = await migration.globalMeta(combined); - const replacementsPerFile: Map = new Map(); - const {replacements} = await migration.migrate(globalMeta); - const changesPerFile = groupReplacementsByFile(replacements); - - for (const [file, changes] of changesPerFile) { - if (!replacementsPerFile.has(file)) { - replacementsPerFile.set(file, changes); - } - } - - for (const [file, changes] of replacementsPerFile) { - const recorder = tree.beginUpdate(file); - for (const c of changes) { - recorder - .remove(c.data.position, c.data.end - c.data.position) - .insertRight(c.data.position, c.data.toInsert); - } - tree.commitUpdate(recorder); - } - - const { - counters: {removedImports, changedFiles}, - } = await migration.stats(globalMeta); - let statsMessage: string; - - if (removedImports === 0) { - statsMessage = 'Schematic could not find unused imports in the project'; - } else { - statsMessage = - `Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` + - `in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`; - } - - context.logger.info(''); - context.logger.info(statsMessage); }; } diff --git a/packages/core/schematics/ng-generate/output-migration/index.ts b/packages/core/schematics/ng-generate/output-migration/index.ts index ca25df33f426..869ec8bb6175 100644 --- a/packages/core/schematics/ng-generate/output-migration/index.ts +++ b/packages/core/schematics/ng-generate/output-migration/index.ts @@ -6,18 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Rule, SchematicsException} from '@angular-devkit/schematics'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem'; -import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements'; -import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; -import { - CompilationUnitData, - OutputMigration, -} from '../../migrations/output-migration/output-migration'; -import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge'; -import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units'; +import {Rule} from '@angular-devkit/schematics'; +import {OutputMigration} from '../../migrations/output-migration/output-migration'; +import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit'; interface Options { path: string; @@ -26,99 +17,53 @@ interface Options { export function migrate(options: Options): Rule { return async (tree, context) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot run output migration.', - ); - } - - const fs = new DevkitMigrationFilesystem(tree); - setFileSystem(fs); - - const migration = new OutputMigration({ - shouldMigrate: (_, file) => { - return ( - file.rootRelativePath.startsWith(fs.normalize(options.path)) && - !/(^|\/)node_modules\//.test(file.rootRelativePath) - ); + await runMigrationInDevkit({ + tree, + getMigration: (fs) => + new OutputMigration({ + shouldMigrate: (_, file) => { + return ( + file.rootRelativePath.startsWith(fs.normalize(options.path)) && + !/(^|\/)node_modules\//.test(file.rootRelativePath) + ); + }, + }), + beforeProgramCreation: (tsconfigPath) => { + context.logger.info(`Preparing analysis for: ${tsconfigPath}...`); }, - }); - - const analysisPath = fs.resolve(options.analysisDir); - const unitResults: CompilationUnitData[] = []; - const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { - context.logger.info(`Preparing analysis for: ${tsconfigPath}..`); - - const baseInfo = migration.createProgram(tsconfigPath, fs); - const info = migration.prepareProgram(baseInfo); - - // Support restricting the analysis to subfolders for larger projects. - if (analysisPath !== '/') { - info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath)); - info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => - sf.fileName.startsWith(analysisPath), + afterProgramCreation: (info, fs) => { + const analysisPath = fs.resolve(options.analysisDir); + + // Support restricting the analysis to subfolders for larger projects. + if (analysisPath !== '/') { + info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath)); + info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => + sf.fileName.startsWith(analysisPath), + ); + } + }, + beforeUnitAnalysis: (tsconfigPath) => { + context.logger.info(`Scanning for outputs: ${tsconfigPath}...`); + }, + afterAllAnalyzed: () => { + context.logger.info(``); + context.logger.info(`Processing analysis data between targets...`); + context.logger.info(``); + }, + afterAnalysisFailure: () => { + context.logger.error('Migration failed unexpectedly with no analysis data'); + }, + whenDone: ({counters}) => { + const {detectedOutputs, problematicOutputs, successRate} = counters; + const migratedOutputs = detectedOutputs - problematicOutputs; + const successRatePercent = (successRate * 100).toFixed(2); + + context.logger.info(''); + context.logger.info(`Successfully migrated to outputs as functions 🎉`); + context.logger.info( + ` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`, ); - } - - return {info, tsconfigPath}; + }, }); - - // Analyze phase. Treat all projects as compilation units as - // this allows us to support references between those. - for (const {info, tsconfigPath} of programInfos) { - context.logger.info(`Scanning for outputs: ${tsconfigPath}..`); - unitResults.push(await migration.analyze(info)); - } - - context.logger.info(``); - context.logger.info(`Processing analysis data between targets..`); - context.logger.info(``); - - const combined = await synchronouslyCombineUnitData(migration, unitResults); - if (combined === null) { - context.logger.error('Migration failed unexpectedly with no analysis data'); - return; - } - - const globalMeta = await migration.globalMeta(combined); - const replacementsPerFile: Map = new Map(); - - for (const {info, tsconfigPath} of programInfos) { - context.logger.info(`Migrating: ${tsconfigPath}..`); - - const {replacements} = await migration.migrate(globalMeta); - const changesPerFile = groupReplacementsByFile(replacements); - - for (const [file, changes] of changesPerFile) { - if (!replacementsPerFile.has(file)) { - replacementsPerFile.set(file, changes); - } - } - } - - context.logger.info(`Applying changes..`); - for (const [file, changes] of replacementsPerFile) { - const recorder = tree.beginUpdate(file); - for (const c of changes) { - recorder - .remove(c.data.position, c.data.end - c.data.position) - .insertLeft(c.data.position, c.data.toInsert); - } - tree.commitUpdate(recorder); - } - - const { - counters: {detectedOutputs, problematicOutputs, successRate}, - } = await migration.stats(globalMeta); - const migratedOutputs = detectedOutputs - problematicOutputs; - const successRatePercent = (successRate * 100).toFixed(2); - - context.logger.info(''); - context.logger.info(`Successfully migrated to outputs as functions 🎉`); - context.logger.info( - ` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`, - ); }; } diff --git a/packages/core/schematics/ng-generate/self-closing-tags-migration/index.ts b/packages/core/schematics/ng-generate/self-closing-tags-migration/index.ts index 6b308e03a3c9..d7499860a03e 100644 --- a/packages/core/schematics/ng-generate/self-closing-tags-migration/index.ts +++ b/packages/core/schematics/ng-generate/self-closing-tags-migration/index.ts @@ -6,18 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Rule, SchematicsException} from '@angular-devkit/schematics'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem'; -import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements'; -import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; -import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge'; -import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units'; -import { - SelfClosingTagsCompilationUnitData, - SelfClosingTagsMigration, -} from '../../migrations/self-closing-tags-migration/self-closing-tags-migration'; +import {Rule} from '@angular-devkit/schematics'; +import {SelfClosingTagsMigration} from '../../migrations/self-closing-tags-migration/self-closing-tags-migration'; +import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit'; interface Options { path: string; @@ -26,88 +17,39 @@ interface Options { export function migrate(options: Options): Rule { return async (tree, context) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot run self-closing tags migration.', - ); - } - - const fs = new DevkitMigrationFilesystem(tree); - setFileSystem(fs); - - const migration = new SelfClosingTagsMigration({ - shouldMigrate: (file) => { - return ( - file.rootRelativePath.startsWith(fs.normalize(options.path)) && - !/(^|\/)node_modules\//.test(file.rootRelativePath) + await runMigrationInDevkit({ + tree, + getMigration: (fs) => + new SelfClosingTagsMigration({ + shouldMigrate: (file) => { + return ( + file.rootRelativePath.startsWith(fs.normalize(options.path)) && + !/(^|\/)node_modules\//.test(file.rootRelativePath) + ); + }, + }), + beforeProgramCreation: (tsconfigPath) => { + context.logger.info(`Preparing analysis for: ${tsconfigPath}...`); + }, + beforeUnitAnalysis: (tsconfigPath) => { + context.logger.info(`Scanning for component tags: ${tsconfigPath}...`); + }, + afterAllAnalyzed: () => { + context.logger.info(``); + context.logger.info(`Processing analysis data between targets...`); + context.logger.info(``); + }, + afterAnalysisFailure: () => { + context.logger.error('Migration failed unexpectedly with no analysis data'); + }, + whenDone: ({counters}) => { + const {touchedFilesCount, replacementCount} = counters; + context.logger.info(''); + context.logger.info(`Successfully migrated to self-closing tags 🎉`); + context.logger.info( + ` -> Migrated ${replacementCount} components to self-closing tags in ${touchedFilesCount} component files.`, ); }, }); - - const unitResults: SelfClosingTagsCompilationUnitData[] = []; - const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { - context.logger.info(`Preparing analysis for: ${tsconfigPath}..`); - - const baseInfo = migration.createProgram(tsconfigPath, fs); - const info = migration.prepareProgram(baseInfo); - - return {info, tsconfigPath}; - }); - - // Analyze phase. Treat all projects as compilation units as - // this allows us to support references between those. - for (const {info, tsconfigPath} of programInfos) { - context.logger.info(`Scanning for component tags: ${tsconfigPath}..`); - unitResults.push(await migration.analyze(info)); - } - - context.logger.info(``); - context.logger.info(`Processing analysis data between targets..`); - context.logger.info(``); - - const combined = await synchronouslyCombineUnitData(migration, unitResults); - if (combined === null) { - context.logger.error('Migration failed unexpectedly with no analysis data'); - return; - } - - const globalMeta = await migration.globalMeta(combined); - const replacementsPerFile: Map = new Map(); - - for (const {tsconfigPath} of programInfos) { - context.logger.info(`Migrating: ${tsconfigPath}..`); - - const {replacements} = await migration.migrate(globalMeta); - const changesPerFile = groupReplacementsByFile(replacements); - - for (const [file, changes] of changesPerFile) { - if (!replacementsPerFile.has(file)) { - replacementsPerFile.set(file, changes); - } - } - } - - context.logger.info(`Applying changes..`); - for (const [file, changes] of replacementsPerFile) { - const recorder = tree.beginUpdate(file); - for (const c of changes) { - recorder - .remove(c.data.position, c.data.end - c.data.position) - .insertLeft(c.data.position, c.data.toInsert); - } - tree.commitUpdate(recorder); - } - - const { - counters: {touchedFilesCount, replacementCount}, - } = await migration.stats(globalMeta); - - context.logger.info(''); - context.logger.info(`Successfully migrated to self-closing tags 🎉`); - context.logger.info( - ` -> Migrated ${replacementCount} components to self-closing tags in ${touchedFilesCount} component files.`, - ); }; } diff --git a/packages/core/schematics/ng-generate/signal-input-migration/index.ts b/packages/core/schematics/ng-generate/signal-input-migration/index.ts index f97d225c01d4..4d4b5b69a662 100644 --- a/packages/core/schematics/ng-generate/signal-input-migration/index.ts +++ b/packages/core/schematics/ng-generate/signal-input-migration/index.ts @@ -6,16 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Rule, SchematicsException} from '@angular-devkit/schematics'; - +import {Rule} from '@angular-devkit/schematics'; import {SignalInputMigration} from '../../migrations/signal-migration/src'; -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem'; -import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements'; -import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; -import {CompilationUnitData} from '../../migrations/signal-migration/src/batch/unit_data'; -import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge'; -import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units'; +import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit'; interface Options { path: string; @@ -26,108 +19,64 @@ interface Options { export function migrate(options: Options): Rule { return async (tree, context) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot run signal input migration.', - ); - } - - const fs = new DevkitMigrationFilesystem(tree); - setFileSystem(fs); - - const migration = new SignalInputMigration({ - bestEffortMode: options.bestEffortMode, - insertTodosForSkippedFields: options.insertTodos, - shouldMigrateInput: (input) => { - return ( - input.file.rootRelativePath.startsWith(fs.normalize(options.path)) && - !/(^|\/)node_modules\//.test(input.file.rootRelativePath) - ); + await runMigrationInDevkit({ + tree, + getMigration: (fs) => + new SignalInputMigration({ + bestEffortMode: options.bestEffortMode, + insertTodosForSkippedFields: options.insertTodos, + shouldMigrateInput: (input) => { + return ( + input.file.rootRelativePath.startsWith(fs.normalize(options.path)) && + !/(^|\/)node_modules\//.test(input.file.rootRelativePath) + ); + }, + }), + beforeProgramCreation: (tsconfigPath) => { + context.logger.info(`Preparing analysis for: ${tsconfigPath}...`); }, - }); - const analysisPath = fs.resolve(options.analysisDir); - const unitResults: CompilationUnitData[] = []; - const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { - context.logger.info(`Preparing analysis for: ${tsconfigPath}..`); - - const baseInfo = migration.createProgram(tsconfigPath, fs); - const info = migration.prepareProgram(baseInfo); - - // Support restricting the analysis to subfolders for larger projects. - if (analysisPath !== '/') { - info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath)); - info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => - sf.fileName.startsWith(analysisPath), - ); - } - - return {info, tsconfigPath}; - }); - - // Analyze phase. Treat all projects as compilation units as - // this allows us to support references between those. - for (const {info, tsconfigPath} of programInfos) { - context.logger.info(`Scanning for inputs: ${tsconfigPath}..`); - - unitResults.push(await migration.analyze(info)); - } - - context.logger.info(``); - context.logger.info(`Processing analysis data between targets..`); - context.logger.info(``); - - const combined = await synchronouslyCombineUnitData(migration, unitResults); - if (combined === null) { - context.logger.error('Migration failed unexpectedly with no analysis data'); - return; - } - - const globalMeta = await migration.globalMeta(combined); - const replacementsPerFile: Map = new Map(); - - for (const {info, tsconfigPath} of programInfos) { - context.logger.info(`Migrating: ${tsconfigPath}..`); - - const {replacements} = await migration.migrate(globalMeta, info); - const changesPerFile = groupReplacementsByFile(replacements); - - for (const [file, changes] of changesPerFile) { - if (!replacementsPerFile.has(file)) { - replacementsPerFile.set(file, changes); + afterProgramCreation: (info, fs) => { + const analysisPath = fs.resolve(options.analysisDir); + + // Support restricting the analysis to subfolders for larger projects. + if (analysisPath !== '/') { + info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath)); + info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => + sf.fileName.startsWith(analysisPath), + ); } - } - } - - context.logger.info(`Applying changes..`); - for (const [file, changes] of replacementsPerFile) { - const recorder = tree.beginUpdate(file); - for (const c of changes) { - recorder - .remove(c.data.position, c.data.end - c.data.position) - .insertLeft(c.data.position, c.data.toInsert); - } - tree.commitUpdate(recorder); - } - - const {counters} = await migration.stats(globalMeta); - const migratedInputs = counters.sourceInputs - counters.incompatibleInputs; + }, + beforeUnitAnalysis: (tsconfigPath) => { + context.logger.info(`Scanning for inputs: ${tsconfigPath}...`); + }, + afterAllAnalyzed: () => { + context.logger.info(``); + context.logger.info(`Processing analysis data between targets...`); + context.logger.info(``); + }, + afterAnalysisFailure: () => { + context.logger.error('Migration failed unexpectedly with no analysis data'); + }, + whenDone: ({counters}) => { + const {sourceInputs, incompatibleInputs} = counters; + const migratedInputs = sourceInputs - incompatibleInputs; - context.logger.info(''); - context.logger.info(`Successfully migrated to signal inputs 🎉`); - context.logger.info(` -> Migrated ${migratedInputs}/${counters.sourceInputs} inputs.`); + context.logger.info(''); + context.logger.info(`Successfully migrated to signal inputs 🎉`); + context.logger.info(` -> Migrated ${migratedInputs}/${sourceInputs} inputs.`); - if (counters.incompatibleInputs > 0 && !options.insertTodos) { - context.logger.warn(`To see why ${counters.incompatibleInputs} inputs couldn't be migrated`); - context.logger.warn(`consider re-running with "--insert-todos" or "--best-effort-mode".`); - } + if (incompatibleInputs > 0 && !options.insertTodos) { + context.logger.warn(`To see why ${incompatibleInputs} inputs couldn't be migrated`); + context.logger.warn(`consider re-running with "--insert-todos" or "--best-effort-mode".`); + } - if (options.bestEffortMode) { - context.logger.warn( - `You ran with best effort mode. Manually verify all code ` + - `works as intended, and fix where necessary.`, - ); - } + if (options.bestEffortMode) { + context.logger.warn( + `You ran with best effort mode. Manually verify all code ` + + `works as intended, and fix where necessary.`, + ); + } + }, + }); }; } diff --git a/packages/core/schematics/ng-generate/signal-queries-migration/index.ts b/packages/core/schematics/ng-generate/signal-queries-migration/index.ts index 2d3514b23630..9da1f2e2d0ec 100644 --- a/packages/core/schematics/ng-generate/signal-queries-migration/index.ts +++ b/packages/core/schematics/ng-generate/signal-queries-migration/index.ts @@ -6,18 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Rule, SchematicsException} from '@angular-devkit/schematics'; - -import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; -import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem'; -import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements'; -import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; -import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge'; -import { - CompilationUnitData, - SignalQueriesMigration, -} from '../../migrations/signal-queries-migration/migration'; -import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units'; +import {Rule} from '@angular-devkit/schematics'; +import {SignalQueriesMigration} from '../../migrations/signal-queries-migration/migration'; +import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit'; interface Options { path: string; @@ -28,113 +19,67 @@ interface Options { export function migrate(options: Options): Rule { return async (tree, context) => { - const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree); - - if (!buildPaths.length && !testPaths.length) { - throw new SchematicsException( - 'Could not find any tsconfig file. Cannot run signal queries migration.', - ); - } - - const fs = new DevkitMigrationFilesystem(tree); - setFileSystem(fs); - - const migration = new SignalQueriesMigration({ - bestEffortMode: options.bestEffortMode, - insertTodosForSkippedFields: options.insertTodos, - shouldMigrateQuery: (_query, file) => { - return ( - file.rootRelativePath.startsWith(fs.normalize(options.path)) && - !/(^|\/)node_modules\//.test(file.rootRelativePath) - ); + await runMigrationInDevkit({ + tree, + getMigration: (fs) => + new SignalQueriesMigration({ + bestEffortMode: options.bestEffortMode, + insertTodosForSkippedFields: options.insertTodos, + shouldMigrateQuery: (_query, file) => { + return ( + file.rootRelativePath.startsWith(fs.normalize(options.path)) && + !/(^|\/)node_modules\//.test(file.rootRelativePath) + ); + }, + }), + beforeProgramCreation: (tsconfigPath) => { + context.logger.info(`Preparing analysis for: ${tsconfigPath}...`); }, - }); - const analysisPath = fs.resolve(options.analysisDir); - const unitResults: CompilationUnitData[] = []; - const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { - context.logger.info(`Preparing analysis for: ${tsconfigPath}..`); - - const baseInfo = migration.createProgram(tsconfigPath, fs); - const info = migration.prepareProgram(baseInfo); - - // Support restricting the analysis to subfolders for larger projects. - if (analysisPath !== '/') { - info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath)); - info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => - sf.fileName.startsWith(analysisPath), - ); - } - - return {info, tsconfigPath}; - }); - - // Analyze phase. Treat all projects as compilation units as - // this allows us to support references between those. - for (const {info, tsconfigPath} of programInfos) { - context.logger.info(`Scanning for queries: ${tsconfigPath}..`); - - unitResults.push(await migration.analyze(info)); - } - - context.logger.info(``); - context.logger.info(`Processing analysis data between targets..`); - context.logger.info(``); - - const combined = await synchronouslyCombineUnitData(migration, unitResults); - if (combined === null) { - context.logger.error('Migration failed unexpectedly with no analysis data'); - return; - } - - const globalMeta = await migration.globalMeta(combined); - const replacementsPerFile: Map = new Map(); - - for (const {info, tsconfigPath} of programInfos) { - context.logger.info(`Migrating: ${tsconfigPath}..`); - - const {replacements} = await migration.migrate(globalMeta, info); - const changesPerFile = groupReplacementsByFile(replacements); - - for (const [file, changes] of changesPerFile) { - if (!replacementsPerFile.has(file)) { - replacementsPerFile.set(file, changes); + afterProgramCreation: (info, fs) => { + const analysisPath = fs.resolve(options.analysisDir); + + // Support restricting the analysis to subfolders for larger projects. + if (analysisPath !== '/') { + info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath)); + info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) => + sf.fileName.startsWith(analysisPath), + ); } - } - } - - context.logger.info(`Applying changes..`); - for (const [file, changes] of replacementsPerFile) { - const recorder = tree.beginUpdate(file); - for (const c of changes) { - recorder - .remove(c.data.position, c.data.end - c.data.position) - .insertLeft(c.data.position, c.data.toInsert); - } - tree.commitUpdate(recorder); - } - - context.logger.info(''); - context.logger.info(`Successfully migrated to signal queries 🎉`); + }, + beforeUnitAnalysis: (tsconfigPath) => { + context.logger.info(`Scanning for queries: ${tsconfigPath}...`); + }, + afterAnalysisFailure: () => { + context.logger.error('Migration failed unexpectedly with no analysis data'); + }, + afterAllAnalyzed: () => { + context.logger.info(``); + context.logger.info(`Processing analysis data between targets...`); + context.logger.info(``); + }, + whenDone: ({counters}) => { + context.logger.info(''); + context.logger.info(`Successfully migrated to signal queries 🎉`); - const { - counters: {queriesCount, incompatibleQueries, multiQueries}, - } = await migration.stats(globalMeta); - const migratedQueries = queriesCount - incompatibleQueries; + const {queriesCount, incompatibleQueries} = counters; + const migratedQueries = queriesCount - incompatibleQueries; - context.logger.info(''); - context.logger.info(`Successfully migrated to signal queries 🎉`); - context.logger.info(` -> Migrated ${migratedQueries}/${queriesCount} queries.`); + context.logger.info(''); + context.logger.info(`Successfully migrated to signal queries 🎉`); + context.logger.info(` -> Migrated ${migratedQueries}/${queriesCount} queries.`); - if (incompatibleQueries > 0 && !options.insertTodos) { - context.logger.warn(`To see why ${incompatibleQueries} queries couldn't be migrated`); - context.logger.warn(`consider re-running with "--insert-todos" or "--best-effort-mode".`); - } + if (incompatibleQueries > 0 && !options.insertTodos) { + context.logger.warn(`To see why ${incompatibleQueries} queries couldn't be migrated`); + context.logger.warn(`consider re-running with "--insert-todos" or "--best-effort-mode".`); + } - if (options.bestEffortMode) { - context.logger.warn( - `You ran with best effort mode. Manually verify all code ` + - `works as intended, and fix where necessary.`, - ); - } + if (options.bestEffortMode) { + context.logger.warn( + `You ran with best effort mode. Manually verify all code ` + + `works as intended, and fix where necessary.`, + ); + } + }, + }); }; } diff --git a/packages/core/schematics/utils/tsurge/helpers/angular_devkit/BUILD.bazel b/packages/core/schematics/utils/tsurge/helpers/angular_devkit/BUILD.bazel index 40177f9ec6b8..00b832edd4ac 100644 --- a/packages/core/schematics/utils/tsurge/helpers/angular_devkit/BUILD.bazel +++ b/packages/core/schematics/utils/tsurge/helpers/angular_devkit/BUILD.bazel @@ -6,7 +6,9 @@ ts_library( name = "angular_devkit", srcs = glob(["**/*.ts"]), deps = [ - "//packages/compiler-cli/src/ngtsc/file_system", + "//packages/compiler-cli", + "//packages/core/schematics/utils", + "//packages/core/schematics/utils/tsurge", "@npm//@angular-devkit/core", "@npm//@angular-devkit/schematics", "@npm//@types/node", diff --git a/packages/core/schematics/utils/tsurge/helpers/angular_devkit/devkit_filesystem.ts b/packages/core/schematics/utils/tsurge/helpers/angular_devkit/devkit_filesystem.ts index 00b97fd384ca..399ba7dda14e 100644 --- a/packages/core/schematics/utils/tsurge/helpers/angular_devkit/devkit_filesystem.ts +++ b/packages/core/schematics/utils/tsurge/helpers/angular_devkit/devkit_filesystem.ts @@ -22,7 +22,7 @@ import { FileSystem, PathSegment, PathString, -} from '@angular/compiler-cli/src/ngtsc/file_system'; +} from '@angular/compiler-cli'; import * as posixPath from 'node:path/posix'; /** diff --git a/packages/core/schematics/utils/tsurge/helpers/angular_devkit/index.ts b/packages/core/schematics/utils/tsurge/helpers/angular_devkit/index.ts new file mode 100644 index 000000000000..753858c96002 --- /dev/null +++ b/packages/core/schematics/utils/tsurge/helpers/angular_devkit/index.ts @@ -0,0 +1,10 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export * from './devkit_filesystem'; +export * from './run_in_devkit'; diff --git a/packages/core/schematics/utils/tsurge/helpers/angular_devkit/run_in_devkit.ts b/packages/core/schematics/utils/tsurge/helpers/angular_devkit/run_in_devkit.ts new file mode 100644 index 000000000000..f4fff9d87d86 --- /dev/null +++ b/packages/core/schematics/utils/tsurge/helpers/angular_devkit/run_in_devkit.ts @@ -0,0 +1,119 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {FileSystem, setFileSystem} from '@angular/compiler-cli'; +import {SchematicsException, Tree} from '@angular-devkit/schematics'; +import {DevkitMigrationFilesystem} from './devkit_filesystem'; +import {groupReplacementsByFile} from '../group_replacements'; +import {synchronouslyCombineUnitData} from '../combine_units'; +import {TsurgeFunnelMigration, TsurgeMigration} from '../../migration'; +import {MigrationStats} from '../../base_migration'; +import {Replacement, TextUpdate} from '../../replacement'; +import {ProjectRootRelativePath} from '../../project_paths'; +import {ProgramInfo} from '../../program_info'; +import {getProjectTsConfigPaths} from '../../../../utils/project_tsconfig_paths'; + +/** Information necessary to run a Tsurge migration in the devkit. */ +export interface TsurgeDevkitMigration { + /** Instantiates the migration. */ + getMigration: (fs: FileSystem) => TsurgeMigration; + + /** File tree of the schematic. */ + tree: Tree; + + /** Called before a program is created. Useful to notify the user before processing starts. */ + beforeProgramCreation?: (tsconfigPath: string) => void; + + /** + * Called after a program is created. Useful when the + * structure needs to be modified (e.g. filtering files). + */ + afterProgramCreation?: (info: ProgramInfo, fileSystem: FileSystem) => void; + + /** Called before a unit is analyzed. Useful for logging. */ + beforeUnitAnalysis?: (tsconfigPath: string) => void; + + /** Called after all units are analyzed. Useful for logging. */ + afterAllAnalyzed?: () => void; + + /** Called if analysis has failed. Useful for logging. */ + afterAnalysisFailure?: () => void; + + /** Called when the migration is done running and stats are available. Useful for logging. */ + whenDone?: (stats: MigrationStats) => void; +} + +/** Runs a Tsurge within an Angular Devkit context. */ +export async function runMigrationInDevkit(config: TsurgeDevkitMigration): Promise { + const {buildPaths, testPaths} = await getProjectTsConfigPaths(config.tree); + + if (!buildPaths.length && !testPaths.length) { + throw new SchematicsException('Could not find any tsconfig file. Cannot run the migration.'); + } + + const fs = new DevkitMigrationFilesystem(config.tree); + setFileSystem(fs); + + const migration = config.getMigration(fs); + const unitResults: unknown[] = []; + const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => { + config.beforeProgramCreation?.(tsconfigPath); + const baseInfo = migration.createProgram(tsconfigPath, fs); + const info = migration.prepareProgram(baseInfo); + config.afterProgramCreation?.(info, fs); + return {info, tsconfigPath}; + }); + + for (const {info, tsconfigPath} of programInfos) { + config.beforeUnitAnalysis?.(tsconfigPath); + unitResults.push(await migration.analyze(info)); + } + + config.afterAllAnalyzed?.(); + + const combined = await synchronouslyCombineUnitData(migration, unitResults); + if (combined === null) { + config.afterAnalysisFailure?.(); + return; + } + + const globalMeta = await migration.globalMeta(combined); + let replacements: Replacement[]; + + if (migration instanceof TsurgeFunnelMigration) { + replacements = (await migration.migrate(globalMeta)).replacements; + } else { + replacements = []; + + for (const {info} of programInfos) { + const result = await migration.migrate(globalMeta, info); + replacements.push(...result.replacements); + } + } + + const replacementsPerFile: Map = new Map(); + const changesPerFile = groupReplacementsByFile(replacements); + + for (const [file, changes] of changesPerFile) { + if (!replacementsPerFile.has(file)) { + replacementsPerFile.set(file, changes); + } + } + + for (const [file, changes] of replacementsPerFile) { + const recorder = config.tree.beginUpdate(file); + for (const c of changes) { + recorder + .remove(c.data.position, c.data.end - c.data.position) + .insertRight(c.data.position, c.data.toInsert); + } + config.tree.commitUpdate(recorder); + } + + config.whenDone?.(await migration.stats(globalMeta)); +}