Fix issue around resolving paths in @tailwindcss/vite (#19947)

This PR fixes an issue when using `@tailwindcss/vite` and you're trying
to resolve paths. In the latest 4.2.3 release, we added support for
following the vite `aliases` option.

However, some people run into issues because if you just use `@` as an
alias then using `@tailwindcss/typography` wouldn't resolve because it's
a package, and not something local.

With this PR we fix that by making sure that Vite's resolver can
actually resolve to an absolute path. If not, then we fallback to the
resolving that happens in `@tailwindcss/node`.

Fixes: #19946

## Test plan

1. Added an integration test that reproduces this issue, and is now
solved
2. Tested it on a reproduction provided in the corresponding issue

Before:
<img width="1694" height="1856" alt="image"
src="https://github.com/user-attachments/assets/c50f7104-78b3-476d-9e60-0c83e0976a5c"
/>


After:
<img width="1694" height="1856" alt="image"
src="https://github.com/user-attachments/assets/44ea0e3b-f60f-479f-a4a1-203a6230475b"
/>
This commit is contained in:
Robin Malfait 2026-04-21 14:11:46 +02:00 • committed by GitHub
parent 2e3fa490a5
commit 685c19e266
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 230 additions and 5 deletions

View file

@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Experimental_: add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
### Fixed
- Ensure imports in `@import` and `@plugin` still resolve correctly when using Vite aliases in `@tailwindcss/vite` ([#19947](https://github.com/tailwindlabs/tailwindcss/pull/19947))
## [4.2.3] - 2026-04-20
### Fixed

View file

@ -164,6 +164,130 @@ test(
},
)
test(
'resolves at-sign aliases in production build',
{
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'
import { fileURLToPath } from 'node:url'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
alias: [{ find: '@', replacement: fileURLToPath(new URL('.', import.meta.url)) }],
},
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline custom-underline">Hello, world!</div>
</body>
`,
'src/index.css': css`
@import '@/src/styles/base.css';
@plugin '@/src/plugin.js';
`,
'src/styles/base.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
'src/plugin.js': js`
export default function ({ addUtilities }) {
addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
}
`,
},
},
async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
},
)
test(
'resolves package plugins in production build with at-sign aliases',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"tailwind-scrollbar": "^4.0.2",
"tailwindcss-motion": "^1.1.1",
"vite": "^8"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { fileURLToPath } from 'node:url'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
alias: [{ find: '@', replacement: fileURLToPath(new URL('.', import.meta.url)) }],
},
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="scrollbar scrollbar-thumb-red-500 motion-preset-fade">Hello, world!</div>
</body>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin 'tailwind-scrollbar';
@plugin 'tailwindcss-motion';
`,
},
},
async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]
await fs.expectFileToContain(filename, [
candidate`scrollbar`,
candidate`scrollbar-thumb-red-500`,
candidate`motion-preset-fade`,
])
},
)
describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
test(
'resolves aliases in production build',

View file

@ -0,0 +1,80 @@
import { candidate, css, html, js, json, test, ts } from '../utils'
test(
'sveltekit build resolves package plugins',
{
fs: {
'package.json': json`
{
"type": "module",
"scripts": {
"prepare": "svelte-kit sync || echo ''"
},
"dependencies": {
"svelte": "^5",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6",
"@sveltejs/kit": "^2",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "workspace:^",
"vite": "^8"
}
}
`,
'svelte.config.js': js`
import adapter from '@sveltejs/adapter-auto'
export default {
kit: {
adapter: adapter(),
},
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss(), sveltekit()],
})
`,
'src/app.html': html`
<!doctype html>
<html lang="en">
<head>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
`,
'src/routes/+layout.svelte': html`
<script>
import '../app.css'
</script>
<slot />
`,
'src/routes/+page.svelte': html`
<div class="form-input">Hello, world!</div>
`,
'src/app.css': css`
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
`,
},
},
async ({ exec, fs, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('.svelte-kit/output/client/**/*.css')
expect(files.length).toBeGreaterThan(0)
await fs.expectFileToContain(files[0][0], [candidate`form-input`])
},
)

View file

@ -109,4 +109,3 @@ export default {
],
}
```

View file

@ -64,10 +64,19 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
customCssResolver = async (id: string, base: string) => {
let resolved = await cssResolver(id, base, false, isSSR)
if (resolved && !resolved.endsWith('.css')) return undefined
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) => {
let resolved = await jsResolver(id, base, false, isSSR)
if (!resolved) return
if (resolved === id) return
if (!path.isAbsolute(resolved)) return
return resolved
}
customJsResolver = (id: string, base: string) => jsResolver(id, base, false, isSSR)
} else {
type ResolveIdFn = (
environment: Environment,
@ -111,10 +120,19 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
customCssResolver = async (id: string, base: string) => {
let resolved = await cssResolver(env, id, base, false)
if (resolved && !resolved.endsWith('.css')) return undefined
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) => {
let resolved = await jsResolver(env, id, base, false)
if (!resolved) return
if (resolved === id) return
if (!path.isAbsolute(resolved)) return
return resolved
}
customJsResolver = (id: string, base: string) => jsResolver(env, id, base, false)
}
return new Root(