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!
`, - '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` - - - - -
Hello, world!
- - - `, - '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` + + + + +
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']" + 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` - - - - -
Hello, world!
- - - `, - '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` + + + + +
Hello, world!
+ + + `, + '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` + + + + +
Hello, world!
+ + + `, + '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` -