import { describe } from 'vitest'
import {
candidate,
css,
fetchStyles,
html,
js,
json,
retryAssertion,
test,
ts,
txt,
yaml,
} from '../utils'
test(
'resolves tsconfig paths in production build',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^8"
}
}
`,
'tsconfig.json': json`
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
tsconfigPaths: true,
},
})
`,
'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 '@/styles/base.css';
@plugin '@/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 tsconfig paths in dev mode',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^8"
}
}
`,
'tsconfig.json': json`
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
tsconfigPaths: true,
},
})
`,
'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 '@/styles/base.css';
@plugin '@/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 ({ 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).toContain(candidate`underline`)
expect(styles).toContain(candidate`custom-underline`)
})
},
)
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`,
])
},
)
test(
'resolves package plugins to JS entries in production build when browser points to CSS',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"plugin-browser-css": "workspace:*",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^8"
}
}
`,
'pnpm-workspace.yaml': yaml`
#
packages:
- packages/*
`,
'packages/plugin-browser-css/package.json': json`
{
"name": "plugin-browser-css",
"version": "1.0.0",
"type": "module",
"main": "./index.js",
"module": "./index.js",
"browser": "./browser.css"
}
`,
'packages/plugin-browser-css/index.js': js`
export default function ({ addUtilities }) {
addUtilities({ '.browser-css-plugin': { 'border-bottom': '1px solid green' } })
}
`,
'packages/plugin-browser-css/browser.css': css`
.should-not-be-loaded-as-a-plugin {
display: none;
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'index.html': html`
<html>
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="browser-css-plugin">Hello, world!</div>
</body>
</html>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin 'plugin-browser-css';
`,
},
},
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`browser-css-plugin`])
},
)
test(
'resolves package plugins to JS entries in dev mode when browser points to CSS',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"plugin-browser-css": "workspace:*",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^8"
}
}
`,
'pnpm-workspace.yaml': yaml`
#
packages:
- packages/*
`,
'packages/plugin-browser-css/package.json': json`
{
"name": "plugin-browser-css",
"version": "1.0.0",
"type": "module",
"main": "./index.js",
"module": "./index.js",
"browser": "./browser.css"
}
`,
'packages/plugin-browser-css/index.js': js`
export default function ({ addUtilities }) {
addUtilities({ '.browser-css-plugin': { 'border-bottom': '1px solid green' } })
}
`,
'packages/plugin-browser-css/browser.css': css`
.should-not-be-loaded-as-a-plugin {
display: none;
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss()],
})
`,
'index.html': html`
<html>
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="browser-css-plugin">Hello, world!</div>
</body>
</html>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin 'plugin-browser-css';
`,
},
},
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).toContain(candidate`browser-css-plugin`)
})
},
)
test(
'resolves package plugins in dev mode when package exports CSS files',
{
fs: {
'package.json': json`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"daisyui": "^5",
"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`
<html>
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<button class="btn btn-primary">Hello, world!</button>
</body>
</html>
`,
'src/index.css': css`
@import 'tailwindcss';
@plugin 'daisyui';
`,
},
},
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).toContain('.btn')
expect(styles).toContain('.btn-primary')
})
},
)
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`
<html>
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body></body>
</html>
`,
'src/index.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
@import './themes/glow.css';
`,
'src/themes/glow.css': css`@import './entry.css';`,
'src/themes/entry.css': css`
.do-include-me {
color: green;
}
`,
'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, '-<hash>.css'),
).toMatchInlineSnapshot(`
"
--- ./dist/assets/index-<hash>.css ---
.do-include-me {
color: green;
}
"
`)
},
)
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`
<html>
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body></body>
</html>
`,
'src/index.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
@import './themes/glow.css';
`,
'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' } })
}
`,
'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, '-<hash>.css'),
).toMatchInlineSnapshot(`
"
--- ./dist/assets/index-<hash>.css ---
@layer base {
.do-include-me {
color: green;
}
}
"
`)
},
)
describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
test(
'resolves aliases in production build',
{
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
"vite": "^5.3.5"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { fileURLToPath } from 'node:url'
export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
alias: {
'#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
'#js-alias': fileURLToPath(new URL('./src/plugin.js', 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 '#css-alias';
@plugin '#js-alias';
`,
'src/alias.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 aliases in dev mode',
{
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
${transformer === 'lightningcss' ? `"lightningcss": "^1",` : ''}
"vite": "^5.3.5"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { fileURLToPath } from 'node:url'
export default defineConfig({
css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
build: { cssMinify: false },
plugins: [tailwindcss()],
resolve: {
alias: {
'#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
'#js-alias': fileURLToPath(new URL('./src/plugin.js', 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 '#css-alias';
@plugin '#js-alias';
`,
'src/alias.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 ({ 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).toContain(candidate`underline`)
expect(styles).toContain(candidate`custom-underline`)
})
},
)
})