16

How can I edit localIdentName field of css-loader in Webpack configuration in Nextjs so that I can hash/hide/obfuscate css class names?

The example below is from the New York Times. Note the class names:

enter image description here

1

4 Answers 4

30

None of the answers on this thread will work on newer Next.js versions (v11.1.3-canary.2 and above), because in PR#28529, the Next.js team has switched to a customized version of css-loader that has no defaultGetLocalIdent, nor does it checks if getLocalIndent is null or undefined.

Any solution that ultimately removes getLocalIdent from the loader configuration will lead to TypeError: getLocalIdent is not a function. In other words, it is now mandatory to provide a function as getLocalIdent. Here is an example:

const path = require('path');
const loaderUtils = require('loader-utils');

// based on https://github.com/vercel/next.js/blob/992c46e63bef20d7ab7e40131667ed3debaf67de/packages/next/build/webpack/config/blocks/css/loaders/getCssModuleLocalIdent.ts
const hashOnlyIdent = (context, _, exportName) =>
  loaderUtils
    .getHashDigest(
      Buffer.from(
        `filePath:${path
          .relative(context.rootContext, context.resourcePath)
          .replace(/\\+/g, '/')}#className:${exportName}`,
      ),
      'md4',
      'base64',
      6,
    )
    .replace(/[^a-zA-Z0-9-_]/g, '_')
    .replace(/^(-?\d|--)/, '_$1');

module.exports = {
  webpack(config, { dev }) {
    const rules = config.module.rules
      .find((rule) => typeof rule.oneOf === 'object')
      .oneOf.filter((rule) => Array.isArray(rule.use));

    if (!dev)
      rules.forEach((rule) => {
        rule.use.forEach((moduleLoader) => {
          if (
            moduleLoader.loader?.includes('css-loader') &&
            !moduleLoader.loader?.includes('postcss-loader')
          )
            moduleLoader.options.modules.getLocalIdent = hashOnlyIdent;

            // earlier below statements were sufficient:
            // delete moduleLoader.options.modules.getLocalIdent;
            // moduleLoader.options.modules.localIdentName = '[hash:base64:6]';
        });
      });

    return config;
  },
};

This works on Next v12 and v13 (without TurboPack) too.

Demo:

While development -

On production -

Demo with Experimental App Directory:

Sign up to request clarification or add additional context in comments.

21 Comments

I love you. What a superb, up-to-date, concise and informative answer.
@agoumi Here's a great example that helped me out setting up twin.macro for obfuscating tailwind CSS (with next js) in production - github.com/ben-rogerson/twin.examples/tree/master/… Source: reddit.com/r/nextjs/comments/r9dp2c/comment/hq8zdxu
This is not working for me with next 12.1.6
This worked for me on nextjs 12.2.0 but I needed to replace + and / with _ from the final hash. Here's my full file if anyone want's to try: gist.github.com/herlon214/7da6a39b885014a5d5f51716f6530ce4
|
6

Unfortunately there is no build in support in Nextjs to pass custom configuration to Webpack loaders. But we can work around it by using next.config.js.

First, create next.config.js in the root of your project directory.

For Nextjs 11

module.exports = {
  webpack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
    config.module.rules[3].oneOf.forEach((moduleLoader, i) => {
      Array.isArray(moduleLoader.use) &&
        moduleLoader.use.forEach((l) => {
          if (
            l.loader.includes("\\css-loader") &&
            !l.loader.includes("postcss-loader")
          ) {
            const { getLocalIdent, ...others } = l.options.modules;

            l.options = {
              ...l.options,
              modules: {
                ...others,
                localIdentName: "[hash:base64:6]",
              },
            };
          }
        });
    });
    return config;
  },
};

For Next.js 10.2 or a newer version:

module.exports = {
  webpack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
    config.module.rules[3].oneOf.forEach((moduleLoader, i) => {
      Array.isArray(moduleLoader.use) &&
        moduleLoader.use.forEach((l) => {
          if (
            l.loader.includes("\\css-loader") &&
            !l.loader.includes("postcss-loader")
          ) {
            const { getLocalIdent, ...others } = l.options.modules;

            l.options = {
              ...l.options,
              modules: {
                ...others,
                localIdentName: "[hash:base64:6]",
              },
            };
          }
        });
    });
    return config;
  },
};

Otherwise use this:

module.exports = {
  webpack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
    config.module.rules[1].oneOf.forEach((moduleLoader, i) => {
      Array.isArray(moduleLoader.use) &&
        moduleLoader.use.forEach((l) => {
          if (
            l.loader.includes('\\css-loader') &&
            !l.loader.includes('postcss-loader')
          ) {
            const { getLocalIdent, ...others } = l.options.modules;

            l.options = {
              ...l.options,
              modules: {
                ...others,
                localIdentName: '[hash:base64:6]',
              },
            };
          }
        });
    });
    return config;
  },
};

If you want to hash the class names only in production, you can use process.env.NODE_ENV with an if statement. Like this:

module.exports = {
  webpack(config, { buildId, dev, isServer, defaultLoaders, webpack }) {
    if (process.env.NODE_ENV === "production") {
      ...
      ...

      return config;
    } else {
      return config;
    }
  },
};

5 Comments

does anybody know how this should look for webpack 5?
@jebbie Edited the answer. Check it out.
@user9408899 just tested it and it outputs the css with hashed name, but the html is still the same, so the classnames do not match between html and css, do you have any idea?
any solution for next 12?
This answer is awesome!
4

For those who use Next.js v13 and the accepted answer doesn't work. You have to change a little bit the moduleLoader condition:

if (
    moduleLoader.loader?.includes('css-loader') &&
    !moduleLoader.loader?.includes('postcss-loader') &&
    moduleLoader.options !== undefined &&
    moduleLoader.options.modules !== undefined
) {

1 Comment

If you're running node 14+ you can use optional chaining instead. moduleLoader?.options?.modules
0

This worked for me using Next.js 11:

module.exports = {
        webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
        config.module.rules[3].oneOf.forEach((moduleLoader, i) => {
          Array.isArray(moduleLoader.use) &&
            moduleLoader.use.forEach((l) => {
              if (
                l.loader.includes('\\css-loader') &&
                !l.loader.includes('postcss-loader')
              ) {
                const { getLocalIdent, ...others } = l.options.modules;
    
                l.options = {
                  ...l.options,
                  modules: {
                    ...others,
                    localIdentName: '[hash:base64:6]',
                  },
                };
              }
            });
        });
          return config;
      },
    };

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.