import { relative, resolve } from "path" import { promises as fs } from "fs" import type { Plugin } from "esbuild" import postcss from "postcss" // @ts-expect-error import postcssSass from "@csstools/postcss-sass" import cssModules from "postcss-modules" type Options = Parameters[0] const PLUGIN_NAME = "esbuild-css-modules" async function buildCSSModule(cssFullPath: string, options: Options) { options = { localsConvention: "camelCaseOnly", ...options, } const source = await fs.readFile(cssFullPath) let classNames = {} const { css } = await postcss([ postcssSass(), cssModules({ getJSON(_, json) { classNames = json return classNames }, ...options, }), ]).process(source, { from: cssFullPath, map: false, }) return { css, classNames, } } const srcDir = resolve(__dirname, "../../src") const plugin = (options: Options = {}): Plugin => ({ name: PLUGIN_NAME, async setup(build) { const memfs = new Map() const FS_NAMESPACE = PLUGIN_NAME + "-fs" build.onResolve({ filter: /\.modules?\.s?css$/, namespace: "file" }, async args => { const res = await build.resolve(args.path, { kind: "import-statement", resolveDir: args.resolveDir, }) // This is just the unique ID for this CSS module. We use a relative path to make it easier to debug. const path = relative(srcDir, res.path) return { path, namespace: PLUGIN_NAME, pluginData: { realPath: res.path, }, } }) build.onResolve({ filter: /^@css-modules\/.*/ }, ({ path }) => ({ path, namespace: FS_NAMESPACE, })) build.onLoad({ filter: /./, namespace: FS_NAMESPACE }, ({ path }) => ({ contents: memfs.get(path)!, loader: "css", })) build.onLoad({ filter: /./, namespace: PLUGIN_NAME }, async args => { const tmpFilePath = "@css-modules/" + args.path const { classNames, css } = await buildCSSModule(args.pluginData.realPath, options) memfs.set(tmpFilePath, css) return { contents: `import ${JSON.stringify(tmpFilePath)};\n` + `module.exports = ${JSON.stringify(classNames)};`, loader: "js", } }) }, }) export { plugin as cssModules }