Skip to content

Commit a382066

Browse files
crisbetoatscott
authored andcommitted
refactor(migrations): simplify integration of tsurge migrations into the CLI (#60386) (#60776)
Currently when we reuse a Tsurge migration is reused externally, there's some glue code that needs to be executed in a specific order. The code gets copied between the different migrations which is error-prone and means that bugs may have to be fixed several times. These changes move the common steps out into a separate function so that only the migration-specific logic (mostly instantiation and logging) is left in the schematic. PR Close #60386 PR Close #60776
1 parent f2bfa31 commit a382066

File tree

10 files changed

+360
-496
lines changed

10 files changed

+360
-496
lines changed

packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ ts_library(
2121
"//packages/compiler-cli",
2222
"//packages/compiler-cli/private",
2323
"//packages/compiler-cli/src/ngtsc/core:api",
24-
"//packages/compiler-cli/src/ngtsc/file_system",
2524
"//packages/core/schematics/utils",
2625
"//packages/core/schematics/utils/tsurge",
2726
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",

packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts

+30-77
Original file line numberDiff line numberDiff line change
@@ -6,86 +6,39 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Rule, SchematicsException} from '@angular-devkit/schematics';
10-
11-
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
12-
import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem';
13-
import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements';
14-
import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
15-
import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge';
16-
import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units';
17-
import {CompilationUnitData, UnusedImportsMigration} from './unused_imports_migration';
9+
import {Rule} from '@angular-devkit/schematics';
10+
import {UnusedImportsMigration} from './unused_imports_migration';
11+
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
1812

1913
export function migrate(): Rule {
2014
return async (tree, context) => {
21-
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
22-
23-
if (!buildPaths.length && !testPaths.length) {
24-
throw new SchematicsException(
25-
'Could not find any tsconfig file. Cannot clean up unused imports.',
26-
);
27-
}
28-
29-
const fs = new DevkitMigrationFilesystem(tree);
30-
setFileSystem(fs);
31-
32-
const migration = new UnusedImportsMigration();
33-
const unitResults: CompilationUnitData[] = [];
34-
const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => {
35-
context.logger.info(`Preparing analysis for ${tsconfigPath}`);
36-
37-
const baseInfo = migration.createProgram(tsconfigPath, fs);
38-
const info = migration.prepareProgram(baseInfo);
39-
40-
return {info, tsconfigPath};
15+
await runMigrationInDevkit({
16+
getMigration: () => new UnusedImportsMigration(),
17+
tree,
18+
beforeProgramCreation: (tsconfigPath) => {
19+
context.logger.info(`Preparing analysis for ${tsconfigPath}`);
20+
},
21+
beforeUnitAnalysis: (tsconfigPath) => {
22+
context.logger.info(`Scanning for unused imports using ${tsconfigPath}`);
23+
},
24+
afterAnalysisFailure: () => {
25+
context.logger.error('Schematic failed unexpectedly with no analysis data');
26+
},
27+
whenDone: (stats) => {
28+
const {removedImports, changedFiles} = stats.counters;
29+
let statsMessage: string;
30+
31+
if (removedImports === 0) {
32+
statsMessage = 'Schematic could not find unused imports in the project';
33+
} else {
34+
statsMessage =
35+
`Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` +
36+
`in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`;
37+
}
38+
39+
context.logger.info('');
40+
context.logger.info(statsMessage);
41+
},
4142
});
42-
43-
for (const {info, tsconfigPath} of programInfos) {
44-
context.logger.info(`Scanning for unused imports using ${tsconfigPath}`);
45-
unitResults.push(await migration.analyze(info));
46-
}
47-
48-
const combined = await synchronouslyCombineUnitData(migration, unitResults);
49-
if (combined === null) {
50-
context.logger.error('Schematic failed unexpectedly with no analysis data');
51-
return;
52-
}
53-
54-
const globalMeta = await migration.globalMeta(combined);
55-
const replacementsPerFile: Map<ProjectRootRelativePath, TextUpdate[]> = new Map();
56-
const {replacements} = await migration.migrate(globalMeta);
57-
const changesPerFile = groupReplacementsByFile(replacements);
58-
59-
for (const [file, changes] of changesPerFile) {
60-
if (!replacementsPerFile.has(file)) {
61-
replacementsPerFile.set(file, changes);
62-
}
63-
}
64-
65-
for (const [file, changes] of replacementsPerFile) {
66-
const recorder = tree.beginUpdate(file);
67-
for (const c of changes) {
68-
recorder
69-
.remove(c.data.position, c.data.end - c.data.position)
70-
.insertRight(c.data.position, c.data.toInsert);
71-
}
72-
tree.commitUpdate(recorder);
73-
}
74-
75-
const {
76-
counters: {removedImports, changedFiles},
77-
} = await migration.stats(globalMeta);
78-
let statsMessage: string;
79-
80-
if (removedImports === 0) {
81-
statsMessage = 'Schematic could not find unused imports in the project';
82-
} else {
83-
statsMessage =
84-
`Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` +
85-
`in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`;
86-
}
87-
88-
context.logger.info('');
89-
context.logger.info(statsMessage);
9043
};
9144
}

packages/core/schematics/ng-generate/output-migration/index.ts

+48-103
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Rule, SchematicsException} from '@angular-devkit/schematics';
10-
11-
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
12-
import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem';
13-
import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements';
14-
import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
15-
import {
16-
CompilationUnitData,
17-
OutputMigration,
18-
} from '../../migrations/output-migration/output-migration';
19-
import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge';
20-
import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units';
9+
import {Rule} from '@angular-devkit/schematics';
10+
import {OutputMigration} from '../../migrations/output-migration/output-migration';
11+
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
2112

2213
interface Options {
2314
path: string;
@@ -26,99 +17,53 @@ interface Options {
2617

2718
export function migrate(options: Options): Rule {
2819
return async (tree, context) => {
29-
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
30-
31-
if (!buildPaths.length && !testPaths.length) {
32-
throw new SchematicsException(
33-
'Could not find any tsconfig file. Cannot run output migration.',
34-
);
35-
}
36-
37-
const fs = new DevkitMigrationFilesystem(tree);
38-
setFileSystem(fs);
39-
40-
const migration = new OutputMigration({
41-
shouldMigrate: (_, file) => {
42-
return (
43-
file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
44-
!/(^|\/)node_modules\//.test(file.rootRelativePath)
45-
);
20+
await runMigrationInDevkit({
21+
tree,
22+
getMigration: (fs) =>
23+
new OutputMigration({
24+
shouldMigrate: (_, file) => {
25+
return (
26+
file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
27+
!/(^|\/)node_modules\//.test(file.rootRelativePath)
28+
);
29+
},
30+
}),
31+
beforeProgramCreation: (tsconfigPath) => {
32+
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
4633
},
47-
});
48-
49-
const analysisPath = fs.resolve(options.analysisDir);
50-
const unitResults: CompilationUnitData[] = [];
51-
const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => {
52-
context.logger.info(`Preparing analysis for: ${tsconfigPath}..`);
53-
54-
const baseInfo = migration.createProgram(tsconfigPath, fs);
55-
const info = migration.prepareProgram(baseInfo);
56-
57-
// Support restricting the analysis to subfolders for larger projects.
58-
if (analysisPath !== '/') {
59-
info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
60-
info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) =>
61-
sf.fileName.startsWith(analysisPath),
34+
afterProgramCreation: (info, fs) => {
35+
const analysisPath = fs.resolve(options.analysisDir);
36+
37+
// Support restricting the analysis to subfolders for larger projects.
38+
if (analysisPath !== '/') {
39+
info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
40+
info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) =>
41+
sf.fileName.startsWith(analysisPath),
42+
);
43+
}
44+
},
45+
beforeUnitAnalysis: (tsconfigPath) => {
46+
context.logger.info(`Scanning for outputs: ${tsconfigPath}...`);
47+
},
48+
afterAllAnalyzed: () => {
49+
context.logger.info(``);
50+
context.logger.info(`Processing analysis data between targets...`);
51+
context.logger.info(``);
52+
},
53+
afterAnalysisFailure: () => {
54+
context.logger.error('Migration failed unexpectedly with no analysis data');
55+
},
56+
whenDone: ({counters}) => {
57+
const {detectedOutputs, problematicOutputs, successRate} = counters;
58+
const migratedOutputs = detectedOutputs - problematicOutputs;
59+
const successRatePercent = (successRate * 100).toFixed(2);
60+
61+
context.logger.info('');
62+
context.logger.info(`Successfully migrated to outputs as functions 🎉`);
63+
context.logger.info(
64+
` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`,
6265
);
63-
}
64-
65-
return {info, tsconfigPath};
66+
},
6667
});
67-
68-
// Analyze phase. Treat all projects as compilation units as
69-
// this allows us to support references between those.
70-
for (const {info, tsconfigPath} of programInfos) {
71-
context.logger.info(`Scanning for outputs: ${tsconfigPath}..`);
72-
unitResults.push(await migration.analyze(info));
73-
}
74-
75-
context.logger.info(``);
76-
context.logger.info(`Processing analysis data between targets..`);
77-
context.logger.info(``);
78-
79-
const combined = await synchronouslyCombineUnitData(migration, unitResults);
80-
if (combined === null) {
81-
context.logger.error('Migration failed unexpectedly with no analysis data');
82-
return;
83-
}
84-
85-
const globalMeta = await migration.globalMeta(combined);
86-
const replacementsPerFile: Map<ProjectRootRelativePath, TextUpdate[]> = new Map();
87-
88-
for (const {info, tsconfigPath} of programInfos) {
89-
context.logger.info(`Migrating: ${tsconfigPath}..`);
90-
91-
const {replacements} = await migration.migrate(globalMeta);
92-
const changesPerFile = groupReplacementsByFile(replacements);
93-
94-
for (const [file, changes] of changesPerFile) {
95-
if (!replacementsPerFile.has(file)) {
96-
replacementsPerFile.set(file, changes);
97-
}
98-
}
99-
}
100-
101-
context.logger.info(`Applying changes..`);
102-
for (const [file, changes] of replacementsPerFile) {
103-
const recorder = tree.beginUpdate(file);
104-
for (const c of changes) {
105-
recorder
106-
.remove(c.data.position, c.data.end - c.data.position)
107-
.insertLeft(c.data.position, c.data.toInsert);
108-
}
109-
tree.commitUpdate(recorder);
110-
}
111-
112-
const {
113-
counters: {detectedOutputs, problematicOutputs, successRate},
114-
} = await migration.stats(globalMeta);
115-
const migratedOutputs = detectedOutputs - problematicOutputs;
116-
const successRatePercent = (successRate * 100).toFixed(2);
117-
118-
context.logger.info('');
119-
context.logger.info(`Successfully migrated to outputs as functions 🎉`);
120-
context.logger.info(
121-
` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`,
122-
);
12368
};
12469
}

0 commit comments

Comments
 (0)