diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index ee6894563c..92902c41f3 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -23,9 +23,9 @@ export function resolvePreset(preset: Preset): Pre i[2] = {} const meta = i[2] if (meta.prefix == null && preset.prefix) - meta.prefix = preset.prefix + meta.prefix = toArray(preset.prefix) if (meta.layer == null && preset.layer) - meta.prefix = preset.layer + meta.layer = preset.layer } shortcuts?.forEach(apply) preset.rules?.forEach(apply) @@ -69,8 +69,10 @@ export function resolveConfig( const rulesDynamic = rules .map((rule, i) => { if (isStaticRule(rule)) { - const prefix = rule[2]?.prefix || '' - rulesStaticMap[prefix + rule[0]] = [i, rule[1], rule[2], rule] + const prefixes = toArray(rule[2]?.prefix || '') + prefixes.forEach((prefix) => { + rulesStaticMap[prefix + rule[0]] = [i, rule[1], rule[2], rule] + }) // delete static rules so we can't skip them in matching // but keep the order return undefined diff --git a/packages/core/src/generator/index.ts b/packages/core/src/generator/index.ts index 9abc5cf16c..b895a7ff60 100644 --- a/packages/core/src/generator/index.ts +++ b/packages/core/src/generator/index.ts @@ -1,7 +1,7 @@ import { createNanoEvents } from '../utils/events' import type { CSSEntries, CSSObject, DynamicRule, ExtractorContext, GenerateOptions, GenerateResult, ParsedUtil, PreflightContext, PreparedRule, RawUtil, ResolvedConfig, RuleContext, RuleMeta, Shortcut, ShortcutValue, StringifiedUtil, UserConfig, UserConfigDefaults, UtilObject, Variant, VariantContext, VariantHandler, VariantHandlerContext, VariantMatchedResult } from '../types' import { resolveConfig } from '../config' -import { CONTROL_SHORTCUT_NO_MERGE, TwoKeyMap, e, entriesToCss, expandVariantGroup, isRawUtil, isStaticShortcut, isString, noop, normalizeCSSEntries, normalizeCSSValues, notNull, uniq, warnOnce } from '../utils' +import { CONTROL_SHORTCUT_NO_MERGE, TwoKeyMap, e, entriesToCss, expandVariantGroup, isRawUtil, isStaticShortcut, isString, noop, normalizeCSSEntries, normalizeCSSValues, notNull, toArray, uniq, warnOnce } from '../utils' import { version } from '../../package.json' import { LAYER_DEFAULT, LAYER_PREFLIGHTS } from '../constants' @@ -394,7 +394,12 @@ export class UnoGenerator { return cssBody } - async parseUtil(input: string | VariantMatchedResult, context: RuleContext, internal = false, shortcutPrefix: string | undefined = undefined): Promise<(ParsedUtil | RawUtil)[] | undefined> { + async parseUtil( + input: string | VariantMatchedResult, + context: RuleContext, + internal = false, + shortcutPrefix?: string | string[] | undefined, + ): Promise<(ParsedUtil | RawUtil)[] | undefined> { const [raw, processed, variantHandlers] = isString(input) ? this.matchVariants(input) : input @@ -432,14 +437,17 @@ export class UnoGenerator { // match prefix let unprefixed = processed if (meta?.prefix) { + const prefixes = toArray(meta.prefix) if (shortcutPrefix) { - if (shortcutPrefix !== meta.prefix) + const shortcutPrefixes = toArray(shortcutPrefix) + if (!prefixes.some(i => shortcutPrefixes.includes(i))) continue } else { - if (!processed.startsWith(meta.prefix)) + const prefix = prefixes.find(i => processed.startsWith(i)) + if (prefix == null) continue - unprefixed = processed.slice(meta.prefix.length) + unprefixed = processed.slice(prefix.length) } } @@ -504,9 +512,11 @@ export class UnoGenerator { for (const s of this.config.shortcuts) { let unprefixed = input if (s[2]?.prefix) { - if (!input.startsWith(s[2].prefix)) + const prefixes = toArray(s[2].prefix) + const prefix = prefixes.find(i => input.startsWith(i)) + if (prefix == null) continue - unprefixed = input.slice(s[2].prefix.length) + unprefixed = input.slice(prefix.length) } if (isStaticShortcut(s)) { if (s[0] === unprefixed) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e03a135fa8..4e679338c4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -160,7 +160,7 @@ export interface RuleMeta { /** * Matching prefix before this util */ - prefix?: string + prefix?: string | string[] /** * Internal rules will only be matched for shortcuts but not the user code. @@ -463,7 +463,7 @@ export interface Preset extends ConfigBase { /** * Apply prefix to all utilities and shortcuts */ - prefix?: string + prefix?: string | string[] /** * Apply layer to all utilities and shortcuts */ diff --git a/packages/preset-icons/src/types.ts b/packages/preset-icons/src/types.ts index eb45afe76d..d3249a579b 100644 --- a/packages/preset-icons/src/types.ts +++ b/packages/preset-icons/src/types.ts @@ -25,7 +25,7 @@ export interface IconsOptions { * * @default `i-` */ - prefix?: string + prefix?: string | string[] /** * Extra CSS properties applied to the generated CSS * diff --git a/packages/preset-mini/src/index.ts b/packages/preset-mini/src/index.ts index 83309636c5..5b9c8629ea 100644 --- a/packages/preset-mini/src/index.ts +++ b/packages/preset-mini/src/index.ts @@ -51,7 +51,7 @@ export interface PresetMiniOptions extends PresetOptions { * * @default undefined */ - prefix?: string + prefix?: string | string[] /** * Generate preflight * diff --git a/test/__snapshots__/prefix.test.ts.snap b/test/__snapshots__/prefix.test.ts.snap index db2bea5960..d3d9d75eb3 100644 --- a/test/__snapshots__/prefix.test.ts.snap +++ b/test/__snapshots__/prefix.test.ts.snap @@ -1,6 +1,52 @@ // Vitest Snapshot v1 -exports[`prefix > preset prefix 2`] = ` +exports[`prefix > multiple preset prefix 1`] = ` +"/* layer: default */ +.h-container{max-width:100%;} +.hover\\\\:h-p4:hover{padding:1rem;} +.h-text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));} +.bar, +.bar-bar, +.bar-shortcut, +.foo-bar, +.x-shortcut{color:bar;} +.bar-regex, +.foo-regex, +.regex{color:regex;} +@media (min-width: 640px){ +.h-container{max-width:640px;} +} +@media (min-width: 768px){ +.h-container{max-width:768px;} +} +@media (min-width: 1024px){ +.h-container{max-width:1024px;} +} +@media (min-width: 1280px){ +.h-container{max-width:1280px;} +} +@media (min-width: 1024px){@media (min-width: 1536px){ +.\\\\32 xl\\\\:h-container{max-width:1024px;} +}} +@media (min-width: 1280px){@media (min-width: 1536px){ +.\\\\32 xl\\\\:h-container{max-width:1280px;} +}} +@media (min-width: 1536px){ +.\\\\32 xl\\\\:h-container{max-width:100%;} +.h-container{max-width:1536px;} +} +@media (min-width: 1536px){@media (min-width: 1536px){ +.\\\\32 xl\\\\:h-container{max-width:1536px;} +}} +@media (min-width: 640px){@media (min-width: 1536px){ +.\\\\32 xl\\\\:h-container{max-width:640px;} +}} +@media (min-width: 768px){@media (min-width: 1536px){ +.\\\\32 xl\\\\:h-container{max-width:768px;} +}}" +`; + +exports[`prefix > preset prefix 1`] = ` "/* layer: default */ .h-container{max-width:100%;} .hover\\\\:h-p4:hover{padding:1rem;} diff --git a/test/prefix.test.ts b/test/prefix.test.ts index 889c15da36..69b0253e1f 100644 --- a/test/prefix.test.ts +++ b/test/prefix.test.ts @@ -16,31 +16,77 @@ describe('prefix', () => { ], }) - const { css, matched } = await uno.generate(new Set([ + const unexpected = [ 'text-red', 'hover:p4', 'bar', 'shortcut', - // expected + ] + + const expected = [ 'h-text-red', 'hover:h-p4', 'bar-bar', 'bar-shortcut', 'h-container', '2xl:h-container', + ] + + const { css, matched } = await uno.generate(new Set([ + ...unexpected, + ...expected, ]), { preflights: false }) - expect(matched).toMatchInlineSnapshot(` - Set { - "bar-bar", - "h-text-red", - "hover:h-p4", - "bar-shortcut", - "h-container", - "2xl:h-container", - } - `) + expect([...matched].sort()).toEqual(expected.sort()) + expect(css).toMatchSnapshot() + }) + + test('multiple preset prefix', async () => { + const uno = createGenerator({ + presets: [ + presetUno({ prefix: ['h-', 'x-'] }), + ], + rules: [ + ['bar', { color: 'bar' }, { prefix: ['bar-', 'foo-', ''] }], + [/^regex$/, () => ({ color: 'regex' }), { prefix: ['bar-', 'foo-', ''] }], + ], + shortcuts: [ + ['shortcut', 'bar-bar', { prefix: ['bar-', 'x-'] }], + ], + }) + + const unexpected = [ + 'text-red', + 'hover:p4', + 'bar', + 'shortcut', + ] + + const expected = [ + 'h-text-red', + 'hover:h-p4', + + 'bar-bar', + 'foo-bar', + 'bar', + + 'bar-regex', + 'foo-regex', + 'regex', + + 'bar-shortcut', + 'x-shortcut', + + 'h-container', + '2xl:h-container', + ] + + const { css, matched } = await uno.generate(new Set([ + ...unexpected, + ...expected, + ]), { preflights: false }) + expect([...matched].sort()).toEqual(expected.sort()) expect(css).toMatchSnapshot() }) })