Skip to content

Commit 0ea831c

Browse files
JeanMechekirjs
authored andcommitted
refactor(migrations): keep the control flow migration as ng generate. (#61773)
this way `ng generate @angular/core:control-flow` which has been fairly documented, remains valid. PR Close #61773
1 parent 7c4f74a commit 0ea831c

File tree

5 files changed

+253
-10
lines changed

5 files changed

+253
-10
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pkg_npm(
3232
srcs = [
3333
"collection.json",
3434
"migrations.json",
35+
"//packages/core/schematics/migrations/control-flow-migration:static_files",
3536
"//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files",
3637
"//packages/core/schematics/ng-generate/inject-migration:static_files",
3738
"//packages/core/schematics/ng-generate/output-migration:static_files",

packages/core/schematics/collection.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
"factory": "./bundles/self-closing-tags-migration.cjs#migrate",
5252
"schema": "./ng-generate/self-closing-tags-migration/schema.json",
5353
"aliases": ["self-closing-tag"]
54+
},
55+
"control-flow-migration": {
56+
"description": "Converts the entire application to block control flow syntax",
57+
"factory": "./bundles/control-flow-migration.cjs#migrate",
58+
"schema": "./migrations/control-flow-migration/schema.json",
59+
"aliases": ["control-flow"]
5460
}
5561
}
5662
}

packages/core/schematics/migrations/control-flow-migration/index.ts

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,35 @@
77
*/
88

99
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
10-
import {relative} from 'path';
10+
import {join, relative} from 'path';
1111

1212
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
1313

1414
import {migrateTemplate} from './migration';
1515
import {AnalyzedFile, MigrateError} from './types';
1616
import {analyze} from './util';
1717
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
18+
import {normalizePath} from '../../utils/change_tracker';
1819

