2025-07-07 00:03:25 +08:00

795 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createDir, error, firstUpperCase, readFile, rootDir, toCamel } from "../utils";
import { join } from "path";
import axios from "axios";
import { compact, isEmpty, last, uniqBy, values } from "lodash";
import { createWriteStream } from "fs";
import prettier from "prettier";
import { config } from "../config";
import type { Eps } from "../../types";
import { flatten } from "../uniapp-x/flatten";
import { interfaceToType } from "../uniapp-x/utils";
// 全局 service 对象,用于存储服务结构
const service = {};
// eps 实体列表
let list: Eps.Entity[] = [];
/**
* 获取 eps 请求地址
* @returns {string} eps url
*/
function getEpsUrl(): string {
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?: string): string {
return join(
config.type == "admin" ? config.eps.dist : rootDir(config.eps.dist),
filename || "",
);
}
/**
* 获取对象方法名(排除 namespace、permission 字段)
* @param v 对象
* @returns {string[]} 方法名数组
*/
function getNames(v: any): string[] {
return Object.keys(v).filter((e) => !["namespace", "permission"].includes(e));
}
/**
* 获取字段类型
*/
function getType({ propertyName, type }: any) {
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: string) {
return (name || "").replace(/[:,\s,\/,-]/g, "");
}
/**
* 检查方法名是否合法(不包含特殊字符)
*/
function checkName(name: string) {
return name && !["{", "}", ":"].some((e) => name.includes(e));
}
/**
* 不支持 uniapp-x 平台显示
*/
function noUniappX(text: string, defaultText: string = "") {
if (config.type == "uniapp-x") {
return defaultText;
} else {
return text;
}
}
/**
* 查找字段
* @param sources 字段 source 数组
* @param item eps 实体
* @returns {Eps.Column[]} 字段数组
*/
function findColumns(sources: string[], item: Eps.Entity): Eps.Column[] {
const columns = [item.columns, item.pageColumns].flat().filter(Boolean);
return (sources || [])
.map((e) => columns.find((c) => c.source == e))
.filter(Boolean) as Eps.Column[];
}
/**
* 使用 prettier 格式化 TypeScript 代码
* @param text 代码文本
* @returns {Promise<string|null>} 格式化后的代码
*/
async function formatCode(text: string): Promise<string | null> {
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 (!isEmpty(data) && data) {
list = 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),
};
}
});
list = list.filter((e) => e.prefix.startsWith("/app"));
}
/**
* 创建 eps.json 文件
* @returns {boolean} 是否有更新
*/
function createJson(): boolean {
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) {
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 }: { list: Eps.Entity[]; service: any }) {
/**
* 创建 Entity 接口定义
*/
function createEntity() {
const ignore: string[] = [];
let t0 = "";
for (const item of list) {
if (!checkName(item.name)) continue;
let t = `interface ${formatName(item.name)} {`;
// 合并 columns 和 pageColumns去重
const columns: Eps.Column[] = uniqBy(
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: any, k?: string) {
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: string[] = [];
item.api.forEach((a) => {
// 方法名
const n = toCamel(formatName(a.name || last(a.path.split("/"))!));
// 检查方法名
if (!checkName(n)) return;
if (n) {
// 参数类型
let q: string[] = [];
// 参数列表
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 (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) {
// 创建 eps 描述文件
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: any, i: number) {
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(): { content: string; types: string[] } {
const types: string[] = [];
let chain = "";
/**
* 递归处理 service 树,生成接口代码
* @param d 当前节点
* @param k 前缀
*/
function deep(d: any, k?: string) {
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 || last(a.path.split("/"))!));
// 检查方法名
if (!checkName(n)) return;
if (n) {
// 参数类型
let q: string[] = [];
// 参数列表
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 (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(): Promise<string> {
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 as { code: number; data: any[] };
if (code === 1000) {
let v = "string";
if (!isEmpty(data)) {
v = data.map((e) => `"${e.key}"`).join(" | ");
}
return `type DictKey = ${v}`;
}
})
.catch(() => {
error(`[cool-eps] Error${url}`);
});
return text || "";
}
/**
* 主入口:创建 eps 相关文件和 service
*/
export 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: [],
};
}
}