Skip to content

Commit 538a843

Browse files
fix(core): expand variant groups in positional order (#5287)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 75103d5 commit 538a843

4 files changed

Lines changed: 29 additions & 3 deletions

File tree

packages-engine/core/src/utils/variant-group.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,18 @@ export function parseVariantGroup(str: string | MagicString, separators = ['-',
7474

7575
let expanded: MagicString | string
7676

77+
// `groupsByOffset` is keyed by offset but iterates in insertion order, and a
78+
// group is inserted in the pass that matched it. A group nested inside
79+
// another is matched first, so an outer group starting earlier in the string
80+
// is inserted later than a sibling that starts after it. Rebuilding the
81+
// string from that order interleaves the slices wrongly and leaves the
82+
// unexpanded source behind, so walk the groups in positional order instead.
83+
const groupsInOrder = [...groupsByOffset].sort(([a], [b]) => a - b)
84+
7785
if (typeof str === 'string') {
7886
expanded = ''
7987
let prevOffset = 0
80-
for (const [offset, group] of groupsByOffset) {
88+
for (const [offset, group] of groupsInOrder) {
8189
expanded += str.slice(prevOffset, offset)
8290
expanded += group.items.map(item => item.className).join(' ')
8391
prevOffset = offset + group.length
@@ -86,7 +94,7 @@ export function parseVariantGroup(str: string | MagicString, separators = ['-',
8694
}
8795
else {
8896
expanded = str
89-
for (const [offset, group] of groupsByOffset) {
97+
for (const [offset, group] of groupsInOrder) {
9098
expanded.overwrite(
9199
offset,
92100
offset + group.length,

packages-engine/core/test/variant-group.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ describe('variant-group', () => {
2525
expect(expandVariantGroup('a-(b c-(d e f))')).toEqual('a-b a-c-d a-c-e a-c-f')
2626
})
2727

28+
it('nested group followed by a sibling group', () => {
29+
// A nested group is matched on an earlier pass than the group containing
30+
// it, so the outer group is recorded after a sibling that starts later in
31+
// the string (issue: #4791).
32+
expect(expandVariantGroup('a-(b-(c d) e) f-(g h)'))
33+
.toEqual('a-b-c a-b-d a-e f-g f-h')
34+
expect(expandVariantGroup('[&_p]:(text-(16px #9f4021) lh-18px) [&_img]:(w-full h-auto)'))
35+
.toEqual('[&_p]:text-16px [&_p]:text-#9f4021 [&_p]:lh-18px [&_img]:w-full [&_img]:h-auto')
36+
})
37+
2838
it('spaces', () => {
2939
expect(expandVariantGroup('a-( ~ b c )')).toEqual('a a-b a-c')
3040
})
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export default ['academicons', 'akar-icons', 'ant-design', 'arcticons', 'at-icons', 'basil', 'bi', 'bitcoin-icons', 'boxicons', 'bpmn', 'brandico', 'bubbles', 'bx', 'bxl', 'bxs', 'bytesize', 'carbon', 'catppuccin', 'cbi', 'charm', 'ci', 'cib', 'cif', 'cil', 'circle-flags', 'circum', 'clarity', 'codex', 'codicon', 'covid', 'cryptocurrency-color', 'cryptocurrency', 'cuida', 'dashicons', 'devicon-line', 'devicon-original', 'devicon-plain', 'devicon', 'dinkie-icons', 'duo-icons', 'ei', 'el', 'emblemicons', 'emojione-monotone', 'emojione-v1', 'emojione', 'entypo-social', 'entypo', 'eos-icons', 'ep', 'et', 'eva', 'f7', 'fa-brands', 'fa-regular', 'fa-solid', 'fa', 'fa6-brands', 'fa6-regular', 'fa6-solid', 'fa7-brands', 'fa7-regular', 'fa7-solid', 'fad', 'famicons', 'fe', 'feather', 'file-icons', 'flag', 'flagpack', 'flat-color-icons', 'flat-ui', 'flowbite', 'fluent-color', 'fluent-emoji-flat', 'fluent-emoji-high-contrast', 'fluent-emoji', 'fluent-mdl2', 'fluent', 'fontelico', 'fontisto', 'formkit', 'foundation', 'fxemoji', 'gala', 'game-icons', 'garden', 'gcp', 'geo', 'gg', 'ginetex', 'gis', 'glyphs-poly', 'glyphs', 'gravity-ui', 'griddy-icons', 'gridicons', 'grommet-icons', 'guidance', 'healthicons', 'heroicons-outline', 'heroicons-solid', 'heroicons', 'hugeicons', 'humbleicons', 'ic', 'icomoon-free', 'icon-park-outline', 'icon-park-solid', 'icon-park-twotone', 'icon-park', 'iconamoon', 'iconoir', 'icons8', 'il', 'ion', 'iwwa', 'ix', 'jam', 'k8s', 'la', 'lets-icons', 'line-md', 'lineicons', 'logos', 'ls', 'lsicon', 'lucide-lab', 'lucide', 'mage', 'majesticons', 'maki', 'map', 'marketeq', 'material-icon-theme', 'material-symbols-light', 'material-symbols', 'mdi-light', 'mdi', 'medical-icon', 'memory', 'meteocons', 'meteor-icons', 'mi', 'mingcute', 'mono-icons', 'mynaui', 'nimbus', 'nonicons', 'noto-v1', 'noto', 'nrk', 'octicon', 'oi', 'ooui', 'openmoji', 'osmic', 'oui', 'pajamas', 'pepicons-pencil', 'pepicons-pop', 'pepicons-print', 'pepicons', 'ph', 'picon', 'pinhead', 'pixel', 'pixelarticons', 'prime', 'proicons', 'ps', 'qlementine-icons', 'quill', 'radix-icons', 'raphael', 'reicon', 'ri', 'rivet-icons', 'roentgen', 'selfhst', 'si-glyph', 'si', 'sidekickicons', 'simple-icons', 'simple-line-icons', 'skill-icons', 'solar', 'stash', 'streamline-block', 'streamline-color', 'streamline-cyber-color', 'streamline-cyber', 'streamline-emojis', 'streamline-flex-color', 'streamline-flex', 'streamline-freehand-color', 'streamline-freehand', 'streamline-guidance', 'streamline-kameleon-color', 'streamline-logos', 'streamline-pixel', 'streamline-plump-color', 'streamline-plump', 'streamline-sharp-color', 'streamline-sharp', 'streamline-stickies-color', 'streamline-ultimate-color', 'streamline-ultimate', 'streamline', 'subway', 'svg-spinners', 'system-uicons', 'tabler', 'tdesign', 'teenyicons', 'temaki', 'thesvg-color', 'thesvg', 'token-branded', 'token', 'topcoat', 'twemoji', 'typcn', 'uil', 'uim', 'uis', 'uit', 'uiw', 'unjs', 'vaadin', 'vadivam', 'vs', 'vscode-icons', 'websymbol', 'weui', 'whh', 'wi', 'wordpress', 'wpf', 'zmdi', 'zondicons']
1+
export default ['academicons', 'akar-icons', 'ant-design', 'arcticons', 'at-icons', 'basil', 'bi', 'bitcoin-icons', 'boxicons', 'bpmn', 'brandico', 'bubbles', 'bx', 'bxl', 'bxs', 'bytesize', 'carbon', 'catppuccin', 'cbi', 'charm', 'ci', 'cib', 'cif', 'cil', 'circle-flags', 'circum', 'clarity', 'codex', 'codicon', 'covid', 'cryptocurrency-color', 'cryptocurrency', 'cuida', 'dashicons', 'devicon-line', 'devicon-original', 'devicon-plain', 'devicon', 'dinkie-icons', 'duo-icons', 'ei', 'el', 'emblemicons', 'emojione-monotone', 'emojione-v1', 'emojione', 'entypo-social', 'entypo', 'eos-icons', 'ep', 'et', 'eva', 'f7', 'fa-brands', 'fa-regular', 'fa-solid', 'fa', 'fa6-brands', 'fa6-regular', 'fa6-solid', 'fa7-brands', 'fa7-regular', 'fa7-solid', 'fad', 'famicons', 'fe', 'feather', 'file-icons', 'flag', 'flagpack', 'flat-color-icons', 'flat-ui', 'flowbite', 'fluent-color', 'fluent-emoji-flat', 'fluent-emoji-high-contrast', 'fluent-emoji', 'fluent-mdl2', 'fluent', 'fontelico', 'fontisto', 'formkit', 'foundation', 'fxemoji', 'gala', 'game-icons', 'garden', 'gcp', 'geo', 'gg', 'ginetex', 'gis', 'glyphs-poly', 'glyphs', 'gravity-ui', 'griddy-icons', 'gridicons', 'grommet-icons', 'guidance', 'healthicons', 'heroicons-outline', 'heroicons-solid', 'heroicons', 'hugeicons', 'humbleicons', 'ic', 'icomoon-free', 'icon-park-outline', 'icon-park-solid', 'icon-park-twotone', 'icon-park', 'iconamoon', 'iconoir', 'icons8', 'il', 'ion', 'iwwa', 'ix', 'jam', 'k8s', 'keyline-icons', 'la', 'lets-icons', 'line-md', 'lineicons', 'logos', 'ls', 'lsicon', 'lucide-lab', 'lucide', 'mage', 'majesticons', 'maki', 'map', 'marketeq', 'material-icon-theme', 'material-symbols-light', 'material-symbols', 'mdi-light', 'mdi', 'medical-icon', 'memory', 'meteocons', 'meteor-icons', 'mi', 'mingcute', 'mono-icons', 'mynaui', 'nimbus', 'nonicons', 'noto-v1', 'noto', 'nrk', 'octicon', 'oi', 'ooui', 'openmoji', 'osmic', 'oui', 'pajamas', 'pepicons-pencil', 'pepicons-pop', 'pepicons-print', 'pepicons', 'ph', 'picon', 'pinhead', 'pixel', 'pixelarticons', 'prime', 'proicons', 'ps', 'qlementine-icons', 'quill', 'radix-icons', 'raphael', 'reicon', 'ri', 'rivet-icons', 'roentgen', 'selfhst', 'si-glyph', 'si', 'sidekickicons', 'simple-icons', 'simple-line-icons', 'skill-icons', 'solar', 'stash', 'streamline-block', 'streamline-color', 'streamline-cyber-color', 'streamline-cyber', 'streamline-emojis', 'streamline-flex-color', 'streamline-flex', 'streamline-freehand-color', 'streamline-freehand', 'streamline-guidance', 'streamline-kameleon-color', 'streamline-logos', 'streamline-pixel', 'streamline-plump-color', 'streamline-plump', 'streamline-sharp-color', 'streamline-sharp', 'streamline-stickies-color', 'streamline-ultimate-color', 'streamline-ultimate', 'streamline', 'subway', 'svg-spinners', 'system-uicons', 'tabler', 'tdesign', 'teenyicons', 'temaki', 'thesvg-color', 'thesvg', 'token-branded', 'token', 'topcoat', 'twemoji', 'typcn', 'uil', 'uim', 'uis', 'uit', 'uiw', 'unjs', 'vaadin', 'vadivam', 'vs', 'vscode-icons', 'websymbol', 'weui', 'whh', 'wi', 'wordpress', 'wpf', 'zmdi', 'zondicons']

virtual-shared/integration/test/sort-rules.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,12 @@ describe('sort rules', async () => {
4040
expect(await sort(' none-uno-class mr-1 ml-1 '))
4141
.toMatchInlineSnapshot('"none-uno-class ml-1 mr-1"')
4242
})
43+
44+
it('nested arbitrary value group before a sibling group', async () => {
45+
// The sorted output is fed back into the source by the eslint autofixer,
46+
// so an expansion that drops or reorders fragments rewrites valid markup
47+
// into broken classes (issue: #4791).
48+
expect(await sort('[&_p]:(text-(16px #9f4021) lh-18px mt-15px) [&_img]:(w-full h-auto)'))
49+
.toMatchInlineSnapshot(`"[&_p]:(mt-15px text-16px text-#9f4021 lh-18px) [&_img]:(h-auto w-full)"`)
50+
})
4351
})

0 commit comments

Comments
 (0)
Sponsor
SponsoredKunjungi sekarang
Promo