diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 93311ed03c..4d8211af72 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1,4 +1,4 @@ -import type { Postprocessor, Preprocessor, ResolvedConfig, Shortcut, ThemeExtender, UserConfig, UserConfigDefaults, UserShortcuts } from './types' +import type { Postprocessor, Preprocessor, Preset, ResolvedConfig, Shortcut, ThemeExtender, UserConfig, UserConfigDefaults, UserShortcuts } from './types' import { clone, isStaticRule, mergeDeep, normalizeVariant, toArray, uniq } from './utils' import { extractorSplit } from './extractors' import { DEAFULT_LAYERS } from './constants' @@ -11,12 +11,24 @@ export function resolveShortcuts(shortcuts: UserShortcuts): Shortcut[] { }) } +export function resolvePreset(preset: Preset): Preset { + if (preset.prefix) { + preset.rules?.forEach((i) => { + if (i[2]) + i[2].prefix ||= preset.prefix + else + i[2] = { prefix: preset.prefix } + }) + } + return preset +} + export function resolveConfig( userConfig: UserConfig = {}, defaults: UserConfigDefaults = {}, ): ResolvedConfig { const config = Object.assign({}, defaults, userConfig) as UserConfigDefaults - const rawPresets = (config.presets || []).flatMap(toArray) + const rawPresets = (config.presets || []).flatMap(toArray).map(resolvePreset) const sortedPresets = [ ...rawPresets.filter(p => p.enforce === 'pre'), @@ -45,7 +57,8 @@ export function resolveConfig( rules.forEach((rule, i) => { if (isStaticRule(rule)) { - rulesStaticMap[rule[0]] = [i, rule[1], rule[2], rule] + const prefix = rule[2]?.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 delete rules[i] diff --git a/packages/core/src/generator/index.ts b/packages/core/src/generator/index.ts index 1a037e9ef3..4ffe7218f6 100644 --- a/packages/core/src/generator/index.ts +++ b/packages/core/src/generator/index.ts @@ -445,7 +445,10 @@ export class UnoGenerator { // dynamic rules const [matcher, handler, meta] = rule - const match = processed.match(matcher) + if (meta?.prefix && !processed.startsWith(meta.prefix)) + continue + const unprefixed = meta?.prefix ? processed.slice(meta.prefix.length) : processed + const match = unprefixed.match(matcher) if (!match) continue diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f42b4841c2..d49078cbbd 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -157,6 +157,11 @@ export interface RuleMeta { */ autocomplete?: Arrayable + /** + * Matching prefix before this util + */ + prefix?: string + /** * Internal rules will only be matched for shortcuts but not the user code. * @default false @@ -449,6 +454,10 @@ export interface Preset extends ConfigBase { * Preset options for other tools like IDE to consume */ options?: PresetOptions + /** + * Apply prefix to all utilities + */ + prefix?: string } export interface GeneratorOptions { diff --git a/packages/preset-mini/src/index.ts b/packages/preset-mini/src/index.ts index 985c5f7c87..67514b4b21 100644 --- a/packages/preset-mini/src/index.ts +++ b/packages/preset-mini/src/index.ts @@ -24,6 +24,12 @@ export interface PresetMiniOptions extends PresetOptions { * @default 'un-' */ variablePrefix?: string + /** + * Utils prefix + * + * @default undefined + */ + prefix?: string } export const presetMini = (options: PresetMiniOptions = {}): Preset => { @@ -40,6 +46,7 @@ export const presetMini = (options: PresetMiniOptions = {}): Preset => { ? VarPrefixPostprocessor(options.variablePrefix) : undefined, preflights, + prefix: options.prefix, } } diff --git a/packages/preset-uno/src/index.ts b/packages/preset-uno/src/index.ts index fc43892a92..6b58b7cee1 100644 --- a/packages/preset-uno/src/index.ts +++ b/packages/preset-uno/src/index.ts @@ -23,6 +23,7 @@ export const presetUno = (options: PresetUnoOptions = {}): Preset => { ], options, preflights, + prefix: options.prefix, } } diff --git a/packages/preset-wind/src/index.ts b/packages/preset-wind/src/index.ts index b0923b57cd..6682ea0028 100644 --- a/packages/preset-wind/src/index.ts +++ b/packages/preset-wind/src/index.ts @@ -26,6 +26,7 @@ export const presetWind = (options: PresetWindOptions = {}): Preset => { variants: variants(options), options, preflights, + prefix: options.prefix, } } diff --git a/test/__snapshots__/prefix.test.ts.snap b/test/__snapshots__/prefix.test.ts.snap index 12fb7a9075..3e5d89beee 100644 --- a/test/__snapshots__/prefix.test.ts.snap +++ b/test/__snapshots__/prefix.test.ts.snap @@ -1,29 +1,8 @@ // Vitest Snapshot v1 -exports[`prefix 1`] = ` -"/* layer: icons */ -.dark .uno\\\\:dark\\\\:i-carbon-moon{--un-icon:url(\\"data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 32 32' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3Z'/%3E%3C/svg%3E\\");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;} -/* layer: shortcuts */ -.uno\\\\:btn{margin-right:2.5rem;} -.uno\\\\:btn1{margin-left:2.5rem;margin-right:2.5rem;} -/* layer: default */ -.uno\\\\:\\\\!p-5px{padding:5px !important;} -.uno\\\\:hover\\\\:p-4:hover{padding:1rem;} -.uno\\\\:pl-10px{padding-left:10px;} -@media (min-width: 640px){ -.uno\\\\:sm\\\\:p-6{padding:1.5rem;} -}" -`; - -exports[`tailwind prefix 1`] = ` -"/* layer: shortcuts */ -.uno-btn{margin-right:2.5rem;} -.uno\\\\:btn1{margin-left:2.5rem;margin-right:2.5rem;} -/* layer: default */ -.\\\\!uno\\\\:p-5px{padding:5px !important;} -.hover\\\\:uno\\\\:p-4:hover{padding:1rem;} -.uno\\\\:pl-10px{padding-left:10px;} -@media (min-width: 640px){ -.sm\\\\:uno\\\\:p-6{padding:1.5rem;} -}" +exports[`prefix > preset prefix 2`] = ` +"/* layer: default */ +.hover\\\\:foo-p4:hover{padding:1rem;} +.foo-text-red{--un-text-opacity:1;color:rgba(248,113,113,var(--un-text-opacity));} +.bar-bar{color:bar;}" `; diff --git a/test/__snapshots__/preprocess.test.ts.snap b/test/__snapshots__/preprocess.test.ts.snap new file mode 100644 index 0000000000..f485ef49ce --- /dev/null +++ b/test/__snapshots__/preprocess.test.ts.snap @@ -0,0 +1,29 @@ +// Vitest Snapshot v1 + +exports[`preprocess > prefix 1`] = ` +"/* layer: icons */ +.dark .uno\\\\:dark\\\\:i-carbon-moon{--un-icon:url(\\"data:image/svg+xml;utf8,%3Csvg preserveAspectRatio='xMidYMid meet' viewBox='0 0 32 32' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3Z'/%3E%3C/svg%3E\\");mask:var(--un-icon) no-repeat;mask-size:100% 100%;-webkit-mask:var(--un-icon) no-repeat;-webkit-mask-size:100% 100%;background-color:currentColor;width:1em;height:1em;} +/* layer: shortcuts */ +.uno\\\\:btn{margin-right:2.5rem;} +.uno\\\\:btn1{margin-left:2.5rem;margin-right:2.5rem;} +/* layer: default */ +.uno\\\\:\\\\!p-5px{padding:5px !important;} +.uno\\\\:hover\\\\:p-4:hover{padding:1rem;} +.uno\\\\:pl-10px{padding-left:10px;} +@media (min-width: 640px){ +.uno\\\\:sm\\\\:p-6{padding:1.5rem;} +}" +`; + +exports[`preprocess > tailwind prefix 1`] = ` +"/* layer: shortcuts */ +.uno-btn{margin-right:2.5rem;} +.uno\\\\:btn1{margin-left:2.5rem;margin-right:2.5rem;} +/* layer: default */ +.\\\\!uno\\\\:p-5px{padding:5px !important;} +.hover\\\\:uno\\\\:p-4:hover{padding:1rem;} +.uno\\\\:pl-10px{padding-left:10px;} +@media (min-width: 640px){ +.sm\\\\:uno\\\\:p-6{padding:1.5rem;} +}" +`; diff --git a/test/prefix.test.ts b/test/prefix.test.ts index deb03e1aeb..b12313e810 100644 --- a/test/prefix.test.ts +++ b/test/prefix.test.ts @@ -1,75 +1,35 @@ import { createGenerator } from '@unocss/core' -import presetUno from '@unocss/preset-uno' -import presetIcons from '@unocss/preset-icons' -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' +import presetMini from '@unocss/preset-mini' -test('prefix', async () => { - const positive = [ - 'uno:pl-10px', - 'uno:hover:p-4', - 'uno:sm:p-6', - 'uno:!p-5px', - 'uno:btn', - 'uno:btn1', - 'uno:dark:i-carbon-moon', - ] +describe('prefix', () => { + test('preset prefix', async () => { + const uno = createGenerator({ + presets: [ + presetMini({ prefix: 'foo-' }), + ], + rules: [ + ['bar', { color: 'bar' }, { prefix: 'bar-' }], + ], + }) - const negative = [ - 'pl-10px', - 'hover:p-4', - '!p-5px', - 'btn', - 'btn1', - 'i-carbon-moon', - ] + const { css, matched } = await uno.generate(new Set([ + 'text-red', + 'foo-text-red', + 'hover:p4', + 'hover:foo-p4', + 'bar', + 'bar-bar', + ]), { preflights: false }) - const uno = createGenerator({ - preprocess: m => m.startsWith('uno:') ? m.substr(4) : '', - presets: [ - presetUno(), - presetIcons(), - ], - shortcuts: { - btn: 'mr-10', - btn1: 'ml-10 btn', - }, - }) - const { css, matched } = await uno.generate(new Set([...positive, ...negative]), { preflights: false }) - expect(matched).eql(new Set(positive)) - expect(css).toMatchSnapshot() -}) - -test('tailwind prefix', async () => { - const positive = [ - 'uno:pl-10px', - 'hover:uno:p-4', - 'sm:uno:p-6', - '!uno:p-5px', - 'uno-btn', - 'uno:btn1', - ] - - const negative = [ - 'pl-10px', - 'hover:p-4', - '!p-5px', - 'btn', - 'btn1', - ] + expect(matched).toMatchInlineSnapshot(` + Set { + "bar-bar", + "foo-text-red", + "hover:foo-p4", + } + `) - const prefixRE = /uno[:-]/ - const uno = createGenerator({ - preprocess: m => prefixRE.test(m) ? m.replace(prefixRE, '') : '', - presets: [ - presetUno(), - presetIcons(), - ], - shortcuts: { - btn: 'mr-10', - btn1: 'ml-10 btn', - }, + expect(css).toMatchSnapshot() }) - const { css, matched } = await uno.generate(new Set([...positive, ...negative]), { preflights: false }) - expect(matched).eql(new Set(positive)) - expect(css).toMatchSnapshot() }) diff --git a/test/preprocess.test.ts b/test/preprocess.test.ts new file mode 100644 index 0000000000..ec7695ad00 --- /dev/null +++ b/test/preprocess.test.ts @@ -0,0 +1,77 @@ +import { createGenerator } from '@unocss/core' +import presetUno from '@unocss/preset-uno' +import presetIcons from '@unocss/preset-icons' +import { describe, expect, test } from 'vitest' + +describe('preprocess', () => { + test('prefix', async () => { + const positive = [ + 'uno:pl-10px', + 'uno:hover:p-4', + 'uno:sm:p-6', + 'uno:!p-5px', + 'uno:btn', + 'uno:btn1', + 'uno:dark:i-carbon-moon', + ] + + const negative = [ + 'pl-10px', + 'hover:p-4', + '!p-5px', + 'btn', + 'btn1', + 'i-carbon-moon', + ] + + const uno = createGenerator({ + preprocess: m => m.startsWith('uno:') ? m.substr(4) : '', + presets: [ + presetUno(), + presetIcons(), + ], + shortcuts: { + btn: 'mr-10', + btn1: 'ml-10 btn', + }, + }) + const { css, matched } = await uno.generate(new Set([...positive, ...negative]), { preflights: false }) + expect(matched).eql(new Set(positive)) + expect(css).toMatchSnapshot() + }) + + test('tailwind prefix', async () => { + const positive = [ + 'uno:pl-10px', + 'hover:uno:p-4', + 'sm:uno:p-6', + '!uno:p-5px', + 'uno-btn', + 'uno:btn1', + ] + + const negative = [ + 'pl-10px', + 'hover:p-4', + '!p-5px', + 'btn', + 'btn1', + ] + + const prefixRE = /uno[:-]/ + const uno = createGenerator({ + preprocess: m => prefixRE.test(m) ? m.replace(prefixRE, '') : '', + presets: [ + presetUno(), + presetIcons(), + ], + shortcuts: { + btn: 'mr-10', + btn1: 'ml-10 btn', + }, + }) + const { css, matched } = await uno.generate(new Set([...positive, ...negative]), { preflights: false }) + expect(matched).eql(new Set(positive)) + expect(css).toMatchSnapshot() + }) +})