Skip to content

fix(rule-utils): keep -1 out of the tagged pseudo variant sort - #5339

Merged
antfu merged 1 commit into
unocss:mainfrom
giaBaoJS:fix/tagged-pseudo-sort-index
Sep 14, 2026
Merged

antfu merged 1 commit into
unocss:mainfrom
giaBaoJS:fix/tagged-pseudo-sort-index

Conversation

@giaBaoJS

Copy link
Copy Markdown
Contributor

Description

createTaggedPseudoClassMatcher builds the variant sort with

sort: PseudoClassesKeys.indexOf(pseudoName) ?? PseudoClassesColonKeys.indexOf(pseudoName),

indexOf returns -1 on a miss, never null/undefined, so ?? never falls through: the
second lookup is dead code and the -1 is passed on as if it were a real sort index.

The bracket form (group-[...]) and the pseudo var form (group-not-[...]) carry no pseudo
class name at all (pseudoName defaults to ''), so their sort is always -1. Two things
follow:

  1. -1 sorts ahead of every utility that has no sort, so those variants are lifted out of the
    alphabetical order a dynamic rule is documented to use ("the output of those matched under a
    single dynamic rule will be sorted alphabetically within the group").
  2. -1 is not nullish, so at sort: variantSort ?? metaSort it also replaces the rule's own
    symbols.sort. A rule that orders its own tokens loses that order entirely under an
    arbitrary tagged variant, and since the selectors involved have identical specificity the
    emitted order is what decides the cascade.

Affects group-[...], peer-[...], parent-[...], previous-[...] and the *-not-[...],
*-is-[...], *-where-[...] forms in preset-mini, preset-wind3 and preset-wind4.

Reproduction

preset-wind4, with font-11 font-12 font-13 font-14 font-16 font-17 font-18 font-19 written as
group-[:hover]:font-11, group-[[data-attr]]:font-12, group-[.as-parent_&]:font-13,
group-[.not-parent]:font-14, group-[:hover]/label:font-16, group-[[data-attr]]/label:font-17,
group-[.as-parent_&]/label:font-18, group-[.not-parent]/label:font-19, alongside the plain
font-[system-ui] font-$font-name font-550 font-mono font-sans font-thin (these are already in
test/assets/preset-wind4-targets.ts).

Produced now, all eight arbitrary variants jump to the head of the font- group:

.group:hover .group-\[\:hover\]\:font-11{--un-font-weight:11;font-weight:11;}
.group[data-attr] .group-\[\[data-attr\]\]\:font-12{--un-font-weight:12;font-weight:12;}
...
.font-\[system-ui\]{font-family:system-ui;}
.font-\$font-name{font-family:var(--font-name);}
.font-550{--un-font-weight:550;font-weight:550;}
.font-mono{font-family:var(--font-mono);}

Expected, and what this PR generates, is the alphabetical position inside the group:

.font-\[system-ui\]{font-family:system-ui;}
.font-\$font-name{font-family:var(--font-name);}
.group:hover .group-\[\:hover\]\:font-11{--un-font-weight:11;font-weight:11;}
.group[data-attr] .group-\[\[data-attr\]\]\:font-12{--un-font-weight:12;font-weight:12;}
...
.font-550{--un-font-weight:550;font-weight:550;}
.font-mono{font-family:var(--font-mono);}

The same shift shows up in preset-mini, where it moves two rules of equal specificity past each
other: .group\/label[data-attr] .group-\[\[data-attr\]\]\/label\:font-17 used to be emitted
before .group[data-state=open] .group-data-\[state\=open\]\:font-bold, even though the
alphabetical order of the two selectors puts font-bold first. Both are (0,3,0) and both set
font-weight, so the emitted order is what wins.

Fix

createPseudoClassesAndElements a few lines below already normalises the same lookup
(indexOf to -1 to undefined). This extracts that into getPseudoSortIndex and uses it on
both paths, so a miss yields undefined and the sort falls back to the rule's own value. Known
pseudo class names keep the index they have today.

Tests

Three cases in packages-presets/rule-utils/test/pseudo.test.ts:

  • arbitrary tagged pseudo variant order: foo-1, group-hover:foo-2, group-[.on]:foo-3,
    group-not-[.off]:foo-4, peer-[.on]:foo-5 from one dynamic rule. Asserts foo-1 is emitted
    before the three arbitrary variants, and that group-hover still sorts by its pseudo index.
    Fails on main with expected 170 to be less than 106.
  • arbitrary tagged pseudo variant keeps the rule sort: one rule whose symbols.sort values are
    the reverse of the tokens' alphabetical order, so the assertion can only pass if the rule sort
    survives the variant. On main the two rules come out alphabetically instead.
  • colon-only pseudo class sorts by its own index: pins the PseudoClassesColonKeys branch of
    the shared helper through backdrop:, which exists only in that map.

Two snapshot files (preset-mini-targets.css, preset-wind4-targets.css) move the affected rules
into their alphabetical position; the diff is ordering only, no rule gained, lost or changed a
declaration.

pnpm build && pnpm test: 730 passed, 3 skipped, up from 727 passed, 3 skipped on main, no
other test changed. pnpm typecheck and eslint clean.

Note

#5332 edits the prefix: line directly above the one changed here, so the two will conflict
textually if both land. They are independent fixes and either rebase is a one line resolution.

`createTaggedPseudoClassMatcher` computed its sort with
`PseudoClassesKeys.indexOf(name) ?? PseudoClassesColonKeys.indexOf(name)`.
`indexOf` returns -1 rather than a nullish value, so the right-hand side was
dead and the -1 miss value was handed on as a real sort.

For `group-[...]`, `peer-[...]`, `parent-[...]`, `previous-[...]` and the
`*-not-[...]` forms there is no pseudo class name at all, so the sort was
always -1. That hoisted those utilities out of the alphabetical order their
dynamic rule is documented to use, and, because -1 is not nullish, it also
replaced the rule's own `symbols.sort` in `applyVariants`.

Reuse the normalisation `createPseudoClassesAndElements` already performs, so
a miss becomes `undefined` and the sort falls back to the rule's own value.
@giaBaoJS
giaBaoJS requested review from antfu and zyyv as code owners September 13, 2026 11:43
@netlify

netlify Bot commented Sep 13, 2026

Copy link
Copy Markdown

Deploy Preview for unocss ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 2a0a9a4
🔍 Latest deploy log https://app.netlify.com/projects/unocss/deploys/6aa68c491cabd50009e500cb
😎 Deploy Preview https://deploy-preview-5339--unocss.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new

pkg-pr-new Bot commented Sep 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

commit: 2a0a9a4

@antfu
antfu added this pull request to the merge queue Sep 14, 2026
Merged via the queue into unocss:main with commit 23b90e7 Sep 14, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Sponsor
SponsoredKunjungi sekarang
Promo