This commit is contained in:
icssoa 2025-05-26 02:08:55 +08:00
parent 6fae7235cc
commit d2c8f554e9
14 changed files with 2973 additions and 615 deletions

View File

@ -1,2 +1,33 @@
import type { Config } from "../types"; export declare const config: {
export declare const config: Config.Data; type: string;
reqUrl: string;
demo: boolean;
nameTag: boolean;
eps: {
enable: boolean;
api: string;
dist: string;
mapping: ({
custom: ({ propertyName, type }: {
propertyName: string;
type: string;
}) => null;
type?: undefined;
test?: undefined;
} | {
type: string;
test: string[];
custom?: undefined;
})[];
};
svg: {
skipNames: string[];
};
tailwind: {
enable: boolean;
remUnit: number;
remPrecision: number;
rpxRatio: number;
darkTextClass: string;
};
};

View File

@ -7,6 +7,7 @@
const config = { const config = {
type: "admin", type: "admin",
reqUrl: "", reqUrl: "",
nameTag: true,
eps: { eps: {
enable: true, enable: true,
api: "", api: "",
@ -46,9 +47,10 @@
}, },
tailwind: { tailwind: {
enable: true, enable: true,
remUnit: 16, remUnit: 14,
remPrecision: 6, remPrecision: 6,
rpxRatio: 2, rpxRatio: 2,
darkTextClass: "dark:text-surface-50",
}, },
}; };
@ -1048,332 +1050,550 @@ if (typeof window !== 'undefined') {
}; };
} }
// @ts-ignore
/** /**
* Tailwind CSS 特殊字符映射表 * 特殊字符映射表
* 用于将类名中的特殊字符转换为安全字符避免编译或运行时冲突
*/ */
const TAILWIND_SAFE_CHAR_MAP = { const SAFE_CHAR_MAP = {
"[": "-", "[": "-bracket-start-",
"]": "-", "]": "-bracket-end-",
"(": "-", "(": "-paren-start-",
")": "-", ")": "-paren-end-",
"{": "-", "{": "-brace-start-",
"}": "-", "}": "-brace-end-",
$: "-v-", $: "-dollar-",
"#": "-h-", "#": "-hash-",
"!": "-i-", "!": "-important-",
"/": "-s-", "/": "-slash-",
":": "-c-", ":": "-colon-",
",": "-2c-", };
/**
* 获取动态类名
*/
const getDynamicClassNames = (value) => {
const names = new Set();
// 匹配数组中的字符串元素(如 'text-center'
const arrayRegex = /['"](.*?)['"]/g;
let arrayMatch;
while ((arrayMatch = arrayRegex.exec(value)) !== null) {
arrayMatch[1].trim() && names.add(arrayMatch[1]);
}
// 匹配对象键(如 { 'text-a': 1 }
const objKeyRegex = /[{,]\s*['"](.*?)['"]\s*:/g;
let objKeyMatch;
while ((objKeyMatch = objKeyRegex.exec(value)) !== null) {
objKeyMatch[1].trim() && names.add(objKeyMatch[1]);
}
// 匹配三元表达式中的字符串(如 'dark' 和 'light'
const ternaryRegex = /(\?|:)\s*['"](.*?)['"]/g;
let ternaryMatch;
while ((ternaryMatch = ternaryRegex.exec(value)) !== null) {
ternaryMatch[2].trim() && names.add(ternaryMatch[2]);
}
return Array.from(names);
}; };
/** /**
* Tailwind CSS 常用类名前缀集合 * 获取类名
* 按功能分类便于维护和扩展
*/ */
const TAILWIND_CLASS_PREFIXES = [ function getClassNames(html) {
// 间距 const classRegex = /(?:class|:class)\s*=\s*(["'])([\s\S]*?)\1/gi;
"p-", const classNames = new Set();
"px-", let match;
"py-", while ((match = classRegex.exec(html)) !== null) {
"pt-", const isStaticClass = match[0].startsWith("class");
"pr-", const value = match[2].trim();
"pb-", if (isStaticClass) {
"pl-", // 处理静态 class
"m-", value.split(/\s+/).forEach((name) => name && classNames.add(name));
"mx-", }
"my-", else {
"mt-", // 处理动态 :class
"mr-", getDynamicClassNames(value).forEach((name) => classNames.add(name));
"mb-", }
"ml-", }
"gap-", return Array.from(classNames);
"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 颜色变量映射 * 获取 class 内容
* 用于移除不需要的 CSS 变量声明
*/ */
const TAILWIND_COLOR_VARS = { function getClassContent(html) {
"--tw-text-opacity": 1, const regex = /(?:class|:class)\s*=\s*(['"])([\s\S]*?)\1/g;
"--tw-bg-opacity": 1, const texts = [];
let match;
while ((match = regex.exec(html)) !== null) {
texts.push(match[2]);
}
return texts;
}
/**
* 获取节点
*/
function getNodes(code) {
const nodes = [];
const templateMatch = /<template>([\s\S]*?)<\/template>/g.exec(code);
if (!templateMatch) {
return nodes;
}
const templateContent = templateMatch[1];
const regex = /<([^>]+)>/g;
let match;
while ((match = regex.exec(templateContent)) !== null) {
if (!match[1].startsWith("/")) {
nodes.push(match[1]);
}
}
return nodes.map((e) => `<${e}>`);
}
/**
* 添加 script 标签内容
*/
function addScriptContent(code, content) {
const scriptMatch = /<script\b[^>]*>([\s\S]*?)<\/script>/g.exec(code);
if (!scriptMatch) {
return code;
}
const scriptContent = scriptMatch[1];
const scriptStartIndex = scriptMatch.index + scriptMatch[0].indexOf(">") + 1;
const scriptEndIndex = scriptStartIndex + scriptContent.length;
return (code.substring(0, scriptStartIndex) +
"\n" +
content +
"\n" +
scriptContent.trim() +
code.substring(scriptEndIndex));
}
/**
* 判断是否为 Tailwind 类名
*/
function isTailwindClass(className) {
const prefixes = [
// 布局
"container",
"flex",
"grid",
"block",
"inline",
"hidden",
"visible",
// 间距
"p-",
"px-",
"py-",
"pt-",
"pr-",
"pb-",
"pl-",
"m-",
"mx-",
"my-",
"mt-",
"mr-",
"mb-",
"ml-",
"space-",
"gap-",
// 尺寸
"w-",
"h-",
"min-w-",
"max-w-",
"min-h-",
"max-h-",
// 颜色
"bg-",
"text-",
"border-",
"ring-",
"shadow-",
// 边框
"border",
"rounded",
"ring",
// 字体
"font-",
"text-",
"leading-",
"tracking-",
"antialiased",
// 定位
"absolute",
"relative",
"fixed",
"sticky",
"static",
"top-",
"right-",
"bottom-",
"left-",
"inset-",
"z-",
// 变换
"transform",
"translate-",
"rotate-",
"scale-",
"skew-",
// 过渡
"transition",
"duration-",
"ease-",
"delay-",
// 交互
"cursor-",
"select-",
"pointer-events-",
// 溢出
"overflow-",
"truncate",
// 滚动
"scroll-",
// 伪类和响应式
"hover:",
"focus:",
"active:",
"disabled:",
"group-hover:",
];
const statePrefixes = ["dark:", "light:", "sm:", "md:", "lg:", "xl:", "2xl:"];
for (const prefix of prefixes) {
if (className.startsWith(prefix)) {
return true;
}
for (const statePrefix of statePrefixes) {
if (className.startsWith(statePrefix + prefix)) {
return true;
}
}
}
return false;
}
// @ts-ignore
/**
* Tailwind 默认值
*/
const TW_DEFAULT_VALUES = {
"--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,
}; };
/** /**
* 转换类名中的特殊字符为安全字符 * 转换类名中的特殊字符为安全字符
* @param value 原始类名或值
* @param isSelector 是否为选择器true或普通值false
* @returns 转换后的安全字符串
*/ */
function toSafeTailwindClass(value, isSelector = false) { function toSafeClass(className) {
// 处理任意值语法(如 w-[100px] if (className.includes(":host")) {
const arbitrary = value.match(/^(.+?)-\[(.*?)\]$/); return className;
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; let safeClassName = className;
// 移除转义字符 // 移除转义字符
if (safeValue.includes("\\")) { if (safeClassName.includes("\\")) {
safeValue = safeValue.replace(/\\/g, ""); safeClassName = safeClassName.replace(/\\/g, "");
} }
// 替换特殊字符 // 处理暗黑模式
for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) { if (safeClassName.includes(":is")) {
const reg = new RegExp("\\" + char, "g"); if (safeClassName.includes(":is(.dark *)")) {
if (reg.test(safeValue)) { safeClassName = safeClassName.replace(/:is\(.dark \*\)/g, "");
safeValue = safeValue.replace(reg, rep); if (safeClassName.startsWith(".dark:")) {
const className = safeClassName.replace(/^\.dark:/, ".dark:");
safeClassName = `${className}`;
}
} }
} }
return safeValue; // 替换特殊字符
} for (const [char, replacement] of Object.entries(SAFE_CHAR_MAP)) {
/** const regex = new RegExp("\\" + char, "g");
* 将现代 rgb 格式 rgb(234 179 8 / 0.1)转换为标准 rgba 格式 if (regex.test(safeClassName)) {
* @param value rgb 字符串 safeClassName = safeClassName.replace(regex, replacement);
* @returns 标准 rgba 字符串 }
*/
function rgbToRgba(value) {
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; return safeClassName;
} }
/** /**
* PostCSS 插件 rem 单位转换为 rpx并处理 Tailwind 特殊字符 * 转换 RGB RGBA 格式
* @param options 配置项
* @returns PostCSS 插件对象
*/ */
function postcssRemToRpx() { function rgbToRgba(rgbValue) {
return { const match = rgbValue.match(/rgb\(([\d\s]+)\/\s*([\d.]+)\)/);
postcssPlugin: "vite-cool-uniappx-remToRpx", if (!match)
prepare() { return rgbValue;
const handledSelectors = new Set(); const [, rgb, alpha] = match;
const { remUnit = 16, remPrecision = 6, rpxRatio = 2 } = config.tailwind; const [r, g, b] = rgb.split(/\s+/);
const factor = remUnit * rpxRatio; return `rgba(${r}, ${g}, ${b}, ${alpha})`;
return { }
Rule(rule) { function remToRpx(remValue) {
const sel = rule.selector; const { remUnit = 14, remPrecision = 6, rpxRatio = 2 } = config.tailwind;
if (handledSelectors.has(sel)) const conversionFactor = remUnit * rpxRatio;
return; const precision = (remValue.split(".")[1] || "").length;
const safeSel = toSafeTailwindClass(sel, true); const rpxValue = (parseFloat(remValue) * conversionFactor)
if (safeSel !== sel) { .toFixed(precision || remPrecision)
rule.selector = safeSel; .replace(/\.?0+$/, "");
handledSelectors.add(sel); return `${rpxValue}rpx`;
}
},
Declaration(decl) {
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) => {
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 类名为安全字符 * PostCSS 插件
* 并自动注入 rem rpx PostCSS 插件 * 处理类名和单位转换
*/ */
function tailwindPlugin() { function postcssPlugin() {
return { return {
name: "vite-cool-uniappx-tailwind", name: "vite-cool-uniappx-postcss",
enforce: "pre", enforce: "pre",
config() { config() {
return { return {
css: { css: {
postcss: { postcss: {
plugins: [postcssRemToRpx()], plugins: [
{
postcssPlugin: "vite-cool-uniappx-class-mapping",
prepare() {
// 存储 Tailwind 颜色值
const colorValues = {
...TW_DEFAULT_VALUES,
};
return {
// 处理选择器规则
Rule(rule) {
// 转换选择器为安全的类名格式
rule.selector = toSafeClass(rule.selector.replace(/\\/g, ""));
},
// 处理声明规则
Declaration(decl) {
// 跳过包含 no-rem 注释的声明
if (decl.value.includes("/* no-rem */"))
return;
// 处理 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) => {
// 处理单位转换(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();
}
},
};
},
},
],
}, },
}, },
}; };
}, },
transform(code, id) { };
if (!id.includes(".uvue")) }
return null; /**
let resultCode = code; * uvue class 转换插件
const tplMatch = resultCode.match(/<template>([\s\S]*?)<\/template>/); */
if (!tplMatch?.[1]) function transformPlugin() {
return null; return {
let tpl = tplMatch[1]; name: "vite-cool-uniappx-transform",
const tplOrigin = tpl; enforce: "pre",
TAILWIND_CLASS_PREFIXES.forEach((prefix) => { async transform(code, id) {
for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) { const { darkTextClass } = config.tailwind;
const reg = new RegExp(`(${prefix}[^\\s'"]*?\\${char}[^\\s'"]*?)`, "g"); // 判断是否为 uvue 文件
const matches = [...tpl.matchAll(reg)]; if (id.endsWith(".uvue") || id.includes(".uvue?type=page")) {
matches.forEach((m) => { let modifiedCode = code;
const raw = m[1]; // 获取所有节点
const safe = raw.replace(new RegExp("\\" + char, "g"), rep); const nodes = getNodes(code);
if (process.env.NODE_ENV === "development") { // 遍历处理每个节点
console.log(`类名转换: ${raw}${safe}`); nodes.forEach((node) => {
let _node = node;
// 为 text 节点添加暗黑模式文本颜色
if (!_node.includes(darkTextClass) && _node.startsWith("<text")) {
let classIndex = _node.indexOf("class=");
// 处理动态 class
if (classIndex >= 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;
} }
tpl = tpl.replace(raw, safe);
}); });
// 检查是否存在动态类名
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("<script")) {
modifiedCode += '<script lang="ts" setup></script>';
}
modifiedCode = addScriptContent(modifiedCode, "\nimport { isDark as __isDark } from '@/cool';");
}
// 清理空的类名绑定
modifiedCode = modifiedCode
.replaceAll(':class="{}"', "")
.replaceAll('class=""', "")
.replaceAll('class=" "', "");
// console.log(modifiedCode);
return {
code: modifiedCode,
map: { mappings: "" },
};
} }
}); return null;
if (tpl !== tplOrigin) { }
resultCode = resultCode.replace(tplMatch[0], `<template>${tpl}</template>`); else {
return { return null;
code: resultCode,
map: { mappings: "" },
};
} }
return null;
}, },
}; };
} }
/**
* Tailwind 类名转换插件
*/
function tailwindPlugin() {
return [postcssPlugin(), transformPlugin()];
}
function codePlugin() { function codePlugin() {
return { return [
name: "vite-cool-uniappx-code", {
transform(code, id) { name: "vite-cool-uniappx-code-pre",
if (id.endsWith(".json")) { enforce: "pre",
return code.replace("new UTSJSONObject", ""); async transform(code, id) {
} if (id.includes("/cool/virtual.ts")) {
const ctx = await createCtx();
ctx["SAFE_CHAR_MAP"] = [];
for (const i in SAFE_CHAR_MAP) {
ctx["SAFE_CHAR_MAP"].push([i, SAFE_CHAR_MAP[i]]);
}
const theme = await readFile(rootDir("theme.json"), true);
ctx["theme"] = theme;
code = code.replace("export const ctx = {}", `export const ctx = ${JSON.stringify(ctx, null, 4)}`);
return {
code,
map: { mappings: "" },
};
}
if (id.endsWith(".json")) {
const d = JSON.parse(code);
for (let i in d) {
let k = i;
for (let j in SAFE_CHAR_MAP) {
k = k.replaceAll(j, SAFE_CHAR_MAP[j]);
}
d[k] = d[i];
delete d[i];
}
return {
code: JSON.stringify(d),
map: { mappings: "" },
};
}
},
}, },
}; {
name: "vite-cool-uniappx-code",
transform(code, id) {
if (id.endsWith(".json")) {
return {
code: code.replace("new UTSJSONObject", ""),
map: { mappings: "" },
};
}
},
},
];
} }
/** /**
@ -1384,9 +1604,9 @@ if (typeof window !== 'undefined') {
function uniappX() { function uniappX() {
const plugins = []; const plugins = [];
if (config.type == "uniapp-x") { if (config.type == "uniapp-x") {
plugins.push(codePlugin()); plugins.push(...codePlugin());
if (config.tailwind.enable) { if (config.tailwind.enable) {
plugins.push(tailwindPlugin()); plugins.push(...tailwindPlugin());
} }
} }
return plugins; return plugins;

View File

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

View File

@ -0,0 +1,4 @@
/**
*
*/
export declare const SAFE_CHAR_MAP: Record<string, string>;

View File

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

View File

@ -0,0 +1,24 @@
/**
*
*/
export declare const getDynamicClassNames: (value: string) => string[];
/**
*
*/
export declare function getClassNames(html: string): string[];
/**
* class
*/
export declare function getClassContent(html: string): string[];
/**
*
*/
export declare function getNodes(code: string): string[];
/**
* script
*/
export declare function addScriptContent(code: string, content: string): string;
/**
* Tailwind
*/
export declare function isTailwindClass(className: string): boolean;

View File

@ -1,9 +1,8 @@
import type { Config } from "../types"; export const config = {
export const config: Config.Data = {
type: "admin", type: "admin",
reqUrl: "", reqUrl: "",
demo: false, demo: false,
nameTag: true,
eps: { eps: {
enable: true, enable: true,
api: "", api: "",
@ -11,7 +10,7 @@ export const config: Config.Data = {
mapping: [ mapping: [
{ {
// 自定义匹配 // 自定义匹配
custom: ({ propertyName, type }) => { custom: ({ propertyName, type }: { propertyName: string; type: string }) => {
// 如果没有返回null或者不返回则继续遍历其他匹配规则 // 如果没有返回null或者不返回则继续遍历其他匹配规则
return null; return null;
}, },
@ -43,8 +42,9 @@ export const config: Config.Data = {
}, },
tailwind: { tailwind: {
enable: true, enable: true,
remUnit: 16, remUnit: 14,
remPrecision: 6, remPrecision: 6,
rpxRatio: 2, rpxRatio: 2,
darkTextClass: "dark:text-surface-50",
}, },
}; };

View File

@ -1,12 +1,67 @@
import type { Plugin } from "vite"; import type { Plugin } from "vite";
import { SAFE_CHAR_MAP } from "./config";
import { createCtx } from "../ctx";
import { readFile, rootDir } from "../utils";
export function codePlugin() { export function codePlugin(): Plugin[] {
return { return [
name: "vite-cool-uniappx-code", {
transform(code, id) { name: "vite-cool-uniappx-code-pre",
if (id.endsWith(".json")) { enforce: "pre",
return code.replace("new UTSJSONObject", ""); async transform(code, id) {
} if (id.includes("/cool/virtual.ts")) {
const ctx = await createCtx();
ctx["SAFE_CHAR_MAP"] = [];
for (const i in SAFE_CHAR_MAP) {
ctx["SAFE_CHAR_MAP"].push([i, SAFE_CHAR_MAP[i]]);
}
const theme = await readFile(rootDir("theme.json"), true);
ctx["theme"] = theme;
code = code.replace(
"export const ctx = {}",
`export const ctx = ${JSON.stringify(ctx, null, 4)}`,
);
return {
code,
map: { mappings: "" },
};
}
if (id.endsWith(".json")) {
const d = JSON.parse(code);
for (let i in d) {
let k = i;
for (let j in SAFE_CHAR_MAP) {
k = k.replaceAll(j, SAFE_CHAR_MAP[j]);
}
d[k] = d[i];
delete d[i];
}
return {
code: JSON.stringify(d),
map: { mappings: "" },
};
}
},
}, },
} as Plugin; {
name: "vite-cool-uniappx-code",
transform(code, id) {
if (id.endsWith(".json")) {
return {
code: code.replace("new UTSJSONObject", ""),
map: { mappings: "" },
};
}
},
},
];
} }

View File

@ -0,0 +1,16 @@
/**
*
*/
export const SAFE_CHAR_MAP: Record<string, string> = {
"[": "-bracket-start-",
"]": "-bracket-end-",
"(": "-paren-start-",
")": "-paren-end-",
"{": "-brace-start-",
"}": "-brace-end-",
$: "-dollar-",
"#": "-hash-",
"!": "-important-",
"/": "-slash-",
":": "-colon-",
};

View File

@ -12,10 +12,10 @@ export function uniappX() {
const plugins: Plugin[] = []; const plugins: Plugin[] = [];
if (config.type == "uniapp-x") { if (config.type == "uniapp-x") {
plugins.push(codePlugin()); plugins.push(...codePlugin());
if (config.tailwind.enable) { if (config.tailwind.enable) {
plugins.push(tailwindPlugin()); plugins.push(...tailwindPlugin());
} }
} }

View File

@ -2,343 +2,353 @@
import valueParser from "postcss-value-parser"; import valueParser from "postcss-value-parser";
import { config } from "../config"; import { config } from "../config";
import type { Plugin } from "vite"; import type { Plugin } from "vite";
import { SAFE_CHAR_MAP } from "./config";
import {
addScriptContent,
getClassContent,
getClassNames,
getNodes,
isTailwindClass,
} from "./utils";
/** /**
* Tailwind CSS * Tailwind
*
*/ */
const TAILWIND_SAFE_CHAR_MAP: Record<string, string> = { const TW_DEFAULT_VALUES: Record<string, string | number> = {
"[": "-", "--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,
$: "-v-", "--tw-skew-y": 0,
"#": "-h-", "--tw-scale-x": 1,
"!": "-i-", "--tw-scale-y": 1,
"/": "-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 { function toSafeClass(className: string): string {
// 处理任意值语法(如 w-[100px] if (className.includes(":host")) {
const arbitrary = value.match(/^(.+?)-\[(.*?)\]$/); return className;
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; let safeClassName = className;
// 移除转义字符 // 移除转义字符
if (safeValue.includes("\\")) { if (safeClassName.includes("\\")) {
safeValue = safeValue.replace(/\\/g, ""); safeClassName = safeClassName.replace(/\\/g, "");
} }
// 替换特殊字符 // 处理暗黑模式
for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) { if (safeClassName.includes(":is")) {
const reg = new RegExp("\\" + char, "g"); if (safeClassName.includes(":is(.dark *)")) {
if (reg.test(safeValue)) { safeClassName = safeClassName.replace(/:is\(.dark \*\)/g, "");
safeValue = safeValue.replace(reg, rep); if (safeClassName.startsWith(".dark:")) {
const className = safeClassName.replace(/^\.dark:/, ".dark:");
safeClassName = `${className}`;
}
} }
} }
return safeValue; // 替换特殊字符
} for (const [char, replacement] of Object.entries(SAFE_CHAR_MAP)) {
const regex = new RegExp("\\" + char, "g");
/** if (regex.test(safeClassName)) {
* rgb rgb(234 179 8 / 0.1) rgba safeClassName = safeClassName.replace(regex, replacement);
* @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;
return safeClassName;
} }
/** /**
* PostCSS rem rpx Tailwind * RGB RGBA
* @param options
* @returns PostCSS
*/ */
function postcssRemToRpx() { function rgbToRgba(rgbValue: string): string {
return { const match = rgbValue.match(/rgb\(([\d\s]+)\/\s*([\d.]+)\)/);
postcssPlugin: "vite-cool-uniappx-remToRpx", if (!match) return rgbValue;
prepare() {
const handledSelectors = new Set<string>();
const { remUnit = 16, remPrecision = 6, rpxRatio = 2 } = config.tailwind;
const factor = remUnit * rpxRatio;
return { const [, rgb, alpha] = match;
Rule(rule: any) { const [r, g, b] = rgb.split(/\s+/);
const sel = rule.selector; return `rgba(${r}, ${g}, ${b}, ${alpha})`;
if (handledSelectors.has(sel)) return; }
const safeSel = toSafeTailwindClass(sel, true);
if (safeSel !== sel) { function remToRpx(remValue: string): string {
rule.selector = safeSel; const { remUnit = 14, remPrecision = 6, rpxRatio = 2 } = config.tailwind!;
handledSelectors.add(sel); const conversionFactor = remUnit * rpxRatio;
}
}, const precision = (remValue.split(".")[1] || "").length;
Declaration(decl: any) { const rpxValue = (parseFloat(remValue) * conversionFactor)
if (decl.value.includes("/* no-rem */")) return; .toFixed(precision || remPrecision)
if (TAILWIND_COLOR_VARS[decl.prop]) { .replace(/\.?0+$/, "");
decl.remove();
return; return `${rpxValue}rpx`;
}
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 * PostCSS
* rem rpx PostCSS *
*/ */
export function tailwindPlugin() { function postcssPlugin(): Plugin {
return { return {
name: "vite-cool-uniappx-tailwind", name: "vite-cool-uniappx-postcss",
enforce: "pre", enforce: "pre",
config() { config() {
return { return {
css: { css: {
postcss: { postcss: {
plugins: [postcssRemToRpx()], plugins: [
{
postcssPlugin: "vite-cool-uniappx-class-mapping",
prepare() {
// 存储 Tailwind 颜色值
const colorValues = {
...TW_DEFAULT_VALUES,
};
return {
// 处理选择器规则
Rule(rule: any) {
// 转换选择器为安全的类名格式
rule.selector = toSafeClass(
rule.selector.replace(/\\/g, ""),
);
},
// 处理声明规则
Declaration(decl: any) {
// 跳过包含 no-rem 注释的声明
if (decl.value.includes("/* no-rem */")) return;
// 处理 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();
}
},
};
},
},
],
}, },
}, },
}; };
}, },
};
transform(code, id) { }
if (!id.includes(".uvue")) return null;
/**
let resultCode = code; * uvue class
const tplMatch = resultCode.match(/<template>([\s\S]*?)<\/template>/); */
if (!tplMatch?.[1]) return null; function transformPlugin(): Plugin {
return {
let tpl = tplMatch[1]; name: "vite-cool-uniappx-transform",
const tplOrigin = tpl; enforce: "pre",
TAILWIND_CLASS_PREFIXES.forEach((prefix) => { async transform(code, id) {
for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) { const { darkTextClass } = config.tailwind!;
const reg = new RegExp(`(${prefix}[^\\s'"]*?\\${char}[^\\s'"]*?)`, "g");
const matches = [...tpl.matchAll(reg)]; // 判断是否为 uvue 文件
matches.forEach((m) => { if (id.endsWith(".uvue") || id.includes(".uvue?type=page")) {
const raw = m[1]; let modifiedCode = code;
const safe = raw.replace(new RegExp("\\" + char, "g"), rep);
if (process.env.NODE_ENV === "development") { // 获取所有节点
console.log(`类名转换: ${raw}${safe}`); const nodes = getNodes(code);
}
tpl = tpl.replace(raw, safe); // 遍历处理每个节点
}); nodes.forEach((node) => {
} let _node = node;
});
// 为 text 节点添加暗黑模式文本颜色
if (tpl !== tplOrigin) { if (!_node.includes(darkTextClass) && _node.startsWith("<text")) {
resultCode = resultCode.replace(tplMatch[0], `<template>${tpl}</template>`); let classIndex = _node.indexOf("class=");
return {
code: resultCode, // 处理动态 class
map: { mappings: "" }, if (classIndex >= 0) {
}; if (_node[classIndex - 1] == ":") {
} classIndex = _node.lastIndexOf("class=");
return null; }
}, }
} as Plugin;
// 添加暗黑模式类名
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("<script")) {
modifiedCode += '<script lang="ts" setup></script>';
}
modifiedCode = addScriptContent(
modifiedCode,
"\nimport { isDark as __isDark } from '@/cool';",
);
}
// 清理空的类名绑定
modifiedCode = modifiedCode
.replaceAll(':class="{}"', "")
.replaceAll('class=""', "")
.replaceAll('class=" "', "");
// console.log(modifiedCode);
return {
code: modifiedCode,
map: { mappings: "" },
};
}
return null;
} else {
return null;
}
},
};
}
/**
* Tailwind
*/
export function tailwindPlugin() {
return [postcssPlugin(), transformPlugin()];
} }

View File

@ -0,0 +1,238 @@
/**
*
*/
export const getDynamicClassNames = (value: string): string[] => {
const names = new Set<string>();
// 匹配数组中的字符串元素(如 'text-center'
const arrayRegex = /['"](.*?)['"]/g;
let arrayMatch;
while ((arrayMatch = arrayRegex.exec(value)) !== null) {
arrayMatch[1].trim() && names.add(arrayMatch[1]);
}
// 匹配对象键(如 { 'text-a': 1 }
const objKeyRegex = /[{,]\s*['"](.*?)['"]\s*:/g;
let objKeyMatch;
while ((objKeyMatch = objKeyRegex.exec(value)) !== null) {
objKeyMatch[1].trim() && names.add(objKeyMatch[1]);
}
// 匹配三元表达式中的字符串(如 'dark' 和 'light'
const ternaryRegex = /(\?|:)\s*['"](.*?)['"]/g;
let ternaryMatch;
while ((ternaryMatch = ternaryRegex.exec(value)) !== null) {
ternaryMatch[2].trim() && names.add(ternaryMatch[2]);
}
return Array.from(names);
};
/**
*
*/
export function getClassNames(html: string): string[] {
const classRegex = /(?:class|:class)\s*=\s*(["'])([\s\S]*?)\1/gi;
const classNames = new Set<string>();
let match;
while ((match = classRegex.exec(html)) !== null) {
const isStaticClass = match[0].startsWith("class");
const value = match[2].trim();
if (isStaticClass) {
// 处理静态 class
value.split(/\s+/).forEach((name) => name && classNames.add(name));
} else {
// 处理动态 :class
getDynamicClassNames(value).forEach((name) => classNames.add(name));
}
}
return Array.from(classNames);
}
/**
* class
*/
export function getClassContent(html: string) {
const regex = /(?:class|:class)\s*=\s*(['"])([\s\S]*?)\1/g;
const texts: string[] = [];
let match;
while ((match = regex.exec(html)) !== null) {
texts.push(match[2]);
}
return texts;
}
/**
*
*/
export function getNodes(code: string) {
const nodes: string[] = [];
const templateMatch = /<template>([\s\S]*?)<\/template>/g.exec(code);
if (!templateMatch) {
return nodes;
}
const templateContent = templateMatch[1];
const regex = /<([^>]+)>/g;
let match;
while ((match = regex.exec(templateContent)) !== null) {
if (!match[1].startsWith("/")) {
nodes.push(match[1]);
}
}
return nodes.map((e) => `<${e}>`);
}
/**
* script
*/
export function addScriptContent(code: string, content: string) {
const scriptMatch = /<script\b[^>]*>([\s\S]*?)<\/script>/g.exec(code);
if (!scriptMatch) {
return code;
}
const scriptContent = scriptMatch[1];
const scriptStartIndex = scriptMatch.index + scriptMatch[0].indexOf(">") + 1;
const scriptEndIndex = scriptStartIndex + scriptContent.length;
return (
code.substring(0, scriptStartIndex) +
"\n" +
content +
"\n" +
scriptContent.trim() +
code.substring(scriptEndIndex)
);
}
/**
* Tailwind
*/
export function isTailwindClass(className: string): boolean {
const prefixes = [
// 布局
"container",
"flex",
"grid",
"block",
"inline",
"hidden",
"visible",
// 间距
"p-",
"px-",
"py-",
"pt-",
"pr-",
"pb-",
"pl-",
"m-",
"mx-",
"my-",
"mt-",
"mr-",
"mb-",
"ml-",
"space-",
"gap-",
// 尺寸
"w-",
"h-",
"min-w-",
"max-w-",
"min-h-",
"max-h-",
// 颜色
"bg-",
"text-",
"border-",
"ring-",
"shadow-",
// 边框
"border",
"rounded",
"ring",
// 字体
"font-",
"text-",
"leading-",
"tracking-",
"antialiased",
// 定位
"absolute",
"relative",
"fixed",
"sticky",
"static",
"top-",
"right-",
"bottom-",
"left-",
"inset-",
"z-",
// 变换
"transform",
"translate-",
"rotate-",
"scale-",
"skew-",
// 过渡
"transition",
"duration-",
"ease-",
"delay-",
// 交互
"cursor-",
"select-",
"pointer-events-",
// 溢出
"overflow-",
"truncate",
// 滚动
"scroll-",
// 伪类和响应式
"hover:",
"focus:",
"active:",
"disabled:",
"group-hover:",
];
const statePrefixes = ["dark:", "light:", "sm:", "md:", "lg:", "xl:", "2xl:"];
for (const prefix of prefixes) {
if (className.startsWith(prefix)) {
return true;
}
for (const statePrefix of statePrefixes) {
if (className.startsWith(statePrefix + prefix)) {
return true;
}
}
}
return false;
}

View File

@ -116,13 +116,8 @@ export declare namespace Config {
remPrecision?: number; remPrecision?: number;
// 转换比例 // 转换比例
rpxRatio?: number; rpxRatio?: number;
// 暗黑模式文本类名
darkTextClass?: string;
}; };
} }
interface Data {
type: Type;
reqUrl: string;
eps: Config.Eps;
demo: boolean;
[key: string]: any;
}
} }

File diff suppressed because it is too large Load Diff