Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
jasonmorita opened this issue Jan 7, 2019 · 13 comments
Closed

Comments

@jasonmorita
Copy link

jasonmorita commented Jan 7, 2019

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"));
@nagman
Copy link

nagman commented Feb 21, 2019

Same problem here

@jasonmorita
Copy link
Author

jasonmorita commented Feb 21, 2019

@nagman @lucleray take a look at this https://www.npmjs.com/package/@americanexpress/purgecss-loader

https://github.com/americanexpress/purgecss-loader/blob/master/loader.js

@jasonmorita
Copy link
Author

jasonmorita commented 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>;
}

@kostantine
Copy link

kostantine commented Aug 20, 2019

I renamed the {Name}.scss file to {Name}.module.scss.Example: Header.scss renamed to Header.module.scss

@goldmont
Copy link

goldmont commented 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>
		);
	}

}

@Ffloriel
Copy link
Member

Ffloriel commented Dec 8, 2019

Wow, thanks a lot @goldmont for the detailed solution.

@aviishek
Copy link

aviishek commented Mar 20, 2020

@goldmont How I use your solution in craco.config.js instead of ejecting

@Jancat
Copy link

Jancat commented May 20, 2020

@goldmont Is there a way that using config-override.js, instead of eject.

@birkankervan
Copy link

birkankervan commented Jun 23, 2020

@goldmont what about next.js and css module? Is this a good way?

@wood3n
Copy link

wood3n commented Sep 8, 2020

Thank you very much! @goldmont , I just test your code in my own React App, this is the demo —— webpack.config.js.
Maybe it can be little more easy to use. It's my basic environment, glob, postcss-loaderand @fullhuman/postcss-purgecss must be installed

  • "webpack": "^4.44.1",
  • "postcss-loader": "^4.0.0",
  • "glob": "^7.1.6",
  • "@fullhuman/postcss-purgecss": "^2.3.0",

And that's my configuration in webpack.config.js:

module.exports = {
  module: {
    rules:[
      {
        test: /\.css$/i,
        use: [
          "style-loader",
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  "postcss-flexbugs-fixes",
                  "autoprefixer",
                  "postcss-preset-env",
                  [
                    "@fullhuman/postcss-purgecss",          // use @fullhuman/postcss-purgecss
                    {
                      content: [
                        path.join(__dirname, "./public/index.html"),         //to match index.html
                        ...glob.sync(
                          `${path.join(__dirname, "src")}/**/*.jsx`,             //to match React JSX files in src/components/*.jsx
                          {
                            nodir: true,
                          }
                        ),
                      ],
                    },
                  ],
                ],
              },
            },
          },
        ],
      }
    ]
  }
}

@gaweki
Copy link

gaweki commented Sep 29, 2020

sorry for ask, i have error

./src/index.css TypeError: Class constructor extractor cannot be invoked without 'new'

i already reinstall node modules and delete package-lock.json. but still show the error. anyone had this problem?

=======================

ii use @goldmont's config but without the class
require('@fullhuman/postcss-purgecss')({ content: [paths.appHtml, ...glob.sync(${paths.appSrc}/**/*.{jsx,js}, { nodir: true })], }),
nb: add css,sass for imported modules sass and css files

===========================

After irecheck css file, file size minimized. But unused bootstrap class(ex: navbar) is still in the css file. How to solve this?

@vstoyanoff
Copy link

vstoyanoff commented Jan 13, 2021

Hey. Thanks for the wonderful solution to this problem. I am having (maybe) a related issue. When I use composes property the class that is used is purged and I get an error in the build process referenced class name "<classname>" in composes not found. Maybe I need to tweak the configuration a little more?

@zdrukteinis
Copy link

zdrukteinis commented May 31, 2022

I came up with a quick solution - a bit hacky, bet overall super simple to implement!

Add this rule:

{
    test: /\.module\.(scss|sass)$/,
    use: [{
            loader: MiniCssExtractPlugin.loader,
        },
        {
            loader: "css-loader",
            options: {
                modules: {
                    localIdentName: 'css__module__[name]__[local]',
                },
                importLoaders: 1,
            }
        },
        {
            loader: "postcss-loader"
        },
        {
            loader: "sass-loader"
        }
    ],
}

Then in plugins add this purgecss-webpack-plugin initialization:

Importing purgecss-webpack-plugin:

const PurgecssPlugin = require('purgecss-webpack-plugin')

Adding to webpack plugins array:

new PurgecssPlugin({
    paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, {
        nodir: true
    }),
    defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
    safelist: {
        deep: [/css__module__/],
    }
})

The trick:

We set localIdentName as module__[name]__[local], which makes class names like this:

css__module__TimeSlotPicker-module__container

Where TimeSlotPicker is TimeSlotPicke.module.scss and container is the class.

After that we add safelist, where in deep we tell Purge CSS Plugin to ignore anything that has css__module__:

safelist: {
        deep: [/css__module__/],
    }

NOTES:
A better regex could be used for deep prop value (/css__module__/).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests