diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d28a1e1e0660..54512c071671 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,19 +14,32 @@ jobs:
fail-fast: false
matrix:
node-version: [20]
- runner: [namespace-profile-default, windows-latest, macos-14]
+ runner:
+ - name: Windows
+ os: windows-latest
+
+ - name: Linux
+ os: namespace-profile-default
+
+ - name: macOS
+ os: macos-14
+
# Exclude windows and macos from being built on feature branches
on-next-branch:
- ${{ github.ref == 'refs/heads/next' }}
exclude:
- on-next-branch: false
- runner: windows-latest
+ runner:
+ name: Windows
- on-next-branch: false
- runner: macos-14
+ runner:
+ name: macOS
- runs-on: ${{ matrix.runner }}
+ runs-on: ${{ matrix.runner.os }}
timeout-minutes: 30
+ name: ${{ matrix.runner.name }}
+
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
@@ -72,16 +85,11 @@ jobs:
- name: Lint
run: pnpm run lint
# Only lint on linux to avoid \r\n line ending errors
- if: matrix.runner == 'ubuntu-latest'
+ if: matrix.runner.os == 'ubuntu-latest'
- name: Test
run: pnpm run test
- - name: Integration Tests
- run: pnpm run test:integrations
- env:
- GITHUB_WORKSPACE: ${{ github.workspace }}
-
- name: Install Playwright Browsers
run: npx playwright install --with-deps
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
new file mode 100644
index 000000000000..d61223ae72b6
--- /dev/null
+++ b/.github/workflows/integration-tests.yml
@@ -0,0 +1,102 @@
+name: Integration Tests
+
+on:
+ push:
+ branches: [next]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ tests:
+ strategy:
+ fail-fast: false
+ matrix:
+ node-version: [20]
+
+ runner:
+ - name: Windows
+ os: windows-latest
+
+ - name: Linux
+ os: namespace-profile-default
+
+ - name: macOS
+ os: macos-14
+
+ integration:
+ - upgrade
+ - vite
+ - cli
+ - postcss
+
+ # Exclude windows and macos from being built on feature branches
+ on-next-branch:
+ - ${{ github.ref == 'refs/heads/next' }}
+ exclude:
+ - on-next-branch: false
+ runner:
+ name: Windows
+ - on-next-branch: false
+ runner:
+ name: macOS
+
+ runs-on: ${{ matrix.runner.os }}
+ timeout-minutes: 30
+
+ name: ${{ matrix.runner.name }} / ${{ matrix.integration }}
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'pnpm'
+
+ # Cargo already skips downloading dependencies if they already exist
+ - name: Cache cargo
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/bin/
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ target/
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ # Cache the `oxide` Rust build
+ - name: Cache oxide build
+ uses: actions/cache@v4
+ with:
+ path: |
+ ./target/
+ ./crates/node/*.node
+ ./crates/node/index.js
+ ./crates/node/index.d.ts
+ key: ${{ runner.os }}-oxide-${{ hashFiles('./crates/**/*') }}
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Build
+ run: pnpm run build
+ env:
+ CARGO_PROFILE_RELEASE_LTO: 'off'
+ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link'
+
+ - name: Test ${{ matrix.integration }}
+ run: pnpm run test:integrations ./integrations/${{ matrix.integration }}
+ env:
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+
+ - name: Notify Discord
+ if: failure() && github.ref == 'refs/heads/next'
+ uses: discord-actions/message@v2
+ with:
+ webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `next` branch has failed.'
diff --git a/integrations/cli/config.test.ts b/integrations/cli/config.test.ts
index 3e55fa113f46..dd93ade648cc 100644
--- a/integrations/cli/config.test.ts
+++ b/integrations/cli/config.test.ts
@@ -161,7 +161,10 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch')
+ let process = await spawn(
+ 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch',
+ )
+ await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('dist/out.css', [
//
@@ -214,7 +217,10 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch')
+ let process = await spawn(
+ 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch',
+ )
+ await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('dist/out.css', [
//
@@ -267,7 +273,10 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch')
+ let process = await spawn(
+ 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch',
+ )
+ await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('dist/out.css', [
//
diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index c6eac6fa782f..a1620617153e 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -1,7 +1,7 @@
import dedent from 'dedent'
import os from 'node:os'
import path from 'node:path'
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
const STANDALONE_BINARY = (() => {
@@ -156,9 +156,10 @@ describe.each([
},
},
async ({ root, fs, spawn }) => {
- await spawn(`${command} --input src/index.css --output dist/out.css --watch`, {
+ let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch`, {
cwd: path.join(root, 'project-a'),
})
+ await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
@@ -491,7 +492,7 @@ test(
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm tailwindcss --input index.css --output dist/out.css')
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
@@ -722,7 +723,7 @@ test(
>`,
},
},
- async ({ fs, exec, spawn, root }) => {
+ async ({ fs, exec, spawn, root, expect }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
cwd: path.join(root, 'project-a'),
})
@@ -790,9 +791,13 @@ test(
`)
// Watch mode tests
- await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch', {
- cwd: path.join(root, 'project-a'),
- })
+ let process = await spawn(
+ 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch',
+ {
+ cwd: path.join(root, 'project-a'),
+ },
+ )
+ await process.onStderr((m) => m.includes('Done in'))
// Changes to project-a should not be included in the output, we changed the
// base folder to project-b.
@@ -962,7 +967,7 @@ test(
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm tailwindcss --input index.css --output dist/out.css')
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
diff --git a/integrations/package.json b/integrations/package.json
index 900679dc5f68..8023842cd36a 100644
--- a/integrations/package.json
+++ b/integrations/package.json
@@ -4,7 +4,6 @@
"private": true,
"devDependencies": {
"dedent": "1.5.3",
- "fast-glob": "^3.3.2",
- "kill-port": "^2.0.1"
+ "fast-glob": "^3.3.2"
}
}
diff --git a/integrations/postcss/config.test.ts b/integrations/postcss/config.test.ts
index 30da4219d9ab..11a3c4bd6e1c 100644
--- a/integrations/postcss/config.test.ts
+++ b/integrations/postcss/config.test.ts
@@ -148,7 +148,8 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+ let process = await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+ await process.onStderr((m) => m.includes('Waiting for file changes'))
await fs.expectFileToContain('dist/out.css', [
//
@@ -218,7 +219,8 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+ let process = await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+ await process.onStderr((m) => m.includes('Waiting for file changes'))
await fs.expectFileToContain('dist/out.css', [
//
diff --git a/integrations/postcss/core-as-postcss-plugin.test.ts b/integrations/postcss/core-as-postcss-plugin.test.ts
index 85779e7b26f8..17545fea32d8 100644
--- a/integrations/postcss/core-as-postcss-plugin.test.ts
+++ b/integrations/postcss/core-as-postcss-plugin.test.ts
@@ -1,4 +1,4 @@
-import { expect } from 'vitest'
+import { describe } from 'vitest'
import { css, js, json, test } from '../utils'
const variantConfig = {
@@ -29,9 +29,9 @@ const variantConfig = {
},
}
-for (let variant of Object.keys(variantConfig)) {
+describe.each(Object.keys(variantConfig))('%s', (variant) => {
test(
- `can not use \`tailwindcss\` as a postcss module (${variant})`,
+ `can not use \`tailwindcss\` as a postcss module`,
{
fs: {
...variantConfig[variant],
@@ -47,7 +47,7 @@ for (let variant of Object.keys(variantConfig)) {
'src/index.css': css`@import 'tailwindcss';`,
},
},
- async ({ exec }) => {
+ async ({ exec, expect }) => {
expect(
exec('pnpm postcss src/index.css --output dist/out.css', undefined, { ignoreStdErr: true }),
).rejects.toThrowError(
@@ -55,4 +55,4 @@ for (let variant of Object.keys(variantConfig)) {
)
},
)
-}
+})
diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts
index 883bc561560e..4da70ee1f390 100644
--- a/integrations/postcss/index.test.ts
+++ b/integrations/postcss/index.test.ts
@@ -1,6 +1,5 @@
import dedent from 'dedent'
import path from 'node:path'
-import { expect } from 'vitest'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
test(
@@ -724,7 +723,7 @@ test(
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm postcss index.css --output dist/out.css')
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
@@ -954,7 +953,7 @@ test(
`,
},
},
- async ({ fs, exec, spawn, root }) => {
+ async ({ fs, exec, spawn, root, expect }) => {
await exec('pnpm postcss src/index.css --output dist/out.css --verbose', {
cwd: path.join(root, 'project-a'),
})
@@ -1020,7 +1019,6 @@ test(
cwd: path.join(root, 'project-a'),
},
)
-
await process.onStderr((message) => message.includes('Waiting for file changes...'))
// Changes to project-a should not be included in the output, we changed the
@@ -1215,7 +1213,7 @@ test(
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm postcss index.css --output dist/out.css')
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts
index b1472c5284aa..b133a9752840 100644
--- a/integrations/postcss/next.test.ts
+++ b/integrations/postcss/next.test.ts
@@ -1,4 +1,4 @@
-import { expect } from 'vitest'
+import { describe } from 'vitest'
import { candidate, css, fetchStyles, js, json, retryAssertion, test } from '../utils'
test(
@@ -56,7 +56,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm next build')
let files = await fs.glob('.next/static/css/**/*.css')
@@ -70,9 +70,10 @@ test(
])
},
)
-;['turbo', 'webpack'].forEach((bundler) => {
+
+describe.each(['turbo', 'webpack'])('%s', (bundler) => {
test(
- `dev mode (${bundler})`,
+ 'dev mode',
{
fs: {
'package.json': json`
@@ -126,26 +127,35 @@ test(
`,
},
},
- async ({ fs, spawn, getFreePort }) => {
- let port = await getFreePort()
- await spawn(`pnpm next dev ${bundler === 'turbo' ? '--turbo' : ''} --port ${port}`)
+ async ({ fs, spawn, expect }) => {
+ let process = await spawn(`pnpm next dev ${bundler === 'turbo' ? '--turbo' : ''}`)
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)/.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
+
+ await process.onStdout((m) => m.includes('Ready in'))
await retryAssertion(async () => {
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
})
- await retryAssertion(async () => {
- await fs.write(
- 'app/page.js',
- js`
- export default function Page() {
- return
Hello, Next.js!
- }
- `,
- )
+ await fs.write(
+ 'app/page.js',
+ js`
+ export default function Page() {
+ return Hello, Next.js!
+ }
+ `,
+ )
+ await process.onStdout((m) => m.includes('Compiled in'))
- let css = await fetchStyles(port)
+ await retryAssertion(async () => {
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`text-red-500`)
})
diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts
index 42cff58dea40..e656253a9c77 100644
--- a/integrations/upgrade/index.test.ts
+++ b/integrations/upgrade/index.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, css, html, js, json, test, ts } from '../utils'
test(
@@ -26,7 +25,7 @@ test(
'src/fonts.css': css`/* Unrelated CSS file */`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
let output = await exec('npx @tailwindcss/upgrade')
expect(output).toContain('Cannot find any CSS files that reference Tailwind CSS.')
@@ -95,7 +94,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -202,7 +201,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -274,7 +273,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -346,7 +345,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -423,7 +422,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -526,7 +525,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -633,7 +632,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain(
@@ -704,7 +703,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
let packageJsonContent = await fs.read('package.json')
@@ -751,7 +750,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
let packageJsonContent = await fs.read('package.json')
@@ -802,7 +801,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
@@ -877,7 +876,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
@@ -944,7 +943,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(`
@@ -991,7 +990,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(`
@@ -1035,7 +1034,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -1107,7 +1106,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -1218,7 +1217,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -1352,7 +1351,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
let output = await exec('npx @tailwindcss/upgrade --force')
expect(output).toMatch(
@@ -1476,7 +1475,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
@@ -1701,7 +1700,7 @@ test(
`,
},
},
- async ({ exec }) => {
+ async ({ exec, expect }) => {
let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch(
(e) => e.toString(),
)
@@ -1773,7 +1772,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
@@ -1909,7 +1908,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
@@ -2029,7 +2028,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
@@ -2109,7 +2108,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -2193,7 +2192,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -2247,7 +2246,7 @@ test(
'tailwind.config.js': js`module.exports = {}`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
let pkg = JSON.parse(await fs.read('package.json'))
@@ -2326,7 +2325,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
// Files should not be modified
@@ -2425,7 +2424,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
// Files should not be modified
@@ -2532,7 +2531,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade ./src/index.css')
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -2619,7 +2618,7 @@ test(
`,
},
},
- async ({ exec }) => {
+ async ({ exec, expect }) => {
let output = await exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) =>
e.toString(),
)
@@ -2692,7 +2691,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts
index e0173bcf367c..f2643143a124 100644
--- a/integrations/upgrade/js-config.test.ts
+++ b/integrations/upgrade/js-config.test.ts
@@ -1,5 +1,5 @@
import path from 'node:path'
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { css, html, json, test, ts } from '../utils'
test(
@@ -151,7 +151,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(`
@@ -350,7 +350,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -436,7 +436,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
@@ -522,7 +522,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -600,7 +600,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -674,7 +674,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -784,7 +784,7 @@ test(
'project-b/src/index.html': html``,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(`
@@ -882,7 +882,7 @@ test(
'backend/mails/welcome.blade.php': html``,
},
},
- async ({ root, exec, fs }) => {
+ async ({ root, exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade', {
cwd: path.join(root, 'frontend'),
})
@@ -950,7 +950,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1012,7 +1012,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1074,7 +1074,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1113,7 +1113,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1184,7 +1184,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1287,7 +1287,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -1397,7 +1397,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -1499,7 +1499,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
diff --git a/integrations/utils.ts b/integrations/utils.ts
index 9e356c089a43..8f916a3d042f 100644
--- a/integrations/utils.ts
+++ b/integrations/utils.ts
@@ -1,12 +1,11 @@
import dedent from 'dedent'
import fastGlob from 'fast-glob'
-import killPort from 'kill-port'
import { exec, spawn } from 'node:child_process'
import fs from 'node:fs/promises'
-import net from 'node:net'
import { platform, tmpdir } from 'node:os'
import path from 'node:path'
-import { test as defaultTest, expect } from 'vitest'
+import { stripVTControlCharacters } from 'node:util'
+import { test as defaultTest, type ExpectStatic } from 'vitest'
const REPO_ROOT = path.join(__dirname, '..')
const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((name) =>
@@ -35,9 +34,9 @@ interface TestConfig {
}
interface TestContext {
root: string
+ expect: ExpectStatic
exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise
spawn(command: string, options?: ChildProcessOptions): Promise
- getFreePort(): Promise
fs: {
write(filePath: string, content: string): Promise
create(filePaths: string[]): Promise
@@ -54,6 +53,7 @@ interface TestContext {
type TestCallback = (context: TestContext) => Promise | void
interface TestFlags {
only?: boolean
+ skip?: boolean
debug?: boolean
}
@@ -73,11 +73,17 @@ export function test(
name: string,
config: TestConfig,
testCallback: TestCallback,
- { only = false, debug = false }: TestFlags = {},
+ { only = false, skip = false, debug = false }: TestFlags = {},
) {
- return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)(
+ return defaultTest(
name,
- { timeout: TEST_TIMEOUT, retry: process.env.CI ? 2 : 0 },
+ {
+ timeout: TEST_TIMEOUT,
+ retry: process.env.CI ? 2 : 0,
+ only: only || (!process.env.CI && debug),
+ skip,
+ concurrent: true,
+ },
async (options) => {
let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
await fs.mkdir(rootDir, { recursive: true })
@@ -92,6 +98,7 @@ export function test(
let context = {
root,
+ expect: options.expect,
async exec(
command: string,
childProcessOptions: ChildProcessOptions = {},
@@ -155,7 +162,9 @@ export function test(
})
function dispose() {
- child.kill()
+ if (!child.kill()) {
+ child.kill('SIGKILL')
+ }
let timer = setTimeout(
() =>
@@ -199,14 +208,18 @@ export function test(
let content = result.toString()
if (debug || only) console.log(content)
combined.push(['stdout', content])
- stdoutMessages.push(content)
+ for (let line of content.split('\n')) {
+ stdoutMessages.push(stripVTControlCharacters(line))
+ }
notifyNext(stdoutActors, stdoutMessages)
})
child.stderr.on('data', (result) => {
let content = result.toString()
if (debug || only) console.error(content)
combined.push(['stderr', content])
- stderrMessages.push(content)
+ for (let line of content.split('\n')) {
+ stderrMessages.push(stripVTControlCharacters(line))
+ }
notifyNext(stderrActors, stderrMessages)
})
child.on('exit', onExit)
@@ -246,42 +259,6 @@ export function test(
},
}
},
- async getFreePort(): Promise {
- return new Promise((resolve, reject) => {
- let server = net.createServer()
- server.listen(0, () => {
- let address = server.address()
- let port = address === null || typeof address === 'string' ? null : address.port
-
- server.close(() => {
- if (port === null) {
- reject(new Error(`Failed to get a free port: address is ${address}`))
- } else {
- disposables.push(async () => {
- // Wait for 10ms in case the process was just killed
- await new Promise((resolve) => setTimeout(resolve, 10))
-
- // kill-port uses `lsof` on macOS which is expensive and can
- // block for multiple seconds. In order to avoid that for a
- // server that is no longer running, we check if the port is
- // still in use first.
- let isPortTaken = await testIfPortTaken(port)
- if (!isPortTaken) {
- return
- }
-
- try {
- await killPort(port)
- } catch {
- // If the process can not be killed, we can't do anything
- }
- })
- resolve(port)
- }
- })
- })
- })
- },
fs: {
async write(filename: string, content: string | Uint8Array): Promise {
let full = path.join(root, filename)
@@ -374,9 +351,9 @@ export function test(
let fileContent = await this.read(filePath)
for (let content of Array.isArray(contents) ? contents : [contents]) {
if (content instanceof RegExp) {
- expect(fileContent).toMatch(content)
+ options.expect(fileContent).toMatch(content)
} else {
- expect(fileContent).toContain(content)
+ options.expect(fileContent).toContain(content)
}
}
})
@@ -385,7 +362,7 @@ export function test(
return retryAssertion(async () => {
let fileContent = await this.read(filePath)
for (let content of contents) {
- expect(fileContent).not.toContain(content)
+ options.expect(fileContent).not.toContain(content)
}
})
},
@@ -448,6 +425,9 @@ export function test(
test.only = (name: string, config: TestConfig, testCallback: TestCallback) => {
return test(name, config, testCallback, { only: true })
}
+test.skip = (name: string, config: TestConfig, testCallback: TestCallback) => {
+ return test(name, config, testCallback, { skip: true })
+}
test.debug = (name: string, config: TestConfig, testCallback: TestCallback) => {
return test(name, config, testCallback, { debug: true })
}
@@ -462,16 +442,14 @@ async function overwriteVersionsInPackageJson(content: string): Promise
let json = JSON.parse(content)
// Resolve all workspace:^ versions to local tarballs
- ;['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].forEach(
- (key) => {
- let dependencies = json[key] || {}
- for (let dependency in dependencies) {
- if (dependencies[dependency] === 'workspace:^') {
- dependencies[dependency] = resolveVersion(dependency)
- }
+ for (let key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
+ let dependencies = json[key] || {}
+ for (let dependency in dependencies) {
+ if (dependencies[dependency] === 'workspace:^') {
+ dependencies[dependency] = resolveVersion(dependency)
}
- },
- )
+ }
+ }
// Inject transitive dependency overwrite. This is necessary because
// @tailwindcss/vite internally depends on a specific version of
@@ -505,25 +483,6 @@ export function stripTailwindComment(content: string) {
return content.replace(/\/\*! tailwindcss .*? \*\//g, '').trim()
}
-function testIfPortTaken(port: number): Promise {
- return new Promise((resolve) => {
- let client = new net.Socket()
- client.once('connect', () => {
- resolve(true)
- client.end()
- })
- client.once('error', (error: any) => {
- if (error.code !== 'ECONNREFUSED') {
- resolve(true)
- } else {
- resolve(false)
- }
- client.end()
- })
- client.connect({ port: port, host: 'localhost' })
- })
-}
-
export let svg = dedent
export let css = dedent
export let html = dedent
@@ -642,8 +601,12 @@ export async function retryAssertion(
throw error
}
-export async function fetchStyles(port: number, path = '/'): Promise {
- let index = await fetch(`http://localhost:${port}${path}`)
+export async function fetchStyles(base: string, path = '/'): Promise {
+ while (base.endsWith('/')) {
+ base = base.slice(0, -1)
+ }
+
+ let index = await fetch(`${base}${path}`)
let html = await index.text()
let linkRegex = / {
stylesheets.push(
...(await Promise.all(
paths.map(async (path) => {
- let css = await fetch(`http://localhost:${port}${path}`, {
+ let css = await fetch(`${base}${path}`, {
headers: {
Accept: 'text/css',
},
diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts
index 188d6e9f4d80..5b7407e4714b 100644
--- a/integrations/vite/astro.test.ts
+++ b/integrations/vite/astro.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
test(
@@ -35,12 +34,21 @@ test(
`,
},
},
- async ({ fs, spawn, getFreePort }) => {
- let port = await getFreePort()
- await spawn(`pnpm astro dev --port ${port}`)
+ async ({ fs, spawn, expect }) => {
+ let process = await spawn('pnpm astro dev')
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
+
+ await process.onStdout((m) => m.includes('watching for file changes'))
await retryAssertion(async () => {
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
})
@@ -56,7 +64,7 @@ test(
`,
)
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`font-bold`)
})
diff --git a/integrations/vite/config.test.ts b/integrations/vite/config.test.ts
index 3e0401f9e2a9..3bf560e074fb 100644
--- a/integrations/vite/config.test.ts
+++ b/integrations/vite/config.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
test(
@@ -51,7 +50,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
@@ -115,7 +114,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
@@ -181,12 +180,19 @@ test(
`,
},
},
- async ({ fs, getFreePort, spawn }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ async ({ fs, spawn, expect }) => {
+ let process = await spawn('pnpm vite dev')
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
await retryAssertion(async () => {
- let css = await fetchStyles(port, '/index.html')
+ let css = await fetchStyles(url, '/index.html')
expect(css).toContain(candidate`text-primary`)
expect(css).toContain('color: blue')
})
@@ -194,7 +200,7 @@ test(
await retryAssertion(async () => {
await fs.write('my-color.cjs', js`module.exports = 'red'`)
- let css = await fetchStyles(port, '/index.html')
+ let css = await fetchStyles(url, '/index.html')
expect(css).toContain(candidate`text-primary`)
expect(css).toContain('color: red')
})
@@ -253,12 +259,19 @@ test(
`,
},
},
- async ({ fs, getFreePort, spawn }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ async ({ fs, spawn, expect }) => {
+ let process = await spawn('pnpm vite dev')
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
await retryAssertion(async () => {
- let css = await fetchStyles(port, '/index.html')
+ let css = await fetchStyles(url, '/index.html')
expect(css).toContain(candidate`text-primary`)
expect(css).toContain('color: blue')
})
@@ -266,7 +279,7 @@ test(
await retryAssertion(async () => {
await fs.write('my-color.mjs', js`export default 'red'`)
- let css = await fetchStyles(port, '/index.html')
+ let css = await fetchStyles(url, '/index.html')
expect(css).toContain(candidate`text-primary`)
expect(css).toContain('color: red')
})
diff --git a/integrations/vite/css-modules.test.ts b/integrations/vite/css-modules.test.ts
index 31a4456a52c8..668281cb494e 100644
--- a/integrations/vite/css-modules.test.ts
+++ b/integrations/vite/css-modules.test.ts
@@ -1,67 +1,65 @@
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { css, html, test, ts, txt } from '../utils'
-for (let transformer of ['postcss', 'lightningcss']) {
- describe(transformer, () => {
- test(
- `dev mode`,
- {
- fs: {
- 'package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^6"
- }
+describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
+ test(
+ `dev mode`,
+ {
+ fs: {
+ 'package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^6"
}
- `,
- 'vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- })
- `,
- 'index.html': html`
-
-
-
-
-
-
- `,
- 'src/component.ts': ts`
- import { foo } from './component.module.css'
- let root = document.getElementById('root')
- root.className = foo
- root.innerText = 'Hello, world!'
- `,
- 'src/component.module.css': css`
- @import 'tailwindcss/utilities';
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'index.html': html`
+
+
+
+
+
+
+ `,
+ 'src/component.ts': ts`
+ import { foo } from './component.module.css'
+ let root = document.getElementById('root')
+ root.className = foo
+ root.innerText = 'Hello, world!'
+ `,
+ 'src/component.module.css': css`
+ @import 'tailwindcss/utilities';
- .foo {
- @apply underline;
- }
- `,
- },
+ .foo {
+ @apply underline;
+ }
+ `,
},
- async ({ exec, fs }) => {
- await exec(`pnpm vite build`)
+ },
+ async ({ exec, fs, expect }) => {
+ await exec(`pnpm vite build`)
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [filename] = files[0]
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [filename] = files[0]
- await fs.expectFileToContain(filename, [/text-decoration-line: underline;/gi])
- },
- )
- })
-}
+ await fs.expectFileToContain(filename, [/text-decoration-line: underline;/gi])
+ },
+ )
+})
diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts
index 80a2a50c4578..eebe11127a05 100644
--- a/integrations/vite/index.test.ts
+++ b/integrations/vite/index.test.ts
@@ -1,5 +1,5 @@
import path from 'node:path'
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import {
candidate,
css,
@@ -14,454 +14,365 @@ import {
yaml,
} from '../utils'
-for (let transformer of ['postcss', 'lightningcss']) {
- describe(transformer, () => {
- test(
- `production build`,
- {
- fs: {
- 'package.json': json`{}`,
- 'pnpm-workspace.yaml': yaml`
- #
- packages:
- - project-a
- `,
- 'project-a/package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^6"
- }
- }
- `,
- 'project-a/vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
-
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- })
- `,
- 'project-a/index.html': html`
-
-
-
-
- Hello, world!
-
- `,
- 'project-a/tailwind.config.js': js`
- export default {
- content: ['../project-b/src/**/*.js'],
+describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
+ test(
+ `production build`,
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'pnpm-workspace.yaml': yaml`
+ #
+ packages:
+ - project-a
+ `,
+ 'project-a/package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^6"
}
- `,
- 'project-a/src/index.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- @config '../tailwind.config.js';
- @source '../../project-b/src/**/*.html';
- `,
- 'project-b/src/index.html': html`
-
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- },
- },
- async ({ root, fs, exec }) => {
- await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
-
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(1)
- let [filename] = files[0]
-
- await fs.expectFileToContain(filename, [
- candidate`underline`,
- candidate`m-2`,
- candidate`flex`,
- candidate`content-['project-b/src/index.js']`,
- ])
+ }
+ `,
+ 'project-a/vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'project-a/index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'project-a/tailwind.config.js': js`
+ export default {
+ content: ['../project-b/src/**/*.js'],
+ }
+ `,
+ 'project-a/src/index.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ @config '../tailwind.config.js';
+ @source '../../project-b/src/**/*.html';
+ `,
+ 'project-b/src/index.html': html`
+
+ `,
+ 'project-b/src/index.js': js`
+ const className = "content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
},
- )
-
- test(
- `dev mode`,
- {
- fs: {
- 'package.json': json`{}`,
- 'pnpm-workspace.yaml': yaml`
- #
- packages:
- - project-a
- `,
- 'project-a/package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^6"
- }
+ },
+ async ({ root, fs, exec, expect }) => {
+ await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
+
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [filename] = files[0]
+
+ await fs.expectFileToContain(filename, [
+ candidate`underline`,
+ candidate`m-2`,
+ candidate`flex`,
+ candidate`content-['project-b/src/index.js']`,
+ ])
+ },
+ )
+
+ test(
+ 'dev mode',
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'pnpm-workspace.yaml': yaml`
+ #
+ packages:
+ - project-a
+ `,
+ 'project-a/package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^6"
}
- `,
- 'project-a/vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
-
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- })
- `,
- 'project-a/index.html': html`
-
-
-
-
- Hello, world!
-
- `,
- 'project-a/about.html': html`
+ }
+ `,
+ 'project-a/vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'project-a/index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'project-a/about.html': html`
+
+
+
+
+ Tailwind Labs
+
+ `,
+ 'project-a/tailwind.config.js': js`
+ export default {
+ content: ['../project-b/src/**/*.js'],
+ }
+ `,
+ 'project-a/src/index.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ @config '../tailwind.config.js';
+ @source '../../project-b/src/**/*.html';
+ `,
+ 'project-b/src/index.html': html`
+
+ `,
+ 'project-b/src/index.js': js`
+ const className = "content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
+ },
+ },
+ async ({ root, spawn, fs, expect }) => {
+ let process = await spawn('pnpm vite dev', {
+ cwd: path.join(root, 'project-a'),
+ })
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
+
+ // Candidates are resolved lazily, so the first visit of index.html
+ // will only have candidates from this file.
+ await retryAssertion(async () => {
+ let styles = await fetchStyles(url, '/index.html')
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`flex`)
+ expect(styles).not.toContain(candidate`font-bold`)
+ })
+
+ // Going to about.html will extend the candidate list to include
+ // candidates from about.html.
+ await retryAssertion(async () => {
+ let styles = await fetchStyles(url, '/about.html')
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`flex`)
+ expect(styles).toContain(candidate`font-bold`)
+ })
+
+ await retryAssertion(async () => {
+ // Updates are additive and cause new candidates to be added.
+ await fs.write(
+ 'project-a/index.html',
+ html`
- Tailwind Labs
+ Hello, world!
`,
- 'project-a/tailwind.config.js': js`
- export default {
- content: ['../project-b/src/**/*.js'],
- }
- `,
- 'project-a/src/index.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- @config '../tailwind.config.js';
- @source '../../project-b/src/**/*.html';
- `,
- 'project-b/src/index.html': html`
-
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
+ )
+
+ let styles = await fetchStyles(url)
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`flex`)
+ expect(styles).toContain(candidate`font-bold`)
+ expect(styles).toContain(candidate`m-2`)
+ })
+
+ await retryAssertion(async () => {
+ // Manually added `@source`s are watched and trigger a rebuild
+ await fs.write(
+ 'project-b/src/index.js',
+ js`
+ const className = "[.changed_&]:content-['project-b/src/index.js']"
module.exports = { className }
`,
- },
- },
- async ({ root, spawn, getFreePort, fs }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`, {
- cwd: path.join(root, 'project-a'),
- })
-
- // Candidates are resolved lazily, so the first visit of index.html
- // will only have candidates from this file.
- await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/index.html')
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`flex`)
- expect(styles).not.toContain(candidate`font-bold`)
- })
-
- // Going to about.html will extend the candidate list to include
- // candidates from about.html.
- await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/about.html')
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`flex`)
- expect(styles).toContain(candidate`font-bold`)
- })
-
- await retryAssertion(async () => {
- // Updates are additive and cause new candidates to be added.
- await fs.write(
- 'project-a/index.html',
- html`
-
-
-
-
- Hello, world!
-
- `,
- )
-
- let styles = await fetchStyles(port)
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`flex`)
- expect(styles).toContain(candidate`font-bold`)
- expect(styles).toContain(candidate`m-2`)
- })
-
- await retryAssertion(async () => {
- // Manually added `@source`s are watched and trigger a rebuild
- await fs.write(
- 'project-b/src/index.js',
- js`
- const className = "[.changed_&]:content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- )
-
- let styles = await fetchStyles(port)
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`flex`)
- expect(styles).toContain(candidate`font-bold`)
- expect(styles).toContain(candidate`m-2`)
- expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
- })
+ )
+
+ let styles = await fetchStyles(url)
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`flex`)
+ expect(styles).toContain(candidate`font-bold`)
+ expect(styles).toContain(candidate`m-2`)
+ expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
+ })
+
+ await retryAssertion(async () => {
+ // After updates to the CSS file, all previous candidates should still be in
+ // the generated CSS
+ await fs.write(
+ 'project-a/src/index.css',
+ css`
+ ${await fs.read('project-a/src/index.css')}
- await retryAssertion(async () => {
- // After updates to the CSS file, all previous candidates should still be in
- // the generated CSS
- await fs.write(
- 'project-a/src/index.css',
- css`
- ${await fs.read('project-a/src/index.css')}
-
- .red {
- color: red;
- }
- `,
- )
-
- let styles = await fetchStyles(port)
- expect(styles).toContain(candidate`red`)
- expect(styles).toContain(candidate`flex`)
- expect(styles).toContain(candidate`m-2`)
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
- expect(styles).toContain(candidate`font-bold`)
- })
- },
- )
-
- test(
- 'watch mode',
- {
- fs: {
- 'package.json': json`{}`,
- 'pnpm-workspace.yaml': yaml`
- #
- packages:
- - project-a
- `,
- 'project-a/package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^6"
- }
+ .red {
+ color: red;
}
`,
- 'project-a/vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
-
- export default defineConfig({
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- })
- `,
- 'project-a/index.html': html`
-
-
-
-
- Hello, world!
-
- `,
- 'project-a/tailwind.config.js': js`
- export default {
- content: ['../project-b/src/**/*.js'],
- }
- `,
- 'project-a/src/index.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- @import './custom-theme.css';
- @config '../tailwind.config.js';
- @source '../../project-b/src/**/*.html';
- `,
- 'project-a/src/custom-theme.css': css`
- /* Will be overwritten later */
- @theme {
- --color-primary: black;
+ )
+
+ let styles = await fetchStyles(url)
+ expect(styles).toContain(candidate`red`)
+ expect(styles).toContain(candidate`flex`)
+ expect(styles).toContain(candidate`m-2`)
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
+ expect(styles).toContain(candidate`font-bold`)
+ })
+ },
+ )
+
+ test(
+ 'watch mode',
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'pnpm-workspace.yaml': yaml`
+ #
+ packages:
+ - project-a
+ `,
+ 'project-a/package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^6"
}
- `,
- 'project-b/src/index.html': html`
-
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- },
+ }
+ `,
+ 'project-a/vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'project-a/index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'project-a/tailwind.config.js': js`
+ export default {
+ content: ['../project-b/src/**/*.js'],
+ }
+ `,
+ 'project-a/src/index.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ @import './custom-theme.css';
+ @config '../tailwind.config.js';
+ @source '../../project-b/src/**/*.html';
+ `,
+ 'project-a/src/custom-theme.css': css`
+ /* Will be overwritten later */
+ @theme {
+ --color-primary: black;
+ }
+ `,
+ 'project-b/src/index.html': html`
+
+ `,
+ 'project-b/src/index.js': js`
+ const className = "content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
},
- async ({ root, spawn, fs }) => {
- await spawn(`pnpm vite build --watch`, {
- cwd: path.join(root, 'project-a'),
- })
-
- let filename = ''
- await retryAssertion(async () => {
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(1)
- filename = files[0][0]
- })
+ },
+ async ({ root, spawn, fs, expect }) => {
+ let process = await spawn('pnpm vite build --watch', {
+ cwd: path.join(root, 'project-a'),
+ })
+ await process.onStdout((m) => m.includes('built in'))
+
+ let filename = ''
+ await retryAssertion(async () => {
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(1)
+ filename = files[0][0]
+ })
+
+ await fs.expectFileToContain(filename, [
+ candidate`underline`,
+ candidate`flex`,
+ css`
+ .text-primary {
+ color: var(--color-primary);
+ }
+ `,
+ ])
- await fs.expectFileToContain(filename, [
- candidate`underline`,
- candidate`flex`,
+ await retryAssertion(async () => {
+ await fs.write(
+ 'project-a/src/custom-theme.css',
css`
- .text-primary {
- color: var(--color-primary);
+ /* Overriding the primary color */
+ @theme {
+ --color-primary: red;
}
`,
- ])
-
- await retryAssertion(async () => {
- await fs.write(
- 'project-a/src/custom-theme.css',
- css`
- /* Overriding the primary color */
- @theme {
- --color-primary: red;
- }
- `,
- )
-
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(1)
- let [, styles] = files[0]
-
- expect(styles).toContain(css`
- .text-primary {
- color: var(--color-primary);
- }
- `)
- })
+ )
- await retryAssertion(async () => {
- // Updates are additive and cause new candidates to be added.
- await fs.write(
- 'project-a/index.html',
- html`
-
-
-
-
- Hello, world!
-
- `,
- )
-
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(1)
- let [, styles] = files[0]
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`flex`)
- expect(styles).toContain(candidate`m-2`)
- })
-
- await retryAssertion(async () => {
- // Manually added `@source`s are watched and trigger a rebuild
- await fs.write(
- 'project-b/src/index.js',
- js`
- const className = "[.changed_&]:content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- )
-
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(1)
- let [, styles] = files[0]
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`flex`)
- expect(styles).toContain(candidate`m-2`)
- expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
- })
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [, styles] = files[0]
- await retryAssertion(async () => {
- // After updates to the CSS file, all previous candidates should still be in
- // the generated CSS
- await fs.write(
- 'project-a/src/index.css',
- css`
- ${await fs.read('project-a/src/index.css')}
-
- .red {
- color: red;
- }
- `,
- )
-
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(1)
- let [, styles] = files[0]
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`flex`)
- expect(styles).toContain(candidate`m-2`)
- expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
- expect(styles).toContain(candidate`red`)
- })
- },
- )
-
- test(
- `source(none) disables looking at the module graph`,
- {
- fs: {
- 'package.json': json`{}`,
- 'pnpm-workspace.yaml': yaml`
- #
- packages:
- - project-a
- `,
- 'project-a/package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^6"
- }
- }
- `,
- 'project-a/vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
-
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- })
- `,
- 'project-a/index.html': html`
+ expect(styles).toContain(css`
+ .text-primary {
+ color: var(--color-primary);
+ }
+ `)
+ })
+
+ await retryAssertion(async () => {
+ // Updates are additive and cause new candidates to be added.
+ await fs.write(
+ 'project-a/index.html',
+ html`
@@ -469,225 +380,320 @@ for (let transformer of ['postcss', 'lightningcss']) {
Hello, world!
- Hello, world!
-
-
+ Hello, world!
+
- Hello, world!
-
-
+ Hello, world!
+
+
+ Hello, world!
+
+
`,
- 'project-a/src/index.css': css`
- @import 'tailwindcss' source(none);
- @source '../../project-b/src/**/*.html';
- `,
- 'project-b/src/index.html': html`
-
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
+ )
+
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [, styles] = files[0]
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`flex`)
+ expect(styles).toContain(candidate`m-2`)
+ })
+
+ await retryAssertion(async () => {
+ // Manually added `@source`s are watched and trigger a rebuild
+ await fs.write(
+ 'project-b/src/index.js',
+ js`
+ const className = "[.changed_&]:content-['project-b/src/index.js']"
module.exports = { className }
`,
- },
- },
- async ({ root, fs, exec }) => {
- await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
+ )
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
- let [filename] = files[0]
-
- // `underline` and `m-2` are only present from files in the module graph
- // which we've explicitly disabled with source(none) so they should not
- // be present
- await fs.expectFileNotToContain(filename, [
- //
- candidate`underline`,
- candidate`m-2`,
- ])
-
- // The files from `project-b` should be included because there is an
- // explicit `@source` directive for it
- await fs.expectFileToContain(filename, [
- //
- candidate`flex`,
- ])
-
- // The explicit source directive only covers HTML files, so the JS file
- // should not be included
- await fs.expectFileNotToContain(filename, [
- //
- candidate`content-['project-b/src/index.js']`,
- ])
- },
- )
-
- test(
- `source("…") filters the module graph`,
- {
- fs: {
- 'package.json': json`{}`,
- 'pnpm-workspace.yaml': yaml`
- #
- packages:
- - project-a
- `,
- 'project-a/package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^6"
- }
+ let [, styles] = files[0]
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`flex`)
+ expect(styles).toContain(candidate`m-2`)
+ expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
+ })
+
+ await retryAssertion(async () => {
+ // After updates to the CSS file, all previous candidates should still be in
+ // the generated CSS
+ await fs.write(
+ 'project-a/src/index.css',
+ css`
+ ${await fs.read('project-a/src/index.css')}
+
+ .red {
+ color: red;
}
`,
- 'project-a/vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
-
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- })
- `,
- 'project-a/index.html': html`
-
-
-
-
- `,
- 'project-a/app/index.js': js`
- const className = "content-['project-a/app/index.js']"
- export default { className }
- `,
- 'project-a/src/index.css': css`
- @import 'tailwindcss' source('../app');
- @source '../../project-b/src/**/*.html';
- `,
- 'project-b/src/index.html': html`
-
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- },
- },
- async ({ root, fs, exec }) => {
- await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
+ )
let files = await fs.glob('project-a/dist/**/*.css')
expect(files).toHaveLength(1)
- let [filename] = files[0]
-
- // `underline` and `m-2` are present in files in the module graph but
- // we've filtered the module graph such that we only look in
- // `./app/**/*` so they should not be present
- await fs.expectFileNotToContain(filename, [
- //
- candidate`underline`,
- candidate`m-2`,
- candidate`content-['project-a/index.html']`,
- ])
-
- // We've filtered the module graph to only look in ./app/**/* so the
- // candidates from that project should be present
- await fs.expectFileToContain(filename, [
- //
- candidate`content-['project-a/app/index.js']`,
- ])
-
- // Even through we're filtering the module graph explicit sources are
- // additive and as such files from `project-b` should be included
- // because there is an explicit `@source` directive for it
- await fs.expectFileToContain(filename, [
- //
- candidate`content-['project-b/src/index.html']`,
- ])
-
- // The explicit source directive only covers HTML files, so the JS file
- // should not be included
- await fs.expectFileNotToContain(filename, [
- //
- candidate`content-['project-b/src/index.js']`,
- ])
+ let [, styles] = files[0]
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`flex`)
+ expect(styles).toContain(candidate`m-2`)
+ expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`)
+ expect(styles).toContain(candidate`red`)
+ })
+ },
+ )
+
+ test(
+ `source(none) disables looking at the module graph`,
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'pnpm-workspace.yaml': yaml`
+ #
+ packages:
+ - project-a
+ `,
+ 'project-a/package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^6"
+ }
+ }
+ `,
+ 'project-a/vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'project-a/index.html': html`
+
+
+
+
+ `,
+ 'project-a/src/index.css': css`
+ @import 'tailwindcss' source(none);
+ @source '../../project-b/src/**/*.html';
+ `,
+ 'project-b/src/index.html': html`
+
+ `,
+ 'project-b/src/index.js': js`
+ const className = "content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
},
- )
-
- test(
- `source("…") must be a directory`,
- {
- fs: {
- 'package.json': json`{}`,
- 'pnpm-workspace.yaml': yaml`
- #
- packages:
- - project-a
- `,
- 'project-a/package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^6"
- }
+ },
+ async ({ root, fs, exec, expect }) => {
+ await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
+
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [filename] = files[0]
+
+ // `underline` and `m-2` are only present from files in the module graph
+ // which we've explicitly disabled with source(none) so they should not
+ // be present
+ await fs.expectFileNotToContain(filename, [
+ //
+ candidate`underline`,
+ candidate`m-2`,
+ ])
+
+ // The files from `project-b` should be included because there is an
+ // explicit `@source` directive for it
+ await fs.expectFileToContain(filename, [
+ //
+ candidate`flex`,
+ ])
+
+ // The explicit source directive only covers HTML files, so the JS file
+ // should not be included
+ await fs.expectFileNotToContain(filename, [
+ //
+ candidate`content-['project-b/src/index.js']`,
+ ])
+ },
+ )
+
+ test(
+ `source("…") filters the module graph`,
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'pnpm-workspace.yaml': yaml`
+ #
+ packages:
+ - project-a
+ `,
+ 'project-a/package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^6"
}
- `,
- 'project-a/vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
-
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- })
- `,
- 'project-a/index.html': html`
-
-
-
-
- `,
- 'project-a/app/index.js': js`
- const className = "content-['project-a/app/index.js']"
- export default { className }
- `,
- 'project-a/src/index.css': css`
- @import 'tailwindcss' source('../i-do-not-exist');
- @source '../../project-b/src/**/*.html';
- `,
- 'project-b/src/index.html': html`
-
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- },
+ }
+ `,
+ 'project-a/vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'project-a/index.html': html`
+
+
+
+
+ `,
+ 'project-a/app/index.js': js`
+ const className = "content-['project-a/app/index.js']"
+ export default { className }
+ `,
+ 'project-a/src/index.css': css`
+ @import 'tailwindcss' source('../app');
+ @source '../../project-b/src/**/*.html';
+ `,
+ 'project-b/src/index.html': html`
+
+ `,
+ 'project-b/src/index.js': js`
+ const className = "content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
},
- async ({ root, fs, exec }) => {
- await expect(() =>
- exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }),
- ).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist')
+ },
+ async ({ root, fs, exec, expect }) => {
+ await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
+
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [filename] = files[0]
+
+ // `underline` and `m-2` are present in files in the module graph but
+ // we've filtered the module graph such that we only look in
+ // `./app/**/*` so they should not be present
+ await fs.expectFileNotToContain(filename, [
+ //
+ candidate`underline`,
+ candidate`m-2`,
+ candidate`content-['project-a/index.html']`,
+ ])
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(0)
+ // We've filtered the module graph to only look in ./app/**/* so the
+ // candidates from that project should be present
+ await fs.expectFileToContain(filename, [
+ //
+ candidate`content-['project-a/app/index.js']`,
+ ])
+
+ // Even through we're filtering the module graph explicit sources are
+ // additive and as such files from `project-b` should be included
+ // because there is an explicit `@source` directive for it
+ await fs.expectFileToContain(filename, [
+ //
+ candidate`content-['project-b/src/index.html']`,
+ ])
+
+ // The explicit source directive only covers HTML files, so the JS file
+ // should not be included
+ await fs.expectFileNotToContain(filename, [
+ //
+ candidate`content-['project-b/src/index.js']`,
+ ])
+ },
+ )
+
+ test(
+ `source("…") must be a directory`,
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'pnpm-workspace.yaml': yaml`
+ #
+ packages:
+ - project-a
+ `,
+ 'project-a/package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^6"
+ }
+ }
+ `,
+ 'project-a/vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'project-a/index.html': html`
+
+
+
+
+ `,
+ 'project-a/app/index.js': js`
+ const className = "content-['project-a/app/index.js']"
+ export default { className }
+ `,
+ 'project-a/src/index.css': css`
+ @import 'tailwindcss' source('../i-do-not-exist');
+ @source '../../project-b/src/**/*.html';
+ `,
+ 'project-b/src/index.html': html`
+
+ `,
+ 'project-b/src/index.js': js`
+ const className = "content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
},
- )
- })
-}
+ },
+ async ({ root, fs, exec, expect }) => {
+ await expect(() =>
+ exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }),
+ ).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist')
+
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(0)
+ },
+ )
+})
test(
`demote Tailwind roots to regular CSS files and back to Tailwind roots while restoring all candidates`,
@@ -733,14 +739,21 @@ test(
'src/index.css': css`@import 'tailwindcss';`,
},
},
- async ({ spawn, getFreePort, fs }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ async ({ spawn, fs, expect }) => {
+ let process = await spawn('pnpm vite dev')
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
// Candidates are resolved lazily, so the first visit of index.html
// will only have candidates from this file.
await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/index.html')
+ let styles = await fetchStyles(url, '/index.html')
expect(styles).toContain(candidate`underline`)
expect(styles).not.toContain(candidate`font-bold`)
})
@@ -748,7 +761,7 @@ test(
// Going to about.html will extend the candidate list to include
// candidates from about.html.
await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/about.html')
+ let styles = await fetchStyles(url, '/about.html')
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`font-bold`)
})
@@ -757,7 +770,7 @@ test(
// We change the CSS file so it is no longer a valid Tailwind root.
await fs.write('src/index.css', css`@import 'tailwindcss';`)
- let styles = await fetchStyles(port)
+ let styles = await fetchStyles(url)
expect(styles).toContain(candidate`underline`)
expect(styles).toContain(candidate`font-bold`)
})
@@ -801,18 +814,25 @@ test(
'src/index.css': css`@import 'tailwindcss';`,
},
},
- async ({ spawn, getFreePort }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ async ({ spawn, expect }) => {
+ let process = await spawn('pnpm vite dev')
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let baseUrl = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) baseUrl = match[1]
+ return Boolean(baseUrl)
+ })
await retryAssertion(async () => {
// We have to load the .js file first so that the static assets are
// resolved
- await fetch(`http://localhost:${port}/src/index.js`).then((r) => r.text())
+ await fetch(`${baseUrl}/src/index.js`).then((r) => r.text())
let [raw, url] = await Promise.all([
- fetch(`http://localhost:${port}/src/index.css?raw`).then((r) => r.text()),
- fetch(`http://localhost:${port}/src/index.css?url`).then((r) => r.text()),
+ fetch(`${baseUrl}/src/index.css?raw`).then((r) => r.text()),
+ fetch(`${baseUrl}/src/index.css?url`).then((r) => r.text()),
])
expect(firstLine(raw)).toBe(`export default "@import 'tailwindcss';"`)
diff --git a/integrations/vite/multi-root.test.ts b/integrations/vite/multi-root.test.ts
index d2d59f50e4a7..39da43690448 100644
--- a/integrations/vite/multi-root.test.ts
+++ b/integrations/vite/multi-root.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
test(
@@ -65,7 +64,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
@@ -86,7 +85,7 @@ test(
)
test(
- `dev mode`,
+ 'dev mode',
{
fs: {
'package.json': json`
@@ -141,14 +140,21 @@ test(
`,
},
},
- async ({ root, spawn, getFreePort, fs }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ async ({ spawn, expect }) => {
+ let process = await spawn('pnpm vite dev')
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
// Candidates are resolved lazily, so the first visit of index.html
// will only have candidates from this file.
await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/root1.html')
+ let styles = await fetchStyles(url, '/root1.html')
expect(styles).toContain(candidate`one:underline`)
expect(styles).not.toContain(candidate`two:underline`)
})
@@ -156,7 +162,7 @@ test(
// Going to about.html will extend the candidate list to include
// candidates from about.html.
await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/root2.html')
+ let styles = await fetchStyles(url, '/root2.html')
expect(styles).not.toContain(candidate`one:underline`)
expect(styles).toContain(candidate`two:underline`)
})
diff --git a/integrations/vite/nuxt.test.ts b/integrations/vite/nuxt.test.ts
index cfd521e48300..1b834525c923 100644
--- a/integrations/vite/nuxt.test.ts
+++ b/integrations/vite/nuxt.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
const SETUP = {
@@ -37,12 +36,25 @@ const SETUP = {
},
}
-test('dev mode', SETUP, async ({ fs, spawn, getFreePort }) => {
- let port = await getFreePort()
- await spawn(`pnpm nuxt dev --port ${port}`)
+test('dev mode', SETUP, async ({ fs, spawn, expect }) => {
+ let process = await spawn('pnpm nuxt dev', {
+ env: {
+ TEST: 'false', // VERY IMPORTANT OTHERWISE YOU WON'T GET OUTPUT
+ NODE_ENV: 'development',
+ },
+ })
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
+
+ await process.onStdout((m) => m.includes('server warmed up in'))
await retryAssertion(async () => {
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
})
@@ -50,29 +62,36 @@ test('dev mode', SETUP, async ({ fs, spawn, getFreePort }) => {
await fs.write(
'app.vue',
html`
-
+
Hello world!
- `,
+ `,
)
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`font-bold`)
})
})
-test('build', SETUP, async ({ spawn, getFreePort, exec }) => {
- let port = await getFreePort()
- await exec(`pnpm nuxt build`)
- await spawn(`pnpm nuxt preview`, {
+test('build', SETUP, async ({ spawn, exec, expect }) => {
+ await exec('pnpm nuxt build')
+ let process = await spawn('pnpm nuxt preview', {
env: {
- PORT: `${port}`,
+ TEST: 'false',
+ NODE_ENV: 'development',
},
})
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Listening on\s*(http.*)\/?/.exec(m)
+ if (match) url = match[1].replace('http://[::]', 'http://127.0.0.1')
+ return m.includes('Listening on')
+ })
+
await retryAssertion(async () => {
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
})
})
diff --git a/integrations/vite/other-transforms.test.ts b/integrations/vite/other-transforms.test.ts
index a0ee01c19c52..6aef096a7853 100644
--- a/integrations/vite/other-transforms.test.ts
+++ b/integrations/vite/other-transforms.test.ts
@@ -1,5 +1,5 @@
import dedent from 'dedent'
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { css, fetchStyles, html, retryAssertion, test, ts, txt } from '../utils'
function createSetup(transformer: 'postcss' | 'lightningcss') {
@@ -58,115 +58,121 @@ function createSetup(transformer: 'postcss' | 'lightningcss') {
}
}
-for (let transformer of ['postcss', 'lightningcss'] as const) {
- describe(transformer, () => {
- test(`production build`, createSetup(transformer), async ({ fs, exec }) => {
- await exec('pnpm vite build')
+describe.each(['postcss', 'lightningcss'] as const)('%s', (transformer) => {
+ test(`production build`, createSetup(transformer), async ({ fs, exec, expect }) => {
+ await exec('pnpm vite build')
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [filename] = files[0]
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [filename] = files[0]
+
+ await fs.expectFileToContain(filename, [
+ css`
+ .foo {
+ color: blue;
+ }
+ `,
+ // Running the transforms on utilities generated by Tailwind might change in the future
+ dedent`
+ .\[background-color\:red\] {
+ background-color: blue;
+ }
+ `,
+ ])
+ })
+
+ test('dev mode', createSetup(transformer), async ({ spawn, fs, expect }) => {
+ let process = await spawn('pnpm vite dev')
+ await process.onStdout((m) => m.includes('ready in'))
+
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
- await fs.expectFileToContain(filename, [
+ await retryAssertion(async () => {
+ let styles = await fetchStyles(url, '/index.html')
+ expect(styles).toContain(css`
+ .foo {
+ color: blue;
+ }
+ `)
+ // Running the transforms on utilities generated by Tailwind might change in the future
+ expect(styles).toContain(dedent`
+ .\[background-color\:red\] {
+ background-color: blue;
+ }
+ `)
+ })
+
+ await retryAssertion(async () => {
+ await fs.write(
+ 'src/index.css',
css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+
.foo {
- color: blue;
+ background-color: red;
}
`,
- // Running the transforms on utilities generated by Tailwind might change in the future
- dedent`
- .\[background-color\:red\] {
- background-color: blue;
- }
- `,
- ])
+ )
+
+ let styles = await fetchStyles(url)
+ expect(styles).toContain(css`
+ .foo {
+ background-color: blue;
+ }
+ `)
})
+ })
- test(`dev mode`, createSetup(transformer), async ({ spawn, getFreePort, fs }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ test('watch mode', createSetup(transformer), async ({ spawn, fs, expect }) => {
+ let process = await spawn('pnpm vite build --watch')
+ await process.onStdout((m) => m.includes('built in'))
- await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/index.html')
- expect(styles).toContain(css`
- .foo {
- color: blue;
- }
- `)
- // Running the transforms on utilities generated by Tailwind might change in the future
- expect(styles).toContain(dedent`
- .\[background-color\:red\] {
- background-color: blue;
- }
- `)
- })
-
- await retryAssertion(async () => {
- await fs.write(
- 'src/index.css',
- css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
-
- .foo {
- background-color: red;
- }
- `,
- )
-
- let styles = await fetchStyles(port)
- expect(styles).toContain(css`
- .foo {
- background-color: blue;
- }
- `)
- })
- })
+ await retryAssertion(async () => {
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [, styles] = files[0]
- test('watch mode', createSetup(transformer), async ({ spawn, fs }) => {
- await spawn(`pnpm vite build --watch`)
+ expect(styles).toContain(css`
+ .foo {
+ color: blue;
+ }
+ `)
+ // Running the transforms on utilities generated by Tailwind might change in the future
+ expect(styles).toContain(dedent`
+ .\[background-color\:red\] {
+ background-color: blue;
+ }
+ `)
+ })
- await retryAssertion(async () => {
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [, styles] = files[0]
+ await retryAssertion(async () => {
+ await fs.write(
+ 'src/index.css',
+ css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
- expect(styles).toContain(css`
- .foo {
- color: blue;
- }
- `)
- // Running the transforms on utilities generated by Tailwind might change in the future
- expect(styles).toContain(dedent`
- .\[background-color\:red\] {
- background-color: blue;
- }
- `)
- })
-
- await retryAssertion(async () => {
- await fs.write(
- 'src/index.css',
- css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
-
- .foo {
- background-color: red;
- }
- `,
- )
-
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [, styles] = files[0]
-
- expect(styles).toContain(css`
.foo {
- background-color: blue;
+ background-color: red;
}
- `)
- })
+ `,
+ )
+
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [, styles] = files[0]
+
+ expect(styles).toContain(css`
+ .foo {
+ background-color: blue;
+ }
+ `)
})
})
-}
+})
diff --git a/integrations/vite/resolvers.test.ts b/integrations/vite/resolvers.test.ts
index b7098d47fc68..040f843912fd 100644
--- a/integrations/vite/resolvers.test.ts
+++ b/integrations/vite/resolvers.test.ts
@@ -1,143 +1,148 @@
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils'
-for (let transformer of ['postcss', 'lightningcss']) {
- describe(transformer, () => {
- test(
- `resolves aliases in production build`,
- {
- fs: {
- 'package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^5.3.5"
- }
+describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
+ test(
+ 'resolves aliases in production build',
+ {
+ fs: {
+ 'package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^5.3.5"
}
- `,
- 'vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
- import { fileURLToPath } from 'node:url'
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+ import { fileURLToPath } from 'node:url'
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- resolve: {
- alias: {
- '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
- '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
- },
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ resolve: {
+ alias: {
+ '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
+ '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
},
- })
- `,
- 'index.html': html`
-
-
-
-
- Hello, world!
-
- `,
- 'src/index.css': css`
- @import '#css-alias';
- @plugin '#js-alias';
- `,
- 'src/alias.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- `,
- 'src/plugin.js': js`
- export default function ({ addUtilities }) {
- addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
- }
- `,
- },
+ },
+ })
+ `,
+ 'index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/index.css': css`
+ @import '#css-alias';
+ @plugin '#js-alias';
+ `,
+ 'src/alias.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ 'src/plugin.js': js`
+ export default function ({ addUtilities }) {
+ addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
+ }
+ `,
},
- async ({ fs, exec }) => {
- await exec('pnpm vite build')
+ },
+ async ({ fs, exec, expect }) => {
+ await exec('pnpm vite build')
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [filename] = files[0]
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [filename] = files[0]
- await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
- },
- )
+ await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
+ },
+ )
- test(
- `resolves aliases in dev mode`,
- {
- fs: {
- 'package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^5.3.5"
- }
+ test(
+ 'resolves aliases in dev mode',
+ {
+ fs: {
+ 'package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^5.3.5"
}
- `,
- 'vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
- import { fileURLToPath } from 'node:url'
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+ import { fileURLToPath } from 'node:url'
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- resolve: {
- alias: {
- '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
- '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
- },
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ resolve: {
+ alias: {
+ '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
+ '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
},
- })
- `,
- 'index.html': html`
-
-
-
-
- Hello, world!
-
- `,
- 'src/index.css': css`
- @import '#css-alias';
- @plugin '#js-alias';
- `,
- 'src/alias.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- `,
- 'src/plugin.js': js`
- export default function ({ addUtilities }) {
- addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
- }
- `,
- },
+ },
+ })
+ `,
+ 'index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/index.css': css`
+ @import '#css-alias';
+ @plugin '#js-alias';
+ `,
+ 'src/alias.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ 'src/plugin.js': js`
+ export default function ({ addUtilities }) {
+ addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
+ }
+ `,
},
- async ({ root, spawn, getFreePort, fs }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ },
+ async ({ spawn, expect }) => {
+ let process = await spawn('pnpm vite dev')
+ await process.onStdout((m) => m.includes('ready in'))
- await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/index.html')
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`custom-underline`)
- })
- },
- )
- })
-}
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
+
+ await retryAssertion(async () => {
+ let styles = await fetchStyles(url, '/index.html')
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`custom-underline`)
+ })
+ },
+ )
+})
diff --git a/integrations/vite/ssr.test.ts b/integrations/vite/ssr.test.ts
index e66970f8834f..3c3f1cc4beec 100644
--- a/integrations/vite/ssr.test.ts
+++ b/integrations/vite/ssr.test.ts
@@ -1,8 +1,7 @@
-import { expect } from 'vitest'
import { candidate, css, html, json, test, ts } from '../utils'
test(
- `Vite 5`,
+ 'Vite 5',
{
fs: {
'package.json': json`
@@ -55,7 +54,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm vite build --ssr server.ts')
let files = await fs.glob('dist/**/*.css')
@@ -123,7 +122,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm vite build --ssr server.ts')
let files = await fs.glob('dist/**/*.css')
diff --git a/integrations/vite/svelte.test.ts b/integrations/vite/svelte.test.ts
index 3cbfc66fa622..33eca5a13a2f 100644
--- a/integrations/vite/svelte.test.ts
+++ b/integrations/vite/svelte.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, css, html, json, retryAssertion, test, ts } from '../utils'
test(
@@ -93,7 +92,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
@@ -201,8 +200,9 @@ test(
`,
},
},
- async ({ fs, spawn }) => {
- await spawn(`pnpm vite build --watch`)
+ async ({ fs, spawn, expect }) => {
+ let process = await spawn(`pnpm vite build --watch`)
+ await process.onStdout((m) => m.includes('built in'))
await retryAssertion(async () => {
let files = await fs.glob('dist/**/*.css')
diff --git a/integrations/vite/url-rewriting.test.ts b/integrations/vite/url-rewriting.test.ts
index 0e3d0318a0df..a7c6433d4d87 100644
--- a/integrations/vite/url-rewriting.test.ts
+++ b/integrations/vite/url-rewriting.test.ts
@@ -1,97 +1,95 @@
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { binary, css, html, svg, test, ts, txt } from '../utils'
const SIMPLE_IMAGE = `iVBORw0KGgoAAAANSUhEUgAAADAAAAAlAQAAAAAsYlcCAAAACklEQVR4AWMYBQABAwABRUEDtQAAAABJRU5ErkJggg==`
-for (let transformer of ['postcss', 'lightningcss']) {
- describe(transformer, () => {
- test(
- 'can rewrite urls in production builds',
- {
- fs: {
- 'package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "@tailwindcss/vite": "workspace:^",
- "vite": "^6"
- }
+describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
+ test(
+ 'can rewrite urls in production builds',
+ {
+ fs: {
+ 'package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "@tailwindcss/vite": "workspace:^",
+ "vite": "^6"
}
- `,
- 'vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
- export default defineConfig({
- plugins: [tailwindcss()],
- build: {
- assetsInlineLimit: 256,
- cssMinify: false,
- },
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- })
- `,
- 'index.html': html`
-
-
-
-
-
-
-
-
-
-