From bf3d73c2c6fd78d4eea23f5356fb26f3232fabee Mon Sep 17 00:00:00 2001 From: Fabiano Brito Date: Wed, 18 Apr 2018 13:35:55 -0300 Subject: [PATCH] Add Sass loader (#4195) * Installs and adds sass loader task in webpack for dev environment. * Uses Timer's branch of sass-loader without node-sass dependency. * Adds method for handling SASS modules. * Fixes extension of excluded files when looking for scss modules. * Adds support for both .scss and .sass extensions. * Uses ExtractTextPlugin with sass-loader to bundle styles for the production build. * Bundles SASS modules for the production build. * Uses the latest version of sass-loader. * Adds function to create different rules for style loaders in dev environment. * Abstracts style loaders to a common function to avoid repetition. * Simplifies the common function that creates style loaders. * Creates assets for testing SASS/SCSS support. * Creates mock components and unit tests for SASS and SCSS with and without modules. * Creates integration tests for SASS/SCSS support. * Adds node-sass as a template dependency so sass-loader can be tested. * Includes sass tests when test component is mounted. * Fixes asserted module name for sass and scss modules tests. * Removes tests against css imports in SCSS and SASS files. * Updates sass-loader to v7. * Uses getCSSModuleLocalIdent from react-dev-utils. * Fixes tests to match the use of getCSSModuleLocalIdent. * Improves readability of getStyleLoader function. * Uses postcss after sass. * Refactors dev config to simplify common function for style loaders. * Refactors prod config to simplify common function for style loaders. * Use importLoaders config according to css-loader docs. --- .../config/webpack.config.dev.js | 86 ++++++---- .../config/webpack.config.prod.js | 149 +++++++++++------- .../kitchensink/.template.dependencies.json | 1 + .../kitchensink/integration/webpack.test.js | 36 +++++ .../fixtures/kitchensink/src/App.js | 20 +++ .../src/features/webpack/SassInclusion.js | 11 ++ .../features/webpack/SassInclusion.test.js | 17 ++ .../features/webpack/SassModulesInclusion.js | 13 ++ .../webpack/SassModulesInclusion.test.js | 17 ++ .../src/features/webpack/ScssInclusion.js | 11 ++ .../features/webpack/ScssInclusion.test.js | 17 ++ .../features/webpack/ScssModulesInclusion.js | 13 ++ .../webpack/ScssModulesInclusion.test.js | 17 ++ .../webpack/assets/sass-styles.module.sass | 3 + .../features/webpack/assets/sass-styles.sass | 3 + .../webpack/assets/scss-styles.module.scss | 4 + .../features/webpack/assets/scss-styles.scss | 4 + packages/react-scripts/package.json | 1 + 18 files changed, 336 insertions(+), 87 deletions(-) create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassInclusion.js create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassInclusion.test.js create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassModulesInclusion.js create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassModulesInclusion.test.js create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssInclusion.js create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssInclusion.test.js create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssModulesInclusion.js create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssModulesInclusion.test.js create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/sass-styles.module.sass create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/sass-styles.sass create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/scss-styles.module.scss create mode 100644 packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/scss-styles.scss diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index fd18d23d255..7a1b66803bc 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -46,6 +46,31 @@ const postCSSLoaderOptions = { ], }; +// style files regexes +const cssRegex = /\.css$/; +const cssModuleRegex = /\.module\.css$/; +const sassRegex = /\.(scss|sass)$/; +const sassModuleRegex = /\.module\.(scss|sass)$/; + +// common function to get style loaders +const getStyleLoaders = (cssOptions, preProcessor) => { + const loaders = [ + require.resolve('style-loader'), + { + loader: require.resolve('css-loader'), + options: cssOptions, + }, + { + loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, + ]; + if (preProcessor) { + loaders.push(require.resolve(preProcessor)); + } + return loaders; +}; + // This is the development configuration. // It is focused on developer experience and fast rebuilds. // The production configuration is different and lives in a separate file. @@ -243,41 +268,44 @@ module.exports = { // in development "style" loader enables hot editing of CSS. // By default we support CSS Modules with the extension .module.css { - test: /\.css$/, - exclude: /\.module\.css$/, - use: [ - require.resolve('style-loader'), - { - loader: require.resolve('css-loader'), - options: { - importLoaders: 1, - }, - }, - { - loader: require.resolve('postcss-loader'), - options: postCSSLoaderOptions, - }, - ], + test: cssRegex, + exclude: cssModuleRegex, + use: getStyleLoaders({ + importLoaders: 1, + }), }, // Adds support for CSS Modules (https://github.com/css-modules/css-modules) // using the extension .module.css { - test: /\.module\.css$/, - use: [ - require.resolve('style-loader'), - { - loader: require.resolve('css-loader'), - options: { - importLoaders: 1, - modules: true, - getLocalIdent: getCSSModuleLocalIdent, - }, - }, + test: cssModuleRegex, + use: getStyleLoaders({ + importLoaders: 1, + modules: true, + getLocalIdent: getCSSModuleLocalIdent, + }), + }, + // Opt-in support for SASS (using .scss or .sass extensions). + // Chains the sass-loader with the css-loader and the style-loader + // to immediately apply all styles to the DOM. + // By default we support SASS Modules with the + // extensions .module.scss or .module.sass + { + test: sassRegex, + exclude: sassModuleRegex, + use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader'), + }, + // Adds support for CSS Modules, but using SASS + // using the extension .module.scss or .module.sass + { + test: sassModuleRegex, + use: getStyleLoaders( { - loader: require.resolve('postcss-loader'), - options: postCSSLoaderOptions, + importLoaders: 2, + modules: true, + getLocalIdent: getCSSModuleLocalIdent, }, - ], + 'sass-loader' + ), }, // The GraphQL loader preprocesses GraphQL queries in .graphql files. { diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 67fa18150a3..e05c8ae0cb4 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -69,6 +69,49 @@ const postCSSLoaderOptions = { flexbox: 'no-2009', }), ], + sourceMap: shouldUseSourceMap, +}; + +// style files regexes +const cssRegex = /\.css$/; +const cssModuleRegex = /\.module\.css$/; +const sassRegex = /\.(scss|sass)$/; +const sassModuleRegex = /\.module\.(scss|sass)$/; + +// common function to get style loaders +const getStyleLoaders = (cssOptions, preProcessor) => { + const loaders = [ + { + loader: require.resolve('css-loader'), + options: cssOptions, + }, + { + loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, + ]; + if (preProcessor) { + loaders.push({ + loader: require.resolve(preProcessor), + options: { + sourceMap: shouldUseSourceMap, + }, + }); + } + return ExtractTextPlugin.extract( + Object.assign( + { + fallback: { + loader: require.resolve('style-loader'), + options: { + hmr: false, + }, + }, + use: loaders, + }, + extractTextPluginOptions + ) + ); }; // This is the production configuration. @@ -255,69 +298,59 @@ module.exports = { // in the main CSS file. // By default we support CSS Modules with the extension .module.css { - test: /\.css$/, - exclude: /\.module\.css$/, - loader: ExtractTextPlugin.extract( - Object.assign( - { - fallback: { - loader: require.resolve('style-loader'), - options: { - hmr: false, - }, - }, - use: [ - { - loader: require.resolve('css-loader'), - options: { - importLoaders: 1, - minimize: true, - sourceMap: shouldUseSourceMap, - }, - }, - { - loader: require.resolve('postcss-loader'), - options: postCSSLoaderOptions, - }, - ], - }, - extractTextPluginOptions - ) - ), + test: cssRegex, + exclude: cssModuleRegex, + loader: getStyleLoaders({ + importLoaders: 1, + minimize: true, + sourceMap: shouldUseSourceMap, + }), // Note: this won't work without `new ExtractTextPlugin()` in `plugins`. }, // Adds support for CSS Modules (https://github.com/css-modules/css-modules) // using the extension .module.css { - test: /\.module\.css$/, - loader: ExtractTextPlugin.extract( - Object.assign( - { - fallback: { - loader: require.resolve('style-loader'), - options: { - hmr: false, - }, - }, - use: [ - { - loader: require.resolve('css-loader'), - options: { - importLoaders: 1, - minimize: true, - sourceMap: shouldUseSourceMap, - modules: true, - getLocalIdent: getCSSModuleLocalIdent, - }, - }, - { - loader: require.resolve('postcss-loader'), - options: postCSSLoaderOptions, - }, - ], - }, - extractTextPluginOptions - ) + test: cssRegex, + loader: getStyleLoaders({ + importLoaders: 1, + minimize: true, + sourceMap: shouldUseSourceMap, + modules: true, + getLocalIdent: getCSSModuleLocalIdent, + }), + // Note: this won't work without `new ExtractTextPlugin()` in `plugins`. + }, + // Opt-in support for SASS. The logic here is somewhat similar + // as in the CSS routine, except that "sass-loader" runs first + // to compile SASS files into CSS. + // By default we support SASS Modules with the + // extensions .module.scss or .module.sass + { + test: sassRegex, + exclude: sassModuleRegex, + loader: getStyleLoaders( + { + importLoaders: 2, + minimize: true, + sourceMap: shouldUseSourceMap, + }, + 'sass-loader' + ), + // Note: this won't work without `new ExtractTextPlugin()` in `plugins`. + }, + // Adds support for CSS Modules, but using SASS + // using the extension .module.scss or .module.sass + { + test: sassModuleRegex, + loader: getStyleLoaders( + { + importLoaders: 2, + minimize: true, + sourceMap: shouldUseSourceMap, + modules: true, + getLocalIdent: getCSSModuleLocalIdent, + }, + 'sass-loader' ), // Note: this won't work without `new ExtractTextPlugin()` in `plugins`. }, diff --git a/packages/react-scripts/fixtures/kitchensink/.template.dependencies.json b/packages/react-scripts/fixtures/kitchensink/.template.dependencies.json index e4d3bec84f2..a027e57d765 100644 --- a/packages/react-scripts/fixtures/kitchensink/.template.dependencies.json +++ b/packages/react-scripts/fixtures/kitchensink/.template.dependencies.json @@ -6,6 +6,7 @@ "chai": "3.5.0", "jsdom": "9.8.3", "mocha": "3.2.0", + "node-sass": "4.8.3", "normalize.css": "7.0.0", "prop-types": "15.5.6", "test-integrity": "1.0.0" diff --git a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js index 865fb118517..dc10c9a1b08 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js @@ -34,6 +34,42 @@ describe('Integration', () => { ); }); + it('scss inclusion', async () => { + const doc = await initDOM('scss-inclusion'); + + expect( + doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '') + ).to.match(/#feature-scss-inclusion\{background:.+;color:.+}/); + }); + + it('scss modules inclusion', async () => { + const doc = await initDOM('scss-modules-inclusion'); + + expect( + doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '') + ).to.match( + /.+scss-styles_scssModulesInclusion.+\{background:.+;color:.+}/ + ); + }); + + it('sass inclusion', async () => { + const doc = await initDOM('sass-inclusion'); + + expect( + doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '') + ).to.match(/#feature-sass-inclusion\{background:.+;color:.+}/); + }); + + it('sass modules inclusion', async () => { + const doc = await initDOM('sass-modules-inclusion'); + + expect( + doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '') + ).to.match( + /.+sass-styles_sassModulesInclusion.+\{background:.+;color:.+}/ + ); + }); + it('graphql files inclusion', async () => { const doc = await initDOM('graphql-inclusion'); const children = doc.getElementById('graphql-inclusion').children; diff --git a/packages/react-scripts/fixtures/kitchensink/src/App.js b/packages/react-scripts/fixtures/kitchensink/src/App.js index c45ef2a38e8..f5e3d5911b1 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/App.js +++ b/packages/react-scripts/fixtures/kitchensink/src/App.js @@ -86,6 +86,26 @@ class App extends Component { this.setFeature(f.default) ); break; + case 'scss-inclusion': + import('./features/webpack/ScssInclusion').then(f => + this.setFeature(f.default) + ); + break; + case 'scss-modules-inclusion': + import('./features/webpack/ScssModulesInclusion').then(f => + this.setFeature(f.default) + ); + break; + case 'sass-inclusion': + import('./features/webpack/SassInclusion').then(f => + this.setFeature(f.default) + ); + break; + case 'sass-modules-inclusion': + import('./features/webpack/SassModulesInclusion').then(f => + this.setFeature(f.default) + ); + break; case 'custom-interpolation': import('./features/syntax/CustomInterpolation').then(f => this.setFeature(f.default) diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassInclusion.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassInclusion.js new file mode 100644 index 00000000000..c15f175dbfe --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassInclusion.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import './assets/sass-styles.sass'; + +export default () =>

We love useless text.

; diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassInclusion.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassInclusion.test.js new file mode 100644 index 00000000000..c58080ab5b1 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassInclusion.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import SassInclusion from './SassInclusion'; + +describe('sass inclusion', () => { + it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + }); +}); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassModulesInclusion.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassModulesInclusion.js new file mode 100644 index 00000000000..dd832eaea32 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassModulesInclusion.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import styles from './assets/sass-styles.module.sass'; + +export default () => ( +

SASS Modules are working!

+); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassModulesInclusion.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassModulesInclusion.test.js new file mode 100644 index 00000000000..373330a5fac --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/SassModulesInclusion.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import SassModulesInclusion from './SassModulesInclusion'; + +describe('sass modules inclusion', () => { + it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + }); +}); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssInclusion.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssInclusion.js new file mode 100644 index 00000000000..b363f430ea8 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssInclusion.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import './assets/scss-styles.scss'; + +export default () =>

