diff --git a/integrations/vite/input-dependency-race.test.ts b/integrations/vite/input-dependency-race.test.ts new file mode 100644 index 000000000000..b77ecf5f689e --- /dev/null +++ b/integrations/vite/input-dependency-race.test.ts @@ -0,0 +1,100 @@ +import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' + +test( + 'rebuilds when the input stylesheet changes before its dependency stat finishes', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^", + "vite": "^7" + } + } + `, + 'vite.config.ts': ts` + import fs from 'node:fs/promises' + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + let stat = fs.stat + let delayed = false + fs.stat = async (...args) => { + if ( + !delayed && + String(args[0]).endsWith('index.css') && + new Error().stack?.includes('addBuildDependency') + ) { + delayed = true + let result = await stat(...args) + await fs.writeFile('input-stat-pending', 'yes') + while ( + !(await fs.access('release-input-stat').then( + () => true, + () => false, + )) + ) { + await new Promise((resolve) => setTimeout(resolve, 10)) + } + return result + } + return stat(...args) + } + + export default defineConfig({ plugins: [tailwindcss()] }) + `, + 'index.html': html` + +
+ `, + 'src/index.css': css` + @import 'tailwindcss'; + @theme { + --color-brand: red; + } + `, + }, + }, + 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) + }) + + let initialStyles = fetchStyles(url) + await retryAssertion(async () => { + expect(await fs.read('input-stat-pending')).toBe('yes') + }) + + let returnedBeforeStat = await Promise.race([ + initialStyles.then(() => true), + new Promise((resolve) => setTimeout(() => resolve(false), 5000)), + ]) + await fs.write('release-input-stat', 'yes') + expect(returnedBeforeStat).toBe(false) + expect(await initialStyles).toContain('--color-brand: red') + + await fs.write( + 'src/index.css', + css` + @import 'tailwindcss'; + @theme { + --color-brand: blue; + } + `, + ) + + await retryAssertion(async () => { + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`text-brand`) + expect(styles).toContain('--color-brand: blue') + }) + }, +) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 8fc660eaac18..47c5c9d33f9a 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -395,7 +395,7 @@ class Root { clearRequireCache(Array.from(this.buildDependencies.keys())) this.buildDependencies.clear() - this.addBuildDependency(idToPath(inputPath)) + await this.addBuildDependency(idToPath(inputPath)) DEBUG && I.start('Setup compiler') let addBuildDependenciesPromises: Promise[] = []