Skip to content

Commit b6bc938

Browse files
committed
feat(migrations): add schematic to migrate to signal queries (angular#58032)
This commit adds an automated `ng generate` schematic/migration for converting decorator queries to signal queries, as good as possible. PR Close angular#58032
1 parent c8c35d2 commit b6bc938

File tree

11 files changed

+366
-10
lines changed

11 files changed

+366
-10
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pkg_npm(
1717
"//packages/core/schematics/ng-generate/inject-migration:static_files",
1818
"//packages/core/schematics/ng-generate/route-lazy-loading:static_files",
1919
"//packages/core/schematics/ng-generate/signal-input-migration:static_files",
20+
"//packages/core/schematics/ng-generate/signal-queries-migration:static_files",
2021
"//packages/core/schematics/ng-generate/standalone-migration:static_files",
2122
],
2223
validate = False,
@@ -35,6 +36,7 @@ rollup_bundle(
3536
"//packages/core/schematics/ng-generate/route-lazy-loading:index.ts": "route-lazy-loading",
3637
"//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration",
3738
"//packages/core/schematics/ng-generate/signal-input-migration:index.ts": "signal-input-migration",
39+
"//packages/core/schematics/ng-generate/signal-queries-migration:index.ts": "signal-queries-migration",
3840
"//packages/core/schematics/migrations/explicit-standalone-flag:index.ts": "explicit-standalone-flag",
3941
"//packages/core/schematics/migrations/pending-tasks:index.ts": "pending-tasks",
4042
},
@@ -52,6 +54,7 @@ rollup_bundle(
5254
"//packages/core/schematics/ng-generate/inject-migration",
5355
"//packages/core/schematics/ng-generate/route-lazy-loading",
5456
"//packages/core/schematics/ng-generate/signal-input-migration",
57+
"//packages/core/schematics/ng-generate/signal-queries-migration",
5558
"//packages/core/schematics/ng-generate/standalone-migration",
5659
"@npm//@rollup/plugin-commonjs",
5760
"@npm//@rollup/plugin-node-resolve",

packages/core/schematics/collection.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
"factory": "./bundles/signal-input-migration#migrate",
3030
"schema": "./ng-generate/signal-input-migration/schema.json",
3131
"aliases": ["signal-inputs", "signal-input"]
32+
},
33+
"signal-queries-migration": {
34+
"description": "Updates query declarations to signal queries, while also migrating all relevant references.",
35+
"factory": "./bundles/signal-queries-migration#migrate",
36+
"schema": "./ng-generate/signal-queries-migration/schema.json",
37+
"aliases": ["signal-queries", "signal-query", "signal-query-migration"]
3238
}
3339
}
3440
}

