diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index fd18d23d25..7a1b66803b 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 67fa18150a..e05c8ae0cb 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 e4d3bec84f..a027e57d76 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 865fb11851..dc10c9a1b0 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 c45ef2a38e..f5e3d5911b 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 0000000000..c15f175dbf --- /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 0000000000..c58080ab5b --- /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(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 0000000000..373330a5fa --- /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(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 0000000000..81d49588ca --- /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(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 0000000000..5de5283966 --- /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(