fix(compiler): non-literal inline templates incorrectly processed in partial compilation (#41583)

Currently if a component defines a template inline, but not through a
string literal, the partial compilation references the template expression
as is. This is problematic because the component declaration can no longer
be processed by the linker later as there is no static interpretation. e.g.

```js
const myTemplate = `...`;

TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({
  version: "0.0.0-PLACEHOLDER",
  type: TestCmp,
  selector: "test-cmp",
  ngImport: i0,
  template: myTemplate,
  isInline: true
});
```

To fix this, we use the the resolved template in such cases so that
the linker can process the template/component declaration as expected.

PR Close #41583
This commit is contained in:
Paul Gschwendtner 2021-04-14 20:36:24 +02:00 committed by Andrew Kushnir
parent c855bf4c56
commit 62e3f3279d
10 changed files with 166 additions and 16 deletions

View File

@ -886,8 +886,8 @@ export class ComponentDecoratorHandler implements
content: analysis.template.content,
sourceUrl: analysis.template.declaration.resolvedTemplateUrl,
isInline: analysis.template.declaration.isInline,
inlineTemplateExpression: analysis.template.declaration.isInline ?
new WrappedNodeExpr(analysis.template.declaration.expression) :
inlineTemplateLiteralExpression: analysis.template.sourceMapping.type === 'direct' ?
new WrappedNodeExpr(analysis.template.sourceMapping.node) :
null,
};
const meta: R3ComponentMetadata = {...analysis.meta, ...resolution};

View File

