This commit is contained in:
eibons
2025-08-15 21:37:29 +08:00
parent 2d7f77a984
commit ce999372ae
183 changed files with 21567 additions and 5 deletions

View File

@@ -0,0 +1,12 @@
export default {
parser: "@typescript-eslint/parser",
extends: ["plugin:@typescript-eslint/recommended"],
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
ecmaVersion: 2020,
sourceType: "module",
createDefaultProgram: true,
},
rules: {},
};

View File

@@ -0,0 +1,8 @@
{
"tabWidth": 4,
"useTabs": true,
"semi": true,
"jsxBracketSameLine": true,
"singleQuote": false,
"printWidth": 100
}

View File

@@ -0,0 +1,48 @@
declare module "virtual:ctx" {
const ctx: {
serviceLang: string;
modules: string[];
};
export { ctx };
}
declare module "virtual:eps" {
const eps: {
isUpdate: boolean;
list: {
prefix: string;
api: {
method: string;
path: string;
summary: string;
dts: {
[key: string]: string;
};
[key: string]: any;
}[];
namespace: string;
name: string;
module: string;
columns: {
comment: string;
nullable: boolean;
propertyName: string;
type: string;
[key: string]: any;
}[];
[key: string]: any;
}[];
service: any;
};
export { eps };
}
declare module "virtual:demo";
declare module "virtual:svg-register";
declare module "virtual:svg-icons" {
const svgIcons: string[];
export { svgIcons };
}

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
import type { Ctx } from "../../types";
export declare function createCtx(): Promise<Ctx.Data>;

View File

@@ -0,0 +1,2 @@
import type { Plugin } from "vite";
export declare function demo(enable?: boolean): Plugin;

View File

@@ -0,0 +1,7 @@
/**
* 将模板字符串扁平化处理,转换为 Service 类型定义
* @param template - 包含 Service 类型定义的模板字符串
* @returns 处理后的 Service 类型定义字符串
* @throws {Error} 当模板中找不到 Service 类型定义时抛出错误
*/
export declare function flatten(template: string): string;

View File

@@ -0,0 +1,18 @@
import type { Eps } from "../../types";
/**
* 主入口:创建 eps 相关文件和 service
*/
export declare function createEps(): Promise<{
service: {};
serviceCode: {
content: string;
types: string[];
};
list: Eps.Entity[];
isUpdate: boolean;
} | {
service: {};
list: never[];
serviceCode?: undefined;
isUpdate?: undefined;
}>;

View File

@@ -0,0 +1,6 @@
interface Item {
path: string;
code: string;
}
export declare function createFile(data: Item | Item[]): Promise<void>;
export {};

View File

@@ -0,0 +1,2 @@
import type { Config } from "../types";
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,4 @@
export declare function updateProxy(data: {
name: string;
}): Promise<void>;
export declare function getProxyTarget(proxy: any): any;

View File

@@ -0,0 +1,4 @@
export declare function createSvg(): Promise<{
code: string;
svgIcons: string[];
}>;

View File

@@ -0,0 +1,4 @@
export declare function createTag(code: string, id: string): {
map: any;
code: any;
} | null;

View File

@@ -0,0 +1 @@
export declare function test(): void;

View File

@@ -0,0 +1,67 @@
interface PostcssRemToRpxOptions {
remUnit?: number;
remPrecision?: number;
rpxRatio?: number;
}
interface TailwindTransformOptions extends PostcssRemToRpxOptions {
}
/**
* Vite 插件:自动转换 .uvue 文件中的 Tailwind 类名为安全字符
* 并自动注入 rem 转 rpx 的 PostCSS 插件
* @param options 配置项
* @returns Vite 插件对象
*/
export declare function tailwindTransformPlugin(options?: TailwindTransformOptions): {
name: string;
enforce: "pre";
config(): {
css: {
postcss: {
plugins: {
postcssPlugin: string;
prepare(): {
Rule(rule: any): void;
Declaration(decl: any): void;
};
}[];
};
};
};
transform(code: string, id: string): {
code: string;
map: {
mappings: string;
};
} | null;
};
/**
* uniappX 入口,自动注入 Tailwind 类名转换插件
* @param options 配置项
* @returns Vite 插件数组
*/
export declare function uniappX(options?: {
tailwind?: TailwindTransformOptions;
}): {
name: string;
enforce: "pre";
config(): {
css: {
postcss: {
plugins: {
postcssPlugin: string;
prepare(): {
Rule(rule: any): void;
Declaration(decl: any): void;
};
}[];
};
};
};
transform(code: string, id: string): {
code: string;
map: {
mappings: string;
};
} | null;
}[];
export {};

