diff --git a/CHANGELOG.md b/CHANGELOG.md
index a1d007eba..9035dc387 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure content globs defined in `@config` files are relative to that file ([#14314](https://github.com/tailwindlabs/tailwindcss/pull/14314))
- Ensure CSS `theme()` functions are evaluated in media query ranges with collapsed whitespace ((#14321)[https://github.com/tailwindlabs/tailwindcss/pull/14321])
+- Fix support for Nuxt projects in the Vite plugin (requires Nuxt 3.13.1+) ([#14319](https://github.com/tailwindlabs/tailwindcss/pull/14319))
## [4.0.0-alpha.21] - 2024-09-02
diff --git a/integrations/vite/nuxt.test.ts b/integrations/vite/nuxt.test.ts
new file mode 100644
index 000000000..7004ccada
--- /dev/null
+++ b/integrations/vite/nuxt.test.ts
@@ -0,0 +1,64 @@
+import { expect } from 'vitest'
+import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
+
+test(
+ 'dev mode',
+ {
+ fs: {
+ 'package.json': json`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "nuxt": "^3.13.1",
+ "tailwindcss": "workspace:^",
+ "vue": "latest"
+ }
+ }
+ `,
+ 'nuxt.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+
+ // https://nuxt.com/docs/api/configuration/nuxt-config
+ export default defineNuxtConfig({
+ vite: {
+ plugins: [tailwindcss()],
+ },
+
+ css: ['~/assets/css/main.css'],
+ devtools: { enabled: true },
+ compatibilityDate: '2024-08-30',
+ })
+ `,
+ 'app.vue': html`
+
+ Hello world!
+
+ `,
+ 'assets/css/main.css': css`@import 'tailwindcss';`,
+ },
+ },
+ async ({ fs, spawn, getFreePort }) => {
+ let port = await getFreePort()
+ await spawn(`pnpm nuxt dev --port ${port}`)
+
+ await retryAssertion(async () => {
+ let css = await fetchStyles(port)
+ expect(css).toContain(candidate`underline`)
+ })
+
+ await fs.write(
+ 'app.vue',
+ html`
+
+ Hello world!
+
+ `,
+ )
+ await retryAssertion(async () => {
+ let css = await fetchStyles(port)
+ expect(css).toContain(candidate`underline`)
+ expect(css).toContain(candidate`font-bold`)
+ })
+ },
+)
diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts
index ad43ec061..65d863548 100644
--- a/packages/@tailwindcss-vite/src/index.ts
+++ b/packages/@tailwindcss-vite/src/index.ts
@@ -11,7 +11,7 @@ import postcssImport from 'postcss-import'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
export default function tailwindcss(): Plugin[] {
- let server: ViteDevServer | null = null
+ let servers: ViteDevServer[] = []
let config: ResolvedConfig | null = null
let isSSR = false
@@ -58,36 +58,35 @@ export default function tailwindcss(): Plugin[] {
}
function invalidateAllRoots(isSSR: boolean) {
- // If we're building then we don't need to update anything
- if (!server) return
-
- let updates: Update[] = []
- for (let id of roots.keys()) {
- let module = server.moduleGraph.getModuleById(id)
- if (!module) {
- // 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.
- if (!isSSR) {
- // It is safe to remove the item here since we're iterating on a copy
- // of the keys.
- roots.delete(id)
+ for (let server of servers) {
+ let updates: Update[] = []
+ for (let id of roots.keys()) {
+ let module = server.moduleGraph.getModuleById(id)
+ if (!module) {
+ // 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.
+ if (!isSSR) {
+ // It is safe to remove the item here since we're iterating on a copy
+ // of the keys.
+ roots.delete(id)
+ }
+ continue
}
- 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(),
+ })
}
- 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 })
+ if (updates.length > 0) {
+ server.hot.send({ type: 'update', updates })
+ }
}
}
@@ -139,8 +138,8 @@ export default function tailwindcss(): Plugin[] {
name: '@tailwindcss/vite:scan',
enforce: 'pre',
- configureServer(_server) {
- server = _server
+ configureServer(server) {
+ servers.push(server)
},
async configResolved(_config) {
@@ -169,7 +168,7 @@ export default function tailwindcss(): Plugin[] {
},
transform(src, id, options) {
let extension = getExtension(id)
- if (extension === '' || extension === 'css') return
+ if (isPotentialCssRootFile(id)) return
scanFile(id, src, extension, options?.ssr ?? false)
},
},
@@ -193,7 +192,7 @@ export default function tailwindcss(): Plugin[] {
// 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 server?.waitForRequestsIdle?.(id)
+ await Promise.all(servers.map((server) => server.waitForRequestsIdle(id)))
}
let generated = await root.generate(src, (file) => this.addWatchFile(file))