From d47b4417d46f85f9f5bb460576d32aa0104e6d43 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 8 Apr 2021 19:17:52 -0400 Subject: [PATCH] feat(@angular-devkit/build-angular): support processing component inline CSS styles The internal Webpack configuration now includes support for style rules with MIME type conditions. This allows the data URIs generated for inline component CSS styles by the Angular Webpack plugin to be processed with the same loaders as file based styles. --- .../src/browser/specs/styles_spec.ts | 53 +++- .../src/webpack/configs/styles.ts | 278 ++++++++++-------- .../src/webpack/configs/typescript.ts | 1 + .../tests/build/differential-loading-sri.ts | 5 +- tests/legacy-cli/e2e/tests/build/worker.ts | 9 +- 5 files changed, 214 insertions(+), 132 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts index faee30b1d996..7e8e64ec45e5 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts @@ -124,6 +124,56 @@ describe('Browser Builder styles', () => { await browserBuild(architect, host, target, { extractCss: true }); }); + it('supports autoprefixer with inline component styles in JIT mode', async () => { + host.writeMultipleFiles({ + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styles: ['div { flex: 1 }'], + }) + export class AppComponent { + title = 'app'; + } + `, + '.browserslistrc': 'IE 10', + }); + + // Set target to ES5 to avoid differential loading and unnecessary testing time + host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5"'); + + const { files } = await browserBuild(architect, host, target, { aot: false }); + + expect(await files['main.js']).toContain('-ms-flex: 1;'); + }); + + it('supports autoprefixer with inline component styles in AOT mode', async () => { + host.writeMultipleFiles({ + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styles: ['div { flex: 1 }'], + }) + export class AppComponent { + title = 'app'; + } + `, + '.browserslistrc': 'IE 10', + }); + + // Set target to ES5 to avoid differential loading and unnecessary testing time + host.replaceInFile('tsconfig.json', '"target": "es2017"', '"target": "es5"'); + + const { files } = await browserBuild(architect, host, target, { aot: true }); + + expect(await files['main.js']).toContain('-ms-flex: 1;'); + }); + extensionsWithImportSupport.forEach(ext => { it(`supports imports in ${ext} files`, async () => { host.writeMultipleFiles({ @@ -456,7 +506,8 @@ describe('Browser Builder styles', () => { main = await files['main.js']; expect(styles).toContain(`url('/assets/global-img-absolute.svg')`); expect(main).toContain(`url('/assets/component-img-absolute.svg')`); - }); + // NOTE: Timeout for large amount of builds in test. Test should be split up when refactored. + }, 4 * 60 * 1000); it(`supports bootstrap@4 with full path`, async () => { const bootstrapPath = dirname(require.resolve('bootstrap/package.json')); 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 7beee6ec2c2e..d3b59f85c07f 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/styles.ts @@ -17,7 +17,11 @@ import { RemoveHashPlugin, SuppressExtractedTextChunksWebpackPlugin, } from '../plugins'; -import { assetNameTemplateFactory, getOutputHashFormat, normalizeExtraEntryPoints } from '../utils/helpers'; +import { + assetNameTemplateFactory, + getOutputHashFormat, + normalizeExtraEntryPoints, +} from '../utils/helpers'; function resolveGlobalStyles( styleEntrypoints: ExtraEntryPoint[], @@ -80,7 +84,8 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio const hashFormat = getOutputHashFormat(buildOptions.outputHashing as string); // use includePaths from appConfig - const includePaths = buildOptions.stylePreprocessorOptions?.includePaths?.map(p => path.resolve(root, p)) ?? []; + const includePaths = + buildOptions.stylePreprocessorOptions?.includePaths?.map((p) => path.resolve(root, p)) ?? []; // Process global styles. const { entryPoints, noInjectNames, paths: globalStylePaths } = resolveGlobalStyles( @@ -143,7 +148,7 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio const { supportedBrowsers } = new BuildBrowserFeatures(wco.projectRoot); const postcssOptionsCreator = (inlineSourcemaps: boolean, extracted: boolean | undefined) => { // tslint:disable-next-line: no-any - return (loader: any) => ({ + const optionGenerator = (loader: any) => ({ map: inlineSourcemaps ? { inline: true, @@ -152,7 +157,7 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio : undefined, plugins: [ postcssImports({ - resolve: (url: string) => url.startsWith('~') ? url.substr(1) : url, + resolve: (url: string) => (url.startsWith('~') ? url.substr(1) : url), load: (filename: string) => { return new Promise((resolve, reject) => { loader.fs.readFile(filename, (err: Error, data: Buffer) => { @@ -186,24 +191,26 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio }), ], }); + // postcss-loader fails when trying to determine configuration files for data URIs + optionGenerator.config = false; + + return optionGenerator; }; // load component css as raw strings const componentsSourceMap = !!( - cssSourceMap + cssSourceMap && // Never use component css sourcemap when style optimizations are on. // It will just increase bundle size without offering good debug experience. - && !buildOptions.optimization.styles.minify + !buildOptions.optimization.styles.minify && // Inline all sourcemap types except hidden ones, which are the same as no sourcemaps // for component css. - && !buildOptions.sourceMap.hidden + !buildOptions.sourceMap.hidden ); if (buildOptions.extractCss) { // extract global css from js files into own css file. - extraPlugins.push( - new MiniCssExtractPlugin({ filename: `[name]${hashFormat.extract}.css` }), - ); + extraPlugins.push(new MiniCssExtractPlugin({ filename: `[name]${hashFormat.extract}.css` })); if (!buildOptions.hmr) { // don't remove `.js` files for `.css` when we are using HMR these contain HMR accept codes. @@ -212,129 +219,150 @@ export function getStylesConfig(wco: WebpackConfigOptions): webpack.Configuratio } } - // Rule for all supported style types - const styleRule: webpack.RuleSetRule = { - test: /\.(?:css|scss|sass|less|styl)$/, - rules: [ - // Setup processing rules for global and component styles - { - oneOf: [ - // Component styles are all styles except defined global styles - { - exclude: globalStylePaths, - use: [ - { loader: require.resolve('raw-loader') }, - { - loader: require.resolve('postcss-loader'), - options: { - implementation: require('postcss'), - postcssOptions: postcssOptionsCreator(componentsSourceMap, false), - }, - }, - ], - }, - // Global styles are only defined global styles - { - include: globalStylePaths, - use: [ - buildOptions.extractCss - ? { - loader: MiniCssExtractPlugin.loader, - } - : require.resolve('style-loader'), - { - loader: require.resolve('css-loader'), - options: { - url: false, - sourceMap: !!cssSourceMap, - }, - }, - { - loader: require.resolve('postcss-loader'), - options: { - implementation: require('postcss'), - postcssOptions: postcssOptionsCreator(false, buildOptions.extractCss), - sourceMap: !!cssSourceMap, - }, - }, - ], - }, - ], + const componentStyleLoaders: webpack.RuleSetUseItem[] = [ + { loader: require.resolve('raw-loader') }, + { + loader: require.resolve('postcss-loader'), + options: { + implementation: require('postcss'), + postcssOptions: postcssOptionsCreator(componentsSourceMap, false), + }, + }, + ]; + + const globalStyleLoaders: webpack.RuleSetUseItem[] = [ + buildOptions.extractCss + ? { + loader: MiniCssExtractPlugin.loader, + } + : require.resolve('style-loader'), + { + loader: require.resolve('css-loader'), + options: { + url: false, + sourceMap: !!cssSourceMap, + }, + }, + { + loader: require.resolve('postcss-loader'), + options: { + implementation: require('postcss'), + postcssOptions: postcssOptionsCreator(false, buildOptions.extractCss), + sourceMap: !!cssSourceMap, }, - // Setup preprocessor rules for all styles - { - oneOf: [ - // No preprocessing required for CSS - { test: /\.css$/, use: [] }, - { - test: /\.scss$|\.sass$/, - use: [ - { - loader: require.resolve('resolve-url-loader'), - options: { - sourceMap: cssSourceMap, - }, - }, - { - loader: require.resolve('sass-loader'), - options: { - implementation: sassImplementation, - sourceMap: true, - sassOptions: { - // 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', - }, - }, - }, - ], + }, + ]; + + const styleLanguages: { + extensions: string[]; + mimetype?: string; + use: webpack.RuleSetUseItem[]; + }[] = [ + { + extensions: ['css'], + mimetype: 'text/css', + use: [], + }, + { + extensions: ['sass', 'scss'], + use: [ + { + loader: require.resolve('resolve-url-loader'), + options: { + sourceMap: cssSourceMap, + }, + }, + { + loader: require.resolve('sass-loader'), + options: { + implementation: sassImplementation, + sourceMap: true, + sassOptions: { + // 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', + }, }, - { - test: /\.less$/, - use: [ - { - loader: require.resolve('less-loader'), - options: { - implementation: require('less'), - sourceMap: cssSourceMap, - lessOptions: { - javascriptEnabled: true, - paths: includePaths, - }, - }, - }, - ], + }, + ], + }, + { + extensions: ['less'], + use: [ + { + loader: require.resolve('less-loader'), + options: { + implementation: require('less'), + sourceMap: cssSourceMap, + lessOptions: { + javascriptEnabled: true, + paths: includePaths, + }, }, - { - test: /\.styl$/, - use: [ - { - loader: require.resolve('stylus-loader'), - options: { - sourceMap: cssSourceMap, - stylusOptions: { - compress: false, - sourceMap: { comment: false }, - paths: includePaths, - }, - }, - }, - ], + }, + ], + }, + { + extensions: ['styl'], + use: [ + { + loader: require.resolve('stylus-loader'), + options: { + sourceMap: cssSourceMap, + stylusOptions: { + compress: false, + sourceMap: { comment: false }, + paths: includePaths, + }, }, - ], - }, - ], - }; + }, + ], + }, + ]; + + const inlineLanguageRules: webpack.RuleSetRule[] = []; + const fileLanguageRules: webpack.RuleSetRule[] = []; + for (const language of styleLanguages) { + if (language.mimetype) { + // inline component styles use data URIs and processing is selected by mimetype + inlineLanguageRules.push({ + mimetype: language.mimetype, + use: [...componentStyleLoaders, ...language.use], + }); + } + + fileLanguageRules.push({ + test: new RegExp(`\\.(?:${language.extensions.join('|')})$`, 'i'), + rules: [ + // Setup processing rules for global and component styles + { + oneOf: [ + // Component styles are all styles except defined global styles + { + exclude: globalStylePaths, + use: componentStyleLoaders, + }, + // Global styles are only defined global styles + { + include: globalStylePaths, + use: globalStyleLoaders, + }, + ], + }, + { use: language.use }, + ], + }); + } return { entry: entryPoints, module: { - rules: [styleRule], + rules: [...fileLanguageRules, ...inlineLanguageRules], }, plugins: extraPlugins, }; 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 58ef4baf5028..36add374b1c8 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts @@ -57,6 +57,7 @@ function createIvyPlugin( fileReplacements, jitMode: !aot, emitNgModuleScope: !optimize, + inlineStyleMimeType: 'text/css', }); } 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 43d0f5726b64..dc4d18765d12 100644 --- a/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts +++ b/tests/legacy-cli/e2e/tests/build/differential-loading-sri.ts @@ -56,11 +56,12 @@ export default async function () { '--output-path=dist/second', ); + const chunkId = '751'; const codeHashES5 = createHash('sha384') - .update(await readFile('dist/first/434-es5.js')) + .update(await readFile(`dist/first/${chunkId}-es5.js`)) .digest('base64'); const codeHashes2017 = createHash('sha384') - .update(await readFile('dist/first/434-es2017.js')) + .update(await readFile(`dist/first/${chunkId}-es2017.js`)) .digest('base64'); await expectFileToMatch('dist/first/runtime-es5.js', 'sha384-' + codeHashES5); diff --git a/tests/legacy-cli/e2e/tests/build/worker.ts b/tests/legacy-cli/e2e/tests/build/worker.ts index eb86bdae5c9d..5bc1866cc99a 100644 --- a/tests/legacy-cli/e2e/tests/build/worker.ts +++ b/tests/legacy-cli/e2e/tests/build/worker.ts @@ -35,10 +35,11 @@ export default async function () { await expectFileToMatch('dist/test-project/main-es2017.js', 'src_app_app_worker_ts'); await ng('build', '--output-hashing=none'); - await expectFileToExist('dist/test-project/609-es5.js'); - await expectFileToMatch('dist/test-project/main-es5.js', '609'); - await expectFileToExist('dist/test-project/609-es2017.js'); - await expectFileToMatch('dist/test-project/main-es2017.js', '609'); + const chunkId = '283'; + 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`); + await expectFileToMatch('dist/test-project/main-es2017.js', chunkId); // console.warn has to be used because chrome only captures warnings and errors by default // https://github.com/angular/protractor/issues/2207