diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4449f460368e..493e3ef92f84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support derived spacing scales based on a single `--spacing` theme value ([#14857](https://github.com/tailwindlabs/tailwindcss/pull/14857))
- Add `svh`, `dvh`, `svw`, `dvw`, and `auto` values to all width/height/size utilities ([#14857](https://github.com/tailwindlabs/tailwindcss/pull/14857))
- Add new `**` variant ([#14903](https://github.com/tailwindlabs/tailwindcss/pull/14903))
+- Process `
+ `,
+ 'src/components.css': css`
+ .foo {
+ @apply text-red-500;
+ }
+ `,
+ },
+ },
+ async ({ fs, exec }) => {
+ await exec('pnpm vite build')
+
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+
+ await fs.expectFileToContain(files[0][0], [candidate`underline`, candidate`foo`])
+ },
+)
+
+test(
+ 'watch mode',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "type": "module",
+ "dependencies": {
+ "svelte": "^4.2.18",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^3.1.1",
+ "@tailwindcss/vite": "workspace:^",
+ "vite": "^5.3.5"
+ }
+ }
+ `,
+ 'vite.config.ts': ts`
+ import { defineConfig } from 'vite'
+ import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'
+ import tailwindcss from '@tailwindcss/vite'
+
+ export default defineConfig({
+ plugins: [
+ svelte({
+ preprocess: [vitePreprocess()],
+ }),
+ tailwindcss(),
+ ],
+ })
+ `,
+ 'index.html': html`
+
+
+
+
+
+
+
+ `,
+ 'src/main.ts': ts`
+ import App from './App.svelte'
+ const app = new App({
+ target: document.body,
+ })
+ `,
+ 'src/App.svelte': html`
+
+
+ Hello {name}!
+
+
+ `,
+ 'src/components.css': css`
+ .foo {
+ @apply text-red-500;
+ }
+ `,
+ },
+ },
+ async ({ fs, spawn }) => {
+ await spawn(`pnpm vite build --watch`)
+
+ let filename = ''
+ await retryAssertion(async () => {
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ filename = files[0][0]
+ })
+
+ await fs.expectFileToContain(filename, [candidate`foo`, candidate`underline`])
+
+ await fs.write(
+ 'src/components.css',
+ css`
+ .bar {
+ @apply text-green-500;
+ }
+ `,
+ )
+ await retryAssertion(async () => {
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [, css] = files[0]
+ expect(css).toContain(candidate`underline`)
+ expect(css).toContain(candidate`bar`)
+ expect(css).not.toContain(candidate`foo`)
+ })
+ },
+)
diff --git a/packages/@tailwindcss-vite/package.json b/packages/@tailwindcss-vite/package.json
index 7de95525e11d..8a3e6330641b 100644
--- a/packages/@tailwindcss-vite/package.json
+++ b/packages/@tailwindcss-vite/package.json
@@ -31,6 +31,7 @@
"@tailwindcss/node": "workspace:^",
"@tailwindcss/oxide": "workspace:^",
"lightningcss": "catalog:",
+ "svelte-preprocess": "^6.0.2",
"tailwindcss": "workspace:^"
},
"devDependencies": {
diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts
index 1ec1eac5e736..422437d60f4e 100644
--- a/packages/@tailwindcss-vite/src/index.ts
+++ b/packages/@tailwindcss-vite/src/index.ts
@@ -4,6 +4,7 @@ import { Scanner } from '@tailwindcss/oxide'
import { Features, transform } from 'lightningcss'
import fs from 'node:fs/promises'
import path from 'node:path'
+import { sveltePreprocess } from 'svelte-preprocess'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
const SPECIAL_QUERY_RE = /[?&](raw|url)\b/
@@ -53,9 +54,14 @@ export default function tailwindcss(): Plugin[] {
function invalidateAllRoots(isSSR: boolean) {
for (let server of servers) {
let updates: Update[] = []
- for (let id of roots.keys()) {
+ for (let [id, root] of roots.entries()) {
let module = server.moduleGraph.getModuleById(id)
if (!module) {
+ // The module for this root might not exist yet
+ if (root.builtBeforeTransform) {
+ return
+ }
+
// Note: Removing this during SSR is not safe and will produce
// inconsistent results based on the timing of the removal and
// the order / timing of transforms.
@@ -152,6 +158,7 @@ export default function tailwindcss(): Plugin[] {
}
return [
+ svelteProcessor(roots),
{
// Step 1: Scan source files for candidates
name: '@tailwindcss/vite:scan',
@@ -189,6 +196,19 @@ export default function tailwindcss(): Plugin[] {
let root = roots.get(id)
+ if (root.builtBeforeTransform) {
+ root.builtBeforeTransform.forEach((file) => this.addWatchFile(file))
+ root.builtBeforeTransform = undefined
+ // When a root was built before this transform hook, the candidate
+ // list might be outdated already by the time the transform hook is
+ // called.
+ //
+ // This requires us to build the CSS file again. However, we do not
+ // expect dependencies to have changed, so we can avoid a full
+ // rebuild.
+ root.requiresRebuild = false
+ }
+
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
@@ -220,6 +240,18 @@ export default function tailwindcss(): Plugin[] {
let root = roots.get(id)
+ if (root.builtBeforeTransform) {
+ root.builtBeforeTransform.forEach((file) => this.addWatchFile(file))
+ root.builtBeforeTransform = undefined
+ // When a root was built before this transform hook, the candidate
+ // list might be outdated already by the time the transform hook is
+ // called.
+ //
+ // Since we already do a second render pass in build mode, we don't
+ // need to do any more work here.
+ return
+ }
+
// 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.
@@ -266,11 +298,13 @@ function getExtension(id: string) {
}
function isPotentialCssRootFile(id: string) {
+ if (id.includes('/.vite/')) return
let extension = getExtension(id)
let isCssFile =
(extension === 'css' ||
(extension === 'vue' && id.includes('&lang.css')) ||
- (extension === 'astro' && id.includes('&lang.css'))) &&
+ (extension === 'astro' && id.includes('&lang.css')) ||
+ (extension === 'svelte' && id.includes('&lang.css'))) &&
// Don't intercept special static asset resources
!SPECIAL_QUERY_RE.test(id)
@@ -338,6 +372,14 @@ class Root {
// `renderStart` hook.
public lastContent: string = ''
+ // When set, indicates that the root was built before the Vite transform hook
+ // was being called. This can happen in scenarios like when preprocessing
+ // `