diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts index c99dc0861..f50b34e58 100644 --- a/integrations/postcss/next.test.ts +++ b/integrations/postcss/next.test.ts @@ -1,5 +1,5 @@ import { expect } from 'vitest' -import { candidate, css, fetchStylesFromIndex, js, json, retryAssertion, test } from '../utils' +import { candidate, css, fetchStyles, js, json, retryAssertion, test } from '../utils' test( 'production build', @@ -131,7 +131,7 @@ test( await spawn(`pnpm next dev ${bundler === 'turbo' ? '--turbo' : ''} --port ${port}`) await retryAssertion(async () => { - let css = await fetchStylesFromIndex(port) + let css = await fetchStyles(port) expect(css).toContain(candidate`underline`) }) @@ -145,7 +145,7 @@ test( ) await retryAssertion(async () => { - let css = await fetchStylesFromIndex(port) + let css = await fetchStyles(port) expect(css).toContain(candidate`underline`) expect(css).toContain(candidate`text-red-500`) }) diff --git a/integrations/utils.ts b/integrations/utils.ts index 65b76c960..144b69e1e 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -262,8 +262,15 @@ export function test( await fs.mkdir(dir, { recursive: true }) await fs.writeFile(full, content) }, - read(filePath: string) { - return fs.readFile(path.resolve(root, filePath), 'utf8') + async read(filePath: string) { + let content = await fs.readFile(path.resolve(root, filePath), 'utf8') + + // Ensure that files read on Windows have \r\n line endings removed + if (IS_WINDOWS) { + content = content.replace(/\r\n/g, '\n') + } + + return content }, async glob(pattern: string) { let files = await fastGlob(pattern, { cwd: root }) @@ -500,8 +507,8 @@ export async function retryAssertion( throw error } -export async function fetchStylesFromIndex(port: number): Promise { - let index = await fetch(`http://localhost:${port}`) +export async function fetchStyles(port: number, path = '/'): Promise { + let index = await fetch(`http://localhost:${port}${path}`) let html = await index.text() let regex = /Hello, world! `, + 'project-a/about.html': html` + + + + +
Tailwind Labs
+ + `, 'project-a/src/index.css': css` @import 'tailwindcss/theme' theme(reference); @import 'tailwindcss/utilities'; @@ -119,15 +138,27 @@ test( }, async ({ root, spawn, getFreePort, fs }) => { let port = await getFreePort() - let process = await spawn(`pnpm vite dev --port ${port}`, { + await spawn(`pnpm vite dev --port ${port}`, { cwd: path.join(root, 'project-a'), }) - await process.onStdout((message) => message.includes('ready in')) + // Candidates are resolved lazily, so the first visit of index.html + // will only have candidates from this file. + await retryAssertion(async () => { + let css = await fetchStyles(port, '/index.html') + expect(css).toContain(candidate`underline`) + expect(css).not.toContain(candidate`font-bold`) + }) - let css = await fetchStylesFromIndex(port) - expect(css).toContain(candidate`underline`) + // Going to about.html will extend the candidate list to include + // candidates from about.html. + await retryAssertion(async () => { + let css = await fetchStyles(port, '/about.html') + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + }) + // Updates are additive and cause new candidates to be added. await fs.write( 'project-a/index.html', html` @@ -139,11 +170,14 @@ test( `, ) - await process.onStdout((message) => message.includes('page reload')) - - css = await fetchStylesFromIndex(port) - expect(css).toContain(candidate`m-2`) + await retryAssertion(async () => { + let css = await fetchStyles(port) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + expect(css).toContain(candidate`m-2`) + }) + // Manually added `@source`s are watched and trigger a rebuild await fs.write( 'project-b/src/index.js', js` @@ -151,9 +185,33 @@ test( module.exports = { className } `, ) - await process.onStdout((message) => message.includes('page reload')) + await retryAssertion(async () => { + let css = await fetchStyles(port) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + expect(css).toContain(candidate`m-2`) + expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + }) - css = await fetchStylesFromIndex(port) - expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + // 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; + } + `, + ) + await retryAssertion(async () => { + let css = await fetchStyles(port) + expect(css).toContain(candidate`red`) + expect(css).toContain(candidate`m-2`) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + expect(css).toContain(candidate`font-bold`) + }) }, ) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 75dea4b76..25d07dbfd 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -11,7 +11,7 @@ export default function tailwindcss(): Plugin[] { let config: ResolvedConfig | null = null let scanner: Scanner | null = null let changedContent: { content: string; extension: string }[] = [] - let candidates: string[] = [] + let candidates = new Set() // In serve mode this is treated as a set — the content doesn't matter. // In build mode, we store file contents to use them in renderChunk. @@ -69,10 +69,9 @@ export default function tailwindcss(): Plugin[] { } // Parse all candidates given the resolved files - let newCandidates = scanner.scanFiles([{ content: src, extension }]) - for (let candidate of newCandidates) { + for (let candidate of scanner.scanFiles([{ content: src, extension }])) { updated = true - candidates.push(candidate) + candidates.add(candidate) } return updated @@ -100,11 +99,13 @@ export default function tailwindcss(): Plugin[] { // This should not be here, but right now the Vite plugin is setup where we // setup a new scanner and compiler every time we request the CSS file // (regardless whether it actually changed or not). - let initialCandidates = scanner.scan() + for (let candidate of scanner.scan()) { + candidates.add(candidate) + } if (changedContent.length > 0) { for (let candidate of scanner.scanFiles(changedContent.splice(0))) { - initialCandidates.push(candidate) + candidates.add(candidate) } } @@ -128,7 +129,7 @@ export default function tailwindcss(): Plugin[] { addWatchFile(path.posix.join(relative, glob.pattern)) } - return build(candidates.splice(0).concat(initialCandidates)) + return build(Array.from(candidates)) } async function generateOptimizedCss(