refactor(core): run the control flow migration during ng update (#60492)

The migration had to be run manually before this change. It is now part of the update process when migrating to v20.

PR Close #60492
This commit is contained in:
Matthieu Riegler
2025-03-20 19:12:07 +01:00
committed by Jessica Janiuk
parent b407157ee8
commit e40b5c9ebd
18 changed files with 21 additions and 202 deletions
+2 -2
View File
@@ -692,7 +692,7 @@ export class NgSwitch {
static ɵfac: i0.ɵɵFactoryDeclaration<NgSwitch, never>;
}
// @public
// @public @deprecated
export class NgSwitchCase implements DoCheck {
constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>, ngSwitch: NgSwitch);
ngDoCheck(): void;
@@ -703,7 +703,7 @@ export class NgSwitchCase implements DoCheck {
static ɵfac: i0.ɵɵFactoryDeclaration<NgSwitchCase, [null, null, { optional: true; host: true; }]>;
}
// @public
// @public @deprecated
export class NgSwitchDefault {
constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<Object>, ngSwitch: NgSwitch);
// (undocumented)
+2 -3
View File
@@ -14,7 +14,6 @@ pkg_npm(
"migrations.json",
"package.json",
"//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files",
"//packages/core/schematics/ng-generate/control-flow-migration:static_files",
"//packages/core/schematics/ng-generate/inject-migration:static_files",
"//packages/core/schematics/ng-generate/output-migration:static_files",
"//packages/core/schematics/ng-generate/route-lazy-loading:static_files",
@@ -35,7 +34,6 @@ rollup_bundle(
name = "bundles",
config_file = ":rollup.config.js",
entry_points = {
"//packages/core/schematics/ng-generate/control-flow-migration:index.ts": "control-flow-migration",
"//packages/core/schematics/ng-generate/inject-migration:index.ts": "inject-migration",
"//packages/core/schematics/ng-generate/route-lazy-loading:index.ts": "route-lazy-loading",
"//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration",
@@ -47,6 +45,7 @@ rollup_bundle(
"//packages/core/schematics/ng-generate/self-closing-tags-migration:index.ts": "self-closing-tags-migration",
"//packages/core/schematics/migrations/inject-flags:index.ts": "inject-flags",
"//packages/core/schematics/migrations/test-bed-get:index.ts": "test-bed-get",
"//packages/core/schematics/migrations/control-flow-migration:index.ts": "control-flow-migration",
},
format = "cjs",
link_workspace_root = True,
@@ -56,10 +55,10 @@ rollup_bundle(
"//packages/core/schematics/test:__pkg__",
],
deps = [
"//packages/core/schematics/migrations/control-flow-migration",
"//packages/core/schematics/migrations/inject-flags",
"//packages/core/schematics/migrations/test-bed-get",
"//packages/core/schematics/ng-generate/cleanup-unused-imports",
"//packages/core/schematics/ng-generate/control-flow-migration",
"//packages/core/schematics/ng-generate/inject-migration",
"//packages/core/schematics/ng-generate/output-migration",
"//packages/core/schematics/ng-generate/route-lazy-loading",
-6
View File
@@ -6,12 +6,6 @@
"schema": "./ng-generate/standalone-migration/schema.json",
"aliases": ["standalone"]
},
"control-flow-migration": {
"description": "Converts the entire application to block control flow syntax",
"factory": "./bundles/control-flow-migration#migrate",
"schema": "./ng-generate/control-flow-migration/schema.json",
"aliases": ["control-flow"]
},
"inject-migration": {
"description": "Converts usages of constructor-based injection to the inject() function",
"factory": "./bundles/inject-migration#migrate",
+6
View File
@@ -9,6 +9,12 @@
"version": "20.0.0",
"description": "Replaces usages of the deprecated TestBed.get method with TestBed.inject",
"factory": "./bundles/test-bed-get#migrate"
},
"control-flow-migration": {
"version": "20.0.0",
"description": "Converts the entire application to block control flow syntax",
"factory": "./bundles/control-flow-migration#migrate",
"optional": true
}
}
}
@@ -7,45 +7,31 @@
*/
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
import {join, relative} from 'path';
import {relative} from 'path';
import {normalizePath} from '../../utils/change_tracker';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
import {migrateTemplate} from './migration';
import {AnalyzedFile, MigrateError} from './types';
import {analyze} from './util';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
interface Options {
path: string;
format: boolean;
}
export function migrate(options: Options): Rule {
export function migrate(): Rule {
return async (tree: Tree, context: SchematicContext) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const pathToMigrate = normalizePath(join(basePath, options.path));
let allPaths = [];
if (pathToMigrate.trim() !== '') {
allPaths.push(pathToMigrate);
}
const allPaths = [...buildPaths, ...testPaths];
if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot run the control flow migration.',
'Could not find any tsconfig file. Cannot run the http providers migration.',
);
}
let errors: string[] = [];
for (const tsconfigPath of allPaths) {
const migrateErrors = runControlFlowMigration(
tree,
tsconfigPath,
basePath,
pathToMigrate,
options,
);
const migrateErrors = runControlFlowMigration(tree, tsconfigPath, basePath);
errors = [...errors, ...migrateErrors];
}
@@ -58,33 +44,11 @@ export function migrate(options: Options): Rule {
};
}
function runControlFlowMigration(
tree: Tree,
tsconfigPath: string,
basePath: string,
pathToMigrate: string,
schematicOptions: Options,
): string[] {
if (schematicOptions.path.startsWith('..')) {
throw new SchematicsException(
'Cannot run control flow migration outside of the current project.',
);
}
function runControlFlowMigration(tree: Tree, tsconfigPath: string, basePath: string): string[] {
const program = createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter(
(sourceFile) =>
sourceFile.fileName.startsWith(pathToMigrate) &&
canMigrateFile(basePath, sourceFile, program),
);
if (sourceFiles.length === 0) {
throw new SchematicsException(
`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the control flow migration.`,
);
}
.filter((sourceFile) => canMigrateFile(basePath, sourceFile, program));
const analysis = new Map<string, AnalyzedFile>();
const migrateErrors = new Map<string, MigrateError[]>();
@@ -108,14 +72,7 @@ function runControlFlowMigration(
const template = content.slice(start, end);
const length = (end ?? content.length) - start;
const {migrated, errors} = migrateTemplate(
template,
type,
node,
file,
schematicOptions.format,
analysis,
);
const {migrated, errors} = migrateTemplate(template, type, node, file, true, analysis);
if (migrated !== null) {
update.remove(start, length);
@@ -22,7 +22,6 @@ jasmine_node_test(
"//packages/core/schematics:collection.json",
"//packages/core/schematics:migrations.json",
"//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files",
"//packages/core/schematics/ng-generate/control-flow-migration:static_files",
"//packages/core/schematics/ng-generate/inject-migration:static_files",
"//packages/core/schematics/ng-generate/output-migration:static_files",
"//packages/core/schematics/ng-generate/route-lazy-loading:static_files",
@@ -31,7 +31,7 @@ describe('control flow migration', () => {
}
beforeEach(() => {
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../collection.json'));
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json'));
host = new TempScopedNodeJsSyncHost();
tree = new UnitTestTree(new HostTree(host));
@@ -67,100 +67,6 @@ describe('control flow migration', () => {
shx.rm('-r', tmpDirPath);
});
describe('path', () => {
it('should throw an error if no files match the passed-in path', async () => {
let error: string | null = null;
writeFile(
'dir.ts',
`
import {Directive} from '@angular/core';
@Directive({selector: '[dir]'})
export class MyDir {}
`,
);
try {
await runMigration('./foo');
} catch (e: any) {
error = e.message;
}
expect(error).toMatch(
/Could not find any files to migrate under the path .*\/foo\. Cannot run the control flow migration/,
);
});
it('should throw an error if a path outside of the project is passed in', async () => {
let error: string | null = null;
writeFile(
'dir.ts',
`
import {Directive} from '@angular/core';
@Directive({selector: '[dir]'})
export class MyDir {}
`,
);
try {
await runMigration('../foo');
} catch (e: any) {
error = e.message;
}
expect(error).toBe('Cannot run control flow migration outside of the current project.');
});
it('should only migrate the paths that were passed in', async () => {
writeFile(
'comp.ts',
`
import {Component} from '@angular/core';
import {NgIf} from '@angular/common';
@Component({
imports: [NgIf, NgFor,NgSwitch,NgSwitchCase ,NgSwitchDefault],
template: \`<div><span *ngIf="toggle">This should be hidden</span></div>\`
})
class Comp {
toggle = false;
}
`,
);
writeFile(
'skip.ts',
`
import {Component} from '@angular/core';
import {NgIf} from '@angular/common';
@Component({
imports: [NgIf],
template: \`<div *ngIf="show">Show me</div>\`
})
class Comp {
show = false;
}
`,
);
await runMigration('./comp.ts');
const migratedContent = tree.readContent('/comp.ts');
const skippedContent = tree.readContent('/skip.ts');
expect(migratedContent).toContain(
'template: `<div>@if (toggle) {<span>This should be hidden</span>}</div>`',
);
expect(migratedContent).toContain('imports: []');
expect(migratedContent).not.toContain(`import {NgIf} from '@angular/common';`);
expect(skippedContent).toContain('template: `<div *ngIf="show">Show me</div>`');
expect(skippedContent).toContain('imports: [NgIf]');
expect(skippedContent).toContain(`import {NgIf} from '@angular/common';`);
});
});
describe('ngIf', () => {
it('should migrate an inline template', async () => {
writeFile(
@@ -5596,48 +5502,6 @@ describe('control flow migration', () => {
expect(actual).toBe(expected);
});
it('should migrate an if else case and not format', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
import {NgIf} from '@angular/common';
@Component({
templateUrl: './comp.html'
})
class Comp {
show = false;
}
`,
);
writeFile(
'/comp.html',
[
`<div>`,
`<span *ngIf="show;else elseBlock">Content here</span>`,
`<ng-template #elseBlock>Else Content</ng-template>`,
`</div>`,
].join('\n'),
);
await runMigration(undefined, false);
const content = tree.readContent('/comp.html');
expect(content).toBe(
[
`<div>`,
`@if (show) {`,
`<span>Content here</span>`,
`} @else {`,
`Else Content`,
`}\n`,
`</div>`,
].join('\n'),
);
});
it('should ignore formatting on i18n sections', async () => {
writeFile(
'/comp.ts',