Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions integrations/vite/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,199 @@ test(
},
)

test(
'optimize option: advanced Lightning CSS settings',
{
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"lightningcss": "^1",
"vite": "^7"
}
}
`,
'vite.config.ts': ts`
import { Features } from 'lightningcss'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'

export default defineConfig({
build: { cssMinify: false },
plugins: [
tailwindcss({
optimize: {
include: Features.Nesting,
targets: { chrome: 999 << 16 },
drafts: { customMedia: false },
nonStandard: { deepSelectorCombinator: true },
},
}),
],
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="hover:flex min-[700px]:grid custom">Hello, world!</div>
</body>
`,
'src/index.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';

@custom-media --viewport-medium (width >= 700px);

@media (--viewport-medium) {
.custom {
display: flex;
}
}
`,
},
},
async ({ exec, expect, fs }) => {
await exec('pnpm vite build')

let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]

let content = await fs.read(filename)
expect(content).toContain('.hover\\:flex:hover {')
expect(content).toContain('@media (width >= 700px) {')
expect(content).toContain('@custom-media --viewport-medium (width >= 700px);')
expect(content).toContain('@media (--viewport-medium) {')
},
)

test(
'optimize option: advanced Lightning CSS exclude',
{
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"lightningcss": "^1",
"vite": "^7"
}
}
`,
'vite.config.ts': ts`
import { Features } from 'lightningcss'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'

