diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index 28c218b81227..ab22fa20cc5f 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -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` + + + + +
Hello, world!
+ + `, + '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` + + + + +
Hello, world!
+ + `, + '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` + + + + +
Hello, world!
+ + `, + '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`, { diff --git a/packages/@tailwindcss-node/src/optimize.ts b/packages/@tailwindcss-node/src/optimize.ts index 6e60f8b873f6..2102a110f11d 100644 --- a/packages/@tailwindcss-node/src/optimize.ts +++ b/packages/@tailwindcss-node/src/optimize.ts @@ -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 { @@ -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 * @@ -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({ @@ -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, diff --git a/packages/@tailwindcss-vite/README.md b/packages/@tailwindcss-vite/README.md index 1a5d25c73652..3cf161966b26 100644 --- a/packages/@tailwindcss-vite/README.md +++ b/packages/@tailwindcss-vite/README.md @@ -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: + +```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, + }), + ], +}) +``` diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index ac3e1c33c9af..1c9e9e0b5941 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -5,6 +5,7 @@ import { Instrumentation, normalizePath, optimize, + Polyfills, toSourceMap, } from '@tailwindcss/node' import { clearRequireCache } from '@tailwindcss/node/require-cache' @@ -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[1]>, 'file' | 'map'> } export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { @@ -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, ) @@ -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 } }, }, @@ -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, }) @@ -389,6 +399,7 @@ class Root { private base: string, private enableSourceMaps: boolean, + private polyfills: Polyfills, private customCssResolver: (id: string, base: string) => Promise, private customJsResolver: (id: string, base: string) => Promise, ) {} @@ -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[] = [] 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))