Skip to content

CSS Modules – random vs deterministic class names in production #3972

@michaelhogg

Description

@michaelhogg

Firstly, many thanks to @ro-savage for implementing CSS Modules in #2285 (merged and ready for release in [email protected] – see #3815) 🎉

I'm opening this issue to continue the discussion regarding random-vs-deterministic localIdentName class names in production builds.

⚠️ There were some important comments from @heavi5ide and @simonrelet which apparently were forgotten about, possibly due to a force-push which made it impossible to reach the original commit containing the comments (a15df83).


For ease of reading, I'm copying the relevant comments from here:

Click 👈

[@ro-savage] When using css modules, class names follow a deterministic convention rather than the standard random hash with the covention [directory]__[filename]___[classname].

Given src/components/Button/Button.module.css with a class .primary {}, the generated classname will be src-components-Button__Button-module___primary.

This is done to allow targeting off elements via classname, causes minimal overhead with gzip-enabled, and allows a developer to find component location in the devtools.


and here:

Click 👈

[@ro-savage] I've updated the pull request to use deterministic classnames rather than random hash, as suggested by another user (sorry can't find the comment!).

Instead of someFileName__someClassName___hash it becomes directory__someFileName___someClassName.

This allows predictability as to what the class name will be, so that it can be targeted.

This can can not clash because the file would have to be in the same dir + same file name + same class name. Which if it is, it is the same class anyway. It also shouldn't contribute very minimally to filesize because gzip should handle the repetition of long classnames.

This has been how react-scripts-cssmodules has been naming things for a couple months.


and here:

Click 👈

[@klzns] Should the production build expose the app architecture?

[@amannn] In regards to @klzns comment, I'd also appreciate not exposing the file path

[@andriijas] Why does it need to be deterministic and targetable? kind of kills the whole module thing. Why not recommend people to add additional static class if it needs to be targetable? Im upp for hashes.

[@sompylasar] Web marketing people often freak out if they cannot attach the out-of-the-box tools like HeapAnalytics or MouseFlow to specific elements of a website/webapp by id/classname (or if the classnames they attach to are changing with each release).


and here (you'll need to click the "Show outdated" link to see the comments):

Click 👈

[@recidive] One thing that's very useful, specially when you are using element based event tracking in your site/app, is naming classes based on component path instead of a hash so classnames don't change on every build. This can be accomplished with localIdentName: '[path][name]__[local]'. Making classnames less volatile is useful for automations that rely on element classnames.

[@ro-savage] Is there any security implications of showing the repos paths?

[@ro-savage] After rewriting this to hash the path, I decided to see if I could to a PR to css-loader to add it as an option.

Once I looked at the source, I can see they are already generating the hash based on the path.
options.content = options.hashPrefix + request + "+" + localName;
which translates to relative/path/to/file+classname

I had never tested it before, but if you build it multiple times the classname should not change. Unless you change the file location or the classname itself.

@recidive & @heavi5ide are you getting different classnames with each build?

[@heavi5ide] Yes I just tested this out and it seems you are correct and this shouldn't really be a problem at all. @recidive are you sure this is still an issue? It looks like the hash generation code that @ro-savage linked to in css-loader was changed "to something more reproducable" almost two years ago: webpack-contrib/css-loader@55ad466

[@simonrelet] I'm probably missing an important thing here and I understand the need for the naming during the development, but why isn't it just [hash:base64:*] in production?


I think there are two key points which merit further discussion:


1️⃣ The concerns of @klzns and @amannn regarding exposing the source code's file paths in production builds.

I share their concerns – I would feel uncomfortable seeing class names such as src-components-Button__Button-module___primary in production.  I understand the benefits of deterministic/predictable class names which don't change with each build (so that elements can be targeted by class name), but this can be achieved without exposing the source code's file paths.


2️⃣ The fact that css-loader's [hash] token for localIdentName is actually deterministic (not random), as @heavi5ide and @simonrelet noticed.

Click for details of getLocalIdent() and interpolateName() 👈

CreateReactApp / ReactScripts currently uses css-loader v0.28.7.  Looking at the source code, the localIdentName template is passed to getLocalIdent(), which effectively generates the class name in this way:

//                Filepath of the CSS file, relative to the project root dir         Source CSS class name
options.content = path.relative(options.context, loaderContext.resourcePath) + "+" + localName;

loaderUtils.interpolateName(loaderContext, localIdentName, options);

interpolateName() is provided by Webpack's loader-utils library, and the description of the [hash] token is:

the hash of options.content (Buffer) (by default it's the hex digest of the md5 hash)

[<hashType>:hash:<digestType>:<length>] optionally one can configure

  • other hashTypes, i. e. sha1, md5, sha256, sha512
  • other digestTypes, i. e. hex, base26, base32, base36, base49, base52, base58, base62, base64
  • and length the length in chars

The default template for localIdentName is "[hash:base64]", which generates the MD5 hash (as raw binary data) and then base64-encodes it.


So [hash] is not random.  It's the digest of: <Source CSS relative filepath> "+" <Source CSS class name>


So my recommendation is to change localIdentName in packages/react-scripts/config/webpack.config.prod.js from:

localIdentName: '[path]__[name]___[local]'

to something like:

localIdentName: '[hash:base64]'

The hash will be deterministic, predictable, unique and targetable, without exposing the source code's file paths.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions