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:
parent
c855bf4c56
commit
62e3f3279d
|
@ -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};
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
function TestCmp_div_0_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "div");
|
||||
i0.ɵɵtext(1, "Hello");
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
function TestCmp_div_0_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "div");
|
||||
i0.ɵɵtext(1, "Hello!");
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
function TestCmp_div_0_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
i0.ɵɵelementStart(0, "div");
|
||||
i0.ɵɵtext(1, "Hello!");
|
||||
i0.ɵɵelementEnd();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue