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
This commit is contained in:
Paul Gschwendtner
2024-08-12 15:32:37 +00:00
committed by Andrew Kushnir
parent 310a0e0eda
commit d608b857c6
10 changed files with 285 additions and 9 deletions
@@ -15,6 +15,7 @@ ts_library(
"//packages/compiler-cli",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/annotations/common",
"//packages/compiler-cli/src/ngtsc/annotations/component",
"//packages/compiler-cli/src/ngtsc/annotations/directive",
"//packages/compiler-cli/src/ngtsc/core",
"//packages/compiler-cli/src/ngtsc/core:api",
@@ -15,15 +15,16 @@ import {PartialEvaluator} from '../../../../../compiler-cli/src/ngtsc/partial_ev
import {NgtscProgram} from '../../../../../compiler-cli/src/ngtsc/program';
import {TypeScriptReflectionHost} from '../../../../../compiler-cli/src/ngtsc/reflection';
import assert from 'assert';
import {ResourceLoader} from '../../../../../compiler-cli/src/ngtsc/annotations';
import {NgCompiler} from '../../../../../compiler-cli/src/ngtsc/core';
import {ReferenceEmitter} from '../../../../../compiler-cli/src/ngtsc/imports';
import {isShim} from '../../../../../compiler-cli/src/ngtsc/shims';
import {TemplateTypeChecker} from '../../../../../compiler-cli/src/ngtsc/typecheck/api';
import {
ParsedConfiguration,
readConfiguration,
} from '../../../../../compiler-cli/src/perform_compile';
import {TemplateTypeChecker} from '../../../../../compiler-cli/src/ngtsc/typecheck/api';
import {ReferenceEmitter} from '../../../../../compiler-cli/src/ngtsc/imports';
import {isShim} from '../../../../../compiler-cli/src/ngtsc/shims';
import {NgCompiler} from '../../../../../compiler-cli/src/ngtsc/core';
import assert from 'assert';
/**
* Interface containing the analysis information
@@ -41,6 +42,7 @@ export interface AnalysisProgramInfo {
dtsMetadataReader: DtsMetadataReader;
evaluator: PartialEvaluator;
refEmitter: ReferenceEmitter;
resourceLoader: ResourceLoader;
}
/** Creates and prepares analysis for the given TypeScript project. */
@@ -116,6 +118,7 @@ export function prepareAnalysisInfo(
const reflector = new TypeScriptReflectionHost(typeChecker);
const evaluator = new PartialEvaluator(reflector, typeChecker, null);
const dtsMetadataReader = new DtsMetadataReader(typeChecker, reflector);
const resourceLoader = compiler['resourceManager'];
const limitToRootNamesOnly = process.env['LIMIT_TO_ROOT_NAMES_ONLY'] === '1';
if (limitToRootNamesOnly) {
@@ -148,5 +151,6 @@ export function prepareAnalysisInfo(
typeChecker,
refEmitter,
templateTypeChecker,
resourceLoader,
};
}
@@ -20,6 +20,8 @@ import {identifyPotentialTypeScriptReference} from './references/identify_ts_ref
import {PartialDirectiveTypeInCatalystTests} from '../pattern_advisors/partial_directive_type';
import {InputReferenceKind} from '../utils/input_reference';
import {GroupedTsAstVisitor} from '../utils/grouped_ts_ast_visitor';
import {ResourceLoader} from '../../../../../../compiler-cli/src/ngtsc/annotations';
import {PartialEvaluator} from '../../../../../../compiler-cli/src/ngtsc/partial_evaluator';
/**
* Phase where we iterate through all source file references and
@@ -38,6 +40,8 @@ export function pass2_IdentifySourceFileReferences(
host: MigrationHost,
checker: ts.TypeChecker,
reflector: ReflectionHost,
resourceLoader: ResourceLoader,
evaluator: PartialEvaluator,
templateTypeChecker: TemplateTypeChecker,
groupedTsAstVisitor: GroupedTsAstVisitor,
knownInputs: KnownInputs,
@@ -51,7 +55,18 @@ export function pass2_IdentifySourceFileReferences(
const visitor = (node: ts.Node) => {
if (ts.isClassDeclaration(node)) {
identifyTemplateReferences(node, host, checker, templateTypeChecker, result, knownInputs);
identifyTemplateReferences(
node,
host,
reflector,
checker,
evaluator,
templateTypeChecker,
resourceLoader,
host.options,
result,
knownInputs,
);
identifyHostBindingReferences(node, host, checker, reflector, result, knownInputs);
}
@@ -7,13 +7,24 @@
*/
import ts from 'typescript';
import {ResourceLoader} from '../../../../../../../compiler-cli/src/ngtsc/annotations';
import {TemplateTypeChecker} from '../../../../../../../compiler-cli/src/ngtsc/typecheck/api';
import {InputIncompatibilityReason} from '../../input_detection/incompatibility';
import {KnownInputs} from '../../input_detection/known_inputs';
import {TemplateReferenceVisitor} from '../../input_detection/template_reference_visitor';
import {MigrationHost} from '../../migration_host';
import {MigrationResult} from '../../result';
import {InputReferenceKind} from '../../utils/input_reference';
import {TemplateTypeChecker} from '../../../../../../../compiler-cli/src/ngtsc/typecheck/api';
import {TemplateReferenceVisitor} from '../../input_detection/template_reference_visitor';
import {
ClassDeclaration,
ReflectionHost,
} from '../../../../../../../compiler-cli/src/ngtsc/reflection';
import {PartialEvaluator} from '../../../../../../../compiler-cli/src/ngtsc/partial_evaluator';
import {extractTemplate} from '../../../../../../../compiler-cli/src/ngtsc/annotations/component/src/resources';
import {attemptExtractTemplateDefinition} from '../../utils/extract_template';
import {NgCompilerOptions} from '../../../../../../../compiler-cli/src/ngtsc/core/api';
import {CompilationMode} from '../../../../../../../compiler-cli/src/ngtsc/transform';
import {TmplAstNode} from '@angular/compiler';
/**
* Checks whether the given class has an Angular template, and resolves
@@ -22,12 +33,30 @@ import {TemplateReferenceVisitor} from '../../input_detection/template_reference
export function identifyTemplateReferences(
node: ts.ClassDeclaration,
host: MigrationHost,
reflector: ReflectionHost,
checker: ts.TypeChecker,
evaluator: PartialEvaluator,
templateTypeChecker: TemplateTypeChecker,
resourceLoader: ResourceLoader,
options: NgCompilerOptions,
result: MigrationResult,
knownInputs: KnownInputs,
) {
const template = templateTypeChecker.getTemplate(node);
const template =
templateTypeChecker.getTemplate(node) ??
// If there is no template registered in the TCB or compiler, the template may
// be skipped due to an explicit `jit: true` setting. We try to detect this case
// and parse the template manually.
extractTemplateWithoutCompilerAnalysis(
node,
checker,
reflector,
resourceLoader,
evaluator,
options,
host,
);
if (template !== null) {
const visitor = new TemplateReferenceVisitor(
host,
@@ -64,3 +93,45 @@ export function identifyTemplateReferences(
}
}
}
/**
* Attempts to extract a `@Component` template from the given class,
* without relying on the `NgCompiler` program analysis.
*
* This is useful for JIT components using `jit: true` which were not
* processed by the Angular compiler, but may still have templates that
* contain references to inputs that we can resolve via the fallback
* reference resolutions (that does not use the type check block).
*/
function extractTemplateWithoutCompilerAnalysis(
node: ts.ClassDeclaration,
checker: ts.TypeChecker,
reflector: ReflectionHost,
resourceLoader: ResourceLoader,
evaluator: PartialEvaluator,
options: NgCompilerOptions,
host: MigrationHost,
): TmplAstNode[] | null {
if (node.name === undefined) {
return null;
}
const tmplDef = attemptExtractTemplateDefinition(node, checker, reflector, host);
if (tmplDef === null) {
return null;
}
return extractTemplate(
node as ClassDeclaration,
tmplDef,
evaluator,
null,
resourceLoader,
{
enableBlockSyntax: true,
enableLetSyntax: true,
usePoisonedData: true,
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat !== false,
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs === true,
},
CompilationMode.FULL,
).nodes;
}
@@ -37,6 +37,7 @@ export function executeAnalysisPhase(
dtsMetadataReader,
typeChecker,
templateTypeChecker,
resourceLoader,
evaluator,
refEmitter,
}: AnalysisProgramInfo,
@@ -70,6 +71,8 @@ export function executeAnalysisPhase(
host,
typeChecker,
reflector,
resourceLoader,
evaluator,
templateTypeChecker,
pass2And3SourceFileVisitor,
knownInputs,
@@ -0,0 +1,90 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import ts from 'typescript';
import {
ReflectionHost,
reflectObjectLiteral,
} from '../../../../../../compiler-cli/src/ngtsc/reflection';
import {MigrationHost} from '../migration_host';
import {getAngularDecorators} from '../../../../../../compiler-cli/src/ngtsc/annotations';
import {PartialEvaluator} from './../../../../../../compiler-cli/src/ngtsc/partial_evaluator';
import {
ExternalTemplateDeclaration,
InlineTemplateDeclaration,
} from '../../../../../../compiler-cli/src/ngtsc/annotations/component/src/resources';
import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler';
import path from 'path';
/**
* Attempts to extract the `TemplateDefinition` for the given
* class, if possible.
*
* The definition can then be used with the Angular compiler to
* load/parse the given template.
*/
export function attemptExtractTemplateDefinition(
node: ts.ClassDeclaration,
checker: ts.TypeChecker,
reflector: ReflectionHost,
host: MigrationHost,
): InlineTemplateDeclaration | ExternalTemplateDeclaration | null {
const classDecorators = reflector.getDecoratorsOfDeclaration(node);
const evaluator = new PartialEvaluator(reflector, checker, null);
const ngDecorators =
classDecorators !== null
? getAngularDecorators(classDecorators, ['Component'], host.isMigratingCore)
: [];
if (
ngDecorators.length === 0 ||
ngDecorators[0].args === null ||
ngDecorators[0].args.length === 0 ||
!ts.isObjectLiteralExpression(ngDecorators[0].args[0])
) {
return null;
}
const properties = reflectObjectLiteral(ngDecorators[0].args[0]);
const templateProp = properties.get('template');
const templateUrlProp = properties.get('templateUrl');
const containingFile = node.getSourceFile().fileName;
// inline template.
if (templateProp !== undefined) {
const templateStr = evaluator.evaluate(templateProp);
if (typeof templateStr === 'string') {
return {
isInline: true,
expression: templateProp,
interpolationConfig: DEFAULT_INTERPOLATION_CONFIG,
preserveWhitespaces: false,
resolvedTemplateUrl: containingFile,
templateUrl: containingFile,
} as InlineTemplateDeclaration;
}
}
// external template.
if (templateUrlProp !== undefined) {
const templateUrl = evaluator.evaluate(templateUrlProp);
if (typeof templateUrl === 'string') {
return {
isInline: false,
interpolationConfig: DEFAULT_INTERPOLATION_CONFIG,
preserveWhitespaces: false,
templateUrlExpression: templateUrlProp,
templateUrl,
resolvedTemplateUrl: path.join(path.dirname(containingFile), templateUrl),
};
}
}
return null;
}
@@ -0,0 +1,27 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// tslint:disable
import {Component, Input} from '@angular/core';
@Component({
jit: true,
template: '{{test}}',
})
class JitTrueComponent {
@Input() test = true;
}
@Component({
jit: true,
templateUrl: './jit_true_component_external_tmpl.html',
})
class JitTrueComponentExternalTmpl {
@Input() test = true;
}
@@ -560,6 +560,38 @@ import { Component, input } from '@angular/core';
export class InlineTmpl {
justify = input<'start' | 'end'>('end');
}
@@@@@@ jit_true_component_external_tmpl.html @@@@@@
{{test()}}
@@@@@@ jit_true_components.ts @@@@@@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// tslint:disable
import { Component, input } from '@angular/core';
@Component({
jit: true,
template: '{{test()}}',
})
class JitTrueComponent {
test = input(true);
}
@Component({
jit: true,
templateUrl: './jit_true_component_external_tmpl.html',
})
class JitTrueComponentExternalTmpl {
test = input(true);
}
@@@@@@ loop_labels.ts @@@@@@
// tslint:disable
@@ -560,6 +560,38 @@ import { Component, input } from '@angular/core';
export class InlineTmpl {
justify = input<'start' | 'end'>('end');
}
@@@@@@ jit_true_component_external_tmpl.html @@@@@@
{{test()}}
@@@@@@ jit_true_components.ts @@@@@@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// tslint:disable
import { Component, input } from '@angular/core';
@Component({
jit: true,
template: '{{test()}}',
})
class JitTrueComponent {
test = input(true);
}
@Component({
jit: true,
templateUrl: './jit_true_component_external_tmpl.html',
})
class JitTrueComponentExternalTmpl {
test = input(true);
}
@@@@@@ loop_labels.ts @@@@@@
// tslint:disable