From bac563e5ee1efcda4bfb1334ecc0906796584cbd Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 12 Apr 2021 20:36:56 -0400 Subject: [PATCH] feat(@angular-devkit/build-angular): support specifying stylesheet language for inline component styles A new build option named `inlineStyleLanguage` has been introduced that will allow a project to define the stylesheet language used in an application's inline component styles. Inline component styles are styles defined via the `styles` property within the Angular `Component` decorator. Both JIT and AOT mode are supported. However, JIT mode requires that inline styles only be string literals (compile-time partial evaluation is not supported in JIT mode). Currently supported language options are: `CSS` (default), `Sass`, `SCSS`, and `Less`. If the option is not specified, `CSS` will be used and enables existing projects to continue to function as expected. --- .../build_angular/src/browser/schema.json | 11 ++ .../options/inline-style-language_spec.ts | 110 ++++++++++++++++++ .../build_angular/src/karma/schema.json | 11 ++ .../build_angular/src/server/schema.json | 11 ++ .../build_angular/src/utils/build-options.ts | 2 + .../src/webpack/configs/styles.ts | 35 +++++- .../src/webpack/configs/typescript.ts | 19 ++- .../tests/build/differential-loading-sri.ts | 2 +- tests/legacy-cli/e2e/tests/build/worker.ts | 2 +- 9 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/browser/tests/options/inline-style-language_spec.ts diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index ebb85c2ecb89..b229e7b5627f 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -40,6 +40,17 @@ "$ref": "#/definitions/extraEntryPoint" } }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": [ + "css", + "less", + "sass", + "scss" + ] + }, "stylePreprocessorOptions": { "description": "Options to pass to style preprocessors.", "type": "object", diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/inline-style-language_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/inline-style-language_spec.ts new file mode 100644 index 000000000000..8bcdd8f0ce25 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/tests/options/inline-style-language_spec.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. 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 { buildWebpackBrowser } from '../../index'; +import { InlineStyleLanguage } from '../../schema'; +import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => { + describe('Option: "inlineStyleLanguage"', () => { + beforeEach(async () => { + // Setup application component with inline style property + await harness.modifyFile('src/app/app.component.ts', (content) => { + return content + .replace('styleUrls', 'styles') + .replace('./app.component.css', '__STYLE_MARKER__'); + }); + }); + + for (const aot of [true, false]) { + describe(`[${aot ? 'AOT' : 'JIT'}]`, () => { + it('supports SCSS inline component styles when set to "scss"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + inlineStyleLanguage: InlineStyleLanguage.Scss, + aot, + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '__STYLE_MARKER__', + '$primary-color: green;\\nh1 { color: $primary-color; }', + ), + ); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.toContain('color: green'); + }); + + it('supports Sass inline component styles when set to "sass"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + inlineStyleLanguage: InlineStyleLanguage.Sass, + aot, + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '__STYLE_MARKER__', + '$primary-color: green\\nh1\\n\\tcolor: $primary-color', + ), + ); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.toContain('color: green'); + }); + + // Stylus currently does not function due to the sourcemap logic within the `stylus-loader` + // which tries to read each stylesheet directly from disk. In this case, each stylesheet is + // virtual and cannot be read from disk. This issue affects data URIs in general. + // xit('supports Stylus inline component styles when set to "stylus"', async () => { + // harness.useTarget('build', { + // ...BASE_OPTIONS, + // inlineStyleLanguage: InlineStyleLanguage.Stylus, + // aot, + // }); + + // await harness.modifyFile('src/app/app.component.ts', (content) => + // content.replace( + // '__STYLE_MARKER__', + // '$primary-color = green;\\nh1 { color: $primary-color; }', + // ), + // ); + + // const { result } = await harness.executeOnce(); + + // expect(result?.success).toBe(true); + // harness.expectFile('dist/main.js').content.toContain('color: green'); + // }); + + it('supports Less inline component styles when set to "less"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + inlineStyleLanguage: InlineStyleLanguage.Less, + aot, + }); + + await harness.modifyFile('src/app/app.component.ts', (content) => + content.replace( + '__STYLE_MARKER__', + '@primary-color: green;\\nh1 { color: @primary-color; }', + ), + ); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBe(true); + harness.expectFile('dist/main.js').content.toContain('color: green'); + }); + }); + } + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/schema.json b/packages/angular_devkit/build_angular/src/karma/schema.json index 7b2583982c31..75cd984ec0f1 100644 --- a/packages/angular_devkit/build_angular/src/karma/schema.json +++ b/packages/angular_devkit/build_angular/src/karma/schema.json @@ -44,6 +44,17 @@ "$ref": "#/definitions/extraEntryPoint" } }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": [ + "css", + "less", + "sass", + "scss" + ] + }, "stylePreprocessorOptions": { "description": "Options to pass to style preprocessors", "type": "object", diff --git a/packages/angular_devkit/build_angular/src/server/schema.json b/packages/angular_devkit/build_angular/src/server/schema.json index 089c2e81485e..aa4c01521026 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.json +++ b/packages/angular_devkit/build_angular/src/server/schema.json @@ -13,6 +13,17 @@ "default": "tsconfig.app.json", "description": "The name of the TypeScript configuration file." }, + "inlineStyleLanguage": { + "description": "The stylesheet language to use for the application's inline component styles.", + "type": "string", + "default": "css", + "enum": [ + "css", + "less", + "sass", + "scss" + ] + }, "stylePreprocessorOptions": { "description": "Options to pass to style preprocessors", "type": "object", diff --git a/packages/angular_devkit/build_angular/src/utils/build-options.ts b/packages/angular_devkit/build_angular/src/utils/build-options.ts index ec106c90c68c..433e194d1040 100644 --- a/packages/angular_devkit/build_angular/src/utils/build-options.ts +++ b/packages/angular_devkit/build_angular/src/utils/build-options.ts @@ -15,6 +15,7 @@ import { ExtraEntryPoint, I18NMissingTranslation, IndexUnion, + InlineStyleLanguage, Localize, SourceMapClass, } from '../browser/schema'; @@ -67,6 +68,7 @@ export interface BuildOptions { stylePreprocessorOptions?: { includePaths: string[] }; platform?: 'browser' | 'server'; fileReplacements: NormalizedFileReplacement[]; + inlineStyleLanguage?: InlineStyleLanguage; allowedCommonJsDependencies?: string[]; diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts b/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts index d3b59f85c07f..288653c610a5 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts @@ -264,7 +264,8 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio use: [], }, { - extensions: ['sass', 'scss'], + extensions: ['scss'], + mimetype: 'text/x-scss', use: [ { loader: require.resolve('resolve-url-loader'), @@ -291,8 +292,39 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio }, ], }, + { + extensions: ['sass'], + mimetype: 'text/x-sass', + use: [ + { + loader: require.resolve('resolve-url-loader'), + options: { + sourceMap: cssSourceMap, + }, + }, + { + loader: require.resolve('sass-loader'), + options: { + implementation: sassImplementation, + sourceMap: true, + sassOptions: { + indentedSyntax: true, + // bootstrap-sass requires a minimum precision of 8 + precision: 8, + includePaths, + // Use expanded as otherwise sass will remove comments that are needed for autoprefixer + // Ex: /* autoprefixer grid: autoplace */ + // tslint:disable-next-line: max-line-length + // See: https://github.com/webpack-contrib/sass-loader/blob/45ad0be17264ceada5f0b4fb87e9357abe85c4ff/src/getSassOptions.js#L68-L70 + outputStyle: 'expanded', + }, + }, + }, + ], + }, { extensions: ['less'], + mimetype: 'text/x-less', use: [ { loader: require.resolve('less-loader'), @@ -309,6 +341,7 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio }, { extensions: ['styl'], + mimetype: 'text/x-stylus', use: [ { loader: require.resolve('stylus-loader'), diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts index 36add374b1c8..e9d35ef1880e 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts @@ -51,13 +51,30 @@ function createIvyPlugin( } } + let inlineStyleMimeType; + switch (buildOptions.inlineStyleLanguage) { + case 'less': + inlineStyleMimeType = 'text/x-less'; + break; + case 'sass': + inlineStyleMimeType = 'text/x-sass'; + break; + case 'scss': + inlineStyleMimeType = 'text/x-scss'; + break; + case 'css': + default: + inlineStyleMimeType = 'text/css'; + break; + } + return new AngularWebpackPlugin({ tsconfig, compilerOptions, fileReplacements, jitMode: !aot, emitNgModuleScope: !optimize, - inlineStyleMimeType: 'text/css', + inlineStyleMimeType, }); } diff --git a/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts b/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts index dc4d18765d12..cc0be82b7d95 100644 --- a/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts +++ b/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts @@ -56,7 +56,7 @@ export default async function () { '--output-path=dist/second', ); - const chunkId = '751'; + const chunkId = '730'; const codeHashES5 = createHash('sha384') .update(await readFile(`dist/first/${chunkId}-es5.js`)) .digest('base64'); diff --git a/tests/legacy-cli/e2e/tests/build/worker.ts b/tests/legacy-cli/e2e/tests/build/worker.ts index 5bc1866cc99a..69e03193aec7 100644 --- a/tests/legacy-cli/e2e/tests/build/worker.ts +++ b/tests/legacy-cli/e2e/tests/build/worker.ts @@ -35,7 +35,7 @@ export default async function () { await expectFileToMatch('dist/test-project/main-es2017.js', 'src_app_app_worker_ts'); await ng('build', '--output-hashing=none'); - const chunkId = '283'; + const chunkId = '137'; await expectFileToExist(`dist/test-project/${chunkId}-es5.js`); await expectFileToMatch('dist/test-project/main-es5.js', chunkId); await expectFileToExist(`dist/test-project/${chunkId}-es2017.js`);