diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index cdb6830ad..04adaa526 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -30,6 +30,20 @@ const publicUrl = ''; // Get environment variables to inject into our app. const env = getClientEnvironment(publicUrl); +// Options for PostCSS as we reference these options twice +// Adds vendor prefixing to support IE9 and above +const postCSSLoaderOptions = { + // Necessary for external CSS imports to work + // https://github.com/facebookincubator/create-react-app/issues/2677 + ident: 'postcss', + plugins: () => [ + require('postcss-flexbugs-fixes'), + autoprefixer({ + flexbox: 'no-2009', + }), + ], +}; + // 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. @@ -123,7 +137,7 @@ module.exports = { // please link the files into your node_modules/ and let module-resolution kick in. // Make sure your source files are compiled, as they will not be processed in any way. new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), - new TsconfigPathsPlugin({configFile: paths.appTsConfig}) + new TsconfigPathsPlugin({ configFile: paths.appTsConfig }), ], }, module: { @@ -176,6 +190,7 @@ module.exports = { // in development "style" loader enables hot editing of CSS. { test: /\.css$/, + exclude: /\.module\.css$/, use: [ require.resolve('style-loader'), { @@ -186,30 +201,34 @@ module.exports = { }, { loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, + ], + }, + // 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: { - // Necessary for external CSS imports to work - // https://github.com/facebookincubator/create-react-app/issues/2677 - ident: 'postcss', - plugins: () => [ - require('postcss-flexbugs-fixes'), - autoprefixer({ - browsers: [ - '>1%', - 'last 4 versions', - 'Firefox ESR', - 'not ie < 9', // React doesn't support IE8 anyway - ], - flexbox: 'no-2009', - }), - ], + importLoaders: 1, + modules: true, + localIdentName: '[path]__[name]___[local]', }, }, + { + loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, ], }, - // BIKO::START { test: /\.scss$/, + exclude: /\.module\.scss$/, use: [ require.resolve('style-loader'), { @@ -220,24 +239,27 @@ module.exports = { }, { loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, + require.resolve('sass-loader'), + ], + }, + { + test: /\.module\.scss$/, + use: [ + require.resolve('style-loader'), + { + loader: require.resolve('css-loader'), options: { - // Necessary for external CSS imports to work - // https://github.com/facebookincubator/create-react-app/issues/2677 - ident: 'postcss', - plugins: () => [ - require('postcss-flexbugs-fixes'), - autoprefixer({ - browsers: [ - '>1%', - 'last 4 versions', - 'Firefox ESR', - 'not ie < 9', // React doesn't support IE8 anyway - ], - flexbox: 'no-2009', - }), - ], + importLoaders: 1, + modules: true, + localIdentName: '[path]__[name]___[local]', }, }, + { + loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, require.resolve('sass-loader'), ], }, diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index de2f2f064..d2d19ce26 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -55,6 +55,20 @@ const extractTextPluginOptions = shouldUseRelativeAssetPaths { publicPath: Array(cssFilename.split('/').length).join('../') } : {}; +// Options for PostCSS as we reference these options twice +// Adds vendor prefixing to support IE9 and above +const postCSSLoaderOptions = { + // Necessary for external CSS imports to work + // https://github.com/facebookincubator/create-react-app/issues/2677 + ident: 'postcss', + plugins: () => [ + require('postcss-flexbugs-fixes'), + autoprefixer({ + flexbox: 'no-2009', + }), + ], +}; + // This is the production configuration. // It compiles slowly and is focused on producing a fast and minimal bundle. // The development configuration is different and lives in a separate file. @@ -130,7 +144,7 @@ module.exports = { // please link the files into your node_modules/ and let module-resolution kick in. // Make sure your source files are compiled, as they will not be processed in any way. new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), - new TsconfigPathsPlugin({configFile: paths.appTsConfig}) + new TsconfigPathsPlugin({ configFile: paths.appTsConfig }), ], }, module: { @@ -186,8 +200,10 @@ module.exports = { // tags. If you use code splitting, however, any async bundles will still // use the "style" loader inside the async code so CSS from them won't be // 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( { @@ -208,24 +224,43 @@ module.exports = { }, { loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, + ], + }, + extractTextPluginOptions + ) + ), + // 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: { - // Necessary for external CSS imports to work - // https://github.com/facebookincubator/create-react-app/issues/2677 - ident: 'postcss', - plugins: () => [ - require('postcss-flexbugs-fixes'), - autoprefixer({ - browsers: [ - '>1%', - 'last 4 versions', - 'Firefox ESR', - 'not ie < 9', // React doesn't support IE8 anyway - ], - flexbox: 'no-2009', - }), - ], + importLoaders: 1, + minimize: true, + sourceMap: shouldUseSourceMap, + modules: true, + localIdentName: '[path]__[name]___[local]', }, }, + { + loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, ], }, extractTextPluginOptions @@ -237,6 +272,7 @@ module.exports = { // BIKO::START { test: /\.scss$/, + exclude: /\.module\.scss$/, loader: ExtractTextPlugin.extract( Object.assign( { @@ -257,24 +293,47 @@ module.exports = { }, { loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, + { + loader: require.resolve('sass-loader'), + options: { + sourceMap: shouldUseSourceMap, + }, + }, + ], + }, + extractTextPluginOptions + ) + ), + // Note: this won't work without `new ExtractTextPlugin()` in `plugins`. + }, + { + test: /\.module\.scss$/, + loader: ExtractTextPlugin.extract( + Object.assign( + { + fallback: { + loader: require.resolve('style-loader'), + options: { + hmr: false, + }, + }, + use: [ + { + loader: require.resolve('css-loader'), options: { - // Necessary for external CSS imports to work - // https://github.com/facebookincubator/create-react-app/issues/2677 - ident: 'postcss', - plugins: () => [ - require('postcss-flexbugs-fixes'), - autoprefixer({ - browsers: [ - '>1%', - 'last 4 versions', - 'Firefox ESR', - 'not ie < 9', // React doesn't support IE8 anyway - ], - flexbox: 'no-2009', - }), - ], + importLoaders: 1, + minimize: true, + sourceMap: shouldUseSourceMap, + modules: true, + localIdentName: '[path]__[name]___[local]', }, }, + { + loader: require.resolve('postcss-loader'), + options: postCSSLoaderOptions, + }, { loader: require.resolve('sass-loader'), options: { diff --git a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js index 1fe5c9f64..06ec83602 100644 --- a/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js +++ b/packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js @@ -21,6 +21,16 @@ describe('Integration', () => { ).to.match(/#feature-css-inclusion\{background:.+;color:.+}/); }); + it('css modules inclusion', async () => { + const doc = await initDOM('css-modules-inclusion'); + + expect( + doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '') + ).to.match( + /.+__style-module___cssModulesInclusion+\{background:.+;color:.+}/ + ); + }); + it('image inclusion', async () => { const doc = await initDOM('image-inclusion'); diff --git a/packages/react-scripts/fixtures/kitchensink/src/App.js b/packages/react-scripts/fixtures/kitchensink/src/App.js index b5f741a34..08e17468e 100644 --- a/packages/react-scripts/fixtures/kitchensink/src/App.js +++ b/packages/react-scripts/fixtures/kitchensink/src/App.js @@ -52,114 +52,137 @@ class App extends Component { const feature = window.location.hash.slice(1); switch (feature) { case 'array-destructuring': - import( - './features/syntax/ArrayDestructuring' - ).then(f => this.setFeature(f.default)); + import('./features/syntax/ArrayDestructuring').then(f => + this.setFeature(f.default) + ); break; case 'array-spread': import('./features/syntax/ArraySpread').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'async-await': import('./features/syntax/AsyncAwait').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'class-properties': import('./features/syntax/ClassProperties').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'computed-properties': - import( - './features/syntax/ComputedProperties' - ).then(f => this.setFeature(f.default)); + import('./features/syntax/ComputedProperties').then(f => + this.setFeature(f.default) + ); break; case 'css-inclusion': import('./features/webpack/CssInclusion').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); + break; + case 'css-modules-inclusion': + import('./features/webpack/CssModulesInclusion').then(f => + this.setFeature(f.default) + ); break; case 'custom-interpolation': - import( - './features/syntax/CustomInterpolation' - ).then(f => this.setFeature(f.default)); + import('./features/syntax/CustomInterpolation').then(f => + this.setFeature(f.default) + ); break; case 'default-parameters': import('./features/syntax/DefaultParameters').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'destructuring-and-await': - import( - './features/syntax/DestructuringAndAwait' - ).then(f => this.setFeature(f.default)); + import('./features/syntax/DestructuringAndAwait').then(f => + this.setFeature(f.default) + ); break; case 'file-env-variables': import('./features/env/FileEnvVariables').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'generators': import('./features/syntax/Generators').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'image-inclusion': import('./features/webpack/ImageInclusion').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'json-inclusion': import('./features/webpack/JsonInclusion').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'linked-modules': import('./features/webpack/LinkedModules').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'node-path': import('./features/env/NodePath').then(f => this.setFeature(f.default)); break; case 'no-ext-inclusion': import('./features/webpack/NoExtInclusion').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'object-destructuring': - import( - './features/syntax/ObjectDestructuring' - ).then(f => this.setFeature(f.default)); + import('./features/syntax/ObjectDestructuring').then(f => + this.setFeature(f.default) + ); break; case 'object-spread': import('./features/syntax/ObjectSpread').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'promises': import('./features/syntax/Promises').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'public-url': import('./features/env/PublicUrl').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'rest-and-default': import('./features/syntax/RestAndDefault').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'rest-parameters': import('./features/syntax/RestParameters').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'shell-env-variables': import('./features/env/ShellEnvVariables').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'svg-inclusion': import('./features/webpack/SvgInclusion').then(f => - this.setFeature(f.default)); + this.setFeature(f.default) + ); break; case 'template-interpolation': - import( - './features/syntax/TemplateInterpolation' - ).then(f => this.setFeature(f.default)); + import('./features/syntax/TemplateInterpolation').then(f => + this.setFeature(f.default) + ); break; case 'unknown-ext-inclusion': - import( - './features/webpack/UnknownExtInclusion' - ).then(f => this.setFeature(f.default)); + import('./features/webpack/UnknownExtInclusion').then(f => + this.setFeature(f.default) + ); break; default: throw new Error(`Missing feature "${feature}"`); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/CssModulesInclusion.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/CssModulesInclusion.js new file mode 100644 index 000000000..0f96ae161 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/CssModulesInclusion.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/style.module.css'; + +export default () => ( +

