diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index 7b4ff3e2b5..793dc4e090 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -6,8 +6,7 @@ * 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'; /** @@ -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. */ 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`. * diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index d6197ac821..4c5dccfd74 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {ParseError, parseTemplate, TmplAstNode} from '@angular/compiler'; import * as ts from 'typescript'; import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; @@ -14,7 +15,7 @@ import {IncrementalBuild} from '../../incremental/api'; import {ReflectionHost} from '../../reflection'; import {isShim} from '../../shims'; 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 {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics'; @@ -37,6 +38,47 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { private compilerHost: Pick, private priorBuild: IncrementalBuild) {} + 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 * type-checking program. @@ -106,6 +148,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { const sfPath = absoluteFromSourceFile(sf); if (this.state.has(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) { // 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); - if (previousResults === null || !previousResults.isComplete) { + if (previousResults === null || !previousResults.isComplete || + previousResults.templateOverrides !== null) { return; } @@ -214,6 +261,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { if (!this.state.has(path)) { this.state.set(path, { hasInlines: false, + templateOverrides: null, sourceManager: new TemplateSourceManager(), isComplete: false, shimData: new Map(), @@ -248,6 +296,11 @@ export interface FileTypeCheckingData { */ sourceManager: TemplateSourceManager; + /** + * Map of template overrides applied to any components in this input file. + */ + templateOverrides: Map|null; + /** * Data for each shim generated from this input file. * @@ -280,6 +333,20 @@ class WholeProgramTypeCheckingHost implements TypeCheckingHost { 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 { const fileData = this.impl.getFileData(sfPath); fileData.shimData.set(data.path, data); @@ -324,6 +391,20 @@ class SingleFileTypeCheckingHost implements TypeCheckingHost { 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 { this.assertPath(sfPath); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index e5a155321d..5c5bb0c2f7 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -102,6 +102,12 @@ export interface TypeCheckingHost { */ 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. */ @@ -175,6 +181,13 @@ export class TypeCheckContextImpl implements TypeCheckContext { 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 // unsupported inlining operations. let missingInlines: ClassDeclaration[] = []; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts index b331c058eb..3e1763319a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts @@ -209,5 +209,114 @@ runInEachFileSystem(() => { 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': '
'}, + }]); + + const sf = getSourceFileOrError(program, fileName); + const cmp = getClass(sf, 'Cmp'); + + const tcbReal = templateTypeChecker.getTypeCheckBlock(cmp)!; + expect(tcbReal.getText()).toContain('div'); + + templateTypeChecker.overrideComponentTemplate(cmp, ''); + 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': '
'}, + }]); + + const sf = getSourceFileOrError(program, fileName); + const cmp = getClass(sf, 'Cmp'); + + templateTypeChecker.overrideComponentTemplate(cmp, ''); + 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
and not . + 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': '
'}, + 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, '
'); + + 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': '
'}}, + {fileName: file2, templates: {'Cmp2': ''}} + ]); + + 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, '

'); + + // 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); + }); + }); }); });