@ -493,3 +493,77 @@ export declare class MyModule {
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
}
/****************************************************************************************************
* PARTIAL FILE: non_literal_template.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
const myTemplate = `<div *ngIf="show">Hello</div>`;
export class TestCmp {
}
TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component });
TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, selector: "test-cmp", ngImport: i0, template: "<div *ngIf=\"show\">Hello</div>", isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{
type: Component,
args: [{ selector: 'test-cmp', template: myTemplate }]
}] });
/****************************************************************************************************
* PARTIAL FILE: non_literal_template.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestCmp {
static ɵfac: i0.ɵɵFactoryDeclaration<TestCmp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestCmp, "test-cmp", never, {}, {}, never, never>;
}
/****************************************************************************************************
* PARTIAL FILE: non_literal_template_with_substitution.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
const greeting = 'Hello!';
const myTemplate = `<div *ngIf="show">${greeting}</div>`;
export class TestCmp {
}
TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component });
TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, selector: "test-cmp", ngImport: i0, template: "<div *ngIf=\"show\">Hello!</div>", isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{
type: Component,
args: [{ selector: 'test-cmp', template: myTemplate }]
}] });
/****************************************************************************************************
* PARTIAL FILE: non_literal_template_with_substitution.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestCmp {
static ɵfac: i0.ɵɵFactoryDeclaration<TestCmp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestCmp, "test-cmp", never, {}, {}, never, never>;
}
/****************************************************************************************************
* PARTIAL FILE: non_literal_template_with_concatenation.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
const greeting = 'Hello!';
const myTemplate = '<div *ngIf="show">' + greeting + '</div>';
export class TestCmp {
}
TestCmp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, deps: [], target: i0.ɵɵFactoryTarget.Component });
TestCmp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", type: TestCmp, selector: "test-cmp", ngImport: i0, template: "<div *ngIf=\"show\">Hello!</div>", isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestCmp, decorators: [{
type: Component,
args: [{ selector: 'test-cmp', template: myTemplate }]
}] });
/****************************************************************************************************
* PARTIAL FILE: non_literal_template_with_concatenation.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestCmp {
static ɵfac: i0.ɵɵFactoryDeclaration<TestCmp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestCmp, "test-cmp", never, {}, {}, never, never>;
}

View File

@ -160,6 +160,24 @@
]
}
]
},
{
"description": "should support inline non-literal templates",
"inputFiles": [
"non_literal_template.ts"
]
},
{
"description": "should support inline non-literal templates using substitution",
"inputFiles": [
"non_literal_template_with_substitution.ts"
]
},
{
"description": "should support inline non-literal templates using string concatenation",
"inputFiles": [
"non_literal_template_with_concatenation.ts"
]
}
]
}

View File

@ -0,0 +1,7 @@
function TestCmp_div_0_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "div");
i0.ɵɵtext(1, "Hello");
i0.ɵɵelementEnd();
}
}

View File

@ -0,0 +1,7 @@
import {Component} from '@angular/core';
const myTemplate = `<div *ngIf="show">Hello</div>`;
@Component({selector: 'test-cmp', template: myTemplate})
export class TestCmp {
}

View File

@ -0,0 +1,7 @@
function TestCmp_div_0_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "div");
i0.ɵɵtext(1, "Hello!");
i0.ɵɵelementEnd();
}
}

View File

@ -0,0 +1,8 @@
import {Component} from '@angular/core';
const greeting = 'Hello!';
const myTemplate = '<div *ngIf="show">' + greeting + '</div>';
@Component({selector: 'test-cmp', template: myTemplate})
export class TestCmp {
}

View File

@ -0,0 +1,7 @@
function TestCmp_div_0_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "div");
i0.ɵɵtext(1, "Hello!");
i0.ɵɵelementEnd();
}
}

View File

@ -0,0 +1,8 @@
import {Component} from '@angular/core';
const greeting = 'Hello!';
const myTemplate = `<div *ngIf="show">${greeting}</div>`;
@Component({selector: 'test-cmp', template: myTemplate})
export class TestCmp {
}

View File

@ -42,8 +42,11 @@ export interface DeclareComponentTemplateInfo {
*/
isInline: boolean;
/** Expression that resolves to the inline template. */
inlineTemplateExpression: o.Expression|null;
/**
* If the template was defined inline by a direct string literal, then this is that literal
* expression. Otherwise `null`, if the template was not defined inline or was not a literal.
*/
inlineTemplateLiteralExpression: o.Expression|null;
}
/**
@ -111,19 +114,30 @@ export function createComponentDefinitionMap(
function getTemplateExpression(
template: ParsedTemplate, templateInfo: DeclareComponentTemplateInfo): o.Expression {
if (templateInfo.isInline) {
// The template is inline so we can just reuse the current expression node.
return templateInfo.inlineTemplateExpression!;
} else {
// The template is external so we must synthesize an expression node with the appropriate
// source-span.
const contents = templateInfo.content;
const file = new ParseSourceFile(contents, templateInfo.sourceUrl);
const start = new ParseLocation(file, 0, 0, 0);
const end = computeEndLocation(file, contents);
const span = new ParseSourceSpan(start, end);
return o.literal(contents, null, span);
// If the template has been defined using a direct literal, we use that expression directly
// without any modifications. This is ensures proper source mapping from the partially
// compiled code to the source file declaring the template. Note that this does not capture
// template literals referenced indirectly through an identifier.
if (templateInfo.inlineTemplateLiteralExpression !== null) {
return templateInfo.inlineTemplateLiteralExpression;
}
// If the template is defined inline but not through a literal, the template has been resolved
// through static interpretation. We create a literal but cannot provide any source span. Note
// that we cannot use the expression defining the template because the linker expects the template
// to be defined as a literal in the declaration.
if (templateInfo.isInline) {
return o.literal(templateInfo.content, null, null);
}
// The template is external so we must synthesize an expression node with
// the appropriate source-span.
const contents = templateInfo.content;
const file = new ParseSourceFile(contents, templateInfo.sourceUrl);
const start = new ParseLocation(file, 0, 0, 0);
const end = computeEndLocation(file, contents);
const span = new ParseSourceSpan(start, end);
return o.literal(contents, null, span);
}
function computeEndLocation(file: ParseSourceFile, contents: string): ParseLocation {