We love useless text.

; diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssInclusion.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssInclusion.test.js new file mode 100644 index 00000000000..81d49588cac --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssInclusion.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import ScssInclusion from './ScssInclusion'; + +describe('scss inclusion', () => { + it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + }); +}); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssModulesInclusion.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssModulesInclusion.js new file mode 100644 index 00000000000..ef0e2bf4c3c --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssModulesInclusion.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import styles from './assets/scss-styles.module.scss'; + +export default () => ( +

SCSS Modules are working!

+); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssModulesInclusion.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssModulesInclusion.test.js new file mode 100644 index 00000000000..5de52839666 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/ScssModulesInclusion.test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import ScssModulesInclusion from './ScssModulesInclusion'; + +describe('scss modules inclusion', () => { + it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + }); +}); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/sass-styles.module.sass b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/sass-styles.module.sass new file mode 100644 index 00000000000..09773b05d85 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/sass-styles.module.sass @@ -0,0 +1,3 @@ +.sassModulesInclusion + background: darkblue + color: lightblue diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/sass-styles.sass b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/sass-styles.sass new file mode 100644 index 00000000000..28935fe49b1 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/sass-styles.sass @@ -0,0 +1,3 @@ +#feature-sass-inclusion + background: ghostwhite + color: crimson diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/scss-styles.module.scss b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/scss-styles.module.scss new file mode 100644 index 00000000000..5beb80ebabe --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/scss-styles.module.scss @@ -0,0 +1,4 @@ +.scssModulesInclusion { + background: darkblue; + color: lightblue; +} diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/scss-styles.scss b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/scss-styles.scss new file mode 100644 index 00000000000..b2f91e2e2f3 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/scss-styles.scss @@ -0,0 +1,4 @@ +#feature-scss-inclusion { + background: ghostwhite; + color: crimson; +} diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index 66359e30661..cafb43d9aa0 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -58,6 +58,7 @@ "raf": "3.4.0", "react-dev-utils": "^5.0.0", "resolve": "1.6.0", + "sass-loader": "7.0.0", "style-loader": "0.19.1", "svgr": "1.8.1", "sw-precache-webpack-plugin": "0.11.4",