CSS Modules are working!

+); diff --git a/packages/react-scripts/fixtures/kitchensink/src/features/webpack/CssModulesInclusion.test.js b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/CssModulesInclusion.test.js new file mode 100644 index 000000000..6eae30fb8 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/CssModulesInclusion.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 CssModulesInclusion from './CssModulesInclusion'; + +describe('css 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/style.module.css b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/style.module.css new file mode 100644 index 000000000..2ce765188 --- /dev/null +++ b/packages/react-scripts/fixtures/kitchensink/src/features/webpack/assets/style.module.css @@ -0,0 +1,4 @@ +.cssModulesInclusion { + background: darkblue; + color: lightblue; +} diff --git a/packages/react-scripts/package.json b/packages/react-scripts/package.json index e7f6b00da..7a8fb8a31 100644 --- a/packages/react-scripts/package.json +++ b/packages/react-scripts/package.json @@ -27,12 +27,13 @@ "css-loader": "0.28.7", "dotenv": "4.0.0", "extract-text-webpack-plugin": "3.0.2", - "file-loader": "0.11.2", "fork-ts-checker-webpack-plugin": "^0.2.8", - "fs-extra": "3.0.1", - "html-webpack-plugin": "2.29.0", - "jest": "20.0.4", "node-sass": "^4.7.2", + "file-loader": "1.1.6", + "fs-extra": "5.0.0", + "html-webpack-plugin": "2.30.1", + "identity-obj-proxy": "3.0.0", + "jest": "22.1.1", "object-assign": "4.1.1", "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.8", diff --git a/packages/react-scripts/scripts/utils/createJestConfig.js b/packages/react-scripts/scripts/utils/createJestConfig.js index 3a744c971..7705a0cf5 100644 --- a/packages/react-scripts/scripts/utils/createJestConfig.js +++ b/packages/react-scripts/scripts/utils/createJestConfig.js @@ -38,10 +38,12 @@ module.exports = (resolve, rootDir) => { ), }, transformIgnorePatterns: [ - '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$' + '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|ts|tsx)$', + '^.+\\.module\\.css$', ], moduleNameMapper: { '^react-native$': 'react-native-web', + '^.+\\.module\\.css$': 'identity-obj-proxy', }, moduleFileExtensions: [ 'mjs', diff --git a/packages/react-scripts/template/README.md b/packages/react-scripts/template/README.md index 6f0338d7e..6a8886366 100644 --- a/packages/react-scripts/template/README.md +++ b/packages/react-scripts/template/README.md @@ -23,6 +23,7 @@ You can find the most recent version of this guide [here](https://github.com/fac - [Importing a Component](#importing-a-component) - [Code Splitting](#code-splitting) - [Adding a Stylesheet](#adding-a-stylesheet) +- [Adding a CSS Modules stylesheet](#adding-a-css-modules-stylesheet) - [Post-Processing CSS](#post-processing-css) - [Adding a CSS Preprocessor (Sass, Less etc.)](#adding-a-css-preprocessor-sass-less-etc) - [Adding Images, Fonts, and Files](#adding-images-fonts-and-files) @@ -493,6 +494,51 @@ In development, expressing dependencies this way allows your styles to be reload If you are concerned about using Webpack-specific semantics, you can put all your CSS right into `src/index.css`. It would still be imported from `src/index.js`, but you could always remove that import if you later migrate to a different build tool. +## Adding a CSS Modules stylesheet + +This project supports [CSS Modules](https://github.com/css-modules/css-modules) alongside regular stylesheets using the **[name].module.css** file naming convention. CSS Modules allows the scoping of CSS by automatically creating a unique classname of the format **[dir]\_\_[filename]___[classname]**. + +An advantage of this is the ability to repeat the same classname within many CSS files without worrying about a clash. + +### `Button.module.css` + +```css +.button { + padding: 20px; +} +``` + +### `another-stylesheet.css` + +```css +.button { + color: green; +} +``` + +### `Button.js` + +```js +import React, { Component } from 'react'; +import './another-stylesheet.css'; // Import regular stylesheet +import styles from './Button.module.css'; // Import css modules stylesheet as styles + +class Button extends Component { + render() { + // You can use them as regular CSS styles + return
; + } +} +``` +### `exported HTML` +No clashes from other `.button` classnames + +```html +
+``` + +**This is an optional feature.** Regular html stylesheets and js imported stylesheets are fully supported. CSS Modules are only added when explictly named as a css module stylesheet using the extension `.module.css`. + ## Post-Processing CSS This project setup minifies your CSS and adds vendor prefixes to it automatically through [Autoprefixer](https://github.com/postcss/autoprefixer) so you don’t need to worry about it.