From d3a7955ee3c29dd3fffb8c18b2ad3c57665a93f5 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 11 Feb 2025 11:25:47 +0100 Subject: [PATCH 01/16] Add tests --- integrations/vite/astro.test.ts | 63 +++++++++++++++-- integrations/vite/index.test.ts | 91 +++++++++---------------- packages/@tailwindcss-vite/src/index.ts | 38 +++++++++-- 3 files changed, 122 insertions(+), 70 deletions(-) diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts index 5b7407e4714b..81db3e82c6d5 100644 --- a/integrations/vite/astro.test.ts +++ b/integrations/vite/astro.test.ts @@ -1,4 +1,4 @@ -import { candidate, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' +import { candidate, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils' test( 'dev mode', @@ -19,11 +19,7 @@ test( import { defineConfig } from 'astro/config' // https://astro.build/config - export default defineConfig({ - vite: { - plugins: [tailwindcss()], - }, - }) + export default defineConfig({ vite: { plugins: [tailwindcss()] } }) `, 'src/pages/index.astro': html`
Hello, world!
@@ -70,3 +66,58 @@ test( }) }, ) + +test( + 'build mode', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "astro": "^4.15.2", + "react": "^19", + "react-dom": "^19", + "@astrojs/react": "^4", + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + } + } + `, + 'astro.config.mjs': ts` + import tailwindcss from '@tailwindcss/vite' + import react from '@astrojs/react' + import { defineConfig } from 'astro/config' + + // https://astro.build/config + export default defineConfig({ vite: { plugins: [tailwindcss()] }, integrations: [react()] }) + `, + 'src/pages/index.astro': html` + --- + import ClientOnly from './client-only'; + --- + +
Hello, world!
+ + + + + `, + 'src/pages/client-only.jsx': js` + export default function ClientOnly() { + return
Hello, world!
+ } + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm astro build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + + await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`overline`]) + }, +) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index c0bee2b878cc..1b22fb7a8f36 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -14,7 +14,12 @@ import { yaml, } from '../utils' -describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { +describe.each([ + ['postcss', 'module-graph'], + ['postcss', 'file-system'], + ['lightningcss', 'module-graph'], + ['lightningcss', 'file-system'], +])('using %s via %s', (transformer, scanner) => { test( `production build`, { @@ -45,7 +50,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss()], + plugins: [tailwindcss({ scanner: '${scanner}' })], }) `, 'project-a/index.html': html` @@ -57,9 +62,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { `, 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } + export default { content: ['../project-b/src/**/*.js'] } `, 'project-a/src/index.css': css` @import 'tailwindcss/theme' theme(reference); @@ -122,7 +125,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss()], + plugins: [tailwindcss({ scanner: '${scanner}' })], }) `, 'project-a/index.html': html` @@ -142,9 +145,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { `, 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } + export default { content: ['../project-b/src/**/*.js'] } `, 'project-a/src/index.css': css` @import 'tailwindcss/theme' theme(reference); @@ -162,9 +163,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { }, }, async ({ root, spawn, fs, expect }) => { - let process = await spawn('pnpm vite dev', { - cwd: path.join(root, 'project-a'), - }) + let process = await spawn('pnpm vite dev', { cwd: path.join(root, 'project-a') }) await process.onStdout((m) => m.includes('ready in')) let url = '' @@ -174,17 +173,19 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { return Boolean(url) }) - // Candidates are resolved lazily, so the first visit of index.html - // will only have candidates from this file. + // Candidates are resolved lazily in module-graph mode, 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`) + + if (scanner === 'module-graph') { + expect(styles).not.toContain(candidate`font-bold`) + } }) - // Going to about.html will extend the candidate list to include - // candidates from about.html. + // 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`) @@ -232,8 +233,8 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { }) await retryAssertion(async () => { - // After updates to the CSS file, all previous candidates should still be in - // the generated CSS + // After updates to the CSS file, all previous candidates should still be in the generated + // stylesheet. await fs.write( 'project-a/src/index.css', css` @@ -283,10 +284,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) + export default defineConfig({ build: { cssMinify: false }, plugins: [tailwindcss()] }) `, 'project-a/index.html': html` @@ -297,9 +295,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { `, 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } + export default { content: ['../project-b/src/**/*.js'] } `, 'project-a/src/index.css': css` @import 'tailwindcss/theme' theme(reference); @@ -324,9 +320,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { }, }, async ({ root, spawn, fs, expect }) => { - let process = await spawn('pnpm vite build --watch', { - cwd: path.join(root, 'project-a'), - }) + let process = await spawn('pnpm vite build --watch', { cwd: path.join(root, 'project-a') }) await process.onStdout((m) => m.includes('built in')) let filename = '' @@ -465,7 +459,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss()], + plugins: [tailwindcss({ scanner: '${scanner}' })], }) `, 'project-a/index.html': html` @@ -551,7 +545,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss()], + plugins: [tailwindcss({ scanner: '${scanner}' })], }) `, 'project-a/index.html': html` @@ -653,7 +647,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss()], + plugins: [tailwindcss({ scanner: '${scanner}' })], }) `, 'project-a/index.html': html` @@ -702,23 +696,15 @@ test( 'package.json': json` { "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^6" - } + "dependencies": { "@tailwindcss/vite": "workspace:^", "tailwindcss": "workspace:^" }, + "devDependencies": { "vite": "^6" } } `, 'vite.config.ts': ts` import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) + export default defineConfig({ build: { cssMinify: false }, plugins: [tailwindcss()] }) `, 'index.html': html` @@ -784,23 +770,15 @@ test( 'package.json': json` { "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - "vite": "^6" - } + "dependencies": { "@tailwindcss/vite": "workspace:^", "tailwindcss": "workspace:^" }, + "devDependencies": { "vite": "^6" } } `, 'vite.config.ts': ts` import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) + export default defineConfig({ build: { cssMinify: false }, plugins: [tailwindcss()] }) `, 'index.html': html` @@ -860,10 +838,7 @@ test( import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) + export default defineConfig({ build: { cssMinify: false }, plugins: [tailwindcss()] }) `, 'index.html': html` diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index ce61fdf9b64c..bcad05a19f2c 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -13,7 +13,13 @@ const INLINE_STYLE_ID_RE = /[?&]index\=\d+\.css$/ const IGNORED_DEPENDENCIES = ['tailwind-merge'] -export default function tailwindcss(): Plugin[] { +type ScannerMode = 'automatic' | 'module-graph' | 'file-system' + +export default function tailwindcss( + { scanner: scannerMode = 'automatic' }: { scanner: ScannerMode } = { + scanner: 'automatic', + }, +): Plugin[] { let servers: ViteDevServer[] = [] let config: ResolvedConfig | null = null @@ -59,13 +65,18 @@ export default function tailwindcss(): Plugin[] { return new Root( id, () => moduleGraphCandidates, - config!.base, + scannerMode, + config!.root, customCssResolver, customJsResolver, ) }) function scanFile(id: string, content: string, extension: string) { + if (scannerMode === 'file-system') { + return + } + for (let dependency of IGNORED_DEPENDENCIES) { // We validated that Vite IDs always use posix style path separators, even on Windows. // In dev build, Vite precompiles dependencies @@ -197,6 +208,11 @@ export default function tailwindcss(): Plugin[] { config = _config minify = config.build.cssMinify !== false isSSR = config.build.ssr !== false && config.build.ssr !== undefined + + if (shouldDisableModuleGraph(config) && scannerMode === 'automatic') { + console.warn('Detected an Astro.js build and opted-out of using the Vite module graph.') + scannerMode = 'file-system' + } }, // Scan all non-CSS files for candidates @@ -416,6 +432,7 @@ class Root { constructor( private id: string, private getSharedCandidates: () => Map>, + private scannerMode: ScannerMode, private base: string, private customCssResolver: (id: string, base: string) => Promise, @@ -458,9 +475,13 @@ class Root { return [] } - // No root specified, use the module graph + // No root specified, use the module graph unless we are in file-based scanner mode if (this.compiler.root === null) { - return [] + if (this.scannerMode === 'file-system') { + return [{ base: this.base, pattern: '**/*' }] + } else { + return [] + } } // Use the specified root @@ -547,8 +568,9 @@ class Root { } private sharedCandidates(): Set { - if (!this.compiler) return new Set() - if (this.compiler.root === 'none') return new Set() + if (!this.compiler || this.scannerMode === 'file-system' || this.compiler.root === 'none') { + return new Set() + } const HAS_DRIVE_LETTER = /^[A-Z]:/ @@ -581,3 +603,7 @@ class Root { return shared } } + +function shouldDisableModuleGraph(config: ResolvedConfig) { + return config.plugins.some((p) => p.name === 'astro:scripts:page-ssr') +} From 3be7f925723c65ef3b5376c86cfc36e52c45fcbb Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 11 Feb 2025 15:40:39 +0100 Subject: [PATCH 02/16] Add more test cases --- integrations/vite/react-router.test.ts | 178 ++++++++++++++++++++++++ integrations/vite/ssr.test.ts | 83 +++++------ packages/@tailwindcss-vite/src/index.ts | 10 +- 3 files changed, 218 insertions(+), 53 deletions(-) create mode 100644 integrations/vite/react-router.test.ts diff --git a/integrations/vite/react-router.test.ts b/integrations/vite/react-router.test.ts new file mode 100644 index 000000000000..436947680099 --- /dev/null +++ b/integrations/vite/react-router.test.ts @@ -0,0 +1,178 @@ +import { candidate, css, fetchStyles, json, retryAssertion, test, ts, txt } from '../utils' + +const WORKSPACE = { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@react-router/dev": "^7", + "@react-router/node": "^7", + "@react-router/serve": "^7", + "@tailwindcss/vite": "workspace:^", + "@types/node": "^20", + "@types/react-dom": "^19", + "@types/react": "^19", + "isbot": "^5", + "react-dom": "^19", + "react-router": "^7", + "react": "^19", + "tailwindcss": "workspace:^", + "vite": "^5" + } + } + `, + 'react-router.config.ts': ts` + import type { Config } from '@react-router/dev/config' + export default { ssr: true } satisfies Config + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import { reactRouter } from '@react-router/dev/vite' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [tailwindcss(), reactRouter()], + }) + `, + 'app/routes/home.tsx': ts` + export default function Home() { + return

Welcome to React Router

+ } + `, + 'app/app.css': css`@import 'tailwindcss';`, + 'app/routes.ts': ts` + import { type RouteConfig, index } from '@react-router/dev/routes' + export default [index('routes/home.tsx')] satisfies RouteConfig + `, + 'app/root.tsx': ts` + import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' + import './app.css' + export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ) + } + + export default function App() { + return + } + `, +} + +test('dev mode', { fs: WORKSPACE }, async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm react-router dev') + + 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(url) + expect(css).toContain(candidate`font-bold`) + }) + + await retryAssertion(async () => { + await fs.write( + 'app/routes/home.tsx', + ts` + export default function Home() { + return

Welcome to React Router

+ } + `, + ) + + let css = await fetchStyles(url) + expect(css).toContain(candidate`underline`) + expect(css).toContain(candidate`font-bold`) + }) +}) + +test('build mode', { fs: WORKSPACE }, async ({ spawn, exec, expect }) => { + await exec('pnpm react-router build') + let process = await spawn('pnpm react-router-serve ./build/server/index.js') + + let url = '' + await process.onStdout((m) => { + let match = /\[react-router-serve\]\s*(http.*)\ \/?/.exec(m) + if (match) url = match[1] + return url != '' + }) + + await retryAssertion(async () => { + let css = await fetchStyles(url) + expect(css).toContain(candidate`font-bold`) + }) +}) + +test( + 'build mode using ?url stylesheet imports should only build one stylesheet (requires `file-system` scanner)', + { + fs: { + ...WORKSPACE, + 'app/root.tsx': ts` + import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router' + import styles from './app.css?url' + export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: styles }] + export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ) + } + + export default function App() { + return + } + `, + 'vite.config.ts': ts` + import { defineConfig } from 'vite' + import { reactRouter } from '@react-router/dev/vite' + import tailwindcss from '@tailwindcss/vite' + + export default defineConfig({ + plugins: [tailwindcss({ scanner: 'file-system' }), reactRouter()], + }) + `, + '.gitignore': txt` + node_modules/ + build/ + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm react-router build') + + let files = await fs.glob('build/client/assets/**/*.css') + + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [candidate`font-bold`]) + }, +) diff --git a/integrations/vite/ssr.test.ts b/integrations/vite/ssr.test.ts index 3c3f1cc4beec..b0bf92126c23 100644 --- a/integrations/vite/ssr.test.ts +++ b/integrations/vite/ssr.test.ts @@ -1,5 +1,30 @@ import { candidate, css, html, json, test, ts } from '../utils' +const WORKSPACE = { + 'index.html': html` + +
+ + + `, + 'src/index.css': css`@import 'tailwindcss';`, + 'src/index.ts': ts` + import './index.css' + + document.querySelector('#app').innerHTML = \` +
Hello, world!
+ \` + `, + 'server.ts': ts` + import css from './src/index.css?url' + + document.querySelector('#app').innerHTML = \` + +
Hello, world!
+ \` + `, +} + test( 'Vite 5', { @@ -26,32 +51,10 @@ test( cssMinify: false, ssrEmitAssets: true, }, - plugins: [tailwindcss()], - ssr: { resolve: { conditions: [] } }, + plugins: [tailwindcss({ scanner: 'file-system' })], }) `, - 'index.html': html` - -
- - - `, - 'src/index.css': css`@import 'tailwindcss';`, - 'src/index.ts': ts` - import './index.css' - - document.querySelector('#app').innerHTML = \` -
Hello, world!
- \` - `, - 'server.ts': ts` - import css from './src/index.css?url' - - document.querySelector('#app').innerHTML = \` - -
Hello, world!
- \` - `, + ...WORKSPACE, }, }, async ({ fs, exec, expect }) => { @@ -62,9 +65,10 @@ test( let [filename] = files[0] await fs.expectFileToContain(filename, [ - // candidate`underline`, candidate`m-2`, + candidate`overline`, + candidate`m-3`, ]) }, ) @@ -94,32 +98,10 @@ test( cssMinify: false, ssrEmitAssets: true, }, - plugins: [tailwindcss()], - ssr: { resolve: { conditions: [] } }, + plugins: [tailwindcss({ scanner: 'file-system' })], }) `, - 'index.html': html` - -
- - - `, - 'src/index.css': css`@import 'tailwindcss';`, - 'src/index.ts': ts` - import './index.css' - - document.querySelector('#app').innerHTML = \` -
Hello, world!
- \` - `, - 'server.ts': ts` - import css from './src/index.css?url' - - document.querySelector('#app').innerHTML = \` - -
Hello, world!
- \` - `, + ...WORKSPACE, }, }, async ({ fs, exec, expect }) => { @@ -130,9 +112,10 @@ test( let [filename] = files[0] await fs.expectFileToContain(filename, [ - // candidate`underline`, candidate`m-2`, + candidate`overline`, + candidate`m-3`, ]) }, ) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index bcad05a19f2c..2b6266a133ae 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -209,9 +209,13 @@ export default function tailwindcss( minify = config.build.cssMinify !== false isSSR = config.build.ssr !== false && config.build.ssr !== undefined - if (shouldDisableModuleGraph(config) && scannerMode === 'automatic') { - console.warn('Detected an Astro.js build and opted-out of using the Vite module graph.') - scannerMode = 'file-system' + if (scannerMode === 'automatic') { + if (shouldDisableModuleGraph(config)) { + console.warn('Detected an Astro.js build and opted-out of using the Vite module graph.') + scannerMode = 'file-system' + return + } + scannerMode = 'module-graph' } }, From ef5de5b0fc4794b1725d3796c1b9203325f009d0 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 14 Feb 2025 15:18:17 +0100 Subject: [PATCH 03/16] Fix Astro by adding the componentns folder instead of falling to full fs scanning --- integrations/vite/astro.test.ts | 5 +++-- packages/@tailwindcss-vite/src/index.ts | 30 ++++++++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts index 81db3e82c6d5..8df7eb224813 100644 --- a/integrations/vite/astro.test.ts +++ b/integrations/vite/astro.test.ts @@ -92,9 +92,10 @@ test( // https://astro.build/config export default defineConfig({ vite: { plugins: [tailwindcss()] }, integrations: [react()] }) `, + // prettier-ignore 'src/pages/index.astro': html` --- - import ClientOnly from './client-only'; + import ClientOnly from '../components/client-only'; ---
Hello, world!
@@ -105,7 +106,7 @@ test( @import 'tailwindcss'; `, - 'src/pages/client-only.jsx': js` + 'src/components/client-only.jsx': js` export default function ClientOnly() { return
Hello, world!
} diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 2b6266a133ae..818af68bb2f7 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -13,11 +13,11 @@ const INLINE_STYLE_ID_RE = /[?&]index\=\d+\.css$/ const IGNORED_DEPENDENCIES = ['tailwind-merge'] -type ScannerMode = 'automatic' | 'module-graph' | 'file-system' +type ScannerMode = 'module-graph' | 'file-system' export default function tailwindcss( - { scanner: scannerMode = 'automatic' }: { scanner: ScannerMode } = { - scanner: 'automatic', + { scanner: scannerMode = 'module-graph' }: { scanner: ScannerMode } = { + scanner: 'module-graph', }, ): Plugin[] { let servers: ViteDevServer[] = [] @@ -26,6 +26,8 @@ export default function tailwindcss( let isSSR = false let minify = false + let additionalFileSystemSources: string[] = [] + // The Vite extension has two types of sources for candidates: // // 1. The module graph: These are all modules that vite transforms and we want @@ -67,6 +69,7 @@ export default function tailwindcss( () => moduleGraphCandidates, scannerMode, config!.root, + additionalFileSystemSources, customCssResolver, customJsResolver, ) @@ -209,13 +212,8 @@ export default function tailwindcss( minify = config.build.cssMinify !== false isSSR = config.build.ssr !== false && config.build.ssr !== undefined - if (scannerMode === 'automatic') { - if (shouldDisableModuleGraph(config)) { - console.warn('Detected an Astro.js build and opted-out of using the Vite module graph.') - scannerMode = 'file-system' - return - } - scannerMode = 'module-graph' + if (isAstro(config)) { + additionalFileSystemSources.push(path.join(config.root, 'src', 'components')) } }, @@ -438,6 +436,7 @@ class Root { private getSharedCandidates: () => Map>, private scannerMode: ScannerMode, private base: string, + private additionalFileSystemSources: string[], private customCssResolver: (id: string, base: string) => Promise, private customJsResolver: (id: string, base: string) => Promise, @@ -492,6 +491,15 @@ class Root { return [this.compiler.root] })().concat(this.compiler.globs) + if (this.additionalFileSystemSources) { + sources = sources.concat( + this.additionalFileSystemSources.map((source) => ({ + base: source, + pattern: '**/*', + })), + ) + } + this.scanner = new Scanner({ sources }) } @@ -608,6 +616,6 @@ class Root { } } -function shouldDisableModuleGraph(config: ResolvedConfig) { +function isAstro(config: ResolvedConfig) { return config.plugins.some((p) => p.name === 'astro:scripts:page-ssr') } From 7af20b159484ba251f6b66929f02bc00b1ea94df Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 14 Feb 2025 16:23:34 +0100 Subject: [PATCH 04/16] Add docs --- packages/@tailwindcss-vite/README.md | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/@tailwindcss-vite/README.md b/packages/@tailwindcss-vite/README.md index 95ec9d87ddcc..d82f437d6ee6 100644 --- a/packages/@tailwindcss-vite/README.md +++ b/packages/@tailwindcss-vite/README.md @@ -21,10 +21,43 @@ --- +# `@tailwindcss/vite` + ## Documentation For full documentation, visit [tailwindcss.com](https://tailwindcss.com). +--- + +## Advanced topics + +### API reference + +The Vite plugin can be configured by passing an object to the `tailwindcss()`. Here is a full list of available options: + +| Property | Values | +| --------------------------------------------- | ----------------------------------------- | +| [`scanner`](#disabling-module-graph-scanning) | `module-graph` _(default)_, `file-system` | + +### Disabling module-graph scanning + +Our Vite plugin is designed to take the Vite module graph into account when scanning for utilities used in your project. This will work well in most cases since the module graph contains all markup that will be in your final build. + +However, sometimes your Vite setup is split across different build steps (e.g. when using SSR builds). If that is the case, you might find that the client build might contain more utilities since it traverses all components while the server build doesn't. + +To ensure that both builds read all components from your project, set the `scanner` option to `file-system`: + +```js +import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [tailwindcss({ scanner: 'file-system' })], +}) +``` + +--- + ## Community For help, discussion about best practices, or any other conversation that would benefit from being searchable: From 9ae420c691060cba725154fb667688948fe59cf9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 14 Feb 2025 16:30:50 +0100 Subject: [PATCH 05/16] Revert "Fix Astro by adding the componentns folder instead of falling to full fs scanning" This reverts commit daacc5eadf028f427e28da8977fed71f77344d19. --- integrations/vite/astro.test.ts | 5 ++--- packages/@tailwindcss-vite/src/index.ts | 30 +++++++++---------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts index 8df7eb224813..81db3e82c6d5 100644 --- a/integrations/vite/astro.test.ts +++ b/integrations/vite/astro.test.ts @@ -92,10 +92,9 @@ test( // https://astro.build/config export default defineConfig({ vite: { plugins: [tailwindcss()] }, integrations: [react()] }) `, - // prettier-ignore 'src/pages/index.astro': html` --- - import ClientOnly from '../components/client-only'; + import ClientOnly from './client-only'; ---
Hello, world!
@@ -106,7 +105,7 @@ test( @import 'tailwindcss'; `, - 'src/components/client-only.jsx': js` + 'src/pages/client-only.jsx': js` export default function ClientOnly() { return
Hello, world!
} diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 818af68bb2f7..2b6266a133ae 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -13,11 +13,11 @@ const INLINE_STYLE_ID_RE = /[?&]index\=\d+\.css$/ const IGNORED_DEPENDENCIES = ['tailwind-merge'] -type ScannerMode = 'module-graph' | 'file-system' +type ScannerMode = 'automatic' | 'module-graph' | 'file-system' export default function tailwindcss( - { scanner: scannerMode = 'module-graph' }: { scanner: ScannerMode } = { - scanner: 'module-graph', + { scanner: scannerMode = 'automatic' }: { scanner: ScannerMode } = { + scanner: 'automatic', }, ): Plugin[] { let servers: ViteDevServer[] = [] @@ -26,8 +26,6 @@ export default function tailwindcss( let isSSR = false let minify = false - let additionalFileSystemSources: string[] = [] - // The Vite extension has two types of sources for candidates: // // 1. The module graph: These are all modules that vite transforms and we want @@ -69,7 +67,6 @@ export default function tailwindcss( () => moduleGraphCandidates, scannerMode, config!.root, - additionalFileSystemSources, customCssResolver, customJsResolver, ) @@ -212,8 +209,13 @@ export default function tailwindcss( minify = config.build.cssMinify !== false isSSR = config.build.ssr !== false && config.build.ssr !== undefined - if (isAstro(config)) { - additionalFileSystemSources.push(path.join(config.root, 'src', 'components')) + if (scannerMode === 'automatic') { + if (shouldDisableModuleGraph(config)) { + console.warn('Detected an Astro.js build and opted-out of using the Vite module graph.') + scannerMode = 'file-system' + return + } + scannerMode = 'module-graph' } }, @@ -436,7 +438,6 @@ class Root { private getSharedCandidates: () => Map>, private scannerMode: ScannerMode, private base: string, - private additionalFileSystemSources: string[], private customCssResolver: (id: string, base: string) => Promise, private customJsResolver: (id: string, base: string) => Promise, @@ -491,15 +492,6 @@ class Root { return [this.compiler.root] })().concat(this.compiler.globs) - if (this.additionalFileSystemSources) { - sources = sources.concat( - this.additionalFileSystemSources.map((source) => ({ - base: source, - pattern: '**/*', - })), - ) - } - this.scanner = new Scanner({ sources }) } @@ -616,6 +608,6 @@ class Root { } } -function isAstro(config: ResolvedConfig) { +function shouldDisableModuleGraph(config: ResolvedConfig) { return config.plugins.some((p) => p.name === 'astro:scripts:page-ssr') } From e39dad2ba27d3c8c83c059c193e84a51634ecda6 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 14 Feb 2025 16:33:02 +0100 Subject: [PATCH 06/16] Cleanups --- packages/@tailwindcss-vite/README.md | 10 ++++++---- packages/@tailwindcss-vite/src/index.ts | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/@tailwindcss-vite/README.md b/packages/@tailwindcss-vite/README.md index d82f437d6ee6..e42242103744 100644 --- a/packages/@tailwindcss-vite/README.md +++ b/packages/@tailwindcss-vite/README.md @@ -35,9 +35,9 @@ For full documentation, visit [tailwindcss.com](https://tailwindcss.com). The Vite plugin can be configured by passing an object to the `tailwindcss()`. Here is a full list of available options: -| Property | Values | -| --------------------------------------------- | ----------------------------------------- | -| [`scanner`](#disabling-module-graph-scanning) | `module-graph` _(default)_, `file-system` | +| Property | Values | +| --------------------------------------------- | ------------------------------------------------------ | +| [`scanner`](#disabling-module-graph-scanning) | `automatic` _(default)_, `module-graph`, `file-system` | ### Disabling module-graph scanning @@ -45,7 +45,9 @@ Our Vite plugin is designed to take the Vite module graph into account when scan However, sometimes your Vite setup is split across different build steps (e.g. when using SSR builds). If that is the case, you might find that the client build might contain more utilities since it traverses all components while the server build doesn't. -To ensure that both builds read all components from your project, set the `scanner` option to `file-system`: +When the `scanner` is set to `automatic`, we will automatically use the `file-system` scanner for Astro builds. + +To ensure that both builds read all components from your project in other cases, set the `scanner` option to `file-system`: ```js import { defineConfig } from 'vite' diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 2b6266a133ae..b2dfe44a9ea9 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -211,7 +211,6 @@ export default function tailwindcss( if (scannerMode === 'automatic') { if (shouldDisableModuleGraph(config)) { - console.warn('Detected an Astro.js build and opted-out of using the Vite module graph.') scannerMode = 'file-system' return } From f007ff183f4ec6931a3d294ff9e327104ea77021 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Fri, 14 Feb 2025 16:53:48 +0100 Subject: [PATCH 07/16] Add change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ebc53c22058..e202be36fd22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370)) +- Vite: Add a new `scanner` option to disable module-graph based scanning ([#16425](https://github.com/tailwindlabs/tailwindcss/pull/16425)) ### Fixed From ebfd6964df1432d05a0df07785ed776dce4630b5 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 18 Feb 2025 14:37:41 +0100 Subject: [PATCH 08/16] Remove module graph scanner --- packages/@tailwindcss-vite/src/index.ts | 352 ++++-------------------- 1 file changed, 50 insertions(+), 302 deletions(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index b2dfe44a9ea9..2aaeb7dc4588 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -4,47 +4,20 @@ import { Scanner } from '@tailwindcss/oxide' import { Features as LightningCssFeatures, transform } from 'lightningcss' import fs from 'node:fs/promises' import path from 'node:path' -import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite' +import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' const DEBUG = env.DEBUG const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/ const COMMON_JS_PROXY_RE = /\?commonjs-proxy/ const INLINE_STYLE_ID_RE = /[?&]index\=\d+\.css$/ -const IGNORED_DEPENDENCIES = ['tailwind-merge'] - -type ScannerMode = 'automatic' | 'module-graph' | 'file-system' - -export default function tailwindcss( - { scanner: scannerMode = 'automatic' }: { scanner: ScannerMode } = { - scanner: 'automatic', - }, -): Plugin[] { +export default function tailwindcss(): Plugin[] { let servers: ViteDevServer[] = [] let config: ResolvedConfig | null = null let isSSR = false let minify = false - // The Vite extension has two types of sources for candidates: - // - // 1. The module graph: These are all modules that vite transforms and we want - // them to be automatically scanned for candidates. - // 2. Root defined `@source`s - // - // Module graph candidates are global to the Vite extension since we do not - // know which CSS roots will be used for the modules. We are using a custom - // scanner instance with auto source discovery disabled to parse these. - // - // For candidates coming from custom `@source` directives of the CSS roots, we - // create an individual scanner for each root. - // - // Note: To improve performance, we do not remove candidates from this set. - // This means a longer-ongoing dev mode session might contain candidates that - // are no longer referenced in code. - let moduleGraphCandidates = new DefaultMap>(() => new Set()) - let moduleGraphScanner = new Scanner({}) - let roots: DefaultMap = new DefaultMap((id) => { let cssResolver = config!.createResolver({ ...config!.resolve, @@ -62,138 +35,9 @@ export default function tailwindcss( function customJsResolver(id: string, base: string) { return jsResolver(id, base, true, isSSR) } - return new Root( - id, - () => moduleGraphCandidates, - scannerMode, - config!.root, - customCssResolver, - customJsResolver, - ) + return new Root(id, config!.root, customCssResolver, customJsResolver) }) - function scanFile(id: string, content: string, extension: string) { - if (scannerMode === 'file-system') { - return - } - - for (let dependency of IGNORED_DEPENDENCIES) { - // We validated that Vite IDs always use posix style path separators, even on Windows. - // In dev build, Vite precompiles dependencies - if (id.includes(`.vite/deps/${dependency}.js`)) { - return - } - // In prod builds, use the node_modules path - if (id.includes(`/node_modules/${dependency}/`)) { - return - } - } - - let updated = false - for (let candidate of moduleGraphScanner.scanFiles([{ content, extension }])) { - updated = true - moduleGraphCandidates.get(id).add(candidate) - } - - if (updated) { - invalidateAllRoots() - } - } - - function invalidateAllRoots() { - for (let server of servers) { - let updates: Update[] = [] - for (let [id] of roots.entries()) { - let module = server.moduleGraph.getModuleById(id) - if (!module) continue - - roots.get(id).requiresRebuild = false - server.moduleGraph.invalidateModule(module) - updates.push({ - type: `${module.type}-update`, - path: module.url, - acceptedPath: module.url, - timestamp: Date.now(), - }) - } - if (updates.length > 0) { - server.hot.send({ type: 'update', updates }) - } - } - } - - async function regenerateOptimizedCss( - root: Root, - addWatchFile: (file: string) => void, - I: Instrumentation, - ) { - let content = root.lastContent - let generated = await root.generate(content, addWatchFile, I) - if (generated === false) { - return - } - DEBUG && I.start('Optimize CSS') - let result = optimizeCss(generated, { minify }) - DEBUG && I.end('Optimize CSS') - return result - } - - // Manually run the transform functions of non-Tailwind plugins on the given CSS - async function transformWithPlugins(context: Rollup.PluginContext, id: string, css: string) { - let transformPluginContext = { - ...context, - getCombinedSourcemap: () => { - throw new Error('getCombinedSourcemap not implemented') - }, - } - - for (let plugin of config!.plugins) { - if (!plugin.transform) continue - - if (plugin.name.startsWith('@tailwindcss/')) { - // We do not run any Tailwind transforms anymore - continue - } else if ( - plugin.name.startsWith('vite:') && - // Apply the vite:css plugin to generated CSS for transformations like - // URL path rewriting and image inlining. - plugin.name !== 'vite:css' && - // In build mode, since `renderStart` runs after all transformations, we - // need to also apply vite:css-post. - plugin.name !== 'vite:css-post' && - // The vite:vue plugin handles CSS specific post-processing for Vue - plugin.name !== 'vite:vue' - ) { - continue - } else if (plugin.name === 'ssr-styles') { - // The Nuxt ssr-styles plugin emits styles from server-side rendered - // components, we can't run it in the `renderStart` phase so we're - // skipping it. - continue - } - - let transformHandler = - 'handler' in plugin.transform! ? plugin.transform.handler : plugin.transform! - - try { - // Directly call the plugin's transform function to process the - // generated CSS. In build mode, this updates the chunks later used to - // generate the bundle. In serve mode, the transformed source should be - // applied in transform. - let result = await transformHandler.call(transformPluginContext, css, id) - if (!result) continue - if (typeof result === 'string') { - css = result - } else if (result.code) { - css = result.code - } - } catch (e) { - console.error(`Error running ${plugin.name} on Tailwind CSS output. Skipping.`) - } - } - return css - } - return [ { // Step 1: Scan source files for candidates @@ -208,27 +52,6 @@ export default function tailwindcss( config = _config minify = config.build.cssMinify !== false isSSR = config.build.ssr !== false && config.build.ssr !== undefined - - if (scannerMode === 'automatic') { - if (shouldDisableModuleGraph(config)) { - scannerMode = 'file-system' - return - } - scannerMode = 'module-graph' - } - }, - - // Scan all non-CSS files for candidates - transformIndexHtml(html, { path }) { - // SolidStart emits HTML chunks with an undefined path and the html content of `\`. - if (!path) return - - scanFile(path, html, 'html') - }, - transform(src, id, options) { - let extension = getExtension(id) - if (isPotentialCssRootFile(id)) return - scanFile(id, src, extension) }, }, @@ -246,22 +69,14 @@ export default function tailwindcss( let root = roots.get(id) - if (!options?.ssr) { - // Wait until all other files have been processed, so we can extract - // all candidates before generating CSS. This must not be called - // during SSR or it will block the server. - // - // The reason why we can not rely on the invalidation here is that the - // users would otherwise see a flicker in the styles as the CSS might - // be loaded with an invalid set of candidates first. - await Promise.all(servers.map((server) => server.waitForRequestsIdle(id))) - } - let generated = await root.generate(src, (file) => this.addWatchFile(file), I) if (!generated) { roots.delete(id) return src } + + I.end('[@tailwindcss/vite] Generate CSS (serve)') + I.report() return { code: generated } }, }, @@ -280,45 +95,20 @@ export default function tailwindcss( let root = roots.get(id) - // We do a first pass to generate valid CSS for the downstream plugins. - // However, since not all candidates are guaranteed to be extracted by - // this time, we have to re-run a transform for the root later. let generated = await root.generate(src, (file) => this.addWatchFile(file), I) if (!generated) { roots.delete(id) return src } - return { code: generated } - }, + I.end('[@tailwindcss/vite] Generate CSS (build)') - // `renderStart` runs in the bundle generation stage after all transforms. - // We must run before `enforce: post` so the updated chunks are picked up - // by vite:css-post. - async renderStart() { - using I = new Instrumentation() - I.start('[@tailwindcss/vite] (render start)') - - for (let [id, root] of roots.entries()) { - let generated = await regenerateOptimizedCss( - root, - // During the renderStart phase, we can not add watch files since - // those would not be causing a refresh of the right CSS file. This - // should not be an issue since we did already process the CSS file - // before and the dependencies should not be changed (only the - // candidate list might have) - () => {}, - I, - ) - if (!generated) { - roots.delete(id) - continue - } + I.start('[@tailwindcss/vite] Optimize CSS') + generated = optimizeCss(generated, { minify }) + I.end('[@tailwindcss/vite] Optimize CSS') - // These plugins have side effects which, during build, results in CSS - // being written to the output dir. We need to run them here to ensure - // the CSS is written before the bundle is generated. - await transformWithPlugins(this, id, generated) - } + I.report() + + return { code: generated } }, }, ] satisfies Plugin[] @@ -344,7 +134,7 @@ function optimizeCss( input: string, { file = 'input.css', minify = false }: { file?: string; minify?: boolean } = {}, ) { - function optimize(code: Buffer | Uint8Array) { + function optimize(code: Buffer | Uint8Array | any) { return transform({ filename: file, code, @@ -402,40 +192,24 @@ class DefaultMap extends Map { } class Root { - // Content is only used in serve mode where we need to capture the initial - // contents of the root file so that we can restore it during the - // `renderStart` hook. - public lastContent: string = '' - // The lazily-initialized Tailwind compiler components. These are persisted // throughout rebuilds but will be re-initialized if the rebuild strategy is // set to `full`. private compiler?: Awaited> - public requiresRebuild: boolean = true - - // This is the compiler-specific scanner instance that is used only to scan - // files for custom @source paths. All other modules we scan for candidates - // will use the shared moduleGraphScanner instance. + // The lazily-initialized Tailwind scanner. private scanner?: Scanner // List of all candidates that were being returned by the root scanner during // the lifetime of the root. private candidates: Set = new Set() - // List of all dependencies captured while generating the root. These are - // retained so we can clear the require cache when we rebuild the root. - private dependencies = new Set() - - // The resolved path given to `source(…)`. When not given this is `null`. - private basePath: string | null = null - - public overwriteCandidates: string[] | null = null + // List of all build dependencies (e.g. imported stylesheets or plugins) and + // their last modification timestamp + private buildDependencies = new Map() constructor( private id: string, - private getSharedCandidates: () => Map>, - private scannerMode: ScannerMode, private base: string, private customCssResolver: (id: string, base: string) => Promise, @@ -449,29 +223,41 @@ class Root { addWatchFile: (file: string) => void, I: Instrumentation, ): Promise { - this.lastContent = content - + let requiresBuildPromise = this.requiresBuild() let inputPath = idToPath(this.id) let inputBase = path.dirname(path.resolve(inputPath)) - if (!this.compiler || !this.scanner || this.requiresRebuild) { - clearRequireCache(Array.from(this.dependencies)) - this.dependencies = new Set([idToPath(inputPath)]) + console.log({ requiresRebuild: await requiresBuildPromise }) + + if (!this.compiler || !this.scanner || (await requiresBuildPromise)) { + clearRequireCache(Array.from(this.buildDependencies.keys())) + this.buildDependencies.clear() + this.buildDependencies = new Map() + + this.addBuildDependency(idToPath(inputPath)) DEBUG && I.start('Setup compiler') + console.log({ + notCompiler: !this.compiler, + notScanner: !this.scanner, + }) + let addBuildDependenciesPromises: Promise[] = [] this.compiler = await compile(content, { base: inputBase, shouldRewriteUrls: true, onDependency: (path) => { addWatchFile(path) - this.dependencies.add(path) + addBuildDependenciesPromises.push(this.addBuildDependency(path)) }, customCssResolver: this.customCssResolver, customJsResolver: this.customJsResolver, }) + await Promise.all(addBuildDependenciesPromises) DEBUG && I.end('Setup compiler') + DEBUG && I.start('Setup scanner') + let sources = (() => { // Disable auto source detection if (this.compiler.root === 'none') { @@ -480,11 +266,7 @@ class Root { // No root specified, use the module graph unless we are in file-based scanner mode if (this.compiler.root === null) { - if (this.scannerMode === 'file-system') { - return [{ base: this.base, pattern: '**/*' }] - } else { - return [] - } + return [{ base: this.base, pattern: '**/*' }] } // Use the specified root @@ -492,6 +274,7 @@ class Root { })().concat(this.compiler.globs) this.scanner = new Scanner({ sources }) + DEBUG && I.end('Setup scanner') } if ( @@ -503,7 +286,7 @@ class Root { return false } - if (!this.overwriteCandidates || this.compiler.features & Features.Utilities) { + if (this.compiler.features & Features.Utilities) { // 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). @@ -549,64 +332,29 @@ class Root { `The path given to \`source(…)\` must be a directory but got \`source(${basePath})\` instead.`, ) } - - this.basePath = basePath - } else if (root === null) { - this.basePath = null } } } - this.requiresRebuild = true - DEBUG && I.start('Build CSS') - let result = this.compiler.build( - this.overwriteCandidates - ? this.overwriteCandidates - : [...this.sharedCandidates(), ...this.candidates], - ) + let result = this.compiler.build([...this.candidates]) DEBUG && I.end('Build CSS') return result } - private sharedCandidates(): Set { - if (!this.compiler || this.scannerMode === 'file-system' || this.compiler.root === 'none') { - return new Set() - } - - const HAS_DRIVE_LETTER = /^[A-Z]:/ - - let shouldIncludeCandidatesFrom = (id: string) => { - if (this.basePath === null) return true - - if (id.startsWith(this.basePath)) return true - - // This is a windows absolute path that doesn't match so return false - if (HAS_DRIVE_LETTER.test(id)) return false - - // We've got a path that's not absolute and not on Windows - // TODO: this is probably a virtual module -- not sure if we need to scan it - if (!id.startsWith('/')) return true - - // This is an absolute path on POSIX and it does not match - return false - } - - let shared = new Set() - - for (let [id, candidates] of this.getSharedCandidates()) { - if (!shouldIncludeCandidatesFrom(id)) continue + private async addBuildDependency(path: string) { + let stat = await fs.stat(path) + this.buildDependencies.set(path, stat.mtimeMs) + } - for (let candidate of candidates) { - shared.add(candidate) + private async requiresBuild(): Promise { + for (let [path, mtime] of this.buildDependencies) { + let stat = await fs.stat(path) + if (stat.mtimeMs > mtime) { + return true } } - - return shared + return false } } - -function shouldDisableModuleGraph(config: ResolvedConfig) { - return config.plugins.some((p) => p.name === 'astro:scripts:page-ssr') -} From e47ec4bdd4201bd69eb3e0e3ac18949718b9a349 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 18 Feb 2025 14:43:15 +0100 Subject: [PATCH 09/16] Revert changes --- CHANGELOG.md | 1 - integrations/vite/index.test.ts | 91 ++++++++++++++++---------- integrations/vite/react-router.test.ts | 2 +- integrations/vite/ssr.test.ts | 4 +- packages/@tailwindcss-vite/README.md | 35 ---------- 5 files changed, 61 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e202be36fd22..9ebc53c22058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - _Experimental_: Add `user-valid` and `user-invalid` variants ([#12370](https://github.com/tailwindlabs/tailwindcss/pull/12370)) -- Vite: Add a new `scanner` option to disable module-graph based scanning ([#16425](https://github.com/tailwindlabs/tailwindcss/pull/16425)) ### Fixed diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index 1b22fb7a8f36..c0bee2b878cc 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -14,12 +14,7 @@ import { yaml, } from '../utils' -describe.each([ - ['postcss', 'module-graph'], - ['postcss', 'file-system'], - ['lightningcss', 'module-graph'], - ['lightningcss', 'file-system'], -])('using %s via %s', (transformer, scanner) => { +describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { test( `production build`, { @@ -50,7 +45,7 @@ describe.each([ export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss({ scanner: '${scanner}' })], + plugins: [tailwindcss()], }) `, 'project-a/index.html': html` @@ -62,7 +57,9 @@ describe.each([ `, 'project-a/tailwind.config.js': js` - export default { content: ['../project-b/src/**/*.js'] } + export default { + content: ['../project-b/src/**/*.js'], + } `, 'project-a/src/index.css': css` @import 'tailwindcss/theme' theme(reference); @@ -125,7 +122,7 @@ describe.each([ export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss({ scanner: '${scanner}' })], + plugins: [tailwindcss()], }) `, 'project-a/index.html': html` @@ -145,7 +142,9 @@ describe.each([ `, 'project-a/tailwind.config.js': js` - export default { content: ['../project-b/src/**/*.js'] } + export default { + content: ['../project-b/src/**/*.js'], + } `, 'project-a/src/index.css': css` @import 'tailwindcss/theme' theme(reference); @@ -163,7 +162,9 @@ describe.each([ }, }, async ({ root, spawn, fs, expect }) => { - let process = await spawn('pnpm vite dev', { cwd: path.join(root, 'project-a') }) + let process = await spawn('pnpm vite dev', { + cwd: path.join(root, 'project-a'), + }) await process.onStdout((m) => m.includes('ready in')) let url = '' @@ -173,19 +174,17 @@ describe.each([ return Boolean(url) }) - // Candidates are resolved lazily in module-graph mode, so the first visit of index.html will - // only have candidates from this file. + // 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`) - - if (scanner === 'module-graph') { - expect(styles).not.toContain(candidate`font-bold`) - } + expect(styles).not.toContain(candidate`font-bold`) }) - // Going to about.html will extend the candidate list to include candidates from about.html. + // 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`) @@ -233,8 +232,8 @@ describe.each([ }) await retryAssertion(async () => { - // After updates to the CSS file, all previous candidates should still be in the generated - // stylesheet. + // 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` @@ -284,7 +283,10 @@ describe.each([ import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' - export default defineConfig({ build: { cssMinify: false }, plugins: [tailwindcss()] }) + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) `, 'project-a/index.html': html` @@ -295,7 +297,9 @@ describe.each([ `, 'project-a/tailwind.config.js': js` - export default { content: ['../project-b/src/**/*.js'] } + export default { + content: ['../project-b/src/**/*.js'], + } `, 'project-a/src/index.css': css` @import 'tailwindcss/theme' theme(reference); @@ -320,7 +324,9 @@ describe.each([ }, }, async ({ root, spawn, fs, expect }) => { - let process = await spawn('pnpm vite build --watch', { cwd: path.join(root, 'project-a') }) + let process = await spawn('pnpm vite build --watch', { + cwd: path.join(root, 'project-a'), + }) await process.onStdout((m) => m.includes('built in')) let filename = '' @@ -459,7 +465,7 @@ describe.each([ export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss({ scanner: '${scanner}' })], + plugins: [tailwindcss()], }) `, 'project-a/index.html': html` @@ -545,7 +551,7 @@ describe.each([ export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss({ scanner: '${scanner}' })], + plugins: [tailwindcss()], }) `, 'project-a/index.html': html` @@ -647,7 +653,7 @@ describe.each([ export default defineConfig({ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, build: { cssMinify: false }, - plugins: [tailwindcss({ scanner: '${scanner}' })], + plugins: [tailwindcss()], }) `, 'project-a/index.html': html` @@ -696,15 +702,23 @@ test( 'package.json': json` { "type": "module", - "dependencies": { "@tailwindcss/vite": "workspace:^", "tailwindcss": "workspace:^" }, - "devDependencies": { "vite": "^6" } + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^6" + } } `, 'vite.config.ts': ts` import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' - export default defineConfig({ build: { cssMinify: false }, plugins: [tailwindcss()] }) + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) `, 'index.html': html` @@ -770,15 +784,23 @@ test( 'package.json': json` { "type": "module", - "dependencies": { "@tailwindcss/vite": "workspace:^", "tailwindcss": "workspace:^" }, - "devDependencies": { "vite": "^6" } + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^6" + } } `, 'vite.config.ts': ts` import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' - export default defineConfig({ build: { cssMinify: false }, plugins: [tailwindcss()] }) + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) `, 'index.html': html` @@ -838,7 +860,10 @@ test( import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' - export default defineConfig({ build: { cssMinify: false }, plugins: [tailwindcss()] }) + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) `, 'index.html': html` diff --git a/integrations/vite/react-router.test.ts b/integrations/vite/react-router.test.ts index 436947680099..5b9574fd658b 100644 --- a/integrations/vite/react-router.test.ts +++ b/integrations/vite/react-router.test.ts @@ -156,7 +156,7 @@ test( import tailwindcss from '@tailwindcss/vite' export default defineConfig({ - plugins: [tailwindcss({ scanner: 'file-system' }), reactRouter()], + plugins: [tailwindcss(), reactRouter()], }) `, '.gitignore': txt` diff --git a/integrations/vite/ssr.test.ts b/integrations/vite/ssr.test.ts index b0bf92126c23..d4d227b8e931 100644 --- a/integrations/vite/ssr.test.ts +++ b/integrations/vite/ssr.test.ts @@ -51,7 +51,7 @@ test( cssMinify: false, ssrEmitAssets: true, }, - plugins: [tailwindcss({ scanner: 'file-system' })], + plugins: [tailwindcss()], }) `, ...WORKSPACE, @@ -98,7 +98,7 @@ test( cssMinify: false, ssrEmitAssets: true, }, - plugins: [tailwindcss({ scanner: 'file-system' })], + plugins: [tailwindcss()], }) `, ...WORKSPACE, diff --git a/packages/@tailwindcss-vite/README.md b/packages/@tailwindcss-vite/README.md index e42242103744..95ec9d87ddcc 100644 --- a/packages/@tailwindcss-vite/README.md +++ b/packages/@tailwindcss-vite/README.md @@ -21,45 +21,10 @@ --- -# `@tailwindcss/vite` - ## Documentation For full documentation, visit [tailwindcss.com](https://tailwindcss.com). ---- - -## Advanced topics - -### API reference - -The Vite plugin can be configured by passing an object to the `tailwindcss()`. Here is a full list of available options: - -| Property | Values | -| --------------------------------------------- | ------------------------------------------------------ | -| [`scanner`](#disabling-module-graph-scanning) | `automatic` _(default)_, `module-graph`, `file-system` | - -### Disabling module-graph scanning - -Our Vite plugin is designed to take the Vite module graph into account when scanning for utilities used in your project. This will work well in most cases since the module graph contains all markup that will be in your final build. - -However, sometimes your Vite setup is split across different build steps (e.g. when using SSR builds). If that is the case, you might find that the client build might contain more utilities since it traverses all components while the server build doesn't. - -When the `scanner` is set to `automatic`, we will automatically use the `file-system` scanner for Astro builds. - -To ensure that both builds read all components from your project in other cases, set the `scanner` option to `file-system`: - -```js -import { defineConfig } from 'vite' -import tailwindcss from '@tailwindcss/vite' - -export default defineConfig({ - plugins: [tailwindcss({ scanner: 'file-system' })], -}) -``` - ---- - ## Community For help, discussion about best practices, or any other conversation that would benefit from being searchable: From 2f8bfa96108c23cdeca54b5edc6d8890d5ceeec9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 18 Feb 2025 15:07:01 +0100 Subject: [PATCH 10/16] Fix tests --- integrations/vite/index.test.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index c0bee2b878cc..46e47eae3f0b 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -92,7 +92,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { }, ) - test( + test.only( 'dev mode', { fs: { @@ -174,21 +174,10 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { 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`) }) From c3419ea777cdaafcacbaa624b3b4e81a3daecdd9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 18 Feb 2025 15:55:47 +0100 Subject: [PATCH 11/16] Remove debugging code --- packages/@tailwindcss-vite/src/index.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 2aaeb7dc4588..63910a2a4bd7 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -227,8 +227,6 @@ class Root { let inputPath = idToPath(this.id) let inputBase = path.dirname(path.resolve(inputPath)) - console.log({ requiresRebuild: await requiresBuildPromise }) - if (!this.compiler || !this.scanner || (await requiresBuildPromise)) { clearRequireCache(Array.from(this.buildDependencies.keys())) this.buildDependencies.clear() @@ -237,10 +235,6 @@ class Root { this.addBuildDependency(idToPath(inputPath)) DEBUG && I.start('Setup compiler') - console.log({ - notCompiler: !this.compiler, - notScanner: !this.scanner, - }) let addBuildDependenciesPromises: Promise[] = [] this.compiler = await compile(content, { base: inputBase, @@ -264,7 +258,7 @@ class Root { return [] } - // No root specified, use the module graph unless we are in file-based scanner mode + // No root specified, auto-detect based on the `**/*` pattern if (this.compiler.root === null) { return [{ base: this.base, pattern: '**/*' }] } From f1427dbbc5e5222b0348e8d0da26cf647889b073 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 18 Feb 2025 16:02:42 +0100 Subject: [PATCH 12/16] Remove leftover .only --- integrations/vite/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index 46e47eae3f0b..0033c246fc70 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -92,7 +92,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { }, ) - test.only( + test( 'dev mode', { fs: { From 7085306353abf122e9760739597f8bab745ee2de Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 18 Feb 2025 16:13:50 +0100 Subject: [PATCH 13/16] More cleanups --- integrations/vite/index.test.ts | 12 +----------- packages/@tailwindcss-vite/src/index.ts | 3 --- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index 0033c246fc70..afc58c558bf5 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -685,7 +685,7 @@ describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { }) test( - `demote Tailwind roots to regular CSS files and back to Tailwind roots while restoring all candidates`, + `demote Tailwind roots to regular CSS files and back to Tailwind roots`, { fs: { 'package.json': json` @@ -739,19 +739,9 @@ test( 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).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`font-bold`) }) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 63910a2a4bd7..314b67d47520 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -76,7 +76,6 @@ export default function tailwindcss(): Plugin[] { } I.end('[@tailwindcss/vite] Generate CSS (serve)') - I.report() return { code: generated } }, }, @@ -106,8 +105,6 @@ export default function tailwindcss(): Plugin[] { generated = optimizeCss(generated, { minify }) I.end('[@tailwindcss/vite] Optimize CSS') - I.report() - return { code: generated } }, }, From 7cea30b9fdc4b4d76b578babc618f793a47cb679 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 18 Feb 2025 16:31:13 +0100 Subject: [PATCH 14/16] Prefix all debug code with `DEBUG &&` --- packages/@tailwindcss-vite/src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 314b67d47520..901344a6ea4e 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -65,7 +65,7 @@ export default function tailwindcss(): Plugin[] { if (!isPotentialCssRootFile(id)) return using I = new Instrumentation() - I.start('[@tailwindcss/vite] Generate CSS (serve)') + DEBUG && I.start('[@tailwindcss/vite] Generate CSS (serve)') let root = roots.get(id) @@ -75,7 +75,7 @@ export default function tailwindcss(): Plugin[] { return src } - I.end('[@tailwindcss/vite] Generate CSS (serve)') + DEBUG && I.end('[@tailwindcss/vite] Generate CSS (serve)') return { code: generated } }, }, @@ -90,7 +90,7 @@ export default function tailwindcss(): Plugin[] { if (!isPotentialCssRootFile(id)) return using I = new Instrumentation() - I.start('[@tailwindcss/vite] Generate CSS (build)') + DEBUG && I.start('[@tailwindcss/vite] Generate CSS (build)') let root = roots.get(id) @@ -99,11 +99,11 @@ export default function tailwindcss(): Plugin[] { roots.delete(id) return src } - I.end('[@tailwindcss/vite] Generate CSS (build)') + DEBUG && I.end('[@tailwindcss/vite] Generate CSS (build)') - I.start('[@tailwindcss/vite] Optimize CSS') + DEBUG && I.start('[@tailwindcss/vite] Optimize CSS') generated = optimizeCss(generated, { minify }) - I.end('[@tailwindcss/vite] Optimize CSS') + DEBUG && I.end('[@tailwindcss/vite] Optimize CSS') return { code: generated } }, From f37e4ade9919863d52de62897c42797f0ebfb64c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 20 Feb 2025 13:29:26 +0100 Subject: [PATCH 15/16] Add change log --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d3ea3b17c7..e7143ff1b106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Remove invalid `!important` on CSS variable declarations ([#16668](https://github.com/tailwindlabs/tailwindcss/pull/16668)) +- Vite: Automatic source detection now ignores files and directories specified in your `.gitignore` file ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631)) +- Vite: Ensure setups with multiple Vite builds work as expected ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631)) +- Vite: Ensure Astro production builds contain classes for client-only components ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631)) +- Vite: Ensure utility classes are read without escaping special characters ([#16631](https://github.com/tailwindlabs/tailwindcss/pull/16631)) ## [4.0.7] - 2025-02-18 From 51d50e4df580b86734a882ca0158f21310ee656e Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 20 Feb 2025 15:12:05 +0100 Subject: [PATCH 16/16] Remove duplicate code --- packages/@tailwindcss-vite/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 901344a6ea4e..8e3d2348ea16 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -227,7 +227,6 @@ class Root { if (!this.compiler || !this.scanner || (await requiresBuildPromise)) { clearRequireCache(Array.from(this.buildDependencies.keys())) this.buildDependencies.clear() - this.buildDependencies = new Map() this.addBuildDependency(idToPath(inputPath))