Skip to content

Commit ca59164

Browse files
devversionbenlesh
authored andcommitted
refactor(core): allow developers to select static-query migration strategy (angular#29876)
Currently there are two available migration strategies for the `static-query` schematic. Both have benefits and negatives which depend on what the developer prefers. Since we can't decide which migration strategy is the best for a given project, the developer should be able to select a specific strategy through a simple choice prompt. In order to be able to use prompts in a migration schematic, we need to take advantage of the "inquirer" package which is also used by the CLI schematic prompts (schematic prompts are usually only statically defined in the schema). Additionally the schematic needs to be made "async" because with prompts the schematic can no longer execute synchronously without implementing some logic that blocks the execution. PR Close angular#29876
1 parent 2ba799d commit ca59164

File tree

7 files changed

+371
-172
lines changed

7 files changed

+371
-172
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@types/diff": "^3.5.1",
5050
"@types/fs-extra": "4.0.2",
5151
"@types/hammerjs": "2.0.35",
52+
"@types/inquirer": "^0.0.44",
5253
"@types/jasmine": "^2.8.8",
5354
"@types/jasminewd2": "^2.0.6",
5455
"@types/minimist": "^1.2.0",

packages/core/schematics/migrations/static-queries/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ts_library(
1515
"//packages/core/schematics/utils",
1616
"@npm//@angular-devkit/schematics",
1717
"@npm//@types/node",
18+
"@npm//rxjs",
1819
"@npm//typescript",
1920
],
2021
)

packages/core/schematics/migrations/static-queries/index.ts

+75-34
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {logging} from '@angular-devkit/core';
109
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
1110
import {dirname, relative} from 'path';
11+
import {from} from 'rxjs';
1212
import * as ts from 'typescript';
1313

1414
import {NgComponentTemplateVisitor} from '../../utils/ng_component_template';
1515
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
16+
import {getInquirer, supportsPrompt} from '../../utils/schematics_prompt';
1617
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
1718
import {TypeScriptVisitor, visitAllNodes} from '../../utils/typescript/visit_nodes';
1819

@@ -22,34 +23,83 @@ import {TimingStrategy} from './strategies/timing-strategy';
2223
import {QueryUsageStrategy} from './strategies/usage_strategy/usage_strategy';
2324
import {getTransformedQueryCallExpr} from './transform';
2425

25-
type Logger = logging.LoggerApi;
26-
2726
/** Entry point for the V8 static-query migration. */
2827
export default function(): Rule {
2928
return (tree: Tree, context: SchematicContext) => {
30-
const projectTsConfigPaths = getProjectTsConfigPaths(tree);
31-
const basePath = process.cwd();
32-
33-
if (!projectTsConfigPaths.length) {
34-
throw new SchematicsException(
35-
'Could not find any tsconfig file. Cannot migrate queries ' +
36-
'to explicit timing.');
37-
}
38-
39-
for (const tsconfigPath of projectTsConfigPaths) {
40-
runStaticQueryMigration(tree, tsconfigPath, basePath, context.logger);
41-
}
29+
// We need to cast the returned "Observable" to "any" as there is a
30+
// RxJS version mismatch that breaks the TS compilation.
31+
return from(runMigration(tree, context).then(() => tree)) as any;
4232
};
4333
}
4434

