From 6b36ceb9dcb05c1a3ea14849bcd2c6550796cf8c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 8 Aug 2024 18:17:08 +0200 Subject: [PATCH] Vite: Support Tailwind in Svelte ` + `, + '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 + // `