Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions integrations/vite/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']`])
},
)
83 changes: 59 additions & 24 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Comment on lines +14 to 16
Copy link

@coderabbitai coderabbitai bot Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Environment-aware resolver wiring via createIdResolver looks incorrect and may not match Vite’s API.

In createRoot’s env-aware branch you do:

let cssResolver = vite.createIdResolver(env.config, { ... })
let jsResolver = vite.createIdResolver(env.config, env.config.resolve)

customCssResolver = (id, base) => cssResolver(env, id, base, true)
customJsResolver = (id, base) => jsResolver(env, id, base, true)

But Vite’s createIdResolver currently returns a ResolveIdFn-style function whose signature is (id, importer?, aliasOnly?, ssr?), i.e. it does not take an Environment argument first. Passing env as the first parameter here is very likely to both:

  • Break types against the published vite d.ts.
  • Call the resolver with mis-ordered arguments at runtime (treating env as the id, etc.), causing incorrect resolution or hard-to-debug errors.

The legacy !env path correctly adapts config!.createResolver:

customCssResolver = (id, base) => cssResolver(id, base, true, isSSR)
customJsResolver  = (id, base) => jsResolver(id, base, true, isSSR)

The env-aware branch should mirror this style, with environment-specific behavior coming from env.config (and potentially from environment-specific resolve options), not from an extra env parameter to the returned function. For example:

const cssResolver = vite.createIdResolver(env.config, {
  ...env.config.resolve,
  extensions: ['.css'],
  mainFields: ['style'],
  conditions: ['style', 'development|production'],
  tryIndex: false,
  preferRelative: true,
})

const jsResolver = vite.createIdResolver(env.config, env.config.resolve)

customCssResolver = (id, base) => cssResolver(id, base, true /* aliasOnly */)
customJsResolver  = (id, base) => jsResolver(id, base, true /* aliasOnly */)

This keeps the adapter shape uniform between legacy and env-aware paths and avoids depending on an undocumented resolver signature.

Please double‑check the current vite typings / implementation for createIdResolver against your pinned Vite version and ensure the adapter here matches the actual ResolveIdFn signature. If desired, you can verify with a minimal TS snippet compiling against that version of vite:

Also applies to: 32-32, 38-60, 62-75, 77-85


🏁 Script executed:

#!/bin/bash
# Quick check of createIdResolver signature in your repo
fd 'vite.d.ts' node_modules -x sed -n '1,260p' {}

Length of output: 99






🏁 Script executed:

#!/bin/bash
# Examine the actual createRoot and resolver implementations
cat packages/@tailwindcss-vite/src/index.ts | head -150

Length of output: 4634


🌐 Web query:

Vite createIdResolver API function signature parameters

💡 Result:

Signature (TypeScript):

createIdResolver(config: ResolvedConfig, options?: Partial) => ResolveIdFn

Parameters:

  • config: ResolvedConfig — the resolved Vite config.
  • options?: Partial — optional partial resolve options (e.g. root, isProduction, asSrc, ssrConfig, preferRelative, etc.).

Return:

  • ResolveIdFn (a resolver function used internally, e.g. by optimizer / CSS handling). [1][2]

Sources:
[1] Vite API docs (createResolver / createIdResolver).
[2] vite package API listing (jsdocs.io).



---

🌐 Web query:

```web
Vite ResolveIdFn signature type definition

💡 Result:

The ResolveIdFn type (internal Vite resolver) has the following TypeScript signature:

(this: PluginContext, source: string, importer?: string, options?: {
attributes?: Record<string,string>;
custom?: CustomPluginOptions;
skip?: Set;
skipCalls?: readonly SkipInformation[];
ssr?: boolean;
isEntry?: boolean;
}) => Promise | ResolveIdResult. [1][2]

Sources:

  • Vite types (dist/node index) — jsDocs summary. [1]
  • Example ResolverObject / alias types referencing resolveId signature (UNPKG). [2]


---

🌐 Web query:

```web
Vite internal createIdResolver returns function type what parameters

💡 Result:

createIdResolver(...) returns a Resolve function with signature roughly:

