feat(compiler): configurable @property handling for --tw-* slots

Tailwind registers internal slots (`--tw-content`, `--tw-shadow`, …) via
`@property` rules that supply their defaults. `decompose` dropped those
rules, so utilities that reference a slot without setting it locally —
notably `after:*`/`before:*` emitting `content: var(--tw-content)` — left
a dangling reference that resolves to nothing and suppresses the
pseudo-element.

`decompose` now captures the `@property` registrations, and `emitCss`
gains a `properties` option to handle them, configurably:

  - `mode: 'emit'`   — emit `@property` rules for referenced slots,
    preserving Tailwind's typed defaults.
  - `mode: 'inline'` — substitute each slot's `initial-value` into the
    values that reference it (a superset of `inlineVars` for the matched
    slots), so the output is fully self-contained.
  - `resolve(name, captured)` hook — override or supply a slot's config
    (initial-value / syntax / inherits), or register one Tailwind didn't.
  - `match` — restrict handling to matching names (default `/^--tw-/`).

`tailwindPlugin` forwards `properties` through its `onCss` path. Adds 9
tests (capture, emit, inline, resolve override, match, back-compat,
plugin e2e). Compiler suite: 178 passing.
This commit is contained in:
Rahim
2026-06-17 17:13:40 -07:00
parent fbe5ffeea3
commit 0486765848
8 changed files with 341 additions and 18 deletions
+20 -2
View File
@@ -5,7 +5,13 @@ import { tagName } from '../matchers';
import { analyzeStyles, type StyleSegment, type StyleVisitor } from '../styles';
import { decompose, type UtilityCss } from './decompose';
import type { DesignSystem } from './design-system';
import { type CompiledRule, type EmittedCss, emitCss, type HoistOptions } from './emit';
import {
type CompiledRule,
type EmittedCss,
emitCss,
type HoistOptions,
type RegisteredPropertiesOptions,
} from './emit';
import { EvaluationError, loadTokenModule, type TokenValue } from './evaluator';
import { type DeriveClassNameOptions, DiagnosticError, deriveClassName, type NameTransform } from './naming';
@@ -83,6 +89,17 @@ export interface TailwindPluginOptions {
* driving `emitCss` themselves should pass the same value through.
*/
inlineVars?: true | RegExp;
/**
* Handle Tailwind's `@property`-registered slots (`--tw-content`, etc.) that
* are referenced but never set, so the output isn't broken (e.g.
* `content: var(--tw-content)`). See `RegisteredPropertiesOptions` — choose
* `'emit'` (ship `@property` rules) or `'inline'` (bake initial-values in),
* with an optional `resolve` hook for the per-property config.
*
* Forwarded to the internal `emitCss` call when `onCss` is set; consumers
* driving `emitCss` themselves should pass the same value through.
*/
properties?: RegisteredPropertiesOptions;
}
/**
@@ -127,7 +144,7 @@ function inlinedPlugin(options: TailwindPluginOptions): ts.TransformerFactory<ts
* ───────────────────────────────────────────────────────────────────────── */
function vanillaCssPlugin(options: TailwindPluginOptions): ts.TransformerFactory<ts.SourceFile> {
const { design, transformName, overrides, bagFor, onRules, onCss, emit, hoistVars, inlineVars } = options;
const { design, transformName, overrides, bagFor, onRules, onCss, emit, hoistVars, inlineVars, properties } = options;
const env = buildTokenEnv(options.sourcePath);
@@ -241,6 +258,7 @@ function vanillaCssPlugin(options: TailwindPluginOptions): ts.TransformerFactory
...(emit ?? {}),
...(hoistVars !== undefined ? { hoist: hoistVars } : {}),
...(inlineVars !== undefined ? { inlineVars } : {}),
...(properties ? { properties } : {}),
resolveThemeVar: (name) => design.resolveThemeVar(name),
...(themeSelector ? { themeSelector } : {}),
})