Skip to content

Commit 94b3505

Browse files
authored
feat: implement TailwindCSSRspackPlugin (#2)
* feat: implement TailwindCSSRspackPlugin * fix: Windows path
1 parent 31a877e commit 94b3505

File tree

18 files changed

+1468
-13
lines changed

18 files changed

+1468
-13
lines changed

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,19 @@
1717
"files": ["dist"],
1818
"scripts": {
1919
"build": "rslib build",
20+
"bump": "npx bumpp",
2021
"dev": "rslib build --watch",
2122
"lint": "biome check .",
2223
"lint:write": "biome check . --write",
2324
"prepare": "simple-git-hooks && npm run build",
24-
"test": "playwright test",
25-
"bump": "npx bumpp"
25+
"test": "playwright test"
2626
},
2727
"simple-git-hooks": {
2828
"pre-commit": "npm run lint:write"
2929
},
30+
"dependencies": {
31+
"postcss": "^8.4.47"
32+
},
3033
"devDependencies": {
3134
"@biomejs/biome": "^1.9.4",
3235
"@playwright/test": "^1.48.2",
@@ -35,10 +38,12 @@
3538
"@types/node": "^22.9.0",
3639
"playwright": "^1.48.2",
3740
"simple-git-hooks": "^2.11.1",
41+
"tailwindcss": "^3.4.14",
3842
"typescript": "^5.6.3"
3943
},
4044
"peerDependencies": {
41-
"@rsbuild/core": "1.x"
45+
"@rsbuild/core": "1.x",
46+
"tailwindcss": "^3"
4247
},
4348
"peerDependenciesMeta": {
4449
"@rsbuild/core": {

pnpm-lock.yaml

Lines changed: 814 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/TailwindCSSRspackPlugin.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { mkdir, writeFile } from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { pathToFileURL } from 'node:url';
4+
5+
import type { Rspack } from '@rsbuild/core';
6+
7+
/**
8+
* The options for {@link TailwindRspackPlugin}.
9+
*
10+
* @public
11+
*/
12+
interface TailwindRspackPluginOptions {
13+
/**
14+
* The path to the configuration of Tailwind CSS.
15+
*
16+
* @remarks
17+
*
18+
* The default value is `tailwind.config.js`.
19+
*
20+
* @example
21+
*
22+
* Use absolute path:
23+
*
24+
* ```js
25+
* // rspack.config.js
26+
* import path from 'node:path'
27+
* import { fileURLToPath } from 'node:url'
28+
*
29+
* import { TailwindRspackPlugin } from '@rsbuild/plugin-tailwindcss'
30+
*
31+
* const __dirname = path.dirname(fileURLToPath(import.meta.url))
32+
*
33+
* export default {
34+
* plugins: [
35+
* new TailwindRspackPlugin({
36+
* config: path.resolve(__dirname, './config/tailwind.config.js'),
37+
* }),
38+
* ],
39+
* }
40+
* ```
41+
*
42+
* @example
43+
*
44+
* Use relative path:
45+
*
46+
* ```js
47+
* // rspack.config.js
48+
* import { TailwindRspackPlugin } from '@rsbuild/plugin-tailwindcss'
49+
*
50+
* export default {
51+
* plugins: [
52+
* new TailwindRspackPlugin({
53+
* config: './config/tailwind.config.js',
54+
* }),
55+
* ],
56+
* }
57+
* ```
58+
*/
59+
config?: string;
60+
}
61+
62+
/**
63+
* The Rspack plugin for Tailwind integration.
64+
*
65+
* @public
66+
*/
67+
class TailwindRspackPlugin {
68+
constructor(
69+
private readonly options?: TailwindRspackPluginOptions | undefined,
70+
) {}
71+
72+
/**
73+
* `defaultOptions` is the default options that the {@link TailwindRspackPlugin} uses.
74+
*
75+
* @public
76+
*/
77+
static defaultOptions: Readonly<Required<TailwindRspackPluginOptions>> =
78+
Object.freeze<Required<TailwindRspackPluginOptions>>({
79+
config: 'tailwind.config.js',
80+
});
81+
82+
/**
83+
* The entry point of a Rspack plugin.
84+
* @param compiler - the Rspack compiler
85+
*/
86+
apply(compiler: Rspack.Compiler): void {
87+
new TailwindRspackPluginImpl(
88+
compiler,
89+
Object.assign({}, TailwindRspackPlugin.defaultOptions, this.options),
90+
);
91+
}
92+
}
93+
94+
export { TailwindRspackPlugin };
95+
export type { TailwindRspackPluginOptions };
96+
97+
type NoUndefinedField<T> = {
98+
[P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>>;
99+
};
100+
101+
class TailwindRspackPluginImpl {
102+
name = 'TailwindRspackPlugin';
103+
104+
constructor(
105+
private compiler: Rspack.Compiler,
106+
private options: NoUndefinedField<TailwindRspackPluginOptions>,
107+
) {
108+
const { RawSource } = compiler.webpack.sources;
109+
compiler.hooks.thisCompilation.tap(this.name, (compilation) => {
110+
compilation.hooks.processAssets.tapPromise(this.name, async () => {
111+
await Promise.all(
112+
[...compilation.entrypoints.entries()].map(
113+
async ([entryName, entrypoint]) => {
114+
const cssFiles = entrypoint
115+
.getFiles()
116+
.filter((file) => file.endsWith('.css'))
117+
.map((file) => compilation.getAsset(file))
118+
.filter((file) => !!file);
119+
120+
if (cssFiles.length === 0) {
121+
// Ignore entrypoint without CSS files.
122+
return;
123+
}
124+
125+
// collect all the modules corresponding to specific entry
126+
const entryModules = new Set<string>();
127+
128+
for (const chunk of entrypoint.chunks) {
129+
const modules =
130+
compilation.chunkGraph.getChunkModulesIterable(chunk);
131+
for (const module of modules) {
132+
collectModules(module, entryModules);
133+
}
134+
}
135+
136+
const [
137+
{ default: postcss },
138+
{ default: tailwindcss },
139+
configPath,
140+
] = await Promise.all([
141+
import('postcss'),
142+
import('tailwindcss'),
143+
this.#prepareTailwindConfig(entryName, entryModules),
144+
]);
145+
146+
const postcssTransform = postcss([
147+
// We use a config path to avoid performance issue of TailwindCSS
148+
// See: https://github.com/tailwindlabs/tailwindcss/issues/14229
149+
tailwindcss({
150+
config: configPath,
151+
}),
152+
]);
153+
154+
// iterate all css asset in entry and inject entry modules into tailwind content
155+
await Promise.all(
156+
cssFiles.map(async (asset) => {
157+
const content = asset.source.source();
158+
// transform .css which contains tailwind mixin
159+
// FIXME: add custom postcss config
160+
const transformResult = await postcssTransform.process(
161+
content,
162+
{ from: asset.name },
163+
);
164+
// FIXME: add sourcemap support
165+
compilation.updateAsset(
166+
asset.name,
167+
new RawSource(transformResult.css),
168+
);
169+
}),
170+
);
171+
},
172+
),
173+
);
174+
});
175+
});
176+
}
177+
178+
async #prepareTailwindConfig(
179+
entryName: string,
180+
entryModules: Set<string>,
181+
): Promise<string> {
182+
const userConfig = path.isAbsolute(this.options.config)
183+
? this.options.config
184+
: // biome-ignore lint/style/noNonNullAssertion: should have context
185+
path.resolve(this.compiler.options.context!, this.options.config);
186+
187+
const outputDir = path.resolve(
188+
// biome-ignore lint/style/noNonNullAssertion: should have `output.path`
189+
this.compiler.options.output.path!,
190+
'.rsbuild',
191+
entryName,
192+
);
193+
await mkdir(outputDir, { recursive: true });
194+
195+
const configPath = path.resolve(outputDir, 'tailwind.config.mjs');
196+
197+
await writeFile(
198+
configPath,
199+
`\
200+
import config from '${pathToFileURL(userConfig)}'
201+
export default {
202+
...config,
203+
content: ${JSON.stringify(Array.from(entryModules))}
204+
}`,
205+
);
206+
207+
return configPath;
208+
}
209+
}
210+
211+
function collectModules(
212+
module: Rspack.Module,
213+
entryModules: Set<string>,
214+
): void {
215+
if (module.modules) {
216+
for (const innerModule of module.modules) {
217+
collectModules(innerModule, entryModules);
218+
}
219+
} else if (module.resource) {
220+
entryModules.add(module.resource);
221+
}
222+
}

src/index.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,54 @@
11
import type { RsbuildPlugin } from '@rsbuild/core';
22

3+
import { TailwindRspackPlugin } from './TailwindCSSRspackPlugin.js';
4+
35
export type PluginTailwindCSSOptions = {
6+
/**
7+
* The path to the configuration of Tailwind CSS.
8+
*
9+
* @remarks
10+
*
11+
* The default value is `tailwind.config.js`.
12+
*
13+
* @example
14+
*
15+
* Use relative path:
16+
*
17+
* ```js
18+
* // rsbuild.config.ts
19+
* import { pluginTailwindCSS } from '@byted-lynx/plugin-tailwindcss'
20+
*
21+
* export default {
22+
* plugins: [
23+
* pluginTailwindCSS({
24+
* config: './config/tailwind.config.js',
25+
* }),
26+
* ],
27+
* }
28+
* ```
29+
*
30+
* @example
31+
*
32+
* Use absolute path:
33+
*
34+
* ```js
35+
* // rsbuild.config.ts
36+
* import path from 'node:path'
37+
* import { fileURLToPath } from 'node:url'
38+
*
39+
* import { pluginTailwindCSS } from '@rsbuild/plugin-tailwindcss'
40+
*
41+
* const __dirname = path.dirname(fileURLToPath(import.meta.url))
42+
*
43+
* export default {
44+
* plugins: [
45+
* pluginTailwindCSS({
46+
* config: path.resolve(__dirname, './config/tailwind.config.js'),
47+
* }),
48+
* ],
49+
* }
50+
* ```
51+
*/
452
config?: string;
553
};
654

@@ -9,7 +57,18 @@ export const pluginTailwindCSS = (
957
): RsbuildPlugin => ({
1058
name: 'rsbuild:tailwindcss',
1159

12-
setup() {
13-
console.log('Hello Rsbuild!', options);
60+
setup(api) {
61+
api.modifyBundlerChain({
62+
order: 'post',
63+
handler(chain) {
64+
chain
65+
.plugin('tailwindcss')
66+
.use(TailwindRspackPlugin, [
67+
{ config: options.config ?? 'tailwind.config.js' },
68+
]);
69+
},
70+
});
1471
},
1572
});
73+
74+
export { TailwindRspackPlugin } from './TailwindCSSRspackPlugin.js';

0 commit comments

Comments
 (0)