Skip to content

Commit dff4de0

Browse files
crisbetodevversion
authored andcommitted
feat(migrations): add a combined migration for all signals APIs (#58259)
Adds a combined `@angular/core:signals` migration that combines all of the signals-related migrations into one for the apps that want to do it all in one go. All of the heavy-lifting was already done by the individual migrations, these changes only chain them together for a more convenient developer experience. PR Close #58259
1 parent b542f15 commit dff4de0

File tree

10 files changed

+285
-0
lines changed

10 files changed

+285
-0
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pkg_npm(
1818
"//packages/core/schematics/ng-generate/route-lazy-loading:static_files",
1919
"//packages/core/schematics/ng-generate/signal-input-migration:static_files",
2020
"//packages/core/schematics/ng-generate/signal-queries-migration:static_files",
21+
"//packages/core/schematics/ng-generate/signals:static_files",
2122
"//packages/core/schematics/ng-generate/standalone-migration:static_files",
2223
],
2324
validate = False,
@@ -35,6 +36,7 @@ rollup_bundle(
3536
"//packages/core/schematics/ng-generate/inject-migration:index.ts": "inject-migration",
3637
"//packages/core/schematics/ng-generate/route-lazy-loading:index.ts": "route-lazy-loading",
3738
"//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration",
39+
"//packages/core/schematics/ng-generate/signals:index.ts": "signals",
3840
"//packages/core/schematics/ng-generate/signal-input-migration:index.ts": "signal-input-migration",
3941
"//packages/core/schematics/ng-generate/signal-queries-migration:index.ts": "signal-queries-migration",
4042
"//packages/core/schematics/migrations/explicit-standalone-flag:index.ts": "explicit-standalone-flag",
@@ -55,6 +57,7 @@ rollup_bundle(
5557
"//packages/core/schematics/ng-generate/route-lazy-loading",
5658
"//packages/core/schematics/ng-generate/signal-input-migration",
5759
"//packages/core/schematics/ng-generate/signal-queries-migration",
60+
"//packages/core/schematics/ng-generate/signals",
5861
"//packages/core/schematics/ng-generate/standalone-migration",
5962
"@npm//@rollup/plugin-commonjs",
6063
"@npm//@rollup/plugin-node-resolve",

packages/core/schematics/collection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
"factory": "./bundles/signal-queries-migration#migrate",
3636
"schema": "./ng-generate/signal-queries-migration/schema.json",
3737
"aliases": ["signal-queries", "signal-query", "signal-query-migration"]
38+
},
39+
"signals": {
40+
"description": "Combines all signals-related migrations into a single migration",
41+
"factory": "./bundles/signals#migrate",
42+
"schema": "./ng-generate/signals/schema.json"
3843
}
3944
}
4045
}

packages/core/schematics/ng-generate/signal-input-migration/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package(
44
default_visibility = [
55
"//packages/core/schematics:__pkg__",
66
"//packages/core/schematics/migrations/google3:__pkg__",
7+
"//packages/core/schematics/ng-generate/signals:__pkg__",
78
"//packages/core/schematics/test:__pkg__",
89
],
910
)

