refactor(compiler-cli): allow overriding templates in the type checker (#38105)
This commit adds an `overrideComponentTemplate` operation to the template type-checker. This operation changes the template used during template type-checking operations. Overriding a template causes any previous work for it to be discarded, and the template type-checking engine will regenerate the TCB for that template on the next request. This operation can be used by a consumer such as the language service to get rapid feedback or diagnostics as the user is editing a template file, without the need for a full incremental build iteration. Closes #38058 PR Close #38105
This commit is contained in:
parent
3f2257f209
commit
0314fd4b6e
@ -6,8 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TmplAstNode} from '@angular/compiler';
|
import {ParseError, TmplAstNode} from '@angular/compiler';
|
||||||
|
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,6 +23,21 @@ import * as ts from 'typescript';
|
|||||||
* query, depending on the method either `null` will be returned or an error will be thrown.
|
* query, depending on the method either `null` will be returned or an error will be thrown.
|
||||||
*/
|
*/
|
||||||
export interface TemplateTypeChecker {
|
export interface TemplateTypeChecker {
|
||||||
|
/**
|
||||||
|
* Clear all overrides and return the template type-checker to the original input program state.
|
||||||
|
*/
|
||||||
|
resetOverrides(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a new template string that will be used in place of the user-defined template when
|
||||||
|
* checking or operating on the given component.
|
||||||
|
*
|
||||||
|
* The compiler will parse this template for diagnostics, and will return any parsing errors if it
|
||||||
|
* is not valid. If the template cannot be parsed correctly, no override will occur.
|
||||||
|
*/
|
||||||
|
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
|
||||||
|
{nodes: TmplAstNode[], errors?: ParseError[]};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all `ts.Diagnostic`s currently available for the given `ts.SourceFile`.
|
* Get all `ts.Diagnostic`s currently available for the given `ts.SourceFile`.
|
||||||
*
|
*
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {ParseError, parseTemplate, TmplAstNode} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||||
@ -14,7 +15,7 @@ import {IncrementalBuild} from '../../incremental/api';
|
|||||||
import {ReflectionHost} from '../../reflection';
|
import {ReflectionHost} from '../../reflection';
|
||||||
import {isShim} from '../../shims';
|
import {isShim} from '../../shims';
|
||||||
import {getSourceFileOrNull} from '../../util/src/typescript';
|
import {getSourceFileOrNull} from '../../util/src/typescript';
|
||||||
import {OptimizeFor, ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
import {OptimizeFor, ProgramTypeCheckAdapter, TemplateId, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
|
||||||
|
|
||||||
import {InliningMode, ShimTypeCheckingData, TypeCheckContextImpl, TypeCheckingHost} from './context';
|
import {InliningMode, ShimTypeCheckingData, TypeCheckContextImpl, TypeCheckingHost} from './context';
|
||||||
import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics';
|
import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics';
|
||||||
@ -37,6 +38,47 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
|||||||
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
|
private compilerHost: Pick<ts.CompilerHost, 'getCanonicalFileName'>,
|
||||||
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>) {}
|
private priorBuild: IncrementalBuild<unknown, FileTypeCheckingData>) {}
|
||||||
|
|
||||||
|
resetOverrides(): void {
|
||||||
|
for (const fileRecord of this.state.values()) {
|
||||||
|
if (fileRecord.templateOverrides !== null) {
|
||||||
|
fileRecord.templateOverrides = null;
|
||||||
|
fileRecord.shimData.clear();
|
||||||
|
fileRecord.isComplete = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overrideComponentTemplate(component: ts.ClassDeclaration, template: string):
|
||||||
|
{nodes: TmplAstNode[], errors?: ParseError[]} {
|
||||||
|
const {nodes, errors} = parseTemplate(template, 'override.html', {
|
||||||
|
preserveWhitespaces: true,
|
||||||
|
leadingTriviaChars: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors !== undefined) {
|
||||||
|
return {nodes, errors};
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = absoluteFromSourceFile(component.getSourceFile());
|
||||||
|
|
||||||
|
const fileRecord = this.getFileData(filePath);
|
||||||
|
const id = fileRecord.sourceManager.getTemplateId(component);
|
||||||
|
|
||||||
|
if (fileRecord.templateOverrides === null) {
|
||||||
|
fileRecord.templateOverrides = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
fileRecord.templateOverrides.set(id, nodes);
|
||||||
|
|
||||||
|
// Clear data for the shim in question, so it'll be regenerated on the next request.
|
||||||
|
const shimFile = this.typeCheckingStrategy.shimPathForComponent(component);
|
||||||
|
fileRecord.shimData.delete(shimFile);
|
||||||
|
fileRecord.isComplete = false;
|
||||||
|
this.isComplete = false;
|
||||||
|
|
||||||
|
return {nodes};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve type-checking diagnostics from the given `ts.SourceFile` using the most recent
|
* Retrieve type-checking diagnostics from the given `ts.SourceFile` using the most recent
|
||||||
* type-checking program.
|
* type-checking program.
|
||||||
@ -106,6 +148,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
|||||||
const sfPath = absoluteFromSourceFile(sf);
|
const sfPath = absoluteFromSourceFile(sf);
|
||||||
if (this.state.has(sfPath)) {
|
if (this.state.has(sfPath)) {
|
||||||
const existingResults = this.state.get(sfPath)!;
|
const existingResults = this.state.get(sfPath)!;
|
||||||
|
if (existingResults.templateOverrides !== null) {
|
||||||
|
// Cannot adopt prior results if template overrides have been requested.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (existingResults.isComplete) {
|
if (existingResults.isComplete) {
|
||||||
// All data for this file has already been generated, so no need to adopt anything.
|
// All data for this file has already been generated, so no need to adopt anything.
|
||||||
@ -114,7 +160,8 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousResults = this.priorBuild.priorTypeCheckingResultsFor(sf);
|
const previousResults = this.priorBuild.priorTypeCheckingResultsFor(sf);
|
||||||
if (previousResults === null || !previousResults.isComplete) {
|
if (previousResults === null || !previousResults.isComplete ||
|
||||||
|
previousResults.templateOverrides !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,6 +261,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
|
|||||||
if (!this.state.has(path)) {
|
if (!this.state.has(path)) {
|
||||||
this.state.set(path, {
|
this.state.set(path, {
|
||||||
hasInlines: false,
|
hasInlines: false,
|
||||||
|
templateOverrides: null,
|
||||||
sourceManager: new TemplateSourceManager(),
|
sourceManager: new TemplateSourceManager(),
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
shimData: new Map(),
|
shimData: new Map(),
|
||||||
@ -248,6 +296,11 @@ export interface FileTypeCheckingData {
|
|||||||
*/
|
*/
|
||||||
sourceManager: TemplateSourceManager;
|
sourceManager: TemplateSourceManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of template overrides applied to any components in this input file.
|
||||||
|
*/
|
||||||
|
templateOverrides: Map<TemplateId, TmplAstNode[]>|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data for each shim generated from this input file.
|
* Data for each shim generated from this input file.
|
||||||
*
|
*
|
||||||
@ -280,6 +333,20 @@ class WholeProgramTypeCheckingHost implements TypeCheckingHost {
|
|||||||
return !fileData.shimData.has(shimPath);
|
return !fileData.shimData.has(shimPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TmplAstNode[]|null {
|
||||||
|
const fileData = this.impl.getFileData(sfPath);
|
||||||
|
if (fileData.templateOverrides === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateId = fileData.sourceManager.getTemplateId(node);
|
||||||
|
if (fileData.templateOverrides.has(templateId)) {
|
||||||
|
return fileData.templateOverrides.get(templateId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
recordShimData(sfPath: AbsoluteFsPath, data: ShimTypeCheckingData): void {
|
recordShimData(sfPath: AbsoluteFsPath, data: ShimTypeCheckingData): void {
|
||||||
const fileData = this.impl.getFileData(sfPath);
|
const fileData = this.impl.getFileData(sfPath);
|
||||||
fileData.shimData.set(data.path, data);
|
fileData.shimData.set(data.path, data);
|
||||||
@ -324,6 +391,20 @@ class SingleFileTypeCheckingHost implements TypeCheckingHost {
|
|||||||
return !this.fileData.shimData.has(shimPath);
|
return !this.fileData.shimData.has(shimPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TmplAstNode[]|null {
|
||||||
|
this.assertPath(sfPath);
|
||||||
|
if (this.fileData.templateOverrides === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateId = this.fileData.sourceManager.getTemplateId(node);
|
||||||
|
if (this.fileData.templateOverrides.has(templateId)) {
|
||||||
|
return this.fileData.templateOverrides.get(templateId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
recordShimData(sfPath: AbsoluteFsPath, data: ShimTypeCheckingData): void {
|
recordShimData(sfPath: AbsoluteFsPath, data: ShimTypeCheckingData): void {
|
||||||
this.assertPath(sfPath);
|
this.assertPath(sfPath);
|
||||||
|
|
||||||
|
@ -102,6 +102,12 @@ export interface TypeCheckingHost {
|
|||||||
*/
|
*/
|
||||||
shouldCheckComponent(node: ts.ClassDeclaration): boolean;
|
shouldCheckComponent(node: ts.ClassDeclaration): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given component has had its template overridden, and retrieve the new template
|
||||||
|
* nodes if so.
|
||||||
|
*/
|
||||||
|
getTemplateOverride(sfPath: AbsoluteFsPath, node: ts.ClassDeclaration): TmplAstNode[]|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report data from a shim generated from the given input file path.
|
* Report data from a shim generated from the given input file path.
|
||||||
*/
|
*/
|
||||||
@ -175,6 +181,13 @@ export class TypeCheckContextImpl implements TypeCheckContext {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sfPath = absoluteFromSourceFile(ref.node.getSourceFile());
|
||||||
|
|
||||||
|
const overrideTemplate = this.host.getTemplateOverride(sfPath, ref.node);
|
||||||
|
if (overrideTemplate !== null) {
|
||||||
|
template = overrideTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
// Accumulate a list of any directives which could not have type constructors generated due to
|
// Accumulate a list of any directives which could not have type constructors generated due to
|
||||||
// unsupported inlining operations.
|
// unsupported inlining operations.
|
||||||
let missingInlines: ClassDeclaration[] = [];
|
let missingInlines: ClassDeclaration[] = [];
|
||||||
|
@ -209,5 +209,114 @@ runInEachFileSystem(() => {
|
|||||||
expect(absoluteFromSourceFile(diags[0].relatedInformation![0].file!)).toBe(dirFile);
|
expect(absoluteFromSourceFile(diags[0].relatedInformation![0].file!)).toBe(dirFile);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('template overrides', () => {
|
||||||
|
it('should override a simple template', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const {program, templateTypeChecker} = setup([{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': '<div></div>'},
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
const tcbReal = templateTypeChecker.getTypeCheckBlock(cmp)!;
|
||||||
|
expect(tcbReal.getText()).toContain('div');
|
||||||
|
|
||||||
|
templateTypeChecker.overrideComponentTemplate(cmp, '<span></span>');
|
||||||
|
const tcbOverridden = templateTypeChecker.getTypeCheckBlock(cmp);
|
||||||
|
expect(tcbOverridden).not.toBeNull();
|
||||||
|
expect(tcbOverridden!.getText()).not.toContain('div');
|
||||||
|
expect(tcbOverridden!.getText()).toContain('span');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear overrides on request', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const {program, templateTypeChecker} = setup([{
|
||||||
|
fileName,
|
||||||
|
templates: {'Cmp': '<div></div>'},
|
||||||
|
}]);
|
||||||
|
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
templateTypeChecker.overrideComponentTemplate(cmp, '<span></span>');
|
||||||
|
const tcbOverridden = templateTypeChecker.getTypeCheckBlock(cmp)!;
|
||||||
|
expect(tcbOverridden.getText()).not.toContain('div');
|
||||||
|
expect(tcbOverridden.getText()).toContain('span');
|
||||||
|
|
||||||
|
templateTypeChecker.resetOverrides();
|
||||||
|
|
||||||
|
// The template should be back to the original, which has <div> and not <span>.
|
||||||
|
const tcbReal = templateTypeChecker.getTypeCheckBlock(cmp)!;
|
||||||
|
expect(tcbReal.getText()).toContain('div');
|
||||||
|
expect(tcbReal.getText()).not.toContain('span');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should override a template and make use of previously unused directives', () => {
|
||||||
|
const fileName = absoluteFrom('/main.ts');
|
||||||
|
const dirFile = absoluteFrom('/dir.ts');
|
||||||
|
const {program, templateTypeChecker} = setup(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
fileName,
|
||||||
|
source: `export class Cmp {}`,
|
||||||
|
templates: {'Cmp': '<div></div>'},
|
||||||
|
declarations: [{
|
||||||
|
name: 'TestDir',
|
||||||
|
selector: '[dir]',
|
||||||
|
file: dirFile,
|
||||||
|
type: 'directive',
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fileName: dirFile,
|
||||||
|
source: `export class TestDir {}`,
|
||||||
|
templates: {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{inlining: false});
|
||||||
|
const sf = getSourceFileOrError(program, fileName);
|
||||||
|
const cmp = getClass(sf, 'Cmp');
|
||||||
|
|
||||||
|
// TestDir is initially unused. Note that this checks the entire text of the ngtypecheck
|
||||||
|
// file, to ensure it captures not just the TCB function but also any inline type
|
||||||
|
// constructors.
|
||||||
|
const tcbReal = templateTypeChecker.getTypeCheckBlock(cmp)!;
|
||||||
|
expect(tcbReal.getSourceFile().text).not.toContain('TestDir');
|
||||||
|
|
||||||
|
templateTypeChecker.overrideComponentTemplate(cmp, '<div dir></div>');
|
||||||
|
|
||||||
|
const tcbOverridden = templateTypeChecker.getTypeCheckBlock(cmp);
|
||||||
|
expect(tcbOverridden).not.toBeNull();
|
||||||
|
expect(tcbOverridden!.getSourceFile().text).toContain('TestDir');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not invalidate other templates when an override is requested', () => {
|
||||||
|
const file1 = absoluteFrom('/file1.ts');
|
||||||
|
const file2 = absoluteFrom('/file2.ts');
|
||||||
|
const {program, templateTypeChecker, programStrategy} = setup([
|
||||||
|
{fileName: file1, templates: {'Cmp1': '<div></div>'}},
|
||||||
|
{fileName: file2, templates: {'Cmp2': '<span></span>'}}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const cmp1 = getClass(getSourceFileOrError(program, file1), 'Cmp1');
|
||||||
|
const cmp2 = getClass(getSourceFileOrError(program, file2), 'Cmp2');
|
||||||
|
|
||||||
|
// To test this scenario, Cmp1's type check block will be captured, then Cmp2's template
|
||||||
|
// will be overridden. Cmp1's type check block should not change as a result.
|
||||||
|
const originalTcb = templateTypeChecker.getTypeCheckBlock(cmp1)!;
|
||||||
|
|
||||||
|
templateTypeChecker.overrideComponentTemplate(cmp2, '<p></p>');
|
||||||
|
|
||||||
|
// Trigger generation of the TCB for Cmp2.
|
||||||
|
templateTypeChecker.getTypeCheckBlock(cmp2);
|
||||||
|
|
||||||
|
// Verify that Cmp1's TCB has not changed.
|
||||||
|
const currentTcb = templateTypeChecker.getTypeCheckBlock(cmp1)!;
|
||||||
|
expect(currentTcb).toBe(originalTcb);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user