diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts
index c114828bc..5d7b08af6 100644
--- a/integrations/vite/index.test.ts
+++ b/integrations/vite/index.test.ts
@@ -1,5 +1,5 @@
import path from 'node:path'
-import { expect } from 'vitest'
+import { describe, expect } from 'vitest'
import {
candidate,
css,
@@ -12,206 +12,342 @@ import {
ts,
yaml,
} from '../utils'
+;['postcss', 'lightningcss'].forEach((transformer) => {
+ describe.concurrent(transformer, () => {
+ test(
+ `production build`,
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'pnpm-workspace.yaml': yaml`
+ #
+ packages:
+ - project-a
+ `,
+ 'project-a/package.json': json`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ "vite": "^5.3.5"
+ }
+ }
+ `,
+ 'project-a/vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
-test(
- 'production build',
- {
- fs: {
- 'package.json': json`{}`,
- 'pnpm-workspace.yaml': yaml`
- #
- packages:
- - project-a
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'project-a/index.html': html`
+
+
+
+
+ Hello, world!
+
`,
- 'project-a/package.json': json`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- "vite": "^5.3.5"
- }
- }
- `,
- 'project-a/vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
+ 'project-a/src/index.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ @source '../../project-b/src/**/*.js';
+ `,
+ 'project-b/src/index.js': js`
+ const className = "content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
+ },
+ },
+ async ({ root, fs, exec }) => {
+ await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
- export default defineConfig({
- build: { cssMinify: false },
- plugins: [tailwindcss()],
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [filename] = files[0]
+
+ await fs.expectFileToContain(filename, [
+ candidate`underline`,
+ candidate`m-2`,
+ candidate`content-['project-b/src/index.js']`,
+ ])
+ },
+ )
+
+ test(
+ `dev mode`,
+ {
+ fs: {
+ 'package.json': json`{}`,
+ 'pnpm-workspace.yaml': yaml`
+ #
+ packages:
+ - project-a
+ `,
+ 'project-a/package.json': json`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ "vite": "^5.3.5"
+ }
+ }
+ `,
+ 'project-a/vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ })
+ `,
+ 'project-a/index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'project-a/about.html': html`
+
+
+
+
+ Tailwind Labs
+
+ `,
+ 'project-a/src/index.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ @source '../../project-b/src/**/*.js';
+ `,
+ 'project-b/src/index.js': js`
+ const className = "content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
+ },
+ },
+ async ({ root, spawn, getFreePort, fs }) => {
+ let port = await getFreePort()
+ await spawn(`pnpm vite dev --port ${port}`, {
+ cwd: path.join(root, 'project-a'),
})
- `,
- 'project-a/index.html': html`
-
-
-
-
- Hello, world!
-
- `,
- 'project-a/src/index.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- @source '../../project-b/src/**/*.js';
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- },
- },
- async ({ root, fs, exec }) => {
- await exec('pnpm vite build', { cwd: path.join(root, 'project-a') })
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(1)
- let [filename] = files[0]
-
- await fs.expectFileToContain(filename, [
- candidate`underline`,
- candidate`m-2`,
- candidate`content-['project-b/src/index.js']`,
- ])
- },
-)
-
-test(
- 'dev mode',
- {
- fs: {
- 'package.json': json`{}`,
- 'pnpm-workspace.yaml': yaml`
- #
- packages:
- - project-a
- `,
- 'project-a/package.json': json`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- "vite": "^5.3.5"
- }
- }
- `,
- 'project-a/vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
-
- export default defineConfig({
- build: { cssMinify: false },
- plugins: [tailwindcss()],
+ // 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`)
})
- `,
- 'project-a/index.html': html`
+
+ // 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`
-
-
-
- Hello, world!
-
+
+
+
+ Hello, world!
+
`,
- 'project-a/about.html': html`
-
-
-
-
- Tailwind Labs
-
- `,
- 'project-a/src/index.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- @source '../../project-b/src/**/*.js';
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- },
- },
- async ({ root, spawn, getFreePort, fs }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`, {
- cwd: path.join(root, 'project-a'),
- })
+ )
+ 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`)
+ })
- // 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`)
- })
+ // Manually added `@source`s are watched and trigger a rebuild
+ await fs.write(
+ 'project-b/src/index.js',
+ js`
+ const className = "[.changed_&]:content-['project-b/src/index.js']"
+ module.exports = { className }
+ `,
+ )
+ 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']`)
+ })
- // 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`)
- })
+ // 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')}
- // Updates are additive and cause new candidates to be added.
- await fs.write(
- 'project-a/index.html',
- html`
-
-
-
-
- Hello, world!
-
- `,
+ .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`)
+ })
+ },
)
- 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`
- const className = "[.changed_&]:content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- )
- 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']`)
- })
+ // test(
+ // 'watch mode',
+ // {
+ // fs: {
+ // 'package.json': json`{}`,
+ // 'pnpm-workspace.yaml': yaml`
+ // #
+ // packages:
+ // - project-a
+ // `,
+ // 'project-a/package.json': json`
+ // {
+ // "type": "module",
+ // "dependencies": {
+ // "@tailwindcss/vite": "workspace:^",
+ // "tailwindcss": "workspace:^"
+ // },
+ // "devDependencies": {
+ // "vite": "^5.3.5"
+ // }
+ // }
+ // `,
+ // 'project-a/vite.config.ts': ts`
+ // import tailwindcss from '@tailwindcss/vite'
+ // import { defineConfig } from 'vite'
- // 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')}
+ // export default defineConfig({
+ // build: { cssMinify: false },
+ // plugins: [tailwindcss()],
+ // })
+ // `,
+ // 'project-a/index.html': html`
+ //
+ //
+ //
+ //
+ // Hello, world!
+ //
+ // `,
+ // 'project-a/src/index.css': css`
+ // @import 'tailwindcss/theme' theme(reference);
+ // @import 'tailwindcss/utilities';
+ // @source '../../project-b/src/**/*.js';
+ // `,
+ // 'project-b/src/index.js': js`
+ // const className = "content-['project-b/src/index.js']"
+ // module.exports = { className }
+ // `,
+ // },
+ // },
+ // async ({ root, spawn, fs }) => {
+ // await spawn(`pnpm vite build --watch`, {
+ // cwd: path.join(root, 'project-a'),
+ // })
- .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`)
- })
- },
-)
+ // let filename = ''
+ // await retryAssertion(async () => {
+ // let files = await fs.glob('project-a/dist/**/*.css')
+ // expect(files).toHaveLength(1)
+ // filename = files[0][0]
+ // })
+
+ // await fs.expectFileToContain(filename, [candidate`underline`])
+
+ // await new Promise((resolve) => setTimeout(resolve, 1000))
+
+ // // Updates are additive and cause new candidates to be added.
+ // await fs.write(
+ // 'project-a/index.html',
+ // html`
+ //
+ //
+ //
+ //
+ // Hello, world!
+ //
+ // `,
+ // )
+ // await retryAssertion(async () => {
+ // let files = await fs.glob('project-a/dist/**/*.css')
+ // let filenames = files.map(([filename]) => filename)
+ // // Expect a rebuild to have a different .css hash
+ // expect(filenames).not.toContain(filename)
+ // expect(filenames).toHaveLength(1)
+ // filename = filenames[0]
+ // })
+ // await fs.expectFileToContain(filename, [candidate`underline`, candidate`m-2`])
+
+ // // Manually added `@source`s are watched and trigger a rebuild
+ // await fs.write(
+ // 'project-b/src/index.js',
+ // js`
+ // const className = "[.changed_&]:content-['project-b/src/index.js']"
+ // module.exports = { className }
+ // `,
+ // )
+ // await fs.expectFileToContain(filename, [
+ // candidate`underline`,
+ // candidate`m-2`,
+ // 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 files = await fs.glob('project-a/dist/**/*.css')
+ // let filenames = files.map(([filename]) => filename)
+ // // Expect a rebuild to have a different .css hash
+ // expect(filenames).not.toContain(filename)
+ // expect(filenames).toHaveLength(1)
+ // filename = filenames[0]
+ // })
+ // await fs.expectFileToContain(filename, [
+ // candidate`red`,
+ // candidate`underline`,
+ // candidate`m-2`,
+ // candidate`[.changed_&]:content-['project-b/src/index.js']`,
+ // ])
+ // },
+ // )
+ })
+})
diff --git a/packages/@tailwindcss-vite/package.json b/packages/@tailwindcss-vite/package.json
index cd3aa761e..c032b4ee2 100644
--- a/packages/@tailwindcss-vite/package.json
+++ b/packages/@tailwindcss-vite/package.json
@@ -31,13 +31,15 @@
"@tailwindcss/node": "workspace:^",
"@tailwindcss/oxide": "workspace:^",
"lightningcss": "catalog:",
- "postcss-load-config": "^6.0.1",
+ "postcss": "^8.4.41",
+ "postcss-import": "^16.1.0",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"@types/node": "catalog:",
- "vite": "catalog:",
- "internal-postcss-fix-relative-paths": "workspace:^"
+ "@types/postcss-import": "^14.0.3",
+ "internal-postcss-fix-relative-paths": "workspace:^",
+ "vite": "catalog:"
},
"peerDependencies": {
"vite": "^5.2.0"
diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts
index 9314f8cf0..235575b69 100644
--- a/packages/@tailwindcss-vite/src/index.ts
+++ b/packages/@tailwindcss-vite/src/index.ts
@@ -4,9 +4,11 @@ import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import fixRelativePathsPlugin, { normalizePath } from 'internal-postcss-fix-relative-paths'
import { Features, transform } from 'lightningcss'
+import fs from 'node:fs/promises'
import { pathToFileURL } from 'node:url'
import path from 'path'
-import postcssrc from 'postcss-load-config'
+import postcss from 'postcss'
+import postcssImport from 'postcss-import'
import { compile } from 'tailwindcss'
import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite'
@@ -16,23 +18,19 @@ export default function tailwindcss(): Plugin[] {
let scanner: Scanner | null = null
let changedContent: { content: string; extension: string }[] = []
let candidates = new Set()
- let fullRebuildPaths: string[] = []
+ let fullRebuildPaths = new Set()
+ let rebuildStrategy: 'full' | 'incremental' = 'full'
+ let compiler: null | Awaited> = null
// 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.
- let cssModules: Record<
- string,
- {
- content: string
- handled: boolean
- }
- > = {}
+ let cssModules: Record = {}
let isSSR = false
let minify = false
let cssPlugins: readonly Plugin[] = []
// Trigger update to all CSS modules
- function updateCssModules(isSSR: boolean) {
+ function queueRebuild(isSSR: boolean) {
// If we're building then we don't need to update anything
if (!server) return
@@ -82,56 +80,85 @@ export default function tailwindcss(): Plugin[] {
return updated
}
+ function onFileChange(src: string) {
+ if (fullRebuildPaths.has(idToPath(src))) {
+ rebuildStrategy = 'full'
+ return
+ }
+ }
+
async function generateCss(css: string, inputPath: string, addWatchFile: (file: string) => void) {
await import('@tailwindcss/node/esm-cache-hook')
let inputBasePath = path.dirname(path.resolve(inputPath))
- clearRequireCache(fullRebuildPaths)
- fullRebuildPaths = []
- let { build, globs } = await compile(css, {
- loadPlugin: async (pluginPath) => {
- if (pluginPath[0] !== '.') {
- return import(pluginPath).then((m) => m.default ?? m)
- }
- let resolvedPath = path.resolve(inputBasePath, pluginPath)
- let [module, moduleDependencies] = await Promise.all([
- import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
- getModuleDependencies(resolvedPath),
- ])
+ if (compiler === null || rebuildStrategy === 'full') {
+ rebuildStrategy = 'incremental'
+ clearRequireCache(Array.from(fullRebuildPaths))
+ fullRebuildPaths.clear()
+ fullRebuildPaths.add(idToPath(inputPath))
- addWatchFile(resolvedPath)
- fullRebuildPaths.push(resolvedPath)
- for (let file of moduleDependencies) {
- addWatchFile(file)
- fullRebuildPaths.push(file)
- }
- return module.default ?? module
- },
+ // Resolve `@import`s
+ let postcssCompiled = await postcss([
+ postcssImport({
+ load(path) {
+ fullRebuildPaths.add(path)
+ addWatchFile(path)
+ return fs.readFile(path, 'utf8')
+ },
+ }),
+ fixRelativePathsPlugin(),
+ ]).process(css, {
+ from: inputPath,
+ to: inputPath,
+ })
+ css = postcssCompiled.css
- loadConfig: async (configPath) => {
- if (configPath[0] !== '.') {
- return import(configPath).then((m) => m.default ?? m)
- }
+ compiler = await compile(css, {
+ loadPlugin: async (pluginPath) => {
+ if (pluginPath[0] !== '.') {
+ return import(pluginPath).then((m) => m.default ?? m)
+ }
- let resolvedPath = path.resolve(inputBasePath, configPath)
- let [module, moduleDependencies] = await Promise.all([
- import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
- getModuleDependencies(resolvedPath),
- ])
+ let resolvedPath = path.resolve(inputBasePath, pluginPath)
+ let [module, moduleDependencies] = await Promise.all([
+ import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
+ getModuleDependencies(resolvedPath),
+ ])
- addWatchFile(resolvedPath)
- fullRebuildPaths.push(resolvedPath)
- for (let file of moduleDependencies) {
- addWatchFile(file)
- fullRebuildPaths.push(file)
- }
- return module.default ?? module
- },
- })
+ addWatchFile(resolvedPath)
+ fullRebuildPaths.add(resolvedPath)
+ for (let file of moduleDependencies) {
+ addWatchFile(file)
+ fullRebuildPaths.add(file)
+ }
+ return module.default ?? module
+ },
+
+ loadConfig: async (configPath) => {
+ if (configPath[0] !== '.') {
+ return import(configPath).then((m) => m.default ?? m)
+ }
+
+ let resolvedPath = path.resolve(inputBasePath, configPath)
+ let [module, moduleDependencies] = await Promise.all([
+ import(pathToFileURL(resolvedPath).href + '?id=' + Date.now()),
+ getModuleDependencies(resolvedPath),
+ ])
+
+ addWatchFile(resolvedPath)
+ fullRebuildPaths.add(resolvedPath)
+ for (let file of moduleDependencies) {
+ addWatchFile(file)
+ fullRebuildPaths.add(file)
+ }
+ return module.default ?? module
+ },
+ })
+ }
scanner = new Scanner({
- sources: globs.map((pattern) => ({
+ sources: compiler.globs.map((pattern) => ({
base: inputBasePath, // Globs are relative to the input.css file
pattern,
})),
@@ -170,7 +197,7 @@ export default function tailwindcss(): Plugin[] {
addWatchFile(path.posix.join(relative, glob.pattern))
}
- return build(Array.from(candidates))
+ return compiler.build(Array.from(candidates))
}
async function generateOptimizedCss(
@@ -239,106 +266,57 @@ export default function tailwindcss(): Plugin[] {
...(config.command === 'build' ? ['vite:css-post'] : []),
]
+ console.log('plugins')
+ console.log(config.plugins)
cssPlugins = config.plugins.filter((plugin) => {
return allowedPlugins.includes(plugin.name)
})
},
- // Append the postcss-fix-relative-paths plugin
- async config(config) {
- let postcssConfig = config.css?.postcss
-
- if (typeof postcssConfig === 'string') {
- // We expand string configs to their PostCSS config object similar to
- // how Vite does it.
- // See: https://github.com/vitejs/vite/blob/440783953a55c6c63cd09ec8d13728dc4693073d/packages/vite/src/node/plugins/css.ts#L1580
- let searchPath = typeof postcssConfig === 'string' ? postcssConfig : config.root
- let parsedConfig = await postcssrc({}, searchPath).catch((e: Error) => {
- if (!e.message.includes('No PostCSS Config found')) {
- if (e instanceof Error) {
- let { name, message, stack } = e
- e.name = 'Failed to load PostCSS config'
- e.message = `Failed to load PostCSS config (searchPath: ${searchPath}): [${name}] ${message}\n${stack}`
- e.stack = '' // add stack to message to retain stack
- throw e
- } else {
- throw new Error(`Failed to load PostCSS config: ${e}`)
- }
- }
- return null
- })
- if (parsedConfig !== null) {
- postcssConfig = {
- options: parsedConfig.options,
- plugins: parsedConfig.plugins,
- } as any
- } else {
- postcssConfig = {}
- }
- config.css = { postcss: postcssConfig }
- }
-
- // postcssConfig is no longer a string after the above. This test is to
- // avoid TypeScript errors below.
- if (typeof postcssConfig === 'string') {
- return
- }
-
- if (!postcssConfig || !postcssConfig?.plugins) {
- config.css = config.css || {}
- config.css.postcss = postcssConfig || {}
- config.css.postcss.plugins = [fixRelativePathsPlugin() as any]
- } else {
- postcssConfig.plugins.push(fixRelativePathsPlugin() as any)
- }
- },
-
// Scan index.html for candidates
- transformIndexHtml(html) {
- let updated = scan(html, 'html')
+ transformIndexHtml(html, context) {
+ onFileChange(context.filename)
- // In serve mode, if the generated CSS contains a URL that causes the
- // browser to load a page (e.g. an URL to a missing image), triggering a
- // CSS update will cause an infinite loop. We only trigger if the
- // candidates have been updated.
+ let updated = scan(html, 'html')
if (updated) {
- updateCssModules(isSSR)
+ queueRebuild(isSSR)
}
},
// Scan all non-CSS files for candidates
transform(src, id, options) {
+ onFileChange(id)
+
if (id.includes('/.vite/')) return
let extension = getExtension(id)
if (extension === '' || extension === 'css') return
- scan(src, extension)
- updateCssModules(options?.ssr ?? false)
+ let updated = scan(src, extension)
+ if (updated) {
+ queueRebuild(options?.ssr ?? false)
+ }
},
},
- /*
- * The plugins that generate CSS must run after 'enforce: pre' so @imports
- * are expanded in transform.
- */
-
{
// Step 2 (serve mode): Generate CSS
name: '@tailwindcss/vite:generate:serve',
apply: 'serve',
+ enforce: 'pre',
async transform(src, id, options) {
if (!isTailwindCssFile(id, src)) return
+ onFileChange(id)
// In serve mode, we treat cssModules as a set, ignoring the value.
cssModules[id] = { content: '', handled: true }
- 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.
- await server?.waitForRequestsIdle?.(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.
+ // await server?.waitForRequestsIdle?.(id)
+ // }
let code = await transformWithPlugins(
this,
@@ -353,10 +331,23 @@ export default function tailwindcss(): Plugin[] {
// Step 2 (full build): Generate CSS
name: '@tailwindcss/vite:generate:build',
apply: 'build',
+ enforce: 'pre',
- transform(src, id) {
+ async transform(src, id) {
if (!isTailwindCssFile(id, src)) return
+ onFileChange(id)
+
cssModules[id] = { content: src, handled: false }
+
+ // We do a first pass to generate valid CSS for the downstream plugins.
+ // However, it's likely that we need to regenerate this later once all
+ // candidates have been extracted.
+ let code = await transformWithPlugins(
+ this,
+ id,
+ await generateCss(src, id, (file) => this.addWatchFile(file)),
+ )
+ return { code }
},
// renderChunk runs in the bundle generation stage after all transforms.
@@ -390,7 +381,7 @@ function getExtension(id: string) {
function isTailwindCssFile(id: string, src: string) {
let extension = getExtension(id)
let isCssFile = extension === 'css' || (extension === 'vue' && id.includes('&lang.css'))
- return isCssFile && src.includes('@tailwind')
+ return isCssFile
}
function optimizeCss(
@@ -416,3 +407,7 @@ function optimizeCss(
errorRecovery: true,
}).code.toString()
}
+
+function idToPath(id: string) {
+ return path.resolve(id.replace(/\?.*$/, ''))
+}
diff --git a/playgrounds/vite/src/app.css b/playgrounds/vite/src/app.css
index adbd8e19f..ba7a2f60a 100644
--- a/playgrounds/vite/src/app.css
+++ b/playgrounds/vite/src/app.css
@@ -1,2 +1,3 @@
@import 'tailwindcss';
+@import './bar.css';
@plugin "./plugin.js";
diff --git a/playgrounds/vite/src/bar.css b/playgrounds/vite/src/bar.css
new file mode 100644
index 000000000..c6dd2c88f
--- /dev/null
+++ b/playgrounds/vite/src/bar.css
@@ -0,0 +1,3 @@
+.red {
+ color: red;
+}
diff --git a/playgrounds/vite/src/bar.tsx b/playgrounds/vite/src/bar.tsx
index 1efa2c004..b4b9f70c1 100644
--- a/playgrounds/vite/src/bar.tsx
+++ b/playgrounds/vite/src/bar.tsx
@@ -1,7 +1,7 @@
export function Bar() {
return (
-
Bar
+ Bar
)
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d51514c73..f6a41b4b4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -283,10 +283,25 @@ importers:
version: link:../../crates/node
lightningcss:
specifier: 'catalog:'
+<<<<<<< HEAD
version: 1.26.0(patch_hash=5hwfyehqvg5wjb7mwtdvubqbl4)
postcss-load-config:
specifier: ^6.0.1
version: 6.0.1(postcss@8.4.41)
+||||||| parent of 9ab6a7c5 (Simplify Vite plugin and handle all CSS transforms in pre-transform hook)
+ version: 1.26.0
+ postcss-load-config:
+ specifier: ^6.0.1
+ version: 6.0.1(postcss@8.4.41)
+=======
+ version: 1.26.0
+ postcss:
+ specifier: ^8.4.41
+ version: 8.4.41
+ postcss-import:
+ specifier: ^16.1.0
+ version: 16.1.0(postcss@8.4.41)
+>>>>>>> 9ab6a7c5 (Simplify Vite plugin and handle all CSS transforms in pre-transform hook)
tailwindcss:
specifier: workspace:^
version: link:../tailwindcss
@@ -294,6 +309,9 @@ importers:
'@types/node':
specifier: 'catalog:'
version: 20.14.13
+ '@types/postcss-import':
+ specifier: ^14.0.3
+ version: 14.0.3
internal-postcss-fix-relative-paths:
specifier: workspace:^
version: link:../internal-postcss-fix-relative-paths