19-
export function migrate(): Rule {
20+
interface Options {
21+
path?: string;
22+
format: boolean;
23+
}
24+
25+
export function migrate(options: Options): Rule {
2026
return async (tree: Tree, context: SchematicContext) => {
21-
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
27+
let allPaths = [];
2228
const basePath = process.cwd();
23-
const allPaths = [...buildPaths, ...testPaths];
29+
let pathToMigrate: string | undefined;
30+
if (options.path) {
31+
pathToMigrate = normalizePath(join(basePath, options.path));
32+
if (pathToMigrate.trim() !== '') {
33+
allPaths.push(pathToMigrate);
34+
}
35+
} else {
36+
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
37+
allPaths = [...buildPaths, ...testPaths];
38+
}
2439

2540
if (!allPaths.length) {
2641
throw new SchematicsException(
@@ -31,7 +46,13 @@ export function migrate(): Rule {
3146
let errors: string[] = [];
3247

3348
for (const tsconfigPath of allPaths) {
34-
const migrateErrors = runControlFlowMigration(tree, tsconfigPath, basePath);
49+
const migrateErrors = runControlFlowMigration(
50+
tree,
51+
tsconfigPath,
52+
basePath,
53+
pathToMigrate,
54+
options,
55+
);
3556
errors = [...errors, ...migrateErrors];
3657
}
3758

@@ -44,12 +65,33 @@ export function migrate(): Rule {
4465
};
4566
}
4667

47-
function runControlFlowMigration(tree: Tree, tsconfigPath: string, basePath: string): string[] {
68+
function runControlFlowMigration(
69+
tree: Tree,
70+
tsconfigPath: string,
71+
basePath: string,
72+
pathToMigrate?: string,
73+
schematicOptions?: Options,
74+
): string[] {
75+
if (schematicOptions?.path?.startsWith('..')) {
76+
throw new SchematicsException(
77+
'Cannot run control flow migration outside of the current project.',
78+
);
79+
}
80+
4881
const program = createMigrationProgram(tree, tsconfigPath, basePath);
4982
const sourceFiles = program
5083
.getSourceFiles()
51-
.filter((sourceFile) => canMigrateFile(basePath, sourceFile, program));
52-
84+
.filter(
85+
(sourceFile) =>
86+
(pathToMigrate ? sourceFile.fileName.startsWith(pathToMigrate) : true) &&
87+
canMigrateFile(basePath, sourceFile, program),
88+
);
89+
90+
if (sourceFiles.length === 0) {
91+
throw new SchematicsException(
92+
`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the control flow migration.`,
93+
);
94+
}
5395
const analysis = new Map<string, AnalyzedFile>();
5496
const migrateErrors = new Map<string, MigrateError[]>();
5597

@@ -72,7 +114,14 @@ function runControlFlowMigration(tree: Tree, tsconfigPath: string, basePath: str
72114
const template = content.slice(start, end);
73115
const length = (end ?? content.length) - start;
74116

75-
const {migrated, errors} = migrateTemplate(template, type, node, file, true, analysis);
117+
const {migrated, errors} = migrateTemplate(
118+
template,
119+
type,
120+
node,
121+
file,
122+
schematicOptions?.format ?? true,
123+
analysis,
124+
);
76125

77126
if (migrated !== null) {
78127
update.remove(start, length);

packages/core/schematics/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jasmine_node_test(
2424
"//packages/core/schematics:bundles",
2525
"//packages/core/schematics:collection.json",
2626
"//packages/core/schematics:migrations.json",
27+
"//packages/core/schematics/migrations/control-flow-migration:static_files",
2728
"//packages/core/schematics/ng-generate/cleanup-unused-imports:static_files",
2829
"//packages/core/schematics/ng-generate/inject-migration:static_files",
2930
"//packages/core/schematics/ng-generate/output-migration:static_files",

packages/core/schematics/test/control_flow_migration_spec.ts

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/test
1313
import {runfiles} from '@bazel/runfiles';
1414
import shx from 'shelljs';
1515

16-
describe('control flow migration', () => {
16+
describe('control flow migration (ng update)', () => {
1717
let runner: SchematicTestRunner;
1818
let host: TempScopedNodeJsSyncHost;
1919
let tree: UnitTestTree;
@@ -6831,3 +6831,189 @@ describe('control flow migration', () => {
68316831
});
68326832
});
68336833
});
6834+
6835+
describe('control flow migration (ng generate)', () => {
6836+
let runner: SchematicTestRunner;
6837+
let host: TempScopedNodeJsSyncHost;
6838+
let tree: UnitTestTree;
6839+
let tmpDirPath: string;
6840+
let previousWorkingDir: string;
6841+
let errorOutput: string[] = [];
6842+
let warnOutput: string[] = [];
6843+
6844+
function writeFile(filePath: string, contents: string) {
6845+
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
6846+
}
6847+
6848+
function runMigration(path: string | undefined = undefined, format: boolean = true) {
6849+
return runner.runSchematic('control-flow-migration', {path, format}, tree);
6850+
}
6851+
6852+
beforeEach(() => {
6853+
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../collection.json'));
6854+
host = new TempScopedNodeJsSyncHost();
6855+
tree = new UnitTestTree(new HostTree(host));
6856+
6857+
errorOutput = [];
6858+
warnOutput = [];
6859+
runner.logger.subscribe((e: logging.LogEntry) => {
6860+
if (e.level === 'error') {
6861+
errorOutput.push(e.message);
6862+
} else if (e.level === 'warn') {
6863+
warnOutput.push(e.message);
6864+
}
6865+
});
6866+
6867+
writeFile('/tsconfig.json', '{}');
6868+
writeFile(
6869+
'/angular.json',
6870+
JSON.stringify({
6871+
version: 1,
6872+
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
6873+
}),
6874+
);
6875+
6876+
previousWorkingDir = shx.pwd();
6877+
tmpDirPath = getSystemPath(host.root);
6878+
6879+
// Switch into the temporary directory path. This allows us to run
6880+
// the schematic against our custom unit test tree.
6881+
shx.cd(tmpDirPath);
6882+
});
6883+
6884+
afterEach(() => {
6885+
shx.cd(previousWorkingDir);
6886+
shx.rm('-r', tmpDirPath);
6887+
});
6888+
6889+
describe('path', () => {
6890+
it('should throw an error if no files match the passed-in path', async () => {
6891+
let error: string | null = null;
6892+
6893+
writeFile(
6894+
'dir.ts',
6895+
`
6896+
import {Directive} from '@angular/core';
6897+
@Directive({selector: '[dir]'})
6898+
export class MyDir {}
6899+
`,
6900+
);
6901+
6902+
try {
6903+
await runMigration('./foo');
6904+
} catch (e: any) {
6905+
error = e.message;
6906+
}
6907+
6908+
expect(error).toMatch(
6909+
/Could not find any files to migrate under the path .*\/foo\. Cannot run the control flow migration/,
6910+
);
6911+
});
6912+
6913+
it('should throw an error if a path outside of the project is passed in', async () => {
6914+
let error: string | null = null;
6915+
6916+
writeFile(
6917+
'dir.ts',
6918+
`
6919+
import {Directive} from '@angular/core';
6920+
@Directive({selector: '[dir]'})
6921+
export class MyDir {}
6922+
`,
6923+
);
6924+
6925+
try {
6926+
await runMigration('../foo');
6927+
} catch (e: any) {
6928+
error = e.message;
6929+
}
6930+
expect(error).toBe('Cannot run control flow migration outside of the current project.');
6931+
});
6932+
6933+
it('should only migrate the paths that were passed in', async () => {
6934+
writeFile(
6935+
'comp.ts',
6936+
`
6937+
import {Component} from '@angular/core';
6938+
import {NgIf} from '@angular/common';
6939+
@Component({
6940+
imports: [NgIf, NgFor,NgSwitch,NgSwitchCase ,NgSwitchDefault],
6941+
template: \`<div><span *ngIf="toggle">This should be hidden</span></div>\`
6942+
})
6943+
class Comp {
6944+
toggle = false;
6945+
}
6946+
`,
6947+
);
6948+
6949+
writeFile(
6950+
'skip.ts',
6951+
`
6952+
import {Component} from '@angular/core';
6953+
import {NgIf} from '@angular/common';
6954+
@Component({
6955+
imports: [NgIf],
6956+
template: \`<div *ngIf="show">Show me</div>\`
6957+
})
6958+
class Comp {
6959+
show = false;
6960+
}
6961+
`,
6962+
);
6963+
6964+
await runMigration('./comp.ts');
6965+
const migratedContent = tree.readContent('/comp.ts');
6966+
const skippedContent = tree.readContent('/skip.ts');
6967+
6968+
expect(migratedContent).toContain(
6969+
'template: `<div>@if (toggle) {<span>This should be hidden</span>}</div>`',
6970+
);
6971+
expect(migratedContent).toContain('imports: []');
6972+
expect(migratedContent).not.toContain(`import {NgIf} from '@angular/common';`);
6973+
expect(skippedContent).toContain('template: `<div *ngIf="show">Show me</div>`');
6974+
expect(skippedContent).toContain('imports: [NgIf]');
6975+
expect(skippedContent).toContain(`import {NgIf} from '@angular/common';`);
6976+
});
6977+
});
6978+
6979+
it('should migrate an if else case and not format', async () => {
6980+
writeFile(
6981+
'/comp.ts',
6982+
`
6983+
import {Component} from '@angular/core';
6984+
import {NgIf} from '@angular/common';
6985+
@Component({
6986+
templateUrl: './comp.html'
6987+
})
6988+
class Comp {
6989+
show = false;
6990+
}
6991+
`,
6992+
);
6993+
6994+
writeFile(
6995+
'/comp.html',
6996+
[
6997+
`<div>`,
6998+
`<span *ngIf="show;else elseBlock">Content here</span>`,
6999+
`<ng-template #elseBlock>Else Content</ng-template>`,
7000+
`</div>`,
7001+
].join('\n'),
7002+
);
7003+
7004+
await runMigration(undefined, false);
7005+
const content = tree.readContent('/comp.html');
7006+
7007+
expect(content).toBe(
7008+
[
7009+
`<div>`,
7010+
`@if (show) {`,
7011+
`<span>Content here</span>`,
7012+
`} @else {`,
7013+
`Else Content`,
7014+
`}\n`,
7015+
`</div>`,
7016+
].join('\n'),
7017+
);
7018+
});
7019+
});

0 commit comments

Comments
 (0)