diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 27f78f34432..2750a554f23 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -35,7 +35,7 @@ "runtime": 2835, "main": 237828, "polyfills": 33842, - "src_app_lazy_lazy_module_ts": 795 + "src_app_lazy_lazy_module_ts": 780 } }, "forms": { diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index 21d2ae247bf..1d57e1436b7 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -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( diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts index 314dcfcadfb..3ebbbc872eb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts @@ -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, 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. diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/GOLDEN_PARTIAL.js index 84b8d5bc7b6..7a8039e5285 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/component_styles/GOLDEN_PARTIAL.js @@ -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 { } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 4c24dddbd88..1f42439a266 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -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 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: \` + + + \`, + }) + export class TestCmp {} + `); + + env.driveMain(); + const jsContents = env.getContents('test.js').replace(/]*>/g, ''); + expect(jsContents).not.toContain('styles'); + expect(jsContents).not.toContain('styleUrls'); + }); + }); + describe('non-exported classes', () => { beforeEach(() => env.tsconfig({compileNonExportedClasses: false})); diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index ad897b5bfd1..b7c35374361 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -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;