mirror of
https://github.com/zoriya/v10.git
synced 2026-08-08 23:27:58 +00:00
refactor(compiler): capture tailwind property rules from ast
This commit is contained in:
@@ -29,11 +29,14 @@ export const styles = {
|
||||
),
|
||||
button: cn(
|
||||
'group',
|
||||
'flex items-center',
|
||||
'relative flex items-center',
|
||||
'disabled:opacity-50',
|
||||
'data-[availability=unsupported]:hidden',
|
||||
'aria-expanded:bg-current/10',
|
||||
'focus-visible:outline-current'
|
||||
'focus-visible:outline-current',
|
||||
'before:absolute',
|
||||
"before:content-['x']",
|
||||
'after:absolute'
|
||||
),
|
||||
icon: cn(
|
||||
'hidden opacity-0',
|
||||
@@ -95,7 +98,7 @@ describe('tailwind output modes', () => {
|
||||
"export function Fixture() {
|
||||
return (<FixtureRoot className="container p-4 [--media-controls-transition-duration:100ms] [color:var(--media-color-primary,oklch(1_0_0))] [&_video]:block [&:fullscreen]:[--media-border-radius:0]">
|
||||
<Controls className="flex flex-wrap [--media-popover-side-offset:0.5rem] pointer-fine:transition-[scale,filter,opacity] motion-reduce:[--media-controls-transition-duration:50ms] contrast-more:[--media-surface-background-color:oklch(0_0_0)] [@media(prefers-reduced-transparency:reduce)]:[--media-surface-background-color:oklch(0_0_0)] @2xl/media-root:flex-nowrap">
|
||||
<Button className="group flex items-center disabled:opacity-50 data-[availability=unsupported]:hidden aria-expanded:bg-current/10 focus-visible:outline-current">
|
||||
<Button className="group relative flex items-center disabled:opacity-50 data-[availability=unsupported]:hidden aria-expanded:bg-current/10 focus-visible:outline-current before:absolute before:content-['x'] after:absolute">
|
||||
<Icon className="hidden opacity-0 group-data-paused:block group-data-paused:opacity-100 group-not-data-paused:opacity-0"/>
|
||||
</Button>
|
||||
<div className="absolute [left:var(--media-slider-pointer)] has-[[role=img]:not([data-hidden])]:opacity-100"/>
|
||||
@@ -115,7 +118,10 @@ describe('tailwind output modes', () => {
|
||||
tailwind({
|
||||
mode: 'extract',
|
||||
input: cssPath,
|
||||
vars: { hoist: { rootSelector: '.media-test-skin' } },
|
||||
vars: {
|
||||
hoist: { rootSelector: '.media-test-skin' },
|
||||
properties: { mode: 'inline', variables: [{ match: /^--tw-content$/ }] },
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -139,6 +145,8 @@ describe('tailwind output modes', () => {
|
||||
|
||||
const css = result.assets[0]!.source;
|
||||
expect(css).not.toMatch(/\.media-test-skin\s*{[^}]*--media-border-radius/);
|
||||
expect(css).not.toContain('--tw-content');
|
||||
expect(css).not.toContain('var(--tw-content)');
|
||||
expect(css).toMatchInlineSnapshot(`
|
||||
".media-test-skin {
|
||||
--default-transition-duration: 150ms;
|
||||
@@ -153,6 +161,17 @@ describe('tailwind output modes', () => {
|
||||
.button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: 'x';
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
@@ -178,7 +197,7 @@ describe('tailwind output modes', () => {
|
||||
|
||||
.fixture-root {
|
||||
--media-controls-transition-duration: 100ms;
|
||||
color: var(--media-color-primary,oklch(1 0 0));
|
||||
color: var(--media-color-primary, oklch(1 0 0));
|
||||
padding: calc(var(--spacing) * 4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -124,6 +124,12 @@ describe('analyzeUtility — @property registrations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not capture @property text from declaration values', () => {
|
||||
const r = analyzeUtility("before:content-['@property_--fake_{}']", design);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r!.properties?.map((p) => p.name)).toEqual(['--tw-content']);
|
||||
});
|
||||
|
||||
it('omits `properties` when a utility registers none', () => {
|
||||
const r = analyzeUtility('flex', design);
|
||||
expect(r!.properties).toBeUndefined();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
type DeclarationBlock as CssDeclarationBlock,
|
||||
type Location2 as CssLocation,
|
||||
type PropertyRule as CssPropertyRule,
|
||||
type Rule as CssRule,
|
||||
type StyleRule as CssStyleRule,
|
||||
type StyleSheet as CssStyleSheet,
|
||||
@@ -98,7 +99,7 @@ export function analyzeUtility(utility: string, design: DesignSystem): UtilityCs
|
||||
collectRuleBranches(stylesheet.rules, [], branches, context);
|
||||
|
||||
// Tailwind appends `@property --tw-* { ... }` registrations after the utility rule.
|
||||
const properties = collectProperties(context);
|
||||
const properties = collectProperties(stylesheet.rules, context);
|
||||
const declarations = branches.flatMap((branch) => branch.declarations);
|
||||
const variants = branches[0]?.variants ?? [];
|
||||
|
||||
@@ -138,35 +139,6 @@ function createAnalysisContext(css: string): AnalysisContext {
|
||||
return { css, lineStarts };
|
||||
}
|
||||
|
||||
/** Parse every `@property --name { ... }` block from a compiled utility. */
|
||||
function parseProperties(css: string): PropertyRule[] {
|
||||
const out: PropertyRule[] = [];
|
||||
const re = /@property\s+(--[A-Za-z0-9_-]+)\s*\{/g;
|
||||
let match = re.exec(css);
|
||||
while (match !== null) {
|
||||
const name = match[1]!;
|
||||
const openIdx = match.index + match[0].length - 1;
|
||||
const block = readBalancedBlock(css, openIdx);
|
||||
if (!block) break;
|
||||
|
||||
const rule: PropertyRule = { name };
|
||||
for (const decl of block.inner.split(';')) {
|
||||
const colon = decl.indexOf(':');
|
||||
if (colon === -1) continue;
|
||||
const prop = decl.slice(0, colon).trim();
|
||||
const value = decl.slice(colon + 1).trim();
|
||||
if (!value) continue;
|
||||
if (prop === 'syntax') rule.syntax = value;
|
||||
else if (prop === 'inherits') rule.inherits = value === 'true';
|
||||
else if (prop === 'initial-value') rule.initialValue = value;
|
||||
}
|
||||
out.push(rule);
|
||||
re.lastIndex = block.end + 1;
|
||||
match = re.exec(css);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectRuleBranches(
|
||||
rules: readonly CssRule[],
|
||||
variants: readonly Variant[],
|
||||
@@ -275,8 +247,59 @@ function selectorTailFromStyleRule(rule: CssStyleRule, context: AnalysisContext)
|
||||
return /^\s/.test(rawTail) ? ` ${trimmedRight.trim()}` : trimmedRight.trim();
|
||||
}
|
||||
|
||||
function collectProperties(context: AnalysisContext): PropertyRule[] {
|
||||
return parseProperties(context.css);
|
||||
function collectProperties(rules: readonly CssRule[], context: AnalysisContext): PropertyRule[] {
|
||||
const properties: PropertyRule[] = [];
|
||||
for (const rule of rules) collectPropertyRule(rule, properties, context);
|
||||
return properties;
|
||||
}
|
||||
|
||||
function collectPropertyRule(rule: CssRule, properties: PropertyRule[], context: AnalysisContext): void {
|
||||
switch (rule.type) {
|
||||
case 'property':
|
||||
properties.push(propertyRuleFromAst(rule.value, context));
|
||||
return;
|
||||
case 'media':
|
||||
case 'container':
|
||||
case 'supports':
|
||||
case 'layer-block':
|
||||
case 'moz-document':
|
||||
case 'scope':
|
||||
case 'starting-style':
|
||||
for (const child of rule.value.rules) collectPropertyRule(child, properties, context);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function propertyRuleFromAst(rule: CssPropertyRule, context: AnalysisContext): PropertyRule {
|
||||
// Use Lightning's AST for rule identity, but source text for descriptors so
|
||||
// emitted CSS preserves Tailwind's authored values instead of serializer output.
|
||||
const descriptors = propertyDescriptorsFromSource(rule, context);
|
||||
return {
|
||||
name: rule.name,
|
||||
...(descriptors.syntax ? { syntax: descriptors.syntax } : {}),
|
||||
inherits: rule.inherits,
|
||||
...(descriptors.initialValue ? { initialValue: descriptors.initialValue } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function propertyDescriptorsFromSource(
|
||||
rule: CssPropertyRule,
|
||||
context: AnalysisContext
|
||||
): { syntax?: string; initialValue?: string } {
|
||||
const open = findBlockOpen(context.css, indexFromLocation(context, rule.loc));
|
||||
if (open === -1) return {};
|
||||
|
||||
const block = readBalancedBlock(context.css, open);
|
||||
if (!block) return {};
|
||||
|
||||
const descriptors: { syntax?: string; initialValue?: string } = {};
|
||||
for (const declaration of parseLocalDeclarations(block.inner)) {
|
||||
if (declaration.property === 'syntax') descriptors.syntax = declaration.value;
|
||||
else if (declaration.property === 'initial-value') descriptors.initialValue = declaration.value;
|
||||
}
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
function declarationsForBlock(block: CssDeclarationBlock, loc: CssLocation, context: AnalysisContext): Declaration[] {
|
||||
|
||||
Reference in New Issue
Block a user