From 67a8999d160e1871a4770010d6fc7006e60aebed Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 24 Apr 2026 12:43:13 +0200 Subject: [PATCH 1/4] add failing integration test --- integrations/vite/resolvers.test.ts | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/integrations/vite/resolvers.test.ts b/integrations/vite/resolvers.test.ts index 5ff5df5b174b..e392881b9726 100644 --- a/integrations/vite/resolvers.test.ts +++ b/integrations/vite/resolvers.test.ts @@ -513,6 +513,76 @@ test( }, ) +test( + 'resolve relative CSS files correctly', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^8" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + + + + `, + 'src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + @import './themes/glow.css'; + `, + // References a file in the current folder, which names happens to match a + // file in the parent folder as well. + 'src/themes/glow.css': css`@import './entry.css';`, + 'src/themes/entry.css': css` + .do-include-me { + color: green; + } + `, + + // Never rerefenced, so should not be included + 'src/entry.css': css` + .do-not-include-me { + color: red; + } + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm vite build') + + expect((await fs.dumpFiles('./dist/**/*.css')).replace(/-([a-zA-Z0-9]*?)\.css/g, '-.css')) + .toMatchInlineSnapshot(` + " + --- ./dist/assets/index-.css --- + .do-include-me { + color: green; + } + " + `) + }, +) + describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { test( 'resolves aliases in production build', From 0be1b1536505750fb98a3c95d0782dcdb5eddb6f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 24 Apr 2026 13:01:19 +0200 Subject: [PATCH 2/4] add similar test with `@plugin` --- integrations/vite/resolvers.test.ts | 72 +++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/integrations/vite/resolvers.test.ts b/integrations/vite/resolvers.test.ts index e392881b9726..416737960eb1 100644 --- a/integrations/vite/resolvers.test.ts +++ b/integrations/vite/resolvers.test.ts @@ -583,6 +583,78 @@ test( }, ) +test( + 'resolve relative JS files correctly', + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^8" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + + + + `, + 'src/index.css': css` + @reference 'tailwindcss/theme'; + @import 'tailwindcss/utilities'; + @import './themes/glow.css'; + `, + // References a file in the current folder, which names happens to match a + // file in the parent folder as well. + 'src/themes/glow.css': css`@plugin "./my-plugin.js";`, + 'src/themes/my-plugin.js': ts` + export default function ({ addBase }) { + addBase({ '.do-include-me': { color: 'green' } }) + } + `, + + // Never rerefenced, so should not be included + 'src/my-plugin.js': css` + export default function ({ addBase }) { + addBase({ '.do-not-include-me': { 'color': 'red' } }) + } + `, + }, + }, + async ({ exec, fs, expect }) => { + await exec('pnpm vite build') + + expect((await fs.dumpFiles('./dist/**/*.css')).replace(/-([a-zA-Z0-9]*?)\.css/g, '-.css')) + .toMatchInlineSnapshot(` + " + --- ./dist/assets/index-.css --- + @layer base { + .do-include-me { + color: green; + } + } + " + `) + }, +) + describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { test( 'resolves aliases in production build', From b8931036095a1c9472d3e97656c6f4ce2a75f72f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 24 Apr 2026 13:36:17 +0200 Subject: [PATCH 3/4] resolve files from an importer instead of a base dir path --- packages/@tailwindcss-vite/src/index.ts | 118 +++++++++++++----------- 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index b0c25b5d7fe6..c6b2786f7bf1 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -33,6 +33,44 @@ export type PluginOptions = { optimize?: boolean | { minify?: boolean } } +function createCustomResolver( + resolvers: ((id: string, importer: string) => Promise)[], + filter = (_path: string) => true, +) { + return async (id: string, base: string) => { + // The resolver expects an `importer` file. We don't really know where the + // current `id` was imported from, but Vite will essentially do a + // `path.dirname(importer)` so it doesn't really matter. + // + // It does matter that this is a file, otherwise we would go up a directory, + // which means that we would be resolving files from a parent folder first, + // instead of the current folder we are in. + let importer = path.resolve(base, '__placeholder__.css') + + for (let resolver of resolvers) { + let resolved = await resolver(id, importer) + + // If we didn't resolve, we don't have to bail immediately, but we can try + // the next resolver + if (!resolved) continue + + if (resolved === id) continue + + // Looks like a relative file, let's resolve it to an absolute path + if (resolved[0] === '.') resolved = path.resolve(base, resolved) + + // Must adhere to additional filters (e.g.: must be a .css file) + if (!filter(resolved)) continue + + // If it's not an absolute path, then we don't really know how to read + // the file from disk. + if (!path.isAbsolute(resolved)) continue + + return resolved + } + } +} + export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { let servers: ViteDevServer[] = [] let config: ResolvedConfig | null = null @@ -62,32 +100,20 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { let jsResolver = config!.createResolver(config!.resolve) - customCssResolver = async (id: string, base: string) => { - let resolved = await cssResolver(id, base, false, isSSR) - if (!resolved) return - if (resolved === id) return - if (!path.isAbsolute(resolved)) return - if (!resolved.endsWith('.css')) return - return resolved - } - customJsResolver = async (id: string, base: string) => { - // Resolve Vite aliases first so `@plugin "@/foo"` keeps working, but - // let bare package specifiers fall through to Node-style resolution. - let resolved = await jsResolver(id, base, true, isSSR) - if (resolved && resolved !== id) { - if (path.isAbsolute(resolved)) return resolved - if (resolved[0] === '.') return path.resolve(base, resolved) - } - - // Fall back to Vite's full resolver for features like tsconfigPaths, - // but reject CSS results since plugins must resolve to executable code. - resolved = await jsResolver(id, base, false, isSSR) - if (!resolved) return - if (resolved === id) return - if (!path.isAbsolute(resolved)) return - if (resolved.endsWith('.css')) return - return resolved - } + customCssResolver = createCustomResolver( + [ + (id, importer) => cssResolver(id, importer, true, isSSR), + (id, importer) => cssResolver(id, importer, false, isSSR), + ], + (path) => path.endsWith('.css'), + ) + customJsResolver = createCustomResolver( + [ + (id, importer) => jsResolver(id, importer, true, isSSR), + (id, importer) => jsResolver(id, importer, false, isSSR), + ], + (path) => !path.endsWith('.css'), + ) } else { type ResolveIdFn = ( environment: Environment, @@ -129,32 +155,20 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { let jsResolver = createBackCompatIdResolver(env.config, env.config.resolve) - customCssResolver = async (id: string, base: string) => { - let resolved = await cssResolver(env, id, base, false) - if (!resolved) return - if (resolved === id) return - if (!path.isAbsolute(resolved)) return - if (!resolved.endsWith('.css')) return - return resolved - } - customJsResolver = async (id: string, base: string) => { - // Resolve Vite aliases first so `@plugin "@/foo"` keeps working, but - // let bare package specifiers fall through to Node-style resolution. - let resolved = await jsResolver(env, id, base, true) - if (resolved && resolved !== id) { - if (path.isAbsolute(resolved)) return resolved - if (resolved[0] === '.') return path.resolve(base, resolved) - } - - // Fall back to Vite's full resolver for features like tsconfigPaths, - // but reject CSS results since plugins must resolve to executable code. - resolved = await jsResolver(env, id, base, false) - if (!resolved) return - if (resolved === id) return - if (!path.isAbsolute(resolved)) return - if (resolved.endsWith('.css')) return - return resolved - } + customCssResolver = createCustomResolver( + [ + (id, importer) => cssResolver(env, id, importer, true), + (id, importer) => cssResolver(env, id, importer, false), + ], + (path) => path.endsWith('.css'), + ) + customJsResolver = createCustomResolver( + [ + (id, importer) => jsResolver(env, id, importer, true), + (id, importer) => jsResolver(env, id, importer, false), + ], + (path) => !path.endsWith('.css'), + ) } return new Root( From dd81a5ca9af9d4b2fc819c90beae2b44eeb1ae18 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 24 Apr 2026 14:41:05 +0200 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d7e57da1da4..29bb63b49c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Ensure `@plugin` resolves package JavaScript entries instead of browser CSS entries when using `@tailwindcss/vite` ([#19949](https://github.com/tailwindlabs/tailwindcss/pull/19949)) +- Fix relative `@import` and `@plugin` paths resolving from the wrong directory when using `@tailwindcss/vite` ([#19965](https://github.com/tailwindlabs/tailwindcss/pull/19965)) ## [4.2.4] - 2026-04-21