(id: string, importer?: string, aliasOnly?: boolean, ssr?: boolean) => Promise<string | undefined>.

Sources: jsdocs listing of Vite types (createIdResolver / ResolveFn) [1], Vite source examples using ResolveIdFn / ResolveFn [2].


Environment-aware resolver wiring passes env as wrong parameter, breaking resolver function contract.

In createRoot's env-aware branch (lines 72–73), you pass the environment as the first argument to the resolver:

customCssResolver = (id: string, base: string) => cssResolver(env, id, base, true)
customJsResolver = (id: string, base: string) => jsResolver(env, id, base, true)

However, the function returned by createIdResolver has signature (id: string, importer?: string, aliasOnly?: boolean, ssr?: boolean). Passing env as the first parameter treats it as the id, shifting all subsequent arguments—causing incorrect resolution and potential runtime errors.

The legacy branch (line 58) correctly aligns arguments:

customCssResolver = (id: string, base: string) => cssResolver(id, base, true, isSSR)

Since you already pass env.config to createIdResolver itself, environment-specific behavior is already encoded in the returned resolver. Remove the env parameter from the adapter calls:

customCssResolver = (id: string, base: string) => cssResolver(id, base, true)
customJsResolver = (id: string, base: string) => jsResolver(id, base, true)
🤖 Prompt for AI Agents
In packages/@tailwindcss-vite/src/index.ts around lines 72 to 73, the env-aware
branch incorrectly passes env as the first argument to the resolver adapter
(shifting the resolver's parameters); update the adapter calls to match the
resolver signature by removing the env argument so each adapter calls the
resolver as (id, base, true) — the environment was already baked into
createIdResolver via env.config, so simply drop env from those calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got the types wrong. The types for config.createResolver and vite.createIdResolver differ.

The function vite.createIdResolver returns a ResolveIdFn:

declare function createIdResolver(config: ResolvedConfig, options?: Partial<InternalResolveOptions>): ResolveIdFn;

The type declaration for ResolveIdFn:

type ResolveIdFn = (environment: PartialEnvironment, id: string, importer?: string, aliasOnly?: boolean) => Promise<string | undefined>

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thecrypticace, thank you for the correction! You're absolutely right. I apologize for the incorrect analysis.

The new Vite Environment API's createIdResolver does indeed return a ResolveIdFn that takes environment as the first parameter:

type ResolveIdFn = (environment: PartialEnvironment, id: string, importer?: string, aliasOnly?: boolean) => Promise<string | undefined>

So the code at lines 72-73 is correct as written:

customCssResolver = (id: string, base: string) => cssResolver(env, id, base, true)
customJsResolver = (id: string, base: string) => jsResolver(env, id, base, true)

This properly passes the environment as the first argument to match the new API signature. The implementation correctly handles both the legacy config.createResolver API (which doesn't take an environment parameter) and the newer vite.createIdResolver API (which does).


✏️ Learnings added
Learnt from: thecrypticace
Repo: tailwindlabs/tailwindcss PR: 18970
File: packages/@tailwindcss-vite/src/index.ts:14-16
Timestamp: 2025-11-14T18:55:26.993Z
Learning: In Vite's Environment API, `vite.createIdResolver(config, options)` returns a `ResolveIdFn` with signature `(environment: PartialEnvironment, id: string, importer?: string, aliasOnly?: boolean) => Promise<string | undefined>`. The environment must be passed as the first parameter when calling the resolver.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

const DEBUG = env.DEBUG
const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/
Expand All @@ -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 = config!.createResolver(config!.resolve)
function customJsResolver(id: string, base: string) {
return jsResolver(id, base, true, isSSR)
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)
}

return new Root(
id,
config!.root,
Expand All @@ -59,7 +83,7 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
customCssResolver,
customJsResolver,
)
})
}

return [
{
Expand Down Expand Up @@ -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) {
Expand All @@ -129,7 +158,6 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
name: '@tailwindcss/vite:generate:build',
apply: 'build',
enforce: 'pre',

transform: {
filter: {
id: {
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}

Expand Down