diff --git a/packages/angular_devkit/build_angular/src/builders/application/schema.json b/packages/angular_devkit/build_angular/src/builders/application/schema.json index dba0d9a48953..df51a24a980a 100644 --- a/packages/angular_devkit/build_angular/src/builders/application/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/application/schema.json @@ -162,6 +162,11 @@ "type": "boolean", "description": "Extract and inline critical CSS definitions to improve first paint time.", "default": true + }, + "removeSpecialComments": { + "type": "boolean", + "description": "Remove comments in global CSS that contains '@license' or '@preserve' or that starts with '//!' or '/*!'.", + "default": true } }, "additionalProperties": false diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/options/inline-critical_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/optimization-inline-critical_spec.ts similarity index 100% rename from packages/angular_devkit/build_angular/src/builders/application/tests/options/inline-critical_spec.ts rename to packages/angular_devkit/build_angular/src/builders/application/tests/options/optimization-inline-critical_spec.ts diff --git a/packages/angular_devkit/build_angular/src/builders/application/tests/options/optimization-remove-special-comments_spec.ts b/packages/angular_devkit/build_angular/src/builders/application/tests/options/optimization-remove-special-comments_spec.ts new file mode 100644 index 000000000000..9a8ede16af23 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/application/tests/options/optimization-remove-special-comments_spec.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "removeSpecialComments"', () => { + beforeEach(async () => { + await harness.writeFile( + 'src/styles.css', + ` + /* normal-comment */ + /*! important-comment */ + div { flex: 1 } + `, + ); + }); + + it(`should retain special comments when 'removeSpecialComments' is set to 'false'`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + extractLicenses: true, + styles: ['src/styles.css'], + optimization: { + styles: { + removeSpecialComments: false, + }, + }, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness + .expectFile('dist/browser/styles.css') + .content.toMatch(/\/\*! important-comment \*\/[\s\S]*div{flex:1}/); + }); + + it(`should not retain special comments when 'removeSpecialComments' is set to 'true'`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + extractLicenses: true, + styles: ['src/styles.css'], + optimization: { + styles: { + removeSpecialComments: true, + }, + }, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.not.toContain('important-comment'); + }); + + it(`should not retain special comments when 'removeSpecialComments' is not set`, async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + extractLicenses: true, + styles: ['src/styles.css'], + optimization: { + styles: {}, + }, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/browser/styles.css').content.not.toContain('important-comment'); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts index e326bddbde8d..640e6bcd05fc 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/global-styles.ts @@ -65,7 +65,13 @@ export function createGlobalStylesBundleOptions( }, loadCache, ); - buildOptions.legalComments = options.extractLicenses ? 'none' : 'eof'; + + // Keep special CSS comments `/*! comment */` in place when `removeSpecialComments` is disabled. + // These comments are special for a number of CSS tools such as Critters and PurgeCSS. + buildOptions.legalComments = optimizationOptions.styles?.removeSpecialComments + ? 'none' + : 'inline'; + buildOptions.entryPoints = entryPoints; buildOptions.plugins.unshift( diff --git a/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts b/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts index 9bbf455b86f5..2458a3669a6c 100644 --- a/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts +++ b/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts @@ -11,7 +11,7 @@ import { OptimizationClass, OptimizationUnion, StylesClass, -} from '../builders/browser/schema'; +} from '../builders/application/schema'; export type NormalizedOptimizationOptions = Required< Omit @@ -24,14 +24,17 @@ export function normalizeOptimization( optimization: OptimizationUnion = true, ): NormalizedOptimizationOptions { if (typeof optimization === 'object') { + const styleOptimization = !!optimization.styles; + return { scripts: !!optimization.scripts, styles: typeof optimization.styles === 'object' ? optimization.styles : { - minify: !!optimization.styles, - inlineCritical: !!optimization.styles, + minify: styleOptimization, + removeSpecialComments: styleOptimization, + inlineCritical: styleOptimization, }, fonts: typeof optimization.fonts === 'object' @@ -47,6 +50,7 @@ export function normalizeOptimization( styles: { minify: optimization, inlineCritical: optimization, + removeSpecialComments: optimization, }, fonts: { inline: optimization,