|
1 | | -import { Plugin } from "esbuild"; |
| 1 | +import { Plugin, PluginBuild } from "esbuild"; |
2 | 2 | import fs from "node:fs"; |
3 | 3 | import path from "node:path"; |
4 | 4 | import postcss from "postcss"; |
5 | 5 | import postcssModules from "postcss-modules"; |
6 | 6 | import autoPrefixer from "autoprefixer"; |
| 7 | +import { compile } from "sass"; |
7 | 8 |
|
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 | +} |
22 | 34 |
|
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 | +} |
24 | 41 |
|
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 | + })); |
35 | 56 |
|
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 | + }; |
41 | 61 |
|
42 | | - /** apply auto-prefixer to css */ |
| 62 | + const source = compile(pluginData.pathDir).css; |
43 | 63 |
|
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 }); |
50 | 73 |
|
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 | + }); |
56 | 79 |
|
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); |
61 | 104 | }, |
62 | 105 | }); |
63 | 106 |
|
64 | | -const uuid = () => (Date.now() * Math.random()).toString(36).slice(0, 8); |
65 | | - |
66 | | -export = cssModulePlugin; |
| 107 | +export = cssPlugin; |
0 commit comments