Skip to content

Commit b8c82fa

Browse files
devversionAndrewKushnir
authored andcommitted
refactor(migrations): handle jit: true component templates in signal input migration (#57347)
Components with `jit: true` are not processed by the Angular compiler, so we cannot ask the template checker for the parsed template; simply because the template wasn't attempted to be parsed. We still can migrate simple cases of such components, commonly seen in unit tests. We do this by manually parsing the template and making use of the reference fallback resolution that is also used for host bindings (where we don't have any type check block information). PR Close #57347
1 parent fa77c9e commit b8c82fa

File tree

10 files changed

+285
-9
lines changed

10 files changed

+285
-9
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ts_library(
1515
"//packages/compiler-cli",
1616
"//packages/compiler-cli/src/ngtsc/annotations",
1717
"//packages/compiler-cli/src/ngtsc/annotations/common",
18+
"//packages/compiler-cli/src/ngtsc/annotations/component",
1819
"//packages/compiler-cli/src/ngtsc/annotations/directive",
1920
"//packages/compiler-cli/src/ngtsc/core",
2021
"//packages/compiler-cli/src/ngtsc/core:api",

packages/core/schematics/migrations/signal-migration/src/create_program.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ import {PartialEvaluator} from '../../../../../compiler-cli/src/ngtsc/partial_ev
1515
import {NgtscProgram} from '../../../../../compiler-cli/src/ngtsc/program';
1616
import {TypeScriptReflectionHost} from '../../../../../compiler-cli/src/ngtsc/reflection';
1717

18+
import assert from 'assert';
19+
import {ResourceLoader} from '../../../../../compiler-cli/src/ngtsc/annotations';
20+
import {NgCompiler} from '../../../../../compiler-cli/src/ngtsc/core';
21+
import {ReferenceEmitter} from '../../../../../compiler-cli/src/ngtsc/imports';
22+
import {isShim} from '../../../../../compiler-cli/src/ngtsc/shims';
23+
import {TemplateTypeChecker} from '../../../../../compiler-cli/src/ngtsc/typecheck/api';
1824
import {
1925
ParsedConfiguration,
2026
readConfiguration,
2127
} from '../../../../../compiler-cli/src/perform_compile';
22-
import {TemplateTypeChecker} from '../../../../../compiler-cli/src/ngtsc/typecheck/api';
23-
import {ReferenceEmitter} from '../../../../../compiler-cli/src/ngtsc/imports';
24-
import {isShim} from '../../../../../compiler-cli/src/ngtsc/shims';
25-
import {NgCompiler} from '../../../../../compiler-cli/src/ngtsc/core';
26-
import assert from 'assert';
2728

2829
/**
2930
* Interface containing the analysis information
@@ -41,6 +42,7 @@ export interface AnalysisProgramInfo {
4142
dtsMetadataReader: DtsMetadataReader;
4243
evaluator: PartialEvaluator;
4344
refEmitter: ReferenceEmitter;
45+
resourceLoader: ResourceLoader;
4446
}
4547

4648
/** Creates and prepares analysis for the given TypeScript project. */
@@ -116,6 +118,7 @@ export function prepareAnalysisInfo(
116118
const reflector = new TypeScriptReflectionHost(typeChecker);
117119
const evaluator = new PartialEvaluator(reflector, typeChecker, null);
118120
const dtsMetadataReader = new DtsMetadataReader(typeChecker, reflector);
121+
const resourceLoader = compiler['resourceManager'];
119122

120123
const limitToRootNamesOnly = process.env['LIMIT_TO_ROOT_NAMES_ONLY'] === '1';
121124
if (limitToRootNamesOnly) {
@@ -148,5 +151,6 @@ export function prepareAnalysisInfo(
148151
typeChecker,
149152
refEmitter,
150153
templateTypeChecker,
154+
resourceLoader,
151155
};
152156
}

packages/core/schematics/migrations/signal-migration/src/passes/2_find_source_file_references.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {identifyPotentialTypeScriptReference} from './references/identify_ts_ref
2020
import {PartialDirectiveTypeInCatalystTests} from '../pattern_advisors/partial_directive_type';
2121
import {InputReferenceKind} from '../utils/input_reference';
2222
import {GroupedTsAstVisitor} from '../utils/grouped_ts_ast_visitor';
23+
import {ResourceLoader} from '../../../../../../compiler-cli/src/ngtsc/annotations';
24+
import {PartialEvaluator} from '../../../../../../compiler-cli/src/ngtsc/partial_evaluator';
2325

2426
/**
2527
* Phase where we iterate through all source file references and
@@ -38,6 +40,8 @@ export function pass2_IdentifySourceFileReferences(
3840
host: MigrationHost,
3941
checker: ts.TypeChecker,
4042
reflector: ReflectionHost,
43+
resourceLoader: ResourceLoader,
44+
evaluator: PartialEvaluator,
4145
templateTypeChecker: TemplateTypeChecker,
4246
groupedTsAstVisitor: GroupedTsAstVisitor,
4347
knownInputs: KnownInputs,
@@ -51,7 +55,18 @@ export function pass2_IdentifySourceFileReferences(
5155

5256
const visitor = (node: ts.Node) => {
5357
if (ts.isClassDeclaration(node)) {
54-
identifyTemplateReferences(node, host, checker, templateTypeChecker, result, knownInputs);
58+
identifyTemplateReferences(
59+
node,
60+
host,
61+
reflector,
62+
checker,
63+
evaluator,
64+
templateTypeChecker,
65+
resourceLoader,
66+
host.options,
67+
result,
68+
knownInputs,
69+
);
5570
identifyHostBindingReferences(node, host, checker, reflector, result, knownInputs);
5671
}
5772

packages/core/schematics/migrations/signal-migration/src/passes/references/identify_template_references.ts

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,24 @@
77
*/
88

99
import ts from 'typescript';
10+
import {ResourceLoader} from '../../../../../../../compiler-cli/src/ngtsc/annotations';
11+
import {TemplateTypeChecker} from '../../../../../../../compiler-cli/src/ngtsc/typecheck/api';
1012
import {InputIncompatibilityReason} from '../../input_detection/incompatibility';
1113
import {KnownInputs} from '../../input_detection/known_inputs';
14+
import {TemplateReferenceVisitor} from '../../input_detection/template_reference_visitor';
1215
import {MigrationHost} from '../../migration_host';
1316
import {MigrationResult} from '../../result';
1417
import {InputReferenceKind} from '../../utils/input_reference';
15-
import {TemplateTypeChecker} from '../../../../../../../compiler-cli/src/ngtsc/typecheck/api';
16-
import {TemplateReferenceVisitor} from '../../input_detection/template_reference_visitor';
18+
import {
19+
ClassDeclaration,
20+
ReflectionHost,
21+
} from '../../../../../../../compiler-cli/src/ngtsc/reflection';
22+
import {PartialEvaluator} from '../../../../../../../compiler-cli/src/ngtsc/partial_evaluator';
23+
import {extractTemplate} from '../../../../../../../compiler-cli/src/ngtsc/annotations/component/src/resources';
24+
import {attemptExtractTemplateDefinition} from '../../utils/extract_template';
25+
import {NgCompilerOptions} from '../../../../../../../compiler-cli/src/ngtsc/core/api';
26+
import {CompilationMode} from '../../../../../../../compiler-cli/src/ngtsc/transform';
27+
import {TmplAstNode} from '@angular/compiler';
1728

1829
/**
1930
* Checks whether the given class has an Angular template, and resolves
@@ -22,12 +33,30 @@ import {TemplateReferenceVisitor} from '../../input_detection/template_reference
2233
export function identifyTemplateReferences(
2334
node: ts.ClassDeclaration,
2435
host: MigrationHost,
36+
reflector: ReflectionHost,
2537
checker: ts.TypeChecker,
38+
evaluator: PartialEvaluator,
2639
templateTypeChecker: TemplateTypeChecker,
40+
resourceLoader: ResourceLoader,
41+
options: NgCompilerOptions,
2742
result: MigrationResult,
2843
knownInputs: KnownInputs,
2944
) {
30-
const template = templateTypeChecker.getTemplate(node);
45+
const template =
46+
templateTypeChecker.getTemplate(node) ??
47+
// If there is no template registered in the TCB or compiler, the template may
48+
// be skipped due to an explicit `jit: true` setting. We try to detect this case
49+
// and parse the template manually.
50+
extractTemplateWithoutCompilerAnalysis(
51+
node,
52+
checker,
53+
reflector,
54+
resourceLoader,
55+
evaluator,
56+
options,
57+
host,
58+
);
59+
3160
if (template !== null) {
3261
const visitor = new TemplateReferenceVisitor(
3362
host,
@@ -64,3 +93,45 @@ export function identifyTemplateReferences(
6493
}
6594
}
6695
}
96+
97+
/**
98+
* Attempts to extract a `@Component` template from the given class,
99+
* without relying on the `NgCompiler` program analysis.
100+
*
101+
* This is useful for JIT components using `jit: true` which were not
102+
* processed by the Angular compiler, but may still have templates that
103+
* contain references to inputs that we can resolve via the fallback
104+
* reference resolutions (that does not use the type check block).
105+
*/
106+
function extractTemplateWithoutCompilerAnalysis(
107+
node: ts.ClassDeclaration,
108+
checker: ts.TypeChecker,
109+
reflector: ReflectionHost,
110+
resourceLoader: ResourceLoader,
111+
evaluator: PartialEvaluator,
112+
options: NgCompilerOptions,
113+
host: MigrationHost,
114+
): TmplAstNode[] | null {
115+
if (node.name === undefined) {
116+
return null;
117+
}
118+
const tmplDef = attemptExtractTemplateDefinition(node, checker, reflector, host);
119+
if (tmplDef === null) {
120+
return null;
121+
}
122+
return extractTemplate(
123+
node as ClassDeclaration,
124+
tmplDef,
125+
evaluator,
126+
null,
127+
resourceLoader,
128+
{
129+
enableBlockSyntax: true,
130+
enableLetSyntax: true,
131+
usePoisonedData: true,
132+
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat !== false,
133+
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs === true,
134+
},
135+
CompilationMode.FULL,
136+
).nodes;
137+
}

packages/core/schematics/migrations/signal-migration/src/phase_analysis.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function executeAnalysisPhase(
3737
dtsMetadataReader,
3838
typeChecker,
3939
templateTypeChecker,
40+
resourceLoader,
4041
evaluator,
4142
refEmitter,
4243
}: AnalysisProgramInfo,
@@ -70,6 +71,8 @@ export function executeAnalysisPhase(
7071
host,
7172
typeChecker,
7273
reflector,
74+
resourceLoader,
75+
evaluator,
7376
templateTypeChecker,
7477
pass2And3SourceFileVisitor,
7578
knownInputs,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
import {
11+
ReflectionHost,
12+
reflectObjectLiteral,
13+
} from '../../../../../../compiler-cli/src/ngtsc/reflection';
14+
import {MigrationHost} from '../migration_host';
15+
import {getAngularDecorators} from '../../../../../../compiler-cli/src/ngtsc/annotations';
16+
import {PartialEvaluator} from './../../../../../../compiler-cli/src/ngtsc/partial_evaluator';
17+
import {
18+
ExternalTemplateDeclaration,
19+
InlineTemplateDeclaration,
20+
} from '../../../../../../compiler-cli/src/ngtsc/annotations/component/src/resources';
21+
import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler';
22+
import path from 'path';
23+
24+
/**
25+
* Attempts to extract the `TemplateDefinition` for the given
26+
* class, if possible.
27+
*
28+
* The definition can then be used with the Angular compiler to
29+
* load/parse the given template.
30+
*/
31+
export function attemptExtractTemplateDefinition(
32+
node: ts.ClassDeclaration,
33+
checker: ts.TypeChecker,
34+
reflector: ReflectionHost,
35+
host: MigrationHost,
36+
): InlineTemplateDeclaration | ExternalTemplateDeclaration | null {
37+
const classDecorators = reflector.getDecoratorsOfDeclaration(node);
38+
const evaluator = new PartialEvaluator(reflector, checker, null);
39+
40+
const ngDecorators =
41+
classDecorators !== null
42+
? getAngularDecorators(classDecorators, ['Component'], host.isMigratingCore)
43+
: [];
44+
45+
if (
46+
ngDecorators.length === 0 ||
47+
ngDecorators[0].args === null ||
48+
ngDecorators[0].args.length === 0 ||
49+
!ts.isObjectLiteralExpression(ngDecorators[0].args[0])
50+
) {
51+
return null;
52+
}
53+
54+
const properties = reflectObjectLiteral(ngDecorators[0].args[0]);
55+
const templateProp = properties.get('template');
56+
const templateUrlProp = properties.get('templateUrl');
57+
const containingFile = node.getSourceFile().fileName;
58+
59+
// inline template.
60+
if (templateProp !== undefined) {
61+
const templateStr = evaluator.evaluate(templateProp);
62+
if (typeof templateStr === 'string') {
63+
return {
64+
isInline: true,
65+
expression: templateProp,
66+
interpolationConfig: DEFAULT_INTERPOLATION_CONFIG,
67+
preserveWhitespaces: false,
68+
resolvedTemplateUrl: containingFile,
69+
templateUrl: containingFile,
70+
} as InlineTemplateDeclaration;
71+
}
72+
}
73+
74+
// external template.
75+
if (templateUrlProp !== undefined) {
76+
const templateUrl = evaluator.evaluate(templateUrlProp);
77+
if (typeof templateUrl === 'string') {
78+
return {
79+
isInline: false,
80+
interpolationConfig: DEFAULT_INTERPOLATION_CONFIG,
81+
preserveWhitespaces: false,
82+
templateUrlExpression: templateUrlProp,
83+
templateUrl,
84+
resolvedTemplateUrl: path.join(path.dirname(containingFile), templateUrl),
85+
};
86+
}
87+
}
88+
89+
return null;
90+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{test}}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.io/license
7+
*/
8+
9+
// tslint:disable
10+
11+
import {Component, Input} from '@angular/core';
12+
13+
@Component({
14+
jit: true,
15+
template: '{{test}}',
16+
})
17+
class JitTrueComponent {
18+
@Input() test = true;
19+
}
20+
21+
@Component({
22+
jit: true,
23+
templateUrl: './jit_true_component_external_tmpl.html',
24+
})
25+
class JitTrueComponentExternalTmpl {
26+
@Input() test = true;
27+
}

packages/core/schematics/migrations/signal-migration/test/golden.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,38 @@ import { Component, input } from '@angular/core';
560560
export class InlineTmpl {
561561
justify = input<'start' | 'end'>('end');
562562
}
563+
@@@@@@ jit_true_component_external_tmpl.html @@@@@@
564+
565+
{{test()}}
566+
@@@@@@ jit_true_components.ts @@@@@@
567+
568+
/**
569+
* @license
570+
* Copyright Google LLC All Rights Reserved.
571+
*
572+
* Use of this source code is governed by an MIT-style license that can be
573+
* found in the LICENSE file at https://angular.io/license
574+
*/
575+
576+
// tslint:disable
577+
578+
import { Component, input } from '@angular/core';
579+
580+
@Component({
581+
jit: true,
582+
template: '{{test()}}',
583+
})
584+
class JitTrueComponent {
585+
test = input(true);
586+
}
587+
588+
@Component({
589+
jit: true,
590+
templateUrl: './jit_true_component_external_tmpl.html',
591+
})
592+
class JitTrueComponentExternalTmpl {
593+
test = input(true);
594+
}
563595
@@@@@@ loop_labels.ts @@@@@@
564596

565597
// tslint:disable

0 commit comments

Comments
 (0)