question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Webpack 4 + React + CSS modules stripping all classes from CSS bundle

See original GitHub issue

When using CSS modules with React and Webpack 4, all classes are removed from the CSS bundle.

In the options for css-loader, if I have modules: true the CSS bundle is totally empty.

If I comment that out, the CSS bundle is as expected with all unused classes removed, however the JS bundle no longer has the CSS classes on the component elements.

If I add the SCSS files to the entry and do not use CSS modules, the CSS bundle is correct as well.

The issue appears to be when combining modules: true and purgecss.

The same is true is I do not use mini-css-extract-plugin and let the CSS go into the bundle.

Here is my Webpack config:

const path = require("path");
const glob = require("glob");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgecssPlugin = require("purgecss-webpack-plugin");

const PATHS = {
  src: path.join(__dirname, "src")
};

module.exports = {
  entry: "./src/App.js",
  output: {
    filename: "bundle.js",
    path: path.join(__dirname, "dist")
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: "styles",
          test: /\.css$/,
          chunks: "all",
          enforce: true
        }
      }
    }
  },
  module: {
    rules: [
      {
        test: /\.js/,
        loader: "babel-loader",
        include: __dirname + "/src",
        query: {
          presets: ["react"]
        }
      },
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              modules: true,
              camelCase: true,
              importLoaders: 1,
              localIdentName: "[name]--[local]--[hash:base64:5]"
            }
          },
          "sass-loader"
        ]
      }
    ]
  },
  plugins: [
    new CopyWebpackPlugin([{ from: `src/index.html`, to: "index.html" }]),
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true })
    }),
    new MiniCssExtractPlugin({
      filename: "[name].css"
    })
  ]
};

Here is the entry:

import React from "react";
import ReactDOM from "react-dom";

import Sub from "./Sub";
import { appContainer } from "./App.scss";

function App() {
  return (
    <div className={appContainer}>
      Hi from app.
      <Sub />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:13 (1 by maintainers)

github_iconTop GitHub Comments

8reactions
goldmontcommented, Aug 30, 2019

webpack: 4.29.6 react: 16.8.6 CSS modules: enabled @fullhuman/postcss-purgecss: 1.2.0

I had this problem too but I managed to solve it. Its like PurgeCSS *Plugin* is invoked in a intermediate phase when React components still have the original CSS classes names set in JSX while CSS modules classes have the new hashed name. Since PurgeCSS parses every JS/JSX/HTML file extracting from them all used CSS classes names, when it compares these last with the new hashed CSS classes names obviously none of them is used because each name is different from the other and thus all your CSS get purged. To make things work, we will use postcss-loader and @fullhuman/postcss-purgecss. You will need the following packages:

  • glob-all
  • mini-css-extract-plugin
  • react-dev-utils
  • style-loader
  • css-loader
  • sass-loader
  • node-sass
  • postcss-loader
  • postcss-scss
  • postcss-flexbugs-fixes
  • postcss-preset-env
  • postcss-normalize
  • @fullhuman/postcss-purgecss

You can also install them as dev dependencies.

Obviously you need to eject by running yarn run eject or npm run eject.

const glob = require('glob-all');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');

const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

module.exports = function(webpackEnv) {

    const isEnvDevelopment = webpackEnv === 'development';
    const isEnvProduction = webpackEnv === 'production';
    const shouldUseRelativeAssetPaths = publicPath === './';

    const getStyleLoaders = (cssOptions, preProcessor) => {
		const loaders = [
			isEnvDevelopment && require.resolve('style-loader'),
			isEnvProduction && {
				loader: MiniCssExtractPlugin.loader,
				options: shouldUseRelativeAssetPaths ? { publicPath: '../../' } : {}
			},
			{
				loader: require.resolve('css-loader'),
				options: cssOptions
			},
			{
				loader: require.resolve('postcss-loader'),
				options: {
					ident: 'postcss',
					syntax: 'postcss-scss',
					plugins: () => [
						require('postcss-flexbugs-fixes'),
						require('postcss-preset-env')({
							autoprefixer: {
								flexbox: 'no-2009'
							},
							stage: 3
						}),
						require('@fullhuman/postcss-purgecss')({
							content: [ paths.appHtml, ...glob.sync(path.join(paths.appSrc, '/**/*.{js,jsx}'), { nodir: true }) ],
							extractors: [
								{
									extractor: class {
										static extract(content) {
											return content.match(/[\w-/:]+(?<!:)/g) || [];
										}
									},
									extensions: [ 'html', 'js', 'jsx' ]
								}
							]
						}),
						require('postcss-normalize')
					].filter(Boolean),
					sourceMap: isEnvProduction && shouldUseSourceMap
				}
			}
		].filter(Boolean);
		if (preProcessor) {
			loaders.push({
				loader: require.resolve(preProcessor),
				options: {
					sourceMap: isEnvProduction && shouldUseSourceMap
				}
			});
		}
		return loaders;
	};

    return {

        /* {...} */

        module: {
            rules: [

              /* {...} */
    
              {
                oneOf: [

                    /* {...} */
    
                    {
                        test: /\.module\.(scss|sass)$/,
                        use: getStyleLoaders(
                            {
                                importLoaders: 2,
                                sourceMap: isEnvProduction && shouldUseSourceMap,
                                modules: true,
                                getLocalIdent: getCSSModuleLocalIdent
                            },
                            'sass-loader'
                        )
                    }
    
                    /* {...} */

                ]
              }
    
              /* {...} */

            ]
        },

        /* {...} */
        
    };

};

You also need to remove PurgecssPlugin from Webpack plugins list.

This code snippet is the piece of my Webpack configuration which is responsible of hashing and purging of CSS. It should work straightforward, I hope I didn’t leave any pieces back.

P.S. Since I use both Tailwind CSS and SASS to style my HTML, in PurgeCSS configuration I had to write an extractor to prevent Tailwind classes to get purged. You can delete it if you don’t need it.

Furthermore you can use regular module syntax in your JSX just like this:

// @flow

import styles from './Test.module.scss';

import * as React from 'react';

type Props = {};

type State = {};

export default class Test extends React.Component<Props, State> {

	render(): * {
		return (
			<div className={styles.myCssClass}></div>
		);
	}

}
5reactions
jasonmoritacommented, Feb 21, 2019

One last thing, I had to change the CSS “syntax” to something like this

import React from "react";

import styles from "./Component.scss";

export default function Component() {
  return <div className={styles["component-container"]}>😴</div>;
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

[PURGECSS]: Webpack 5 + VueJS + CSS modules stripping ...
When using CSS modules with VueJS and Webpack 5, all classes are removed from the CSS bundle. In the options for css-loader, ...
Read more >
style-loader - webpack
Automatically injects styles into the DOM using multiple <style></style> . It is default behaviour. ... import styles from "./styles.css"; const divElement = ...
Read more >
32 - Enable and Use CSS Modules In React - Geekstrick
That is possible, and it is possible in the setup we use here. We can use a feature named CSS modules and I...
Read more >
Eliminating Unused CSS - SurviveJS
It walks through your code and figures out which CSS classes are being used as often there is enough information for it to...
Read more >
Eliminating Unused CSS | webpack surviveJS - GitHub Pages
It walks through your code and figures out which CSS classes are being used. Often there is enough information for it to strip...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found