Skip to content

Commit 126617c

Browse files
committed
chore: cascade order improvements
1 parent 04caac1 commit 126617c

2 files changed

Lines changed: 80 additions & 1 deletion

File tree

packages/crosswind/src/generator.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2393,11 +2393,42 @@ export class CSSGenerator {
23932393
return minify ? parts.join('') : parts.join('\n\n')
23942394
}
23952395

2396+
/**
2397+
* Rank a rule by utility specificity so shorthands emit BEFORE directional
2398+
* utilities. Without this, class authoring order decides the cascade and
2399+
* combinations like `m-0 mx-auto` silently break (the shorthand resets
2400+
* the auto margins). Matches Tailwind's own stylesheet ordering.
2401+
*
2402+
* 0 — shorthand (`m-*`, `p-*`, `border`, `rounded`, `inset-*`)
2403+
* 1 — axis (`mx-*`, `my-*`, `px-*`, `py-*`, `inset-x-*`, corner radii)
2404+
* 2 — side (`mt-*`, `mr-*`, `pt-*`, `top-*`, `border-t-*`, …)
2405+
*/
2406+
private getUtilityRank(selector: string): number {
2407+
const m = selector.match(/\.(?:[a-zA-Z0-9_-]*\\?:)*([a-zA-Z0-9_-]+)/)
2408+
if (!m) return 0
2409+
const cls = m[1]
2410+
if (/^(?:mx|my|px|py|inset-x|inset-y|border-x|border-y|scroll-mx|scroll-my|scroll-px|scroll-py|space-x|space-y)-/.test(cls)
2411+
|| /^rounded-(?:[tlbr]|tl|tr|bl|br)(?:-|$)/.test(cls)) {
2412+
return 1
2413+
}
2414+
if (/^(?:mt|mr|mb|ml|pt|pr|pb|pl|top|right|bottom|left|border-t|border-r|border-b|border-l|scroll-mt|scroll-mr|scroll-mb|scroll-ml|scroll-pt|scroll-pr|scroll-pb|scroll-pl)-/.test(cls)) {
2415+
return 2
2416+
}
2417+
return 0
2418+
}
2419+
23962420
/**
23972421
* Convert rules to CSS string
23982422
*/
23992423
private rulesToCSS(rules: CSSRule[], minify: boolean): string {
2400-
const grouped = this.groupRulesBySelector(rules)
2424+
// Stable sort by utility rank so shorthand utilities emit before their
2425+
// axis/side counterparts. Stable so that within the same rank, original
2426+
// insertion order (which drives other cascade semantics) is preserved.
2427+
const ranked = rules.map((r, i) => ({ r, i, rank: this.getUtilityRank(r.selector) }))
2428+
ranked.sort((a, b) => a.rank - b.rank || a.i - b.i)
2429+
const sorted = ranked.map(x => x.r)
2430+
2431+
const grouped = this.groupRulesBySelector(sorted)
24012432
const parts: string[] = []
24022433

24032434
for (const [selector, properties] of grouped.entries()) {

packages/crosswind/test/spacing.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,4 +526,52 @@ describe('Edge Cases', () => {
526526
expect(css).toContain('-')
527527
})
528528
})
529+
530+
// ============================================================================
531+
// Regression: Tailwind-style cascade ordering. Shorthand utilities (`m-0`,
532+
// `p-0`, etc.) MUST emit before directional counterparts (`mx-auto`) so that
533+
// a combination like `class="m-0 mx-auto"` keeps the auto margins — the
534+
// shorthand coming last would otherwise reset them and silently break
535+
// horizontal centering.
536+
// ============================================================================
537+
describe('Cascade order — shorthand vs directional', () => {
538+
it('emits m-0 BEFORE mx-auto regardless of generate() order', () => {
539+
const gen = new CSSGenerator(defaultConfig)
540+
// Authoring order: mx-auto first, m-0 second — the buggy case
541+
gen.generate('mx-auto')
542+
gen.generate('m-0')
543+
const css = gen.toCSS(false)
544+
const mPos = css.indexOf('.m-0')
545+
const mxPos = css.indexOf('.mx-auto')
546+
expect(mPos).toBeGreaterThan(-1)
547+
expect(mxPos).toBeGreaterThan(-1)
548+
// mx-auto must appear AFTER m-0 in the output
549+
expect(mxPos).toBeGreaterThan(mPos)
550+
})
551+
552+
it('emits p-2 BEFORE py-4 so py overrides shorthand on equal specificity', () => {
553+
const gen = new CSSGenerator(defaultConfig)
554+
gen.generate('py-4')
555+
gen.generate('p-2')
556+
const css = gen.toCSS(false)
557+
expect(css.indexOf('.py-4')).toBeGreaterThan(css.indexOf('.p-2'))
558+
})
559+
560+
it('emits mx-auto BEFORE mt-4 (axis before single-side)', () => {
561+
const gen = new CSSGenerator(defaultConfig)
562+
gen.generate('mt-4')
563+
gen.generate('mx-auto')
564+
const css = gen.toCSS(false)
565+
expect(css.indexOf('.mt-4')).toBeGreaterThan(css.indexOf('.mx-auto'))
566+
})
567+
568+
it('stably preserves order within the same rank', () => {
569+
const gen = new CSSGenerator(defaultConfig)
570+
gen.generate('p-2')
571+
gen.generate('m-0')
572+
const css = gen.toCSS(false)
573+
// Both rank 0; p-2 was emitted first, so it stays first
574+
expect(css.indexOf('.p-2')).toBeLessThan(css.indexOf('.m-0'))
575+
})
576+
})
529577
})

0 commit comments

Comments
 (0)