From 4b3c2d89c562a66e18374cdab8c8929b94dddfba Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 14 Oct 2024 10:15:47 +0000 Subject: [PATCH] feat(@angular/build): add `sassOptions` to `stylePreprocessorOptions` in application builder This commit introduces the functionality to configure a limited number of options for Sass processing in the Angular application builder. The following options have been added to enhance the Sass integration: - **futureDeprecations**: Specifies features that are scheduled for deprecation. The compiler will treat these as active and emit warnings as necessary. - **fatalDeprecations**: Identifies Sass features that are already deprecated and will cause build failures if used. - **silenceDeprecations**: This option suppresses deprecation warnings for specified versions. Usage example: ```json "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputHashing": "none", "namedChunks": true, "stylePreprocessorOptions": { "sassOptions": { "futureDeprecations": ["color-functions"], "fatalDeprecations": ["color-functions"], "silenceDeprecations": ["1.77.0"] } } } } } ``` For more information about these options, please refer to the Sass documentation: https://sass-lang.com/documentation/js-api/interfaces/options/ Closes #28587 --- .../src/builders/application/schema.json | 28 ++++++ .../tests/options/sass-options_spec.ts | 89 +++++++++++++++++++ .../tools/esbuild/compiler-plugin-options.ts | 3 + .../build/src/tools/esbuild/global-styles.ts | 3 + .../esbuild/stylesheets/bundle-options.ts | 4 +- .../esbuild/stylesheets/sass-language.ts | 6 +- .../stylesheets/stylesheet-plugin-factory.ts | 14 +++ 7 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 packages/angular/build/src/builders/application/tests/options/sass-options_spec.ts diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index 60e725568eb0..2765dbf24c4b 100644 --- a/packages/angular/build/src/builders/application/schema.json +++ b/packages/angular/build/src/builders/application/schema.json @@ -125,6 +125,34 @@ "type": "string" }, "default": [] + }, + "sassOptions": { + "description": "Options to pass to the sass preprocessor.", + "type": "object", + "properties": { + "fatalDeprecations": { + "description": "A set of deprecations to treat as fatal. If a deprecation warning of any provided type is encountered during compilation, the compiler will error instead. If a Version is provided, then all deprecations that were active in that compiler version will be treated as fatal.", + "type": "array", + "items": { + "type": "string" + } + }, + "silenceDeprecations": { + "description": "A set of future deprecations to opt into early. Future deprecations passed here will be treated as active by the compiler, emitting warnings as necessary.", + "type": "array", + "items": { + "type": "string" + } + }, + "futureDeprecations": { + "description": "A set of future deprecations to opt into early. Future deprecations passed here will be treated as active by the compiler, emitting warnings as necessary.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false } }, "additionalProperties": false diff --git a/packages/angular/build/src/builders/application/tests/options/sass-options_spec.ts b/packages/angular/build/src/builders/application/tests/options/sass-options_spec.ts new file mode 100644 index 000000000000..8c97246c741f --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/options/sass-options_spec.ts @@ -0,0 +1,89 @@ +/** + * @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.dev/license + */ + +import { buildApplication } from '../../index'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; +import { logging } from '@angular-devkit/core'; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Option: "sassOptions"', () => { + it('should cause the build to fail when using `fatalDeprecations` in global styles', async () => { + await harness.writeFile('src/styles.scss', 'p { color: darken(red, 10%) }'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.scss'], + stylePreprocessorOptions: { + sassOptions: { + fatalDeprecations: ['color-functions'], + }, + }, + }); + + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + + expect(result?.success).toBeFalse(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('darken() is deprecated'), + }), + ); + }); + + it('should succeed without `fatalDeprecations` despite using deprecated color functions', async () => { + await harness.writeFiles({ + 'src/styles.scss': 'p { color: darken(red, 10%) }', + 'src/app/app.component.scss': 'p { color: darken(red, 10%) }', + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('./app.component.css', 'app.component.scss'); + }); + + harness.useTarget('build', { + ...BASE_OPTIONS, + styles: ['src/styles.scss'], + stylePreprocessorOptions: { + sassOptions: {}, + }, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + }); + + it('should cause the build to fail when using `fatalDeprecations` in component styles', async () => { + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content.replace('./app.component.css', 'app.component.scss'); + }); + + await harness.writeFile('src/app/app.component.scss', 'p { color: darken(red, 10%) }'); + + harness.useTarget('build', { + ...BASE_OPTIONS, + stylePreprocessorOptions: { + sassOptions: { + fatalDeprecations: ['color-functions'], + }, + }, + }); + + const { result, logs } = await harness.executeOnce({ + outputLogsOnFailure: false, + }); + + expect(result?.success).toBeFalse(); + expect(logs).not.toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('darken() is deprecated'), + }), + ); + }); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts index 37aa318dd36c..3a4c6bbe9077 100644 --- a/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts +++ b/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts @@ -68,6 +68,9 @@ export function createCompilerPluginOptions( sourcemapOptions.styles && !sourcemapOptions.hidden ? 'linked' : false, outputNames, includePaths: stylePreprocessorOptions?.includePaths, + // string[] | undefined' is not assignable to type '(Version | DeprecationOrId)[] | undefined'. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sassOptions: stylePreprocessorOptions?.sassOptions as any, externalDependencies, target, inlineStyleLanguage, diff --git a/packages/angular/build/src/tools/esbuild/global-styles.ts b/packages/angular/build/src/tools/esbuild/global-styles.ts index bb6b095419ea..c1192fe4ce5f 100644 --- a/packages/angular/build/src/tools/esbuild/global-styles.ts +++ b/packages/angular/build/src/tools/esbuild/global-styles.ts @@ -63,6 +63,9 @@ export function createGlobalStylesBundleOptions( bundles: '[name]', }, includePaths: stylePreprocessorOptions?.includePaths, + // string[] | undefined' is not assignable to type '(Version | DeprecationOrId)[] | undefined'. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sassOptions: stylePreprocessorOptions?.sassOptions as any, tailwindConfiguration, postcssConfiguration, cacheOptions, diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts b/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts index 42196d507776..56c6ab6035d8 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/bundle-options.ts @@ -16,7 +16,7 @@ import { CssStylesheetLanguage } from './css-language'; import { createCssResourcePlugin } from './css-resource-plugin'; import { LessStylesheetLanguage } from './less-language'; import { SassStylesheetLanguage } from './sass-language'; -import { StylesheetPluginFactory } from './stylesheet-plugin-factory'; +import { StylesheetPluginFactory, StylesheetPluginSassOptions } from './stylesheet-plugin-factory'; export interface BundleStylesheetOptions { workspaceRoot: string; @@ -26,6 +26,7 @@ export interface BundleStylesheetOptions { sourcemap: boolean | 'external' | 'inline' | 'linked'; outputNames: { bundles: string; media: string }; includePaths?: string[]; + sassOptions?: StylesheetPluginSassOptions; externalDependencies?: string[]; target: string[]; tailwindConfiguration?: { file: string; package: string }; @@ -51,6 +52,7 @@ export function createStylesheetBundleOptions( inlineComponentData, tailwindConfiguration: options.tailwindConfiguration, postcssConfiguration: options.postcssConfiguration, + sassOptions: options.sassOptions, }, cache, ); diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts index a55969268901..1496ad96247c 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/sass-language.ts @@ -94,8 +94,9 @@ async function compileString( // failing resolution attempts. const resolutionCache = new MemoryCache(); const packageRootCache = new MemoryCache(); - const warnings: PartialMessage[] = []; + const { silenceDeprecations, futureDeprecations, fatalDeprecations } = options.sassOptions ?? {}; + try { const { css, sourceMap, loadedUrls } = await sassWorkerPool.compileStringAsync(data, { url: pathToFileURL(filePath), @@ -104,6 +105,9 @@ async function compileString( loadPaths: options.includePaths, sourceMap: options.sourcemap, sourceMapIncludeSources: options.sourcemap, + silenceDeprecations, + fatalDeprecations, + futureDeprecations, quietDeps: true, importers: [ { diff --git a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts index 72bc83779eb7..1cfcc9bd7207 100644 --- a/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts +++ b/packages/angular/build/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.ts @@ -11,9 +11,18 @@ import glob from 'fast-glob'; import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { extname } from 'node:path'; +import type { Options } from 'sass'; import type { PostcssConfiguration } from '../../../utils/postcss-configuration'; import { LoadResultCache, createCachedLoad } from '../load-result-cache'; +/** + * Configuration options for handling Sass-specific deprecations in a stylesheet plugin. + */ +export type StylesheetPluginSassOptions = Pick< + Options<'async'>, + 'futureDeprecations' | 'fatalDeprecations' | 'silenceDeprecations' +>; + /** * Convenience type for a postcss processor. */ @@ -60,6 +69,11 @@ export interface StylesheetPluginOptions { * and any tailwind usage must be manually configured in the custom postcss usage. */ postcssConfiguration?: PostcssConfiguration; + + /** + * Optional Options for configuring Sass behavior. + */ + sassOptions?: StylesheetPluginSassOptions; } /**