View File

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

View File

@@ -0,0 +1,9 @@
type primaryColor = "emerald" | "green" | "lime" | "orange" | "amber" | "yellow" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink";
type surfaceColor = "slate" | "gray" | "zinc" | "neutral" | "stone" | "soho" | "viva" | "ocean";
export declare function colorPalette(options: {
primary: primaryColor;
surface: surfaceColor;
}): {
[x: string]: string;
};
export {};

View File

@@ -0,0 +1,8 @@
/**
* 特殊字符映射表
*/
export declare const SAFE_CHAR_MAP: Record<string, string>;
/**
* 特殊字符映射表(国际化)
*/
export declare const SAFE_CHAR_MAP_LOCALE: Record<string, string>;

View File

@@ -0,0 +1,7 @@
/**
* 将模板字符串扁平化处理,转换为 Service 类型定义
* @param template - 包含 Service 类型定义的模板字符串
* @returns 处理后的 Service 类型定义字符串
* @throws {Error} 当模板中找不到 Service 类型定义时抛出错误
*/
export declare function flatten(template: string): string;

View File

@@ -0,0 +1,7 @@
import type { Plugin } from "vite";
/**
* uniappX 入口,自动注入 Tailwind 类名转换插件
* @param options 配置项
* @returns Vite 插件数组
*/
export declare function uniappX(): Promise<Plugin<any>[]>;

View File

@@ -0,0 +1,9 @@
import type { Plugin } from "vite";
/**
* 转换类名中的特殊字符为安全字符
*/
export declare function toSafeClass(className: string): string;
/**
* Tailwind 类名转换插件
*/
export declare function tailwindPlugin(): Plugin<any>[];

View File

@@ -0,0 +1,28 @@
/**
* 获取动态类名
*/
export declare const getDynamicClassNames: (value: string) => string[];
/**
* 获取类名
*/
export declare function getClassNames(code: string): string[];
/**
* 获取 class 内容
*/
export declare function getClassContent(code: string): string[];
/**
* 获取节点
*/
export declare function getNodes(code: string): string[];
/**
* 添加 script 标签内容
*/
export declare function addScriptContent(code: string, content: string): string;
/**
* 判断是否为 Tailwind 类名
*/
export declare function isTailwindClass(className: string): boolean;
/**
* 将 interface 转换为 type
*/
export declare function interfaceToType(code: string): string;

View File

@@ -0,0 +1,11 @@
import prettier from "prettier";
export declare function rootDir(path: string): string;
export declare function firstUpperCase(value: string): string;
export declare function toCamel(str: string): string;
export declare function createDir(path: string, recursive?: boolean): void;
export declare function readFile(path: string, json?: boolean): any;
export declare function writeFile(path: string, data: any): void | "";
export declare function parseJson(req: any): Promise<any>;
export declare function formatContent(content: string, options?: prettier.Options): Promise<string>;
export declare function error(message: string): void;
export declare function success(message: string): void;

View File

@@ -0,0 +1,2 @@
import type { Plugin } from "vite";
export declare function virtual(): Promise<Plugin>;

View File

@@ -0,0 +1,41 @@
{
"name": "@cool-vue/vite-plugin",
"version": "8.2.3",
"description": "cool-admin、cool-uni builder",
"types": "./dist/index.d.ts",
"main": "/dist/index.js",
"scripts": {
"build": "rollup --c --bundleConfigAsCjs"
},
"keywords": [],
"author": "cool",
"license": "ISC",
"files": [
"types/*",
"dist/*",
"client.d.ts",
"package.json"
],
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.6",
"@types/lodash": "^4.17.15",
"@types/node": "^20.12.7",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"eslint": "^9.1.1",
"postcss-value-parser": "^4.2.0",
"rollup": "^4.16.2",
"tslib": "^2.6.2",
"typescript": "^5.4.5",
"vite": "^5.4.14"
},
"dependencies": {
"@vue/compiler-sfc": "^3.5.13",
"axios": "^1.6.8",
"glob": "^10.3.12",
"lodash": "^4.17.21",
"magic-string": "^0.30.17",
"prettier": "^3.4.2",
"svgo": "^3.3.2"
}
}

