egoist / tsup

The simplest and fastest way to bundle your TypeScript libraries.
https://tsup.egoist.dev
MIT License
9.24k stars 218 forks source link

[Wanted] Support CSS module #536

Open zhmushan opened 2 years ago

zhmushan commented 2 years ago

Upvote & Fund

Fund with Polar

mdarche commented 2 years ago

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 commented 2 years ago
My solution in 2021, not sure if there is a better one currently, FYI👇 https://github.com/searchfe/cosmic/commit/d4fd6223a818f8134258130514cfd45f842784ef ```ts 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 commented 2 years ago

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 commented 2 years ago

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

moishinetzer commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

how about scss/sass ?? need some guide

wilkinsonjack1993 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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

Hope it helps!

figmatom commented 1 year ago

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 commented 1 year ago

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

fwextensions commented 1 year ago

@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 commented 11 months ago

@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 commented 11 months ago

@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 commented 11 months ago

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 commented 11 months ago

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

gopal-virtual commented 5 months ago

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 commented 3 months ago

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