diff --git a/docs/css.md b/docs/css.md
index 82580028d..a863929eb 100644
--- a/docs/css.md
+++ b/docs/css.md
@@ -4,7 +4,7 @@
Webpacker supports importing CSS, Sass and SCSS files directly into your JavaScript files.
-## Import styles into your JS app
+## Import global styles into your JS app
```sass
// app/javascript/hello_react/styles/hello-react.sass
@@ -30,6 +30,36 @@ const Hello = props => (
)
```
+## Import scoped styles into your JS app
+
+Stylesheets end with '.modules.*' is treated as [CSS Modules](https://github.com/css-modules/css-modules).
+
+```sass
+// app/javascript/hello_react/styles/hello-react.module.sass
+
+.helloReact
+ padding: 20px
+ font-size: 12px
+```
+
+```js
+// React component example
+// app/javascripts/packs/hello_react.jsx
+
+import React from 'react'
+import helloIcon from '../hello_react/images/icon.png'
+import styles from '../hello_react/styles/hello-react'
+
+const Hello = props => (
+
+
+
Hello {props.name}!
+
+)
+```
+
+**Note:** Declared class is referenced as object property in JavaScript.
+
## Link styles from your Rails views
diff --git a/lib/install/config/webpacker.yml b/lib/install/config/webpacker.yml
index d3f24e1b4..6da493bcb 100644
--- a/lib/install/config/webpacker.yml
+++ b/lib/install/config/webpacker.yml
@@ -18,6 +18,9 @@ default: &default
- .sass
- .scss
- .css
+ - .module.sass
+ - .module.scss
+ - .module.css
- .png
- .svg
- .gif
diff --git a/package/__tests__/config.js b/package/__tests__/config.js
index 712ffc178..e5b6b94cc 100644
--- a/package/__tests__/config.js
+++ b/package/__tests__/config.js
@@ -15,6 +15,9 @@ describe('Webpacker.yml config', () => {
'.sass',
'.scss',
'.css',
+ '.module.sass',
+ '.module.scss',
+ '.module.css',
'.png',
'.svg',
'.gif',
diff --git a/package/rules/css.js b/package/rules/css.js
index 521f21d51..8354040b8 100644
--- a/package/rules/css.js
+++ b/package/rules/css.js
@@ -1,39 +1,7 @@
-const ExtractTextPlugin = require('extract-text-webpack-plugin')
-const path = require('path')
-const { dev_server: devServer } = require('../config')
+const styleRuleFactory = require('./style_rule_factory')
-const postcssConfigPath = path.resolve(process.cwd(), '.postcssrc.yml')
-const isProduction = process.env.NODE_ENV === 'production'
-const inDevServer = process.argv.find(v => v.includes('webpack-dev-server'))
-const isHMR = inDevServer && (devServer && devServer.hmr)
-const extractCSS = !(isHMR) || isProduction
-
-const styleLoader = {
- loader: 'style-loader',
- options: {
- hmr: isHMR,
- sourceMap: true
- }
-}
-
-const extractOptions = {
- fallback: styleLoader,
- use: [
- { loader: 'css-loader', options: { minimize: isProduction, sourceMap: true, importLoaders: 2 } },
- { loader: 'postcss-loader', options: { sourceMap: true, config: { path: postcssConfigPath } } }
- ]
-}
-
-// For production extract styles to a separate bundle
-const extractCSSLoader = {
- test: /\.(css)$/i,
- use: ExtractTextPlugin.extract(extractOptions)
-}
-
-// For hot-reloading use regular loaders
-const inlineCSSLoader = {
- test: /\.(css)$/i,
- use: [styleLoader].concat(extractOptions.use)
-}
-
-module.exports = extractCSS ? extractCSSLoader : inlineCSSLoader
+module.exports = styleRuleFactory(
+ /\.(css)$/i,
+ false,
+ []
+)
diff --git a/package/rules/index.js b/package/rules/index.js
index 7784dd9cc..1a4ea3c2a 100644
--- a/package/rules/index.js
+++ b/package/rules/index.js
@@ -2,10 +2,14 @@ const babel = require('./babel')
const file = require('./file')
const css = require('./css')
const sass = require('./sass')
+const moduleCss = require('./module.css')
+const moduleSass = require('./module.sass')
module.exports = {
babel,
css,
sass,
+ moduleCss,
+ moduleSass,
file
}
diff --git a/package/rules/module.css.js b/package/rules/module.css.js
new file mode 100644
index 000000000..6671d8548
--- /dev/null
+++ b/package/rules/module.css.js
@@ -0,0 +1,7 @@
+const styleRuleFactory = require('./style_rule_factory')
+
+module.exports = styleRuleFactory(
+ /\.(css)$/i,
+ true,
+ []
+)
diff --git a/package/rules/module.sass.js b/package/rules/module.sass.js
new file mode 100644
index 000000000..9a8f01a4f
--- /dev/null
+++ b/package/rules/module.sass.js
@@ -0,0 +1,10 @@
+const styleRuleFactory = require('./style_rule_factory')
+
+module.exports = styleRuleFactory(
+ /\.(scss|sass)$/i,
+ true,
+ [{
+ loader: 'sass-loader',
+ options: { sourceMap: true }
+ }]
+)
diff --git a/package/rules/sass.js b/package/rules/sass.js
index 9a8107a2c..41cec0899 100644
--- a/package/rules/sass.js
+++ b/package/rules/sass.js
@@ -1,15 +1,10 @@
-const cssLoader = require('./css')
-const deepMerge = require('../utils/deep_merge')
+const styleRuleFactory = require('./style_rule_factory')
-// Duplicate and remove css loader object reference
-let sassLoader = JSON.parse(JSON.stringify(cssLoader))
-
-sassLoader = deepMerge(sassLoader, {
- test: /\.(scss|sass)$/i,
- use: [{
+module.exports = styleRuleFactory(
+ /\.(scss|sass)$/i,
+ false,
+ [{
loader: 'sass-loader',
options: { sourceMap: true }
}]
-})
-
-module.exports = sassLoader
+)
diff --git a/package/rules/style_rule_factory.js b/package/rules/style_rule_factory.js
new file mode 100644
index 000000000..a6d9bf6a5
--- /dev/null
+++ b/package/rules/style_rule_factory.js
@@ -0,0 +1,60 @@
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
+const path = require('path')
+const { dev_server: devServer } = require('../config')
+
+const postcssConfigPath = path.resolve(process.cwd(), '.postcssrc.yml')
+const isProduction = process.env.NODE_ENV === 'production'
+const inDevServer = process.argv.find(v => v.includes('webpack-dev-server'))
+const isHMR = inDevServer && (devServer && devServer.hmr)
+const extractCSS = !(isHMR) || isProduction
+
+const styleRuleFactory = (test, modules, preprocessors) => {
+ const styleLoader = {
+ loader: 'style-loader',
+ options: {
+ hmr: isHMR,
+ sourceMap: true
+ }
+ }
+
+ const extractOptions = {
+ fallback: styleLoader,
+ use: [
+ {
+ loader: 'css-loader',
+ options: {
+ minimize: isProduction, sourceMap: true, importLoaders: 2, modules
+ }
+ },
+ {
+ loader: 'postcss-loader',
+ options: {
+ sourceMap: true, config: { path: postcssConfigPath }
+ }
+ },
+ ...preprocessors
+ ]
+ }
+
+ const modulesCondition = modules
+ ? { include: /\.module\.[a-z]+$/ }
+ : { exclude: /\.module\.[a-z]+$/ }
+
+ // For production extract styles to a separate bundle
+ const extractCSSLoader = Object.assign(
+ {},
+ { test, use: ExtractTextPlugin.extract(extractOptions) },
+ modulesCondition
+ )
+
+ // For hot-reloading use regular loaders
+ const inlineCSSLoader = Object.assign(
+ {},
+ { test, use: [styleLoader].concat(extractOptions.use) },
+ modulesCondition
+ )
+
+ return extractCSS ? extractCSSLoader : inlineCSSLoader
+}
+
+module.exports = styleRuleFactory
diff --git a/test/test_app/config/webpacker.yml b/test/test_app/config/webpacker.yml
index c2a2a46c6..beaf1831c 100644
--- a/test/test_app/config/webpacker.yml
+++ b/test/test_app/config/webpacker.yml
@@ -20,6 +20,9 @@ default: &default
- .sass
- .scss
- .css
+ - .module.sass
+ - .module.scss
+ - .module.css
- .png
- .svg
- .gif