View File

@@ -0,0 +1,21 @@
import typescript from "@rollup/plugin-typescript";
import { defineConfig } from "rollup";
export default defineConfig({
input: ["src/index.ts"],
external: ["lodash"],
output: {
name: "index",
format: "umd",
file: "dist/index.js",
},
plugins: [
typescript({
module: "esnext",
exclude: ["./node_modules/**"],
}),
],
});

View File

@@ -0,0 +1,68 @@
import type { Plugin } from "vite";
import { createEps } from "./eps";
import { parseJson } from "./utils";
import { updatePlugin } from "./plugin";
import { updateProxy } from "./proxy";
import { createFile } from "./file";
import { config } from "./config";
import { createTag } from "./tag";
export function base(): Plugin {
return {
name: "vite-cool-base",
enforce: "pre",
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
function done(data: any) {
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;
},
};
}

View File

@@ -0,0 +1,56 @@
import type { Type } from "../types";
export const config = {
type: "admin" as Type,
reqUrl: "",
demo: false,
nameTag: true,
eps: {
enable: true,
api: "",
dist: "./build/cool",
mapping: [
{
// 自定义匹配
custom: ({ propertyName, type }: { propertyName: string; type: string }) => {
// 如果没有返回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",
},
};

View File

@@ -0,0 +1,109 @@
import { join } from "path";
import { error, readFile, rootDir, writeFile } from "../utils";
import { glob } from "glob";
import { assign, cloneDeep, orderBy } from "lodash";
import { config } from "../config";
import fs from "fs";
import axios from "axios";
import util from "node:util";
import type { Ctx } from "../../types";
export async function createCtx() {
let ctx: Ctx.Data = {
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 = cloneDeep(ctx);
// 删除临时页面
ctx.pages = ctx.pages?.filter((e) => !e.isTemp);
ctx.subPackages = ctx.subPackages?.filter((e) => !e.isTemp);
// 加载 uni_modules 配置文件
const files = await glob(rootDir("uni_modules") + "/**/pages_init.json", {
stat: true,
withFileTypes: true,
});
for (const file of files) {
if (file.isFile()) {
const { pages = [], subPackages = [] }: Ctx.Data = readFile(
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) {
assign(d, e);
} else {
if (isSub) {
ctx.subPackages?.unshift(e);
} else {
ctx.pages?.unshift(e);
}
}
});
}
}
// 排序后检测,避免加载顺序问题
function order(d: Ctx.Data) {
return {
pages: orderBy(d.pages, "path"),
subPackages: 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;
}

View File

@@ -0,0 +1,50 @@
import type { Plugin } from "vite";
import { glob } from "glob";
import path from "path";
import { readFileSync } from "fs";
import { rootDir } from "./utils";
export function demo(enable?: boolean): Plugin {
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: any = {};
if (enable) {
const files = await 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]
] = readFileSync(p, "utf-8");
}
}
}
return `
export const demo = ${JSON.stringify(demo)};
`;
}
},
};
}

View File

@@ -0,0 +1,796 @@
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),
};
}
});
if (config.type == "uniapp-x" || config.type == "app") {
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 && list.length > 0) {
// 创建 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: [],
};
}
}

View File

@@ -0,0 +1,40 @@
import { createWriteStream } from "fs";
import { join } from "path";
import { createDir, formatContent } from "../utils";
import { isArray } from "lodash";
interface Item {
path: string;
code: string;
}
// 创建文件
export async function createFile(data: Item | Item[]) {
const list = isArray(data) ? data : [data];
for (const item of list) {
const { path, code } = item;
// 格式化内容
const content = await formatContent(code, {
parser: "vue",
});
// 目录路径
const dir = (path || "").split("/");
// 文件名
const fname = dir.pop();
// 源码路径
const srcPath = `./src/${dir.join("/")}`;
// 创建目录
createDir(srcPath, true);
// 创建文件
createWriteStream(join(srcPath, fname!), {
flags: "w",
}).write(content);
}
}

View File