packages/core/schematics/migrations/signal-queries-migration/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ ts_library(
66
["**/*.ts"],
77
exclude = ["*.spec.ts"],
88
),
9+
visibility = ["//packages/core/schematics/ng-generate/signal-queries-migration:__pkg__"],
910
deps = [
1011
"//packages/compiler",
1112
"//packages/compiler-cli",

packages/core/schematics/migrations/signal-queries-migration/migration.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
confirmAsSerializable,
1616
MigrationStats,
1717
ProgramInfo,
18+
projectFile,
1819
Replacement,
1920
Serializable,
2021
TsurgeComplexMigration,
@@ -47,9 +48,7 @@ import {removeQueryListToArrayCall} from './fn_to_array_removal';
4748
import {replaceQueryListGetCall} from './fn_get_replacement';
4849
import {checkForIncompatibleQueryListAccesses} from './incompatible_query_list_fns';
4950
import {replaceQueryListFirstAndLastReferences} from './fn_first_last_replacement';
50-
51-
// TODO: Consider re-using inheritance logic from input migration
52-
// TODO: Consider re-using problematic pattern recognition logic from input migration
51+
import {MigrationConfig} from './migration_config';
5352

5453
export interface CompilationUnitData {
5554
knownQueryFields: Record<ClassFieldUniqueKey, {fieldName: string; isMulti: boolean}>;
@@ -74,13 +73,17 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
7473
CompilationUnitData,
7574
GlobalUnitData
7675
> {
76+
constructor(private readonly config: MigrationConfig = {}) {
77+
super();
78+
}
79+
7780
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
7881
assert(info.ngCompiler !== null, 'Expected queries migration to have an Angular program.');
7982
// TODO: This stage for this migration doesn't necessarily need a full
8083
// compilation unit program.
8184

8285
// Pre-Analyze the program and get access to the template type checker.
83-
const {templateTypeChecker} = await info.ngCompiler['ensureAnalyzed']();
86+
const {templateTypeChecker} = info.ngCompiler['ensureAnalyzed']();
8487

8588
const {sourceFiles, program} = info;
8689
const checker = program.getTypeChecker();
@@ -118,6 +121,7 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
118121
// eagerly detect such and later filter those problematic references that
119122
// turned out to refer to queries.
120123
// TODO: Consider skipping this extra work when running in non-batch mode.
124+
// TODO: Also consider skipping if we know this query cannot be part.
121125
{
122126
shouldTrackClassReference: (_class) => false,
123127
attemptRetrieveDescriptorFromSymbol: (s) => getClassFieldDescriptorForSymbol(s, info),
@@ -200,9 +204,14 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
200204
const referenceResult: ReferenceResult<ClassFieldDescriptor> = {references: []};
201205
const sourceQueries: ExtractedQuery[] = [];
202206

203-
const isMigratedQuery = (id: ClassFieldUniqueKey) =>
204-
globalMetadata.knownQueryFields[id] !== undefined &&
205-
globalMetadata.problematicQueries[id] === undefined;
207+
const isMigratedQuery = (descriptor: ClassFieldDescriptor) =>
208+
globalMetadata.knownQueryFields[descriptor.key] !== undefined &&
209+
globalMetadata.problematicQueries[descriptor.key] === undefined &&
210+
(this.config.shouldMigrateQuery === undefined ||
211+
this.config.shouldMigrateQuery(
212+
descriptor,
213+
projectFile(descriptor.node.getSourceFile(), info),
214+
));
206215

207216
// Detect all queries in this unit.
208217
const queryWholeProgramVisitor = (node: ts.Node) => {
@@ -278,8 +287,9 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
278287
for (const extractedQuery of sourceQueries) {
279288
const node = extractedQuery.node;
280289
const sf = node.getSourceFile();
290+
const descriptor = {key: extractedQuery.id, node: extractedQuery.node};
281291

282-
if (!isMigratedQuery(extractedQuery.id)) {
292+
if (!isMigratedQuery(descriptor)) {
283293
updateFileState(filesWithIncompleteMigration, sf, extractedQuery.kind);
284294
continue;
285295
}
@@ -300,9 +310,9 @@ export class SignalQueriesMigration extends TsurgeComplexMigration<
300310
const referenceMigrationHost: ReferenceMigrationHost<ClassFieldDescriptor> = {
301311
printer,
302312
replacements,
303-
shouldMigrateReferencesToField: (field) => isMigratedQuery(field.key),
313+
shouldMigrateReferencesToField: (field) => isMigratedQuery(field),
304314
shouldMigrateReferencesToClass: (clazz) =>
305-
!!knownQueries.getQueryFieldsOfClass(clazz)?.some((q) => isMigratedQuery(q.key)),
315+
!!knownQueries.getQueryFieldsOfClass(clazz)?.some((q) => isMigratedQuery(q)),
306316
};
307317
migrateTypeScriptReferences(referenceMigrationHost, referenceResult.references, checker, info);
308318
migrateTemplateReferences(referenceMigrationHost, referenceResult.references);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 {ProjectFile} from '../../utils/tsurge';
10+
import {ClassFieldDescriptor} from '../signal-migration/src';
11+
12+
export interface MigrationConfig {
13+
/**
14+
* Whether the given query should be migrated. With batch execution, this
15+
* callback fires for foreign queries from other compilation units too.
16+
*
17+
* Treating a query as non-migrated means that no references to it are
18+
* migrated, nor the actual declaration (if it's part of the sources).
19+
*
20+
* If no function is specified here, the migration will migrate all
21+
* inputs and references it discovers in compilation units. This is the
22+
* running assumption for batch mode and LSC mode where the migration
23+
* assumes all seen queries are migrated.
24+
*/
25+
shouldMigrateQuery?: (query: ClassFieldDescriptor, file: ProjectFile) => boolean;
26+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 = "signal-queries-migration",
18+
srcs = glob(["**/*.ts"]),
19+
tsconfig = "//packages/core/schematics:tsconfig.json",
20+
deps = [
21+
"//packages/compiler-cli/src/ngtsc/file_system",
22+
"//packages/core/schematics/migrations/signal-queries-migration:migration",
23+
"//packages/core/schematics/utils",
24+
"//packages/core/schematics/utils/tsurge",
25+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
26+
"@npm//@angular-devkit/schematics",
27+
"@npm//@types/node",
28+
],
29+
)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Signal queries migration
2+
3+
The Angular team provides an automated migration for converting decorator
4+
queries to signal queries. E.g. `@ViewChild` will be converted to `viewChild()`.
5+
6+
Aside from the query declarations, the migration will also take care of all
7+
references to updated queries.
8+
9+
## How to run this migration?
10+
11+
The migration can be run using the following command:
12+
13+
```bash
14+
ng generate @angular/core:signal-queries-migration
15+
```
16+
17+
## What does it change?
18+
19+
1. `@ViewChild()`, `@ViewChildren`, `@ContentChild` and `@ContentChildren` class members
20+
are updated to their signal equivalents.
21+
2. References in your application to migrated queries are updated to call the signal.
22+
- This includes references in templates, host bindings or TypeScript code.
23+
24+
**Before**
25+
26+
```typescript
27+
import {Component, ContentChild} from '@angular/core';
28+
29+
@Component({
30+
template: `Has ref: {{someRef ? 'Yes' : 'No'}}`
31+
})
32+
export class MyComponent {
33+
@ContentChild('someRef') ref: ElementRef|undefined = undefined;
34+
35+
someMethod() {
36+
if (this.ref) {
37+
this.ref.nativeElement;
38+
}
39+
}
40+
}
41+
```
42+
43+
**After**
44+
45+
```typescript
46+
import {Component, contentChild} from '@angular/core';
47+
48+
@Component({
49+
template: `Has ref: {{someRef() ? 'Yes' : 'No'}}`
50+
})
51+
export class MyComponent {
52+
readonly ref = contentChild<ElementRef>('someRef');
53+
54+
someMethod() {
55+
const refValue = this.ref();
56+
if (refValue) {
57+
refValue.nativeElement;
58+
}
59+
}
60+
}
61+
```
62+
63+
## Configuration options
64+
65+
The migration supports a few options to fine-tune the migration for your specific needs.
66+
67+
### `--path`
68+
69+
By default, the migration will update your whole Angular CLI workspace.
70+
You can limit the migration to a specific sub-directory using this option.
71+
72+
### `--analysis-dir`
73+
74+
Optional flag that can be used in large code projects.
75+
76+
In large projects you may use this option to reduce the amount of files being analyzed.
77+
By default, the migration analyzes the whole workspace, regardless of the `--path` option, in
78+
order to update all references affected by a query declaration being migrated.
79+
80+
With this option, you can limit analysis to a sub-folder. Note that this means that any
81+
references outside this directory are silently skipped, potentially breaking your build.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 {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 {
17+
CompilationUnitData,
18+
SignalQueriesMigration,
19+
} from '../../migrations/signal-queries-migration/migration';
20+
21+
interface Options {
22+
path: string;
23+
analysisDir: string;
24+
}
25+
26+
export function migrate(options: Options): Rule {
27+
return async (tree, context) => {
28+
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
29+
30+
if (!buildPaths.length && !testPaths.length) {
31+
throw new SchematicsException(
32+
'Could not find any tsconfig file. Cannot run signal input migration.',
33+
);
34+
}
35+
36+
const fs = new DevkitMigrationFilesystem(tree);
37+
setFileSystem(fs);
38+
39+
const migration = new SignalQueriesMigration({
40+
shouldMigrateQuery: (_query, file) => {
41+
return (
42+
file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
43+
!/(^|\/)node_modules\//.test(file.rootRelativePath)
44+
);
45+
},
46+
});
47+
const analysisPath = fs.resolve(options.analysisDir);
48+
const unitResults: CompilationUnitData[] = [];
49+
const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => {
50+
context.logger.info(`Preparing analysis for: ${tsconfigPath}..`);
51+
52+
const baseInfo = migration.createProgram(tsconfigPath, fs);
53+
const info = migration.prepareProgram(baseInfo);
54+
55+
// Support restricting the analysis to subfolders for larger projects.
56+
if (analysisPath !== '/') {
57+
info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
58+
info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) =>
59+
sf.fileName.startsWith(analysisPath),
60+
);
61+
}
62+
63+
return {info, tsconfigPath};
64+
});
65+
66+
// Analyze phase. Treat all projects as compilation units as
67+
// this allows us to support references between those.
68+
for (const {info, tsconfigPath} of programInfos) {
69+
context.logger.info(`Scanning for queries: ${tsconfigPath}..`);
70+
71+
unitResults.push(await migration.analyze(info));
72+
}
73+
74+
context.logger.info(``);
75+
context.logger.info(`Processing analysis data between targets..`);
76+
context.logger.info(``);
77+
78+
const merged = await migration.merge(unitResults);
79+
const replacementsPerFile: Map<ProjectRootRelativePath, TextUpdate[]> = new Map();
80+
81+
for (const {info, tsconfigPath} of programInfos) {
82+
context.logger.info(`Migrating: ${tsconfigPath}..`);
83+
84+
const replacements = await migration.migrate(merged, info);
85+
const changesPerFile = groupReplacementsByFile(replacements);
86+
87+
for (const [file, changes] of changesPerFile) {
88+
if (!replacementsPerFile.has(file)) {
89+
replacementsPerFile.set(file, changes);
90+
}
91+
}
92+
}
93+
94+
context.logger.info(`Applying changes..`);
95+
for (const [file, changes] of replacementsPerFile) {
96+
const recorder = tree.beginUpdate(file);
97+
for (const c of changes) {
98+
recorder
99+
.remove(c.data.position, c.data.end - c.data.position)
100+
.insertLeft(c.data.position, c.data.toInsert);
101+
}
102+
tree.commitUpdate(recorder);
103+
}
104+
105+
context.logger.info('');
106+
context.logger.info(`Successfully migrated to signal queries 🎉`);
107+
};
108+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "AngularSignalQueriesMigration",
4+
"title": "Angular Signal Queries migration",
5+
"type": "object",
6+
"properties": {
7+
"path": {
8+
"type": "string",
9+
"description": "Path to the directory where all queries should be migrated.",
10+
"x-prompt": "Which directory do you want to migrate?",
11+
"default": "./"
12+
},
13+
"analysisDir": {
14+
"type": "string",
15+
"description": "Path to the directory that should be analyzed. References to migrated queries are migrated based on this folder. Useful for larger projects if the analysis takes too long and the analysis scope can be narrowed.",
16+
"default": "./"
17+
}
18+
}
19+
}

0 commit comments

Comments
 (0)