diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3837abf85c..5428fa88f59f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Find utilities when using the Angular class shorthand syntax ([#15974](https://github.com/tailwindlabs/tailwindcss/pull/15974)) - Find utilities when using functions inside arrays ([#15974](https://github.com/tailwindlabs/tailwindcss/pull/15974)) - Ensure that `@tailwindcss/browser` does not pollute the global namespace ([#15978](https://github.com/tailwindlabs/tailwindcss/pull/15978)) +- Ensure that `tailwind-merge` is not scanned when using the Vite plugin ([#16005](https://github.com/tailwindlabs/tailwindcss/pull/16005)) - Ensure CSS theme variables are available within shadow roots ([#15975](https://github.com/tailwindlabs/tailwindcss/pull/15975)) - Fix crash when project lives in the `/` directory ([#15988](https://github.com/tailwindlabs/tailwindcss/pull/15988)) - Ensure `@custom-variant` has a non-empty selector list ([#16009](https://github.com/tailwindlabs/tailwindcss/pull/16009)) diff --git a/integrations/vite/ignored-packages.test.ts b/integrations/vite/ignored-packages.test.ts new file mode 100644 index 000000000000..f7f85ff14975 --- /dev/null +++ b/integrations/vite/ignored-packages.test.ts @@ -0,0 +1,81 @@ +import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils' + +const WORKSPACE = { + fs: { + 'package.json': txt` + { + "type": "module", + "dependencies": { + "tailwind-merge": "^2", + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^6" + } + } + `, + '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.js': js` + import { twMerge } from 'tailwind-merge' + + twMerge('underline') + + console.log('underline') + `, + 'src/index.css': css`@import 'tailwindcss/utilities' layer(utilities);`, + }, +} + +test( + 'does not scan tailwind-merge in production builds', + WORKSPACE, + async ({ fs, exec, expect }) => { + await exec('pnpm vite build') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [, content] = files[0] + + expect(content).toMatchInlineSnapshot(` + "@layer utilities { + .underline { + text-decoration-line: underline; + } + } + " + `) + }, +) + +test('does not scan tailwind-merge in dev builds', WORKSPACE, async ({ spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + + expect(styles).not.toContain(candidate`flex`) + }) +}) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 2fedf73231f2..c8976c0276da 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -9,6 +9,8 @@ import type { Plugin, ResolvedConfig, Rollup, Update, ViteDevServer } from 'vite const DEBUG = env.DEBUG const SPECIAL_QUERY_RE = /[?&](raw|url)\b/ +const IGNORED_DEPENDENCIES = ['tailwind-merge'] + export default function tailwindcss(): Plugin[] { let servers: ViteDevServer[] = [] let config: ResolvedConfig | null = null @@ -62,6 +64,18 @@ export default function tailwindcss(): Plugin[] { }) function scanFile(id: string, content: string, extension: string, isSSR: boolean) { + for (let dependency of IGNORED_DEPENDENCIES) { + // We validated that Vite IDs always use posix style path separators, even on Windows. + // In dev build, Vite precompiles dependencies + if (id.includes(`.vite/deps/${dependency}.js`)) { + return + } + // In prod builds, use the node_modules path + if (id.includes(`/node_modules/${dependency}/`)) { + return + } + } + let updated = false for (let candidate of moduleGraphScanner.scanFiles([{ content, extension }])) { updated = true