Skip to content

Commit

Permalink
Add Sass loader (#4195)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
Fabianopb authored and iansu committed Apr 18, 2018
1 parent 836bb39 commit bf3d73c
Show file tree
Hide file tree
Showing 18 changed files with 336 additions and 87 deletions.
86 changes: 57 additions & 29 deletions packages/react-scripts/config/webpack.config.dev.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
{
Expand Down
149 changes: 91 additions & 58 deletions packages/react-scripts/config/webpack.config.prod.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
},
Expand Down
Expand Up @@ -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"
Expand Down
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions packages/react-scripts/fixtures/kitchensink/src/App.js
Expand Up @@ -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)
Expand Down
@@ -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 () => <p id="feature-sass-inclusion">We love useless text.</p>;
@@ -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(<SassInclusion />, div);
});
});
@@ -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 () => (
<p className={styles.sassModulesInclusion}>SASS Modules are working!</p>
);

1 comment on commit bf3d73c

@yujingz
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 sass, finally!!

Please sign in to comment.