From 45c2fa51747b9438adb82e124397d27227e10ca6 Mon Sep 17 00:00:00 2001 From: mackwang Date: Wed, 10 Jun 2026 14:20:40 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(unocss-plugin):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20wx:class=20=E5=AF=B9=E8=B1=A1=E5=86=99=E6=B3=95=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E7=B1=BB=E5=90=8D=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 unocss-plugin 无法处理 wx:class="{{ { 'ml-17rpx': flag } }}" 对象写法的问题。 模板编译阶段 transDynamicClassExpr 会将 wx:class 对象 key 中的特殊字符 编码为合法 identifier(如 ml-17rpx → ml_da_17rpxMpxEscape),导致 unocss-plugin 扫描编译产物时 parseStrings 无法识别,对应 CSS 不会被生成。 新增 parseMpxEscapeKeys,通过正则扫描 MpxEscape 结尾的 identifier key, 用 unescapeKey 还原原始类名后交给 transformClasses 处理,再重新编码写回 wxml。 支持的写法: - wx:class="{{ { 'ml-17rpx': flag } }}" — 普通类名 - wx:class="{{ { 'h-100%': true } }}" — 含特殊字符的类名 - wx:class="{{ { 'ml-17rpx h-100%': flag } }}" — key 中空格分隔的多个类名 - wx:class="{{ { '*card': flag } }}" — alias 展开 同步重构 trans-dynamic-class-expr.js: - mpEscape → escapeClassName,keyEscape → escapeKey,新增对应的 unescape 函数 - 硬编码字符串提取为具名常量(KEY_ESCAPE_SUFFIX/DASH/SPACE) - escapeReg 改为从 classNameEscapeMap 动态生成,保持 map 和 reg 同步 - classNameDecodeReg 按 token 长度降序排列,避免短 token 优先匹配 Co-Authored-By: Claude Opus 4.6 --- packages/unocss-plugin/lib/index.js | 6 ++ packages/unocss-plugin/lib/parser.js | 24 +++++++ .../trans-dynamic-class-expr.js | 67 ++++++++++++++++--- 3 files changed, 86 insertions(+), 11 deletions(-) diff --git a/packages/unocss-plugin/lib/index.js b/packages/unocss-plugin/lib/index.js index 74054bbad2..2a2fcc2ffd 100644 --- a/packages/unocss-plugin/lib/index.js +++ b/packages/unocss-plugin/lib/index.js @@ -14,11 +14,13 @@ const transformerVariantGroup = require('@unocss/transformer-variant-group') const { parseClasses, parseStrings, + parseMpxEscapeKeys, parseMustache, stringifyAttr, parseComments, parseCommentConfig } = require('./parser') +const { escapeKey, escapeClassName } = require('@mpxjs/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr') const { getReplaceSource, getConcatSource, getRawSource } = require('./source') const { transformStyle, @@ -358,6 +360,10 @@ class MpxUnocssPlugin { result = transformClasses(result, classNameHandler) expSource.replace(start, end, result) }) + parseMpxEscapeKeys(exp).forEach(({ result, start, end }) => { + const expanded = transformClasses(result, classNameHandler) + expSource.replace(start, end, escapeKey(escapeClassName(expanded))) + }) return expSource.source() }, str => transformClasses(str, classNameHandler)) if (replaced) { diff --git a/packages/unocss-plugin/lib/parser.js b/packages/unocss-plugin/lib/parser.js index 1ce02abc72..f2edf29dd5 100644 --- a/packages/unocss-plugin/lib/parser.js +++ b/packages/unocss-plugin/lib/parser.js @@ -1,4 +1,5 @@ const { parseMustache, stringifyAttr } = require('@mpxjs/webpack-plugin/lib/template-compiler/compiler') +const { unescapeKey } = require('@mpxjs/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr') function parseClasses (content) { const output = [] @@ -78,9 +79,32 @@ function parseStrings (content) { return output } +// 匹配对象字面量中经过 MpxEscape 编码的标识符形式的 key,如 { ml_da_17rpxMpxEscape: flag } +// key 前面必须是 { 或 ,(加可选空格),后面是 :,且必须以 MpxEscape 结尾 +const objKeyReg = /(?:[{,]\s*)([\w-]+?MpxEscape)(?=\s*:)/gm + +function parseMpxEscapeKeys (content) { + const output = [] + if (!content) { return output } + let match + objKeyReg.lastIndex = 0 + while (match = objKeyReg.exec(content)) { + const raw = match[1] + const end = match.index + match[0].length - 1 + const start = end - raw.length + 1 + output.push({ + result: unescapeKey(raw), + start, + end + }) + } + return output +} + module.exports = { parseClasses, parseStrings, + parseMpxEscapeKeys, parseComments, parseCommentConfig, parseMustache, diff --git a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js index ac61d261ed..07504c5cf5 100644 --- a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js +++ b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js @@ -3,8 +3,10 @@ const t = require('@babel/types') const traverse = require('@babel/traverse').default const generate = require('@babel/generator').default const isValidIdentifierStr = require('../utils/is-valid-identifier-str') -const escapeReg = /[()[\]{}#!.:,%'"+$]/g -const escapeMap = { +function escapeRegExp (str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} +const classNameEscapeMap = { '(': '_pl_', ')': '_pr_', '[': '_bl_', @@ -23,22 +25,65 @@ const escapeMap = { '+': '_a_', $: '_si_' } +const classNameEscapeReg = new RegExp('[' + Object.keys(classNameEscapeMap).map(escapeRegExp).join('') + ']', 'g') + +// classNameEscapeMap 的反向映射,用于还原 escapeClassName 编码 +const classNameDecodeMap = Object.keys(classNameEscapeMap).reduce((acc, key) => { + acc[classNameEscapeMap[key]] = key + return acc +}, {}) +const classNameDecodeReg = new RegExp(Object.keys(classNameDecodeMap).sort((a, b) => b.length - a.length).map(escapeRegExp).join('|'), 'g') -function mpEscape (str) { - return str.replace(escapeReg, function (match) { - if (escapeMap[match]) return escapeMap[match] +function escapeClassName (str) { + return str.replace(classNameEscapeReg, function (match) { + if (classNameEscapeMap[match]) return classNameEscapeMap[match] // unknown escaped return '_u_' }) } -function keyEscape (str) { - let result = str.replace(/-/g, '_da_').replace(/\s+/g, '_sp_') - if (result !== str) result += 'MpxEscape' - return result +function unescapeClassName (str) { + return str.replace(classNameDecodeReg, m => classNameDecodeMap[m] || m) +} + +const KEY_ESCAPE_SUFFIX = 'MpxEscape' +const KEY_ESCAPE_DASH = '_da_' +const KEY_ESCAPE_SPACE = '_sp_' + +const keyEscapeMap = { + '-': KEY_ESCAPE_DASH, + ' ': KEY_ESCAPE_SPACE } +const keyDecodeMap = Object.keys(keyEscapeMap).reduce((acc, key) => { + acc[keyEscapeMap[key]] = key + return acc +}, {}) +const keyDecodeReg = new RegExp(Object.keys(keyDecodeMap).map(escapeRegExp).join('|'), 'g') + +function escapeKey (str) { + const result = str.replace(/-/g, KEY_ESCAPE_DASH).replace(/\s+/g, KEY_ESCAPE_SPACE) + if (result !== str) return result + KEY_ESCAPE_SUFFIX + return str +} + +function unescapeKey (str) { + if (str.endsWith(KEY_ESCAPE_SUFFIX)) { + return unescapeClassName( + str.slice(0, -KEY_ESCAPE_SUFFIX.length).replace(keyDecodeReg, m => keyDecodeMap[m]) + ) + } + return str +} + +module.exports = transDynamicClassExpr +module.exports.KEY_ESCAPE_SUFFIX = KEY_ESCAPE_SUFFIX +module.exports.KEY_ESCAPE_DASH = KEY_ESCAPE_DASH +module.exports.KEY_ESCAPE_SPACE = KEY_ESCAPE_SPACE +module.exports.unescapeKey = unescapeKey +module.exports.escapeKey = escapeKey +module.exports.escapeClassName = escapeClassName -module.exports = function transDynamicClassExpr (expr, { error } = {}) { +function transDynamicClassExpr (expr, { error } = {}) { try { const ast = babylon.parse(expr, { plugins: [ @@ -50,7 +95,7 @@ module.exports = function transDynamicClassExpr (expr, { error } = {}) { path.node.properties.forEach((property) => { if (t.isObjectProperty(property) && !property.computed) { const rawPropertyName = property.key.name || property.key.value - const propertyName = keyEscape(mpEscape(rawPropertyName)) + const propertyName = escapeKey(escapeClassName(rawPropertyName)) if (!isValidIdentifierStr(propertyName)) { error && error(`Dynamic classname [${rawPropertyName}] can not be escaped as a valid identifier, which is not supported.`) } else { From 91c88c66c7cbd130020cae27a3e47d870d9c6b3a Mon Sep 17 00:00:00 2001 From: mackwang Date: Wed, 10 Jun 2026 14:21:28 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor(trans-dynamic-class-expr):=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=20classN?= =?UTF-8?q?ameDecodeReg=20=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当前 classNameDecodeMap 的所有 key 互不重叠,无需按长度降序排列。 Co-Authored-By: Claude Opus 4.6 --- .../lib/template-compiler/trans-dynamic-class-expr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js index 07504c5cf5..bfd32fd078 100644 --- a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js +++ b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js @@ -32,7 +32,7 @@ const classNameDecodeMap = Object.keys(classNameEscapeMap).reduce((acc, key) => acc[classNameEscapeMap[key]] = key return acc }, {}) -const classNameDecodeReg = new RegExp(Object.keys(classNameDecodeMap).sort((a, b) => b.length - a.length).map(escapeRegExp).join('|'), 'g') +const classNameDecodeReg = new RegExp(Object.keys(classNameDecodeMap).map(escapeRegExp).join('|'), 'g') function escapeClassName (str) { return str.replace(classNameEscapeReg, function (match) { From e7b7d772d67c9e4eca2499db641e01ca9fda93fd Mon Sep 17 00:00:00 2001 From: mackwang Date: Wed, 10 Jun 2026 15:51:30 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dwx:class=20?= =?UTF-8?q?=E4=B8=8D=E6=94=AF=E6=8C=81unocss=20alias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/unocss-plugin/lib/parser.js | 6 +++--- .../lib/template-compiler/trans-dynamic-class-expr.js | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/unocss-plugin/lib/parser.js b/packages/unocss-plugin/lib/parser.js index f2edf29dd5..a27b47fce9 100644 --- a/packages/unocss-plugin/lib/parser.js +++ b/packages/unocss-plugin/lib/parser.js @@ -79,9 +79,9 @@ function parseStrings (content) { return output } -// 匹配对象字面量中经过 MpxEscape 编码的标识符形式的 key,如 { ml_da_17rpxMpxEscape: flag } -// key 前面必须是 { 或 ,(加可选空格),后面是 :,且必须以 MpxEscape 结尾 -const objKeyReg = /(?:[{,]\s*)([\w-]+?MpxEscape)(?=\s*:)/gm +// 匹配对象字面量中标识符形式的 key,如 { ml_da_17rpxMpxEscape: flag, a: true } +// key 前面必须是 { 或 ,(加可选空格),后面是 : +const objKeyReg = /(?:[{,]\s*)([\w-]+?)(?=\s*:)/gm function parseMpxEscapeKeys (content) { const output = [] diff --git a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js index bfd32fd078..e32a57cb49 100644 --- a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js +++ b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js @@ -52,7 +52,8 @@ const KEY_ESCAPE_SPACE = '_sp_' const keyEscapeMap = { '-': KEY_ESCAPE_DASH, - ' ': KEY_ESCAPE_SPACE + ' ': KEY_ESCAPE_SPACE, + '*': '_st_' } const keyDecodeMap = Object.keys(keyEscapeMap).reduce((acc, key) => { acc[keyEscapeMap[key]] = key @@ -61,7 +62,7 @@ const keyDecodeMap = Object.keys(keyEscapeMap).reduce((acc, key) => { const keyDecodeReg = new RegExp(Object.keys(keyDecodeMap).map(escapeRegExp).join('|'), 'g') function escapeKey (str) { - const result = str.replace(/-/g, KEY_ESCAPE_DASH).replace(/\s+/g, KEY_ESCAPE_SPACE) + const result = str.replace(/-/g, KEY_ESCAPE_DASH).replace(/\s+/g, KEY_ESCAPE_SPACE).replace(/\*/g, '_st_') if (result !== str) return result + KEY_ESCAPE_SUFFIX return str } From 597db01a10ad3e539fa65d7238ac35320551d987 Mon Sep 17 00:00:00 2001 From: mackwang Date: Mon, 22 Jun 2026 15:57:51 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/template-compiler/trans-dynamic-class-expr.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js index e32a57cb49..340f42cc00 100644 --- a/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js +++ b/packages/webpack-plugin/lib/template-compiler/trans-dynamic-class-expr.js @@ -47,12 +47,10 @@ function unescapeClassName (str) { } const KEY_ESCAPE_SUFFIX = 'MpxEscape' -const KEY_ESCAPE_DASH = '_da_' -const KEY_ESCAPE_SPACE = '_sp_' const keyEscapeMap = { - '-': KEY_ESCAPE_DASH, - ' ': KEY_ESCAPE_SPACE, + '-': '_da_', + ' ': '_sp_', '*': '_st_' } const keyDecodeMap = Object.keys(keyEscapeMap).reduce((acc, key) => { @@ -62,7 +60,7 @@ const keyDecodeMap = Object.keys(keyEscapeMap).reduce((acc, key) => { const keyDecodeReg = new RegExp(Object.keys(keyDecodeMap).map(escapeRegExp).join('|'), 'g') function escapeKey (str) { - const result = str.replace(/-/g, KEY_ESCAPE_DASH).replace(/\s+/g, KEY_ESCAPE_SPACE).replace(/\*/g, '_st_') + const result = str.replace(/-/g, '_da_').replace(/\s+/g, '_sp_').replace(/\*/g, '_st_') if (result !== str) return result + KEY_ESCAPE_SUFFIX return str } @@ -77,9 +75,6 @@ function unescapeKey (str) { } module.exports = transDynamicClassExpr -module.exports.KEY_ESCAPE_SUFFIX = KEY_ESCAPE_SUFFIX -module.exports.KEY_ESCAPE_DASH = KEY_ESCAPE_DASH -module.exports.KEY_ESCAPE_SPACE = KEY_ESCAPE_SPACE module.exports.unescapeKey = unescapeKey module.exports.escapeKey = escapeKey module.exports.escapeClassName = escapeClassName