Support Vite Environment API (#18970)

Fixes #18002

Very much a work in progress b/c I don't (yet) understand how the newer
APIs are intended to function.

- [x] Needs env specific tests that verify the environment API is being
used
This commit is contained in:
Jordan Pittman 2025-11-18 12:38:57 -05:00 • committed by GitHub
parent c532955dd1
commit e4b6c67bcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 120 additions and 24 deletions

View file

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Include filename and line numbers in CSS parse errors ([#19282](https://github.com/tailwindlabs/tailwindcss/pull/19282))
- Skip comments in Ruby files when checking for class names ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
- Skip over arbitrary property utilities with a top-level `!` in the value ([#19243](https://github.com/tailwindlabs/tailwindcss/pull/19243))
- Support environment API in `@tailwindcss/vite` ([#18970](https://github.com/tailwindlabs/tailwindcss/pull/18970))
### Added

View file

@ -1103,3 +1103,63 @@ test(
expect(content).toContain('display: flex;')
},
)
test(
`the plugin works when using the environment API`,
{
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^7"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [tailwindcss()],
builder: {},
environments: {
server: {
build: {
cssMinify: false,
emitAssets: true,
rollupOptions: { input: './src/server.ts' },
},
},
},
})
`,
// Has to exist or the build fails
'index.html': html`
<div class="content-['index.html']"></div>
`,
'src/server.ts': js`
// Import the stylesheet in the server build
import a from './index.css?url'
console.log(a)
`,
'src/index.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
},
},
async ({ root, fs, exec, expect }) => {
await exec('pnpm vite build', { cwd: root })
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
await fs.expectFileToContain(filename, [candidate`content-['index.html']`])
},
)

View file

@ -11,7 +11,8 @@ import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import fs from 'node:fs/promises'
import path from 'node:path'
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
import type { Environment, Plugin, ResolvedConfig, ViteDevServer } from 'vite'
import * as vite from 'vite'
const DEBUG = env.DEBUG
const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/
@ -28,28 +29,51 @@ export type PluginOptions = {
export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
let servers: ViteDevServer[] = []
let config: ResolvedConfig | null = null
let rootsByEnv = new DefaultMap<string, Map<string, Root>>((env: string) => new Map())
let isSSR = false
let shouldOptimize = true
let minify = true
let roots: DefaultMap<string, Root> = new DefaultMap((id) => {
let cssResolver = config!.createResolver({
...config!.resolve,
extensions: ['.css'],
mainFields: ['style'],
conditions: ['style', 'development|production'],
tryIndex: false,
preferRelative: true,
})
function customCssResolver(id: string, base: string) {
return cssResolver(id, base, true, isSSR)
function createRoot(env: Environment | null, id: string) {
type ResolveFn = (id: string, base: string) => Promise<string | false | undefined>
let customCssResolver: ResolveFn
let customJsResolver: ResolveFn
if (!env) {
// Older, pre-environment Vite API
// TODO: Can we drop this??
let cssResolver = config!.createResolver({
...config!.resolve,
extensions: ['.css'],
mainFields: ['style'],
conditions: ['style', 'development|production'],
tryIndex: false,
preferRelative: true,
})
let jsResolver = config!.createResolver(config!.resolve)
customCssResolver = (id: string, base: string) => cssResolver(id, base, true, isSSR)
customJsResolver = (id: string, base: string) => jsResolver(id, base, true, isSSR)
} else {
// Newer Vite versions
let cssResolver = vite.createIdResolver(env.config, {
...env.config.resolve,
extensions: ['.css'],
mainFields: ['style'],
conditions: ['style', 'development|production'],
tryIndex: false,
preferRelative: true,
})
let jsResolver = vite.createIdResolver(env.config, env.config.resolve)
customCssResolver = (id: string, base: string) => cssResolver(env, id, base, true)
customJsResolver = (id: string, base: string) => jsResolver(env, id, base, true)
}
let jsResolver = config!.createResolver(config!.resolve)
function customJsResolver(id: string, base: string) {
return jsResolver(id, base, true, isSSR)
}
return new Root(
id,
config!.root,
@ -59,7 +83,7 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
customCssResolver,
customJsResolver,
)
})
}
return [
{
@ -110,7 +134,12 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
using I = new Instrumentation()
DEBUG && I.start('[@tailwindcss/vite] Generate CSS (serve)')
let roots = rootsByEnv.get(this.environment?.name ?? 'default')
let root = roots.get(id)
if (!root) {
root ??= createRoot(this.environment ?? null, id)
roots.set(id, root)
}
let result = await root.generate(src, (file) => this.addWatchFile(file), I)
if (!result) {
@ -129,7 +158,6 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
name: '@tailwindcss/vite:generate:build',
apply: 'build',
enforce: 'pre',
transform: {
filter: {
id: {
@ -143,7 +171,12 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
using I = new Instrumentation()
DEBUG && I.start('[@tailwindcss/vite] Generate CSS (build)')
let roots = rootsByEnv.get(this.environment?.name ?? 'default')
let root = roots.get(id)
if (!root) {
root ??= createRoot(this.environment ?? null, id)
roots.set(id, root)
}
let result = await root.generate(src, (file) => this.addWatchFile(file), I)
if (!result) {
@ -174,13 +207,15 @@ function getExtension(id: string) {
}
function isPotentialCssRootFile(id: string) {
if (id.includes('/.vite/')) return
if (id.includes('/.vite/')) return false
// Don't intercept special static asset resources
if (SPECIAL_QUERY_RE.test(id)) return false
if (COMMON_JS_PROXY_RE.test(id)) return false
let extension = getExtension(id)
let isCssFile =
(extension === 'css' || id.includes('&lang.css') || id.match(INLINE_STYLE_ID_RE)) &&
// Don't intercept special static asset resources
!SPECIAL_QUERY_RE.test(id) &&
!COMMON_JS_PROXY_RE.test(id)
let isCssFile = extension === 'css' || id.includes('&lang.css') || id.match(INLINE_STYLE_ID_RE)
return isCssFile
}