Skip to content

Commit bb8d709

Browse files
crisbetodylhunn
authored andcommitted
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
1 parent 9f55800 commit bb8d709

File tree

6 files changed

+98
-33
lines changed

6 files changed

+98
-33
lines changed

goldens/size-tracking/integration-payloads.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"runtime": 2835,
3636
"main": 237828,
3737
"polyfills": 33842,
38-
"src_app_lazy_lazy_module_ts": 795
38+
"src_app_lazy_lazy_module_ts": 780
3939
}
4040
},
4141
"forms": {

packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {NgModuleSymbol} from '../../ng_module';
3232

3333
import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics';
3434
import {ComponentAnalysisData, ComponentResolutionData} from './metadata';
35-
import {_extractTemplateStyleUrls, extractComponentStyleUrls, extractStyleResources, extractTemplate, makeResourceNotFoundError, ParsedTemplateWithSource, parseTemplateDeclaration, preloadAndParseTemplate, ResourceTypeForDiagnostics, StyleUrlMeta, transformDecoratorToInlineResources} from './resources';
35+
import {_extractTemplateStyleUrls, extractComponentStyleUrls, extractStyleResources, extractTemplate, makeResourceNotFoundError, ParsedTemplateWithSource, parseTemplateDeclaration, preloadAndParseTemplate, ResourceTypeForDiagnostics, StyleUrlMeta, transformDecoratorResources} from './resources';
3636
import {ComponentSymbol} from './symbol';
3737
import {animationTriggerResolver, collectAnimationNames, validateAndFlattenComponentImports} from './util';
3838

@@ -420,7 +420,7 @@ export class ComponentDecoratorHandler implements
420420
typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector),
421421
classMetadata: extractClassMetadata(
422422
node, this.reflector, this.isCore, this.annotateForClosureCompiler,
423-
dec => transformDecoratorToInlineResources(dec, component, styles, template)),
423+
dec => transformDecoratorResources(dec, component, styles, template)),
424424
template,
425425
providersRequiringFactory,
426426
viewProvidersRequiringFactory,
@@ -869,7 +869,7 @@ export class ComponentDecoratorHandler implements
869869
styles.push(styleText);
870870
}
871871

872-
analysis.meta.styles = styles;
872+
analysis.meta.styles = styles.filter(s => s.trim().length > 0);
873873
}
874874

875875
compileFull(

packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ export function makeResourceNotFoundError(
417417
* Additionally, the references to external resources would require libraries to ship
418418
* external resources exclusively for the class metadata.
419419
*/
420-
export function transformDecoratorToInlineResources(
420+
export function transformDecoratorResources(
421421
dec: Decorator, component: Map<string, ts.Expression>, styles: string[],
422422
template: ParsedTemplateWithSource): Decorator {
423423
if (dec.name !== 'Component') {
@@ -426,7 +426,7 @@ export function transformDecoratorToInlineResources(
426426

427427
// If no external resources are referenced, preserve the original decorator
428428
// for the best source map experience when the decorator is emitted in TS.
429-
if (!component.has('templateUrl') && !component.has('styleUrls')) {
429+
if (!component.has('templateUrl') && !component.has('styleUrls') && !component.has('styles')) {
430430
return dec;
431431
}
432432

@@ -438,13 +438,22 @@ export function transformDecoratorToInlineResources(
438438
metadata.set('template', ts.factory.createStringLiteral(template.content));
439439
}
440440

441-
// Set the `styles` property if the `styleUrls` property is set.
442-
if (metadata.has('styleUrls')) {
441+
if (metadata.has('styleUrls') || metadata.has('styles')) {
442+
metadata.delete('styles');
443443
metadata.delete('styleUrls');
444-
metadata.set(
445-
'styles',
446-
ts.factory.createArrayLiteralExpression(
447-
styles.map(s => ts.factory.createStringLiteral(s))));
444+
445+
if (styles.length > 0) {
446+
const styleNodes = styles.reduce((result, style) => {
447+
if (style.trim().length > 0) {
448+
result.push(ts.factory.createStringLiteral(style));
449+
}
450+
return result;
451+
}, [] as ts.StringLiteral[]);
452+
453+
if (styleNodes.length > 0) {
454+
metadata.set('styles', ts.factory.createArrayLiteralExpression(styleNodes));
455+
}
456+
}
448457
}
449458

450459
// Convert the metadata to TypeScript AST object literal element nodes.

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/GOLDEN_PARTIAL.js

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,7 @@ MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.
99
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; }"] });
1010
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
1111
type: Component,
12-
args: [{
13-
selector: 'my-component',
14-
styles: [
15-
'div.foo { color: red; }', ':host p:nth-child(even) { --webkit-transition: 1s linear all; }'
16-
],
17-
template: '...'
18-
}]
12+
args: [{ selector: 'my-component', template: '...', styles: ["div.foo { color: red; }", ":host p:nth-child(even) { --webkit-transition: 1s linear all; }"] }]
1913
}] });
2014
export class MyModule {
2115
}
@@ -52,12 +46,7 @@ MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.
5246
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 });
5347
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
5448
type: Component,
55-
args: [{
56-
selector: 'my-component',
57-
encapsulation: ViewEncapsulation.None,
58-
styles: ['div.tall { height: 123px; }', ':host.small p { height:5px; }'],
59-
template: '...'
60-
}]
49+
args: [{ selector: 'my-component', encapsulation: ViewEncapsulation.None, template: '...', styles: ["div.tall { height: 123px; }", ":host.small p { height:5px; }"] }]
6150
}] });
6251
export class MyModule {
6352
}
@@ -94,12 +83,7 @@ MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.
9483
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 });
9584
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
9685
type: Component,
97-
args: [{
98-
encapsulation: ViewEncapsulation.ShadowDom,
99-
selector: 'my-component',
100-
styles: ['div.cool { color: blue; }', ':host.nice p { color: gold; }'],
101-
template: '...'
102-
}]
86+
args: [{ encapsulation: ViewEncapsulation.ShadowDom, selector: 'my-component', template: '...', styles: ["div.cool { color: blue; }", ":host.nice p { color: gold; }"] }]
10387
}] });
10488
export class MyModule {
10589
}

