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 = [
|
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)))
|
||||||
|
@ -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[];
|
||||||
|
}
|
||||||
|
@ -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>(
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user