2295 lines
87 KiB
JavaScript
2295 lines
87 KiB
JavaScript
![]() |
(function (global, factory) {
|
|||
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('fs'), require('path'), require('prettier'), require('axios'), require('lodash'), require('@vue/compiler-sfc'), require('magic-string'), require('glob'), require('node:util'), require('svgo'), require('postcss-value-parser')) :
|
|||
|
typeof define === 'function' && define.amd ? define(['exports', 'fs', 'path', 'prettier', 'axios', 'lodash', '@vue/compiler-sfc', 'magic-string', 'glob', 'node:util', 'svgo', 'postcss-value-parser'], factory) :
|
|||
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.index = {}, global.fs, global.path, global.prettier, global.axios, global.lodash, global.compilerSfc, global.magicString, global.glob, global.util, global.svgo, global.valueParser));
|
|||
|
})(this, (function (exports, fs, path, prettier, axios, lodash, compilerSfc, magicString, glob, util, svgo, valueParser) { 'use strict';
|
|||
|
|
|||
|
const config = {
|
|||
|
type: "admin",
|
|||
|
reqUrl: "",
|
|||
|
nameTag: true,
|
|||
|
eps: {
|
|||
|
enable: true,
|
|||
|
api: "",
|
|||
|
dist: "./build/cool",
|
|||
|
mapping: [
|
|||
|
{
|
|||
|
// 自定义匹配
|
|||
|
custom: ({ propertyName, type }) => {
|
|||
|
// 如果没有,返回null或者不返回,则继续遍历其他匹配规则
|
|||
|
return null;
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
type: "string",
|
|||
|
test: ["varchar", "text", "simple-json"],
|
|||
|
},
|
|||
|
{
|
|||
|
type: "string[]",
|
|||
|
test: ["simple-array"],
|
|||
|
},
|
|||
|
{
|
|||
|
type: "Date",
|
|||
|
test: ["datetime", "date"],
|
|||
|
},
|
|||
|
{
|
|||
|
type: "number",
|
|||
|
test: ["tinyint", "int", "decimal"],
|
|||
|
},
|
|||
|
{
|
|||
|
type: "BigInt",
|
|||
|
test: ["bigint"],
|
|||
|
},
|
|||
|
{
|
|||
|
type: "any",
|
|||
|
test: ["json"],
|
|||
|
},
|
|||
|
],
|
|||
|
},
|
|||
|
svg: {
|
|||
|
skipNames: ["base"],
|
|||
|
},
|
|||
|
tailwind: {
|
|||
|
enable: true,
|
|||
|
remUnit: 14,
|
|||
|
remPrecision: 6,
|
|||
|
rpxRatio: 2,
|
|||
|
darkTextClass: "dark:text-surface-50",
|
|||
|
},
|
|||
|
};
|
|||
|
|
|||
|
// 根目录
|
|||
|
function rootDir(path$1) {
|
|||
|
switch (config.type) {
|
|||
|
case "app":
|
|||
|
case "uniapp-x":
|
|||
|
return path.join(process.env.UNI_INPUT_DIR, path$1);
|
|||
|
default:
|
|||
|
return path.join(process.cwd(), path$1);
|
|||
|
}
|
|||
|
}
|
|||
|
// 首字母大写
|
|||
|
function firstUpperCase(value) {
|
|||
|
return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) {
|
|||
|
return $1.toUpperCase() + $2;
|
|||
|
});
|
|||
|
}
|
|||
|
// 横杠转驼峰
|
|||
|
function toCamel(str) {
|
|||
|
return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) {
|
|||
|
return $1 + $2.toUpperCase();
|
|||
|
});
|
|||
|
}
|
|||
|
// 创建目录
|
|||
|
function createDir(path, recursive) {
|
|||
|
try {
|
|||
|
if (!fs.existsSync(path))
|
|||
|
fs.mkdirSync(path, { recursive });
|
|||
|
}
|
|||
|
catch (err) { }
|
|||
|
}
|
|||
|
// 读取文件
|
|||
|
function readFile(path, json) {
|
|||
|
try {
|
|||
|
const content = fs.readFileSync(path, "utf8");
|
|||
|
return json
|
|||
|
? JSON.parse(content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
|
|||
|
: content;
|
|||
|
}
|
|||
|
catch (err) { }
|
|||
|
return "";
|
|||
|
}
|
|||
|
// 写入文件
|
|||
|
function writeFile(path, data) {
|
|||
|
try {
|
|||
|
return fs.writeFileSync(path, data);
|
|||
|
}
|
|||
|
catch (err) { }
|
|||
|
return "";
|
|||
|
}
|
|||
|
// 解析body
|
|||
|
function parseJson(req) {
|
|||
|
return new Promise((resolve) => {
|
|||
|
let d = "";
|
|||
|
req.on("data", function (chunk) {
|
|||
|
d += chunk;
|
|||
|
});
|
|||
|
req.on("end", function () {
|
|||
|
try {
|
|||
|
resolve(JSON.parse(d));
|
|||
|
}
|
|||
|
catch {
|
|||
|
resolve({});
|
|||
|
}
|
|||
|
});
|
|||
|
});
|
|||
|
}
|
|||
|
// 格式化内容
|
|||
|
function formatContent(content, options) {
|
|||
|
return prettier.format(content, {
|
|||
|
parser: "typescript",
|
|||
|
useTabs: true,
|
|||
|
tabWidth: 4,
|
|||
|
endOfLine: "lf",
|
|||
|
semi: true,
|
|||
|
...options,
|
|||
|
});
|
|||
|
}
|
|||
|
function error(message) {
|
|||
|
console.log("\x1B[31m%s\x1B[0m", message);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 将模板字符串扁平化处理,转换为 Service 类型定义
|
|||
|
* @param template - 包含 Service 类型定义的模板字符串
|
|||
|
* @returns 处理后的 Service 类型定义字符串
|
|||
|
* @throws {Error} 当模板中找不到 Service 类型定义时抛出错误
|
|||
|
*/
|
|||
|
function flatten(template) {
|
|||
|
// 查找 Service 类型定义的起始位置
|
|||
|
const startIndex = template.indexOf("export type Service = {");
|
|||
|
// 保留 Service 类型定义前的内容
|
|||
|
let header = template.substring(0, startIndex);
|
|||
|
// 获取 Service 类型定义及其内容,去除换行和制表符
|
|||
|
const serviceContent = template.substring(startIndex).replace(/\n|\t/g, "");
|
|||
|
let interfaces = "";
|
|||
|
let serviceFields = "";
|
|||
|
// 解析内容并生成接口定义
|
|||
|
parse(serviceContent).forEach(({ key, content, level }) => {
|
|||
|
interfaces += `\nexport interface ${firstUpperCase(key)}Interface {${content}}\n`;
|
|||
|
serviceFields += `${key}: ${firstUpperCase(key)}Interface;`;
|
|||
|
});
|
|||
|
return `${header}${interfaces}\nexport type Service = {${serviceFields}}`;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 查找匹配的右花括号位置
|
|||
|
* @param str - 要搜索的字符串
|
|||
|
* @param startIndex - 开始搜索的位置
|
|||
|
* @returns 匹配的右花括号位置
|
|||
|
* @throws {Error} 当找不到匹配的右花括号时抛出错误
|
|||
|
*/
|
|||
|
function findClosingBrace(str, startIndex) {
|
|||
|
let braceCount = 1;
|
|||
|
let currentIndex = startIndex;
|
|||
|
while (currentIndex < str.length && braceCount > 0) {
|
|||
|
if (str[currentIndex] === "{")
|
|||
|
braceCount++;
|
|||
|
if (str[currentIndex] === "}")
|
|||
|
braceCount--;
|
|||
|
currentIndex++;
|
|||
|
}
|
|||
|
if (braceCount !== 0) {
|
|||
|
throw new Error("Unmatched braces in the template");
|
|||
|
}
|
|||
|
return currentIndex - 1;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 解析内容中的嵌套结构
|
|||
|
* @param content - 要解析的内容字符串
|
|||
|
* @returns 解析结果数组,包含解析出的键值对
|
|||
|
*/
|
|||
|
function parse(content, level = 0) {
|
|||
|
// 匹配形如 xxx: { ... } 的结构
|
|||
|
const interfacePattern = /(\w+)\s*:\s*\{/g;
|
|||
|
const result = [];
|
|||
|
let match;
|
|||
|
while ((match = interfacePattern.exec(content)) !== null) {
|
|||
|
const startIndex = match.index + match[0].length;
|
|||
|
const endIndex = findClosingBrace(content, startIndex);
|
|||
|
if (endIndex > startIndex) {
|
|||
|
let parsedContent = content.substring(startIndex, endIndex).trim();
|
|||
|
// 处理嵌套结构
|
|||
|
if (parsedContent.includes("{") && parsedContent.includes("}")) {
|
|||
|
const nestedInterfaces = parse(parsedContent, level + 1);
|
|||
|
// 替换嵌套的内容为接口引用
|
|||
|
if (nestedInterfaces.length > 0) {
|
|||
|
nestedInterfaces.forEach((nestedInterface) => {
|
|||
|
const pattern = `${nestedInterface.key}: {${nestedInterface.content}};`;
|
|||
|
const replacement = `${nestedInterface.key}: ${firstUpperCase(nestedInterface.key)}Interface`;
|
|||
|
parsedContent = parsedContent.replace(pattern, replacement);
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
// 将解析结果添加到数组开头
|
|||
|
result.unshift({
|
|||
|
key: match[1],
|
|||
|
level,
|
|||
|
content: parsedContent,
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 获取动态类名
|
|||
|
*/
|
|||
|
const getDynamicClassNames = (value) => {
|
|||
|
const names = new Set();
|
|||
|
// 匹配函数调用中的对象参数(如 parseClass({'!bg-surface-50': hoverable}))
|
|||
|
const functionCallRegex = /\w+\s*\(\s*\{([^}]*)\}\s*\)/gs;
|
|||
|
let funcMatch;
|
|||
|
while ((funcMatch = functionCallRegex.exec(value)) !== null) {
|
|||
|
const objContent = funcMatch[1];
|
|||
|
// 提取对象中的键
|
|||
|
const keyRegex = /['"](.*?)['"]\s*:/gs;
|
|||
|
let keyMatch;
|
|||
|
while ((keyMatch = keyRegex.exec(objContent)) !== null) {
|
|||
|
keyMatch[1].trim() && names.add(keyMatch[1]);
|
|||
|
}
|
|||
|
}
|
|||
|
// 匹配对象键(如 { 'text-a': 1 })- 优化版本,避免跨行错误匹配
|
|||
|
const objKeyRegex = /[{,]\s*['"](.*?)['"]\s*:/gs;
|
|||
|
let objKeyMatch;
|
|||
|
while ((objKeyMatch = objKeyRegex.exec(value)) !== null) {
|
|||
|
const className = objKeyMatch[1].trim();
|
|||
|
// 确保是有效的CSS类名,避免匹配到错误内容
|
|||
|
if (className && !className.includes("\n") && !className.includes("\t")) {
|
|||
|
names.add(className);
|
|||
|
}
|
|||
|
}
|
|||
|
// 匹配数组中的字符串元素(如 'text-center')- 优化版本
|
|||
|
const arrayStringRegex = /(?:^|[,\[\s])\s*['"](.*?)['"]/gs;
|
|||
|
let arrayMatch;
|
|||
|
while ((arrayMatch = arrayStringRegex.exec(value)) !== null) {
|
|||
|
const className = arrayMatch[1].trim();
|
|||
|
// 确保是有效的CSS类名
|
|||
|
if (className && !className.includes("\n") && !className.includes("\t")) {
|
|||
|
names.add(className);
|
|||
|
}
|
|||
|
}
|
|||
|
// 匹配三元表达式中的字符串(如 'dark' 和 'light')
|
|||
|
const ternaryRegex = /(\?|:)\s*['"](.*?)['"]/gs;
|
|||
|
let ternaryMatch;
|
|||
|
while ((ternaryMatch = ternaryRegex.exec(value)) !== null) {
|
|||
|
ternaryMatch[2].trim() && names.add(ternaryMatch[2]);
|
|||
|
}
|
|||
|
// 匹配反引号模板字符串 - 改进版本
|
|||
|
const templateRegex = /`([^`]*)`/gs;
|
|||
|
let templateMatch;
|
|||
|
while ((templateMatch = templateRegex.exec(value)) !== null) {
|
|||
|
const templateContent = templateMatch[1];
|
|||
|
// 提取模板字符串中的普通文本部分(排除 ${} 表达式)
|
|||
|
const textParts = templateContent.split(/\$\{[^}]*\}/);
|
|||
|
textParts.forEach((part) => {
|
|||
|
part.trim()
|
|||
|
.split(/\s+/)
|
|||
|
.forEach((className) => {
|
|||
|
className.trim() && names.add(className.trim());
|
|||
|
});
|
|||
|
});
|
|||
|
// 提取模板字符串中 ${} 表达式内的字符串
|
|||
|
const expressionRegex = /\$\{([^}]*)\}/gs;
|
|||
|
let expressionMatch;
|
|||
|
while ((expressionMatch = expressionRegex.exec(templateContent)) !== null) {
|
|||
|
const expression = expressionMatch[1];
|
|||
|
// 递归处理表达式中的动态类名
|
|||
|
getDynamicClassNames(expression).forEach((name) => names.add(name));
|
|||
|
}
|
|||
|
}
|
|||
|
// 处理混合字符串(模板字符串 + 普通文本),如 "`text-red-900` text-red-1000"
|
|||
|
const mixedStringRegex = /`[^`]*`\s+([a-zA-Z0-9\-_\s]+)/g;
|
|||
|
let mixedMatch;
|
|||
|
while ((mixedMatch = mixedStringRegex.exec(value)) !== null) {
|
|||
|
const additionalClasses = mixedMatch[1].trim().split(/\s+/);
|
|||
|
additionalClasses.forEach((className) => {
|
|||
|
className.trim() && names.add(className.trim());
|
|||
|
});
|
|||
|
}
|
|||
|
// 处理普通字符串,多个类名用空格分割
|
|||
|
const stringRegex = /['"]([\w\s\-!:\/]+?)['"]/gs;
|
|||
|
let stringMatch;
|
|||
|
while ((stringMatch = stringRegex.exec(value)) !== null) {
|
|||
|
const classNames = stringMatch[1].trim().split(/\s+/);
|
|||
|
classNames.forEach((className) => {
|
|||
|
className.trim() && names.add(className.trim());
|
|||
|
});
|
|||
|
}
|
|||
|
return Array.from(names);
|
|||
|
};
|
|||
|
/**
|
|||
|
* 获取类名
|
|||
|
*/
|
|||
|
function getClassNames(code) {
|
|||
|
// 修改正则表达式以支持多行匹配,避免内层引号冲突
|
|||
|
const classRegex = /(?:class|:class|:pt|:hover-class)\s*=\s*(['"`])((?:[^'"`\\]|\\.|`[^`]*`|'[^']*'|"[^"]*")*?)\1/gis;
|
|||
|
const classNames = new Set();
|
|||
|
let match;
|
|||
|
while ((match = classRegex.exec(code)) !== null) {
|
|||
|
const attribute = match[0].split("=")[0].trim();
|
|||
|
const isStaticClass = attribute === "class" || attribute === "hover-class";
|
|||
|
const isPtAttribute = attribute.includes("pt");
|
|||
|
const value = match[2].trim();
|
|||
|
if (isStaticClass) {
|
|||
|
// 处理静态 class 和 hover-class
|
|||
|
value.split(/\s+/).forEach((name) => name && classNames.add(name));
|
|||
|
}
|
|||
|
else if (isPtAttribute) {
|
|||
|
// 处理 :pt 属性中的 className
|
|||
|
parseClasNameFromPt(value, classNames);
|
|||
|
}
|
|||
|
else {
|
|||
|
// 处理动态 :class 和 :hover-class
|
|||
|
getDynamicClassNames(value).forEach((name) => classNames.add(name));
|
|||
|
}
|
|||
|
}
|
|||
|
return Array.from(classNames);
|
|||
|
}
|
|||
|
/**
|
|||
|
* 从 :pt 属性中解析 className
|
|||
|
*/
|
|||
|
function parseClasNameFromPt(value, classNames) {
|
|||
|
// 递归查找所有 className 属性
|
|||
|
const classNameRegex = /className\s*:\s*/g;
|
|||
|
let match;
|
|||
|
while ((match = classNameRegex.exec(value)) !== null) {
|
|||
|
const startPos = match.index + match[0].length;
|
|||
|
const classNameValue = extractComplexValue(value, startPos);
|
|||
|
if (classNameValue) {
|
|||
|
// 如果是字符串字面量
|
|||
|
if (classNameValue.startsWith('"') ||
|
|||
|
classNameValue.startsWith("'") ||
|
|||
|
classNameValue.startsWith("`")) {
|
|||
|
if (classNameValue.startsWith("`")) {
|
|||
|
// 处理模板字符串
|
|||
|
getDynamicClassNames(classNameValue).forEach((name) => classNames.add(name));
|
|||
|
}
|
|||
|
else {
|
|||
|
// 处理普通字符串
|
|||
|
const strMatch = classNameValue.match(/['"](.*?)['"]/);
|
|||
|
if (strMatch) {
|
|||
|
strMatch[1].split(/\s+/).forEach((name) => name && classNames.add(name));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
// 处理动态值(如函数调用、对象等)
|
|||
|
getDynamicClassNames(classNameValue).forEach((name) => classNames.add(name));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
/**
|
|||
|
* 提取复杂值(支持嵌套引号和括号)
|
|||
|
*/
|
|||
|
function extractComplexValue(text, startPos) {
|
|||
|
let pos = startPos;
|
|||
|
let depth = 0;
|
|||
|
let inString = false;
|
|||
|
let stringChar = "";
|
|||
|
let result = "";
|
|||
|
// 跳过开头的空白字符
|
|||
|
while (pos < text.length && /\s/.test(text[pos])) {
|
|||
|
pos++;
|
|||
|
}
|
|||
|
while (pos < text.length) {
|
|||
|
const char = text[pos];
|
|||
|
if (!inString) {
|
|||
|
if (char === '"' || char === "'" || char === "`") {
|
|||
|
inString = true;
|
|||
|
stringChar = char;
|
|||
|
result += char;
|
|||
|
}
|
|||
|
else if (char === "{" || char === "(" || char === "[") {
|
|||
|
depth++;
|
|||
|
result += char;
|
|||
|
}
|
|||
|
else if (char === "}" || char === ")" || char === "]") {
|
|||
|
if (depth === 0 && char === "}") {
|
|||
|
// 遇到顶层的 } 时结束
|
|||
|
break;
|
|||
|
}
|
|||
|
depth--;
|
|||
|
result += char;
|
|||
|
}
|
|||
|
else if (char === "," && depth === 0) {
|
|||
|
// 遇到顶层的逗号时结束
|
|||
|
break;
|
|||
|
}
|
|||
|
else if (char === "\n" && depth === 0 && result.trim() !== "") {
|
|||
|
// 如果遇到换行且不在嵌套结构中,且已有内容,则结束
|
|||
|
break;
|
|||
|
}
|
|||
|
else {
|
|||
|
result += char;
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
result += char;
|
|||
|
if (char === stringChar && text[pos - 1] !== "\\") {
|
|||
|
inString = false;
|
|||
|
stringChar = "";
|
|||
|
// 如果字符串结束且depth为0,检查是否应该结束
|
|||
|
if (depth === 0) {
|
|||
|
// 看看下一个非空白字符是什么
|
|||
|
let nextPos = pos + 1;
|
|||
|
while (nextPos < text.length && /\s/.test(text[nextPos])) {
|
|||
|
nextPos++;
|
|||
|
}
|
|||
|
if (nextPos < text.length && (text[nextPos] === "," || text[nextPos] === "}")) {
|
|||
|
// 如果下一个字符是逗号或右括号,则结束
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
pos++;
|
|||
|
}
|
|||
|
return result.trim() || null;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 获取 class 内容
|
|||
|
*/
|
|||
|
function getClassContent(code) {
|
|||
|
// 修改正则表达式以支持多行匹配,避免内层引号冲突
|
|||
|
const regex = /(?:class|:class|:pt|:hover-class)\s*=\s*(['"`])((?:[^'"`\\]|\\.|`[^`]*`|'[^']*'|"[^"]*")*?)\1/gis;
|
|||
|
const texts = [];
|
|||
|
let match;
|
|||
|
while ((match = regex.exec(code)) !== null) {
|
|||
|
const attribute = match[0].split("=")[0].trim();
|
|||
|
const isPtAttribute = attribute.includes("pt");
|
|||
|
const value = match[2];
|
|||
|
if (isPtAttribute) {
|
|||
|
// 手动解析 className 值
|
|||
|
const classNameRegex = /className\s*:\s*/g;
|
|||
|
let classNameMatchResult;
|
|||
|
while ((classNameMatchResult = classNameRegex.exec(value)) !== null) {
|
|||
|
const startPos = classNameMatchResult.index + classNameMatchResult[0].length;
|
|||
|
const classNameValue = extractComplexValue(value, startPos);
|
|||
|
if (classNameValue) {
|
|||
|
texts.push(classNameValue);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
texts.push(value);
|
|||
|
}
|
|||
|
}
|
|||
|
return texts;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 获取节点
|
|||
|
*/
|
|||
|
function getNodes(code) {
|
|||
|
const nodes = [];
|
|||
|
// 找到所有顶级template标签的完整内容
|
|||
|
function findTemplateContents(content) {
|
|||
|
const results = [];
|
|||
|
let index = 0;
|
|||
|
while (index < content.length) {
|
|||
|
const templateStart = content.indexOf("<template", index);
|
|||
|
if (templateStart === -1)
|
|||
|
break;
|
|||
|
// 找到模板标签的结束位置
|
|||
|
const tagEnd = content.indexOf(">", templateStart);
|
|||
|
if (tagEnd === -1)
|
|||
|
break;
|
|||
|
// 使用栈来匹配配对的template标签
|
|||
|
let stack = 1;
|
|||
|
let currentPos = tagEnd + 1;
|
|||
|
while (currentPos < content.length && stack > 0) {
|
|||
|
const nextTemplateStart = content.indexOf("<template", currentPos);
|
|||
|
const nextTemplateEnd = content.indexOf("</template>", currentPos);
|
|||
|
if (nextTemplateEnd === -1)
|
|||
|
break;
|
|||
|
// 如果开始标签更近,说明有嵌套
|
|||
|
if (nextTemplateStart !== -1 && nextTemplateStart < nextTemplateEnd) {
|
|||
|
// 找到开始标签的完整结束
|
|||
|
const nestedTagEnd = content.indexOf(">", nextTemplateStart);
|
|||
|
if (nestedTagEnd !== -1) {
|
|||
|
stack++;
|
|||
|
currentPos = nestedTagEnd + 1;
|
|||
|
}
|
|||
|
else {
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
// 找到结束标签
|
|||
|
stack--;
|
|||
|
currentPos = nextTemplateEnd + 11; // '</template>'.length
|
|||
|
}
|
|||
|
}
|
|||
|
if (stack === 0) {
|
|||
|
// 提取template内容(不包括template标签本身)
|
|||
|
const templateContent = content.substring(tagEnd + 1, currentPos - 11);
|
|||
|
results.push(templateContent);
|
|||
|
index = currentPos;
|
|||
|
}
|
|||
|
else {
|
|||
|
// 如果没有找到匹配的结束标签,跳过这个开始标签
|
|||
|
index = tagEnd + 1;
|
|||
|
}
|
|||
|
}
|
|||
|
return results;
|
|||
|
}
|
|||
|
// 递归提取所有template内容中的节点
|
|||
|
function extractNodesFromContent(content) {
|
|||
|
// 先提取当前内容中的所有标签
|
|||
|
const regex = /<([^>]+)>/g;
|
|||
|
let match;
|
|||
|
while ((match = regex.exec(content)) !== null) {
|
|||
|
if (!match[1].startsWith("/") && !match[1].startsWith("template")) {
|
|||
|
nodes.push(match[1]);
|
|||
|
}
|
|||
|
}
|
|||
|
// 递归处理嵌套的template
|
|||
|
const nestedTemplates = findTemplateContents(content);
|
|||
|
nestedTemplates.forEach((templateContent) => {
|
|||
|
extractNodesFromContent(templateContent);
|
|||
|
});
|
|||
|
}
|
|||
|
// 获取所有顶级template内容
|
|||
|
const templateContents = findTemplateContents(code);
|
|||
|
// 处理每个template内容
|
|||
|
templateContents.forEach((templateContent) => {
|
|||
|
extractNodesFromContent(templateContent);
|
|||
|
});
|
|||
|
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:", "dark:!", "light:", "sm:", "md:", "lg:", "xl:", "2xl:"];
|
|||
|
if (className.startsWith("!") && !className.includes("!=")) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
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;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 将 interface 转换为 type
|
|||
|
*/
|
|||
|
function interfaceToType(code) {
|
|||
|
// 匹配 interface 定义
|
|||
|
const interfaceRegex = /interface\s+(\w+)(\s*extends\s+\w+)?\s*\{([^}]*)\}/g;
|
|||
|
// 将 interface 转换为 type
|
|||
|
return code.replace(interfaceRegex, (match, name, extends_, content) => {
|
|||
|
// 处理可能存在的 extends
|
|||
|
const extendsStr = extends_ ? extends_ : "";
|
|||
|
// 返回转换后的 type 定义
|
|||
|
return `type ${name}${extendsStr} = {${content}}`;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// 全局 service 对象,用于存储服务结构
|
|||
|
const service = {};
|
|||
|
// eps 实体列表
|
|||
|
let list = [];
|
|||
|
/**
|
|||
|
* 获取 eps 请求地址
|
|||
|
* @returns {string} eps url
|
|||
|
*/
|
|||
|
function getEpsUrl() {
|
|||
|
let url = config.eps.api;
|
|||
|
if (!url) {
|
|||
|
url = config.type;
|
|||
|
}
|
|||
|
switch (url) {
|
|||
|
case "app":
|
|||
|
case "uniapp-x":
|
|||
|
url = "/app/base/comm/eps";
|
|||
|
break;
|
|||
|
case "admin":
|
|||
|
url = "/admin/base/open/eps";
|
|||
|
break;
|
|||
|
}
|
|||
|
return url;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 获取 eps 路径
|
|||
|
* @param filename 文件名
|
|||
|
* @returns {string} 完整路径
|
|||
|
*/
|
|||
|
function getEpsPath(filename) {
|
|||
|
return path.join(config.type == "admin" ? config.eps.dist : rootDir(config.eps.dist), filename || "");
|
|||
|
}
|
|||
|
/**
|
|||
|
* 获取对象方法名(排除 namespace、permission 字段)
|
|||
|
* @param v 对象
|
|||
|
* @returns {string[]} 方法名数组
|
|||
|
*/
|
|||
|
function getNames(v) {
|
|||
|
return Object.keys(v).filter((e) => !["namespace", "permission"].includes(e));
|
|||
|
}
|
|||
|
/**
|
|||
|
* 获取字段类型
|
|||
|
*/
|
|||
|
function getType({ propertyName, type }) {
|
|||
|
for (const map of config.eps.mapping) {
|
|||
|
if (map.custom) {
|
|||
|
const resType = map.custom({ propertyName, type });
|
|||
|
if (resType)
|
|||
|
return resType;
|
|||
|
}
|
|||
|
if (map.test) {
|
|||
|
if (map.test.includes(type))
|
|||
|
return map.type;
|
|||
|
}
|
|||
|
}
|
|||
|
return type;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 格式化方法名,去除特殊字符
|
|||
|
*/
|
|||
|
function formatName(name) {
|
|||
|
return (name || "").replace(/[:,\s,\/,-]/g, "");
|
|||
|
}
|
|||
|
/**
|
|||
|
* 检查方法名是否合法(不包含特殊字符)
|
|||
|
*/
|
|||
|
function checkName(name) {
|
|||
|
return name && !["{", "}", ":"].some((e) => name.includes(e));
|
|||
|
}
|
|||
|
/**
|
|||
|
* 不支持 uniapp-x 平台显示
|
|||
|
*/
|
|||
|
function noUniappX(text, defaultText = "") {
|
|||
|
if (config.type == "uniapp-x") {
|
|||
|
return defaultText;
|
|||
|
}
|
|||
|
else {
|
|||
|
return text;
|
|||
|
}
|
|||
|
}
|
|||
|
/**
|
|||
|
* 查找字段
|
|||
|
* @param sources 字段 source 数组
|
|||
|
* @param item eps 实体
|
|||
|
* @returns {Eps.Column[]} 字段数组
|
|||
|
*/
|
|||
|
function findColumns(sources, item) {
|
|||
|
const columns = [item.columns, item.pageColumns].flat().filter(Boolean);
|
|||
|
return (sources || [])
|
|||
|
.map((e) => columns.find((c) => c.source == e))
|
|||
|
.filter(Boolean);
|
|||
|
}
|
|||
|
/**
|
|||
|
* 使用 prettier 格式化 TypeScript 代码
|
|||
|
* @param text 代码文本
|
|||
|
* @returns {Promise<string|null>} 格式化后的代码
|
|||
|
*/
|
|||
|
async function formatCode(text) {
|
|||
|
return prettier
|
|||
|
.format(text, {
|
|||
|
parser: "typescript",
|
|||
|
useTabs: true,
|
|||
|
tabWidth: 4,
|
|||
|
endOfLine: "lf",
|
|||
|
semi: true,
|
|||
|
singleQuote: false,
|
|||
|
printWidth: 100,
|
|||
|
trailingComma: "none",
|
|||
|
})
|
|||
|
.catch(() => {
|
|||
|
error(`[cool-eps] Failed to format /build/cool/eps.d.ts. Please delete the file and try again`);
|
|||
|
return null;
|
|||
|
});
|
|||
|
}
|
|||
|
/**
|
|||
|
* 获取 eps 数据(本地优先,远程兜底)
|
|||
|
*/
|
|||
|
async function getData() {
|
|||
|
// 读取本地 eps.json
|
|||
|
list = readFile(getEpsPath("eps.json"), true) || [];
|
|||
|
// 拼接请求地址
|
|||
|
const url = config.reqUrl + getEpsUrl();
|
|||
|
// 请求远程 eps 数据
|
|||
|
await axios
|
|||
|
.get(url, {
|
|||
|
timeout: 5000,
|
|||
|
})
|
|||
|
.then((res) => {
|
|||
|
const { code, data, message } = res.data;
|
|||
|
if (code === 1000) {
|
|||
|
if (!lodash.isEmpty(data) && data) {
|
|||
|
list = lodash.values(data).flat();
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
error(`[cool-eps] ${message || "Failed to fetch data"}`);
|
|||
|
}
|
|||
|
})
|
|||
|
.catch(() => {
|
|||
|
error(`[cool-eps] API service is not running → ${url}`);
|
|||
|
});
|
|||
|
// 初始化处理,补全缺省字段
|
|||
|
list.forEach((e) => {
|
|||
|
if (!e.namespace)
|
|||
|
e.namespace = "";
|
|||
|
if (!e.api)
|
|||
|
e.api = [];
|
|||
|
if (!e.columns)
|
|||
|
e.columns = [];
|
|||
|
if (!e.search) {
|
|||
|
e.search = {
|
|||
|
fieldEq: findColumns(e.pageQueryOp?.fieldEq, e),
|
|||
|
fieldLike: findColumns(e.pageQueryOp?.fieldLike, e),
|
|||
|
keyWordLikeFields: findColumns(e.pageQueryOp?.keyWordLikeFields, e),
|
|||
|
};
|
|||
|
}
|
|||
|
});
|
|||
|
if (config.type == "uniapp-x" || config.type == "app") {
|
|||
|
list = list.filter((e) => e.prefix.startsWith("/app"));
|
|||
|
}
|
|||
|
}
|
|||
|
/**
|
|||
|
* 创建 eps.json 文件
|
|||
|
* @returns {boolean} 是否有更新
|
|||
|
*/
|
|||
|
function createJson() {
|
|||
|
if (config.type == "uniapp-x") {
|
|||
|
return false;
|
|||
|
}
|
|||
|
const arr = list.map((e) => {
|
|||
|
return {
|
|||
|
prefix: e.prefix,
|
|||
|
name: e.name || "",
|
|||
|
api: e.api.map((apiItem) => ({
|
|||
|
name: apiItem.name,
|
|||
|
method: apiItem.method,
|
|||
|
path: apiItem.path,
|
|||
|
})),
|
|||
|
search: e.search,
|
|||
|
};
|
|||
|
});
|
|||
|
const content = JSON.stringify(arr);
|
|||
|
const local_content = readFile(getEpsPath("eps.json"));
|
|||
|
// 判断是否需要更新
|
|||
|
const isUpdate = content != local_content;
|
|||
|
if (isUpdate) {
|
|||
|
fs.createWriteStream(getEpsPath("eps.json"), {
|
|||
|
flags: "w",
|
|||
|
}).write(content);
|
|||
|
}
|
|||
|
return isUpdate;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 创建 eps 类型描述文件(d.ts/ts)
|
|||
|
* @param param0 list: eps实体列表, service: service对象
|
|||
|
*/
|
|||
|
async function createDescribe({ list, service }) {
|
|||
|
/**
|
|||
|
* 创建 Entity 接口定义
|
|||
|
*/
|
|||
|
function createEntity() {
|
|||
|
const ignore = [];
|
|||
|
let t0 = "";
|
|||
|
for (const item of list) {
|
|||
|
if (!checkName(item.name))
|
|||
|
continue;
|
|||
|
let t = `interface ${formatName(item.name)} {`;
|
|||
|
// 合并 columns 和 pageColumns,去重
|
|||
|
const columns = lodash.uniqBy(lodash.compact([...(item.columns || []), ...(item.pageColumns || [])]), "source");
|
|||
|
for (const col of columns || []) {
|
|||
|
t += `
|
|||
|
/**
|
|||
|
* ${col.comment}
|
|||
|
*/
|
|||
|
${col.propertyName}?: ${getType({
|
|||
|
propertyName: col.propertyName,
|
|||
|
type: col.type,
|
|||
|
})};
|
|||
|
`;
|
|||
|
}
|
|||
|
t += `
|
|||
|
/**
|
|||
|
* 任意键值
|
|||
|
*/
|
|||
|
[key: string]: any;
|
|||
|
}
|
|||
|
`;
|
|||
|
if (!ignore.includes(item.name)) {
|
|||
|
ignore.push(item.name);
|
|||
|
t0 += t + "\n\n";
|
|||
|
}
|
|||
|
}
|
|||
|
return t0;
|
|||
|
}
|
|||
|
/**
|
|||
|
* 创建 Controller 接口定义
|
|||
|
*/
|
|||
|
async function createController() {
|
|||
|
let controller = "";
|
|||
|
let chain = "";
|
|||
|
let pageResponse = "";
|
|||
|
/**
|
|||
|
* 递归处理 service 树,生成接口定义
|
|||
|
* @param d 当前节点
|
|||
|
* @param k 前缀
|
|||
|
*/
|
|||
|
function deep(d, k) {
|
|||
|
if (!k)
|
|||
|
k = "";
|
|||
|
for (const i in d) {
|
|||
|
const name = k + toCamel(firstUpperCase(formatName(i)));
|
|||
|
// 检查方法名
|
|||
|
if (!checkName(name))
|
|||
|
continue;
|
|||
|
if (d[i].namespace) {
|
|||
|
// 查找配置
|
|||
|
const item = list.find((e) => (e.prefix || "") === `/${d[i].namespace}`);
|
|||
|
if (item) {
|
|||
|
//
|
|||
|
let t = `interface ${name} {`;
|
|||
|
// 插入方法
|
|||
|
if (item.api) {
|
|||
|
// 权限列表
|
|||
|
const permission = [];
|
|||
|
item.api.forEach((a) => {
|
|||
|
// 方法名
|
|||
|
const n = toCamel(formatName(a.name || lodash.last(a.path.split("/"))));
|
|||
|
// 检查方法名
|
|||
|
if (!checkName(n))
|
|||
|
return;
|
|||
|
if (n) {
|
|||
|
// 参数类型
|
|||
|
let q = [];
|
|||
|
// 参数列表
|
|||
|
const { parameters = [] } = a.dts || {};
|
|||
|
parameters.forEach((p) => {
|
|||
|
if (p.description) {
|
|||
|
q.push(`\n/** ${p.description} */\n`);
|
|||
|
}
|
|||
|
// 检查参数名
|
|||
|
if (!checkName(p.name)) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
const a = `${p.name}${p.required ? "" : "?"}`;
|
|||
|
const b = `${p.schema.type || "string"}`;
|
|||
|
q.push(`${a}: ${b};`);
|
|||
|
});
|
|||
|
if (lodash.isEmpty(q)) {
|
|||
|
q = ["any"];
|
|||
|
}
|
|||
|
else {
|
|||
|
q.unshift("{");
|
|||
|
q.push("}");
|
|||
|
}
|
|||
|
// 返回类型
|
|||
|
let res = "";
|
|||
|
// 实体名
|
|||
|
const en = item.name || "any";
|
|||
|
switch (a.path) {
|
|||
|
case "/page":
|
|||
|
res = `${name}PageResponse`;
|
|||
|
pageResponse += `
|
|||
|
interface ${name}PageResponse {
|
|||
|
pagination: PagePagination;
|
|||
|
list: ${en}[];
|
|||
|
}
|
|||
|
`;
|
|||
|
break;
|
|||
|
case "/list":
|
|||
|
res = `${en} []`;
|
|||
|
break;
|
|||
|
case "/info":
|
|||
|
res = en;
|
|||
|
break;
|
|||
|
default:
|
|||
|
res = "any";
|
|||
|
break;
|
|||
|
}
|
|||
|
// 方法描述
|
|||
|
t += `
|
|||
|
/**
|
|||
|
* ${a.summary || n}
|
|||
|
*/
|
|||
|
${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<${res}>;
|
|||
|
`;
|
|||
|
if (!permission.includes(n)) {
|
|||
|
permission.push(n);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
// 权限标识
|
|||
|
t += noUniappX(`
|
|||
|
/**
|
|||
|
* 权限标识
|
|||
|
*/
|
|||
|
permission: { ${permission.map((e) => `${e}: string;`).join("\n")} };
|
|||
|
`);
|
|||
|
// 权限状态
|
|||
|
t += noUniappX(`
|
|||
|
/**
|
|||
|
* 权限状态
|
|||
|
*/
|
|||
|
_permission: { ${permission.map((e) => `${e}: boolean;`).join("\n")} };
|
|||
|
`);
|
|||
|
// 请求
|
|||
|
t += noUniappX(`
|
|||
|
request: Request;
|
|||
|
`);
|
|||
|
}
|
|||
|
t += "}\n\n";
|
|||
|
controller += t;
|
|||
|
chain += `${formatName(i)}: ${name};`;
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
chain += `${formatName(i)}: {`;
|
|||
|
deep(d[i], name);
|
|||
|
chain += "};";
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
// 遍历 service 树
|
|||
|
deep(service);
|
|||
|
return `
|
|||
|
type json = any;
|
|||
|
|
|||
|
interface PagePagination {
|
|||
|
size: number;
|
|||
|
page: number;
|
|||
|
total: number;
|
|||
|
[key: string]: any;
|
|||
|
};
|
|||
|
|
|||
|
interface PageResponse<T> {
|
|||
|
pagination: PagePagination;
|
|||
|
list: T[];
|
|||
|
[key: string]: any;
|
|||
|
};
|
|||
|
|
|||
|
${pageResponse}
|
|||
|
|
|||
|
${controller}
|
|||
|
|
|||
|
${noUniappX(`interface RequestOptions {
|
|||
|
url: string;
|
|||
|
method?: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT';
|
|||
|
data?: any;
|
|||
|
params?: any;
|
|||
|
headers?: any;
|
|||
|
timeout?: number;
|
|||
|
[key: string]: any;
|
|||
|
}`)}
|
|||
|
|
|||
|
${noUniappX("type Request = (options: RequestOptions) => Promise<any>;")}
|
|||
|
|
|||
|
${await createDict()}
|
|||
|
|
|||
|
type Service = {
|
|||
|
${noUniappX("request: Request;")}
|
|||
|
|
|||
|
${chain}
|
|||
|
}
|
|||
|
`;
|
|||
|
}
|
|||
|
// 组装文件内容
|
|||
|
let text = `
|
|||
|
${createEntity()}
|
|||
|
${await createController()}
|
|||
|
`;
|
|||
|
// 文件名
|
|||
|
let name = "eps.d.ts";
|
|||
|
if (config.type == "uniapp-x") {
|
|||
|
name = "eps.ts";
|
|||
|
text = text
|
|||
|
.replaceAll("interface ", "export interface ")
|
|||
|
.replaceAll("type ", "export type ")
|
|||
|
.replaceAll("[key: string]: any;", "");
|
|||
|
text = flatten(text);
|
|||
|
text = interfaceToType(text);
|
|||
|
}
|
|||
|
else {
|
|||
|
text = `
|
|||
|
declare namespace Eps {
|
|||
|
${text}
|
|||
|
}
|
|||
|
`;
|
|||
|
}
|
|||
|
// 格式化文本内容
|
|||
|
const content = await formatCode(text);
|
|||
|
const local_content = readFile(getEpsPath(name));
|
|||
|
// 是否需要更新
|
|||
|
if (content && content != local_content && list.length > 0) {
|
|||
|
// 创建 eps 描述文件
|
|||
|
fs.createWriteStream(getEpsPath(name), {
|
|||
|
flags: "w",
|
|||
|
}).write(content);
|
|||
|
}
|
|||
|
}
|
|||
|
/**
|
|||
|
* 构建 service 对象树
|
|||
|
*/
|
|||
|
function createService() {
|
|||
|
// 路径第一层作为 id 标识
|
|||
|
const id = getEpsUrl().split("/")[1];
|
|||
|
list.forEach((e) => {
|
|||
|
// 请求地址
|
|||
|
const path = e.prefix[0] == "/" ? e.prefix.substring(1, e.prefix.length) : e.prefix;
|
|||
|
// 分隔路径,去除 id,转驼峰
|
|||
|
const arr = path.replace(id, "").split("/").filter(Boolean).map(toCamel);
|
|||
|
/**
|
|||
|
* 递归构建 service 树
|
|||
|
* @param d 当前节点
|
|||
|
* @param i 当前索引
|
|||
|
*/
|
|||
|
function deep(d, i) {
|
|||
|
const k = arr[i];
|
|||
|
if (k) {
|
|||
|
// 是否最后一个
|
|||
|
if (arr[i + 1]) {
|
|||
|
if (!d[k]) {
|
|||
|
d[k] = {};
|
|||
|
}
|
|||
|
deep(d[k], i + 1);
|
|||
|
}
|
|||
|
else {
|
|||
|
// 不存在则创建
|
|||
|
if (!d[k]) {
|
|||
|
d[k] = {
|
|||
|
permission: {},
|
|||
|
};
|
|||
|
}
|
|||
|
if (!d[k].namespace) {
|
|||
|
d[k].namespace = path;
|
|||
|
}
|
|||
|
// 创建权限
|
|||
|
if (d[k].namespace) {
|
|||
|
getNames(d[k]).forEach((i) => {
|
|||
|
d[k].permission[i] =
|
|||
|
`${d[k].namespace.replace(`${id}/`, "")}/${i}`.replace(/\//g, ":");
|
|||
|
});
|
|||
|
}
|
|||
|
// 创建搜索
|
|||
|
d[k].search = e.search;
|
|||
|
// 创建方法
|
|||
|
e.api.forEach((a) => {
|
|||
|
// 方法名
|
|||
|
const n = a.path.replace("/", "");
|
|||
|
if (n && !/[-:]/g.test(n)) {
|
|||
|
d[k][n] = a;
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
deep(service, 0);
|
|||
|
});
|
|||
|
}
|
|||
|
/**
|
|||
|
* 创建 service 代码
|
|||
|
* @returns {string} service 代码
|
|||
|
*/
|
|||
|
function createServiceCode() {
|
|||
|
const types = [];
|
|||
|
let chain = "";
|
|||
|
/**
|
|||
|
* 递归处理 service 树,生成接口代码
|
|||
|
* @param d 当前节点
|
|||
|
* @param k 前缀
|
|||
|
*/
|
|||
|
function deep(d, k) {
|
|||
|
if (!k)
|
|||
|
k = "";
|
|||
|
for (const i in d) {
|
|||
|
if (["swagger"].includes(i)) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
const name = k + toCamel(firstUpperCase(formatName(i)));
|
|||
|
// 检查方法名
|
|||
|
if (!checkName(name))
|
|||
|
continue;
|
|||
|
if (d[i].namespace) {
|
|||
|
// 查找配置
|
|||
|
const item = list.find((e) => (e.prefix || "") === `/${d[i].namespace}`);
|
|||
|
if (item) {
|
|||
|
//
|
|||
|
let t = `{`;
|
|||
|
// 插入方法
|
|||
|
if (item.api) {
|
|||
|
item.api.forEach((a) => {
|
|||
|
// 方法名
|
|||
|
const n = toCamel(formatName(a.name || lodash.last(a.path.split("/"))));
|
|||
|
// 检查方法名
|
|||
|
if (!checkName(n))
|
|||
|
return;
|
|||
|
if (n) {
|
|||
|
// 参数类型
|
|||
|
let q = [];
|
|||
|
// 参数列表
|
|||
|
const { parameters = [] } = a.dts || {};
|
|||
|
parameters.forEach((p) => {
|
|||
|
if (p.description) {
|
|||
|
q.push(`\n/** ${p.description} */\n`);
|
|||
|
}
|
|||
|
// 检查参数名
|
|||
|
if (!checkName(p.name)) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
const a = `${p.name}${p.required ? "" : "?"}`;
|
|||
|
const b = `${p.schema.type || "string"}`;
|
|||
|
q.push(`${a}: ${b}, `);
|
|||
|
});
|
|||
|
if (lodash.isEmpty(q)) {
|
|||
|
q = ["any"];
|
|||
|
}
|
|||
|
else {
|
|||
|
q.unshift("{");
|
|||
|
q.push("}");
|
|||
|
}
|
|||
|
if (item.name) {
|
|||
|
types.push(item.name);
|
|||
|
}
|
|||
|
// 返回类型
|
|||
|
let res = "";
|
|||
|
// 实体名
|
|||
|
const en = item.name || "any";
|
|||
|
switch (a.path) {
|
|||
|
case "/page":
|
|||
|
res = `${name}PageResponse`;
|
|||
|
types.push(res);
|
|||
|
break;
|
|||
|
case "/list":
|
|||
|
res = `${en}[]`;
|
|||
|
break;
|
|||
|
case "/info":
|
|||
|
res = en;
|
|||
|
break;
|
|||
|
default:
|
|||
|
res = "any";
|
|||
|
break;
|
|||
|
}
|
|||
|
// 方法描述
|
|||
|
t += `
|
|||
|
/**
|
|||
|
* ${a.summary || n}
|
|||
|
*/
|
|||
|
${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")})${noUniappX(`: Promise<${res}>`)} {
|
|||
|
return request<${res}>({
|
|||
|
url: "/${d[i].namespace}${a.path}",
|
|||
|
method: "${(a.method || "get").toLocaleUpperCase()}",
|
|||
|
data,
|
|||
|
});
|
|||
|
},
|
|||
|
`;
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
t += `} as ${name}\n`;
|
|||
|
types.push(name);
|
|||
|
chain += `${formatName(i)}: ${t},\n`;
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
chain += `${formatName(i)}: {`;
|
|||
|
deep(d[i], name);
|
|||
|
chain += `} as ${firstUpperCase(i)}Interface,`;
|
|||
|
types.push(`${firstUpperCase(i)}Interface`);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
// 遍历 service 树
|
|||
|
deep(service);
|
|||
|
return {
|
|||
|
content: `{ ${chain} }`,
|
|||
|
types,
|
|||
|
};
|
|||
|
}
|
|||
|
/**
|
|||
|
* 获取字典类型定义
|
|||
|
* @returns {Promise<string>} 字典类型 type 定义
|
|||
|
*/
|
|||
|
async function createDict() {
|
|||
|
let p = "";
|
|||
|
switch (config.type) {
|
|||
|
case "app":
|
|||
|
case "uniapp-x":
|
|||
|
p = "/app";
|
|||
|
break;
|
|||
|
case "admin":
|
|||
|
p = "/admin";
|
|||
|
break;
|
|||
|
}
|
|||
|
const url = config.reqUrl + p + "/dict/info/types";
|
|||
|
const text = await axios
|
|||
|
.get(url)
|
|||
|
.then((res) => {
|
|||
|
const { code, data } = res.data;
|
|||
|
if (code === 1000) {
|
|||
|
let v = "string";
|
|||
|
if (!lodash.isEmpty(data)) {
|
|||
|
v = data.map((e) => `"${e.key}"`).join(" | ");
|
|||
|
}
|
|||
|
return `type DictKey = ${v}`;
|
|||
|
}
|
|||
|
})
|
|||
|
.catch(() => {
|
|||
|
error(`[cool-eps] Error:${url}`);
|
|||
|
});
|
|||
|
return text || "";
|
|||
|
}
|
|||
|
/**
|
|||
|
* 主入口:创建 eps 相关文件和 service
|
|||
|
*/
|
|||
|
async function createEps() {
|
|||
|
if (config.eps.enable) {
|
|||
|
// 获取 eps 数据
|
|||
|
await getData();
|
|||
|
// 构建 service 对象
|
|||
|
createService();
|
|||
|
const serviceCode = createServiceCode();
|
|||
|
// 创建 eps 目录
|
|||
|
createDir(getEpsPath(), true);
|
|||
|
// 创建 eps.json 文件
|
|||
|
const isUpdate = createJson();
|
|||
|
// 创建类型描述文件
|
|||
|
createDescribe({ service, list });
|
|||
|
return {
|
|||
|
service,
|
|||
|
serviceCode,
|
|||
|
list,
|
|||
|
isUpdate,
|
|||
|
};
|
|||
|
}
|
|||
|
else {
|
|||
|
return {
|
|||
|
service: {},
|
|||
|
list: [],
|
|||
|
};
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function getPlugin(name) {
|
|||
|
let code = readFile(rootDir(`./src/plugins/${name}/config.ts`));
|
|||
|
// 设置插件配置
|
|||
|
const set = (key, value) => {
|
|||
|
const regex = new RegExp(`(return\\s*{[^}]*?\\b${key}\\b\\s*:\\s*)([^,}]+)`);
|
|||
|
if (regex.test(code)) {
|
|||
|
code = code.replace(regex, `$1${JSON.stringify(value)}`);
|
|||
|
}
|
|||
|
else {
|
|||
|
const insertPos = code.indexOf("return {") + 8;
|
|||
|
code =
|
|||
|
code.slice(0, insertPos) +
|
|||
|
`\n ${key}: ${JSON.stringify(value)},` +
|
|||
|
code.slice(insertPos);
|
|||
|
}
|
|||
|
};
|
|||
|
// 保存插件配置
|
|||
|
const save = async () => {
|
|||
|
const content = await formatContent(code);
|
|||
|
writeFile(rootDir(`./src/plugins/${name}/config.ts`), content);
|
|||
|
};
|
|||
|
return {
|
|||
|
set,
|
|||
|
save,
|
|||
|
};
|
|||
|
}
|
|||
|
// 修改插件
|
|||
|
async function updatePlugin(options) {
|
|||
|
const plugin = getPlugin(options.name);
|
|||
|
if (options.enable !== undefined) {
|
|||
|
plugin.set("enable", options.enable);
|
|||
|
}
|
|||
|
await plugin.save();
|
|||
|
}
|
|||
|
|
|||
|
function getPath() {
|
|||
|
return rootDir(`.${config.type == "admin" ? "/src" : ""}/config/proxy.ts`);
|
|||
|
}
|
|||
|
async function updateProxy(data) {
|
|||
|
let code = readFile(getPath());
|
|||
|
const regex = /const\s+value\s*=\s*['"]([^'"]+)['"]/;
|
|||
|
if (regex.test(code)) {
|
|||
|
code = code.replace(regex, `const value = '${data.name}'`);
|
|||
|
}
|
|||
|
writeFile(getPath(), code);
|
|||
|
}
|
|||
|
function getProxyTarget(proxy) {
|
|||
|
const code = readFile(getPath());
|
|||
|
const regex = /const\s+value\s*=\s*['"]([^'"]+)['"]/;
|
|||
|
const match = code.match(regex);
|
|||
|
if (match) {
|
|||
|
const value = match[1];
|
|||
|
try {
|
|||
|
const { target, rewrite } = proxy[`/${value}/`];
|
|||
|
return target + rewrite(`/${value}`);
|
|||
|
}
|
|||
|
catch (err) {
|
|||
|
error(`[cool-proxy] Error:${value} → ` + getPath());
|
|||
|
return "";
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 创建文件
|
|||
|
async function createFile(data) {
|
|||
|
const list = lodash.isArray(data) ? data : [data];
|
|||
|
for (const item of list) {
|
|||
|
const { path: path$1, code } = item;
|
|||
|
// 格式化内容
|
|||
|
const content = await formatContent(code, {
|
|||
|
parser: "vue",
|
|||
|
});
|
|||
|
// 目录路径
|
|||
|
const dir = (path$1 || "").split("/");
|
|||
|
// 文件名
|
|||
|
const fname = dir.pop();
|
|||
|
// 源码路径
|
|||
|
const srcPath = `./src/${dir.join("/")}`;
|
|||
|
// 创建目录
|
|||
|
createDir(srcPath, true);
|
|||
|
// 创建文件
|
|||
|
fs.createWriteStream(path.join(srcPath, fname), {
|
|||
|
flags: "w",
|
|||
|
}).write(content);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function createTag(code, id) {
|
|||
|
if (/\.vue$/.test(id)) {
|
|||
|
let s;
|
|||
|
const str = () => s || (s = new magicString(code));
|
|||
|
const { descriptor } = compilerSfc.parse(code);
|
|||
|
if (!descriptor.script && descriptor.scriptSetup) {
|
|||
|
const res = compilerSfc.compileScript(descriptor, { id });
|
|||
|
const { name, lang } = res.attrs;
|
|||
|
str().appendLeft(0, `<script lang="${lang}">
|
|||
|
import { defineComponent } from 'vue'
|
|||
|
export default defineComponent({
|
|||
|
name: "${name}"
|
|||
|
})
|
|||
|
<\/script>`);
|
|||
|
return {
|
|||
|
map: str().generateMap(),
|
|||
|
code: str().toString(),
|
|||
|
};
|
|||
|
}
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
function base() {
|
|||
|
return {
|
|||
|
name: "vite-cool-base",
|
|||
|
enforce: "pre",
|
|||
|
configureServer(server) {
|
|||
|
server.middlewares.use(async (req, res, next) => {
|
|||
|
function done(data) {
|
|||
|
res.writeHead(200, { "Content-Type": "text/html;charset=UTF-8" });
|
|||
|
res.end(JSON.stringify(data));
|
|||
|
}
|
|||
|
if (req.originalUrl?.includes("__cool")) {
|
|||
|
const body = await parseJson(req);
|
|||
|
switch (req.url) {
|
|||
|
// 创建文件
|
|||
|
case "/__cool_createFile":
|
|||
|
await createFile(body);
|
|||
|
break;
|
|||
|
// 创建 eps 文件
|
|||
|
case "/__cool_eps":
|
|||
|
await createEps();
|
|||
|
break;
|
|||
|
// 更新插件
|
|||
|
case "/__cool_updatePlugin":
|
|||
|
await updatePlugin(body);
|
|||
|
break;
|
|||
|
// 设置代理
|
|||
|
case "/__cool_updateProxy":
|
|||
|
await updateProxy(body);
|
|||
|
break;
|
|||
|
default:
|
|||
|
return done({
|
|||
|
code: 1001,
|
|||
|
message: "Unknown request",
|
|||
|
});
|
|||
|
}
|
|||
|
done({
|
|||
|
code: 1000,
|
|||
|
});
|
|||
|
}
|
|||
|
else {
|
|||
|
next();
|
|||
|
}
|
|||
|
});
|
|||
|
},
|
|||
|
transform(code, id) {
|
|||
|
if (config.nameTag) {
|
|||
|
return createTag(code, id);
|
|||
|
}
|
|||
|
return code;
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
function demo(enable) {
|
|||
|
const virtualModuleIds = ["virtual:demo"];
|
|||
|
return {
|
|||
|
name: "vite-cool-demo",
|
|||
|
enforce: "pre",
|
|||
|
resolveId(id) {
|
|||
|
if (virtualModuleIds.includes(id)) {
|
|||
|
return "\0" + id;
|
|||
|
}
|
|||
|
},
|
|||
|
async load(id) {
|
|||
|
if (id === "\0virtual:demo") {
|
|||
|
const demo = {};
|
|||
|
if (enable) {
|
|||
|
const files = await glob.glob(rootDir("./src/modules/demo/views/crud/components") + "/**", {
|
|||
|
stat: true,
|
|||
|
withFileTypes: true,
|
|||
|
});
|
|||
|
for (const file of files) {
|
|||
|
if (file.isFile()) {
|
|||
|
const p = path.join(file.path, file.name);
|
|||
|
demo[p
|
|||
|
.replace(/\\/g, "/")
|
|||
|
.split("src/modules/demo/views/crud/components/")[1]] = fs.readFileSync(p, "utf-8");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return `
|
|||
|
export const demo = ${JSON.stringify(demo)};
|
|||
|
`;
|
|||
|
}
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
async function createCtx() {
|
|||
|
let ctx = {
|
|||
|
serviceLang: "Node",
|
|||
|
};
|
|||
|
if (config.type == "app" || config.type == "uniapp-x") {
|
|||
|
const manifest = readFile(rootDir("manifest.json"), true);
|
|||
|
// 文件路径
|
|||
|
const ctxPath = rootDir("pages.json");
|
|||
|
// 页面配置
|
|||
|
ctx = readFile(ctxPath, true);
|
|||
|
// 原数据,做更新比较用
|
|||
|
const ctxData = lodash.cloneDeep(ctx);
|
|||
|
// 删除临时页面
|
|||
|
ctx.pages = ctx.pages?.filter((e) => !e.isTemp);
|
|||
|
ctx.subPackages = ctx.subPackages?.filter((e) => !e.isTemp);
|
|||
|
// 加载 uni_modules 配置文件
|
|||
|
const files = await glob.glob(rootDir("uni_modules") + "/**/pages_init.json", {
|
|||
|
stat: true,
|
|||
|
withFileTypes: true,
|
|||
|
});
|
|||
|
for (const file of files) {
|
|||
|
if (file.isFile()) {
|
|||
|
const { pages = [], subPackages = [] } = readFile(path.join(file.path, file.name), true);
|
|||
|
// 合并到 pages 中
|
|||
|
[...pages, ...subPackages].forEach((e) => {
|
|||
|
e.isTemp = true;
|
|||
|
const isSub = !!e.root;
|
|||
|
const d = isSub
|
|||
|
? ctx.subPackages?.find((a) => a.root == e.root)
|
|||
|
: ctx.pages?.find((a) => a.path == e.path);
|
|||
|
if (d) {
|
|||
|
lodash.assign(d, e);
|
|||
|
}
|
|||
|
else {
|
|||
|
if (isSub) {
|
|||
|
ctx.subPackages?.unshift(e);
|
|||
|
}
|
|||
|
else {
|
|||
|
ctx.pages?.unshift(e);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
// 排序后检测,避免加载顺序问题
|
|||
|
function order(d) {
|
|||
|
return {
|
|||
|
pages: lodash.orderBy(d.pages, "path"),
|
|||
|
subPackages: lodash.orderBy(d.subPackages, "root"),
|
|||
|
};
|
|||
|
}
|
|||
|
// 是否需要更新 pages.json
|
|||
|
if (!util.isDeepStrictEqual(order(ctxData), order(ctx))) {
|
|||
|
console.log("[cool-ctx] pages updated");
|
|||
|
writeFile(ctxPath, JSON.stringify(ctx, null, 4));
|
|||
|
}
|
|||
|
// appid
|
|||
|
ctx.appid = manifest.appid;
|
|||
|
}
|
|||
|
if (config.type == "admin") {
|
|||
|
const list = fs.readdirSync(rootDir("./src/modules"));
|
|||
|
ctx.modules = list.filter((e) => !e.includes("."));
|
|||
|
await axios
|
|||
|
.get(config.reqUrl + "/admin/base/comm/program", {
|
|||
|
timeout: 5000,
|
|||
|
})
|
|||
|
.then((res) => {
|
|||
|
const { code, data, message } = res.data;
|
|||
|
if (code === 1000) {
|
|||
|
ctx.serviceLang = data || "Node";
|
|||
|
}
|
|||
|
else {
|
|||
|
error(`[cool-ctx] ${message}`);
|
|||
|
}
|
|||
|
})
|
|||
|
.catch((err) => {
|
|||
|
// console.error(['[cool-ctx] ', err.message])
|
|||
|
});
|
|||
|
}
|
|||
|
return ctx;
|
|||
|
}
|
|||
|
|
|||
|
let svgIcons = [];
|
|||
|
function findSvg(dir) {
|
|||
|
const arr = [];
|
|||
|
const dirs = fs.readdirSync(dir, {
|
|||
|
withFileTypes: true,
|
|||
|
});
|
|||
|
// 获取当前目录的模块名
|
|||
|
const moduleName = dir.match(/[/\\](?:src[/\\](?:plugins|modules)[/\\])([^/\\]+)/)?.[1] || "";
|
|||
|
for (const d of dirs) {
|
|||
|
if (d.isDirectory()) {
|
|||
|
arr.push(...findSvg(dir + d.name + "/"));
|
|||
|
}
|
|||
|
else {
|
|||
|
if (path.extname(d.name) == ".svg") {
|
|||
|
const baseName = path.basename(d.name, ".svg");
|
|||
|
// 判断是否需要跳过拼接模块名
|
|||
|
let shouldSkip = config.svg.skipNames?.includes(moduleName);
|
|||
|
// 跳过包含icon-
|
|||
|
if (baseName.includes("icon-")) {
|
|||
|
shouldSkip = true;
|
|||
|
}
|
|||
|
const iconName = shouldSkip ? baseName : `${moduleName}-${baseName}`;
|
|||
|
svgIcons.push(iconName);
|
|||
|
const svg = fs.readFileSync(dir + d.name)
|
|||
|
.toString()
|
|||
|
.replace(/(\r)|(\n)/g, "")
|
|||
|
.replace(/<svg([^>+].*?)>/, (_, $2) => {
|
|||
|
let width = 0;
|
|||
|
let height = 0;
|
|||
|
let content = $2.replace(/(width|height)="([^>+].*?)"/g, (_, s2, s3) => {
|
|||
|
if (s2 === "width") {
|
|||
|
width = s3;
|
|||
|
}
|
|||
|
else if (s2 === "height") {
|
|||
|
height = s3;
|
|||
|
}
|
|||
|
return "";
|
|||
|
});
|
|||
|
if (!/(viewBox="[^>+].*?")/g.test($2)) {
|
|||
|
content += `viewBox="0 0 ${width} ${height}"`;
|
|||
|
}
|
|||
|
return `<symbol id="icon-${iconName}" ${content}>`;
|
|||
|
})
|
|||
|
.replace("</svg>", "</symbol>");
|
|||
|
arr.push(svg);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return arr;
|
|||
|
}
|
|||
|
function compilerSvg() {
|
|||
|
svgIcons = [];
|
|||
|
return findSvg(rootDir("./src/"))
|
|||
|
.map((e) => {
|
|||
|
return svgo.optimize(e)?.data || e;
|
|||
|
})
|
|||
|
.join("");
|
|||
|
}
|
|||
|
async function createSvg() {
|
|||
|
const html = compilerSvg();
|
|||
|
const code = `
|
|||
|
if (typeof window !== 'undefined') {
|
|||
|
function loadSvg() {
|
|||
|
const svgDom = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|||
|
svgDom.style.position = 'absolute';
|
|||
|
svgDom.style.width = '0';
|
|||
|
svgDom.style.height = '0';
|
|||
|
svgDom.setAttribute('xmlns','http://www.w3.org/2000/svg');
|
|||
|
svgDom.setAttribute('xmlns:link','http://www.w3.org/1999/xlink');
|
|||
|
svgDom.innerHTML = '${html}';
|
|||
|
document.body.insertBefore(svgDom, document.body.firstChild);
|
|||
|
}
|
|||
|
|
|||
|
loadSvg();
|
|||
|
}
|
|||
|
`;
|
|||
|
return { code, svgIcons };
|
|||
|
}
|
|||
|
|
|||
|
async function virtual() {
|
|||
|
const virtualModuleIds = [
|
|||
|
"virtual:eps",
|
|||
|
"virtual:ctx",
|
|||
|
"virtual:svg-register",
|
|||
|
"virtual:svg-icons",
|
|||
|
];
|
|||
|
createEps();
|
|||
|
return {
|
|||
|
name: "vite-cool-virtual",
|
|||
|
enforce: "pre",
|
|||
|
configureServer(server) {
|
|||
|
server.middlewares.use(async (req, res, next) => {
|
|||
|
// 页面刷新时触发
|
|||
|
if (req.url == "/@vite/client") {
|
|||
|
// 重新加载虚拟模块
|
|||
|
virtualModuleIds.forEach((vm) => {
|
|||
|
const mod = server.moduleGraph.getModuleById(`\0${vm}`);
|
|||
|
if (mod) {
|
|||
|
server.moduleGraph.invalidateModule(mod);
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
next();
|
|||
|
});
|
|||
|
},
|
|||
|
handleHotUpdate({ file, server }) {
|
|||
|
// 文件修改时触发
|
|||
|
if (!["pages.json", "dist", "build/cool", "eps.json", "eps.d.ts"].some((e) => file.includes(e))) {
|
|||
|
createCtx();
|
|||
|
createEps().then((data) => {
|
|||
|
if (data.isUpdate) {
|
|||
|
// 通知客户端刷新
|
|||
|
(server.hot || server.ws).send({
|
|||
|
type: "custom",
|
|||
|
event: "eps-update",
|
|||
|
data,
|
|||
|
});
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
},
|
|||
|
resolveId(id) {
|
|||
|
if (virtualModuleIds.includes(id)) {
|
|||
|
return "\0" + id;
|
|||
|
}
|
|||
|
},
|
|||
|
async load(id) {
|
|||
|
if (id === "\0virtual:eps") {
|
|||
|
const eps = await createEps();
|
|||
|
return `
|
|||
|
export const eps = ${JSON.stringify(eps)}
|
|||
|
`;
|
|||
|
}
|
|||
|
if (id === "\0virtual:ctx") {
|
|||
|
const ctx = await createCtx();
|
|||
|
return `
|
|||
|
export const ctx = ${JSON.stringify(ctx)}
|
|||
|
`;
|
|||
|
}
|
|||
|
if (id == "\0virtual:svg-register") {
|
|||
|
const { code } = await createSvg();
|
|||
|
return code;
|
|||
|
}
|
|||
|
if (id == "\0virtual:svg-icons") {
|
|||
|
const { svgIcons } = await createSvg();
|
|||
|
return `
|
|||
|
export const svgIcons = ${JSON.stringify(svgIcons)}
|
|||
|
`;
|
|||
|
}
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 特殊字符映射表
|
|||
|
*/
|
|||
|
const SAFE_CHAR_MAP = {
|
|||
|
"[": "-bracket-start-",
|
|||
|
"]": "-bracket-end-",
|
|||
|
"(": "-paren-start-",
|
|||
|
")": "-paren-end-",
|
|||
|
"{": "-brace-start-",
|
|||
|
"}": "-brace-end-",
|
|||
|
$: "-dollar-",
|
|||
|
"#": "-hash-",
|
|||
|
"!": "-important-",
|
|||
|
"/": "-slash-",
|
|||
|
":": "-colon-",
|
|||
|
};
|
|||
|
/**
|
|||
|
* 特殊字符映射表(国际化)
|
|||
|
*/
|
|||
|
const SAFE_CHAR_MAP_LOCALE = {
|
|||
|
"[": "-bracket-start-",
|
|||
|
"]": "-bracket-end-",
|
|||
|
"(": "-paren-start-",
|
|||
|
")": "-paren-end-",
|
|||
|
"{": "-brace-start-",
|
|||
|
"}": "-brace-end-",
|
|||
|
$: "-dollar-",
|
|||
|
"#": "-hash-",
|
|||
|
"!": "-important-",
|
|||
|
"/": "-slash-",
|
|||
|
":": "-colon-",
|
|||
|
" ": "-space-",
|
|||
|
"<": "-lt-",
|
|||
|
">": "-gt-",
|
|||
|
"&": "-amp-",
|
|||
|
"|": "-pipe-",
|
|||
|
"^": "-caret-",
|
|||
|
"~": "-tilde-",
|
|||
|
"`": "-backtick-",
|
|||
|
"'": "-single-quote-",
|
|||
|
".": "-dot-",
|
|||
|
"?": "-question-",
|
|||
|
"*": "-star-",
|
|||
|
"+": "-plus-",
|
|||
|
"-": "-dash-",
|
|||
|
_: "-underscore-",
|
|||
|
"=": "-equal-",
|
|||
|
"%": "-percent-",
|
|||
|
"@": "-at-",
|
|||
|
};
|
|||
|
|
|||
|
// @ts-ignore
|
|||
|
/**
|
|||
|
* 转换类名中的特殊字符为安全字符
|
|||
|
*/
|
|||
|
function toSafeClass(className) {
|
|||
|
if (className.includes(":host")) {
|
|||
|
return className;
|
|||
|
}
|
|||
|
// 如果是表达式,则不进行转换
|
|||
|
if (["!=", "!==", "?", ":", "="].includes(className)) {
|
|||
|
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) {
|
|||
|
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) {
|
|||
|
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() {
|
|||
|
return {
|
|||
|
name: "vite-cool-uniappx-postcss",
|
|||
|
enforce: "pre",
|
|||
|
config() {
|
|||
|
return {
|
|||
|
css: {
|
|||
|
postcss: {
|
|||
|
plugins: [
|
|||
|
{
|
|||
|
postcssPlugin: "vite-cool-uniappx-class-mapping",
|
|||
|
prepare() {
|
|||
|
return {
|
|||
|
// 处理选择器规则
|
|||
|
Rule(rule) {
|
|||
|
if (rule.selector.includes("uni-") ||
|
|||
|
[".button-hover"].some((e) => rule.selector.includes(e))) {
|
|||
|
return;
|
|||
|
}
|
|||
|
// 转换选择器为安全的类名格式
|
|||
|
rule.selector = toSafeClass(rule.selector.replace(/\\/g, ""));
|
|||
|
},
|
|||
|
// 处理声明规则
|
|||
|
Declaration(decl) {
|
|||
|
const className = decl.parent.selector || "";
|
|||
|
if (!decl.parent._twValues) {
|
|||
|
decl.parent._twValues = {};
|
|||
|
}
|
|||
|
// 处理 Tailwind 自定义属性
|
|||
|
if (decl.prop.includes("--tw-")) {
|
|||
|
decl.parent._twValues[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" &&
|
|||
|
className.includes("text-")) {
|
|||
|
decl.prop = "font-size";
|
|||
|
}
|
|||
|
// 删除不支持的属性
|
|||
|
if (["filter"].includes(decl.prop)) {
|
|||
|
decl.remove();
|
|||
|
return;
|
|||
|
}
|
|||
|
// 处理 flex-1
|
|||
|
if (decl.prop == "flex") {
|
|||
|
if (decl.value.startsWith("1")) {
|
|||
|
decl.value = "1";
|
|||
|
}
|
|||
|
}
|
|||
|
// 处理 vertical-align 属性
|
|||
|
if (decl.prop == "vertical-align") {
|
|||
|
decl.remove();
|
|||
|
}
|
|||
|
// 处理 visibility 属性
|
|||
|
if (decl.prop == "visibility") {
|
|||
|
decl.remove();
|
|||
|
}
|
|||
|
// 处理 sticky 属性
|
|||
|
if (className == ".sticky") {
|
|||
|
if (decl.prop == "position" ||
|
|||
|
decl.value == "sticky") {
|
|||
|
decl.remove();
|
|||
|
}
|
|||
|
}
|
|||
|
// 解析声明值
|
|||
|
const parsed = valueParser(decl.value);
|
|||
|
let hasChanges = false;
|
|||
|
// 遍历并处理声明值中的节点
|
|||
|
parsed.walk((node) => {
|
|||
|
// 处理单位转换(rem -> rpx)
|
|||
|
if (node.type === "word") {
|
|||
|
const unit = valueParser.unit(node.value);
|
|||
|
if (typeof unit != "boolean") {
|
|||
|
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-")) {
|
|||
|
if (decl.parent._twValues) {
|
|||
|
node.type = "word";
|
|||
|
node.value =
|
|||
|
decl.parent._twValues[twKey] ||
|
|||
|
"none";
|
|||
|
hasChanges = true;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
// 更新声明值
|
|||
|
if (hasChanges) {
|
|||
|
decl.value = parsed.toString();
|
|||
|
}
|
|||
|
// 移除 Tailwind 生成的无效 none 变换
|
|||
|
const nones = [
|
|||
|
"translate(none, none)",
|
|||
|
"rotate(none)",
|
|||
|
"skewX(none)",
|
|||
|
"skewY(none)",
|
|||
|
"scaleX(none)",
|
|||
|
"scaleY(none)",
|
|||
|
];
|
|||
|
if (decl.value) {
|
|||
|
nones.forEach((noneStr) => {
|
|||
|
decl.value = decl.value.replace(noneStr, "");
|
|||
|
if (!decl.value || !decl.value.trim()) {
|
|||
|
decl.value = "none";
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
},
|
|||
|
};
|
|||
|
},
|
|||
|
},
|
|||
|
],
|
|||
|
},
|
|||
|
},
|
|||
|
};
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
/**
|
|||
|
* uvue class 转换插件
|
|||
|
*/
|
|||
|
function transformPlugin() {
|
|||
|
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) => {
|
|||
|
if (node.startsWith("<!--")) {
|
|||
|
return;
|
|||
|
}
|
|||
|
let _node = node;
|
|||
|
// 兼容 <input /> 标签
|
|||
|
if (_node.startsWith("<input")) {
|
|||
|
_node = _node.replace("/>", "</input>");
|
|||
|
}
|
|||
|
// 为 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.replaceAll(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.replaceAll(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.replaceAll(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.replaceAll(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=" "', "");
|
|||
|
return {
|
|||
|
code: modifiedCode,
|
|||
|
map: { mappings: "" },
|
|||
|
};
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
else {
|
|||
|
return null;
|
|||
|
}
|
|||
|
},
|
|||
|
};
|
|||
|
}
|
|||
|
/**
|
|||
|
* Tailwind 类名转换插件
|
|||
|
*/
|
|||
|
function tailwindPlugin() {
|
|||
|
return [postcssPlugin(), transformPlugin()];
|
|||
|
}
|
|||
|
|
|||
|
function codePlugin() {
|
|||
|
return [
|
|||
|
{
|
|||
|
name: "vite-cool-uniappx-code-pre",
|
|||
|
enforce: "pre",
|
|||
|
async transform(code, id) {
|
|||
|
if (id.includes("/cool/ctx/index.ts")) {
|
|||
|
const ctx = await createCtx();
|
|||
|
const theme = await readFile(rootDir("theme.json"), true);
|
|||
|
ctx["SAFE_CHAR_MAP_LOCALE"] = [];
|
|||
|
for (const i in SAFE_CHAR_MAP_LOCALE) {
|
|||
|
ctx["SAFE_CHAR_MAP_LOCALE"].push([i, SAFE_CHAR_MAP_LOCALE[i]]);
|
|||
|
}
|
|||
|
ctx["theme"] = theme;
|
|||
|
code = code.replace("const ctx = {}", `const ctx = ${JSON.stringify(ctx, null, 4)}`);
|
|||
|
}
|
|||
|
if (id.includes("/cool/service/index.ts")) {
|
|||
|
const eps = await createEps();
|
|||
|
if (eps.serviceCode) {
|
|||
|
const { content, types } = eps.serviceCode;
|
|||
|
const typeCode = `import type { ${lodash.uniq(types).join(", ")} } from '../types';`;
|
|||
|
code =
|
|||
|
typeCode +
|
|||
|
"\n\n" +
|
|||
|
code.replace("const service = {}", `const service = ${content}`);
|
|||
|
}
|
|||
|
}
|
|||
|
if (id.endsWith(".json")) {
|
|||
|
const d = JSON.parse(code);
|
|||
|
for (let i in d) {
|
|||
|
let k = i;
|
|||
|
for (let j in SAFE_CHAR_MAP_LOCALE) {
|
|||
|
k = k.replaceAll(j, SAFE_CHAR_MAP_LOCALE[j]);
|
|||
|
}
|
|||
|
if (k != i) {
|
|||
|
d[k] = d[i];
|
|||
|
delete d[i];
|
|||
|
}
|
|||
|
}
|
|||
|
code = JSON.stringify(d);
|
|||
|
}
|
|||
|
return {
|
|||
|
code,
|
|||
|
map: { mappings: "" },
|
|||
|
};
|
|||
|
},
|
|||
|
},
|
|||
|
{
|
|||
|
name: "vite-cool-uniappx-code",
|
|||
|
transform(code, id) {
|
|||
|
if (id.endsWith(".json")) {
|
|||
|
return {
|
|||
|
code: code.replace("new UTSJSONObject", ""),
|
|||
|
map: { mappings: "" },
|
|||
|
};
|
|||
|
}
|
|||
|
},
|
|||
|
},
|
|||
|
];
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* uniappX 入口,自动注入 Tailwind 类名转换插件
|
|||
|
* @param options 配置项
|
|||
|
* @returns Vite 插件数组
|
|||
|
*/
|
|||
|
async function uniappX() {
|
|||
|
const plugins = [];
|
|||
|
if (config.type == "uniapp-x") {
|
|||
|
plugins.push(...codePlugin());
|
|||
|
if (config.tailwind.enable) {
|
|||
|
plugins.push(...tailwindPlugin());
|
|||
|
}
|
|||
|
}
|
|||
|
return plugins;
|
|||
|
}
|
|||
|
|
|||
|
function cool(options) {
|
|||
|
// 应用类型,admin | app
|
|||
|
config.type = options.type;
|
|||
|
// 请求地址
|
|||
|
config.reqUrl = getProxyTarget(options.proxy);
|
|||
|
// 是否开启名称标签
|
|||
|
config.nameTag = options.nameTag ?? true;
|
|||
|
// svg
|
|||
|
if (options.svg) {
|
|||
|
lodash.assign(config.svg, options.svg);
|
|||
|
}
|
|||
|
// Eps
|
|||
|
if (options.eps) {
|
|||
|
const { dist, mapping, api, enable = true } = options.eps;
|
|||
|
// 是否开启
|
|||
|
config.eps.enable = enable;
|
|||
|
// 类型
|
|||
|
if (api) {
|
|||
|
config.eps.api = api;
|
|||
|
}
|
|||
|
// 输出目录
|
|||
|
if (dist) {
|
|||
|
config.eps.dist = dist;
|
|||
|
}
|
|||
|
// 匹配规则
|
|||
|
if (mapping) {
|
|||
|
lodash.merge(config.eps.mapping, mapping);
|
|||
|
}
|
|||
|
}
|
|||
|
// tailwind
|
|||
|
if (options.tailwind) {
|
|||
|
lodash.assign(config.tailwind, options.tailwind);
|
|||
|
}
|
|||
|
return [base(), virtual(), uniappX(), demo(options.demo)];
|
|||
|
}
|
|||
|
|
|||
|
exports.cool = cool;
|
|||
|
|
|||
|
}));
|