Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Wanted] Support CSS module #536

Open
zhmushan opened this issue Jan 7, 2022 · 20 comments
Open

[Wanted] Support CSS module #536

zhmushan opened this issue Jan 7, 2022 · 20 comments
Labels

Comments

@zhmushan
Copy link
Contributor

zhmushan commented Jan 7, 2022

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@mdarche
Copy link

mdarche commented Nov 2, 2022

This would be an awesome built-in addition. Does anybody have a good workaround configuration for now?

I can get postcss-modules to create the JSON mapping while tsup generates a stylesheet, but I can't find a good way to connect those mappings to id and className selectors while building / watching.

It seems like most folks use rollup + @egoist's https://github.com/egoist/rollup-plugin-postcss for this.

@zhmushan
Copy link
Contributor Author

zhmushan commented Nov 3, 2022

My solution in 2021, not sure if there is a better one currently, FYI👇

searchfe/cosmic@d4fd622

export default defineConfig({
  ...,
  esbuildPlugins: [
    ...esbuildPlugins,
    {
      name: "css-module",
      setup(build) {
        build.onResolve(
          { filter: /\.module\.css$/, namespace: "file" },
          async (args) => {
            return {
              path: `${args.path}#css-module`,
              namespace: "css-module",
              pluginData: {
                path: args.path,
              },
            };
          },
        );
        build.onLoad(
          { filter: /#css-module$/, namespace: "css-module" },
          async (args) => {
            const { pluginData } = args;
            const postcss = require("postcss");
            const source = await fs.readFile(pluginData.path, "utf8");

            let cssModule = {};
            await postcss([
              require("postcss-modules")({
                getJSON(_, json) {
                  cssModule = json;
                },
              }),
            ]).process(source, { from: pluginData.path });

            return {
              contents: `
          import "${pluginData.path}"
          export default ${JSON.stringify(cssModule)}
          `,
            };
          },
        );
        build.onResolve(
          { filter: /\.module\.css$/, namespace: "css-module" },
          async (args) => {
            return {
              path: require.resolve(args.path, { paths: [args.resolveDir] }),
              namespace: "file",
            };
          },
        );
      },
    },
  ],
  ...,
});

@krailler
Copy link

krailler commented Nov 3, 2022

This is my modified approach

    esbuildPlugins: [
      {
        name: "css-module",
        setup(build): void {
          build.onResolve(
            { filter: /\.module\.css$/, namespace: "file" },
            (args) => ({
              path: `${args.path}#css-module`,
              namespace: "css-module",
              pluginData: {
                pathDir: path.join(args.resolveDir, args.path),
              },
            })
          );
          build.onLoad(
            { filter: /#css-module$/, namespace: "css-module" },
            async (args) => {
              const { pluginData } = args as {
                pluginData: { pathDir: string };
              };

              const source = await fsPromises.readFile(
                pluginData.pathDir,
                "utf8"
              );

              let cssModule = {};
              const result = await postcss([
                postcssModules({
                  getJSON(_, json) {
                    cssModule = json;
                  },
                }),
              ]).process(source, { from: pluginData.pathDir });

              return {
                pluginData: { css: result.css },
                contents: `import "${
                  pluginData.pathDir
                }"; export default ${JSON.stringify(cssModule)}`,
              };
            }
          );
          build.onResolve(
            { filter: /\.module\.css$/, namespace: "css-module" },
            (args) => ({
              path: path.join(args.resolveDir, args.path, "#css-module-data"),
              namespace: "css-module",
              pluginData: args.pluginData as { css: string },
            })
          );
          build.onLoad(
            { filter: /#css-module-data$/, namespace: "css-module" },
            (args) => ({
              contents: (args.pluginData as { css: string }).css,
              loader: "css",
            })
          );
        },
      },
    ],

@mdarche
Copy link

mdarche commented Nov 3, 2022

@zhmushan @krailler You guys are amazing, thank you for the assist!

@moishinetzer
Copy link

Any official support updates on this @egoist?

CSS modules are pretty much the only way to bundle css safely when developing a component library because the only way to include regular CSS is to inject the styles which can easily clash (think of a button class).

@remy90
Copy link

remy90 commented May 26, 2023

@krailler @mdarche could you share which postcss and postcssModule packages you're using? There appears to be a fair few in the community channels

@zigang93
Copy link

zigang93 commented Jul 4, 2023

how about scss/sass ?? need some guide

@wilkinsonjack1993
Copy link

wilkinsonjack1993 commented Aug 15, 2023

For anyone still having issues with this, I had success with this:

// tsup.config.js
import { defineConfig } from "tsup";
import cssModulesPlugin from "esbuild-css-modules-plugin";

export default defineConfig({
  esbuildPlugins: [cssModulesPlugin()],
});

@chaseottofy
Copy link

chaseottofy commented Aug 31, 2023

krailler Thanks, this was the only solution that worked for me.

I've implemented it in a basic library using only tsup if anyone wants to look over it for help (I couldn't find a basic working example anywhere).
repo-link:monthpicker-lite-js

@0x80
Copy link

0x80 commented Sep 22, 2023

@wilkinsonjack1993 using esbuild-css-modules-plugin my output dist folder got its files located in dist/src instead of directly in dist. I did not find any related configuration options on the plugin.

The @zhmushan / @krailler solution worked for me 👍

@mayank1513
Copy link

I have published this plugin based on discussion here. - https://github.com/mayank1513/esbuild-plugin-css-module

Hope it helps!

@figmatom
Copy link

figmatom commented Oct 8, 2023

Hey y'all, esbuild has native css module support now, but it looks like tsup clobbers it by default, if you add

  loader: {
    '.css': 'local-css',
  },

To your tsup config, this outputs css modules correctly.

@TheeMattOliver
Copy link

@figmatom do you have an example tsup.config.ts file you could share?

@fwextensions
Copy link

@TheeMattOliver, fwiw, this is the tsup config in my package.json after applying the workaround @figmatom mentioned:

  "tsup": {
    "format": [
      "esm"
    ],
    "loader": {
      ".css": "local-css"
    },
    "dts": true,
    "sourcemap": true,
    "clean": true
  }

This correctly imports the class names into the bundled .js. Before, I was getting (unminified) output like this in the bundled file for an imported CSS file:

// src/components/InputButtons.css
var InputButtons_default = {};

But with the change above, I'm getting this:

// src/components/InputButtons.css
var InputButtons_default = {
  inputButton: "InputButtons_inputButton",
  selected: "InputButtons_selected"
};

@paulm17
Copy link

paulm17 commented Nov 25, 2023

@fwextensions what is "local-css" in this instance?

I'm getting Error: Invalid loader value: "local-css"

It's probably an npm package?

Thanks

@h2ck4u
Copy link

h2ck4u commented Nov 28, 2023

@paulm17
Hi I was having the same problem.
But i upgraded the tsup version and it was solved.

"tsup": "^5.10.1", 

to

"tsup": "^8.0.1",

@paulm17
Copy link

paulm17 commented Nov 29, 2023

I've tried all the options here. The problem is what @mdarche said last year.

// src/Badge.module.css
var Badge_default = {};

That's the result of the build process. There's no css to latch onto.

What I should be seeing is something like:

import classes from './Badge.module.mjs';

@paulm17
Copy link

paulm17 commented Dec 3, 2023

Finally got a working solution for me. As I said before there was no connection between the js and css. But I finally figured out a way.

Using @krailler example.

let cssModule = {};

I kept wondering why this was blank and found out that this is connection.

Here is an example. Let's say I have the following classes:

.root {
.root--dot {
.label {
.section {

The following code in the build script:

const newSelector = generateScopedName(name, filename);

translates this to:

.m-347db0ec {
.m-fbd81e3d {
.m-5add502a { 
.m-91fdda9b {

for the new index.css stylesheet in the dist folder.

My new code appends to the cssModules array with the class name and new selector. Which results in:

// css-module:./Badge.module.css#css-module
var Badge_module_default = { 
    "root": "m-347db0ec", 
    "root--dot": "m-fbd81e3d", 
    "label": "m-5add502a", 
    "section": "m-91fdda9b"
 };

I also found out the config params needed to be in a certain order and indeed v8.0.1 at a minimum needed to be used.

Full code here:

import { defineConfig } from "tsup";
import path from "path";
import fsPromises from "fs/promises";
import postcss from "postcss";
import postcssModules from "postcss-modules";
import { generateScopedName } from "hash-css-selector";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  loader: {
    ".css": "local-css",
  },
  dts: true,
  sourcemap: true,
  clean: true,
  esbuildPlugins: [
    {
      name: "css-module",
      setup(build): void {
        build.onResolve(
          { filter: /\.module\.css$/, namespace: "file" },
          (args) => {
            return {
              path: `${args.path}#css-module`,
              namespace: "css-module",
              pluginData: {
                pathDir: path.join(args.resolveDir, args.path),
              },
            };
          },
        );
        build.onLoad(
          { filter: /#css-module$/, namespace: "css-module" },
          async (args) => {
            const { pluginData } = args as {
              pluginData: { pathDir: string };
            };

            const source = await fsPromises.readFile(
              pluginData.pathDir,
              "utf8",
            );

            let cssModule: any = {};
            const result = await postcss([
              postcssModules({
                generateScopedName: function (name, filename) {
                  const newSelector = generateScopedName(name, filename);
                  cssModule[name] = newSelector;

                  return newSelector;
                },
                getJSON: () => {},
                scopeBehaviour: "local",
              }),
            ]).process(source, { from: pluginData.pathDir });

            return {
              pluginData: { css: result.css },
              contents: `import "${
                pluginData.pathDir
              }"; export default ${JSON.stringify(cssModule)}`,
            };
          },
        );
        build.onResolve(
          { filter: /\.module\.css$/, namespace: "css-module" },
          (args) => ({
            path: path.join(args.resolveDir, args.path, "#css-module-data"),
            namespace: "css-module",
            pluginData: args.pluginData as { css: string },
          }),
        );
        build.onLoad(
          { filter: /#css-module-data$/, namespace: "css-module" },
          (args) => ({
            contents: (args.pluginData as { css: string }).css,
            loader: "css",
          }),
        );
      },
    },
  ],
});

All that's left, is to import the index.css and it all works. 😄

chrisvxd pushed a commit to measuredco/puck that referenced this issue Mar 25, 2024
tsup now processes css pre-processes natively
this is also resolves core yarn build --watch not refreshing css changes
see egoist/tsup#536 (comment)
@gopal-virtual
Copy link

gopal-virtual commented May 24, 2024

Hey y'all, esbuild has native css module support now, but it looks like tsup clobbers it by default, if you add

  loader: {
    '.css': 'local-css',
  },

To your tsup config, this outputs css modules correctly.

How can I make the js file to have an import statement for the css? I don't want to add css import in my consumer app.

@darklight9811
Copy link

CSS modules work properly using local-css, but this makes tailwind classes stop working, because it tries to obfuscate them too, but the reference in the code is not obfuscated. Tried using :local and :global flags as mentioned in esbuild docs, but they are not being detected.

Created another ticket for this here #1176

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests