Skip to content

Commit 34eff9d

Browse files
authored
feat: support tailwindcss v3.1.x and v3.2.y (#21)
### Summary Support Tailwind CSS v3.1 and Tailwind CSS v3.2. --- ### Details Since Tailwind CSS support using ESM configuration in V3.3, we need to use CJS configuration before that. As mentioned in #7 (comment), we use `readFile` + `require.resolve` to get the version of `tailwindcss/package.json`. - If `satisfies(version, ^3.3.0)`, we will generate ESM configuration to support both ESM and CJS. - Else, we generate CJS configuration. --- ### Test plan We setup two new entries in the testing matrix - tailwindcss v3.1.0 with Ubuntu - tailwindcss v3.1.0 with Windows As you can see, the test is failing at [0e05c6a](0e05c6a). And after the fix is landed in [e783415](e783415), the test for old tailwindcss is passing. --- ### Related links close: #18 - Tailwind CSS v3.3: - https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.3.0 - tailwindlabs/tailwindcss#10785 - Tailwind CSS v3.2 - https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.2.0 - Tailwind CSS v3.1 - https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.1.0
1 parent b7e19a8 commit 34eff9d

11 files changed

+240
-21
lines changed

.github/workflows/test.yml

+10
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ on:
1414
jobs:
1515
test:
1616
runs-on: ${{ matrix.os }}
17+
name: Test tailwindcss@${{ matrix.tailwindcss }} on ${{ matrix.os }}
1718
strategy:
1819
matrix:
1920
os: [ubuntu-latest, windows-latest]
21+
tailwindcss:
22+
- latest
23+
- "3.1.0" # The fist version that support `options.config`.
2024

2125
# Steps represent a sequence of tasks that will be executed as part of the job
2226
steps:
@@ -35,5 +39,11 @@ jobs:
3539
- name: Install Dependencies
3640
run: pnpm install && npx playwright install chromium
3741

42+
- name: Install tailwindcss@${{ matrix.tailwindcss }}
43+
if: ${{ matrix.tailwindcss }} != "latest"
44+
# Tailwind CSS <= v3.4.0 does not have correct TypeScript definition, which will make `rslib build` fail.
45+
continue-on-error: true
46+
run: pnpm add -D -w tailwindcss@${{ matrix.tailwindcss }}
47+
3848
- name: Run Test
3949
run: pnpm run test

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
"@rslib/core": "^0.1.1",
3636
"@rsbuild/webpack": "^1.1.3",
3737
"@types/node": "^22.10.1",
38+
"@types/semver": "^7.5.8",
3839
"playwright": "^1.49.0",
3940
"postcss": "^8.4.49",
41+
"semver": "^7.6.3",
4042
"simple-git-hooks": "^2.11.1",
4143
"tailwindcss": "^3.4.15",
4244
"typescript": "^5.7.2"

pnpm-lock.yaml

+26-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/TailwindCSSRspackPlugin.ts

+65-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { existsSync } from 'node:fs';
2-
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
2+
import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
3+
import { createRequire } from 'node:module';
34
import { tmpdir } from 'node:os';
45
import path from 'node:path';
56
import { pathToFileURL } from 'node:url';
@@ -286,26 +287,80 @@ class TailwindRspackPluginImpl {
286287
await mkdir(outputDir, { recursive: true });
287288
}
288289

289-
const configPath = path.resolve(outputDir, 'tailwind.config.mjs');
290+
const [configName, configContent] = await this.#generateTailwindConfig(
291+
userConfig,
292+
entryModules,
293+
);
294+
const configPath = path.resolve(outputDir, configName);
290295

291-
const content = JSON.stringify(entryModules);
296+
await writeFile(configPath, configContent);
292297

293-
await writeFile(
294-
configPath,
295-
existsSync(userConfig)
296-
? `\
298+
return configPath;
299+
}
300+
301+
async #resolveTailwindCSSVersion(): Promise<string> {
302+
const require = createRequire(import.meta.url);
303+
const pkgPath = require.resolve('tailwindcss/package.json', {
304+
paths: [this.compiler.context],
305+
});
306+
307+
const content = await readFile(pkgPath, 'utf-8');
308+
309+
const { version } = JSON.parse(content) as { version: string };
310+
311+
return version;
312+
}
313+
314+
async #generateTailwindConfig(
315+
userConfig: string,
316+
entryModules: string[],
317+
): Promise<['tailwind.config.mjs' | 'tailwind.config.cjs', string]> {
318+
const version = await this.#resolveTailwindCSSVersion();
319+
320+
const { default: satisfies } = await import(
321+
'semver/functions/satisfies.js'
322+
);
323+
324+
const content = JSON.stringify(entryModules);
325+
if (satisfies(version, '^3.3.0')) {
326+
// Tailwind CSS support using ESM configuration in v3.3.0
327+
// See:
328+
// - https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.3.0
329+
// - https://github.com/tailwindlabs/tailwindcss/pull/10785
330+
// - https://github.com/rspack-contrib/rsbuild-plugin-tailwindcss/issues/18
331+
//
332+
// In this case, we provide an ESM configuration to support both ESM and CJS.
333+
return [
334+
'tailwind.config.mjs',
335+
existsSync(userConfig)
336+
? `\
297337
import config from '${pathToFileURL(userConfig)}'
298338
export default {
299339
...config,
300340
content: ${content}
301341
}`
302-
: `\
342+
: `\
303343
export default {
304344
content: ${content}
305345
}`,
306-
);
346+
];
347+
}
307348

