fix(compiler): exclude empty styles from emitted metadata (#45459)

Excludes styles that resolve to empty strings from the emitted metadata so that they don't result in empty `<style>` tags at runtime.

Fixes #31191.

PR Close #45459
This commit is contained in:
Kristiyan Kostadinov
2022-05-03 09:39:54 +02:00
committed by Dylan Hunn
parent 9f558003c4
commit bb8d7091c6
6 changed files with 98 additions and 33 deletions
@@ -32,7 +32,7 @@ import {NgModuleSymbol} from '../../ng_module';
import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics';
import {ComponentAnalysisData, ComponentResolutionData} from './metadata';
import {_extractTemplateStyleUrls, extractComponentStyleUrls, extractStyleResources, extractTemplate, makeResourceNotFoundError, ParsedTemplateWithSource, parseTemplateDeclaration, preloadAndParseTemplate, ResourceTypeForDiagnostics, StyleUrlMeta, transformDecoratorToInlineResources} from './resources';
import {_extractTemplateStyleUrls, extractComponentStyleUrls, extractStyleResources, extractTemplate, makeResourceNotFoundError, ParsedTemplateWithSource, parseTemplateDeclaration, preloadAndParseTemplate, ResourceTypeForDiagnostics, StyleUrlMeta, transformDecoratorResources} from './resources';
import {ComponentSymbol} from './symbol';
import {animationTriggerResolver, collectAnimationNames, validateAndFlattenComponentImports} from './util';
@@ -420,7 +420,7 @@ export class ComponentDecoratorHandler implements
typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
classMetadata: extractClassMetadata(
node, this.reflector, this.isCore, this.annotateForClosureCompiler,
dec => transformDecoratorToInlineResources(dec, component, styles, template)),
dec => transformDecoratorResources(dec, component, styles, template)),
template,
providersRequiringFactory,
viewProvidersRequiringFactory,
@@ -869,7 +869,7 @@ export class ComponentDecoratorHandler implements
styles.push(styleText);
}
analysis.meta.styles = styles;
analysis.meta.styles = styles.filter(s => s.trim().length > 0);
}
compileFull(
@@ -417,7 +417,7 @@ export function makeResourceNotFoundError(
* Additionally, the references to external resources would require libraries to ship
* external resources exclusively for the class metadata.
*/
export function transformDecoratorToInlineResources(
export function transformDecoratorResources(
dec: Decorator, component: Map<string, ts.Expression>, styles: string[],
template: ParsedTemplateWithSource): Decorator {
if (dec.name !== 'Component') {
@@ -426,7 +426,7 @@ export function transformDecoratorToInlineResources(
// If no external resources are referenced, preserve the original decorator
// for the best source map experience when the decorator is emitted in TS.
if (!component.has('templateUrl') && !component.has('styleUrls')) {
if (!component.has('templateUrl') && !component.has('styleUrls') && !component.has('styles')) {
return dec;
}
@@ -438,13 +438,22 @@ export function transformDecoratorToInlineResources(
metadata.set('template', ts.factory.createStringLiteral(template.content));
}
// Set the `styles` property if the `styleUrls` property is set.
if (metadata.has('styleUrls')) {
if (metadata.has('styleUrls') || metadata.has('styles')) {
metadata.delete('styles');
metadata.delete('styleUrls');
metadata.set(
'styles',
ts.factory.createArrayLiteralExpression(
styles.map(s => ts.factory.createStringLiteral(s))));
if (styles.length > 0) {
const styleNodes = styles.reduce((result, style) => {
if (style.trim().length > 0) {
result.push(ts.factory.createStringLiteral(style));
}
return result;
}, [] as ts.StringLiteral[]);
if (styleNodes.length > 0) {
metadata.set('styles', ts.factory.createArrayLiteralExpression(styleNodes));
}
}
}
// Convert the metadata to TypeScript AST object literal element nodes.
@@ -9,13 +9,7 @@ MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, selector: "my-component", ngImport: i0, template: '...', isInline: true, styles: ["div.foo { color: red; }", ":host p:nth-child(even) { --webkit-transition: 1s linear all; }"] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{
selector: 'my-component',
styles: [
'div.foo { color: red; }', ':host p:nth-child(even) { --webkit-transition: 1s linear all; }'
],
template: '...'
}]
args: [{ selector: 'my-component', template: '...', styles: ["div.foo { color: red; }", ":host p:nth-child(even) { --webkit-transition: 1s linear all; }"] }]
}] });
export class MyModule {
}
@@ -52,12 +46,7 @@ MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, selector: "my-component", ngImport: i0, template: '...', isInline: true, styles: ["div.tall { height: 123px; }", ":host.small p { height:5px; }"], encapsulation: i0.ViewEncapsulation.None });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{
selector: 'my-component',
encapsulation: ViewEncapsulation.None,
styles: ['div.tall { height: 123px; }', ':host.small p { height:5px; }'],
template: '...'
}]
args: [{ selector: 'my-component', encapsulation: ViewEncapsulation.None, template: '...', styles: ["div.tall { height: 123px; }", ":host.small p { height:5px; }"] }]
}] });
export class MyModule {
}
@@ -94,12 +83,7 @@ MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.
MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, selector: "my-component", ngImport: i0, template: '...', isInline: true, styles: ["div.cool { color: blue; }", ":host.nice p { color: gold; }"], encapsulation: i0.ViewEncapsulation.ShadowDom });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,
args: [{
encapsulation: ViewEncapsulation.ShadowDom,
selector: 'my-component',
styles: ['div.cool { color: blue; }', ':host.nice p { color: gold; }'],
template: '...'
}]
args: [{ encapsulation: ViewEncapsulation.ShadowDom, selector: 'my-component', template: '...', styles: ["div.cool { color: blue; }", ":host.nice p { color: gold; }"] }]
}] });
export class MyModule {
}
@@ -6923,6 +6923,70 @@ function allTests(os: string) {
});
});
describe('empty resources', () => {
it('should not include empty inline styles in the compiled output', () => {
env.write('test.ts', `
import {Component} from '@angular/core';
const someStyle = ' ';
@Component({
selector: 'test-cmp',
styles: ['', someStyle, ' '],
template: '',
})
export class TestCmp {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).not.toContain('styles');
expect(jsContents).not.toContain('styleUrls');
});
it('should not include empty external styles in the compiled output', () => {
env.write('dir/a.css', '');
env.write('dir/b.css', ' ');
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
styleUrls: ['./dir/a.css', './dir/b.css'],
template: '',
})
export class TestCmp {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).not.toContain('styles');
expect(jsContents).not.toContain('styleUrls');
});
it('should not include empty <link> tags that resolve to empty stylesheets', () => {
env.write('dir/a.css', '');
env.write('dir/b.css', ' ');
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'test',
template: \`
<link rel="stylesheet" href="./dir/a.css">
<link rel="stylesheet" href="./dir/b.css">
\`,
})
export class TestCmp {}
`);
env.driveMain();
const jsContents = env.getContents('test.js').replace(/<link [^>]*>/g, '');
expect(jsContents).not.toContain('styles');
expect(jsContents).not.toContain('styleUrls');
});
});
describe('non-exported classes', () => {
beforeEach(() => env.tsconfig({compileNonExportedClasses: false}));
+10 -2
View File
@@ -221,8 +221,16 @@ export function compileComponentFromMetadata(
const styleValues = meta.encapsulation == core.ViewEncapsulation.Emulated ?
compileStyles(meta.styles, CONTENT_ATTR, HOST_ATTR) :
meta.styles;
const strings = styleValues.map(str => constantPool.getConstLiteral(o.literal(str)));
definitionMap.set('styles', o.literalArr(strings));
const styleNodes = styleValues.reduce((result, style) => {
if (style.trim().length > 0) {
result.push(constantPool.getConstLiteral(o.literal(style)));
}
return result;
}, [] as o.Expression[]);
if (styleNodes.length > 0) {
definitionMap.set('styles', o.literalArr(styleNodes));
}
} else if (meta.encapsulation === core.ViewEncapsulation.Emulated) {
// If there is no style, don't generate css selectors on elements
meta.encapsulation = core.ViewEncapsulation.None;