Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

CSS Module locals/hashmap are being overwritten when more than one 'module' exists in same file #1518

Copy link
Copy link
Closed
@RaphaelDDL

Description

@RaphaelDDL
Issue body actions

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:

    options: {
        modules: {
            mode: 'local',
            localIdentName: '[name]_[local]_[hash:base64]',
        },
    },

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 with test: /\.(css|scss)$/ for e.g.)

The issue: When using more than one style tag with a module attr, only the last style 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::

<template>
    <h2>
        multiple module=> <span :class="$style.multi1">orange</span> | <span :class="$style.multi2">purple</span>
    </h2>
</template>

<style lang="scss" module>
.multi1 { color: orange; }
</style>
<style lang="scss" module>
.multi2 { color: purple; }
</style>

When inspecting the loaders output, I can see the hashmaps being created, example:

// Module
___CSS_LOADER_EXPORT___.push([module.id, ".module-multiple_multi1_[HASH]" /* sourcemap, source, etc*/]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
	"multi1": ".module-multiple_multi1_[HASH]"
};

and

// Module
___CSS_LOADER_EXPORT___.push([module.id, ".module-multiple_multi2_[HASH]" /* sourcemap, source, etc*/]);
// Exports
___CSS_LOADER_EXPORT___.locals = {
	"multi2": ".module-multiple_multi2_[HASH]"
};

You can see both were created and exist in the files served to browser
image

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 as multi1 is not in locals

    <h2>
        multiple module=> <span class="">orange</span> | <span class="module-multiple_multi2_[HASH]">purple</span>
    </h2>

This issue also happens with two styles with named modules, e.g. PoC Code module-multiple.vue:

<style lang="scss" module="classes">
.multi1 { color: orange; }
</style>
<style lang="scss" module="classes">
.multi2 { color: purple; }
</style>

The behavior is even weirder when you try multiple modules with different named modules, e.g PoC Code module-named-multiple.vue::

<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>

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

Expected Behavior

The expectation was that multiple modules would work as if shallow copy/merging, where:

  • nameless modules all merge their locals under same nameless scope (for vue, it creates $style, idk others)
  • modules that contain same name all merge their locals under same name/scope
  • multiple modules with different names each merge their locals under each of their name/scope

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:

___CSS_LOADER_EXPORT___.locals = {
	"multi1": ".module-multiple_multi1_[HASH]"
};

Second style, also being a module, takes into account whatever locals already exists and merge, example:

___CSS_LOADER_EXPORT___.locals = {
   "multi1": ".module-multiple_multi1_[HASH]",
   "multi2": ".module-multiple_multi2_[HASH]"
};

Basically, a shallow merge (I assume the place would be in utils.js) for locals, e.g. pseudo-code

___CSS_LOADER_EXPORT___.locals = {
      ...___CSS_LOADER_EXPORT___.locals,
      other locals
}

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 with module, scoped, styles in style and also in external files (style with src). The webpack config is the simplest I could: .vue is parsed by vue-loader, then scss/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 and pepsi. During run and build, I pass which brand I want, Example: npm run start:pepsi will instruct to run in brand 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.

<template>
    <h2 :class="$style.heading2">my cool multi-brand component; has to be (red || black)</h2>
</template>

<style lang="scss" module brand="cocacola">
.heading2 { color: red; }
</style>
<style lang="scss" module brand="pepsi">
.heading2 { color: black; }
</style>

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 information

  System:
    OS: macOS 12.3.1
    CPU: (8) x64 Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
    Memory: 58.40 MB / 32.00 GB
  Binaries:
    Node: 14.21.3 - ~/.nvm/versions/node/v14.21.3/bin/node
    npm: 6.14.18 - ~/.nvm/versions/node/v14.21.3/bin/npm
  Browsers:
    Chrome: 113.0.5672.63
    Safari: 15.4
  Packages:
    babel-loader: ^9.1.2 => 9.1.2
    css-loader: ^6.7.3 => 6.7.3
    html-webpack-plugin: ^5.5.1 => 5.5.1
    null-loader: ^4.0.1 => 4.0.1
    postcss-loader: ^7.3.0 => 7.3.0
    sass-loader: ^13.2.2 => 13.2.2
    simple-functional-loader: ^1.2.1 => 1.2.1
    style-loader: ^3.3.2 => 3.3.2
    vue-loader: ^17.1.0 => 17.1.0
    vue-style-loader: ^4.1.3 => 4.1.3
    webpack: ^5.82.0 => 5.82.0
    webpack-cli: ^5.1.1 => 5.1.1
    webpack-dev-server: ^4.15.0 => 4.15.0
    webpack-merge: ^5.8.0 => 5.8.0

Conclusion

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.