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:
parent
40833ba54b
commit
827e89cfc4
|
@ -21,8 +21,6 @@ const IGNORED_EXAMPLES = [
|
|||
];
|
||||
|
||||
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)
|
||||
'i18n'
|
||||
];
|
||||
|
@ -319,7 +317,10 @@ function getE2eSpecs(basePath, filter) {
|
|||
// Find all e2e specs in a given example folder.
|
||||
function getE2eSpecsFor(basePath, specFile, filter) {
|
||||
// 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}`;
|
||||
// clang-format on
|
||||
return globby(e2eSpecGlob, {cwd: basePath, nodir: true})
|
||||
.then(
|
||||
paths => paths.filter(file => !IGNORED_EXAMPLES.some(ignored => file.startsWith(ignored)))
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* 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 ts from 'typescript';
|
||||
|
||||
|
@ -50,6 +50,13 @@ export class ComponentDecoratorHandler implements
|
|||
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
|
||||
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;
|
||||
|
||||
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 {
|
||||
// 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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const meta = this._resolveLiteral(decorator);
|
||||
const component = reflectObjectLiteral(meta);
|
||||
const promises: Promise<void>[] = [];
|
||||
const containingFile = node.getSourceFile().fileName;
|
||||
|
||||
if (component.has('templateUrl')) {
|
||||
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);
|
||||
// Convert a styleUrl string into a Promise to preload it.
|
||||
const resolveStyleUrl = (styleUrl: string): Promise<void> => {
|
||||
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
|
||||
const promise = this.resourceLoader.preload(resourceUrl);
|
||||
if (promise !== undefined) {
|
||||
promises.push(promise);
|
||||
}
|
||||
}
|
||||
return promise || Promise.resolve();
|
||||
};
|
||||
|
||||
const styleUrls = this._extractStyleUrls(component);
|
||||
if (styleUrls !== null) {
|
||||
for (const styleUrl of styleUrls) {
|
||||
const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile);
|
||||
const promise = this.resourceLoader.preload(resourceUrl);
|
||||
if (promise !== undefined) {
|
||||
promises.push(promise);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A Promise that waits for the template and all <link>ed styles within it to be preloaded.
|
||||
const templateAndTemplateStyleResources =
|
||||
this._preloadAndParseTemplate(node, decorator, component, containingFile).then(template => {
|
||||
if (template === null) {
|
||||
return undefined;
|
||||
} else {
|
||||
return Promise.all(template.styleUrls.map(resolveStyleUrl)).then(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
if (promises.length !== 0) {
|
||||
return Promise.all(promises).then(() => undefined);
|
||||
// Extract all the styleUrls in the decorator.
|
||||
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 {
|
||||
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) !;
|
||||
|
||||
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') ?
|
||||
new WrappedNodeExpr(component.get('viewProviders') !) :
|
||||
null;
|
||||
|
||||
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');
|
||||
// Parse the template.
|
||||
// If a preanalyze phase was executed, the template may already exist in parsed form, so check
|
||||
// the preanalyzeTemplateCache.
|
||||
let template: ParsedTemplate;
|
||||
if (this.preanalyzeTemplateCache.has(node)) {
|
||||
// The template was parsed in preanalyze. Use it and delete it to save memory.
|
||||
template = this.preanalyzeTemplateCache.get(node) !;
|
||||
this.preanalyzeTemplateCache.delete(node);
|
||||
} 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) {
|
||||
throw new Error(
|
||||
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
|
||||
}
|
||||
|
||||
// If the component has a selector, it should be registered with the `LocalModuleScopeRegistry`
|
||||
// so that when this component appears in an `@NgModule` scope, its selector can be determined.
|
||||
// If the component has a selector, it should be registered with the
|
||||
// `LocalModuleScopeRegistry`
|
||||
// so that when this component appears in an `@NgModule` scope, its selector can be
|
||||
// determined.
|
||||
if (metadata.selector !== null) {
|
||||
const ref = new Reference(node);
|
||||
this.scopeRegistry.registerDirective({
|
||||
|
@ -255,20 +239,37 @@ export class ComponentDecoratorHandler implements
|
|||
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;
|
||||
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 (styles === null) {
|
||||
styles = [];
|
||||
}
|
||||
styleUrls.forEach(styleUrl => {
|
||||
for (const styleUrl of styleUrls) {
|
||||
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 =
|
||||
|
@ -289,7 +290,7 @@ export class ComponentDecoratorHandler implements
|
|||
template,
|
||||
viewQueries,
|
||||
encapsulation,
|
||||
interpolation,
|
||||
interpolation: template.interpolation,
|
||||
styles: styles || [],
|
||||
|
||||
// These will be replaced during the compilation step, after all `NgModule`s have been
|
||||
|
@ -416,9 +417,10 @@ export class ComponentDecoratorHandler implements
|
|||
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')) {
|
||||
return null;
|
||||
return extraUrls.length > 0 ? extraUrls : null;
|
||||
}
|
||||
|
||||
const styleUrlsExpr = component.get('styleUrls') !;
|
||||
|
@ -427,9 +429,126 @@ export class ComponentDecoratorHandler implements
|
|||
throw new FatalDiagnosticError(
|
||||
ErrorCode.VALUE_HAS_WRONG_TYPE, styleUrlsExpr, 'styleUrls must be an array of strings');
|
||||
}
|
||||
styleUrls.push(...extraUrls);
|
||||
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 {
|
||||
if (!(expr instanceof ExternalExpr)) {
|
||||
return false;
|
||||
|
@ -471,3 +590,22 @@ function isExpressionForwardReference(
|
|||
function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr<ts.Node> {
|
||||
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[];
|
||||
}
|
||||
|
|
|
@ -3315,6 +3315,43 @@ export const Foo = Foo__PRE_R3__;
|
|||
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>(
|
||||
|
|
|
@ -47,9 +47,12 @@ const IDENT_EVENT_IDX = 10;
|
|||
const TEMPLATE_ATTR_PREFIX = '*';
|
||||
|
||||
// Result of the html AST to Ivy AST transformation
|
||||
export type Render3ParseResult = {
|
||||
nodes: t.Node[]; errors: ParseError[];
|
||||
};
|
||||
export interface Render3ParseResult {
|
||||
nodes: t.Node[];
|
||||
errors: ParseError[];
|
||||
styles: string[];
|
||||
styleUrls: string[];
|
||||
}
|
||||
|
||||
export function htmlAstToRender3Ast(
|
||||
htmlNodes: html.Node[], bindingParser: BindingParser): Render3ParseResult {
|
||||
|
@ -68,28 +71,33 @@ export function htmlAstToRender3Ast(
|
|||
return {
|
||||
nodes: ivyNodes,
|
||||
errors: allErrors,
|
||||
styleUrls: transformer.styleUrls,
|
||||
styles: transformer.styles,
|
||||
};
|
||||
}
|
||||
|
||||
class HtmlAstToIvyAst implements html.Visitor {
|
||||
errors: ParseError[] = [];
|
||||
styles: string[] = [];
|
||||
styleUrls: string[] = [];
|
||||
|
||||
constructor(private bindingParser: BindingParser) {}
|
||||
|
||||
// HTML visitor
|
||||
visitElement(element: html.Element): t.Node|null {
|
||||
const preparsedElement = preparseElement(element);
|
||||
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
|
||||
preparsedElement.type === PreparsedElementType.STYLE) {
|
||||
// Skipping <script> for security reasons
|
||||
// Skipping <style> as we already processed them
|
||||
// in the StyleCompiler
|
||||
if (preparsedElement.type === PreparsedElementType.SCRIPT) {
|
||||
return null;
|
||||
}
|
||||
if (preparsedElement.type === PreparsedElementType.STYLESHEET &&
|
||||
} else if (preparsedElement.type === PreparsedElementType.STYLE) {
|
||||
const contents = textContents(element);
|
||||
if (contents !== null) {
|
||||
this.styles.push(contents);
|
||||
}
|
||||
return null;
|
||||
} else if (
|
||||
preparsedElement.type === PreparsedElementType.STYLESHEET &&
|
||||
isStyleUrlResolvable(preparsedElement.hrefAttr)) {
|
||||
// Skipping stylesheets with either relative urls or package scheme as we already processed
|
||||
// them in the StyleCompiler
|
||||
this.styleUrls.push(preparsedElement.hrefAttr);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -419,3 +427,11 @@ function isEmptyTextNode(node: html.Node): boolean {
|
|||
function isCommentNode(node: html.Node): boolean {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1618,8 +1618,8 @@ export interface ParseTemplateOptions {
|
|||
* @param options options to modify how the template is parsed
|
||||
*/
|
||||
export function parseTemplate(
|
||||
template: string, templateUrl: string,
|
||||
options: ParseTemplateOptions = {}): {errors?: ParseError[], nodes: t.Node[]} {
|
||||
template: string, templateUrl: string, options: ParseTemplateOptions = {}):
|
||||
{errors?: ParseError[], nodes: t.Node[], styleUrls: string[], styles: string[]} {
|
||||
const {interpolationConfig, preserveWhitespaces} = options;
|
||||
const bindingParser = makeBindingParser(interpolationConfig);
|
||||
const htmlParser = new HtmlParser();
|
||||
|
@ -1627,7 +1627,7 @@ export function parseTemplate(
|
|||
htmlParser.parse(template, templateUrl, {...options, tokenizeExpansionForms: true});
|
||||
|
||||
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;
|
||||
|
@ -1650,12 +1650,12 @@ export function parseTemplate(
|
|||
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) {
|
||||
return {errors, nodes: []};
|
||||
return {errors, nodes: [], styleUrls: [], styles: []};
|
||||
}
|
||||
|
||||
return {nodes};
|
||||
return {nodes, styleUrls, styles};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue