diff --git a/packages/core/src/generator/index.ts b/packages/core/src/generator/index.ts index b569c51922..b84332623d 100644 --- a/packages/core/src/generator/index.ts +++ b/packages/core/src/generator/index.ts @@ -1,5 +1,5 @@ import { createNanoEvents } from '../utils/events' -import type { CSSEntries, CSSObject, ExtractorContext, GenerateOptions, GenerateResult, ParsedUtil, PreflightContext, RawUtil, ResolvedConfig, Rule, RuleContext, RuleMeta, Shortcut, StringifiedUtil, UserConfig, UserConfigDefaults, UtilObject, Variant, VariantContext, VariantHandler, VariantMatchedResult } from '../types' +import type { CSSEntries, CSSObject, ExtractorContext, GenerateOptions, GenerateResult, ParsedUtil, PreflightContext, PreparedRule, RawUtil, ResolvedConfig, Rule, RuleContext, RuleMeta, Shortcut, StringifiedUtil, UserConfig, UserConfigDefaults, UtilObject, Variant, VariantContext, VariantHandler, VariantMatchedResult } from '../types' import { resolveConfig } from '../config' import { CONTROL_SHORTCUT_NO_MERGE, TwoKeyMap, e, entriesToCss, expandVariantGroup, isRawUtil, isStaticShortcut, noop, normalizeCSSEntries, normalizeCSSValues, notNull, uniq, warnOnce } from '../utils' import { version } from '../../package.json' @@ -219,28 +219,44 @@ export class UnoGenerator { .sort((a, b) => ((this.parentOrders.get(a[0]) ?? 0) - (this.parentOrders.get(b[0]) ?? 0)) || a[0]?.localeCompare(b[0] || '') || 0) .map(([parent, items]) => { const size = items.length - const sorted = items + const sorted: PreparedRule[] = items .filter(i => (i[4]?.layer || 'default') === layer) - .sort((a, b) => a[0] - b[0] || (a[4]?.sort || 0) - (b[4]?.sort || 0) || a[1]?.localeCompare(b[1] || '') || 0) - .map(a => [a[1] ? applyScope(a[1], scope) : a[1], a[2], !!a[4]?.noMerge]) - .map(a => [a[0] == null ? a[0] : [a[0]], a[1], a[2]]) as [string[] | undefined, string, boolean][] + .sort((a, b) => a[0] - b[0] || (a[4]?.sort || 0) - (b[4]?.sort || 0) || a[1]?.localeCompare(b[1] || '') || a[2]?.localeCompare(b[2] || '') || 0) + .map(([, selector, body,, meta]) => { + const scopedSelector = selector ? applyScope(selector, scope) : selector + return [ + [[scopedSelector ?? '', meta?.sort ?? 0]], + body, + !!meta?.noMerge, + ] + }) if (!sorted.length) return undefined const rules = sorted .reverse() - .map(([selector, body, noMerge], idx) => { - if (!noMerge && selector && this.config.mergeSelectors) { + .map(([selectorSortPair, body, noMerge], idx) => { + if (!noMerge && this.config.mergeSelectors) { // search for rules that has exact same body, and merge them for (let i = idx + 1; i < size; i++) { const current = sorted[i] - if (current && !current[2] && current[0] && current[1] === body) { - current[0].push(...selector) + if (current && !current[2] && ((selectorSortPair && current[0]) || (selectorSortPair == null && current[0] == null)) && current[1] === body) { + if (selectorSortPair && current[0]) + current[0].push(...selectorSortPair) return null } } } - return selector - ? `${[...new Set(selector)].join(`,${nl}`)}{${body}}` + + const selectors = selectorSortPair + ? [...new Set(selectorSortPair + .sort((a, b) => a[1] - b[1] || a[0]?.localeCompare(b[0] || '') || 0) + .map(pair => pair[0]) + .filter(Boolean), + )] + : [] + + return selectors.length + ? `${selectors.join(`,${nl}`)}{${body}}` : body }) .filter(Boolean) @@ -341,16 +357,18 @@ export class UnoGenerator { } constructCustomCSS(context: Readonly, body: CSSObject | CSSEntries, overrideSelector?: string) { - body = normalizeCSSEntries(body) + const normalizedBody = normalizeCSSEntries(body) + if (typeof normalizedBody === 'string') + return normalizedBody - const { selector, entries, parent } = this.applyVariants([0, overrideSelector || context.rawSelector, body, undefined, context.variantHandlers]) + const { selector, entries, parent } = this.applyVariants([0, overrideSelector || context.rawSelector, normalizedBody, undefined, context.variantHandlers]) const cssBody = `${selector}{${entriesToCss(entries)}}` if (parent) return `${parent}{${cssBody}}` return cssBody } - async parseUtil(input: string | VariantMatchedResult, context: RuleContext, internal = false): Promise { + async parseUtil(input: string | VariantMatchedResult, context: RuleContext, internal = false): Promise<(ParsedUtil | RawUtil)[] | undefined> { const [raw, processed, variantHandlers] = typeof input === 'string' ? this.matchVariants(input) : input @@ -367,7 +385,13 @@ export class UnoGenerator { if (staticMatch) { if (staticMatch[1] && (internal || !staticMatch[2]?.internal)) { recordRule(staticMatch[3]) - return [[staticMatch[0], raw, normalizeCSSEntries(staticMatch[1]), staticMatch[2], variantHandlers]] + const index = staticMatch[0] + const entry = normalizeCSSEntries(staticMatch[1]) + const meta = staticMatch[2] + if (typeof entry === 'string') + return [[index, entry, meta]] + else + return [[index, raw, entry, meta, variantHandlers]] } } @@ -399,11 +423,15 @@ export class UnoGenerator { recordRule(rule) - if (typeof result === 'string') - return [[i, result, meta]] const entries = normalizeCSSValues(result).filter(i => i.length) - if (entries.length) - return entries.map(e => [i, raw, e, meta, variantHandlers]) + if (entries.length) { + return entries.map((e) => { + if (typeof e === 'string') + return [i, e, meta] + else + return [i, raw, e, meta, variantHandlers] + }) + } } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b0111ec47e..a0fff75b94 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -164,9 +164,10 @@ export interface RuleMeta { internal?: boolean } -export type CSSValues = CSSObject | CSSEntries | (CSSObject | CSSEntries)[] +export type CSSValue = CSSObject | CSSEntries +export type CSSValues = CSSValue | CSSValue[] -export type DynamicMatcher = ((match: RegExpMatchArray, context: Readonly>) => Awaitable) +export type DynamicMatcher = ((match: RegExpMatchArray, context: Readonly>) => Awaitable) export type DynamicRule = [RegExp, DynamicMatcher] | [RegExp, DynamicMatcher, RuleMeta] export type StaticRule = [string, CSSObject | CSSEntries] | [string, CSSObject | CSSEntries, RuleMeta] export type Rule = DynamicRule | StaticRule @@ -599,6 +600,12 @@ export type StringifiedUtil = readonly [ context: RuleContext | undefined, ] +export type PreparedRule = readonly [ + selector: [string, number][], + body: string, + noMerge: boolean, +] + export interface UtilObject { selector: string entries: CSSEntries diff --git a/packages/core/src/utils/object.ts b/packages/core/src/utils/object.ts index d7e4458e2b..5131a1a160 100644 --- a/packages/core/src/utils/object.ts +++ b/packages/core/src/utils/object.ts @@ -1,16 +1,18 @@ -import type { CSSEntries, CSSObject, CSSValues, DeepPartial, Rule, Shortcut, StaticRule, StaticShortcut } from '../types' +import type { CSSEntries, CSSObject, CSSValue, DeepPartial, Rule, Shortcut, StaticRule, StaticShortcut } from '../types' -export function normalizeCSSEntries(obj: CSSEntries | CSSObject): CSSEntries { +export function normalizeCSSEntries(obj: string | CSSEntries | CSSObject): string | CSSEntries { + if (typeof obj === 'string') + return obj return (!Array.isArray(obj) ? Object.entries(obj) : obj).filter(i => i[1] != null) } -export function normalizeCSSValues(obj: CSSValues): CSSEntries[] { +export function normalizeCSSValues(obj: CSSValue | string | (CSSValue | string)[]): (string | CSSEntries)[] { if (Array.isArray(obj)) { // @ts-expect-error type cast if (obj.find(i => !Array.isArray(i) || Array.isArray(i[0]))) - return (obj as any).map((i: any) => normalizeCSSEntries(i)) + return (obj as (string | CSSValue)[]).map(i => normalizeCSSEntries(i)) else - return [obj as any] + return [obj as CSSEntries] } else { return [normalizeCSSEntries(obj)] diff --git a/packages/preset-wind/src/rules/animation.ts b/packages/preset-wind/src/rules/animation.ts index 7796336252..2b0fd7b198 100644 --- a/packages/preset-wind/src/rules/animation.ts +++ b/packages/preset-wind/src/rules/animation.ts @@ -3,21 +3,30 @@ import { handler as h } from '@unocss/preset-mini/utils' import type { Theme } from '@unocss/preset-mini' export const animations: Rule[] = [ - [/^(?:animate-)?keyframes-(.+)$/, ([, name], { theme, constructCSS }) => { + [/^(?:animate-)?keyframes-(.+)$/, ([, name], { theme }) => { const kf = theme.animation?.keyframes?.[name] - if (kf) - return `@keyframes ${name}${kf}\n${constructCSS({ animation: `${name}` })}` + if (kf) { + return [ + `@keyframes ${name}${kf}`, + { animation: name }, + ] + } }, { autocomplete: ['animate-keyframes-$animation.keyframes', 'keyframes-$animation.keyframes'] }], - [/^animate-(.+)$/, ([, name], { theme, constructCSS }) => { + [/^animate-(.+)$/, ([, name], { theme }) => { const kf = theme.animation?.keyframes?.[name] if (kf) { const duration = theme.animation?.durations?.[name] ?? '1s' const timing = theme.animation?.timingFns?.[name] ?? 'linear' - const props = theme.animation?.properties?.[name] const count = theme.animation?.counts?.[name] ?? 1 - return `@keyframes ${name}${kf}\n${constructCSS( - Object.assign({ animation: `${name} ${duration} ${timing} ${count}` }, props))}` + const props = theme.animation?.properties?.[name] + return [ + `@keyframes ${name}${kf}`, + { + animation: `${name} ${duration} ${timing} ${count}`, + ...props, + }, + ] } return { animation: h.bracket.cssvar(name) } }, { autocomplete: 'animate-$animation.keyframes' }], diff --git a/test/__snapshots__/layer.test.ts.snap b/test/__snapshots__/layer.test.ts.snap index bb7f95555f..6d8aa49659 100644 --- a/test/__snapshots__/layer.test.ts.snap +++ b/test/__snapshots__/layer.test.ts.snap @@ -10,8 +10,8 @@ exports[`layers > static 1`] = ` /* layer: c */ .c5{name:5;} /* layer: d */ -/* RAW 4 */ /* RAW 3 */ +/* RAW 4 */ /* layer: s */ .abcd{name:bar1;name:bar2;name:2;}" `; diff --git a/test/__snapshots__/order.test.ts.snap b/test/__snapshots__/order.test.ts.snap index 7ff414bff7..4f7787ee0f 100644 --- a/test/__snapshots__/order.test.ts.snap +++ b/test/__snapshots__/order.test.ts.snap @@ -1,5 +1,15 @@ // Vitest Snapshot v1 +exports[`order > fully controlled rules merged and sorted by body 1`] = ` +"/* layer: default */ +.uno{--var:uno;} +/* sort: css */ .foo{--foo:0} +/* sort: uno */ .foo{--foo:0} +.bar-css{--bar:css;} +.bar-uno{--bar:uno;} +.css{--var:css;}" +`; + exports[`order > movePseudoElementsEnd 1`] = `".marker\\\\:file\\\\:hover\\\\:selection\\\\:mb-4:hover::marker::file-selector-button::selection"`; exports[`order > multiple variant sorting 1`] = ` diff --git a/test/__snapshots__/preset-attributify.test.ts.snap b/test/__snapshots__/preset-attributify.test.ts.snap index e6be1382e9..8b5c607c38 100644 --- a/test/__snapshots__/preset-attributify.test.ts.snap +++ b/test/__snapshots__/preset-attributify.test.ts.snap @@ -41,37 +41,37 @@ exports[`attributify > autocomplete extractor 5`] = ` exports[`attributify > compatible with full controlled rules 1`] = ` "/* layer: default */ - [custom~=\\"\\\\31 \\"] { - font-size: 1px; + .custom-2 { + font-size: 2px; } /* you can have multiple rules */ - [custom~=\\"\\\\31 \\"]::after { + .custom-2::after { content: 'after'; } - .foo > [custom~=\\"\\\\31 \\"] { + .foo > .custom-2 { color: red; } /* or media queries */ @media (min-width: 680px) { - [custom~=\\"\\\\31 \\"] { + .custom-2 { font-size: 16px; } } - .custom-2 { - font-size: 2px; + [custom~=\\"\\\\31 \\"] { + font-size: 1px; } /* you can have multiple rules */ - .custom-2::after { + [custom~=\\"\\\\31 \\"]::after { content: 'after'; } - .foo > .custom-2 { + .foo > [custom~=\\"\\\\31 \\"] { color: red; } /* or media queries */ @media (min-width: 680px) { - .custom-2 { + [custom~=\\"\\\\31 \\"] { font-size: 16px; } } diff --git a/test/__snapshots__/preset-mini.test.ts.snap b/test/__snapshots__/preset-mini.test.ts.snap index 8bf5b70063..dd23f5ce3e 100644 --- a/test/__snapshots__/preset-mini.test.ts.snap +++ b/test/__snapshots__/preset-mini.test.ts.snap @@ -409,10 +409,10 @@ div:hover .group-\\\\[div\\\\:hover\\\\]-\\\\[combinator\\\\:test-4\\\\]{combina .text-rose{--un-text-opacity:1;color:rgba(251,113,133,var(--un-text-opacity));} .placeholder-color-opacity-60::placeholder{--un-text-opacity:0.6;} .text-opacity-\\\\[13\\\\.3333333\\\\%\\\\]{--un-text-opacity:13.3333333%;} -.italic, -.font-italic{font-style:italic;} -.oblique, -.font-oblique{font-style:oblique;} +.font-italic, +.italic{font-style:italic;} +.font-oblique, +.oblique{font-style:oblique;} .antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-smoothing:grayscale;} .shadow{--un-shadow:var(--un-shadow-inset) 0 1px 3px 0 var(--un-shadow-color, rgba(0,0,0,0.1)),var(--un-shadow-inset) 0 1px 2px -1px var(--un-shadow-color, rgba(0,0,0,0.1));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);} .shadow-none{--un-shadow:0 0 var(--un-shadow-color, rgba(0,0,0,0));box-shadow:var(--un-ring-offset-shadow, 0 0 #0000), var(--un-ring-shadow, 0 0 #0000), var(--un-shadow);} @@ -678,8 +678,8 @@ div:hover .group-\\\\[div\\\\:hover\\\\]-\\\\[combinator\\\\:test-4\\\\]{combina .float-none{float:none;} .clear-both{clear:both;} .clear-none{clear:none;} -.z0, -.z-0{z-index:0;} +.z-0, +.z0{z-index:0;} .-z-1{z-index:-1;} .z-\\\\$variable{z-index:var(--variable);} .z-1{z-index:1;} diff --git a/test/__snapshots__/preset-wind.test.ts.snap b/test/__snapshots__/preset-wind.test.ts.snap index a613df7747..87ebd45480 100644 --- a/test/__snapshots__/preset-wind.test.ts.snap +++ b/test/__snapshots__/preset-wind.test.ts.snap @@ -66,23 +66,19 @@ exports[`preset-wind > targets 1`] = ` .table-auto{table-layout:auto;} .table-empty-cells-visible{empty-cells:show;} .table-empty-cells-hidden{empty-cells:hide;} +@keyframes pulse-alt{from{transform:scale3d(1,1,1)}50%{transform:scale3d(1.05,1.05,1.05)}to{transform:scale3d(1,1,1)}} @keyframes pulse{0%, 100% {opacity:1} 50% {opacity:.5}} +.animate-keyframes-pulse, .keyframes-pulse{animation:pulse;} -@keyframes pulse-alt{from{transform:scale3d(1,1,1)}50%{transform:scale3d(1.05,1.05,1.05)}to{transform:scale3d(1,1,1)}} +.animate-keyframes-pulse-alt, .keyframes-pulse-alt{animation:pulse-alt;} -@keyframes pulse{0%, 100% {opacity:1} 50% {opacity:.5}} -.animate-keyframes-pulse{animation:pulse;} -@keyframes pulse-alt{from{transform:scale3d(1,1,1)}50%{transform:scale3d(1.05,1.05,1.05)}to{transform:scale3d(1,1,1)}} -.animate-keyframes-pulse-alt{animation:pulse-alt;} -@keyframes pulse{0%, 100% {opacity:1} 50% {opacity:.5}} -.animate-pulse{animation:pulse 2s cubic-bezier(0.4,0,.6,1) infinite;} -@keyframes pulse-alt{from{transform:scale3d(1,1,1)}50%{transform:scale3d(1.05,1.05,1.05)}to{transform:scale3d(1,1,1)}} -.animate-pulse-alt{animation:pulse-alt 1s ease-in-out infinite;} -.animate-\\\\[4s_linear_0s_infinite_alternate_move\\\\\\\\_eye\\\\]{animation:4s linear 0s infinite alternate move\\\\_eye;} -.animate-\\\\$variable{animation:var(--variable);} +@keyframes bounce{0%, 100% {transform:translateY(-25%);animation-timing-function:cubic-bezier(0.8,0,1,1)} 50% {transform:translateY(0);animation-timing-function:cubic-bezier(0,0,0.2,1)}} @keyframes ping{0%{transform:scale(1);opacity:1}75%,100%{transform:scale(2);opacity:0}} .\\\\!animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite !important;} -@keyframes bounce{0%, 100% {transform:translateY(-25%);animation-timing-function:cubic-bezier(0.8,0,1,1)} 50% {transform:translateY(0);animation-timing-function:cubic-bezier(0,0,0.2,1)}} +.animate-\\\\[4s_linear_0s_infinite_alternate_move\\\\\\\\_eye\\\\]{animation:4s linear 0s infinite alternate move\\\\_eye;} +.animate-\\\\$variable{animation:var(--variable);} +.animate-pulse{animation:pulse 2s cubic-bezier(0.4,0,.6,1) infinite;} +.animate-pulse-alt{animation:pulse-alt 1s ease-in-out infinite;} .hover\\\\:animate-bounce:hover{animation:bounce 1s linear infinite;} .animate-name-move{animation-name:move;} .animate-duration-\\\\$variable{animation-duration:var(--variable);} diff --git a/test/__snapshots__/selector-no-merge.test.ts.snap b/test/__snapshots__/selector-no-merge.test.ts.snap index cd93ac1e25..e41d307b35 100644 --- a/test/__snapshots__/selector-no-merge.test.ts.snap +++ b/test/__snapshots__/selector-no-merge.test.ts.snap @@ -2,8 +2,8 @@ exports[`selector > rules split selector 1`] = ` "/* layer: default */ -.to-merge, -.merge-candidate{merged:1;} +.merge-candidate, +.to-merge{merged:1;} .not-merged{merged:1;}" `; diff --git a/test/__snapshots__/shortcuts.test.ts.snap b/test/__snapshots__/shortcuts.test.ts.snap index f400318360..5f050c7685 100644 --- a/test/__snapshots__/shortcuts.test.ts.snap +++ b/test/__snapshots__/shortcuts.test.ts.snap @@ -4,10 +4,9 @@ exports[`shortcuts > animate 1`] = ` "/* layer: preflights */ *,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x:var(--un-empty,/*!*/ /*!*/);--un-pan-y:var(--un-empty,/*!*/ /*!*/);--un-pinch-zoom:var(--un-empty,/*!*/ /*!*/);--un-scroll-snap-strictness:proximity;--un-ordinal:var(--un-empty,/*!*/ /*!*/);--un-slashed-zero:var(--un-empty,/*!*/ /*!*/);--un-numeric-figure:var(--un-empty,/*!*/ /*!*/);--un-numeric-spacing:var(--un-empty,/*!*/ /*!*/);--un-numeric-fraction:var(--un-empty,/*!*/ /*!*/);--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 #0000;--un-ring-shadow:0 0 #0000;--un-shadow-inset:var(--un-empty,/*!*/ /*!*/);--un-shadow:0 0 #0000;--un-ring-inset:var(--un-empty,/*!*/ /*!*/);--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur:var(--un-empty,/*!*/ /*!*/);--un-brightness:var(--un-empty,/*!*/ /*!*/);--un-contrast:var(--un-empty,/*!*/ /*!*/);--un-drop-shadow:var(--un-empty,/*!*/ /*!*/);--un-grayscale:var(--un-empty,/*!*/ /*!*/);--un-hue-rotate:var(--un-empty,/*!*/ /*!*/);--un-invert:var(--un-empty,/*!*/ /*!*/);--un-saturate:var(--un-empty,/*!*/ /*!*/);--un-sepia:var(--un-empty,/*!*/ /*!*/);--un-backdrop-blur:var(--un-empty,/*!*/ /*!*/);--un-backdrop-brightness:var(--un-empty,/*!*/ /*!*/);--un-backdrop-contrast:var(--un-empty,/*!*/ /*!*/);--un-backdrop-grayscale:var(--un-empty,/*!*/ /*!*/);--un-backdrop-hue-rotate:var(--un-empty,/*!*/ /*!*/);--un-backdrop-invert:var(--un-empty,/*!*/ /*!*/);--un-backdrop-opacity:var(--un-empty,/*!*/ /*!*/);--un-backdrop-saturate:var(--un-empty,/*!*/ /*!*/);--un-backdrop-sepia:var(--un-empty,/*!*/ /*!*/);} /* layer: shortcuts */ -.loading{transition-duration:1000ms;} +.loading{animation:spin 1s linear infinite;transition-duration:1000ms;} /* layer: default */ -@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} -.loading{animation:spin 1s linear infinite;}" +@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}" `; exports[`shortcuts > dynamic 1`] = ` @@ -125,9 +124,9 @@ exports[`shortcuts > no-merge 1`] = ` "/* layer: preflights */ *,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x:var(--un-empty,/*!*/ /*!*/);--un-pan-y:var(--un-empty,/*!*/ /*!*/);--un-pinch-zoom:var(--un-empty,/*!*/ /*!*/);--un-scroll-snap-strictness:proximity;--un-ordinal:var(--un-empty,/*!*/ /*!*/);--un-slashed-zero:var(--un-empty,/*!*/ /*!*/);--un-numeric-figure:var(--un-empty,/*!*/ /*!*/);--un-numeric-spacing:var(--un-empty,/*!*/ /*!*/);--un-numeric-fraction:var(--un-empty,/*!*/ /*!*/);--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 #0000;--un-ring-shadow:0 0 #0000;--un-shadow-inset:var(--un-empty,/*!*/ /*!*/);--un-shadow:0 0 #0000;--un-ring-inset:var(--un-empty,/*!*/ /*!*/);--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur:var(--un-empty,/*!*/ /*!*/);--un-brightness:var(--un-empty,/*!*/ /*!*/);--un-contrast:var(--un-empty,/*!*/ /*!*/);--un-drop-shadow:var(--un-empty,/*!*/ /*!*/);--un-grayscale:var(--un-empty,/*!*/ /*!*/);--un-hue-rotate:var(--un-empty,/*!*/ /*!*/);--un-invert:var(--un-empty,/*!*/ /*!*/);--un-saturate:var(--un-empty,/*!*/ /*!*/);--un-sepia:var(--un-empty,/*!*/ /*!*/);--un-backdrop-blur:var(--un-empty,/*!*/ /*!*/);--un-backdrop-brightness:var(--un-empty,/*!*/ /*!*/);--un-backdrop-contrast:var(--un-empty,/*!*/ /*!*/);--un-backdrop-grayscale:var(--un-empty,/*!*/ /*!*/);--un-backdrop-hue-rotate:var(--un-empty,/*!*/ /*!*/);--un-backdrop-invert:var(--un-empty,/*!*/ /*!*/);--un-backdrop-opacity:var(--un-empty,/*!*/ /*!*/);--un-backdrop-saturate:var(--un-empty,/*!*/ /*!*/);--un-backdrop-sepia:var(--un-empty,/*!*/ /*!*/);} /* layer: default */ -.with-no-merge{no-merge:1;} -.with-no-merge, -.merge-candidate{merged:1;}" +.merge-candidate, +.with-no-merge{merged:1;} +.with-no-merge{no-merge:1;}" `; exports[`shortcuts > static 1`] = ` diff --git a/test/order.test.ts b/test/order.test.ts index d84d2ba8ee..329fd1e58b 100644 --- a/test/order.test.ts +++ b/test/order.test.ts @@ -113,4 +113,28 @@ describe('order', () => { expect(css).toMatchSnapshot() expect(css).toEqual(css2) }) + + test('fully controlled rules merged and sorted by body', async () => { + const uno = createGenerator({ + rules: [ + ['uno', { '--var': 'uno' }], + [/^foo-(.+)$/, ([, match]) => { + return `/* sort: ${match} */ .foo{--foo:0}` + }], + [/^bar-(.+)$/, ([, match]) => { + return [ + `/* sort: ${match} */ .foo{--foo:0}`, + { '--bar': match }, + ] + }], + ['css', { '--var': 'css' }], + ], + presets: [], + }) + const code = 'foo-uno uno css bar-uno bar-css foo-css' + const { css } = await uno.generate(code, { preflights: false }) + const { css: css2 } = await uno.generate(code, { preflights: false }) + expect(css).toMatchSnapshot() + expect(css).toEqual(css2) + }) })