Skip to content

Commit bf3d73c

Browse files
Fabianopbiansu
authored andcommitted
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.
1 parent 836bb39 commit bf3d73c

18 files changed

+336
-87
lines changed

packages/react-scripts/config/webpack.config.dev.js

+57-29
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,31 @@ const postCSSLoaderOptions = {
4646
],
4747
};
4848

49+
// style files regexes
50+
const cssRegex = /\.css$/;
51+
const cssModuleRegex = /\.module\.css$/;
52+
const sassRegex = /\.(scss|sass)$/;
53+
const sassModuleRegex = /\.module\.(scss|sass)$/;
54+
55+
// common function to get style loaders
56+
const getStyleLoaders = (cssOptions, preProcessor) => {
57+
const loaders = [
58+
require.resolve('style-loader'),
59+
{
60+
loader: require.resolve('css-loader'),
61+
options: cssOptions,
62+
},
63+
{
64+
loader: require.resolve('postcss-loader'),
65+
options: postCSSLoaderOptions,
66+
},
67+
];
68+
if (preProcessor) {
69+
loaders.push(require.resolve(preProcessor));
70+
}
71+
return loaders;
72+
};
73+
4974
// This is the development configuration.
5075
// It is focused on developer experience and fast rebuilds.
5176
// The production configuration is different and lives in a separate file.
@@ -243,41 +268,44 @@ module.exports = {
243268
// in development "style" loader enables hot editing of CSS.
244269
// By default we support CSS Modules with the extension .module.css
245270
{
246-
test: /\.css$/,
247-
exclude: /\.module\.css$/,
248-
use: [
249-
require.resolve('style-loader'),
250-
{
251-
loader: require.resolve('css-loader'),
252-
options: {
253-
importLoaders: 1,
254-
},
255-
},
256-
{
257-
loader: require.resolve('postcss-loader'),
258-
options: postCSSLoaderOptions,
259-
},
260-
],
271+
test: cssRegex,
272+
exclude: cssModuleRegex,
273+
use: getStyleLoaders({
274+
importLoaders: 1,
275+
}),
261276
},
262277
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
263278
// using the extension .module.css
264279
{
265-
test: /\.module\.css$/,
266-
use: [
267-
require.resolve('style-loader'),
268-
{
269-
loader: require.resolve('css-loader'),
270-
options: {
271-
importLoaders: 1,
272-
modules: true,
273-
getLocalIdent: getCSSModuleLocalIdent,
274-
},
275-
},
280+
test: cssModuleRegex,
281+
use: getStyleLoaders({
282+
importLoaders: 1,
283+
modules: true,
284+
getLocalIdent: getCSSModuleLocalIdent,
285+
}),
286+
},
287+
// Opt-in support for SASS (using .scss or .sass extensions).
288+
// Chains the sass-loader with the css-loader and the style-loader
289+
// to immediately apply all styles to the DOM.
290+
// By default we support SASS Modules with the
291+
// extensions .module.scss or .module.sass
292+
{
293+
test: sassRegex,
294+
exclude: sassModuleRegex,
295+
use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader'),
296+
},
297+
// Adds support for CSS Modules, but using SASS
298+
// using the extension .module.scss or .module.sass
299+
{
300+
test: sassModuleRegex,
301+
use: getStyleLoaders(
276302
{
277-
loader: require.resolve('postcss-loader'),
278-
options: postCSSLoaderOptions,
303+
importLoaders: 2,
304+
modules: true,
305+
getLocalIdent: getCSSModuleLocalIdent,
279306
},
280-
],
307+
'sass-loader'
308+
),
281309
},
282310
// The GraphQL loader preprocesses GraphQL queries in .graphql files.
283311
{

packages/react-scripts/config/webpack.config.prod.js

+91-58
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,49 @@ const postCSSLoaderOptions = {
6969
flexbox: 'no-2009',
7070
}),
7171
],
72+
sourceMap: shouldUseSourceMap,
73+
};
74+
75+
// style files regexes
76+
const cssRegex = /\.css$/;
77+
const cssModuleRegex = /\.module\.css$/;
78+
const sassRegex = /\.(scss|sass)$/;
79+
const sassModuleRegex = /\.module\.(scss|sass)$/;
80+
81+
// common function to get style loaders
82+
const getStyleLoaders = (cssOptions, preProcessor) => {
83+
const loaders = [
84+
{
85+
loader: require.resolve('css-loader'),
86+
options: cssOptions,
87+
},
88+
{
89+
loader: require.resolve('postcss-loader'),
90+
options: postCSSLoaderOptions,
91+
},
92+
];
93+
if (preProcessor) {
94+
loaders.push({
95+
loader: require.resolve(preProcessor),
96+
options: {
97+
sourceMap: shouldUseSourceMap,
98+
},
99+
});
100+
}
101+
return ExtractTextPlugin.extract(
102+
Object.assign(
103+
{
104+
fallback: {
105+
loader: require.resolve('style-loader'),
106+
options: {
107+
hmr: false,
108+
},
109+
},
110+
use: loaders,
111+
},
112+
extractTextPluginOptions
113+
)
114+
);
72115
};
73116

