Closed RaphaelDDL closed 1 year ago
Yeah, a known problem with CSS-in-JS solutions without runtime, we can't fix it here, vue-loader
should fix it on own side, also I am not sure, but sometimes some CSS-in-JS solutions can have such limitations, there is the same problem and how developers use it https://github.com/webpack-contrib/css-loader/issues/1384.
We can't control how CSS was imported (in css-loader
), we just generate CSS code and classes for modules
If you look at generated code, you will see (disable devtool
for better reading):
const cssModules = {}
;
cssModules["$style"] = _app_vue_vue_type_style_index_0_id_5ef48958_lang_scss_module_true__WEBPACK_IMPORTED_MODULE_2__["default"];
if (false) {}
cssModules["$style"] = _app_vue_vue_type_style_index_1_id_5ef48958_lang_scss_module_true__WEBPACK_IMPORTED_MODULE_3__["default"]
if (false) {}
And as you can see the styles are just overwritten, it can be solved using Object.assign
instead =
, but again, this code generated by vue-loader (or vue template compiler), example of solution:
const cssModules = {}
;
cssModules["$style"] = {}
;
Object.assign(cssModules["$style"], _app_vue_vue_type_style_index_0_id_5ef48958_lang_scss_module_true__WEBPACK_IMPORTED_MODULE_2__["default"]);
if (false) {}
Object.assign(cssModules["$style"], _app_vue_vue_type_style_index_1_id_5ef48958_lang_scss_module_true__WEBPACK_IMPORTED_MODULE_3__["default"])
if (false) {}
Also want to note again - it can be a limitation on vue side with multiple style tags with the module
attribute
Feel free to feedback
First, thank you very much @alexander-akait for taking your time reading and understanding my issue. I appreciate it
Like I said, I wasn't even sure if multiple modules would be a bug or just not thought in this scenario. That's why I thought in asking in css-loader instead of vue-loader at first, as you would understand better the concept of the CSS Module itself. I guess I got sidetracked because of the locals = {}
in the compiled code made by the css-loader lol.
After seeing more of how is used outside Vue (React) during weekend, I came to see it could be really vue-loader, since in react is manually import style from './a-file.css'
and used style
in the JSX.
With your confirmation showing the un-devtool'd version (nice tip, I'll use it more) and the suggestion of possible fix, which was kinda what I had imagined in the pseudo-code, I understand now that in fact, that it is vue-loader's job to piece each of these style tags which became "separate" css files into a single applicable style, including the module exports hashmaps.
So I went into the real source on where styles are built, in special for modules and what is done in them and yeah, if I change
code += `\ncssModules["${name}"] = ${styleVar}`
to
code += `\n
if(cssModules["${name}"]){
Object.assign(cssModules["${name}"], ${styleVar});
} else {
cssModules["${name}"] = ${styleVar};
}`
it'll parse each css-loader locals
properly for all my test cases (except one, multiple module names style[module=a]
and style[module=b]
still break but I'll looking into it)
I'll try now push this into vue-loader and see if Evan and team will accept.
Thank you very much once again, Alex!
@alexander-akait I do have a following question if you don't mind, regarding one of the issues I pointed, and this one I'm not sure is vue-loader's fault but how the hash is made for the class name:
Circling back to this PoC:
<template>
<h2>
multiple named module=>
<span :class="moduledefault.modulenamedbrands">green</span> |
<span :class="modulecocacola.modulenamedbrands">red</span> |
<span :class="modulepepsi.modulenamedbrands">black</span>
</h2>
</template>
<style lang="scss" module="moduledefault">
.modulenamedbrands {
color: green;
}
</style>
<style lang="scss" module="modulecocacola">
.modulenamedbrands {
color: red;
}
</style>
<style lang="scss" module="modulepepsi">
.modulenamedbrands {
color: black;
}
</style>
3 modules names, moduledefault
, modulecocacola
, modulepepsi
. All three have the same classname, modulenamedbrands
. Ok, so, if everything was working as intended (vue-loader, css-loader, etc), I would expect to be able to access them through moduledefault.modulenamedbrands
, modulecocacola.modulenamedbrands
, modulepepsi.modulenamedbrands
respectively, right?
In this scenario, as I showed in the original post, all them are getting the same name. So I checked the code with devtools off, and seems the module is given for the call, e.g. for moduledefault
"./node_modules/css-loader/dist/cjs.js??clonedRuleSet-3.use[1]!./node_modules/vue-loader/dist/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-3.use[2]!./node_modules/sass-loader/dist/cjs.js??clonedRuleSet-3.use[3]!./node_modules/vue-loader/dist/index.js??ruleSet[1].rules[6].use[0]!./src/components/module-named-multiple.vue?vue&type=style&index=0&id=18f2a545&lang=scss&module=moduledefault":
The resulting hashed class always generates the same for all 3 different modules (the push shows the correct css, the locals all have same name):
/******* MODULEDEFAULT ****/
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh {\n color: green;\n}", ""]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
"modulenamedbrands": "module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh"
};
/******* MODULECOCACOLA ****/
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh {\n color: red;\n}", ""]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
"modulenamedbrands": "module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh"
};
/******* MODULEPEPSI ****/
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh {\n color: black;\n}", ""]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
"modulenamedbrands": "module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh"
};
So, the file name and the class name are the same. Doesn't seem it's taking module name &module=modulecocacola
into consideration when creating the hash, so the hash became the same in all.
I was thinking that I should be the one to tell css-loader that since I am the one that configured localIdentName
to be '[name]_[local]_[hash:base64]'
, but checking https://webpack.js.org/loaders/css-loader/#localidentname in "Supported template strings" it doesn't have anything I can use differently regarding module, and the [hash] is always the same.
I did test having the 3 modules with 3 different classes (each having it's own module name as a class) and worked ok:
/******* MODULEDEFAULT ****/
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh {\n color: green;\n}\n.module-named-multiple_moduledefault_zVQfNcu7rcnFIp5VJ8lC {\n color: green;\n}", ""]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
"modulenamedbrands": "module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh",
"moduledefault": "module-named-multiple_moduledefault_zVQfNcu7rcnFIp5VJ8lC"
};
/******* MODULECOCACOLA ****/
___CSS_LOADER_EXPORT___.push([module.id, ".module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh {\n color: red;\n}\n.module-named-multiple_modulecocacola_EYVlTdkVAbpx1GMCG7VH {\n color: red;\n}", ""]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
"modulenamedbrands": "module-named-multiple_modulenamedbrands_GeT1iohGlC280xnEEYWh",
"modulecocacola": "module-named-multiple_modulecocacola_EYVlTdkVAbpx1GMCG7VH"
};
Is this "different module name same class generating same hashmap" a css-loader issue?
PS.: I was looking into hashFunction for localIdentHashFunction
but I don't see how I could change [hash:base64]
implementation to take modulename into account.
We have this https://github.com/webpack-contrib/css-loader/blob/master/src/utils.js#L340, but because vue-loader doesn't use specific syntax for this (i.e. !=!
, you can found it in our test cases), you have a problem, as I said above we don't know how CSS was imported, so a loader above should think about collisions and solve it and we have !=!
syntax in import
, we have hashSalt
, we allow to use custom function to generate, these solutions to solve such problems
"./node_modules/css-loader/dist/cjs.js??clonedRuleSet-3.use[1]!./node_modules/vue-loader/dist/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-3.use[2]!./node_modules/sass-loader/dist/cjs.js??clonedRuleSet-3.use[3]!./node_modules/vue-loader/dist/index.js??ruleSet[1].rules[6].use[0]!./src/components/module-named-multiple.vue?vue&type=style&index=0&id=18f2a545&lang=scss&module=moduledefault":
as you can see moduledefault
is a part of vue-loader
request
Thank you very much again Alex, I couldn't find exactly how vue-loader would pass this to css-loader but I'll make a separated issue then for them :-)
edit: I've created the PR with my changes, though they don't fix all issues, specifically 2 in the extends
and the one where module name is not forwarded for creating the hash https://github.com/vuejs/vue-loader/pull/2045
Bug report
I'm using CSS modules with Vue3/Webpack5. All dependencies in their latest version as of this post (sass-loader, postcss-loader, css-loader, vue-loader, vue-style-loader, vue, webpack, etc). I've created a PoC with the issue https://github.com/RaphaelDDL/vue3-css-module
The css-loader options for module is as following:
CSS Modules create a locals object
___CSS_LOADER_EXPORT___.locals
which has the object/map of class->hashed class, this happens for each css/style, as vue-loader parses and provides for later loaders each style as a separated css source (which can be captured withtest: /\.(css|scss)$/
for e.g.)The issue: When using more than one
style
tag with amodule
attr, only the laststyle
tag retains it's module locals. Each style will have it's locals parsed, but only the last one in each .vue file retains the hashmap. (Reasoning for why have two modules later). Since https://github.com/css-modules/css-modules seems completely abandoned, I have no idea if this is really a bug or never thought.Actual Behavior
I've described in the PoC readme, but adding here as well. Taking the simplest test-case PoC Code module-nameless.vue::
When inspecting the loaders output, I can see the hashmaps being created, example:
and
You can see both were created and exist in the files served to browser![image](https://github.com/webpack-contrib/css-loader/assets/1087109/5b34fd83-dacf-4aaa-929d-edfff7cce12e)
The issue is that the end-result is that only the file's last style's
locals
hashmap exists for Vue, therefore the resulting HTML is a empty class asmulti1
is not inlocals
This issue also happens with two styles with named modules, e.g. PoC Code module-multiple.vue:
The behavior is even weirder when you try multiple modules with different named modules, e.g PoC Code module-named-multiple.vue::
In this case, module names will be ignored as being separated name/scopes, all styles will create their CSS and all them will have the same name and hash instead, making the last style's class overwrite all others![image](https://github.com/webpack-contrib/css-loader/assets/1087109/e0191da5-c79a-4b2a-bb05-53f6fa0355c6)
Expected Behavior
The expectation was that multiple modules would work as if shallow copy/merging, where:
$style
, idk others)This way, the locals hashmap would work like CSS where same rule/selector that comes later is applied on top of a earlier. Example:
First style generates:
Second style, also being a module, takes into account whatever locals already exists and merge, example:
Basically, a shallow merge (I assume the place would be in utils.js) for locals, e.g. pseudo-code
And same for named module(s), under each name/scope.
How Do We Reproduce?
As mentioned, I created a PoC in https://github.com/RaphaelDDL/vue3-css-module
It contains various views under
src/components/
where I explore all different combinations withmodule
,scoped
, styles instyle
and also in external files (style
withsrc
). The webpack config is the simplest I could: .vue is parsed byvue-loader
, thenscss/css
is parsed by (in this order): sass-loader, postcss-loader, css-loader, vue-style-loader.Now, after all this, the question that would come to mind is "Why have more than one module?":
To support two or more websites within each self-contained component, via an extra attribute in each style tag, called
brand
. In the PoC, I created two brands:cocacola
andpepsi
. During run and build, I pass which brand I want, Example:npm run start:pepsi
will instruct to run inbrand pepsi
, and in the webpack.config regarding the css, I use the CSS query for the other brand(s) and with nullLoader, to delete the other brand's styles.This is fine in all scenarios using styles, scoped, etc, except when modules is involved.
Please paste the results of
npx webpack-cli info
here, and mention other relevant informationConclusion
What I came to learn when debugging is that nullLoader doesn't "delete" the tag, it empties it's contents as it is what arrives as source when querying for .scss/.css. As I've shown before, only the LAST style with a module in each file keeps it's locals hashmap. Which means, if the last module in the file is the one being emptied, the empty style tag will make the entire locals empty (
locals = {}
).With this, I understood that CSS module doesn't support multiple style tags w/ module attribute, doesn't support multiple module attributes with same name, nor multiple different module names, etc.
As a "hacky" way to circumvent this issue (which doesn't cover everything, only when specifically has 2 styles with module, and both are nameless or named the same), I wrote a loader that runs before vue-loader, where I use RegExp to "delete" the other brand's style tag as a whole (commented, https://github.com/RaphaelDDL/vue3-css-module/blob/main/webpack.config.js#L108-L122 ) but of course, this approach is hard to read for those not used to regexp and VERY error-prone (e.g. the regexp atm doesn't cover yet self-closing styles, normally used when used with
src
for external files, nor covers when there are more than 2 brands).With the little documentation CSS Modules has in https://github.com/css-modules/css-modules (seems abandoned, last commit 6 years ago, multiple issues opened without discussion or conclusion), I'm not sure if having multiple modules not working is a real bug or this was never thought as an use-case in the concept of this feature.
I've looked at code regarding module's locals on css-loader utils L1182+, style-loader (index.js L108+, utils L198+) and vue-style-loader (index.js L43), seems they always expect a module or a named module, only?
Thank you for taking your time reading this, I appreciate any hint regarding this.
Best