308-
return configPath;
349+
// Otherwise, we provide an CJS configuration since TailwindCSS would always use `require`.
350+
return [
351+
'tailwind.config.cjs',
352+
existsSync(userConfig)
353+
? `\
354+
const config = require(${JSON.stringify(userConfig)})
355+
module.exports = {
356+
...config,
357+
content: ${content}
358+
}`
359+
: `\
360+
module.exports = {
361+
content: ${content}
362+
}`,
363+
];
309364
}
310365
}
311366

test/cjs/config/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"type": "commonjs"
3+
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
/** @type {import('tailwindcss').Config} */
2-
export default {};
2+
module.exports = {};

test/cjs/index.test.ts

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { dirname, resolve } from 'node:path';
2+
import { fileURLToPath } from 'node:url';
3+
import { expect, test } from '@playwright/test';
4+
import { createRsbuild } from '@rsbuild/core';
5+
import { pluginTailwindCSS } from '../../src';
6+
7+
const __dirname = dirname(fileURLToPath(import.meta.url));
8+
9+
test('should build with relative config', async ({ page }) => {
10+
const rsbuild = await createRsbuild({
11+
cwd: __dirname,
12+
rsbuildConfig: {
13+
plugins: [
14+
pluginTailwindCSS({
15+
config: './config/tailwind.config.js',
16+
}),
17+
],
18+
},
19+
});
20+
21+
await rsbuild.build();
22+
const { server, urls } = await rsbuild.preview();
23+
24+
await page.goto(urls[0]);
25+
26+
const display = await page.evaluate(() => {
27+
const el = document.getElementById('test');
28+
29+
if (!el) {
30+
throw new Error('#test not found');
31+
}
32+
33+
return window.getComputedStyle(el).getPropertyValue('display');
34+
});
35+
36+
expect(display).toBe('flex');
37+
38+
await server.close();
39+
});
40+
41+
test('should build with absolute config', async ({ page }) => {
42+
const rsbuild = await createRsbuild({
43+
cwd: __dirname,
44+
rsbuildConfig: {
45+
plugins: [
46+
pluginTailwindCSS({
47+
config: resolve(__dirname, './config/tailwind.config.js'),
48+
}),
49+
],
50+
},
51+
});
52+
53+
await rsbuild.build();
54+
const { server, urls } = await rsbuild.preview();
55+
56+
await page.goto(urls[0]);
57+
58+
const display = await page.evaluate(() => {
59+
const el = document.getElementById('test');
60+
61+
if (!el) {
62+
throw new Error('#test not found');
63+
}
64+
65+
return window.getComputedStyle(el).getPropertyValue('display');
66+
});
67+
68+
expect(display).toBe('flex');
69+
70+
await server.close();
71+
});
72+
73+
test('should build without tailwind.config.js', async ({ page }) => {
74+
const rsbuild = await createRsbuild({
75+
cwd: __dirname,
76+
rsbuildConfig: {
77+
plugins: [pluginTailwindCSS()],
78+
},
79+
});
80+
81+
await rsbuild.build();
82+
const { server, urls } = await rsbuild.preview();
83+
84+
await page.goto(urls[0]);
85+
86+
const display = await page.evaluate(() => {
87+
const el = document.getElementById('test');
88+
89+
if (!el) {
90+
throw new Error('#test not found');
91+
}
92+
93+
return window.getComputedStyle(el).getPropertyValue('display');
94+
});
95+
96+
expect(display).toBe('flex');
97+
98+
await server.close();
99+
});

test/cjs/src/index.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'tailwindcss/utilities.css';
2+
3+
function className() {
4+
return 'flex';
5+
}
6+
7+
const root = document.getElementById('root');
8+
const element = document.createElement('div');
9+
element.id = 'test';
10+
element.className = className();
11+
root.appendChild(element);

test/config/index.test.ts

+11
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import { fileURLToPath } from 'node:url';
33
import { expect, test } from '@playwright/test';
44
import { createRsbuild } from '@rsbuild/core';
55
import { pluginTailwindCSS } from '../../src';
6+
import { supportESM } from '../helper';
67

78
const __dirname = dirname(fileURLToPath(import.meta.url));
89

910
test('should build with relative config', async ({ page }) => {
11+
test.skip(
12+
!supportESM(),
13+
'Skip since the tailwindcss version does not support ESM configuration',
14+
);
15+
1016
const rsbuild = await createRsbuild({
1117
cwd: __dirname,
1218
rsbuildConfig: {
@@ -39,6 +45,11 @@ test('should build with relative config', async ({ page }) => {
3945
});
4046

4147
test('should build with absolute config', async ({ page }) => {
48+
test.skip(
49+
!supportESM(),
50+
'Skip since the tailwindcss version does not support ESM configuration',
51+
);
52+
4253
const rsbuild = await createRsbuild({
4354
cwd: __dirname,
4455
rsbuildConfig: {

0 commit comments

Comments
 (0)