From 685c19e266a6b75842747acab6fe7076e82df62d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 21 Apr 2026 14:11:46 +0200 Subject: [PATCH] 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: image After: image --- CHANGELOG.md | 4 + integrations/vite/resolvers.test.ts | 124 ++++++++++++++++++++++++ integrations/vite/sveltekit.test.ts | 80 +++++++++++++++ packages/@tailwindcss-postcss/README.md | 1 - packages/@tailwindcss-vite/src/index.ts | 26 ++++- 5 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 integrations/vite/sveltekit.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d616d340..f6fbd9bd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/integrations/vite/resolvers.test.ts b/integrations/vite/resolvers.test.ts index 79ce57553..f2f4b8a0b 100644 --- a/integrations/vite/resolvers.test.ts +++ b/integrations/vite/resolvers.test.ts @@ -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` + + + + +
Hello, world!
+ + `, + '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` + + + + +
Hello, world!
+ + `, + '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', diff --git a/integrations/vite/sveltekit.test.ts b/integrations/vite/sveltekit.test.ts new file mode 100644 index 000000000..92e855da9 --- /dev/null +++ b/integrations/vite/sveltekit.test.ts @@ -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` + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + + `, + 'src/routes/+layout.svelte': html` + + + + `, + 'src/routes/+page.svelte': html` +
Hello, world!
+ `, + '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`]) + }, +) diff --git a/packages/@tailwindcss-postcss/README.md b/packages/@tailwindcss-postcss/README.md index 981ffc42d..3facf2887 100644 --- a/packages/@tailwindcss-postcss/README.md +++ b/packages/@tailwindcss-postcss/README.md @@ -109,4 +109,3 @@ export default { ], } ``` - diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index ac3e1c33c..5b376937c 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -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(