// @ts-ignore import valueParser from "postcss-value-parser"; import { config } from "../config"; import type { Plugin } from "vite"; /** * Tailwind CSS 特殊字符映射表 * 用于将类名中的特殊字符转换为安全字符,避免编译或运行时冲突 */ const TAILWIND_SAFE_CHAR_MAP: Record = { "[": "-", "]": "-", "(": "-", ")": "-", "{": "-", "}": "-", $: "-v-", "#": "-h-", "!": "-i-", "/": "-s-", ":": "-c-", ",": "-2c-", }; /** * Tailwind CSS 常用类名前缀集合 * 按功能分类,便于维护和扩展 */ const TAILWIND_CLASS_PREFIXES: string[] = [ // 间距 "p-", "px-", "py-", "pt-", "pr-", "pb-", "pl-", "m-", "mx-", "my-", "mt-", "mr-", "mb-", "ml-", "gap-", "gap-x-", "gap-y-", "space-x-", "space-y-", "inset-", "top-", "right-", "bottom-", "left-", // 尺寸 "w-", "h-", "min-w-", "min-h-", "max-w-", "max-h-", // 排版 "text-", "font-", "leading-", "tracking-", "indent-", // 边框 "border-", "border-t-", "border-r-", "border-b-", "border-l-", "rounded-", "rounded-t-", "rounded-r-", "rounded-b-", "rounded-l-", "rounded-tl-", "rounded-tr-", "rounded-br-", "rounded-bl-", // 效果 "shadow-", "blur-", "brightness-", "contrast-", "drop-shadow-", "grayscale-", "hue-rotate-", "invert-", "saturate-", "sepia-", "backdrop-blur-", "backdrop-brightness-", "backdrop-contrast-", "backdrop-grayscale-", "backdrop-hue-rotate-", "backdrop-invert-", "backdrop-opacity-", "backdrop-saturate-", "backdrop-sepia-", // 动画 "transition-", "duration-", "delay-", "animate-", // 变换 "translate-x-", "translate-y-", "rotate-", "scale-", "scale-x-", "scale-y-", "skew-x-", "skew-y-", "origin-", // 布局 "columns-", "break-after-", "break-before-", "break-inside-", // Flexbox 和 Grid "basis-", "grow-", "shrink-", "grid-cols-", "grid-rows-", "col-span-", "row-span-", "col-start-", "col-end-", "row-start-", "row-end-", // SVG "stroke-", "stroke-w-", "fill-", ]; /** * Tailwind CSS 颜色变量映射 * 用于移除不需要的 CSS 变量声明 */ const TAILWIND_COLOR_VARS: Record = { "--tw-text-opacity": 1, "--tw-bg-opacity": 1, }; /** * 转换类名中的特殊字符为安全字符 * @param value 原始类名或值 * @param isSelector 是否为选择器(true)或普通值(false) * @returns 转换后的安全字符串 */ function toSafeTailwindClass(value: string, isSelector: boolean = false): string { // 处理任意值语法(如 w-[100px]) const arbitrary = value.match(/^(.+?)-\[(.*?)\]$/); if (arbitrary) { if (isSelector) return value; const [, prefix, content] = arbitrary; const safePrefix = toSafeTailwindClass(prefix, isSelector); const safeContent = content.replace(/[^\d.\w]/g, "-"); return `${safePrefix}-${safeContent}`; } let safeValue = value; // 移除转义字符 if (safeValue.includes("\\")) { safeValue = safeValue.replace(/\\/g, ""); } // 替换特殊字符 for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) { const reg = new RegExp("\\" + char, "g"); if (reg.test(safeValue)) { safeValue = safeValue.replace(reg, rep); } } return safeValue; } /** * 将现代 rgb 格式(如 rgb(234 179 8 / 0.1))转换为标准 rgba 格式 * @param value rgb 字符串 * @returns 标准 rgba 字符串 */ function rgbToRgba(value: string): string { const match = value.match(/rgb\(([\d\s]+)\/\s*([\d.]+)\)/); if (match) { const [, rgb, alpha] = match; const [r, g, b] = rgb.split(/\s+/); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } return value; } /** * PostCSS 插件:将 rem 单位转换为 rpx,并处理 Tailwind 特殊字符 * @param options 配置项 * @returns PostCSS 插件对象 */ function postcssRemToRpx() { return { postcssPlugin: "vite-cool-uniappx-remToRpx", prepare() { const handledSelectors = new Set(); const { remUnit = 16, remPrecision = 6, rpxRatio = 2 } = config.tailwind; const factor = remUnit * rpxRatio; return { Rule(rule: any) { const sel = rule.selector; if (handledSelectors.has(sel)) return; const safeSel = toSafeTailwindClass(sel, true); if (safeSel !== sel) { rule.selector = safeSel; handledSelectors.add(sel); } }, Declaration(decl: any) { if (decl.value.includes("/* no-rem */")) return; if (TAILWIND_COLOR_VARS[decl.prop]) { decl.remove(); return; } if (decl.value.includes("rgb(") && decl.value.includes("/")) { decl.value = rgbToRgba(decl.value); } if (decl.value.includes("rpx") && decl.parent.selector.includes("text-")) { decl.prop = "font-size"; } const parsed = valueParser(decl.value); let changed = false; parsed.walk((node: any) => { if (node.type === "word") { // rem 转 rpx const unit = valueParser.unit(node.value); if (unit?.unit === "rem") { const num = unit.number; const precision = (num.split(".")[1] || "").length; const rpxVal = (parseFloat(num) * factor) .toFixed(precision || remPrecision) .replace(/\.?0+$/, ""); node.value = `${rpxVal}rpx`; changed = true; } // 特殊字符处理 if (node.value.includes(".") || /[[\]()#!/:,]/.test(node.value)) { const safe = toSafeTailwindClass(node.value, true); if (safe !== node.value) { node.value = safe; changed = true; } } } // 处理 var(--tw-xxx) if (node.type === "function" && node.value === "var") { if (node.nodes.length > 0 && node.nodes[0].value.startsWith("--tw-")) { node.type = "word"; node.value = TAILWIND_COLOR_VARS[node.nodes[0].value]; changed = true; } } }); if (changed) { decl.value = parsed.toString(); } }, }; }, }; } postcssRemToRpx.postcss = true; /** * Vite 插件:自动转换 .uvue 文件中的 Tailwind 类名为安全字符 * 并自动注入 rem 转 rpx 的 PostCSS 插件 */ export function tailwindPlugin() { return { name: "vite-cool-uniappx-tailwind", enforce: "pre", config() { return { css: { postcss: { plugins: [postcssRemToRpx()], }, }, }; }, transform(code, id) { if (!id.includes(".uvue")) return null; let resultCode = code; const tplMatch = resultCode.match(/