diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec14657..1afc6cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,9 +14,13 @@ on: jobs: test: runs-on: ${{ matrix.os }} + name: Test tailwindcss@${{ matrix.tailwindcss }} on ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] + tailwindcss: + - latest + - "3.1.0" # The fist version that support `options.config`. # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -35,5 +39,11 @@ jobs: - name: Install Dependencies run: pnpm install && npx playwright install chromium + - name: Install tailwindcss@${{ matrix.tailwindcss }} + if: ${{ matrix.tailwindcss }} != "latest" + # Tailwind CSS <= v3.4.0 does not have correct TypeScript definition, which will make `rslib build` fail. + continue-on-error: true + run: pnpm add -D -w tailwindcss@${{ matrix.tailwindcss }} + - name: Run Test run: pnpm run test diff --git a/package.json b/package.json index 1b399f5..6c43362 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,10 @@ "@rslib/core": "^0.1.1", "@rsbuild/webpack": "^1.1.3", "@types/node": "^22.10.1", + "@types/semver": "^7.5.8", "playwright": "^1.49.0", "postcss": "^8.4.49", + "semver": "^7.6.3", "simple-git-hooks": "^2.11.1", "tailwindcss": "^3.4.15", "typescript": "^5.7.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3708888..475e93b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,12 +29,18 @@ importers: '@types/node': specifier: ^22.10.1 version: 22.10.1 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 playwright: specifier: ^1.49.0 version: 1.49.0 postcss: specifier: ^8.4.49 version: 8.4.49 + semver: + specifier: ^7.6.3 + version: 7.6.3 simple-git-hooks: specifier: ^2.11.1 version: 2.11.1 @@ -279,6 +285,9 @@ packages: '@types/node@22.10.1': resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} + '@types/semver@7.5.8': + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -449,8 +458,8 @@ packages: core-js@3.39.0: resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} - cross-spawn@7.0.5: - resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} cssesc@3.0.0: @@ -875,6 +884,11 @@ packages: resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} engines: {node: '>= 12.13.0'} + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -1056,8 +1070,8 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - yaml@2.6.0: - resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} hasBin: true @@ -1274,6 +1288,8 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/semver@7.5.8': {} + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -1468,7 +1484,7 @@ snapshots: core-js@3.39.0: {} - cross-spawn@7.0.5: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -1546,7 +1562,7 @@ snapshots: foreground-child@3.3.0: dependencies: - cross-spawn: 7.0.5 + cross-spawn: 7.0.6 signal-exit: 4.1.0 fsevents@2.3.2: @@ -1743,7 +1759,7 @@ snapshots: postcss-load-config@4.0.2(postcss@8.4.49): dependencies: lilconfig: 3.1.2 - yaml: 2.6.0 + yaml: 2.6.1 optionalDependencies: postcss: 8.4.49 @@ -1821,6 +1837,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + semver@7.6.3: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -2036,4 +2054,4 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 - yaml@2.6.0: {} + yaml@2.6.1: {} diff --git a/src/TailwindCSSRspackPlugin.ts b/src/TailwindCSSRspackPlugin.ts index fc0ac9f..68f191f 100644 --- a/src/TailwindCSSRspackPlugin.ts +++ b/src/TailwindCSSRspackPlugin.ts @@ -1,5 +1,6 @@ import { existsSync } from 'node:fs'; -import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -286,26 +287,80 @@ class TailwindRspackPluginImpl { await mkdir(outputDir, { recursive: true }); } - const configPath = path.resolve(outputDir, 'tailwind.config.mjs'); + const [configName, configContent] = await this.#generateTailwindConfig( + userConfig, + entryModules, + ); + const configPath = path.resolve(outputDir, configName); - const content = JSON.stringify(entryModules); + await writeFile(configPath, configContent); - await writeFile( - configPath, - existsSync(userConfig) - ? `\ + return configPath; + } + + async #resolveTailwindCSSVersion(): Promise { + const require = createRequire(import.meta.url); + const pkgPath = require.resolve('tailwindcss/package.json', { + paths: [this.compiler.context], + }); + + const content = await readFile(pkgPath, 'utf-8'); + + const { version } = JSON.parse(content) as { version: string }; + + return version; + } + + async #generateTailwindConfig( + userConfig: string, + entryModules: string[], + ): Promise<['tailwind.config.mjs' | 'tailwind.config.cjs', string]> { + const version = await this.#resolveTailwindCSSVersion(); + + const { default: satisfies } = await import( + 'semver/functions/satisfies.js' + ); + + const content = JSON.stringify(entryModules); + if (satisfies(version, '^3.3.0')) { + // Tailwind CSS support using ESM configuration in v3.3.0 + // See: + // - https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.3.0 + // - https://github.com/tailwindlabs/tailwindcss/pull/10785 + // - https://github.com/rspack-contrib/rsbuild-plugin-tailwindcss/issues/18 + // + // In this case, we provide an ESM configuration to support both ESM and CJS. + return [ + 'tailwind.config.mjs', + existsSync(userConfig) + ? `\ import config from '${pathToFileURL(userConfig)}' export default { ...config, content: ${content} }` - : `\ + : `\ export default { content: ${content} }`, - ); + ]; + } - return configPath; + // Otherwise, we provide an CJS configuration since TailwindCSS would always use `require`. + return [ + 'tailwind.config.cjs', + existsSync(userConfig) + ? `\ +const config = require(${JSON.stringify(userConfig)}) +module.exports = { + ...config, + content: ${content} +}` + : `\ +module.exports = { + content: ${content} +}`, + ]; } } diff --git a/test/cjs/config/package.json b/test/cjs/config/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/test/cjs/config/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/basic/tailwind.config.js b/test/cjs/config/tailwind.config.js similarity index 67% rename from test/basic/tailwind.config.js rename to test/cjs/config/tailwind.config.js index f56fafe..3077cf2 100644 --- a/test/basic/tailwind.config.js +++ b/test/cjs/config/tailwind.config.js @@ -1,2 +1,2 @@ /** @type {import('tailwindcss').Config} */ -export default {}; +module.exports = {}; diff --git a/test/cjs/index.test.ts b/test/cjs/index.test.ts new file mode 100644 index 0000000..169b442 --- /dev/null +++ b/test/cjs/index.test.ts @@ -0,0 +1,99 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { expect, test } from '@playwright/test'; +import { createRsbuild } from '@rsbuild/core'; +import { pluginTailwindCSS } from '../../src'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +test('should build with relative config', async ({ page }) => { + const rsbuild = await createRsbuild({ + cwd: __dirname, + rsbuildConfig: { + plugins: [ + pluginTailwindCSS({ + config: './config/tailwind.config.js', + }), + ], + }, + }); + + await rsbuild.build(); + const { server, urls } = await rsbuild.preview(); + + await page.goto(urls[0]); + + const display = await page.evaluate(() => { + const el = document.getElementById('test'); + + if (!el) { + throw new Error('#test not found'); + } + + return window.getComputedStyle(el).getPropertyValue('display'); + }); + + expect(display).toBe('flex'); + + await server.close(); +}); + +test('should build with absolute config', async ({ page }) => { + const rsbuild = await createRsbuild({ + cwd: __dirname, + rsbuildConfig: { + plugins: [ + pluginTailwindCSS({ + config: resolve(__dirname, './config/tailwind.config.js'), + }), + ], + }, + }); + + await rsbuild.build(); + const { server, urls } = await rsbuild.preview(); + + await page.goto(urls[0]); + + const display = await page.evaluate(() => { + const el = document.getElementById('test'); + + if (!el) { + throw new Error('#test not found'); + } + + return window.getComputedStyle(el).getPropertyValue('display'); + }); + + expect(display).toBe('flex'); + + await server.close(); +}); + +test('should build without tailwind.config.js', async ({ page }) => { + const rsbuild = await createRsbuild({ + cwd: __dirname, + rsbuildConfig: { + plugins: [pluginTailwindCSS()], + }, + }); + + await rsbuild.build(); + const { server, urls } = await rsbuild.preview(); + + await page.goto(urls[0]); + + const display = await page.evaluate(() => { + const el = document.getElementById('test'); + + if (!el) { + throw new Error('#test not found'); + } + + return window.getComputedStyle(el).getPropertyValue('display'); + }); + + expect(display).toBe('flex'); + + await server.close(); +}); diff --git a/test/cjs/src/index.js b/test/cjs/src/index.js new file mode 100644 index 0000000..af396cf --- /dev/null +++ b/test/cjs/src/index.js @@ -0,0 +1,11 @@ +import 'tailwindcss/utilities.css'; + +function className() { + return 'flex'; +} + +const root = document.getElementById('root'); +const element = document.createElement('div'); +element.id = 'test'; +element.className = className(); +root.appendChild(element); diff --git a/test/config/index.test.ts b/test/config/index.test.ts index 169b442..bfd9cf9 100644 --- a/test/config/index.test.ts +++ b/test/config/index.test.ts @@ -3,10 +3,16 @@ import { fileURLToPath } from 'node:url'; import { expect, test } from '@playwright/test'; import { createRsbuild } from '@rsbuild/core'; import { pluginTailwindCSS } from '../../src'; +import { supportESM } from '../helper'; const __dirname = dirname(fileURLToPath(import.meta.url)); test('should build with relative config', async ({ page }) => { + test.skip( + !supportESM(), + 'Skip since the tailwindcss version does not support ESM configuration', + ); + const rsbuild = await createRsbuild({ cwd: __dirname, rsbuildConfig: { @@ -39,6 +45,11 @@ test('should build with relative config', async ({ page }) => { }); test('should build with absolute config', async ({ page }) => { + test.skip( + !supportESM(), + 'Skip since the tailwindcss version does not support ESM configuration', + ); + const rsbuild = await createRsbuild({ cwd: __dirname, rsbuildConfig: { diff --git a/test/helper.ts b/test/helper.ts index 33bce14..61a053f 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -1,3 +1,6 @@ +import satisfies from 'semver/functions/satisfies.js'; +import pkg from 'tailwindcss/package.json' with { type: 'json' }; + const portMap = new Map(); export function getRandomPort( @@ -12,3 +15,12 @@ export function getRandomPort( port++; } } + +export function supportESM(): boolean { + // Tailwind CSS support using ESM configuration in v3.3.0 + // See: + // - https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.3.0 + // - https://github.com/tailwindlabs/tailwindcss/pull/10785 + // - https://github.com/rspack-contrib/rsbuild-plugin-tailwindcss/issues/18 + return satisfies(pkg.version, '^3.3.0'); +} diff --git a/test/multi-entries/tailwind.config.js b/test/multi-entries/tailwind.config.js deleted file mode 100644 index f56fafe..0000000 --- a/test/multi-entries/tailwind.config.js +++ /dev/null @@ -1,2 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default {};