From c8578a1f0ef22295ba9482f8f000bfffce780070 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 29 Nov 2023 16:24:52 +0000 Subject: [PATCH] feat(@angular-devkit/build-angular): add option to retain CSS special comments in global styles Prior to this change special CSS comments `/*! comment */` were being removed during minification when using the application builder. This caused tools that ran post build that rely on such comments such as purgeCSS and critters not to function properly. We now provide a `removeSpecialComments` option to enable retention of these comments in global CSS files. Usage example: ```json { "projects": { "my-app": { "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "configurations": { "production": { "optimization": { "styles": { "removeSpecialComments": false } } } } } } } } } ``` Closes: #26432 --- .../src/builders/application/schema.json | 5 ++ ...s => optimization-inline-critical_spec.ts} | 0 ...timization-remove-special-comments_spec.ts | 79 +++++++++++++++++++ .../src/tools/esbuild/global-styles.ts | 8 +- .../src/utils/normalize-optimization.ts | 10 ++- 5 files changed, 98 insertions(+), 4 deletions(-) rename packages/angular_devkit/build_angular/src/builders/application/tests/options/{inline-critical_spec.ts => optimization-inline-critical_spec.ts} (100%) create mode 100644 packages/angular_devkit/build_angular/src/builders/application/tests/options/optimization-remove-special-comments_spec.ts 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,