From e766f161116cc1c5ed794e3e5eda8188b198bf9d Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 16 Oct 2024 07:00:27 +0000 Subject: [PATCH] feat(@angular/build): add `sass` 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": { "sass": { "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 ++++++ .../style-preprocessor-options-sass_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 +++ .../src/tools/webpack/configs/styles.ts | 4 +- 8 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 packages/angular/build/src/builders/application/tests/options/style-preprocessor-options-sass_spec.ts diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index 60e725568eb0..2022969a2f10 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": [] + }, + "sass": { + "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 active deprecations to ignore. If a deprecation warning of any provided type is encountered during compilation, the compiler will ignore it instead.", + "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/style-preprocessor-options-sass_spec.ts b/packages/angular/build/src/builders/application/tests/options/style-preprocessor-options-sass_spec.ts new file mode 100644 index 000000000000..33c1d1cc9a4b --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/options/style-preprocessor-options-sass_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: "stylePreprocessorOptions.sass"', () => { + 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: { + sass: { + 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: { + sass: {}, + }, + }); + + 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: { + sass: { + 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..38bad72a7026 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 + sass: stylePreprocessorOptions?.sass 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..12bdf12f04f7 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 + sass: stylePreprocessorOptions?.sass 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..2a6200124ea5 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, StylesheetPluginsass } 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[]; + sass?: StylesheetPluginsass; externalDependencies?: string[]; target: string[]; tailwindConfiguration?: { file: string; package: string }; @@ -51,6 +52,7 @@ export function createStylesheetBundleOptions( inlineComponentData, tailwindConfiguration: options.tailwindConfiguration, postcssConfiguration: options.postcssConfiguration, + sass: options.sass, }, 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..bb50543360d0 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.sass ?? {}; + 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..ac03c602e658 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 StylesheetPluginsass = 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. + */ + sass?: StylesheetPluginsass; } /** diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts b/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts index 5d92b574502f..f137faca5f8b 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/configs/styles.ts @@ -323,12 +323,12 @@ function getSassLoaderOptions( // Webpack importer is only implemented in the legacy API and we have our own custom Webpack importer. // See: https://github.com/webpack-contrib/sass-loader/blob/997f3eb41d86dd00d5fa49c395a1aeb41573108c/src/utils.js#L642-L651 webpackImporter: false, - sassOptions: (loaderContext: LoaderContext<{}>) => ({ + sass: (loaderContext: LoaderContext<{}>) => ({ importers: [getSassResolutionImporter(loaderContext, root, preserveSymlinks)], loadPaths: includePaths, // Use expanded as otherwise sass will remove comments that are needed for autoprefixer // Ex: /* autoprefixer grid: autoplace */ - // See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70 + // See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getsass.js#L68-L70 style: 'expanded', // Silences compiler warnings from 3rd party stylesheets quietDeps: !verbose,