feat(ivy): support inline <style> and <link> tags in components (#28997)

Angular supports using <style> and <link> tags inline in component
templates, but previously such tags were not implemented within the ngtsc
compiler. This commit introduces that support.

FW-1069 #resolve

PR Close #28997
This commit is contained in:
Alex Rickabaugh 2019-02-26 14:48:42 -08:00 committed by Ben Lesh
parent 40833ba54b
commit 827e89cfc4
5 changed files with 320 additions and 128 deletions

View File

@ -21,8 +21,6 @@ const IGNORED_EXAMPLES = [
]; ];
const fixmeIvyExamples = [ const fixmeIvyExamples = [
// fixmeIvy('FW-1069: ngtsc does not support inline <style> and <link>')
'component-styles',
// fixmeIvy('unknown') app fails at runtime due to missing external service (goog is undefined) // fixmeIvy('unknown') app fails at runtime due to missing external service (goog is undefined)
'i18n' 'i18n'
]; ];
@ -319,7 +317,10 @@ function getE2eSpecs(basePath, filter) {
// Find all e2e specs in a given example folder. // Find all e2e specs in a given example folder.
function getE2eSpecsFor(basePath, specFile, filter) { function getE2eSpecsFor(basePath, specFile, filter) {
// Only get spec file at the example root. // Only get spec file at the example root.
// The formatter doesn't understand nested template string expressions (honestly, neither do I).
// clang-format off
const e2eSpecGlob = `${filter ? `*${filter}*` : '*'}/${specFile}`; const e2eSpecGlob = `${filter ? `*${filter}*` : '*'}/${specFile}`;
// clang-format on
return globby(e2eSpecGlob, {cwd: basePath, nodir: true}) return globby(e2eSpecGlob, {cwd: basePath, nodir: true})
.then( .then(
paths => paths.filter(file => !IGNORED_EXAMPLES.some(ignored => file.startsWith(ignored))) paths => paths.filter(file => !IGNORED_EXAMPLES.some(ignored => file.startsWith(ignored)))

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, R3ComponentMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler'; import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, LexerRange, ParseError, R3ComponentMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
@ -50,6 +50,13 @@ export class ComponentDecoratorHandler implements
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>(); private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
private elementSchemaRegistry = new DomElementSchemaRegistry(); private elementSchemaRegistry = new DomElementSchemaRegistry();
/**
* During the asynchronous preanalyze phase, it's necessary to parse the template to extract
* any potential <link> tags which might need to be loaded. This cache ensures that work is not
* thrown away, and the parsed template is reused during the analyze phase.
*/
private preanalyzeTemplateCache = new Map<ts.Declaration, ParsedTemplate>();
readonly precedence = HandlerPrecedence.PRIMARY; readonly precedence = HandlerPrecedence.PRIMARY;
detect(node: ts.Declaration, decorators: Decorator[]|null): DetectResult<Decorator>|undefined { detect(node: ts.Declaration, decorators: Decorator[]|null): DetectResult<Decorator>|undefined {
@ -69,44 +76,54 @@ export class ComponentDecoratorHandler implements
} }
preanalyze(node: ts.ClassDeclaration, decorator: Decorator): Promise<void>|undefined { preanalyze(node: ts.ClassDeclaration, decorator: Decorator): Promise<void>|undefined {
// In preanalyze, resource URLs associated with the component are asynchronously preloaded via
// the resourceLoader. This is the only time async operations are allowed for a component.
// These resources are:
//
// - the templateUrl, if there is one
// - any styleUrls if present
// - any stylesheets referenced from <link> tags in the template itself
//
// As a result of the last one, the template must be parsed as part of preanalysis to extract
// <link> tags, which may involve waiting for the templateUrl to be resolved first.
// If preloading isn't possible, then skip this step.
if (!this.resourceLoader.canPreload) { if (!this.resourceLoader.canPreload) {
return undefined; return undefined;
} }
const meta = this._resolveLiteral(decorator); const meta = this._resolveLiteral(decorator);
const component = reflectObjectLiteral(meta); const component = reflectObjectLiteral(meta);
const promises: Promise<void>[] = [];
const containingFile = node.getSourceFile().fileName; const containingFile = node.getSourceFile().fileName;
if (component.has('templateUrl')) { // Convert a styleUrl string into a Promise to preload it.
const templateUrlExpr = component.get('templateUrl') !; const resolveStyleUrl = (styleUrl: string): Promise<void> => {
const templateUrl = this.evaluator.evaluate(templateUrlExpr); const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
if (typeof templateUrl !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
}
const resourceUrl = this.resourceLoader.resolve(templateUrl, containingFile);
const promise = this.resourceLoader.preload(resourceUrl); const promise = this.resourceLoader.preload(resourceUrl);
if (promise !== undefined) { return promise || Promise.resolve();
promises.push(promise); };
}
}
const styleUrls = this._extractStyleUrls(component); // A Promise that waits for the template and all <link>ed styles within it to be preloaded.
if (styleUrls !== null) { const templateAndTemplateStyleResources =
for (const styleUrl of styleUrls) { this._preloadAndParseTemplate(node, decorator, component, containingFile).then(template => {
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile); if (template === null) {
const promise = this.resourceLoader.preload(resourceUrl); return undefined;
if (promise !== undefined) { } else {
promises.push(promise); return Promise.all(template.styleUrls.map(resolveStyleUrl)).then(() => undefined);
} }
} });
}
if (promises.length !== 0) { // Extract all the styleUrls in the decorator.
return Promise.all(promises).then(() => undefined); const styleUrls = this._extractStyleUrls(component, []);
if (styleUrls === null) {
// A fast path exists if there are no styleUrls, to just wait for
// templateAndTemplateStyleResources.
return templateAndTemplateStyleResources;
} else { } else {
return undefined; // Wait for both the template and all styleUrl resources to resolve.
return Promise.all([templateAndTemplateStyleResources, ...styleUrls.map(resolveStyleUrl)])
.then(() => undefined);
} }
} }
@ -142,89 +159,56 @@ export class ComponentDecoratorHandler implements
} }
}, undefined) !; }, undefined) !;
let templateStr: string|null = null;
let templateUrl: string = '';
let templateRange: LexerRange|undefined;
let escapedString: boolean = false;
if (component.has('templateUrl')) {
const templateUrlExpr = component.get('templateUrl') !;
const evalTemplateUrl = this.evaluator.evaluate(templateUrlExpr);
if (typeof evalTemplateUrl !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
}
templateUrl = this.resourceLoader.resolve(evalTemplateUrl, containingFile);
templateStr = this.resourceLoader.load(templateUrl);
if (!tsSourceMapBug29300Fixed()) {
// By removing the template URL we are telling the translator not to try to
// map the external source file to the generated code, since the version
// of TS that is running does not support it.
templateUrl = '';
}
} else if (component.has('template')) {
const templateExpr = component.get('template') !;
// We only support SourceMaps for inline templates that are simple string literals.
if (ts.isStringLiteral(templateExpr) || ts.isNoSubstitutionTemplateLiteral(templateExpr)) {
// the start and end of the `templateExpr` node includes the quotation marks, which we must
// strip
templateRange = getTemplateRange(templateExpr);
templateStr = templateExpr.getSourceFile().text;
templateUrl = relativeContextFilePath;
escapedString = true;
} else {
const resolvedTemplate = this.evaluator.evaluate(templateExpr);
if (typeof resolvedTemplate !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateExpr, 'template must be a string');
}
templateStr = resolvedTemplate;
}
} else {
throw new FatalDiagnosticError(
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node, 'component is missing a template');
}
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
if (component.has('preserveWhitespaces')) {
const expr = component.get('preserveWhitespaces') !;
const value = this.evaluator.evaluate(expr);
if (typeof value !== 'boolean') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, 'preserveWhitespaces must be a boolean');
}
preserveWhitespaces = value;
}
const viewProviders: Expression|null = component.has('viewProviders') ? const viewProviders: Expression|null = component.has('viewProviders') ?
new WrappedNodeExpr(component.get('viewProviders') !) : new WrappedNodeExpr(component.get('viewProviders') !) :
null; null;
let interpolation: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG; // Parse the template.
if (component.has('interpolation')) { // If a preanalyze phase was executed, the template may already exist in parsed form, so check
const expr = component.get('interpolation') !; // the preanalyzeTemplateCache.
const value = this.evaluator.evaluate(expr); let template: ParsedTemplate;
if (!Array.isArray(value) || value.length !== 2 || if (this.preanalyzeTemplateCache.has(node)) {
!value.every(element => typeof element === 'string')) { // The template was parsed in preanalyze. Use it and delete it to save memory.
throw new FatalDiagnosticError( template = this.preanalyzeTemplateCache.get(node) !;
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, this.preanalyzeTemplateCache.delete(node);
'interpolation must be an array with 2 elements of string type'); } else {
// The template was not already parsed. Either there's a templateUrl, or an inline template.
if (component.has('templateUrl')) {
const templateUrlExpr = component.get('templateUrl') !;
const evalTemplateUrl = this.evaluator.evaluate(templateUrlExpr);
if (typeof evalTemplateUrl !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
}
const templateUrl = this.resourceLoader.resolve(evalTemplateUrl, containingFile);
const templateStr = this.resourceLoader.load(templateUrl);
template = this._parseTemplate(
component, templateStr, sourceMapUrl(templateUrl), /* templateRange */ undefined,
/* escapedString */ false);
} else {
// Expect an inline template to be present.
const inlineTemplate = this._extractInlineTemplate(component, relativeContextFilePath);
if (inlineTemplate === null) {
throw new FatalDiagnosticError(
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node,
'component is missing a template');
}
const {templateStr, templateUrl, templateRange, escapedString} = inlineTemplate;
template =
this._parseTemplate(component, templateStr, templateUrl, templateRange, escapedString);
} }
interpolation = InterpolationConfig.fromArray(value as[string, string]);
} }
const template = parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig: interpolation,
range: templateRange, escapedString
});
if (template.errors !== undefined) { if (template.errors !== undefined) {
throw new Error( throw new Error(
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`); `Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
} }
// If the component has a selector, it should be registered with the `LocalModuleScopeRegistry` // If the component has a selector, it should be registered with the
// so that when this component appears in an `@NgModule` scope, its selector can be determined. // `LocalModuleScopeRegistry`
// so that when this component appears in an `@NgModule` scope, its selector can be
// determined.
if (metadata.selector !== null) { if (metadata.selector !== null) {
const ref = new Reference(node); const ref = new Reference(node);
this.scopeRegistry.registerDirective({ this.scopeRegistry.registerDirective({
@ -255,20 +239,37 @@ export class ComponentDecoratorHandler implements
viewQueries.push(...queriesFromDecorator.view); viewQueries.push(...queriesFromDecorator.view);
} }
// 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
// component.
let styles: string[]|null = null; let styles: string[]|null = null;
if (component.has('styles')) {
styles = parseFieldArrayValue(component, 'styles', this.evaluator);
}
let styleUrls = this._extractStyleUrls(component); const styleUrls = this._extractStyleUrls(component, template.styleUrls);
if (styleUrls !== null) { if (styleUrls !== null) {
if (styles === null) { if (styles === null) {
styles = []; styles = [];
} }
styleUrls.forEach(styleUrl => { for (const styleUrl of styleUrls) {
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile); const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
styles !.push(this.resourceLoader.load(resourceUrl)); styles.push(this.resourceLoader.load(resourceUrl));
}); }
}
if (component.has('styles')) {
const litStyles = parseFieldArrayValue(component, 'styles', this.evaluator);
if (litStyles !== null) {
if (styles === null) {
styles = litStyles;
} else {
styles.push(...litStyles);
}
}
}
if (template.styles.length > 0) {
if (styles === null) {
styles = template.styles;
} else {
styles.push(...template.styles);
}
} }
const encapsulation: number = const encapsulation: number =
@ -289,7 +290,7 @@ export class ComponentDecoratorHandler implements
template, template,
viewQueries, viewQueries,
encapsulation, encapsulation,
interpolation, interpolation: template.interpolation,
styles: styles || [], styles: styles || [],
// These will be replaced during the compilation step, after all `NgModule`s have been // These will be replaced during the compilation step, after all `NgModule`s have been
@ -416,9 +417,10 @@ export class ComponentDecoratorHandler implements
return resolved; return resolved;
} }
private _extractStyleUrls(component: Map<string, ts.Expression>): string[]|null { private _extractStyleUrls(component: Map<string, ts.Expression>, extraUrls: string[]):
string[]|null {
if (!component.has('styleUrls')) { if (!component.has('styleUrls')) {
return null; return extraUrls.length > 0 ? extraUrls : null;
} }
const styleUrlsExpr = component.get('styleUrls') !; const styleUrlsExpr = component.get('styleUrls') !;
@ -427,9 +429,126 @@ export class ComponentDecoratorHandler implements
throw new FatalDiagnosticError( throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, styleUrlsExpr, 'styleUrls must be an array of strings'); ErrorCode.VALUE_HAS_WRONG_TYPE, styleUrlsExpr, 'styleUrls must be an array of strings');
} }
styleUrls.push(...extraUrls);
return styleUrls as string[]; return styleUrls as string[];
} }
private _preloadAndParseTemplate(
node: ts.Declaration, decorator: Decorator, component: Map<string, ts.Expression>,
containingFile: string): Promise<ParsedTemplate|null> {
if (component.has('templateUrl')) {
// Extract the templateUrl and preload it.
const templateUrlExpr = component.get('templateUrl') !;
const templateUrl = this.evaluator.evaluate(templateUrlExpr);
if (typeof templateUrl !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateUrlExpr, 'templateUrl must be a string');
}
const resourceUrl = this.resourceLoader.resolve(templateUrl, containingFile);
const templatePromise = this.resourceLoader.preload(resourceUrl);
// If the preload worked, then actually load and parse the template, and wait for any style
// URLs to resolve.
if (templatePromise !== undefined) {
return templatePromise.then(() => {
const templateStr = this.resourceLoader.load(resourceUrl);
const template = this._parseTemplate(
component, templateStr, sourceMapUrl(resourceUrl), /* templateRange */ undefined,
/* escapedString */ false);
this.preanalyzeTemplateCache.set(node, template);
return template;
});
} else {
return Promise.resolve(null);
}
} else {
const inlineTemplate = this._extractInlineTemplate(component, containingFile);
if (inlineTemplate === null) {
throw new FatalDiagnosticError(
ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node,
'component is missing a template');
}
const {templateStr, templateUrl, escapedString, templateRange} = inlineTemplate;
const template =
this._parseTemplate(component, templateStr, templateUrl, templateRange, escapedString);
this.preanalyzeTemplateCache.set(node, template);
return Promise.resolve(template);
}
}
private _extractInlineTemplate(
component: Map<string, ts.Expression>, relativeContextFilePath: string): {
templateStr: string,
templateUrl: string,
templateRange: LexerRange|undefined,
escapedString: boolean
}|null {
// If there is no inline template, then return null.
if (!component.has('template')) {
return null;
}
const templateExpr = component.get('template') !;
let templateStr: string;
let templateUrl: string = '';
let templateRange: LexerRange|undefined = undefined;
let escapedString = false;
// We only support SourceMaps for inline templates that are simple string literals.
if (ts.isStringLiteral(templateExpr) || ts.isNoSubstitutionTemplateLiteral(templateExpr)) {
// the start and end of the `templateExpr` node includes the quotation marks, which we
// must
// strip
templateRange = getTemplateRange(templateExpr);
templateStr = templateExpr.getSourceFile().text;
templateUrl = relativeContextFilePath;
escapedString = true;
} else {
const resolvedTemplate = this.evaluator.evaluate(templateExpr);
if (typeof resolvedTemplate !== 'string') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, templateExpr, 'template must be a string');
}
templateStr = resolvedTemplate;
}
return {templateStr, templateUrl, templateRange, escapedString};
}
private _parseTemplate(
component: Map<string, ts.Expression>, templateStr: string, templateUrl: string,
templateRange: LexerRange|undefined, escapedString: boolean): ParsedTemplate {
let preserveWhitespaces: boolean = this.defaultPreserveWhitespaces;
if (component.has('preserveWhitespaces')) {
const expr = component.get('preserveWhitespaces') !;
const value = this.evaluator.evaluate(expr);
if (typeof value !== 'boolean') {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, 'preserveWhitespaces must be a boolean');
}
preserveWhitespaces = value;
}
let interpolation: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG;
if (component.has('interpolation')) {
const expr = component.get('interpolation') !;
const value = this.evaluator.evaluate(expr);
if (!Array.isArray(value) || value.length !== 2 ||
!value.every(element => typeof element === 'string')) {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr,
'interpolation must be an array with 2 elements of string type');
}
interpolation = InterpolationConfig.fromArray(value as[string, string]);
}
return {
interpolation, ...parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig: interpolation,
range: templateRange, escapedString
}),
};
}
private _isCyclicImport(expr: Expression, origin: ts.SourceFile): boolean { private _isCyclicImport(expr: Expression, origin: ts.SourceFile): boolean {
if (!(expr instanceof ExternalExpr)) { if (!(expr instanceof ExternalExpr)) {
return false; return false;
@ -471,3 +590,22 @@ function isExpressionForwardReference(
function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr<ts.Node> { function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr<ts.Node> {
return expr instanceof WrappedNodeExpr; return expr instanceof WrappedNodeExpr;
} }
function sourceMapUrl(resourceUrl: string): string {
if (!tsSourceMapBug29300Fixed()) {
// By removing the template URL we are telling the translator not to try to
// map the external source file to the generated code, since the version
// of TS that is running does not support it.
return '';
} else {
return resourceUrl;
}
}
interface ParsedTemplate {
interpolation: InterpolationConfig;
errors?: ParseError[]|undefined;
nodes: TmplAstNode[];
styleUrls: string[];
styles: string[];
}

View File

@ -3315,6 +3315,43 @@ export const Foo = Foo__PRE_R3__;
expect(jsContents).toContain('export { FooDir as ɵng$root$foo$$FooDir } from "root/foo";'); expect(jsContents).toContain('export { FooDir as ɵng$root$foo$$FooDir } from "root/foo";');
}); });
}); });
describe('inline resources', () => {
it('should process inline <style> tags', () => {
env.tsconfig();
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '<style>h1 {font-size: larger}</style>',
})
export class TestCmp {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('styles: ["h1[_ngcontent-%COMP%] {font-size: larger}"]');
});
it('should process inline <link> tags', () => {
env.tsconfig();
env.write('style.css', `h1 {font-size: larger}`);
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: '<link rel="stylesheet" href="./style.css">',
})
export class TestCmp {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('styles: ["h1[_ngcontent-%COMP%] {font-size: larger}"]');
});
});
}); });
function expectTokenAtPosition<T extends ts.Node>( function expectTokenAtPosition<T extends ts.Node>(

View File

@ -47,9 +47,12 @@ const IDENT_EVENT_IDX = 10;
const TEMPLATE_ATTR_PREFIX = '*'; const TEMPLATE_ATTR_PREFIX = '*';
// Result of the html AST to Ivy AST transformation // Result of the html AST to Ivy AST transformation
export type Render3ParseResult = { export interface Render3ParseResult {
nodes: t.Node[]; errors: ParseError[]; nodes: t.Node[];
}; errors: ParseError[];
styles: string[];
styleUrls: string[];
}
export function htmlAstToRender3Ast( export function htmlAstToRender3Ast(
htmlNodes: html.Node[], bindingParser: BindingParser): Render3ParseResult { htmlNodes: html.Node[], bindingParser: BindingParser): Render3ParseResult {
@ -68,28 +71,33 @@ export function htmlAstToRender3Ast(
return { return {
nodes: ivyNodes, nodes: ivyNodes,
errors: allErrors, errors: allErrors,
styleUrls: transformer.styleUrls,
styles: transformer.styles,
}; };
} }
class HtmlAstToIvyAst implements html.Visitor { class HtmlAstToIvyAst implements html.Visitor {
errors: ParseError[] = []; errors: ParseError[] = [];
styles: string[] = [];
styleUrls: string[] = [];
constructor(private bindingParser: BindingParser) {} constructor(private bindingParser: BindingParser) {}
// HTML visitor // HTML visitor
visitElement(element: html.Element): t.Node|null { visitElement(element: html.Element): t.Node|null {
const preparsedElement = preparseElement(element); const preparsedElement = preparseElement(element);
if (preparsedElement.type === PreparsedElementType.SCRIPT || if (preparsedElement.type === PreparsedElementType.SCRIPT) {
preparsedElement.type === PreparsedElementType.STYLE) {
// Skipping <script> for security reasons
// Skipping <style> as we already processed them
// in the StyleCompiler
return null; return null;
} } else if (preparsedElement.type === PreparsedElementType.STYLE) {
if (preparsedElement.type === PreparsedElementType.STYLESHEET && const contents = textContents(element);
if (contents !== null) {
this.styles.push(contents);
}
return null;
} else if (
preparsedElement.type === PreparsedElementType.STYLESHEET &&
isStyleUrlResolvable(preparsedElement.hrefAttr)) { isStyleUrlResolvable(preparsedElement.hrefAttr)) {
// Skipping stylesheets with either relative urls or package scheme as we already processed this.styleUrls.push(preparsedElement.hrefAttr);
// them in the StyleCompiler
return null; return null;
} }
@ -419,3 +427,11 @@ function isEmptyTextNode(node: html.Node): boolean {
function isCommentNode(node: html.Node): boolean { function isCommentNode(node: html.Node): boolean {
return node instanceof html.Comment; return node instanceof html.Comment;
} }
function textContents(node: html.Element): string|null {
if (node.children.length !== 1 || !(node.children[0] instanceof html.Text)) {
return null;
} else {
return (node.children[0] as html.Text).value;
}
}

View File

@ -1618,8 +1618,8 @@ export interface ParseTemplateOptions {
* @param options options to modify how the template is parsed * @param options options to modify how the template is parsed
*/ */
export function parseTemplate( export function parseTemplate(
template: string, templateUrl: string, template: string, templateUrl: string, options: ParseTemplateOptions = {}):
options: ParseTemplateOptions = {}): {errors?: ParseError[], nodes: t.Node[]} { {errors?: ParseError[], nodes: t.Node[], styleUrls: string[], styles: string[]} {
const {interpolationConfig, preserveWhitespaces} = options; const {interpolationConfig, preserveWhitespaces} = options;
const bindingParser = makeBindingParser(interpolationConfig); const bindingParser = makeBindingParser(interpolationConfig);
const htmlParser = new HtmlParser(); const htmlParser = new HtmlParser();
@ -1627,7 +1627,7 @@ export function parseTemplate(
htmlParser.parse(template, templateUrl, {...options, tokenizeExpansionForms: true}); htmlParser.parse(template, templateUrl, {...options, tokenizeExpansionForms: true});
if (parseResult.errors && parseResult.errors.length > 0) { if (parseResult.errors && parseResult.errors.length > 0) {
return {errors: parseResult.errors, nodes: []}; return {errors: parseResult.errors, nodes: [], styleUrls: [], styles: []};
} }
let rootNodes: html.Node[] = parseResult.rootNodes; let rootNodes: html.Node[] = parseResult.rootNodes;
@ -1650,12 +1650,12 @@ export function parseTemplate(
new I18nMetaVisitor(interpolationConfig, /* keepI18nAttrs */ false), rootNodes); new I18nMetaVisitor(interpolationConfig, /* keepI18nAttrs */ false), rootNodes);
} }
const {nodes, errors} = htmlAstToRender3Ast(rootNodes, bindingParser); const {nodes, errors, styleUrls, styles} = htmlAstToRender3Ast(rootNodes, bindingParser);
if (errors && errors.length > 0) { if (errors && errors.length > 0) {
return {errors, nodes: []}; return {errors, nodes: [], styleUrls: [], styles: []};
} }
return {nodes}; return {nodes, styleUrls, styles};
} }
/** /**