// @ts-ignore import valueParser from "postcss-value-parser"; import { config } from "../config"; import type { Plugin } from "vite"; import { SAFE_CHAR_MAP } from "./config"; import { addScriptContent, getClassContent, getClassNames, getNodes, isTailwindClass, } from "./utils"; /** * Tailwind 默认值 */ const TW_DEFAULT_VALUES: Record = { "--tw-border-spacing-x": 0, "--tw-border-spacing-y": 0, "--tw-translate-x": 0, "--tw-translate-y": 0, "--tw-rotate": 0, "--tw-skew-x": 0, "--tw-skew-y": 0, "--tw-scale-x": 1, "--tw-scale-y": 1, }; /** * 转换类名中的特殊字符为安全字符 */ function toSafeClass(className: string): string { if (className.includes(":host")) { return className; } let safeClassName = className; // 移除转义字符 if (safeClassName.includes("\\")) { safeClassName = safeClassName.replace(/\\/g, ""); } // 处理暗黑模式 if (safeClassName.includes(":is")) { if (safeClassName.includes(":is(.dark *)")) { safeClassName = safeClassName.replace(/:is\(.dark \*\)/g, ""); if (safeClassName.startsWith(".dark:")) { const className = safeClassName.replace(/^\.dark:/, ".dark:"); safeClassName = `${className}`; } } } // 替换特殊字符 for (const [char, replacement] of Object.entries(SAFE_CHAR_MAP)) { const regex = new RegExp("\\" + char, "g"); if (regex.test(safeClassName)) { safeClassName = safeClassName.replace(regex, replacement); } } return safeClassName; } /** * 转换 RGB 为 RGBA 格式 */ function rgbToRgba(rgbValue: string): string { const match = rgbValue.match(/rgb\(([\d\s]+)\/\s*([\d.]+)\)/); if (!match) return rgbValue; const [, rgb, alpha] = match; const [r, g, b] = rgb.split(/\s+/); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function remToRpx(remValue: string): string { const { remUnit = 14, remPrecision = 6, rpxRatio = 2 } = config.tailwind!; const conversionFactor = remUnit * rpxRatio; const precision = (remValue.split(".")[1] || "").length; const rpxValue = (parseFloat(remValue) * conversionFactor) .toFixed(precision || remPrecision) .replace(/\.?0+$/, ""); return `${rpxValue}rpx`; } /** * PostCSS 插件 * 处理类名和单位转换 */ function postcssPlugin(): Plugin { return { name: "vite-cool-uniappx-postcss", enforce: "pre", config() { return { css: { postcss: { plugins: [ { postcssPlugin: "vite-cool-uniappx-class-mapping", prepare() { // 存储 Tailwind 颜色值 const colorValues = { ...TW_DEFAULT_VALUES, }; return { // 处理选择器规则 Rule(rule: any) { if ( rule.selector.includes("uni-") || [".button-hover"].some((e) => rule.selector.includes(e), ) ) { return; } // 转换选择器为安全的类名格式 rule.selector = toSafeClass( rule.selector.replace(/\\/g, ""), ); }, // 处理声明规则 Declaration(decl: any) { // 处理 Tailwind 自定义属性 if (decl.prop.includes("--tw-")) { colorValues[decl.prop] = decl.value.includes("rem") ? remToRpx(decl.value) : decl.value; decl.remove(); return; } // 转换 RGB 颜色为 RGBA 格式 if ( decl.value.includes("rgb(") && decl.value.includes("/") ) { decl.value = rgbToRgba(decl.value); } // 处理文本大小相关样式 if ( decl.value.includes("rpx") && decl.prop == "color" && decl.parent.selector.includes("text-") ) { decl.prop = "font-size"; } // 解析声明值 const parsed = valueParser(decl.value); let hasChanges = false; // 遍历并处理声明值中的节点 parsed.walk((node: any) => { // 处理单位转换(rem -> rpx) if (node.type === "word") { const unit = valueParser.unit(node.value); if (unit?.unit === "rem") { node.value = remToRpx(unit.number); hasChanges = true; } } // 处理 CSS 变量 if ( node.type === "function" && node.value === "var" ) { const twKey = node.nodes[0]?.value; // 替换 Tailwind 变量为实际值 if (twKey?.startsWith("--tw-")) { node.type = "word"; node.value = colorValues[twKey]; hasChanges = true; } } }); // 更新声明值 if (hasChanges) { decl.value = parsed.toString(); } // 删除不支持的属性 if (["filter"].includes(decl.prop)) { decl.remove(); return; } // 移除 undefined decl.value = decl.value.replaceAll(" undefined", ""); }, }; }, }, ], }, }, }; }, }; } /** * uvue class 转换插件 */ function transformPlugin(): Plugin { return { name: "vite-cool-uniappx-transform", enforce: "pre", async transform(code, id) { const { darkTextClass } = config.tailwind!; // 判断是否为 uvue 文件 if (id.endsWith(".uvue") || id.includes(".uvue?type=page")) { let modifiedCode = code; // 获取所有节点 const nodes = getNodes(code); // 遍历处理每个节点 nodes.forEach((node) => { let _node = node; // 兼容 标签 if (_node.startsWith("", ""); } // 为 text 节点添加暗黑模式文本颜色 if (!_node.includes(darkTextClass) && _node.startsWith("= 0) { if (_node[classIndex - 1] == ":") { classIndex = _node.lastIndexOf("class="); } } // 添加暗黑模式类名 if (classIndex >= 0) { _node = _node.substring(0, classIndex + 7) + `${darkTextClass} ` + _node.substring(classIndex + 7, _node.length); } else { _node = _node.substring(0, 5) + ` class="${darkTextClass}" ` + _node.substring(5, _node.length); } } // 获取所有类名 const classNames = getClassNames(_node); // 转换 Tailwind 类名为安全类名 classNames.forEach((name, index) => { if (isTailwindClass(name)) { const safeName = toSafeClass(name); _node = _node.replace(name, safeName); classNames[index] = safeName; } }); // 检查是否存在动态类名 const hasDynamicClass = _node.includes(":class="); // 如果没有动态类名,添加空的动态类名绑定 if (!hasDynamicClass) { _node = _node.slice(0, -1) + ` :class="{}"` + ">"; } // 获取暗黑模式类名 const darkClassNames = classNames.filter((name) => name.startsWith("dark-colon-"), ); // 生成暗黑模式类名的动态绑定 const darkClassContent = darkClassNames .map((name) => { _node = _node.replace(name, ""); return `'${name}': __isDark`; }) .join(","); // 获取所有 class 内容 const classContents = getClassContent(_node); // 处理对象形式的动态类名 const dynamicClassContent_1 = classContents.find( (content) => content.startsWith("{") && content.endsWith("}"), ); if (dynamicClassContent_1) { const v = dynamicClassContent_1[0] + (darkClassContent ? `${darkClassContent},` : "") + dynamicClassContent_1.substring(1); _node = _node.replace(dynamicClassContent_1, v); } // 处理数组形式的动态类名 const dynamicClassContent_2 = classContents.find( (content) => content.startsWith("[") && content.endsWith("]"), ); if (dynamicClassContent_2) { const v = dynamicClassContent_2[0] + `{${darkClassContent}},` + dynamicClassContent_2.substring(1); _node = _node.replace(dynamicClassContent_2, v); } // 更新节点内容 modifiedCode = modifiedCode.replace(node, _node); }); // 如果代码有修改 if (modifiedCode !== code) { // 添加暗黑模式依赖 if (modifiedCode.includes("__isDark")) { if (!modifiedCode.includes("