74117
// This is the production configuration.
@@ -255,69 +298,59 @@ module.exports = {
255298
// in the main CSS file.
256299
// By default we support CSS Modules with the extension .module.css
257300
{
258-
test: /\.css$/,
259-
exclude: /\.module\.css$/,
260-
loader: ExtractTextPlugin.extract(
261-
Object.assign(
262-
{
263-
fallback: {
264-
loader: require.resolve('style-loader'),
265-
options: {
266-
hmr: false,
267-
},
268-
},
269-
use: [
270-
{
271-
loader: require.resolve('css-loader'),
272-
options: {
273-
importLoaders: 1,
274-
minimize: true,
275-
sourceMap: shouldUseSourceMap,
276-
},
277-
},
278-
{
279-
loader: require.resolve('postcss-loader'),
280-
options: postCSSLoaderOptions,
281-
},
282-
],
283-
},
284-
extractTextPluginOptions
285-
)
286-
),
301+
test: cssRegex,
302+
exclude: cssModuleRegex,
303+
loader: getStyleLoaders({
304+
importLoaders: 1,
305+
minimize: true,
306+
sourceMap: shouldUseSourceMap,
307+
}),
287308
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
288309
},
289310
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
290311
// using the extension .module.css
291312
{
292-
test: /\.module\.css$/,
293-
loader: ExtractTextPlugin.extract(
294-
Object.assign(
295-
{
296-
fallback: {
297-
loader: require.resolve('style-loader'),
298-
options: {
299-
hmr: false,
300-
},
301-
},
302-
use: [
303-
{
304-
loader: require.resolve('css-loader'),
305-
options: {
306-
importLoaders: 1,
307-
minimize: true,
308-
sourceMap: shouldUseSourceMap,
309-
modules: true,
310-
getLocalIdent: getCSSModuleLocalIdent,
311-
},
312-
},
313-
{
314-
loader: require.resolve('postcss-loader'),
315-
options: postCSSLoaderOptions,
316-
},
317-
],
318-
},
319-
extractTextPluginOptions
320-
)
313+
test: cssRegex,
314+
loader: getStyleLoaders({
315+
importLoaders: 1,
316+
minimize: true,
317+
sourceMap: shouldUseSourceMap,
318+
modules: true,
319+
getLocalIdent: getCSSModuleLocalIdent,
320+
}),
321+
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
322+
},
323+
// Opt-in support for SASS. The logic here is somewhat similar
324+
// as in the CSS routine, except that "sass-loader" runs first
325+
// to compile SASS files into CSS.
326+
// By default we support SASS Modules with the
327+
// extensions .module.scss or .module.sass
328+
{
329+
test: sassRegex,
330+
exclude: sassModuleRegex,
331+
loader: getStyleLoaders(
332+
{
333+
importLoaders: 2,
334+
minimize: true,
335+
sourceMap: shouldUseSourceMap,
336+
},
337+
'sass-loader'
338+
),
339+
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
340+
},
341+
// Adds support for CSS Modules, but using SASS
342+
// using the extension .module.scss or .module.sass
343+
{
344+
test: sassModuleRegex,
345+
loader: getStyleLoaders(
346+
{
347+
importLoaders: 2,
348+
minimize: true,
349+
sourceMap: shouldUseSourceMap,
350+
modules: true,
351+
getLocalIdent: getCSSModuleLocalIdent,
352+
},
353+
'sass-loader'
321354
),
322355
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
323356
},

packages/react-scripts/fixtures/kitchensink/.template.dependencies.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"chai": "3.5.0",
77
"jsdom": "9.8.3",
88
"mocha": "3.2.0",
9+
"node-sass": "4.8.3",
910
"normalize.css": "7.0.0",
1011
"prop-types": "15.5.6",
1112
"test-integrity": "1.0.0"

packages/react-scripts/fixtures/kitchensink/integration/webpack.test.js

+36
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,42 @@ describe('Integration', () => {
3434
);
3535
});
3636

37+
it('scss inclusion', async () => {
38+
const doc = await initDOM('scss-inclusion');
39+
40+
expect(
41+
doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '')
42+
).to.match(/#feature-scss-inclusion\{background:.+;color:.+}/);
43+
});
44+
45+
it('scss modules inclusion', async () => {
46+
const doc = await initDOM('scss-modules-inclusion');
47+
48+
expect(
49+
doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '')
50+
).to.match(
51+
/.+scss-styles_scssModulesInclusion.+\{background:.+;color:.+}/
52+
);
53+
});
54+
55+
it('sass inclusion', async () => {
56+
const doc = await initDOM('sass-inclusion');
57+
58+
expect(
59+
doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '')
60+
).to.match(/#feature-sass-inclusion\{background:.+;color:.+}/);
61+
});
62+
63+
it('sass modules inclusion', async () => {
64+
const doc = await initDOM('sass-modules-inclusion');
65+
66+
expect(
67+
doc.getElementsByTagName('style')[0].textContent.replace(/\s/g, '')
68+
).to.match(
69+
/.+sass-styles_sassModulesInclusion.+\{background:.+;color:.+}/
70+
);
71+
});
72+
3773
it('graphql files inclusion', async () => {
3874
const doc = await initDOM('graphql-inclusion');
3975
const children = doc.getElementById('graphql-inclusion').children;

packages/react-scripts/fixtures/kitchensink/src/App.js

+20
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,26 @@ class App extends Component {
8686
this.setFeature(f.default)
8787
);
8888
break;
89+
case 'scss-inclusion':
90+
import('./features/webpack/ScssInclusion').then(f =>
91+
this.setFeature(f.default)
92+
);
93+
break;
94+
case 'scss-modules-inclusion':
95+
import('./features/webpack/ScssModulesInclusion').then(f =>
96+
this.setFeature(f.default)
97+
);
98+
break;
99+
case 'sass-inclusion':
100+
import('./features/webpack/SassInclusion').then(f =>
101+
this.setFeature(f.default)
102+
);
103+
break;
104+
case 'sass-modules-inclusion':
105+
import('./features/webpack/SassModulesInclusion').then(f =>
106+
this.setFeature(f.default)
107+
);
108+
break;
89109
case 'custom-interpolation':
90110
import('./features/syntax/CustomInterpolation').then(f =>
91111
this.setFeature(f.default)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import React from 'react';
9+
import './assets/sass-styles.sass';
10+
11+
export default () => <p id="feature-sass-inclusion">We love useless text.</p>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import React from 'react';
9+
import ReactDOM from 'react-dom';
10+
import SassInclusion from './SassInclusion';
11+
12+
describe('sass inclusion', () => {
13+
it('renders without crashing', () => {
14+
const div = document.createElement('div');
15+
ReactDOM.render(<SassInclusion />, div);
16+
});
17+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import React from 'react';
9+
import styles from './assets/sass-styles.module.sass';
10+
11+
export default () => (
12+
<p className={styles.sassModulesInclusion}>SASS Modules are working!</p>
13+
);

0 commit comments

Comments
 (0)