export default defineConfig({
build: { cssMinify: false },
plugins: [
tailwindcss({
optimize: {
minify: false,
include: Features.MediaQueries,
exclude: Features.Nesting,
},
}),
],
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="hover:flex">Hello, world!</div>
</body>
`,
'src/index.css': css`
@reference 'tailwindcss/theme';
@import 'tailwindcss/utilities';
`,
},
},
async ({ exec, expect, fs }) => {
await exec('pnpm vite build')

let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]

let content = await fs.read(filename)
expect(content).toContain('.hover\\:flex {')
expect(content).toContain('&:hover {')
},
)

test(
'polyfills option: disabled',
{
fs: {
'package.json': txt`
{
"type": "module",
"dependencies": {
"@tailwindcss/vite": "workspace:^",
"tailwindcss": "workspace:^"
},
"devDependencies": {
"vite": "^7"
}
}
`,
'vite.config.ts': ts`
import tailwindcss from '@tailwindcss/vite'
import { Polyfills } from 'tailwindcss'
import { defineConfig } from 'vite'

export default defineConfig({
build: { cssMinify: false },
plugins: [tailwindcss({ optimize: false, polyfills: Polyfills.None })],
})
`,
'index.html': html`
<head>
<link rel="stylesheet" href="./src/index.css" />
</head>
<body>
<div class="underline">Hello, world!</div>
</body>
`,
'src/index.css': css`
@import 'tailwindcss/utilities';

@property --no-inherit-value {
syntax: '*';
inherits: false;
initial-value: red;
}
`,
},
},
async ({ exec, expect, fs }) => {
await exec('pnpm vite build')

let files = await fs.glob('dist/**/*.css')
expect(files).toHaveLength(1)
let [filename] = files[0]

let content = await fs.read(filename)
expect(content).toContain('@property --no-inherit-value')
expect(content).not.toContain('@layer properties')
},
)

test(
`the plugin works when using the environment API`,
{
Expand Down
52 changes: 41 additions & 11 deletions packages/@tailwindcss-node/src/optimize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import remapping from '@jridgewell/remapping'
import { Features, transform } from 'lightningcss'
import { Features, transform, type Drafts, type NonStandard, type Targets } from 'lightningcss'
import MagicString from 'magic-string'

export interface OptimizeOptions {
Expand All @@ -13,6 +13,31 @@ export interface OptimizeOptions {
*/
minify?: boolean

/**
* The browser targets for the generated code.
*/
targets?: Targets

/**
* Features that should always be compiled, even when supported by targets.
*/
include?: number

/**
* Features that should never be compiled, even when unsupported by targets.
*/
exclude?: number

/**
* Whether to enable parsing various draft syntax.
*/
drafts?: Drafts

/**
* Whether to enable various non-standard syntax.
*/
nonStandard?: NonStandard

/**
* The output source map before optimization
*
Expand All @@ -28,7 +53,16 @@ export interface TransformResult {

export function optimize(
input: string,
{ file = 'input.css', minify = false, map }: OptimizeOptions = {},
{
file = 'input.css',
minify = false,
map,
drafts,
nonStandard,
include,
exclude,
targets,
}: OptimizeOptions = {},
): TransformResult {
function optimize(code: Buffer | Uint8Array, map: string | undefined) {
return transform({
Expand All @@ -37,15 +71,11 @@ export function optimize(
minify,
sourceMap: typeof map !== 'undefined',
inputSourceMap: map,
drafts: {
customMedia: true,
},
nonStandard: {
deepSelectorCombinator: true,
},
include: Features.Nesting | Features.MediaQueries,
exclude: Features.LogicalProperties | Features.DirSelector | Features.LightDark,
targets: {
drafts: { customMedia: true, ...drafts },
nonStandard: { deepSelectorCombinator: true, ...nonStandard },
include: include ?? Features.Nesting | Features.MediaQueries,
exclude: exclude ?? Features.LogicalProperties | Features.DirSelector | Features.LightDark,
targets: targets ?? {
safari: (16 << 16) | (4 << 8),
ios_saf: (16 << 16) | (4 << 8),
firefox: 128 << 16,
Expand Down
21 changes: 21 additions & 0 deletions packages/@tailwindcss-vite/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,24 @@ export default defineConfig({
],
})
```

Additional Lightning CSS options can be configured through the `optimize` object, for example `drafts`, `nonStandard`, `include`, `exclude`, and `targets`.

## Controlling Tailwind polyfills

By default, Tailwind emits all supported CSS polyfills. You can customize this behavior using the `polyfills` option:
Comment on lines +80 to +82
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mention the CSS Modules polyfill exception.

Line 82 says Tailwind emits all supported polyfills by default, but the Vite implementation masks Polyfills.AtProperty for .module.css files to avoid global non-pure selectors. A short caveat here would keep the docs aligned with the implementation.

📝 Proposed wording
-By default, Tailwind emits all supported CSS polyfills. You can customize this behavior using the `polyfills` option:
+By default, Tailwind emits all supported CSS polyfills. In Vite CSS Modules, the `@property` fallback polyfill is still suppressed because it emits global selectors. You can customize this behavior using the `polyfills` option:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Controlling Tailwind polyfills
By default, Tailwind emits all supported CSS polyfills. You can customize this behavior using the `polyfills` option:
## Controlling Tailwind polyfills
By default, Tailwind emits all supported CSS polyfills. In Vite CSS Modules, the `@property` fallback polyfill is still suppressed because it emits global selectors. You can customize this behavior using the `polyfills` option:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/`@tailwindcss-vite/README.md around lines 80 - 82, Add a short
caveat to the "Controlling Tailwind polyfills" section clarifying that although
Tailwind emits supported polyfills by default, this Vite adapter masks the
Polyfills.AtProperty for .module.css files to avoid introducing global/non-pure
selectors; reference the polyfills option and Polyfills.AtProperty so readers
know this is an intentional Vite-specific exception and not a bug in Tailwind.


```js
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { Polyfills } from 'tailwindcss'

export default defineConfig({
plugins: [
tailwindcss({
// Disable all Tailwind polyfills
polyfills: Polyfills.None,
}),
],
})
```
22 changes: 19 additions & 3 deletions packages/@tailwindcss-vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Instrumentation,
normalizePath,
optimize,
Polyfills,
toSourceMap,
} from '@tailwindcss/node'
import { clearRequireCache } from '@tailwindcss/node/require-cache'
Expand All @@ -27,10 +28,17 @@ const COMMON_JS_PROXY_RE = /\?commonjs-proxy/
const INLINE_STYLE_ID_RE = /[?&]index=\d+\.css$/

export type PluginOptions = {
/**
* Control CSS polyfills emitted by Tailwind.
*
* Defaults to `Polyfills.All`.
*/
polyfills?: Polyfills

/**
* Optimize and minify the output CSS.
*/
optimize?: boolean | { minify?: boolean }
optimize?: boolean | Omit<NonNullable<Parameters<typeof optimize>[1]>, 'file' | 'map'>
}

export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
Expand Down Expand Up @@ -123,6 +131,7 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
// Currently, Vite only supports CSS source maps in development and they
// are off by default. Check to see if we need them or not.
config?.css.devSourcemap ?? false,
opts.polyfills ?? Polyfills.All,
customCssResolver,
customJsResolver,
)
Expand Down Expand Up @@ -153,8 +162,8 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
minify = shouldOptimize && config.build.cssMinify !== false

// But again, the user can override that choice explicitly
if (typeof opts.optimize === 'object') {
minify = opts.optimize.minify !== false
if (typeof opts.optimize === 'object' && opts.optimize.minify !== undefined) {
minify = opts.optimize.minify
}
},
},
Expand Down Expand Up @@ -310,6 +319,7 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] {
if (shouldOptimize) {
DEBUG && I.start('[@tailwindcss/vite] Optimize CSS')
result = optimize(result.code, {
...(typeof opts.optimize === 'object' ? opts.optimize : {}),
minify,
map: result.map,
})
Expand Down Expand Up @@ -389,6 +399,7 @@ class Root {
private base: string,

private enableSourceMaps: boolean,
private polyfills: Polyfills,
private customCssResolver: (id: string, base: string) => Promise<string | false | undefined>,
private customJsResolver: (id: string, base: string) => Promise<string | false | undefined>,
) {}
Expand Down Expand Up @@ -438,12 +449,17 @@ class Root {

this.addBuildDependency(idToPath(inputPath))

// CSS Modules cannot safely receive the `@property` fallback polyfill
// because it emits global `*` rules, which Vite treats as non-pure.
DEBUG && I.start('Setup compiler')
let addBuildDependenciesPromises: Promise<void>[] = []
this.compiler = await compile(content, {
from: this.enableSourceMaps ? this.id : undefined,
base: inputBase,
shouldRewriteUrls: true,
polyfills: inputPath.endsWith('.module.css')
? this.polyfills & ~Polyfills.AtProperty
: this.polyfills,
onDependency: (path) => {
addWatchFile(path)
addBuildDependenciesPromises.push(this.addBuildDependency(path))
Expand Down
Loading