This commit is contained in:
icssoa 2025-05-21 23:43:19 +08:00 committed by icssoa
parent b9ee6dcacf
commit dc9bae188c
6 changed files with 390 additions and 349 deletions

View File

@ -1317,7 +1317,7 @@ if (typeof window !== 'undefined') {
* Vite 插件自动转换 .uvue 文件中的 Tailwind 类名为安全字符
* 并自动注入 rem rpx PostCSS 插件
*/
function tailwindTransformPlugin() {
function tailwindPlugin() {
return {
name: "vite-cool-uniappx-tailwind",
enforce: "pre",
@ -1364,18 +1364,32 @@ if (typeof window !== 'undefined') {
},
};
}
function codePlugin() {
return {
name: "vite-cool-uniappx-code",
transform(code, id) {
if (id.endsWith(".json")) {
return code.replace("new UTSJSONObject", "");
}
},
};
}
/**
* uniappX 入口自动注入 Tailwind 类名转换插件
* @param options 配置项
* @returns Vite 插件数组
*/
function uniappX() {
const plugins = [];
if (config.type == "uniapp-x") {
plugins.push(codePlugin());
if (config.tailwind.enable) {
return [tailwindTransformPlugin()];
plugins.push(tailwindPlugin());
}
}
return [];
return plugins;
}
function cool(options) {

View File

@ -0,0 +1,2 @@
import type { Plugin } from "vite";
export declare function codePlugin(): Plugin;

View File

@ -0,0 +1,6 @@
import type { Plugin } from "vite";
/**
* Vite .uvue Tailwind
* rem rpx PostCSS
*/
export declare function tailwindPlugin(): Plugin;

View File

@ -0,0 +1,12 @@
import type { Plugin } from "vite";
export function codePlugin() {
return {
name: "vite-cool-uniappx-code",
transform(code, id) {
if (id.endsWith(".json")) {
return code.replace("new UTSJSONObject", "");
}
},
} as Plugin;
}

View File

@ -1,348 +1,7 @@
// @ts-ignore
import valueParser from "postcss-value-parser";
import { config } from "../config";
import type { Plugin } from "vite";
import { Config } from "../../types";
/**
* Tailwind CSS
*
*/
const TAILWIND_SAFE_CHAR_MAP: Record<string, string> = {
"[": "-",
"]": "-",
"(": "-",
")": "-",
"{": "-",
"}": "-",
$: "-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<string, number> = {
"--tw-text-opacity": 1,
"--tw-bg-opacity": 1,
};
/**
*
* @param value
* @param isSelector truefalse
* @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<string>();
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
*/
function tailwindTransformPlugin() {
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(/<template>([\s\S]*?)<\/template>/);
if (!tplMatch?.[1]) return null;
let tpl = tplMatch[1];
const tplOrigin = tpl;
TAILWIND_CLASS_PREFIXES.forEach((prefix) => {
for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) {
const reg = new RegExp(`(${prefix}[^\\s'"]*?\\${char}[^\\s'"]*?)`, "g");
const matches = [...tpl.matchAll(reg)];
matches.forEach((m) => {
const raw = m[1];
const safe = raw.replace(new RegExp("\\" + char, "g"), rep);
if (process.env.NODE_ENV === "development") {
console.log(`类名转换: ${raw}${safe}`);
}
tpl = tpl.replace(raw, safe);
});
}
});
if (tpl !== tplOrigin) {
resultCode = resultCode.replace(tplMatch[0], `<template>${tpl}</template>`);
return {
code: resultCode,
map: { mappings: "" },
};
}
return null;
},
} as Plugin;
}
import { config } from "../config";
import { tailwindPlugin } from "./tailwind";
import { codePlugin } from "./code";
/**
* uniappX Tailwind
@ -350,11 +9,15 @@ function tailwindTransformPlugin() {
* @returns Vite
*/
export function uniappX() {
const plugins: Plugin[] = [];
if (config.type == "uniapp-x") {
plugins.push(codePlugin());
if (config.tailwind.enable) {
return [tailwindTransformPlugin()];
plugins.push(tailwindPlugin());
}
}
return [];
return plugins;
}

View File

@ -0,0 +1,344 @@
// @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<string, string> = {
"[": "-",
"]": "-",
"(": "-",
")": "-",
"{": "-",
"}": "-",
$: "-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<string, number> = {
"--tw-text-opacity": 1,
"--tw-bg-opacity": 1,
};
/**
*
* @param value
* @param isSelector truefalse
* @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<string>();
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(/<template>([\s\S]*?)<\/template>/);
if (!tplMatch?.[1]) return null;
let tpl = tplMatch[1];
const tplOrigin = tpl;
TAILWIND_CLASS_PREFIXES.forEach((prefix) => {
for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) {
const reg = new RegExp(`(${prefix}[^\\s'"]*?\\${char}[^\\s'"]*?)`, "g");
const matches = [...tpl.matchAll(reg)];
matches.forEach((m) => {
const raw = m[1];
const safe = raw.replace(new RegExp("\\" + char, "g"), rep);
if (process.env.NODE_ENV === "development") {
console.log(`类名转换: ${raw}${safe}`);
}
tpl = tpl.replace(raw, safe);
});
}
});
if (tpl !== tplOrigin) {
resultCode = resultCode.replace(tplMatch[0], `<template>${tpl}</template>`);
return {
code: resultCode,
map: { mappings: "" },
};
}
return null;
},
} as Plugin;
}