packages/compiler-cli/test/ngtsc/ngtsc_spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6923,6 +6923,70 @@ function allTests(os: string) {
69236923
});
69246924
});
69256925

6926+
describe('empty resources', () => {
6927+
it('should not include empty inline styles in the compiled output', () => {
6928+
env.write('test.ts', `
6929+
import {Component} from '@angular/core';
6930+
6931+
const someStyle = ' ';
6932+
6933+
@Component({
6934+
selector: 'test-cmp',
6935+
styles: ['', someStyle, ' '],
6936+
template: '',
6937+
})
6938+
export class TestCmp {}
6939+
`);
6940+
env.driveMain();
6941+
6942+
const jsContents = env.getContents('test.js');
6943+
expect(jsContents).not.toContain('styles');
6944+
expect(jsContents).not.toContain('styleUrls');
6945+
});
6946+
6947+
it('should not include empty external styles in the compiled output', () => {
6948+
env.write('dir/a.css', '');
6949+
env.write('dir/b.css', ' ');
6950+
env.write('test.ts', `
6951+
import {Component} from '@angular/core';
6952+
6953+
@Component({
6954+
selector: 'test-cmp',
6955+
styleUrls: ['./dir/a.css', './dir/b.css'],
6956+
template: '',
6957+
})
6958+
export class TestCmp {}
6959+
`);
6960+
env.driveMain();
6961+
6962+
const jsContents = env.getContents('test.js');
6963+
expect(jsContents).not.toContain('styles');
6964+
expect(jsContents).not.toContain('styleUrls');
6965+
});
6966+
6967+
it('should not include empty <link> tags that resolve to empty stylesheets', () => {
6968+
env.write('dir/a.css', '');
6969+
env.write('dir/b.css', ' ');
6970+
env.write('test.ts', `
6971+
import {Component} from '@angular/core';
6972+
6973+
@Component({
6974+
selector: 'test',
6975+
template: \`
6976+
<link rel="stylesheet" href="./dir/a.css">
6977+
<link rel="stylesheet" href="./dir/b.css">
6978+
\`,
6979+
})
6980+
export class TestCmp {}
6981+
`);
6982+
6983+
env.driveMain();
6984+
const jsContents = env.getContents('test.js').replace(/<link [^>]*>/g, '');
6985+
expect(jsContents).not.toContain('styles');
6986+
expect(jsContents).not.toContain('styleUrls');
6987+
});
6988+
});
6989+
69266990
describe('non-exported classes', () => {
69276991
beforeEach(() => env.tsconfig({compileNonExportedClasses: false}));
69286992

packages/compiler/src/render3/view/compiler.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,16 @@ export function compileComponentFromMetadata(
221221
const styleValues = meta.encapsulation == core.ViewEncapsulation.Emulated ?
222222
compileStyles(meta.styles, CONTENT_ATTR, HOST_ATTR) :
223223
meta.styles;
224-
const strings = styleValues.map(str => constantPool.getConstLiteral(o.literal(str)));
225-
definitionMap.set('styles', o.literalArr(strings));
224+
const styleNodes = styleValues.reduce((result, style) => {
225+
if (style.trim().length > 0) {
226+
result.push(constantPool.getConstLiteral(o.literal(style)));
227+
}
228+
return result;
229+
}, [] as o.Expression[]);
230+
231+
if (styleNodes.length > 0) {
232+
definitionMap.set('styles', o.literalArr(styleNodes));
233+
}
226234
} else if (meta.encapsulation === core.ViewEncapsulation.Emulated) {
227235
// If there is no style, don't generate css selectors on elements
228236
meta.encapsulation = core.ViewEncapsulation.None;

0 commit comments

Comments
 (0)