diff --git a/packages/transformer-compile-class/src/index.ts b/packages/transformer-compile-class/src/index.ts index dfcae5ec9c..9de6cebf2f 100644 --- a/packages/transformer-compile-class/src/index.ts +++ b/packages/transformer-compile-class/src/index.ts @@ -3,10 +3,35 @@ import { escapeRegExp, expandVariantGroup } from '@unocss/core' export interface CompileClassOptions { /** - * Trigger string - * @default ':uno:' + * Trigger regex literal. The default trigger regex literal matches `:uno:`, + * for example: `
`. + * + * @example + * The trigger additionally allows defining a capture group named `name`, which + * allows custom class names. One possible regex would be: + * + * ``` + * export default defineConfig({ + * transformers: [ + * transformerCompileClass({ + * trigger: /(["'`]):uno(?:-)?(?[^\s\1]+)?:\s([^\1]*?)\1/g + * }), + * ], + * }) + * ``` + * + * This regular expression matches `:uno-MYNAME:` and uses `MYNAME` in + * combination with the class prefix as the final class name, for example: + * `.uno-MYNAME`. It should be noted that the regex literal needs to include + * the global flag `/g`. + * + * @note + * This parameter is backwards compatible. It accepts string only trigger + * words, like `:uno:` or a regex literal. + * + * @default `/(["'`]):uno:\s([^\1]*?)\1/g` */ - trigger?: string + trigger?: string | RegExp /** * Prefix for compile class name @@ -34,25 +59,34 @@ export interface CompileClassOptions { export default function transformerCompileClass(options: CompileClassOptions = {}): SourceCodeTransformer { const { - trigger = ':uno:', + trigger = /(["'`]):uno:\s([^\1]*?)\1/g, classPrefix = 'uno-', hashFn = hash, keepUnknown = true, } = options - const regex = new RegExp(`(["'\`])${escapeRegExp(trigger)}\\s([^\\1]*?)\\1`, 'g') + + // Provides backwards compatibility. We either accept a trigger string which + // gets turned into a regexp (like previously) or a regex literal directly. + const regexp = typeof trigger === 'string' + ? RegExp(`(["'\`])${escapeRegExp(trigger)}\\s([^\\1]*?)\\1`, 'g') + : trigger return { name: '@unocss/transformer-compile-class', enforce: 'pre', async transform(s, _, { uno, tokens }) { - const matches = [...s.original.matchAll(regex)] + const matches = [...s.original.matchAll(regexp)] if (!matches.length) return for (const match of matches) { - let body = expandVariantGroup(match[2].trim()) + let body = (match.length === 4 && match.groups) + ? expandVariantGroup(match[3].trim()) + : expandVariantGroup(match[2].trim()) + const start = match.index! const replacements = [] + if (keepUnknown) { const result = await Promise.all(body.split(/\s+/).filter(Boolean).map(async i => [i, !!await uno.parseToken(i)] as const)) const known = result.filter(([, matched]) => matched).map(([i]) => i) @@ -60,17 +94,28 @@ export default function transformerCompileClass(options: CompileClassOptions = { replacements.push(...unknown) body = known.join(' ') } + if (body) { body = body.split(/\s+/).sort().join(' ') - const hash = hashFn(body) - const className = `${classPrefix}${hash}` + const className = (match.groups && match.groups.name) + ? `${classPrefix}${match.groups.name}` + : `${classPrefix}${hashFn(body)}` + + // FIXME: Ideally we should also check that the hash doesn't match. If the hash is the same, the same class + // name is allowed, as the applied styles are the same. + if (tokens && tokens.has(className)) + throw new Error(`duplicate compile class name '${className}', please choose different class name`) + replacements.unshift(className) if (options.layer) uno.config.shortcuts.push([className, body, { layer: options.layer }]) else uno.config.shortcuts.push([className, body]) - tokens.add(className) + + if (tokens) + tokens.add(className) } + s.overwrite(start + 1, start + match[0].length - 1, replacements.join(' ')) } }, diff --git a/playground/src/auto-imports.d.ts b/playground/src/auto-imports.d.ts index f961d20ef7..d5b24ee1a0 100644 --- a/playground/src/auto-imports.d.ts +++ b/playground/src/auto-imports.d.ts @@ -22,7 +22,9 @@ declare global { const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] const createProjection: typeof import('@vueuse/math')['createProjection'] const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] + const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate'] const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] + const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise'] const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] const cssFormatted: typeof import('./composables/prettier')['cssFormatted'] const customCSS: typeof import('./composables/url')['customCSS'] @@ -139,11 +141,14 @@ declare global { const until: typeof import('@vueuse/core')['until'] const useAbs: typeof import('@vueuse/math')['useAbs'] const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] + const useAnimate: typeof import('@vueuse/core')['useAnimate'] + const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference'] const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast'] + const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes'] const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] @@ -236,6 +241,8 @@ declare global { const useOnline: typeof import('@vueuse/core')['useOnline'] const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] const useParallax: typeof import('@vueuse/core')['useParallax'] + const useParentElement: typeof import('@vueuse/core')['useParentElement'] + const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver'] const usePermission: typeof import('@vueuse/core')['usePermission'] const usePointer: typeof import('@vueuse/core')['usePointer'] const usePointerLock: typeof import('@vueuse/core')['usePointerLock'] @@ -284,7 +291,6 @@ declare global { const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] const useTitle: typeof import('@vueuse/core')['useTitle'] - const useToFixed: typeof import('@vueuse/math')['useToFixed'] const useToNumber: typeof import('@vueuse/core')['useToNumber'] const useToString: typeof import('@vueuse/core')['useToString'] const useToggle: typeof import('@vueuse/core')['useToggle'] @@ -309,8 +315,10 @@ declare global { const watchArray: typeof import('@vueuse/core')['watchArray'] const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] + const watchDeep: typeof import('@vueuse/core')['watchDeep'] const watchEffect: typeof import('vue')['watchEffect'] const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] + const watchImmediate: typeof import('@vueuse/core')['watchImmediate'] const watchOnce: typeof import('@vueuse/core')['watchOnce'] const watchPausable: typeof import('@vueuse/core')['watchPausable'] const watchPostEffect: typeof import('vue')['watchPostEffect'] @@ -347,7 +355,9 @@ declare module 'vue' { readonly createInjectionState: UnwrapRef readonly createProjection: UnwrapRef readonly createReactiveFn: UnwrapRef + readonly createReusableTemplate: UnwrapRef readonly createSharedComposable: UnwrapRef + readonly createTemplatePromise: UnwrapRef readonly createUnrefFn: UnwrapRef readonly cssFormatted: UnwrapRef readonly customCSS: UnwrapRef @@ -464,11 +474,14 @@ declare module 'vue' { readonly until: UnwrapRef readonly useAbs: UnwrapRef readonly useActiveElement: UnwrapRef + readonly useAnimate: UnwrapRef + readonly useArrayDifference: UnwrapRef readonly useArrayEvery: UnwrapRef readonly useArrayFilter: UnwrapRef readonly useArrayFind: UnwrapRef readonly useArrayFindIndex: UnwrapRef readonly useArrayFindLast: UnwrapRef + readonly useArrayIncludes: UnwrapRef readonly useArrayJoin: UnwrapRef readonly useArrayMap: UnwrapRef readonly useArrayReduce: UnwrapRef @@ -561,6 +574,8 @@ declare module 'vue' { readonly useOnline: UnwrapRef readonly usePageLeave: UnwrapRef readonly useParallax: UnwrapRef + readonly useParentElement: UnwrapRef + readonly usePerformanceObserver: UnwrapRef readonly usePermission: UnwrapRef readonly usePointer: UnwrapRef readonly usePointerLock: UnwrapRef @@ -609,7 +624,6 @@ declare module 'vue' { readonly useTimeoutPoll: UnwrapRef readonly useTimestamp: UnwrapRef readonly useTitle: UnwrapRef - readonly useToFixed: UnwrapRef readonly useToNumber: UnwrapRef readonly useToString: UnwrapRef readonly useToggle: UnwrapRef @@ -634,8 +648,10 @@ declare module 'vue' { readonly watchArray: UnwrapRef readonly watchAtMost: UnwrapRef readonly watchDebounced: UnwrapRef + readonly watchDeep: UnwrapRef readonly watchEffect: UnwrapRef readonly watchIgnorable: UnwrapRef + readonly watchImmediate: UnwrapRef readonly watchOnce: UnwrapRef readonly watchPausable: UnwrapRef readonly watchPostEffect: UnwrapRef diff --git a/test/transformer-compile-class.test.ts b/test/transformer-compile-class.test.ts index 6e7fe2184a..974912fe56 100644 --- a/test/transformer-compile-class.test.ts +++ b/test/transformer-compile-class.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'vitest' -import type { UnoGenerator } from '@unocss/core' +import type { SourceCodeTransformer, UnoGenerator } from '@unocss/core' import { createGenerator } from '@unocss/core' import MagicString from 'magic-string' import transformerCompileClass from '@unocss/transformer-compile-class' @@ -11,11 +11,15 @@ describe('transformer-compile-class', () => { presetUno(), ], }) - const transformer = transformerCompileClass() - async function transform(code: string, _uno: UnoGenerator = uno) { + const defaultTransformer = transformerCompileClass() + const customClassNameTransformer = transformerCompileClass({ + trigger: /(["'`]):uno(?:-)?(?[^\s\1]+)?:\s([^\1]*?)\1/g, + }) + + async function transform(code: string, _uno: UnoGenerator = uno, _tranformer: SourceCodeTransformer = defaultTransformer) { const s = new MagicString(code) - await transformer.transform(s, 'foo.js', { uno: _uno, tokens: new Set() } as any) + await _tranformer.transform(s, 'foo.js', { uno: _uno, tokens: new Set() } as any) const result = s.toString() const { css } = await uno.generate(result, { preflights: false }) return { @@ -61,4 +65,46 @@ describe('transformer-compile-class', () => { expect(order1.css).toBe(order2.css) expect(order1.code).toBe(order2.code) }) + + test('custom class name trigger (without class name)', async () => { + const result = await transform(` +
`.trim(), uno, customClassNameTransformer) + + expect(result.code.trim()).toMatchInlineSnapshot(` + "
" + `) + + expect(result.css).toMatchInlineSnapshot(` + "/* layer: shortcuts */ + .uno-trmz0g{--un-bg-opacity:1;background-color:rgba(239,68,68,var(--un-bg-opacity));font-size:1.25rem;line-height:1.75rem;}" + `) + }) + + test('custom class name trigger (with basic class name)', async () => { + const result = await transform(` +
`.trim(), uno, customClassNameTransformer) + + expect(result.code.trim()).toMatchInlineSnapshot(` + "
" + `) + + expect(result.css).toMatchInlineSnapshot(` + "/* layer: shortcuts */ + .uno-foo{--un-bg-opacity:1;background-color:rgba(239,68,68,var(--un-bg-opacity));font-size:1.25rem;line-height:1.75rem;}" + `) + }) + + test('custom class name trigger (with complex class name)', async () => { + const result = await transform(` +
`.trim(), uno, customClassNameTransformer) + + expect(result.code.trim()).toMatchInlineSnapshot(` + "
" + `) + + expect(result.css).toMatchInlineSnapshot(` + "/* layer: shortcuts */ + .uno-foo_bar-baz{--un-bg-opacity:1;background-color:rgba(239,68,68,var(--un-bg-opacity));font-size:1.25rem;line-height:1.75rem;}" + `) + }) })