mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(compiler): vanilla-css correctness — markers, collisions, theme vars
Three correctness fixes for the `vanilla-css` Tailwind target, each previously untested: 1. Preserve marker utilities. Utilities that produce no declarations (`group`, `peer`, `group/<name>`) were silently dropped when rewriting `className` to the semantic name, breaking every descendant `group-*` / `peer-*` variant. They're now kept on the element (`"play-button group"`), alongside any other class Tailwind doesn't recognize. 2. Flag class-name collisions. Two distinct elements that derive the same class name but resolve to different utilities would have their conflicting declarations silently merged into one rule (e.g. the forward/back `seek-icon`, buffering vs preview `spinner-icon`, current/duration `time-value`). The plugin now tracks a per-name utility signature and throws a fixable DiagnosticError on mismatch, while identical recurrences (repeated `Tooltip.Popup`) pass cleanly. 3. Emit referenced theme variables. The generated CSS referenced `var(--spacing)`, `var(--color-*)`, `var(--ease-*)` etc. with nothing defining them. `emitCss` now accepts a `resolveThemeVar` resolver (backed by `design.resolveThemeVar`) and emits a leading, transitively resolved theme block — scoped to the skin's hoist root when set — so the output resolves without a separate Tailwind theme on the page. Adds 11 tests (markers, collisions, theme emission incl. transitive, already-declared, scoped, and back-compat). Compiler suite: 169 passing. Note: `@property`-registered `--tw-*` slots (e.g. `--tw-content`) resolve to undefined and are left for a follow-up.
This commit is contained in:
@@ -146,6 +146,66 @@ describe('tailwindPlugin — target: vanilla-css', () => {
|
||||
expect(code).not.toContain('"flex items-center"');
|
||||
});
|
||||
|
||||
it('preserves marker utilities (group/peer) alongside the derived name', () => {
|
||||
const source = `function App(){ return <PlayButton className="group"/>; }`;
|
||||
const { code } = compile(source, {
|
||||
target: 'react',
|
||||
plugins: [tailwindPlugin({ design, target: 'vanilla-css' })],
|
||||
});
|
||||
// `group` produces no declarations but is required by descendant
|
||||
// `group-*` variants, so it must survive on the element.
|
||||
expect(code).toContain('"play-button group"');
|
||||
});
|
||||
|
||||
it('keeps markers and still emits rules for declaration-producing utilities', () => {
|
||||
const source = `function App(){ return <PlayButton className={cn('flex', 'group')}/>; }`;
|
||||
let captured: readonly CompiledRule[] | undefined;
|
||||
const { code } = compile(source, {
|
||||
target: 'react',
|
||||
plugins: [
|
||||
tailwindPlugin({
|
||||
design,
|
||||
target: 'vanilla-css',
|
||||
onRules: (rules) => {
|
||||
captured = rules;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(code).toContain('"play-button group"');
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured!.map((r) => r.className)).toContain('play-button');
|
||||
expect(captured!.flatMap((r) => r.utility.declarations)).toContainEqual({ property: 'display', value: 'flex' });
|
||||
});
|
||||
|
||||
it('preserves a marker while wrapping pass-through expressions in cn()', () => {
|
||||
const source = `function App(){ return <PlayButton className={cn('group', extra)}/>; }`;
|
||||
const { code } = compile(source, {
|
||||
target: 'react',
|
||||
plugins: [tailwindPlugin({ design, target: 'vanilla-css' })],
|
||||
});
|
||||
expect(code).toMatch(/cn\("play-button group",\s*extra\)/);
|
||||
});
|
||||
|
||||
it('throws a diagnostic when two elements derive the same name with different styles', () => {
|
||||
const source = `function App(){ return <div><SeekIcon className="flex"/><SeekIcon className="block"/></div>; }`;
|
||||
expect(() =>
|
||||
compile(source, {
|
||||
target: 'react',
|
||||
plugins: [tailwindPlugin({ design, target: 'vanilla-css' })],
|
||||
})
|
||||
).toThrow(/class name 'seek-icon' is derived from elements with different styles/);
|
||||
});
|
||||
|
||||
it('does not flag identical recurrences of the same derived name', () => {
|
||||
const source = `function App(){ return <div><PlayButton className="flex"/><PlayButton className="flex"/></div>; }`;
|
||||
const { code } = compile(source, {
|
||||
target: 'react',
|
||||
plugins: [tailwindPlugin({ design, target: 'vanilla-css' })],
|
||||
});
|
||||
expect(code).toContain('"play-button"');
|
||||
});
|
||||
|
||||
it('rewrites className to a token-path-derived name on a bare HTML element', () => {
|
||||
const source = `function App(){ return <div className={styles.bufferingIndicator}/>; }`;
|
||||
const { code } = compile(source, {
|
||||
@@ -381,4 +441,27 @@ function App(){ return <PlayButton className={iconButton}/>; }`;
|
||||
const css = await cssPromise;
|
||||
expect(collapse(css)).toContain(collapse('.foo{display:flex;}'));
|
||||
});
|
||||
|
||||
it('emits referenced theme variables in the onCss output', async () => {
|
||||
// `p-4` lowers to `padding: calc(var(--spacing) * 4)` — the output must
|
||||
// define `--spacing` so it resolves without a separate Tailwind theme.
|
||||
const source = `function App(){ return <Foo className="p-4"/>; }`;
|
||||
const cssPromise = new Promise<string>((resolve) => {
|
||||
compile(source, {
|
||||
target: 'react',
|
||||
plugins: [
|
||||
tailwindPlugin({
|
||||
design,
|
||||
target: 'vanilla-css',
|
||||
hoistVars: { rootSelector: '[data-skin="x"]' },
|
||||
onCss: (out) => {
|
||||
if (out.kind === 'merged') resolve(out.css);
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
const css = await cssPromise;
|
||||
expect(css).toMatch(/\[data-skin="x"\]\s*{[^}]*--spacing:/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user