Skip to content

Commit 861f8dd

Browse files
committed
Fully support scss, scss modules, css modules
1 parent 07d581f commit 861f8dd

5 files changed

Lines changed: 102 additions & 54 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
declare module "*.module.css";
2+
declare module "*.module.scss";

lib/esbuild-plugin-react18-css-example/src/global.css renamed to lib/esbuild-plugin-react18-css-example/src/global.scss

File renamed without changes.

lib/esbuild-plugin-react18-css-example/src/server/fork-me/fork-me.module.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
::placeholder {
2+
color: gray;
3+
}
14
.fork {
25
position: fixed;
36
display: flex;
@@ -19,6 +22,7 @@
1922
text-decoration: none;
2023
z-index: 10000;
2124
}
25+
2226
.fork:hover {
2327
filter: drop-shadow(0 0 5px var(--sc));
2428
}

lib/esbuild-plugin-react18-css/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@
2626
"dependencies": {
2727
"autoprefixer": "^10.4.18",
2828
"postcss": "^8.4.35",
29-
"postcss-modules": "^6.0.0"
29+
"postcss-modules": "^6.0.0",
30+
"sass": "^1.72.0"
3031
},
3132
"peerDependencies": {
32-
"autoprefixer": "^10.4.18",
33-
"postcss": "^8.4.35",
34-
"postcss-modules": "^6.0.0"
33+
"autoprefixer": "10",
34+
"postcss": "8",
35+
"postcss-modules": "6",
36+
"sass": "1"
3537
},
3638
"devDependencies": {
3739
"@types/node": "^20.11.22",
Lines changed: 91 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,107 @@
1-
import { Plugin } from "esbuild";
1+
import { Plugin, PluginBuild } from "esbuild";
22
import fs from "node:fs";
33
import path from "node:path";
44
import postcss from "postcss";
55
import postcssModules from "postcss-modules";
66
import autoPrefixer from "autoprefixer";
7+
import { compile } from "sass";
78

8-
const cssModulePlugin: () => Plugin = () => ({
9-
name: "esbuild-plugin-react18-css-" + uuid(),
10-
setup(build): void {
11-
build.onResolve({ filter: /\.module\.css$/, namespace: "file" }, args => ({
12-
path: `${args.path}#css-module`,
13-
namespace: "css-module",
14-
pluginData: {
15-
pathDir: path.join(args.resolveDir, args.path),
16-
},
17-
}));
18-
build.onLoad({ filter: /#css-module$/, namespace: "css-module" }, async args => {
19-
const { pluginData } = args as {
20-
pluginData: { pathDir: string };
21-
};
9+
const uuid = () => (Date.now() * Math.random()).toString(36).slice(0, 8);
10+
11+
interface CSSModulePluginOptions {
12+
generateScopedName?: string | ((name: string, filename: string, css: string) => string);
13+
skipAutoPrefixer?: boolean;
14+
}
15+
16+
function applyAutoPrefixer(build: PluginBuild, options: CSSModulePluginOptions, write?: boolean) {
17+
build.onEnd(async result => {
18+
if (!options.skipAutoPrefixer) {
19+
for (const f of result.outputFiles?.filter(f => f.path.match(/\.css$/)) || []) {
20+
const { css } = await postcss([autoPrefixer]).process(f.text, { from: f.path });
21+
f.contents = new TextEncoder().encode(css);
22+
}
23+
}
24+
25+
/** assume true if undefined */
26+
if (write === undefined || write) {
27+
result.outputFiles?.forEach(file => {
28+
fs.mkdirSync(path.dirname(file.path), { recursive: true });
29+
fs.writeFileSync(file.path, file.contents);
30+
});
31+
}
32+
});
33+
}
2234

23-
const source = fs.readFileSync(pluginData.pathDir, "utf8");
35+
function handleScss(build: PluginBuild) {
36+
build.onLoad({ filter: /\.scss$/, namespace: "file" }, args => ({
37+
contents: compile(args.path).css,
38+
loader: "css",
39+
}));
40+
}
2441

25-
let cssModule = {};
26-
const result = await postcss([
27-
postcssModules({
28-
getJSON(_, json) {
29-
cssModule = json;
30-
},
31-
generateScopedName: "[name]__[local]",
32-
}),
33-
autoPrefixer,
34-
]).process(source, { from: pluginData.pathDir });
42+
function handleModules(
43+
build: PluginBuild,
44+
{ generateScopedName }: CSSModulePluginOptions,
45+
type: "css" | "scss" = "css",
46+
) {
47+
const namespace = `${type}-module`;
48+
const filter = new RegExp(`\\.module\\.${type}$`);
49+
build.onResolve({ filter, namespace: "file" }, args => ({
50+
path: `${args.path}#${namespace}`,
51+
namespace,
52+
pluginData: {
53+
pathDir: path.join(args.resolveDir, args.path),
54+
},
55+
}));
3556

36-
return {
37-
pluginData: { css: result.css },
38-
contents: `import "${pluginData.pathDir}"; export default ${JSON.stringify(cssModule)}`,
39-
};
40-
});
57+
build.onLoad({ filter: new RegExp(`#${namespace}$`), namespace }, async args => {
58+
const { pluginData } = args as {
59+
pluginData: { pathDir: string };
60+
};
4161

42-
/** apply auto-prefixer to css */
62+
const source = compile(pluginData.pathDir).css;
4363

44-
build.onLoad({ filter: /\.css$/, namespace: "file" }, async args => {
45-
const source = fs.readFileSync(args.path, "utf8");
46-
const result = await postcss([autoPrefixer]).process(source, { from: args.path });
47-
console.log("CSS ------------------------------------ ", result.css);
48-
return { contents: result.css, loader: "css" };
49-
});
64+
let cssModule = {};
65+
const result = await postcss([
66+
postcssModules({
67+
getJSON(_, json) {
68+
cssModule = json;
69+
},
70+
generateScopedName,
71+
}),
72+
]).process(source, { from: pluginData.pathDir });
5073

51-
build.onResolve({ filter: /\.module\.css$/, namespace: "css-module" }, args => ({
52-
path: path.join(args.resolveDir, args.path, "#css-module-data"),
53-
namespace: "css-module",
54-
pluginData: args.pluginData as { css: string },
55-
}));
74+
return {
75+
pluginData: { css: result.css },
76+
contents: `import "${pluginData.pathDir}"; export default ${JSON.stringify(cssModule)}`,
77+
};
78+
});
5679

57-
build.onLoad({ filter: /#css-module-data$/, namespace: "css-module" }, args => ({
58-
contents: (args.pluginData as { css: string }).css,
59-
loader: "css",
60-
}));
80+
build.onResolve({ filter, namespace }, args => ({
81+
path: path.join(args.resolveDir, args.path, `#${namespace}-data`),
82+
namespace,
83+
pluginData: args.pluginData as { css: string },
84+
}));
85+
86+
build.onLoad({ filter: new RegExp(`#${namespace}-data$`), namespace }, args => ({
87+
contents: (args.pluginData as { css: string }).css,
88+
loader: "css",
89+
}));
90+
}
91+
92+
const cssPlugin: (options: CSSModulePluginOptions) => Plugin = (options = {}) => ({
93+
name: "esbuild-plugin-react18-css-" + uuid(),
94+
setup(build): void {
95+
const write = build.initialOptions.write;
96+
if (!options.generateScopedName) {
97+
options.generateScopedName = (name, filename) =>
98+
`${path.basename(filename).split(".")[0]}__${name}`;
99+
}
100+
handleModules(build, options);
101+
handleModules(build, options, "scss");
102+
handleScss(build);
103+
applyAutoPrefixer(build, options, write);
61104
},
62105
});
63106

64-
const uuid = () => (Date.now() * Math.random()).toString(36).slice(0, 8);
65-
66-
export = cssModulePlugin;
107+
export = cssPlugin;

0 commit comments

Comments
 (0)