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 · 6 comments
Closed

Comments

@jasonmorita
Copy link

@jasonmorita 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

This comment has been minimized.

Copy link

@nagman nagman commented Feb 21, 2019

Same problem here

@jasonmorita

This comment has been minimized.

Copy link
Author

@jasonmorita 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

This comment has been minimized.

Copy link

@kostantine kostantine commented Aug 20, 2019

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

@goldmont

This comment has been minimized.

Copy link

@goldmont 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

This comment has been minimized.

Copy link
Member

@Ffloriel Ffloriel commented Dec 8, 2019

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

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

Successfully merging a pull request may close this issue.

None yet
5 participants
You can’t perform that action at this time.