Output unique class names for duplicated classes across multiple chunks
Closed this issue · 3 comments
Related issues:
Feature Proposal
When CSS Modules is used in combination with code splitting, webpack may duplicate CSS across multiple chunks.
This is problematic because CSS files may be loaded in an arbitrary order resulting in unpredictable behaviour.
I believe we could mitigate this issue by ensuring duplicated classes have unique class names for each chunk they appear within. This could be achieved by adding a new template string to localIdentName that represents the current chunk. This would be similar to [chunkhash] as supported by output.
Feature Use Case
The reduced test case outlined below can be found here: https://github.com/OliverJAsh/webpack-css-order-test/tree/code-splitting
Let's say we have a project with this structure:
src/
├─ Button.js
├─ Button.css
├─ Collections.js
├─ Collections.css
├─ Photos.js
├─ Photos.css
├─ main.js
We have a Button component:
src/Button.js:
import * as styles from './Button.css';
export const Button = () => [styles.button];src/Button.css:
.button {
padding: 5px;
color: blue;
}Both Collections.js and Photos.js import Button.js as a component and compose its styles to override color:
src/Photos.js:
import { Button } from './Button';
import * as styles from './Photos.css';
export const Photos = () => [Button, styles.button];src/Photos.css:
.button {
color: black;
}src/Collections.js:
import { Button } from './Button';
import * as styles from './Collections.css';
export const Collections = () => [Button, styles.button];src/Collections.css:
.button {
color: orange;
}Within main.js, we dynamically import Photos.js and Collections.js. Pretend for this test-case that these imports are only triggered on page navigation:
// Loaded when navigating to a "Photos page"
import(/* webpackChunkName: "Photos" */ './Photos');
// Loaded when navigating to a "Collections page"
import(/* webpackChunkName: "Collections" */ './Collections');Because Button.js is only used within 2 dynamic imports, and not directly in the main.js, the contents of Button.css get duplicated and prepended to Photos.css and Collections.css.
The resulting CSS files:
dist/Photos.css:
.src-Button__button {
padding: 5px;
color: blue;
}
.src-Photos__button {
color: black;
}dist/Collections.css:
.src-Button__button {
padding: 5px;
color: blue;
}
.src-Collections__button {
color: orange;
}Having the .src-Button__button rule duplicated across chunks makes it possible to undo the override set in Photos.css if the chunks are loaded in a specific order. The load order may vary depending on which components are rendered on the page, and how the user navigates around the application.
We can illustrate the problem by having a user navigate our app as follows:
Loads Photos Page -> Collections Page -> Photos Page
| | |
|- color: black |- color: orange |- color: blue
The CSS chunk for Collections is loaded after Photos, causing the .src-Button__button rule to be loaded a second time later in the cascade order, undoing the override we've set in Photos.css.
Please paste the results of npx webpack-cli info here, and mention other relevant information
$ npx webpack-cli info
System:
OS: macOS 14.5
CPU: (10) arm64 Apple M1 Pro
Memory: 51.59 MB / 16.00 GB
Binaries:
Node: 20.12.2 - /nix/store/bzzs4kvjyvjjhs3rj08vqpvvzmfggvbv-nodejs-20.12.2/bin/node
Yarn: 1.22.22 - /nix/store/m2fiyh0393635g5vm804ady3rq0j24l4-yarn-1.22.22/bin/yarn
npm: 10.5.0 - /nix/store/bzzs4kvjyvjjhs3rj08vqpvvzmfggvbv-nodejs-20.12.2/bin/npm
Browsers:
Chrome: 125.0.6422.77
Safari: 17.5
Packages:
css-loader: ^7.1.2 => 7.1.2
webpack: ^5.91.0 => 5.91.0
webpack-cli: ^5.1.4 => 5.1.4Sorry, we can't get chunk name/chunk hash/etc while we are building modules, anyway we have https://webpack.js.org/configuration/module/#ruleissuer, so you can setup multiple css-loader loaders with different localIdentNames, another solution is using - https://webpack.js.org/configuration/module/#rulelayer, so you can setup different layers, so you can split your application on layers.
Also CSS modules support - https://github.com/webpack-contrib/css-loader?tab=readme-ov-file#separating-interoperable-css-only-and-css-module-features, so you can create basic button component and use composes (https://github.com/webpack-contrib/css-loader/blob/master/test/fixtures/modules/composes/composes.css#L44C3-L44C11)
Also my recomendation is avoid such situations and give unique name for the each class - i.e. have button-collection/button-photo/etc
Feel free to feedback
Also CSS modules support - https://github.com/webpack-contrib/css-loader?tab=readme-ov-file#separating-interoperable-css-only-and-css-module-features, so you can create basic button component and use
composes(https://github.com/webpack-contrib/css-loader/blob/master/test/fixtures/modules/composes/composes.css#L44C3-L44C11)Also my recomendation is avoid such situations and give unique name for the each class - i.e. have
button-collection/button-photo/etc
Unfortunately neither of these suggestions solve the problem.
anyway we have https://webpack.js.org/configuration/module/#ruleissuer, so you can setup multiple
css-loaderloaders with differentlocalIdentNames, another solution is using - https://webpack.js.org/configuration/module/#rulelayer, so you can setup different layers, so you can split your application on layers.
I don't think it would feasible to do this for all of the places where this issue may occur in a large application, such as the one we have at Unsplash.
I understand, I’m just offering solutions, we simply don’t have chunks available at the time of building the modules
Another solution - use getLocalIdent like a function and get issuer from LoaderContext (i.e. loaderContext._module), it is hacky, but will work, and you can see which things you can use to achive your behaviour.
Feel free to feedback