packages/core/schematics/ng-generate/signal-queries-migration/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package(
44
default_visibility = [
55
"//packages/core/schematics:__pkg__",
66
"//packages/core/schematics/migrations/google3:__pkg__",
7+
"//packages/core/schematics/ng-generate/signals:__pkg__",
78
"//packages/core/schematics/test:__pkg__",
89
],
910
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/migrations/google3:__pkg__",
7+
"//packages/core/schematics/test:__pkg__",
8+
],
9+
)
10+
11+
filegroup(
12+
name = "static_files",
13+
srcs = ["schema.json"],
14+
)
15+
16+
ts_library(
17+
name = "signals",
18+
srcs = glob(["**/*.ts"]),
19+
tsconfig = "//packages/core/schematics:tsconfig.json",
20+
deps = [
21+
"//packages/core/schematics/ng-generate/signal-input-migration",
22+
"//packages/core/schematics/ng-generate/signal-queries-migration",
23+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
24+
"@npm//@angular-devkit/schematics",
25+
"@npm//@types/node",
26+
],
27+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Combined signals migration
2+
3+
Combines all signal-related migrations into a single migration. It includes the following migrations:
4+
* Converting `@Input` to the signal-based `input`. [See documentation](https://github.com/angular/angular/blob/main/packages/core/schematics/ng-generate/signal-input-migration/README.md).
5+
* Converting `@ViewChild`/`@ViewChildren` and `@ContentChild`/`@ContentChildren` to
6+
`viewChild`/`viewChildren` and `contentChild`/`contentChildren`. [See documentation](https://github.com/angular/angular/blob/main/packages/core/schematics/ng-generate/signal-input-migration/README.md).
7+
8+
The primary use case for this migration is to offer developers interested in switching to signals a
9+
single entrypoint from which they can do so.
10+
11+
## How to run this migration?
12+
13+
The migration can be run using the following command:
14+
15+
```bash
16+
ng generate @angular/core:signals
17+
```
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {chain, Rule, SchematicsException} from '@angular-devkit/schematics';
10+
import {migrate as toSignalQueries} from '../signal-queries-migration';
11+
import {migrate as toSignalInputs} from '../signal-input-migration';
12+
13+
const enum SupportedMigrations {
14+
inputs = 'inputs',
15+
queries = 'queries',
16+
}
17+
18+
interface Options {
19+
path: string;
20+
migrations: SupportedMigrations[];
21+
analysisDir: string;
22+
bestEffortMode?: boolean;
23+
insertTodos?: boolean;
24+
}
25+
26+
export function migrate(options: Options): Rule {
27+
// The migrations are independent so we can run them in any order, but we sort them here
28+
// alphabetically so we get a consistent execution order in case of issue reports.
29+
const migrations = options.migrations.slice().sort();
30+
const rules: Rule[] = [];
31+
32+
for (const migration of migrations) {
33+
switch (migration) {
34+
case SupportedMigrations.inputs:
35+
rules.push(toSignalInputs(options));
36+
break;
37+
38+
case SupportedMigrations.queries:
39+
rules.push(toSignalQueries(options));
40+
break;
41+
42+
default:
43+
throw new SchematicsException(`Unsupported migration "${migration}"`);
44+
}
45+
}
46+
47+
return chain(rules);
48+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "AngularSignalMigration",
4+
"title": "Angular Signals migration",
5+
"type": "object",
6+
"properties": {
7+
"migrations": {
8+
"type": "array",
9+
"default": [
10+
"inputs",
11+
"queries"
12+
],
13+
"items": {
14+
"type": "string",
15+
"enum": [
16+
"inputs",
17+
"queries"
18+
]
19+
},
20+
"description": "Signals-related migrations that should be run",
21+
"x-prompt": {
22+
"message": "Which migrations do you want to run?",
23+
"type": "list",
24+
"multiselect": true,
25+
"items": [
26+
{
27+
"value": "inputs",
28+
"label": "Convert `@Input` to the signal-based `input`"
29+
},
30+
{
31+
"value": "queries",
32+
"label": "Convert `@ViewChild`/`@ViewChildren` and `@ContentChild`/`@ContentChildren` to the signal-based `viewChild`/`viewChildren` and `contentChild`/`contentChildren`"
33+
}
34+
]
35+
}
36+
},
37+
"path": {
38+
"type": "string",
39+
"description": "Path to the directory that should be migrated.",
40+
"x-prompt": "Which directory do you want to migrate?",
41+
"default": "./"
42+
},
43+
"analysisDir": {
44+
"type": "string",
45+
"description": "Path to the directory that should be analyzed. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
46+
"default": "./"
47+
},
48+
"bestEffortMode": {
49+
"type": "boolean",
50+
"description": "Whether to eagerly migrate as much as possible, ignoring problematic patterns that would otherwise prevent migration.",
51+
"x-prompt": "Do you want to migrate as much as possible, even if it may break your build?",
52+
"default": false
53+
},
54+
"insertTodos": {
55+
"type": "boolean",
56+
"description": "Whether the migration should add TODOs for code that could not be migrated",
57+
"default": false
58+
}
59+
}
60+
}

packages/core/schematics/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jasmine_node_test(
2525
"//packages/core/schematics/ng-generate/inject-migration:static_files",
2626
"//packages/core/schematics/ng-generate/route-lazy-loading:static_files",
2727
"//packages/core/schematics/ng-generate/signal-queries-migration:static_files",
28+
"//packages/core/schematics/ng-generate/signals:static_files",
2829
"//packages/core/schematics/ng-generate/standalone-migration:static_files",
2930
],
3031
deps = [
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
10+
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
11+
import {HostTree} from '@angular-devkit/schematics';
12+
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
13+
import {runfiles} from '@bazel/runfiles';
14+
import shx from 'shelljs';
15+
16+
describe('combined signals migration', () => {
17+
let runner: SchematicTestRunner;
18+
let host: TempScopedNodeJsSyncHost;
19+
let tree: UnitTestTree;
20+
let tmpDirPath: string;
21+
let previousWorkingDir: string;
22+
23+
function writeFile(filePath: string, contents: string) {
24+
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
25+
}
26+
27+
function runMigration(migrations: string[]) {
28+
return runner.runSchematic('signals', {migrations}, tree);
29+
}
30+
31+
function stripWhitespace(value: string) {
32+
return value.replace(/\s/g, '');
33+
}
34+
35+
beforeEach(() => {
36+
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../collection.json'));
37+
host = new TempScopedNodeJsSyncHost();
38+
tree = new UnitTestTree(new HostTree(host));
39+
40+
writeFile('/tsconfig.json', '{}');
41+
writeFile(
42+
'/angular.json',
43+
JSON.stringify({
44+
version: 1,
45+
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
46+
}),
47+
);
48+
49+
previousWorkingDir = shx.pwd();
50+
tmpDirPath = getSystemPath(host.root);
51+
shx.cd(tmpDirPath);
52+
});
53+
54+
afterEach(() => {
55+
shx.cd(previousWorkingDir);
56+
shx.rm('-r', tmpDirPath);
57+
});
58+
59+
it('should be able to run multiple migrations at the same time', async () => {
60+
writeFile(
61+
'/index.ts',
62+
`
63+
import {ContentChild, Input, ElementRef, Component} from '@angular/core';
64+
65+
@Component({
66+
template: 'The value is {{value}}',
67+
})
68+
export class SomeComponent {
69+
@ContentChild('ref') ref!: ElementRef;
70+
@Input('alias') value: string = 'initial';
71+
}`,
72+
);
73+
74+
await runMigration(['inputs', 'queries']);
75+
76+
expect(stripWhitespace(tree.readContent('/index.ts'))).toBe(
77+
stripWhitespace(`
78+
import {ElementRef, Component, input, contentChild} from '@angular/core';
79+
80+
@Component({
81+
template: 'The value is {{value()}}',
82+
})
83+
export class SomeComponent {
84+
readonly ref = contentChild.required<ElementRef>('ref');
85+
readonly value = input<string>('initial', { alias: "alias" });
86+
}
87+
`),
88+
);
89+
});
90+
91+
it('should be able to run only specific migrations', async () => {
92+
writeFile(
93+
'/index.ts',
94+
`
95+
import {ContentChild, Input, ElementRef, Component} from '@angular/core';
96+
97+
@Component({
98+
template: 'The value is {{value}}',
99+
})
100+
export class SomeComponent {
101+
@ContentChild('ref') ref!: ElementRef;
102+
@Input('alias') value: string = 'initial';
103+
}`,
104+
);
105+
106+
await runMigration(['queries']);
107+
108+
expect(stripWhitespace(tree.readContent('/index.ts'))).toBe(
109+
stripWhitespace(`
110+
import {Input, ElementRef, Component, contentChild} from '@angular/core';
111+
112+
@Component({
113+
template: 'The value is {{value}}',
114+
})
115+
export class SomeComponent {
116+
readonly ref = contentChild.required<ElementRef>('ref');
117+
@Input('alias') value: string = 'initial';
118+
}
119+
`),
120+
);
121+
});
122+
});

0 commit comments

Comments
 (0)