refactor(compiler-cli): split template parsing into declaration/parse steps (#40561)
To prepare for the optimization of template-only changes, this commit refactors the `ComponentDecoratorHandler`'s handling of template parsing. Previously, templates were extracted from the raw decorator metadata and parsed in a single operation. To better handle incremental template updates, this commit splits this operation into a "declaration" step where the template info is extracted from the decorator metadata, and a "parsing" step where the declared template is read and parsed. This allows for re-reading and re-parsing of the declared template at a future point, using the same template declaration extracted from the decorator. PR Close #40561
This commit is contained in:
parent
21e24d1474
commit
52aeb5326d
@ -11,7 +11,7 @@ import * as ts from 'typescript';
|
|||||||
|
|
||||||
import {CycleAnalyzer} from '../../cycles';
|
import {CycleAnalyzer} from '../../cycles';
|
||||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||||
import {absoluteFrom, relative} from '../../file_system';
|
import {absoluteFrom, AbsoluteFsPath, relative} from '../../file_system';
|
||||||
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
||||||
import {DependencyTracker} from '../../incremental/api';
|
import {DependencyTracker} from '../../incremental/api';
|
||||||
import {IndexingContext} from '../../indexer';
|
import {IndexingContext} from '../../indexer';
|
||||||
@ -246,24 +246,14 @@ export class ComponentDecoratorHandler implements
|
|||||||
|
|
||||||
template = preanalyzed;
|
template = preanalyzed;
|
||||||
} else {
|
} else {
|
||||||
// The template was not already parsed. Either there's a templateUrl, or an inline template.
|
const templateDecl = this.parseTemplateDeclaration(decorator, component, containingFile);
|
||||||
if (component.has('templateUrl')) {
|
template = this.extractTemplate(node, templateDecl);
|
||||||
const templateUrlExpr = component.get('templateUrl')!;
|
|
||||||
const templateUrl = this.evaluator.evaluate(templateUrlExpr);
|
|
||||||
if (typeof templateUrl !== 'string') {
|
|
||||||
throw createValueHasWrongTypeError(
|
|
||||||
templateUrlExpr, templateUrl, 'templateUrl must be a string');
|
|
||||||
}
|
}
|
||||||
const resourceUrl = this.resourceLoader.resolve(templateUrl, containingFile);
|
const templateResource =
|
||||||
template = this._extractExternalTemplate(node, component, templateUrlExpr, resourceUrl);
|
template.isInline ? {path: null, expression: component.get('template')!} : {
|
||||||
} else {
|
path: absoluteFrom(template.declaration.resolvedTemplateUrl),
|
||||||
// Expect an inline template to be present.
|
expression: template.sourceMapping.node
|
||||||
template = this._extractInlineTemplate(node, decorator, component, containingFile);
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
const templateResource = template.isInline ?
|
|
||||||
{path: null, expression: component.get('template')!} :
|
|
||||||
{path: absoluteFrom(template.templateUrl), expression: template.sourceMapping.node};
|
|
||||||
|
|
||||||
// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
|
// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
|
||||||
// precede inline styles, and styles defined in the template override styles defined in the
|
// precede inline styles, and styles defined in the template override styles defined in the
|
||||||
@ -732,8 +722,8 @@ export class ComponentDecoratorHandler implements
|
|||||||
// URLs to resolve.
|
// URLs to resolve.
|
||||||
if (templatePromise !== undefined) {
|
if (templatePromise !== undefined) {
|
||||||
return templatePromise.then(() => {
|
return templatePromise.then(() => {
|
||||||
const template =
|
const templateDecl = this.parseTemplateDeclaration(decorator, component, containingFile);
|
||||||
this._extractExternalTemplate(node, component, templateUrlExpr, resourceUrl);
|
const template = this.extractTemplate(node, templateDecl);
|
||||||
this.preanalyzeTemplateCache.set(node, template);
|
this.preanalyzeTemplateCache.set(node, template);
|
||||||
return template;
|
return template;
|
||||||
});
|
});
|
||||||
@ -741,92 +731,134 @@ export class ComponentDecoratorHandler implements
|
|||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const template = this._extractInlineTemplate(node, decorator, component, containingFile);
|
const templateDecl = this.parseTemplateDeclaration(decorator, component, containingFile);
|
||||||
|
const template = this.extractTemplate(node, templateDecl);
|
||||||
this.preanalyzeTemplateCache.set(node, template);
|
this.preanalyzeTemplateCache.set(node, template);
|
||||||
return Promise.resolve(template);
|
return Promise.resolve(template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _extractExternalTemplate(
|
private extractTemplate(node: ClassDeclaration, template: TemplateDeclaration):
|
||||||
node: ClassDeclaration, component: Map<string, ts.Expression>, templateUrlExpr: ts.Expression,
|
ParsedTemplateWithSource {
|
||||||
resourceUrl: string): ParsedTemplateWithSource {
|
if (template.isInline) {
|
||||||
const templateStr = this.resourceLoader.load(resourceUrl);
|
|
||||||
if (this.depTracker !== null) {
|
|
||||||
this.depTracker.addResourceDependency(node.getSourceFile(), absoluteFrom(resourceUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
const template = this._parseTemplate(
|
|
||||||
component, templateStr, /* templateLiteral */ null, sourceMapUrl(resourceUrl),
|
|
||||||
/* templateRange */ undefined,
|
|
||||||
/* escapedString */ false);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...template,
|
|
||||||
sourceMapping: {
|
|
||||||
type: 'external',
|
|
||||||
componentClass: node,
|
|
||||||
node: templateUrlExpr,
|
|
||||||
template: templateStr,
|
|
||||||
templateUrl: resourceUrl,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private _extractInlineTemplate(
|
|
||||||
node: ClassDeclaration, decorator: Decorator, component: Map<string, ts.Expression>,
|
|
||||||
containingFile: string): ParsedTemplateWithSource {
|
|
||||||
if (!component.has('template')) {
|
|
||||||
throw new FatalDiagnosticError(
|
|
||||||
ErrorCode.COMPONENT_MISSING_TEMPLATE, Decorator.nodeForError(decorator),
|
|
||||||
'component is missing a template');
|
|
||||||
}
|
|
||||||
const templateExpr = component.get('template')!;
|
|
||||||
|
|
||||||
let templateStr: string;
|
let templateStr: string;
|
||||||
let templateLiteral: ts.Node|null = null;
|
let templateLiteral: ts.Node|null = null;
|
||||||
let templateUrl: string = '';
|
let templateUrl: string = '';
|
||||||
let templateRange: LexerRange|undefined = undefined;
|
let templateRange: LexerRange|null = null;
|
||||||
let sourceMapping: TemplateSourceMapping;
|
let sourceMapping: TemplateSourceMapping;
|
||||||
let escapedString = false;
|
let escapedString = false;
|
||||||
// We only support SourceMaps for inline templates that are simple string literals.
|
// We only support SourceMaps for inline templates that are simple string literals.
|
||||||
if (ts.isStringLiteral(templateExpr) || ts.isNoSubstitutionTemplateLiteral(templateExpr)) {
|
if (ts.isStringLiteral(template.expression) ||
|
||||||
// the start and end of the `templateExpr` node includes the quotation marks, which we
|
ts.isNoSubstitutionTemplateLiteral(template.expression)) {
|
||||||
// must
|
// the start and end of the `templateExpr` node includes the quotation marks, which we must
|
||||||
// strip
|
// strip
|
||||||
templateRange = getTemplateRange(templateExpr);
|
templateRange = getTemplateRange(template.expression);
|
||||||
templateStr = templateExpr.getSourceFile().text;
|
templateStr = template.expression.getSourceFile().text;
|
||||||
templateLiteral = templateExpr;
|
templateLiteral = template.expression;
|
||||||
templateUrl = containingFile;
|
templateUrl = template.templateUrl;
|
||||||
escapedString = true;
|
escapedString = true;
|
||||||
sourceMapping = {
|
sourceMapping = {
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
node: templateExpr as (ts.StringLiteral | ts.NoSubstitutionTemplateLiteral),
|
node: template.expression,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const resolvedTemplate = this.evaluator.evaluate(templateExpr);
|
const resolvedTemplate = this.evaluator.evaluate(template.expression);
|
||||||
if (typeof resolvedTemplate !== 'string') {
|
if (typeof resolvedTemplate !== 'string') {
|
||||||
throw createValueHasWrongTypeError(
|
throw createValueHasWrongTypeError(
|
||||||
templateExpr, resolvedTemplate, 'template must be a string');
|
template.expression, resolvedTemplate, 'template must be a string');
|
||||||
}
|
}
|
||||||
templateStr = resolvedTemplate;
|
templateStr = resolvedTemplate;
|
||||||
sourceMapping = {
|
sourceMapping = {
|
||||||
type: 'indirect',
|
type: 'indirect',
|
||||||
node: templateExpr,
|
node: template.expression,
|
||||||
componentClass: node,
|
componentClass: node,
|
||||||
template: templateStr,
|
template: templateStr,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = this._parseTemplate(
|
return {
|
||||||
component, templateStr, templateLiteral, templateUrl, templateRange, escapedString);
|
...this._parseTemplate(template, templateStr, templateRange, escapedString),
|
||||||
|
sourceMapping,
|
||||||
|
declaration: template,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const templateStr = this.resourceLoader.load(template.resolvedTemplateUrl);
|
||||||
|
if (this.depTracker !== null) {
|
||||||
|
this.depTracker.addResourceDependency(
|
||||||
|
node.getSourceFile(), absoluteFrom(template.resolvedTemplateUrl));
|
||||||
|
}
|
||||||
|
|
||||||
return {...template, sourceMapping};
|
return {
|
||||||
|
...this._parseTemplate(
|
||||||
|
template, templateStr, /* templateRange */ null,
|
||||||
|
/* escapedString */ false),
|
||||||
|
sourceMapping: {
|
||||||
|
type: 'external',
|
||||||
|
componentClass: node,
|
||||||
|
// TODO(alxhub): TS in g3 is unable to make this inference on its own, so cast it here
|
||||||
|
// until g3 is able to figure this out.
|
||||||
|
node: (template as ExternalTemplateDeclaration).templateUrlExpression,
|
||||||
|
template: templateStr,
|
||||||
|
templateUrl: template.resolvedTemplateUrl,
|
||||||
|
},
|
||||||
|
declaration: template,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parseTemplate(
|
private _parseTemplate(
|
||||||
component: Map<string, ts.Expression>, templateStr: string, templateLiteral: ts.Node|null,
|
template: TemplateDeclaration, templateStr: string, templateRange: LexerRange|null,
|
||||||
templateUrl: string, templateRange: LexerRange|undefined,
|
|
||||||
escapedString: boolean): ParsedComponentTemplate {
|
escapedString: boolean): ParsedComponentTemplate {
|
||||||
|
// We always normalize line endings if the template has been escaped (i.e. is inline).
|
||||||
|
const i18nNormalizeLineEndingsInICUs = escapedString || this.i18nNormalizeLineEndingsInICUs;
|
||||||
|
|
||||||
|
const parsedTemplate = parseTemplate(templateStr, template.sourceMapUrl, {
|
||||||
|
preserveWhitespaces: template.preserveWhitespaces,
|
||||||
|
interpolationConfig: template.interpolationConfig,
|
||||||
|
range: templateRange ?? undefined,
|
||||||
|
escapedString,
|
||||||
|
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
||||||
|
i18nNormalizeLineEndingsInICUs,
|
||||||
|
isInline: template.isInline,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unfortunately, the primary parse of the template above may not contain accurate source map
|
||||||
|
// information. If used directly, it would result in incorrect code locations in template
|
||||||
|
// errors, etc. There are two main problems:
|
||||||
|
//
|
||||||
|
// 1. `preserveWhitespaces: false` annihilates the correctness of template source mapping, as
|
||||||
|
// the whitespace transformation changes the contents of HTML text nodes before they're
|
||||||
|
// parsed into Angular expressions.
|
||||||
|
// 2. By default, the template parser strips leading trivia characters (like spaces, tabs, and
|
||||||
|
// newlines). This also destroys source mapping information.
|
||||||
|
//
|
||||||
|
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
|
||||||
|
// with the above options set to preserve source mappings.
|
||||||
|
|
||||||
|
const {nodes: diagNodes} = parseTemplate(templateStr, template.sourceMapUrl, {
|
||||||
|
preserveWhitespaces: true,
|
||||||
|
interpolationConfig: template.interpolationConfig,
|
||||||
|
range: templateRange ?? undefined,
|
||||||
|
escapedString,
|
||||||
|
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
||||||
|
i18nNormalizeLineEndingsInICUs,
|
||||||
|
leadingTriviaChars: [],
|
||||||
|
isInline: template.isInline,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...parsedTemplate,
|
||||||
|
diagNodes,
|
||||||
|
template: template.isInline ? new WrappedNodeExpr(template.expression) : templateStr,
|
||||||
|
templateUrl: template.resolvedTemplateUrl,
|
||||||
|
isInline: template.isInline,
|
||||||
|
file: new ParseSourceFile(templateStr, template.resolvedTemplateUrl),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseTemplateDeclaration(
|
||||||
|
decorator: Decorator, component: Map<string, ts.Expression>,
|
||||||
|
containingFile: string): TemplateDeclaration {
|
||||||
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
|
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
|
||||||
if (component.has('preserveWhitespaces')) {
|
if (component.has('preserveWhitespaces')) {
|
||||||
const expr = component.get('preserveWhitespaces')!;
|
const expr = component.get('preserveWhitespaces')!;
|
||||||
@ -849,52 +881,39 @@ export class ComponentDecoratorHandler implements
|
|||||||
interpolationConfig = InterpolationConfig.fromArray(value as [string, string]);
|
interpolationConfig = InterpolationConfig.fromArray(value as [string, string]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We always normalize line endings if the template has been escaped (i.e. is inline).
|
if (component.has('templateUrl')) {
|
||||||
const i18nNormalizeLineEndingsInICUs = escapedString || this.i18nNormalizeLineEndingsInICUs;
|
const templateUrlExpr = component.get('templateUrl')!;
|
||||||
|
const templateUrl = this.evaluator.evaluate(templateUrlExpr);
|
||||||
const isInline = component.has('template');
|
if (typeof templateUrl !== 'string') {
|
||||||
const parsedTemplate = parseTemplate(templateStr, templateUrl, {
|
throw createValueHasWrongTypeError(
|
||||||
preserveWhitespaces,
|
templateUrlExpr, templateUrl, 'templateUrl must be a string');
|
||||||
interpolationConfig,
|
}
|
||||||
range: templateRange,
|
const resourceUrl = this.resourceLoader.resolve(templateUrl, containingFile);
|
||||||
escapedString,
|
|
||||||
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
|
||||||
i18nNormalizeLineEndingsInICUs,
|
|
||||||
isInline,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unfortunately, the primary parse of the template above may not contain accurate source map
|
|
||||||
// information. If used directly, it would result in incorrect code locations in template
|
|
||||||
// errors, etc. There are two main problems:
|
|
||||||
//
|
|
||||||
// 1. `preserveWhitespaces: false` annihilates the correctness of template source mapping, as
|
|
||||||
// the whitespace transformation changes the contents of HTML text nodes before they're
|
|
||||||
// parsed into Angular expressions.
|
|
||||||
// 2. By default, the template parser strips leading trivia characters (like spaces, tabs, and
|
|
||||||
// newlines). This also destroys source mapping information.
|
|
||||||
//
|
|
||||||
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
|
|
||||||
// with the above options set to preserve source mappings.
|
|
||||||
|
|
||||||
const {nodes: diagNodes} = parseTemplate(templateStr, templateUrl, {
|
|
||||||
preserveWhitespaces: true,
|
|
||||||
interpolationConfig,
|
|
||||||
range: templateRange,
|
|
||||||
escapedString,
|
|
||||||
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
|
|
||||||
i18nNormalizeLineEndingsInICUs,
|
|
||||||
leadingTriviaChars: [],
|
|
||||||
isInline,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...parsedTemplate,
|
isInline: false,
|
||||||
diagNodes,
|
interpolationConfig,
|
||||||
template: templateLiteral !== null ? new WrappedNodeExpr(templateLiteral) : templateStr,
|
preserveWhitespaces,
|
||||||
templateUrl,
|
templateUrl,
|
||||||
isInline,
|
templateUrlExpression: templateUrlExpr,
|
||||||
file: new ParseSourceFile(templateStr, templateUrl),
|
resolvedTemplateUrl: resourceUrl,
|
||||||
|
sourceMapUrl: sourceMapUrl(resourceUrl),
|
||||||
};
|
};
|
||||||
|
} else if (component.has('template')) {
|
||||||
|
return {
|
||||||
|
isInline: true,
|
||||||
|
interpolationConfig,
|
||||||
|
preserveWhitespaces,
|
||||||
|
expression: component.get('template')!,
|
||||||
|
templateUrl: containingFile,
|
||||||
|
resolvedTemplateUrl: containingFile,
|
||||||
|
sourceMapUrl: containingFile,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new FatalDiagnosticError(
|
||||||
|
ErrorCode.COMPONENT_MISSING_TEMPLATE, Decorator.nodeForError(decorator),
|
||||||
|
'component is missing a template');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _expressionToImportedFile(expr: Expression, origin: ts.SourceFile): ts.SourceFile|null {
|
private _expressionToImportedFile(expr: Expression, origin: ts.SourceFile): ts.SourceFile|null {
|
||||||
@ -978,4 +997,42 @@ export interface ParsedComponentTemplate extends ParsedTemplate {
|
|||||||
|
|
||||||
export interface ParsedTemplateWithSource extends ParsedComponentTemplate {
|
export interface ParsedTemplateWithSource extends ParsedComponentTemplate {
|
||||||
sourceMapping: TemplateSourceMapping;
|
sourceMapping: TemplateSourceMapping;
|
||||||
|
declaration: TemplateDeclaration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common fields extracted from the declaration of a template.
|
||||||
|
*/
|
||||||
|
interface CommonTemplateDeclaration {
|
||||||
|
preserveWhitespaces: boolean;
|
||||||
|
interpolationConfig: InterpolationConfig;
|
||||||
|
templateUrl: string;
|
||||||
|
resolvedTemplateUrl: string;
|
||||||
|
sourceMapUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information extracted from the declaration of an inline template.
|
||||||
|
*/
|
||||||
|
interface InlineTemplateDeclaration extends CommonTemplateDeclaration {
|
||||||
|
isInline: true;
|
||||||
|
expression: ts.Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information extracted from the declaration of an external template.
|
||||||
|
*/
|
||||||
|
interface ExternalTemplateDeclaration extends CommonTemplateDeclaration {
|
||||||
|
isInline: false;
|
||||||
|
templateUrlExpression: ts.Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The declaration of a template extracted from a component decorator.
|
||||||
|
*
|
||||||
|
* This data is extracted and stored separately to faciliate re-interpreting the template
|
||||||
|
* declaration whenever the compiler is notified of a change to a template file. With this
|
||||||
|
* information, `ComponentDecoratorHandler` is able to re-read the template and update the component
|
||||||
|
* record without needing to parse the original decorator again.
|
||||||
|
*/
|
||||||
|
type TemplateDeclaration = InlineTemplateDeclaration|ExternalTemplateDeclaration;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user