35+
/** Runs the V8 migration static-query migration for all determined TypeScript projects. */
36+
async function runMigration(tree: Tree, context: SchematicContext) {
37+
const projectTsConfigPaths = getProjectTsConfigPaths(tree);
38+
const basePath = process.cwd();
39+
const logger = context.logger;
40+
41+
logger.info('------ Static Query migration ------');
42+
logger.info('In preparation for Ivy, developers can now explicitly specify the');
43+
logger.info('timing of their queries. Read more about this here:');
44+
logger.info('https://github.com/angular/angular/pull/28810');
45+
logger.info('');
46+
47+
if (!projectTsConfigPaths.length) {
48+
throw new SchematicsException(
49+
'Could not find any tsconfig file. Cannot migrate queries ' +
50+
'to explicit timing.');
51+
}
52+
53+
// In case prompts are supported, determine the desired migration strategy
54+
// by creating a choice prompt. By default the template strategy is used.
55+
let isUsageStrategy = false;
56+
if (supportsPrompt()) {
57+
logger.info('There are two available migration strategies that can be selected:');
58+
logger.info(' • Template strategy - migration tool (short-term gains, rare corrections)');
59+
logger.info(' • Usage strategy - best practices (long-term gains, manual corrections)');
60+
logger.info('For an easy migration, the template strategy is recommended. The usage');
61+
logger.info('strategy can be used for best practices and a code base that will be more');
62+
logger.info('flexible to changes going forward.');
63+
const {strategyName} = await getInquirer().prompt<{strategyName: string}>({
64+
type: 'list',
65+
name: 'strategyName',
66+
message: 'What migration strategy do you want to use?',
67+
choices: [
68+
{name: 'Template strategy', value: 'template'}, {name: 'Usage strategy', value: 'usage'}
69+
],
70+
default: 'template',
71+
});
72+
logger.info('');
73+
isUsageStrategy = strategyName === 'usage';
74+
} else {
75+
// In case prompts are not supported, we still want to allow developers to opt
76+
// into the usage strategy by specifying an environment variable. The tests also
77+
// use the environment variable as there is no headless way to select via prompt.
78+
isUsageStrategy = !!process.env['NG_STATIC_QUERY_USAGE_STRATEGY'];
79+
}
80+
81+
const failures = [];
82+
for (const tsconfigPath of projectTsConfigPaths) {
83+
failures.push(...await runStaticQueryMigration(tree, tsconfigPath, basePath, isUsageStrategy));
84+
}
85+
86+
if (failures.length) {
87+
logger.info('Some queries cannot be migrated automatically. Please go through');
88+
logger.info('those manually and apply the appropriate timing:');
89+
failures.forEach(failure => logger.warn(`⮑ ${failure}`));
90+
}
91+
92+
logger.info('------------------------------------------------');
93+
}
94+
4595
/**
4696
* Runs the static query migration for the given TypeScript project. The schematic
4797
* analyzes all queries within the project and sets up the query timing based on
4898
* the current usage of the query property. e.g. a view query that is not used in any
4999
* lifecycle hook does not need to be static and can be set up with "static: false".
50100
*/
51-
function runStaticQueryMigration(
52-
tree: Tree, tsconfigPath: string, basePath: string, logger: Logger) {
101+
async function runStaticQueryMigration(
102+
tree: Tree, tsconfigPath: string, basePath: string, isUsageStrategy: boolean) {
53103
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
54104
const host = ts.createCompilerHost(parsed.options, true);
55105

@@ -62,7 +112,6 @@ function runStaticQueryMigration(
62112
return buffer ? buffer.toString() : undefined;
63113
};
64114

65-
const isUsageStrategy = !!process.env['NG_STATIC_QUERY_USAGE_STRATEGY'];
66115
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
67116
const typeChecker = program.getTypeChecker();
68117
const queryVisitor = new NgQueryResolveVisitor(typeChecker);
@@ -100,13 +149,13 @@ function runStaticQueryMigration(
100149
const strategy: TimingStrategy = isUsageStrategy ?
101150
new QueryUsageStrategy(classMetadata, typeChecker) :
102151
new QueryTemplateStrategy(tsconfigPath, classMetadata, host);
103-
const detectionMessages: string[] = [];
152+
const failureMessages: string[] = [];
104153

105154
// In case the strategy could not be set up properly, we just exit the
106155
// migration. We don't want to throw an exception as this could mean
107156
// that other migrations are interrupted.
108157
if (!strategy.setup()) {
109-
return;
158+
return [];
110159
}
111160

112161
// Walk through all source files that contain resolved queries and update
@@ -135,23 +184,15 @@ function runStaticQueryMigration(
135184
update.remove(queryExpr.getStart(), queryExpr.getWidth());
136185
update.insertRight(queryExpr.getStart(), newText);
137186

138-
const {line, character} =
139-
ts.getLineAndCharacterOfPosition(sourceFile, q.decorator.node.getStart());
140-
detectionMessages.push(`${relativePath}@${line + 1}:${character + 1}: ${message}`);
187+
if (message) {
188+
const {line, character} =
189+
ts.getLineAndCharacterOfPosition(sourceFile, q.decorator.node.getStart());
190+
failureMessages.push(`${relativePath}@${line + 1}:${character + 1}: ${message}`);
191+
}
141192
});
142193

143194
tree.commitUpdate(update);
144195
});
145196

146-
if (detectionMessages.length) {
147-
logger.info('------ Static Query migration ------');
148-
logger.info('In preparation for Ivy, developers can now explicitly specify the');
149-
logger.info('timing of their queries. Read more about this here:');
150-
logger.info('https://github.com/angular/angular/pull/28810');
151-
logger.info('');
152-
logger.info('Some queries cannot be migrated automatically. Please go through');
153-
logger.info('those manually and apply the appropriate timing:');
154-
detectionMessages.forEach(failure => logger.warn(`⮑ ${failure}`));
155-
logger.info('------------------------------------------------');
156-
}
197+
return failureMessages;
157198
}

0 commit comments

Comments
 (0)