Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to hash CSS class names in Nextjs?

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

like image 626
user9408899 Avatar asked Mar 22 '21 11:03

user9408899


2 Answers

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/0af3b526408bae26d6b3f8cab75c4229998bf7cb/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(/^(-?\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 too.

Demo:

While development -

On production -

like image 121
brc-dd Avatar answered Oct 14 '22 07:10

brc-dd


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;
    }
  },
};
like image 44
user9408899 Avatar answered Oct 14 '22 06:10

user9408899