wangzhibo
6 天以前 7bd866831780abcf5a59c4cbb7d1be456eea7c99
packages/vite-plugin/src/eps/index.ts
@@ -1,24 +1,17 @@
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 { isEmpty, last, 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 {
// 获取请求地址
function getEpsUrl() {
   let url = config.eps.api;
   if (!url) {
@@ -27,9 +20,9 @@
   switch (url) {
      case "app":
      case "uniapp-x":
         url = "/app/base/comm/eps";
         break;
      case "admin":
         url = "/admin/base/open/eps";
         break;
@@ -38,122 +31,57 @@
   return url;
}
/**
 * 获取 eps 路径
 * @param filename 文件名
 * @returns {string} 完整路径
 */
function getEpsPath(filename?: string): string {
// 获取路径
function getEpsPath(filename?: 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[] {
// 获取方法名
function getNames(v: any) {
   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[] {
// 找字段
function findColumns(sources: string[], item: Eps.Entity) {
   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((err) => {
         console.log(err);
         error(`[cool-eps] File format error, please try again`);
         return null;
      });
// 格式化代码
async function formatCode(text: string) {
   return prettier.format(text, {
      parser: "typescript",
      useTabs: true,
      tabWidth: 4,
      endOfLine: "lf",
      semi: true,
      singleQuote: false,
      printWidth: 100,
      trailingComma: "none",
   });
}
/**
 * 获取 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();
@@ -166,11 +94,18 @@
         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.namespace) {
         e.namespace = "";
      }
      if (!e.api) {
         e.api = [];
      }
      if (!e.columns) {
         e.columns = [];
      }
      if (!e.search) {
         e.search = {
            fieldEq: findColumns(e.pageQueryOp?.fieldEq, e),
@@ -179,40 +114,29 @@
         };
      }
   });
   if (config.type == "uniapp-x" || config.type == "app") {
      list = list.filter((e) => e.prefix.startsWith("/app") || e.prefix.startsWith("/admin"));
   }
}
/**
 * 创建 eps.json 文件
 * @returns {boolean} 是否有更新
 */
function createJson(): boolean {
   let data: any[] = [];
// 创建 json 文件
function createJson() {
   const arr = list.map((e) => {
      return {
         prefix: e.prefix,
         name: e.name || "",
         api: e.api.map((e) => {
            return {
               name: e.name,
               method: e.method,
               path: e.path,
            };
         }),
         search: e.search,
      };
   });
   if (config.type != "uniapp-x") {
      data = 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,
         };
      });
   } else {
      data = list;
   }
   const content = JSON.stringify(data);
   const content = JSON.stringify(arr);
   const local_content = readFile(getEpsPath("eps.json"));
   // 判断是否需要更新
   // 是否需要更新
   const isUpdate = content != local_content;
   if (isUpdate) {
@@ -224,32 +148,51 @@
   return isUpdate;
}
/**
 * 创建 eps 类型描述文件(d.ts/ts)
 * @param param0 list: eps实体列表, service: service对象
 */
// 创建描述文件
async function createDescribe({ list, service }: { list: Eps.Entity[]; service: any }) {
   /**
    * 创建 Entity 接口定义
    */
   // 获取类型
   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, "");
   }
   // 创建 Entity
   function createEntity() {
      const ignore: string[] = [];
      let t0 = "";
      for (const item of list) {
         if (!checkName(item.name)) continue;
         if (formatName(item.name) == "BusinessInterface") {
            console.log(111);
         }
         if (!item.name) continue;
         let t = `interface ${formatName(item.name)} {`;
         // 合并 columns 和 pageColumns,去重
         const columns: Eps.Column[] = uniqBy(
            compact([...(item.columns || []), ...(item.pageColumns || [])]),
            "source",
         );
         // 合并多个列
         const columns: Eps.Column[] = [];
         [item.columns, item.pageColumns]
            .flat()
            .filter(Boolean)
            .forEach((e) => {
               const d = columns.find((c) => c.source == e.source);
               if (!d) {
                  columns.push(e);
               }
            });
         for (const col of columns || []) {
            t += `
@@ -259,7 +202,7 @@
               ${col.propertyName}?: ${getType({
                  propertyName: col.propertyName,
                  type: col.type,
               })};
               })}
            `;
         }
@@ -280,34 +223,23 @@
      return t0;
   }
   /**
    * 创建 Controller 接口定义
    */
   async function createController() {
   // 创建 Service
   async function createDts() {
      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} {`;
                  // 插入方法
@@ -317,10 +249,9 @@
                     item.api.forEach((a) => {
                        // 方法名
                        const n = toCamel(formatName(a.name || last(a.path.split("/"))!));
                        // 检查方法名
                        if (!checkName(n)) return;
                        const n = toCamel(
                           formatName(a.name || last(a.path.split("/")) || ""),
                        );
                        if (n) {
                           // 参数类型
@@ -334,15 +265,14 @@
                                 q.push(`\n/** ${p.description}  */\n`);
                              }
                              // 检查参数名
                              if (!checkName(p.name)) {
                              if (p.name.includes(":")) {
                                 return false;
                              }
                              const a = `${p.name}${p.required ? "" : "?"}`;
                              const b = `${p.schema.type || "string"}`;
                              q.push(`${a}: ${b};`);
                              q.push(`${a}: ${b},`);
                           });
                           if (isEmpty(q)) {
@@ -360,43 +290,35 @@
                           switch (a.path) {
                              case "/page":
                                 res = `${name}PageResponse`;
                                 pageResponse += `
                                    interface ${name}PageResponse {
                                       pagination: PagePagination;
                                       list: ${en}[];
                                    }
                                 `;
                                 res = `
                                 {
                                    pagination: { size: number; page: number; total: number; [key: string]: any };
                                    list: ${en} [];
                                    [key: string]: any;
                                 }
                              `;
                                 break;
                              case "/list":
                                 res = `${en} []`;
                                 break;
                              case "/info":
                                 res = en;
                                 break;
                              default:
                                 res = "any";
                                 break;
                           }
                           // 方法描述
                           if (config.type == "uniapp-x") {
                              t += `
                                 /**
                                  * ${a.summary || n}
                                  */
                                 ${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<any>;
                              `;
                           } else {
                              t += `
                                 /**
                                  * ${a.summary || n}
                                  */
                                 ${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<${res}>;
                              `;
                           }
                           // 描述
                           t += `
                              /**
                               * ${a.summary || n}
                               */
                              ${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<${res}>;
                           `;
                           if (!permission.includes(n)) {
                              permission.push(n);
@@ -405,25 +327,24 @@
                     });
                     // 权限标识
                     t += noUniappX(`
                     t += `
                        /**
                         * 权限标识
                         */
                        permission: { ${permission.map((e) => `${e}: string;`).join("\n")} };
                     `);
                     `;
                     // 权限状态
                     t += noUniappX(`
                     t += `
                        /**
                         * 权限状态
                         */
                        _permission: { ${permission.map((e) => `${e}: boolean;`).join("\n")} };
                     `);
                     `;
                     // 请求
                     t += noUniappX(`
                        request: Request;
                     `);
                     t += `
                        request: Service['request']
                     `;
                  }
                  t += "}\n\n";
@@ -434,99 +355,67 @@
            } else {
               chain += `${formatName(i)}: {`;
               deep(d[i], name);
               chain += "};";
               chain += "},";
            }
         }
      }
      // 遍历 service 树
      // 遍历
      deep(service);
      return `
         type json = any;
         ${await createDict()}
         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>;")}
         type Service = {
            ${noUniappX("request: Request;")}
            /**
             * 基础请求
             */
            request(options?: {
               url: string;
               method?: "POST" | "GET" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
               data?: any;
               params?: any;
               headers?: {
                  authorization?: string;
                  [key: string]: any;
               },
               timeout?: number;
               proxy?: boolean;
               [key: string]: any;
            }): Promise<any>;
            ${chain}
         }
         ${await createDict()}
      `;
   }
   // 组装文件内容
   let text = `
      ${createEntity()}
      ${await createController()}
   // 文件内容
   const text = `
      declare namespace Eps {
         ${createEntity()}
         ${await createDts()}
      }
   `;
   // 文件名
   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));
   const local_content = readFile(getEpsPath("eps.d.ts"));
   // 是否需要更新
   if (content && content != local_content && list.length > 0) {
   if (content != local_content) {
      // 创建 eps 描述文件
      createWriteStream(getEpsPath(name), {
      createWriteStream(getEpsPath("eps.d.ts"), {
         flags: "w",
      }).write(content);
   }
}
/**
 * 构建 service 对象树
 */
// 创建 service
function createService() {
   // 路径第一层作为 id 标识
   const id = getEpsUrl().split("/")[1];
@@ -535,14 +424,10 @@
      // 请求地址
      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];
@@ -552,6 +437,7 @@
               if (!d[k]) {
                  d[k] = {};
               }
               deep(d[k], i + 1);
            } else {
               // 不存在则创建
@@ -580,6 +466,7 @@
               e.api.forEach((a) => {
                  // 方法名
                  const n = a.path.replace("/", "");
                  if (n && !/[-:]/g.test(n)) {
                     d[k][n] = a;
                  }
@@ -592,190 +479,50 @@
   });
}
/**
 * 创建 service 代码
 * @returns {string} service 代码
 */
function createServiceCode(): { content: string; types: string[] } {
   const types: string[] = [];
// 创建 dict
async function createDict() {
   const url = config.reqUrl + "/" + config.type + "/dict/info/types";
   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);
                        }
                        // 方法描述
                        t += `
                           /**
                            * ${a.summary || n}
                            */
                           ${n}(data?: any): Promise<any> {
                              return request({
                                 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
   return 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
 */
// 创建 eps
export async function createEps() {
   if (config.eps.enable) {
      // 获取 eps 数据
      // 获取数据
      await getData();
      // 构建 service 对象
      // 创建 service
      createService();
      const serviceCode = createServiceCode();
      // 创建 eps 目录
      // 创建目录
      createDir(getEpsPath(), true);
      // 创建 eps.json 文件
      // 创建 json 文件
      const isUpdate = createJson();
      // 创建类型描述文件
      // 创建描述文件
      createDescribe({ service, list });
      return {
         service,
         serviceCode,
         list,
         isUpdate,
      };