Skip to content

Production LESS CSS Modules emit literal [local] in class names #7587

@alexandreIFB

Description

@alexandreIFB

In production builds (Volto with css-loader + LESS), classes generated for .module.less files appear with the literal placeholder [local] instead of the actual local class name. Example observed:

Carousel-module__[local]___KTvpt

Root cause: there is a custom getLocalIdent in volto/webpack-plugins/webpack-less-plugin.js. When you define getLocalIdent, css-loader stops post‑processing the template (localIdentName). The custom function calls interpolateName, which does not know the [local] token, so it remains unchanged. In development (dev=true) this getLocalIdent is not applied, hiding the bug.

To Reproduce

  1. In a Volto project using the original webpack-less-plugin.js, create Example.module.less:
    .container {
      color: red;
    }
  2. Import it in a React component:
    import styles from './Example.module.less';
    <div className={styles.container}>Test</div>
  3. Run a production build:
    pnpm --filter @plone/volto build
    
  4. Inspect the final HTML or CSS (build/public/static/css/*.css) and locate the generated class:
    Example-module__[local]___a1B2C
    
  5. Start in dev mode (pnpm --filter @plone/volto start) and confirm the class name is correct (no literal [local]).

Expected behavior
The placeholder [local] should be replaced by the original local class name:

Example-module__container___a1B2C

Preserving the pattern defined in localIdentName: '[name][local]_[hash:base64:5]'.

Additional context
Original config snippet:

modules: {
  auto: true,
  localIdentName: '[name]__[local]___[hash:base64:5]',
  getLocalIdent: getLocalIdent,
}

Custom function:

function getLocalIdent(loaderContext, localIdentName, localName, options) {
  const relativeResourcePath = /* ... */;
  options.content = `${options.hashPrefix}${relativeResourcePath}\x00${localName}`;
  return interpolateName(loaderContext, localIdentName, options);
}

interpolateName replaces [hash], [name], [path], etc., but not [local]. Without css-loader’s internal post-processing, [local] stays literal.

Why it went unnoticed: dev environment doesn’t apply getLocalIdent; more .module.less components were added recently; production class names weren’t inspected before.

Impact:

  • Unexpected class names with literal tokens.
  • Harder debugging and substring-based global overrides.
  • Potential inconsistency where selectors assumed expansion of [local].

Fix options:

  1. Patch getLocalIdent to manually replace [local]:
    function getLocalIdent(loaderContext, localIdentName, localName, options) {
      const relativeResourcePath = normalizePath(
        path.relative(options.context, loaderContext.resourcePath),
      );
      options.content = `${options.hashPrefix}${relativeResourcePath}\x00${localName}`;
      const finalLocalIdentName = localIdentName.includes('[local]')
        ? localIdentName.replace(/\[local]/g, localName)
        : localIdentName;
      return interpolateName(loaderContext, finalLocalIdentName, options);
    }

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