This commit is contained in:
icssoa 2025-05-29 19:35:59 +08:00
parent 400f4429db
commit 3f90cac389
14 changed files with 951 additions and 443 deletions

View File

@ -1,5 +1,6 @@
import type { Type } from "../types";
export declare const config: { export declare const config: {
type: string; type: Type;
reqUrl: string; reqUrl: string;
demo: boolean; demo: boolean;
nameTag: boolean; nameTag: boolean;

View File

@ -0,0 +1,7 @@
/**
* Service
* @param template - Service
* @returns Service
* @throws {Error} Service
*/
export declare function flatten(template: string): string;

View File

@ -1,10 +1,18 @@
import type { Eps } from "../../types"; import type { Eps } from "../../types";
/**
* eps service
*/
export declare function createEps(): Promise<{ export declare function createEps(): Promise<{
service: {}; service: {};
serviceCode: {
content: string;
types: string[];
};
list: Eps.Entity[]; list: Eps.Entity[];
isUpdate: boolean; isUpdate: boolean;
} | { } | {
service: {}; service: {};
list: never[]; list: never[];
serviceCode?: undefined;
isUpdate?: undefined; isUpdate?: undefined;
}>; }>;

View File

@ -1,2 +1,2 @@
import type { Config } from "../types"; import type { Config } from "../types";
export declare function cool(options: Config.Options): (import("vite").Plugin<any> | Promise<import("vite").Plugin<any>> | import("vite").Plugin<any>[])[]; export declare function cool(options: Config.Options): (import("vite").Plugin<any> | Promise<import("vite").Plugin<any>> | Promise<import("vite").Plugin<any>[]>)[];

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
/**
* Service
* @param template - Service
* @returns Service
* @throws {Error} Service
*/
export declare function flatten(template: string): string;

View File

@ -4,4 +4,4 @@ import type { Plugin } from "vite";
* @param options * @param options
* @returns Vite * @returns Vite
*/ */
export declare function uniappX(): Plugin<any>[]; export declare function uniappX(): Promise<Plugin<any>[]>;

View File

@ -22,3 +22,7 @@ export declare function addScriptContent(code: string, content: string): string;
* Tailwind * Tailwind
*/ */
export declare function isTailwindClass(className: string): boolean; export declare function isTailwindClass(className: string): boolean;
/**
* interface type
*/
export declare function interfaceToType(code: string): string;

View File

@ -1,5 +1,7 @@
import type { Type } from "../types";
export const config = { export const config = {
type: "admin", type: "admin" as Type,
reqUrl: "", reqUrl: "",
demo: false, demo: false,
nameTag: true, nameTag: true,
@ -35,6 +37,10 @@ export const config = {
type: "BigInt", type: "BigInt",
test: ["bigint"], test: ["bigint"],
}, },
{
type: "any",
test: ["json"],
},
], ],
}, },
svg: { svg: {

View File

@ -1,12 +1,13 @@
import { createDir, error, firstUpperCase, readFile, rootDir, toCamel } from "../utils"; import { createDir, error, firstUpperCase, readFile, rootDir, toCamel } from "../utils";
import { join } from "path"; import { join } from "path";
import axios from "axios"; import axios from "axios";
import { isEmpty, last, values } from "lodash"; import { compact, isEmpty, last, uniqBy, values } from "lodash";
import { createWriteStream } from "fs"; import { createWriteStream } from "fs";
import prettier from "prettier"; import prettier from "prettier";
import { config } from "../config"; import { config } from "../config";
import type { Eps } from "../../types"; import type { Eps } from "../../types";
import { flatten } from "./flatten"; import { flatten } from "../uniapp-x/flatten";
import { interfaceToType } from "../uniapp-x/utils";
// 全局 service 对象,用于存储服务结构 // 全局 service 对象,用于存储服务结构
const service = {}; const service = {};
@ -58,6 +59,47 @@ function getNames(v: any): string[] {
return Object.keys(v).filter((e) => !["namespace", "permission"].includes(e)); 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) {
if (config.type == "uniapp-x") {
return "";
} else {
return text;
}
}
/** /**
* *
* @param sources source * @param sources source
@ -88,8 +130,7 @@ async function formatCode(text: string): Promise<string | null> {
printWidth: 100, printWidth: 100,
trailingComma: "none", trailingComma: "none",
}) })
.catch((err) => { .catch(() => {
console.log(err);
error( error(
`[cool-eps] Failed to format /build/cool/eps.d.ts. Please delete the file and try again`, `[cool-eps] Failed to format /build/cool/eps.d.ts. Please delete the file and try again`,
); );
@ -139,6 +180,8 @@ async function getData() {
}; };
} }
}); });
list = list.filter((e) => e.prefix.startsWith("/app"));
} }
/** /**
@ -179,36 +222,6 @@ function createJson(): boolean {
* @param param0 list: eps实体列表, service: service对象 * @param param0 list: eps实体列表, service: service对象
*/ */
async function createDescribe({ list, service }: { list: Eps.Entity[]; service: any }) { async function createDescribe({ list, service }: { list: Eps.Entity[]; service: any }) {
/**
*
*/
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));
}
/** /**
* Entity * Entity
*/ */
@ -222,14 +235,10 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
let t = `interface ${formatName(item.name)} {`; let t = `interface ${formatName(item.name)} {`;
// 合并 columns 和 pageColumns去重 // 合并 columns 和 pageColumns去重
const columns: Eps.Column[] = []; const columns: Eps.Column[] = uniqBy(
[item.columns, item.pageColumns] compact([...(item.columns || []), ...(item.pageColumns || [])]),
.flat() "source",
.filter(Boolean) );
.forEach((e) => {
const d = columns.find((c) => c.source == e.source);
if (!d) columns.push(e);
});
for (const col of columns || []) { for (const col of columns || []) {
t += ` t += `
@ -337,29 +346,23 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
// 实体名 // 实体名
const en = item.name || "any"; const en = item.name || "any";
switch (a.path) { if (config.type == "uniapp-x") {
case "/page": res = "any";
if (config.type == "uniapp-x") { } else {
switch (a.path) {
case "/page":
res = `PageResponse<${en}>`; res = `PageResponse<${en}>`;
} else { break;
res = ` case "/list":
{ res = `${en} []`;
pagination: PaginationData; break;
list: ${en} []; case "/info":
[key: string]: any; res = en;
} break;
`; default:
} res = "any";
break; break;
case "/list": }
res = `${en} []`;
break;
case "/info":
res = en;
break;
default:
res = "any";
break;
} }
// 方法描述 // 方法描述
@ -376,27 +379,26 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
} }
}); });
if (config.type != "uniapp-x") { // 权限标识
// 权限标识 t += noUniappX(`
t += ` /**
/** *
* */
*/ permission: { ${permission.map((e) => `${e}: string;`).join("\n")} };
permission: { ${permission.map((e) => `${e}: string;`).join("\n")} }; `);
`;
// 权限状态 // 权限状态
t += ` t += noUniappX(`
/** /**
* *
*/ */
_permission: { ${permission.map((e) => `${e}: boolean;`).join("\n")} }; _permission: { ${permission.map((e) => `${e}: boolean;`).join("\n")} };
`; `);
}
t += ` // 请求
request: Request t += noUniappX(`
`; request: Request;
`);
} }
t += "}\n\n"; t += "}\n\n";
@ -418,40 +420,37 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
return ` return `
type json = any; type json = any;
type PaginationData = { interface PagePagination {
size: number; size: number;
page: number; page: number;
total: number; total: number;
[key: string]: any;
}; };
type PageResponse<T> = { interface PageResponse<T> {
pagination: PaginationData; pagination: PagePagination;
list: T[]; list: T[];
[key: string]: any;
}; };
${controller} ${controller}
interface RequestOptions { ${noUniappX(`interface RequestOptions {
url: string; url: string;
method?: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT'; method?: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT';
data?: any; data?: any;
params?: any; params?: any;
header?: any; headers?: any;
timeout?: number; timeout?: number;
withCredentials?: boolean; [key: string]: any;
firstIpv4?: boolean; }`)}
enableChunked?: boolean;
}
type Request = (options: RequestOptions) => Promise<any>; ${noUniappX("type Request = (options: RequestOptions) => Promise<any>;")}
${await createDict()} ${await createDict()}
type Service = { type Service = {
/** ${noUniappX("request: Request;")}
*
*/
request: Request;
${chain} ${chain}
} }
@ -475,6 +474,7 @@ async function createDescribe({ list, service }: { list: Eps.Entity[]; service:
.replaceAll("[key: string]: any;", ""); .replaceAll("[key: string]: any;", "");
text = flatten(text); text = flatten(text);
text = interfaceToType(text);
} else { } else {
text = ` text = `
declare namespace Eps { declare namespace Eps {
@ -565,6 +565,147 @@ function createService() {
}); });
} }
/**
* 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 = `PageResponse<${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("")})${noUniappX(`: Promise<${res}>`)} {
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 * @returns {Promise<string>} type
@ -606,7 +747,6 @@ async function createDict(): Promise<string> {
/** /**
* eps service * eps service
* @returns {Promise<{service: any, list: Eps.Entity[], isUpdate?: boolean}>}
*/ */
export async function createEps() { export async function createEps() {
if (config.eps.enable) { if (config.eps.enable) {
@ -616,6 +756,8 @@ export async function createEps() {
// 构建 service 对象 // 构建 service 对象
createService(); createService();
const serviceCode = createServiceCode();
// 创建 eps 目录 // 创建 eps 目录
createDir(getEpsPath(), true); createDir(getEpsPath(), true);
@ -627,6 +769,7 @@ export async function createEps() {
return { return {
service, service,
serviceCode,
list, list,
isUpdate, isUpdate,
}; };

View File

@ -3,18 +3,18 @@ import { SAFE_CHAR_MAP } from "./config";
import { createCtx } from "../ctx"; import { createCtx } from "../ctx";
import { readFile, rootDir } from "../utils"; import { readFile, rootDir } from "../utils";
import { createEps } from "../eps"; import { createEps } from "../eps";
import { uniq } from "lodash";
export async function codePlugin(): Promise<Plugin[]> { export function codePlugin(): Plugin[] {
const ctx = await createCtx();
const eps = await createEps();
const theme = await readFile(rootDir("theme.json"), true);
return [ return [
{ {
name: "vite-cool-uniappx-code-pre", name: "vite-cool-uniappx-code-pre",
enforce: "pre", enforce: "pre",
async transform(code, id) { async transform(code, id) {
if (id.includes("/cool/virtual.ts")) { if (id.includes("/cool/ctx/index.ts")) {
const ctx = await createCtx();
const theme = await readFile(rootDir("theme.json"), true);
ctx["SAFE_CHAR_MAP"] = []; ctx["SAFE_CHAR_MAP"] = [];
for (const i in SAFE_CHAR_MAP) { for (const i in SAFE_CHAR_MAP) {
ctx["SAFE_CHAR_MAP"].push([i, SAFE_CHAR_MAP[i]]); ctx["SAFE_CHAR_MAP"].push([i, SAFE_CHAR_MAP[i]]);
@ -27,10 +27,24 @@ export async function codePlugin(): Promise<Plugin[]> {
`const ctx = ${JSON.stringify(ctx, null, 4)}`, `const ctx = ${JSON.stringify(ctx, null, 4)}`,
); );
code = code.replace( return {
"const eps = {}", code,
`const eps = ${JSON.stringify(eps, null, 4).replaceAll("[]", "[] as any[]")}`, map: { mappings: "" },
); };
}
if (id.includes("/cool/service/index.ts")) {
const eps = await createEps();
if (eps.serviceCode) {
const { content, types } = eps.serviceCode;
const typeCode = `import type { ${uniq(types).join(", ")} } from '../types';`;
code =
typeCode +
"\n\n" +
code.replace("const service = {}", `const service = ${content}`);
}
return { return {
code, code,

View File

@ -9,6 +9,8 @@ interface ParseResult {
key: string; key: string;
/** 解析出的内容 */ /** 解析出的内容 */
content: string; content: string;
/** 层级 */
level: number;
} }
/** /**
@ -31,7 +33,7 @@ export function flatten(template: string): string {
let serviceFields = ""; let serviceFields = "";
// 解析内容并生成接口定义 // 解析内容并生成接口定义
parse(serviceContent).forEach(({ key, content }) => { parse(serviceContent).forEach(({ key, content, level }) => {
interfaces += `\nexport interface ${firstUpperCase(key)}Interface {${content}}\n`; interfaces += `\nexport interface ${firstUpperCase(key)}Interface {${content}}\n`;
serviceFields += `${key}: ${firstUpperCase(key)}Interface;`; serviceFields += `${key}: ${firstUpperCase(key)}Interface;`;
}); });
@ -68,7 +70,7 @@ function findClosingBrace(str: string, startIndex: number): number {
* @param content - * @param content -
* @returns * @returns
*/ */
function parse(content: string): ParseResult[] { function parse(content: string, level: number = 0): ParseResult[] {
// 匹配形如 xxx: { ... } 的结构 // 匹配形如 xxx: { ... } 的结构
const interfacePattern = /(\w+)\s*:\s*\{/g; const interfacePattern = /(\w+)\s*:\s*\{/g;
const result: ParseResult[] = []; const result: ParseResult[] = [];
@ -83,7 +85,7 @@ function parse(content: string): ParseResult[] {
// 处理嵌套结构 // 处理嵌套结构
if (parsedContent.includes("{") && parsedContent.includes("}")) { if (parsedContent.includes("{") && parsedContent.includes("}")) {
const nestedInterfaces = parse(parsedContent); const nestedInterfaces = parse(parsedContent, level + 1);
// 替换嵌套的内容为接口引用 // 替换嵌套的内容为接口引用
if (nestedInterfaces.length > 0) { if (nestedInterfaces.length > 0) {
@ -98,6 +100,7 @@ function parse(content: string): ParseResult[] {
// 将解析结果添加到数组开头 // 将解析结果添加到数组开头
result.unshift({ result.unshift({
key: match[1], key: match[1],
level,
content: parsedContent, content: parsedContent,
}); });
} }

View File

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

View File

@ -236,3 +236,20 @@ export function isTailwindClass(className: string): boolean {
return false; return false;
} }
/**
* interface type
*/
export function interfaceToType(code: string) {
// 匹配 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}}`;
});
}