@@ -0,0 +1,54 @@
import { base } from "./base";
import { config } from "./config";
import { demo } from "./demo";
import { getProxyTarget } from "./proxy";
import type { Config } from "../types";
import { virtual } from "./virtual";
import { assign, merge } from "lodash";
import { uniappX } from "./uniapp-x";
export function cool(options: Config.Options) {
// 应用类型admin | app
config.type = options.type;
// 请求地址
config.reqUrl = getProxyTarget(options.proxy);
// 是否开启名称标签
config.nameTag = options.nameTag ?? true;
// svg
if (options.svg) {
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) {
merge(config.eps.mapping, mapping);
}
}
// tailwind
if (options.tailwind) {
assign(config.tailwind, options.tailwind);
}
return [base(), virtual(), uniappX(), demo(options.demo)];
}

View File

@@ -0,0 +1,36 @@
import { error, readFile, rootDir, writeFile } from "../utils";
import { config } from "../config";
function getPath() {
return rootDir(`.${config.type == "admin" ? "/src" : ""}/config/proxy.ts`);
}
export async function updateProxy(data: { name: string }) {
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);
}
export function getProxyTarget(proxy: any) {
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 "";
}
}
}

View File

@@ -0,0 +1,100 @@
import { readdirSync, readFileSync } from "fs";
import { basename, extname } from "path";
import { rootDir } from "../utils";
import svgo from "svgo";
import { config } from "../config";
let svgIcons: string[] = [];
function findSvg(dir: string) {
const arr: string[] = [];
const dirs = 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 (extname(d.name) == ".svg") {
const baseName = 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 = readFileSync(dir + d.name)
.toString()
.replace(/(\r)|(\n)/g, "")
.replace(/<svg([^>+].*?)>/, (_: any, $2: any) => {
let width = 0;
let height = 0;
let content = $2.replace(
/(width|height)="([^>+].*?)"/g,
(_: any, s2: any, s3: any) => {
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("");
}
export 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 };
}

View File

@@ -0,0 +1,32 @@
import { compileScript, parse } from "@vue/compiler-sfc";
import magicString from "magic-string";
export function createTag(code: string, id: string) {
if (/\.vue$/.test(id)) {
let s: any;
const str = () => s || (s = new magicString(code));
const { descriptor } = parse(code);
if (!descriptor.script && descriptor.scriptSetup) {
const res = compileScript(descriptor, { id });
const { name, lang }: any = 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;
}

View File

@@ -0,0 +1,90 @@
import { toSafeClass } from "./uniapp-x/tailwind";
import { getClassContent, getClassNames, getNodes, isTailwindClass } from "./uniapp-x/utils";
export function test() {
const html = `
<template>
1<text :class="parseClass({'!bg-red-50': hoverable})"></text>
2<text :class="a ? 'text-red-100' : 'text-red-200'"></text>
3<text :class="\`text-red-300 \${true ? 'text-red-310' : 'text-red-320'}\`"></text>
4<text class="text-red-330"></text>
5<text :class="{
'text-red-400': a,
'text-red-500': b,
}"></text>
6<text :class="[
'text-red-600',
'text-red-700',
{
'text-red-800': c,
}
]"></text>
7<text :class="\`text-red-900\` text-red-1000"></text>
8<text :pt="{
className: '!text-green-50'
}"></text>
9<text :pt="{
className: parseClass({
'!text-green-100': hoverable,
}),
item: {
className: '!text-green-200'
},
title: {
className: '!text-green-300'
}
}"></text>
10<text :pt="{
className: \`text-green-300 \${true ? 'text-red-310' : 'text-red-320'}\`
}"></text>
11<text hover-class="text-green-400"></text>
12<text :hover-class="\`text-green-400\`"></text>
13<text :hover-class="parseClass({'!text-green-450': hoverable})"></text>
14<text :hover-class="a ? 'text-green-500' : 'text-green-600'"></text>
15<text :hover-class="\`text-green-700 \${true ? 'text-red-310' : 'text-red-320'}\`"></text>
16<cl-timeline-item
icon="checkbox-circle-fill"
:pt="{
title: {
className: '!text-primary-500'
},
icon: {
className: '!text-primary-400'
}
}"
title="更快速"
date="2025-06-28 10:00:00"
content="通过集成 cool-admin 的用户登录、版本管理、消息通知以及订单管理等基础模块。有效降低了开发成本,进而增强了市场竞争力。"
>
</cl-timeline-item>
17<text class="title dark:!text-surface-50"></text>
</template>
`;
const nodes = getNodes(html);
console.log("所有节点:");
nodes.forEach((node, index) => {
console.log(`${index + 1}个节点:`, node);
});
console.log("\n详细分析:");
nodes.forEach((node, index) => {
const classContents = getClassContent(node);
const classNames = getClassNames(node);
console.log(`${index + 1}个节点:`);
console.log("classContents", classContents);
console.log("classNames", classNames);
classNames.forEach((className) => {
console.log("safeClass", toSafeClass(className));
});
console.log("---");
});
console.log(isTailwindClass("dark:!text-surface-50"));
}
test();
// npx tsx src/test.ts

View File

@@ -0,0 +1,82 @@
import type { Plugin } from "vite";
import { SAFE_CHAR_MAP_LOCALE } from "./config";
import { createCtx } from "../ctx";
import { readFile, rootDir } from "../utils";
import { createEps } from "../eps";
import { uniq } from "lodash";
export function codePlugin(): Plugin[] {
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 { ${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: "" },
};
}
},
},
];
}

View File

@@ -0,0 +1,51 @@
/**
* 特殊字符映射表
*/
export const SAFE_CHAR_MAP: Record<string, string> = {
"[": "-bracket-start-",
"]": "-bracket-end-",
"(": "-paren-start-",
")": "-paren-end-",
"{": "-brace-start-",
"}": "-brace-end-",
$: "-dollar-",
"#": "-hash-",
"!": "-important-",
"/": "-slash-",
":": "-colon-",
};
/**
* 特殊字符映射表(国际化)
*/
export const SAFE_CHAR_MAP_LOCALE: Record<string, string> = {
"[": "-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-",
};

View File

@@ -0,0 +1,110 @@
import { firstUpperCase } from '../utils';
/**
* 解析结果的接口定义
* @interface ParseResult
*/
interface ParseResult {
/** 解析出的键名 */
key: string;
/** 解析出的内容 */
content: string;
/** 层级 */
level: number;
}
/**
* 将模板字符串扁平化处理,转换为 Service 类型定义
* @param template - 包含 Service 类型定义的模板字符串
* @returns 处理后的 Service 类型定义字符串
* @throws {Error} 当模板中找不到 Service 类型定义时抛出错误
*/
export function flatten(template: string): string {
// 查找 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: string, startIndex: number): number {
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: string, level: number = 0): ParseResult[] {
// 匹配形如 xxx: { ... } 的结构
const interfacePattern = /(\w+)\s*:\s*\{/g;
const result: ParseResult[] = [];
let match: RegExpExecArray | null;
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;
}

View File

@@ -0,0 +1,23 @@
import type { Plugin } from 'vite';
import { config } from '../config';
import { tailwindPlugin } from './tailwind';
import { codePlugin } from './code';
/**
* uniappX 入口,自动注入 Tailwind 类名转换插件
* @param options 配置项
* @returns Vite 插件数组
*/
export async function uniappX() {
const plugins: Plugin[] = [];
if (config.type == "uniapp-x") {
plugins.push(...codePlugin());
if (config.tailwind.enable) {
plugins.push(...tailwindPlugin());
}
}
return plugins;
}

View File

@@ -0,0 +1,419 @@
// @ts-ignore
import valueParser from 'postcss-value-parser';
import { config } from '../config';
import type { Plugin } from 'vite';
import { SAFE_CHAR_MAP } from './config';
import { addScriptContent, getClassContent, getClassNames, getNodes, isTailwindClass } from './utils';
/**
* 转换类名中的特殊字符为安全字符
*/
export function toSafeClass(className: string): string {
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: string): string {
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: string): string {
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(): Plugin {
return {
name: "vite-cool-uniappx-postcss",
enforce: "pre",
config() {
return {
css: {
postcss: {
plugins: [
{
postcssPlugin: "vite-cool-uniappx-class-mapping",
prepare() {
// 存储 Tailwind 颜色值
const colorValues: Record<string, string> = {};
return {
// 处理选择器规则
Rule(rule: any) {
if (
rule.selector.includes("uni-") ||
[".button-hover"].some((e) =>
rule.selector.includes(e),
)
) {
return;
}
// 转换选择器为安全的类名格式
rule.selector = toSafeClass(
rule.selector.replace(/\\/g, ""),
);
},
// 处理声明规则
Declaration(decl: any) {
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: any) => {
// 处理单位转换(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(): Plugin {
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 类名转换插件
*/
export function tailwindPlugin() {
return [postcssPlugin(), transformPlugin()];
}

View File

@@ -0,0 +1,524 @@
/**
* 获取动态类名
*/
export const getDynamicClassNames = (value: string): string[] => {
const names = new Set<string>();
// 匹配函数调用中的对象参数(如 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);
};
/**
* 获取类名
*/
export function getClassNames(code: string): string[] {
// 修改正则表达式以支持多行匹配,避免内层引号冲突
const classRegex =
/(?:class|:class|:pt|:hover-class)\s*=\s*(['"`])((?:[^'"`\\]|\\.|`[^`]*`|'[^']*'|"[^"]*")*?)\1/gis;
const classNames = new Set<string>();
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: string, classNames: Set<string>) {
// 递归查找所有 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: string, startPos: number): string | null {
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 内容
*/
export function getClassContent(code: string) {
// 修改正则表达式以支持多行匹配,避免内层引号冲突
const regex =
/(?:class|:class|:pt|:hover-class)\s*=\s*(['"`])((?:[^'"`\\]|\\.|`[^`]*`|'[^']*'|"[^"]*")*?)\1/gis;
const texts: string[] = [];
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;
}
/**
* 获取节点
*/
export function getNodes(code: string) {
const nodes: string[] = [];
// 找到所有顶级template标签的完整内容
function findTemplateContents(content: string): string[] {
const results: string[] = [];
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: string): void {
// 先提取当前内容中的所有标签
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 标签内容
*/
export function addScriptContent(code: string, content: string) {
const scriptMatch = /<script\b[^>]*>([\s\S]*?)<\/script>/g.exec(code);
if (!scriptMatch) {
return code;
}
const scriptContent = scriptMatch[1];
const scriptStartIndex = scriptMatch.index + scriptMatch[0].indexOf(">") + 1;
const scriptEndIndex = scriptStartIndex + scriptContent.length;
return (
code.substring(0, scriptStartIndex) +
"\n" +
content +
"\n" +
scriptContent.trim() +
code.substring(scriptEndIndex)
);
}
/**
* 判断是否为 Tailwind 类名
*/
export function isTailwindClass(className: string): boolean {
const prefixes = [
// 布局
"container",
"flex",
"grid",
"block",
"inline",
"hidden",
"visible",
// 间距
"p-",
"px-",
"py-",
"pt-",
"pr-",
"pb-",
"pl-",
"m-",
"mx-",
"my-",
"mt-",
"mr-",
"mb-",
"ml-",
"space-",
"gap-",
// 尺寸
"w-",
"h-",
"min-w-",
"max-w-",
"min-h-",
"max-h-",
// 颜色
"bg-",
"text-",
"border-",
"ring-",
"shadow-",
// 边框
"border",
"rounded",
"ring",
// 字体
"font-",
"text-",
"leading-",
"tracking-",
"antialiased",
// 定位
"absolute",
"relative",
"fixed",
"sticky",
"static",
"top-",
"right-",
"bottom-",
"left-",
"inset-",
"z-",
// 变换
"transform",
"translate-",
"rotate-",
"scale-",
"skew-",
// 过渡
"transition",
"duration-",
"ease-",
"delay-",
// 交互
"cursor-",
"select-",
"pointer-events-",
// 溢出
"overflow-",
"truncate",
// 滚动
"scroll-",
// 伪类和响应式
"hover:",
"focus:",
"active:",
"disabled:",
"group-hover:",
];
const statePrefixes = ["dark:", "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
*/
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}}`;
});
}

View File

@@ -0,0 +1,95 @@
import fs from "fs";
import { join } from "path";
import { config } from "../config";
import prettier from "prettier";
// 根目录
export function rootDir(path: string) {
switch (config.type) {
case "app":
case "uniapp-x":
return join(process.env.UNI_INPUT_DIR!, path);
default:
return join(process.cwd(), path);
}
}
// 首字母大写
export function firstUpperCase(value: string): string {
return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) {
return $1.toUpperCase() + $2;
});
}
// 横杠转驼峰
export function toCamel(str: string): string {
return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) {
return $1 + $2.toUpperCase();
});
}
// 创建目录
export function createDir(path: string, recursive?: boolean) {
try {
if (!fs.existsSync(path)) fs.mkdirSync(path, { recursive });
} catch (err) {}
}
// 读取文件
export function readFile(path: string, json?: boolean) {
try {
const content = fs.readFileSync(path, "utf8");
return json
? JSON.parse(content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
: content;
} catch (err) {}
return "";
}
// 写入文件
export function writeFile(path: string, data: any) {
try {
return fs.writeFileSync(path, data);
} catch (err) {}
return "";
}
// 解析body
export function parseJson(req: any): Promise<any> {
return new Promise((resolve) => {
let d = "";
req.on("data", function (chunk: any) {
d += chunk;
});
req.on("end", function () {
try {
resolve(JSON.parse(d));
} catch {
resolve({});
}
});
});
}
// 格式化内容
export function formatContent(content: string, options?: prettier.Options) {
return prettier.format(content, {
parser: "typescript",
useTabs: true,
tabWidth: 4,
endOfLine: "lf",
semi: true,
...options,
});
}
export function error(message: string) {
console.log("\x1B[31m%s\x1B[0m", message);
}
export function success(message: string) {
console.log("\x1B[32m%s\x1B[0m", message);
}

View File

@@ -0,0 +1,92 @@
import type { Plugin } from "vite";
import { createEps } from "./eps";
import { createCtx } from "./ctx";
import { createSvg } from "./svg";
export async function virtual(): Promise<Plugin> {
const virtualModuleIds: string[] = [
"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)}
`;
}
},
};
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"rootDir": "./src",
"target": "ESNext",
"module": "CommonJS",
"declaration": true,
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"pretty": true,
"resolveJsonModule": true,
"types": ["node", "lodash"]
},
"include": ["src"],
"exclude": ["dist"]
}

View File

@@ -0,0 +1,123 @@
export declare type Type = "admin" | "app" | "uniapp-x";
export declare namespace Eps {
interface Column {
comment: string;
nullable: boolean;
propertyName: string;
source: string;
type: string;
dict: string[] | string;
defaultValue: any;
[key: string]: any;
}
interface Entity {
api: {
dts: {
parameters?: {
description: string;
name: string;
required: boolean;
schema: {
type: string;
};
}[];
};
name: string;
method: string;
path: string;
prefix: string;
summary: string;
tag: string;
}[];
columns: Column[];
pageColumns: Column[];
pageQueryOp: {
fieldEq: string[];
fieldLike: string[];
keyWordLikeFields: string[];
};
search: {
fieldEq: Column[];
fieldLike: Column[];
keyWordLikeFields: Column[];
};
module: string;
name: string;
prefix: string;
[key: string]: any;
}
}
export declare namespace Ctx {
type Pages = {
path?: string;
style?: {
[key: string]: any;
};
[key: string]: any;
}[];
type SubPackages = {
root?: string;
pages?: Ctx.Pages;
[key: string]: any;
}[];
interface Data {
appid?: string;
pages?: Ctx.Pages;
subPackages?: Ctx.SubPackages;
modules?: string[];
serviceLang: "Node" | "Java" | "Go" | "Python";
[key: string]: any;
}
}
export declare namespace Config {
interface Eps {
// 是否开启Eps
enable: boolean;
// 请求地址
api: "app" | "admin" | (string & {});
// 输出目录
dist: string;
// 映射
mapping: {
type?: string;
test?: string[];
custom?(data: { propertyName: string; type: string }): any;
}[];
}
interface Options {
// 应用类型
type: Type;
// 代理配置
proxy: any;
// Eps
eps?: Partial<Config.Eps>;
// 是否开启演示模式
demo?: boolean;
// 是否开启名称标签
nameTag?: boolean;
// svg
svg?: {
// 跳过拼接模块名
skipNames?: string[];
};
// tailwind
tailwind?: {
// 是否开启tailwind
enable?: boolean;
// 根元素字体大小
remUnit?: number;
// 小数位数
remPrecision?: number;
// 转换比例
rpxRatio?: number;
// 暗黑模式文本类名
darkTextClass?: string;
};
}
}

File diff suppressed because it is too large Load Diff