Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(preset-mini)!: fix pseudo variants generation order, close #2713 #2714

Merged
merged 5 commits into from
Jun 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions packages/core/src/generator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,9 +564,15 @@ export class UnoGenerator<Theme extends {} = {}> {
return

return [
(await Promise.all(result.map(
async r => (isString(r) ? (await this.expandShortcut(r, context, depth - 1))?.[0] : undefined) || [r],
))).flat(1).filter(Boolean),
(await Promise.all(result.map(async r =>
(
isString(r)
? (await this.expandShortcut(r, context, depth - 1))?.[0]
: undefined
) || [r],
)))
.flat(1)
.filter(Boolean),
meta,
]
}
Expand Down Expand Up @@ -658,6 +664,7 @@ function applyScope(css: string, scope?: string) {
}

const attributifyRe = /^\[(.+?)(~?=)"(.*)"\]$/

export function toEscapedSelector(raw: string) {
if (attributifyRe.test(raw))
return raw.replace(attributifyRe, (_, n, s, i) => `[${e(n)}${s}"${e(i)}"]`)
Expand Down
4 changes: 2 additions & 2 deletions packages/preset-mini/src/_variants/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { variantNegative } from './negative'
import { variantImportant } from './important'
import { variantCustomMedia, variantPrint } from './media'
import { variantSupports } from './supports'
import { partClasses, variantPseudoClassFunctions, variantPseudoClassesAndElements, variantTaggedPseudoClasses } from './pseudo'
import { variantPartClasses, variantPseudoClassFunctions, variantPseudoClassesAndElements, variantTaggedPseudoClasses } from './pseudo'
import { variantAria } from './aria'
import { variantDataAttribute } from './data'
import { variantContainerQuery } from './container'
Expand All @@ -35,7 +35,7 @@ export function variants(options: PresetMiniOptions): Variant<Theme>[] {
variantPseudoClassFunctions(),
...variantTaggedPseudoClasses(options),

partClasses,
variantPartClasses,
...variantColorsMediaOrClass(options),
...variantLanguageDirections,
variantScope,
Expand Down
50 changes: 50 additions & 0 deletions packages/preset-mini/src/_variants/pseudo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from 'vitest'
import { createGenerator } from '@unocss/core'
import { variantPseudoClassesAndElements } from './pseudo'

// https://github.com/unocss/unocss/issues/2713
test('pseudo variant order', async () => {
const uno = createGenerator({
variants: [
variantPseudoClassesAndElements(),
],
rules: [
[/^foo-(\d)$/, ([_, a]) => ({ text: `foo-${a}` })],
],
})

const css = await uno.generate([
'foo-1',
'hover:foo-2',
'focus:foo-3',
'disabled:foo-4',
]).then(r => r.css)

expect(css.indexOf('foo-1')).toBeLessThan(css.indexOf('foo-2'))
expect(css.indexOf('foo-2')).toBeLessThan(css.indexOf('foo-3'))
expect(css.indexOf('foo-3')).toBeLessThan(css.indexOf('foo-4'))
expect(css)
.toMatchInlineSnapshot(`
"/* layer: default */
.foo-1{text:foo-1;}
.hover\\\\:foo-2:hover{text:foo-2;}
.focus\\\\:foo-3:focus{text:foo-3;}
.disabled\\\\:foo-4:disabled{text:foo-4;}"
`)

const css2 = await uno.generate([
'foo-1',
'hover:foo-1',
'focus:foo-1',
'disabled:foo-1',
]).then(r => r.css)

expect(css2)
.toMatchInlineSnapshot(`
"/* layer: default */
.foo-1{text:foo-1;}
.hover\\\\:foo-1:hover{text:foo-1;}
.focus\\\\:foo-1:focus{text:foo-1;}
.disabled\\\\:foo-1:disabled{text:foo-1;}"
`)
})
77 changes: 42 additions & 35 deletions packages/preset-mini/src/_variants/pseudo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { escapeRegExp, escapeSelector, warnOnce } from '@unocss/core'
import type { PresetMiniOptions } from '..'
import { handler as h, variantGetBracket } from '../_utils'

/**
* Note: the order of following pseudo classes will affect the order of generated css.
*
* Reference: https://github.com/tailwindlabs/tailwindcss/blob/main/src/corePlugins.js#L83
*/
const PseudoClasses: Record<string, string> = Object.fromEntries([
// pseudo elements part 1
['first-letter', '::first-letter'],
Expand All @@ -15,29 +20,32 @@ const PseudoClasses: Record<string, string> = Object.fromEntries([
'target',
['open', '[open]'],

// user action
'hover',
'active',
'focus-visible',
'focus-within',
'focus',

// input
'autofill',
'enabled',
'disabled',
'read-only',
'read-write',
'placeholder-shown',
// forms
'default',
'checked',
'indeterminate',
'placeholder-shown',
'autofill',
'optional',
'required',
'valid',
'invalid',
'in-range',
'out-of-range',
'required',
'optional',
'read-only',
'read-write',

// content
'empty',

// interactions
'focus-within',
'hover',
'focus',
'focus-visible',
'active',
'enabled',
'disabled',

// tree-structural
'root',
Expand All @@ -63,10 +71,14 @@ const PseudoClasses: Record<string, string> = Object.fromEntries([
['file', '::file-selector-button'],
].map(key => Array.isArray(key) ? key : [key, `:${key}`]))

const PseudoClassesKeys = Object.keys(PseudoClasses)

const PseudoClassesColon: Record<string, string> = Object.fromEntries([
['backdrop', '::backdrop'],
].map(key => Array.isArray(key) ? key : [key, `:${key}`]))

const PseudoClassesColonKeys = Object.keys(PseudoClassesColon)

const PseudoClassFunctions = [
'not',
'is',
Expand All @@ -78,22 +90,6 @@ const PseudoClassesStr = Object.entries(PseudoClasses).filter(([, pseudo]) => !p
const PseudoClassesColonStr = Object.entries(PseudoClassesColon).filter(([, pseudo]) => !pseudo.startsWith('::')).map(([key]) => key).join('|')
const PseudoClassFunctionsStr = PseudoClassFunctions.join('|')

function pseudoModifier(pseudo: string) {
if (pseudo === 'focus') {
return {
sort: 10,
noMerge: true,
}
}

if (pseudo === 'active') {
return {
sort: 20,
noMerge: true,
}
}
}

function taggedPseudoClassMatcher(tag: string, parent: string, combinator: string): VariantObject {
const rawRE = new RegExp(`^(${escapeRegExp(parent)}:)(\\S+)${escapeRegExp(combinator)}\\1`)
let splitRE: RegExp
Expand Down Expand Up @@ -163,7 +159,7 @@ function taggedPseudoClassMatcher(tag: string, parent: string, combinator: strin
handle: (input, next) => next({
...input,
prefix: `${prefix}${combinator}${input.prefix}`.replace(rawRE, '$1$2:'),
...pseudoModifier(pseudoName),
sort: PseudoClassesKeys.indexOf(pseudoName) ?? PseudoClassesColonKeys.indexOf(pseudoName),
}),
}
},
Expand All @@ -183,6 +179,7 @@ const excludedPseudo = [
]
const PseudoClassesAndElementsStr = Object.entries(PseudoClasses).map(([key]) => key).join('|')
const PseudoClassesAndElementsColonStr = Object.entries(PseudoClassesColon).map(([key]) => key).join('|')

export function variantPseudoClassesAndElements(): VariantObject {
let PseudoClassesAndElementsRE: RegExp
let PseudoClassesAndElementsColonRE: RegExp
Expand All @@ -197,6 +194,14 @@ export function variantPseudoClassesAndElements(): VariantObject {
const match = input.match(PseudoClassesAndElementsRE) || input.match(PseudoClassesAndElementsColonRE)
if (match) {
const pseudo = PseudoClasses[match[1]] || PseudoClassesColon[match[1]] || `:${match[1]}`

// order of pseudo classes
let index: number | undefined = PseudoClassesKeys.indexOf(match[1])
if (index === -1)
index = PseudoClassesColonKeys.indexOf(match[1])
if (index === -1)
index = undefined

return {
matcher: input.slice(match[0].length),
handle: (input, next) => {
Expand All @@ -211,7 +216,8 @@ export function variantPseudoClassesAndElements(): VariantObject {
return next({
...input,
...selectors,
...pseudoModifier(match[1]),
sort: index,
noMerge: true,
})
},
}
Expand Down Expand Up @@ -259,7 +265,8 @@ export function variantTaggedPseudoClasses(options: PresetMiniOptions = {}): Var
}

const PartClassesRE = /(part-\[(.+)]:)(.+)/
export const partClasses: VariantObject = {

export const variantPartClasses: VariantObject = {
match(input) {
const match = input.match(PartClassesRE)
if (match) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ describe('getUtils', async () => {
undefined,
{
"layer": undefined,
"sort": undefined,
"sort": 22,
},
undefined,
undefined,
true,
],
]
`)
Expand All @@ -70,10 +70,10 @@ describe('getUtils', async () => {
undefined,
{
"layer": undefined,
"sort": undefined,
"sort": 22,
},
undefined,
undefined,
true,
],
]
`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,8 @@ describe('transform', () => {
<Button class="hover:text-red text-sm" />
</div>
`.trim()
expect(await transform(code)).toMatchFileSnapshot('./test-outputs/EverythingProd.svelte')
expect(await transform(code, { combine: false })).toMatchFileSnapshot('./test-outputs/EverythingDev.svelte')
await expect(await transform(code)).toMatchFileSnapshot('./test-outputs/EverythingProd.svelte')
await expect(await transform(code, { combine: false })).toMatchFileSnapshot('./test-outputs/EverythingDev.svelte')
})

// BUG: When this plugin is run on a component library first, and then in a project second, make sure to use different hashing prefixes because when `uno.parseToken()` checks a previously hashed class like `.uno-ssrvwc` it will add it to uno's cache of non-matches, then when `uno.generate()` runs it will not output the result of that shortcut. I don't know the proper solution to this and I don't think clearing uno's cache of non-matches is right. To see this bug run the following test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@
font-size: 0.875rem;
line-height: 1.25rem;
}
:global(._hover\:text-red_7dkb0w:hover) {
--un-text-opacity: 1;
color: rgba(248, 113, 113, var(--un-text-opacity));
}
:global(._text-orange-400_7dkb0w) {
--un-text-opacity: 1;
color: rgba(251, 146, 60, var(--un-text-opacity));
}
:global(._hover\:text-red_7dkb0w:hover) {
--un-text-opacity: 1;
color: rgba(248, 113, 113, var(--un-text-opacity));
}
@media (min-width: 640px) {
:global(._sm\:text-left_7dkb0w) {
text-align: left;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,17 @@
font-size: 0.875rem;
line-height: 1.25rem;
}
:global(.uno-deus5r:hover, .uno-wt89uz:hover) {
:global(.uno-y37ej3) {
--un-text-opacity: 1;
color: rgba(251, 146, 60, var(--un-text-opacity));
}
:global(.uno-deus5r:hover) {
--un-text-opacity: 1;
color: rgba(248, 113, 113, var(--un-text-opacity));
}
:global(.uno-y37ej3) {
:global(.uno-wt89uz:hover) {
--un-text-opacity: 1;
color: rgba(251, 146, 60, var(--un-text-opacity));
color: rgba(248, 113, 113, var(--un-text-opacity));
}
@media (min-width: 640px) {
:global(.uno-rdvmaz) {
Expand Down
4 changes: 2 additions & 2 deletions test/__snapshots__/order.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ exports[`order > multiple variant sorting 1`] = `

exports[`order > pseudo-elements sorting 1`] = `
"/* layer: default */
.dark .dark\\\\:file\\\\:marker\\\\:hover\\\\:bg-red-600:hover::file-selector-button::marker,
.dark .dark\\\\:hover\\\\:file\\\\:marker\\\\:bg-red-600::file-selector-button:hover::marker{--un-bg-opacity:1;background-color:rgba(220,38,38,var(--un-bg-opacity));}"
.dark .dark\\\\:hover\\\\:file\\\\:marker\\\\:bg-red-600::file-selector-button:hover::marker{--un-bg-opacity:1;background-color:rgba(220,38,38,var(--un-bg-opacity));}
.dark .dark\\\\:file\\\\:marker\\\\:hover\\\\:bg-red-600:hover::file-selector-button::marker{--un-bg-opacity:1;background-color:rgba(220,38,38,var(--un-bg-opacity));}"
`;

exports[`order > variant ordering 1`] = `
Expand Down
2 changes: 1 addition & 1 deletion test/__snapshots__/prefix.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ exports[`prefix > multiple preset prefix 1`] = `
exports[`prefix > preset prefix 1`] = `
"/* layer: default */
.h-container{width:100%;}
.dark .dark\\\\:children\\\\:hover\\\\:h-space-x-4:hover>*>:not([hidden])~:not([hidden]),
.dark .dark\\\\:children\\\\:hover\\\\:h-space-x-4:hover>*>:not([hidden])~:not([hidden]){--un-space-x-reverse:0;margin-left:calc(1rem * calc(1 - var(--un-space-x-reverse)));margin-right:calc(1rem * var(--un-space-x-reverse));}
.dark .dark\\\\:hover\\\\:children\\\\:h-space-x-4>*:hover>:not([hidden])~:not([hidden]){--un-space-x-reverse:0;margin-left:calc(1rem * calc(1 - var(--un-space-x-reverse)));margin-right:calc(1rem * var(--un-space-x-reverse));}
.dark .dark\\\\:hover\\\\:children\\\\:h-divide-x>*:hover>:not([hidden])~:not([hidden]){--un-divide-x-reverse:0;border-left-width:calc(1px * calc(1 - var(--un-divide-x-reverse)));border-right-width:calc(1px * var(--un-divide-x-reverse));}
.hover\\\\:h-p4:hover{padding:1rem;}
Expand Down
2 changes: 1 addition & 1 deletion test/__snapshots__/scope.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ exports[`scope 1`] = `
.foo-scope .hover\\\\:p-4:hover{padding:1rem;}
.foo-scope .\\\\!hover\\\\:px-10:hover{padding-left:2.5rem !important;padding-right:2.5rem !important;}
.foo-scope .pl-10px{padding-left:10px;}
.dark .foo-scope .dark\\\\:hover\\\\:text-xl:hover,
.dark .foo-scope .dark\\\\:text-xl{font-size:1.25rem;line-height:1.75rem;}
.dark .foo-scope .dark\\\\:hover\\\\:text-xl:hover{font-size:1.25rem;line-height:1.75rem;}
.variant .foo-scope .scope-\\\\[\\\\.variant\\\\]\\\\:c-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));}
@media (min-width: 640px){
.foo-scope .sm\\\\:text-red-100{--un-text-opacity:1;color:rgba(254,226,226,var(--un-text-opacity));}
Expand Down
Loading