index.js 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('fs'), require('path'), require('prettier'), require('axios'), require('lodash'), require('@vue/compiler-sfc'), require('magic-string'), require('glob'), require('node:util'), require('svgo'), require('postcss-value-parser')) :
  3. typeof define === 'function' && define.amd ? define(['exports', 'fs', 'path', 'prettier', 'axios', 'lodash', '@vue/compiler-sfc', 'magic-string', 'glob', 'node:util', 'svgo', 'postcss-value-parser'], factory) :
  4. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.index = {}, global.fs, global.path, global.prettier, global.axios, global.lodash, global.compilerSfc, global.magicString, global.glob, global.util, global.svgo, global.valueParser));
  5. })(this, (function (exports, fs, path, prettier, axios, lodash, compilerSfc, magicString, glob, util, svgo, valueParser) { 'use strict';
  6. const config = {
  7. type: "admin",
  8. reqUrl: "",
  9. eps: {
  10. enable: true,
  11. api: "",
  12. dist: "./build/cool",
  13. mapping: [
  14. {
  15. // 自定义匹配
  16. custom: ({ propertyName, type }) => {
  17. // 如果没有,返回null或者不返回,则继续遍历其他匹配规则
  18. return null;
  19. },
  20. },
  21. {
  22. type: "string",
  23. test: ["varchar", "text", "simple-json"],
  24. },
  25. {
  26. type: "string[]",
  27. test: ["simple-array"],
  28. },
  29. {
  30. type: "Date",
  31. test: ["datetime", "date"],
  32. },
  33. {
  34. type: "number",
  35. test: ["tinyint", "int", "decimal"],
  36. },
  37. {
  38. type: "BigInt",
  39. test: ["bigint"],
  40. },
  41. ],
  42. },
  43. svg: {
  44. skipNames: ["base"],
  45. },
  46. };
  47. // 根目录
  48. function rootDir(path$1) {
  49. switch (config.type) {
  50. case "app":
  51. case "uniapp-x":
  52. return path.join(process.env.UNI_INPUT_DIR, path$1);
  53. default:
  54. return path.join(process.cwd(), path$1);
  55. }
  56. }
  57. // 首字母大写
  58. function firstUpperCase(value) {
  59. return value.replace(/\b(\w)(\w*)/g, function ($0, $1, $2) {
  60. return $1.toUpperCase() + $2;
  61. });
  62. }
  63. // 横杠转驼峰
  64. function toCamel(str) {
  65. return str.replace(/([^-])(?:-+([^-]))/g, function ($0, $1, $2) {
  66. return $1 + $2.toUpperCase();
  67. });
  68. }
  69. // 创建目录
  70. function createDir(path, recursive) {
  71. try {
  72. if (!fs.existsSync(path))
  73. fs.mkdirSync(path, { recursive });
  74. }
  75. catch (err) { }
  76. }
  77. // 读取文件
  78. function readFile(path, json) {
  79. try {
  80. const content = fs.readFileSync(path, "utf8");
  81. return json
  82. ? JSON.parse(content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
  83. : content;
  84. }
  85. catch (err) { }
  86. return "";
  87. }
  88. // 写入文件
  89. function writeFile(path, data) {
  90. try {
  91. return fs.writeFileSync(path, data);
  92. }
  93. catch (err) { }
  94. return "";
  95. }
  96. // 解析body
  97. function parseJson(req) {
  98. return new Promise((resolve) => {
  99. let d = "";
  100. req.on("data", function (chunk) {
  101. d += chunk;
  102. });
  103. req.on("end", function () {
  104. try {
  105. resolve(JSON.parse(d));
  106. }
  107. catch {
  108. resolve({});
  109. }
  110. });
  111. });
  112. }
  113. // 格式化内容
  114. function formatContent(content, options) {
  115. return prettier.format(content, {
  116. parser: "typescript",
  117. useTabs: true,
  118. tabWidth: 4,
  119. endOfLine: "lf",
  120. semi: true,
  121. ...options,
  122. });
  123. }
  124. function error(message) {
  125. console.log("\x1B[31m%s\x1B[0m", message);
  126. }
  127. const service = {};
  128. let list = [];
  129. // 获取请求地址
  130. function getEpsUrl() {
  131. let url = config.eps.api;
  132. if (!url) {
  133. url = config.type;
  134. }
  135. switch (url) {
  136. case "app":
  137. case "uniapp-x":
  138. url = "/app/base/comm/eps";
  139. break;
  140. case "admin":
  141. url = "/admin/base/open/eps";
  142. break;
  143. }
  144. return url;
  145. }
  146. // 获取路径
  147. function getEpsPath(filename) {
  148. return path.join(config.type == "admin" ? config.eps.dist : rootDir(config.eps.dist), filename || "");
  149. }
  150. // 获取方法名
  151. function getNames(v) {
  152. return Object.keys(v).filter((e) => !["namespace", "permission"].includes(e));
  153. }
  154. // 找字段
  155. function findColumns(sources, item) {
  156. const columns = [item.columns, item.pageColumns].flat().filter(Boolean);
  157. return (sources || [])
  158. .map((e) => columns.find((c) => c.source == e))
  159. .filter(Boolean);
  160. }
  161. // 格式化代码
  162. async function formatCode(text) {
  163. return prettier
  164. .format(text, {
  165. parser: "typescript",
  166. useTabs: true,
  167. tabWidth: 4,
  168. endOfLine: "lf",
  169. semi: true,
  170. singleQuote: false,
  171. printWidth: 100,
  172. trailingComma: "none",
  173. })
  174. .catch((err) => {
  175. console.log(err);
  176. error(`[cool-eps] Failed to format /build/cool/eps.d.ts. Please delete the file and try again`);
  177. return null;
  178. });
  179. }
  180. // 获取数据
  181. async function getData() {
  182. // 读取本地数据
  183. list = readFile(getEpsPath("eps.json"), true) || [];
  184. // 请求地址
  185. const url = config.reqUrl + getEpsUrl();
  186. // 请求数据
  187. await axios
  188. .get(url, {
  189. timeout: 5000,
  190. })
  191. .then((res) => {
  192. const { code, data, message } = res.data;
  193. if (code === 1000) {
  194. if (!lodash.isEmpty(data) && data) {
  195. list = lodash.values(data).flat();
  196. }
  197. }
  198. else {
  199. error(`[cool-eps] ${message || "Failed to fetch data"}`);
  200. }
  201. })
  202. .catch(() => {
  203. error(`[cool-eps] API service is not running → ${url}`);
  204. });
  205. // 初始化处理
  206. list.forEach((e) => {
  207. if (!e.namespace) {
  208. e.namespace = "";
  209. }
  210. if (!e.api) {
  211. e.api = [];
  212. }
  213. if (!e.columns) {
  214. e.columns = [];
  215. }
  216. if (!e.search) {
  217. e.search = {
  218. fieldEq: findColumns(e.pageQueryOp?.fieldEq, e),
  219. fieldLike: findColumns(e.pageQueryOp?.fieldLike, e),
  220. keyWordLikeFields: findColumns(e.pageQueryOp?.keyWordLikeFields, e),
  221. };
  222. }
  223. });
  224. }
  225. // 创建 json 文件
  226. function createJson() {
  227. const arr = list.map((e) => {
  228. return {
  229. prefix: e.prefix,
  230. name: e.name || "",
  231. api: e.api.map((e) => {
  232. return {
  233. name: e.name,
  234. method: e.method,
  235. path: e.path,
  236. };
  237. }),
  238. search: e.search,
  239. };
  240. });
  241. const content = JSON.stringify(arr);
  242. const local_content = readFile(getEpsPath("eps.json"));
  243. // 是否需要更新
  244. const isUpdate = content != local_content;
  245. if (isUpdate) {
  246. fs.createWriteStream(getEpsPath("eps.json"), {
  247. flags: "w",
  248. }).write(content);
  249. }
  250. return isUpdate;
  251. }
  252. // 创建描述文件
  253. async function createDescribe({ list, service }) {
  254. // 获取类型
  255. function getType({ propertyName, type }) {
  256. for (const map of config.eps.mapping) {
  257. if (map.custom) {
  258. const resType = map.custom({ propertyName, type });
  259. if (resType)
  260. return resType;
  261. }
  262. if (map.test) {
  263. if (map.test.includes(type))
  264. return map.type;
  265. }
  266. }
  267. return type;
  268. }
  269. // 格式化方法名
  270. function formatName(name) {
  271. return (name || "").replace(/[:,\s,\/,-]/g, "");
  272. }
  273. // 检查方法名,包含特殊字符则忽略
  274. function checkName(name) {
  275. return name && !["{", "}", ":"].some((e) => name.includes(e));
  276. }
  277. // 创建 Entity
  278. function createEntity() {
  279. const ignore = [];
  280. let t0 = "";
  281. for (const item of list) {
  282. if (!checkName(item.name))
  283. continue;
  284. let t = `interface ${formatName(item.name)} {`;
  285. // 合并多个列
  286. const columns = [];
  287. [item.columns, item.pageColumns]
  288. .flat()
  289. .filter(Boolean)
  290. .forEach((e) => {
  291. const d = columns.find((c) => c.source == e.source);
  292. if (!d) {
  293. columns.push(e);
  294. }
  295. });
  296. for (const col of columns || []) {
  297. t += `
  298. /**
  299. * ${col.comment}
  300. */
  301. ${col.propertyName}?: ${getType({
  302. propertyName: col.propertyName,
  303. type: col.type,
  304. })}
  305. `;
  306. }
  307. t += `
  308. /**
  309. * 任意键值
  310. */
  311. [key: string]: any;
  312. }
  313. `;
  314. if (!ignore.includes(item.name)) {
  315. ignore.push(item.name);
  316. t0 += t + "\n\n";
  317. }
  318. }
  319. return t0;
  320. }
  321. // 创建 Controller
  322. async function createController() {
  323. let controller = "";
  324. let chain = "";
  325. // 处理数据
  326. function deep(d, k) {
  327. if (!k)
  328. k = "";
  329. for (const i in d) {
  330. const name = k + toCamel(firstUpperCase(formatName(i)));
  331. // 检查方法名
  332. if (!checkName(name))
  333. continue;
  334. if (d[i].namespace) {
  335. // 查找配置
  336. const item = list.find((e) => (e.prefix || "") === `/${d[i].namespace}`);
  337. if (item) {
  338. let t = `interface ${name} {`;
  339. // 插入方法
  340. if (item.api) {
  341. // 权限列表
  342. const permission = [];
  343. item.api.forEach((a) => {
  344. // 方法名
  345. const n = toCamel(formatName(a.name || lodash.last(a.path.split("/"))));
  346. // 检查方法名
  347. if (!checkName(n))
  348. return;
  349. if (n) {
  350. // 参数类型
  351. let q = [];
  352. // 参数列表
  353. const { parameters = [] } = a.dts || {};
  354. parameters.forEach((p) => {
  355. if (p.description) {
  356. q.push(`\n/** ${p.description} */\n`);
  357. }
  358. // 检查参数名
  359. if (!checkName(p.name)) {
  360. return false;
  361. }
  362. const a = `${p.name}${p.required ? "" : "?"}`;
  363. const b = `${p.schema.type || "string"}`;
  364. q.push(`${a}: ${b},`);
  365. });
  366. if (lodash.isEmpty(q)) {
  367. q = ["any"];
  368. }
  369. else {
  370. q.unshift("{");
  371. q.push("}");
  372. }
  373. // 返回类型
  374. let res = "";
  375. // 实体名
  376. const en = item.name || "any";
  377. switch (a.path) {
  378. case "/page":
  379. res = `
  380. {
  381. pagination: { size: number; page: number; total: number; [key: string]: any; };
  382. list: ${en} [];
  383. [key: string]: any;
  384. }
  385. `;
  386. break;
  387. case "/list":
  388. res = `${en} []`;
  389. break;
  390. case "/info":
  391. res = en;
  392. break;
  393. default:
  394. res = "any";
  395. break;
  396. }
  397. // 描述
  398. t += `
  399. /**
  400. * ${a.summary || n}
  401. */
  402. ${n}(data${q.length == 1 ? "?" : ""}: ${q.join("")}): Promise<${res}>;
  403. `;
  404. if (!permission.includes(n)) {
  405. permission.push(n);
  406. }
  407. }
  408. });
  409. // 权限标识
  410. t += `
  411. /**
  412. * 权限标识
  413. */
  414. permission: { ${permission.map((e) => `${e}: string;`).join("\n")} };
  415. `;
  416. // 权限状态
  417. t += `
  418. /**
  419. * 权限状态
  420. */
  421. _permission: { ${permission.map((e) => `${e}: boolean;`).join("\n")} };
  422. `;
  423. t += `
  424. request: Service['request']
  425. `;
  426. }
  427. t += "}\n\n";
  428. controller += t;
  429. chain += `${formatName(i)}: ${name};`;
  430. }
  431. }
  432. else {
  433. chain += `${formatName(i)}: {`;
  434. deep(d[i], name);
  435. chain += "},";
  436. }
  437. }
  438. }
  439. // 遍历
  440. deep(service);
  441. return `
  442. type json = any;
  443. ${controller}
  444. interface Service {
  445. /**
  446. * 基础请求
  447. */
  448. request(options?: {
  449. url: string;
  450. method?: "POST" | "GET" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
  451. data?: any;
  452. params?: any;
  453. headers?: any,
  454. timeout?: number;
  455. proxy?: boolean;
  456. [key: string]: any;
  457. }): Promise<any>;
  458. ${chain}
  459. }
  460. ${await createDict()}
  461. `;
  462. }
  463. // 文件内容
  464. let text = `
  465. ${createEntity()}
  466. ${await createController()}
  467. `;
  468. // 文件名
  469. let name = "eps.d.ts";
  470. if (config.type == "uniapp-x") {
  471. name = "eps.uts";
  472. text = text
  473. .replaceAll("interface ", "export interface ")
  474. .replaceAll("type Dict", "export type Dict")
  475. .replaceAll("[key: string]: any;", "");
  476. }
  477. else {
  478. text = `
  479. declare namespace Eps {
  480. ${text}
  481. }
  482. `;
  483. }
  484. // 文本内容
  485. const content = await formatCode(text);
  486. const local_content = readFile(getEpsPath(name));
  487. // 是否需要更新
  488. if (content && content != local_content) {
  489. // 创建 eps 描述文件
  490. fs.createWriteStream(getEpsPath(name), {
  491. flags: "w",
  492. }).write(content);
  493. }
  494. }
  495. // 创建 service
  496. function createService() {
  497. // 路径第一层作为 id 标识
  498. const id = getEpsUrl().split("/")[1];
  499. list.forEach((e) => {
  500. // 请求地址
  501. const path = e.prefix[0] == "/" ? e.prefix.substring(1, e.prefix.length) : e.prefix;
  502. // 分隔路径
  503. const arr = path.replace(id, "").split("/").filter(Boolean).map(toCamel);
  504. // 遍历
  505. function deep(d, i) {
  506. const k = arr[i];
  507. if (k) {
  508. // 是否最后一个
  509. if (arr[i + 1]) {
  510. if (!d[k]) {
  511. d[k] = {};
  512. }
  513. deep(d[k], i + 1);
  514. }
  515. else {
  516. // 不存在则创建
  517. if (!d[k]) {
  518. d[k] = {
  519. permission: {},
  520. };
  521. }
  522. if (!d[k].namespace) {
  523. d[k].namespace = path;
  524. }
  525. // 创建权限
  526. if (d[k].namespace) {
  527. getNames(d[k]).forEach((i) => {
  528. d[k].permission[i] =
  529. `${d[k].namespace.replace(`${id}/`, "")}/${i}`.replace(/\//g, ":");
  530. });
  531. }
  532. // 创建搜索
  533. d[k].search = e.search;
  534. // 创建方法
  535. e.api.forEach((a) => {
  536. // 方法名
  537. const n = a.path.replace("/", "");
  538. if (n && !/[-:]/g.test(n)) {
  539. d[k][n] = a;
  540. }
  541. });
  542. }
  543. }
  544. }
  545. deep(service, 0);
  546. });
  547. }
  548. // 创建 dict
  549. async function createDict() {
  550. let p = "";
  551. switch (config.type) {
  552. case "app":
  553. case "uniapp-x":
  554. p = "/app";
  555. break;
  556. case "admin":
  557. p = "/admin";
  558. break;
  559. }
  560. const url = config.reqUrl + p + "/dict/info/types";
  561. const text = await axios
  562. .get(url)
  563. .then((res) => {
  564. const { code, data } = res.data;
  565. if (code === 1000) {
  566. let v = "string";
  567. if (!lodash.isEmpty(data)) {
  568. v = data.map((e) => `"${e.key}"`).join(" | ");
  569. }
  570. return `type DictKey = ${v}`;
  571. }
  572. })
  573. .catch(() => {
  574. error(`[cool-eps] Error:${url}`);
  575. });
  576. return text || "";
  577. }
  578. // 创建 eps
  579. async function createEps() {
  580. if (config.eps.enable) {
  581. // 获取数据
  582. await getData();
  583. // 创建 service
  584. createService();
  585. // 创建目录
  586. createDir(getEpsPath(), true);
  587. // 创建 json 文件
  588. const isUpdate = createJson();
  589. // 创建描述文件
  590. createDescribe({ service, list });
  591. return {
  592. service,
  593. list,
  594. isUpdate,
  595. };
  596. }
  597. else {
  598. return {
  599. service: {},
  600. list: [],
  601. };
  602. }
  603. }
  604. function getPlugin(name) {
  605. let code = readFile(rootDir(`./src/plugins/${name}/config.ts`));
  606. // 设置插件配置
  607. const set = (key, value) => {
  608. const regex = new RegExp(`(return\\s*{[^}]*?\\b${key}\\b\\s*:\\s*)([^,}]+)`);
  609. if (regex.test(code)) {
  610. code = code.replace(regex, `$1${JSON.stringify(value)}`);
  611. }
  612. else {
  613. const insertPos = code.indexOf("return {") + 8;
  614. code =
  615. code.slice(0, insertPos) +
  616. `\n ${key}: ${JSON.stringify(value)},` +
  617. code.slice(insertPos);
  618. }
  619. };
  620. // 保存插件配置
  621. const save = async () => {
  622. const content = await formatContent(code);
  623. writeFile(rootDir(`./src/plugins/${name}/config.ts`), content);
  624. };
  625. return {
  626. set,
  627. save,
  628. };
  629. }
  630. // 修改插件
  631. async function updatePlugin(options) {
  632. const plugin = getPlugin(options.name);
  633. if (options.enable !== undefined) {
  634. plugin.set("enable", options.enable);
  635. }
  636. await plugin.save();
  637. }
  638. function getPath() {
  639. return rootDir(`.${config.type == "admin" ? "/src" : ""}/config/proxy.ts`);
  640. }
  641. async function updateProxy(data) {
  642. let code = readFile(getPath());
  643. const regex = /const\s+value\s*=\s*['"]([^'"]+)['"]/;
  644. if (regex.test(code)) {
  645. code = code.replace(regex, `const value = '${data.name}'`);
  646. }
  647. writeFile(getPath(), code);
  648. }
  649. function getProxyTarget(proxy) {
  650. const code = readFile(getPath());
  651. const regex = /const\s+value\s*=\s*['"]([^'"]+)['"]/;
  652. const match = code.match(regex);
  653. if (match) {
  654. const value = match[1];
  655. try {
  656. const { target, rewrite } = proxy[`/${value}/`];
  657. return target + rewrite(`/${value}`);
  658. }
  659. catch (err) {
  660. error(`[cool-proxy] Error:${value} → ` + getPath());
  661. return "";
  662. }
  663. }
  664. }
  665. // 创建文件
  666. async function createFile(data) {
  667. const list = lodash.isArray(data) ? data : [data];
  668. for (const item of list) {
  669. const { path: path$1, code } = item;
  670. // 格式化内容
  671. const content = await formatContent(code, {
  672. parser: "vue",
  673. });
  674. // 目录路径
  675. const dir = (path$1 || "").split("/");
  676. // 文件名
  677. const fname = dir.pop();
  678. // 源码路径
  679. const srcPath = `./src/${dir.join("/")}`;
  680. // 创建目录
  681. createDir(srcPath, true);
  682. // 创建文件
  683. fs.createWriteStream(path.join(srcPath, fname), {
  684. flags: "w",
  685. }).write(content);
  686. }
  687. }
  688. function createTag(code, id) {
  689. if (/\.vue$/.test(id)) {
  690. let s;
  691. const str = () => s || (s = new magicString(code));
  692. const { descriptor } = compilerSfc.parse(code);
  693. if (!descriptor.script && descriptor.scriptSetup) {
  694. const res = compilerSfc.compileScript(descriptor, { id });
  695. const { name, lang } = res.attrs;
  696. str().appendLeft(0, `<script lang="${lang}">
  697. import { defineComponent } from 'vue'
  698. export default defineComponent({
  699. name: "${name}"
  700. })
  701. <\/script>`);
  702. return {
  703. map: str().generateMap(),
  704. code: str().toString(),
  705. };
  706. }
  707. }
  708. return null;
  709. }
  710. function base() {
  711. return {
  712. name: "vite-cool-base",
  713. enforce: "pre",
  714. configureServer(server) {
  715. server.middlewares.use(async (req, res, next) => {
  716. function done(data) {
  717. res.writeHead(200, { "Content-Type": "text/html;charset=UTF-8" });
  718. res.end(JSON.stringify(data));
  719. }
  720. if (req.originalUrl?.includes("__cool")) {
  721. const body = await parseJson(req);
  722. switch (req.url) {
  723. // 创建文件
  724. case "/__cool_createFile":
  725. await createFile(body);
  726. break;
  727. // 创建 eps 文件
  728. case "/__cool_eps":
  729. await createEps();
  730. break;
  731. // 更新插件
  732. case "/__cool_updatePlugin":
  733. await updatePlugin(body);
  734. break;
  735. // 设置代理
  736. case "/__cool_updateProxy":
  737. await updateProxy(body);
  738. break;
  739. default:
  740. return done({
  741. code: 1001,
  742. message: "Unknown request",
  743. });
  744. }
  745. done({
  746. code: 1000,
  747. });
  748. }
  749. else {
  750. next();
  751. }
  752. });
  753. },
  754. transform(code, id) {
  755. if (config.nameTag) {
  756. return createTag(code, id);
  757. }
  758. return code;
  759. },
  760. };
  761. }
  762. function demo(enable) {
  763. const virtualModuleIds = ["virtual:demo"];
  764. return {
  765. name: "vite-cool-demo",
  766. enforce: "pre",
  767. resolveId(id) {
  768. if (virtualModuleIds.includes(id)) {
  769. return "\0" + id;
  770. }
  771. },
  772. async load(id) {
  773. if (id === "\0virtual:demo") {
  774. const demo = {};
  775. if (enable) {
  776. const files = await glob.glob(rootDir("./src/modules/demo/views/crud/components") + "/**", {
  777. stat: true,
  778. withFileTypes: true,
  779. });
  780. for (const file of files) {
  781. if (file.isFile()) {
  782. const p = path.join(file.path, file.name);
  783. demo[p
  784. .replace(/\\/g, "/")
  785. .split("src/modules/demo/views/crud/components/")[1]] = fs.readFileSync(p, "utf-8");
  786. }
  787. }
  788. }
  789. return `
  790. export const demo = ${JSON.stringify(demo)};
  791. `;
  792. }
  793. },
  794. };
  795. }
  796. async function createCtx() {
  797. let ctx = {
  798. serviceLang: "Node",
  799. };
  800. if (config.type == "app" || config.type == "uniapp-x") {
  801. const manifest = readFile(rootDir("manifest.json"), true);
  802. // 文件路径
  803. const ctxPath = rootDir("pages.json");
  804. // 页面配置
  805. ctx = readFile(ctxPath, true);
  806. // 原数据,做更新比较用
  807. const ctxData = lodash.cloneDeep(ctx);
  808. // 删除临时页面
  809. ctx.pages = ctx.pages?.filter((e) => !e.isTemp);
  810. ctx.subPackages = ctx.subPackages?.filter((e) => !e.isTemp);
  811. // 加载 uni_modules 配置文件
  812. const files = await glob.glob(rootDir("uni_modules") + "/**/pages_init.json", {
  813. stat: true,
  814. withFileTypes: true,
  815. });
  816. for (const file of files) {
  817. if (file.isFile()) {
  818. const { pages = [], subPackages = [] } = readFile(path.join(file.path, file.name), true);
  819. // 合并到 pages 中
  820. [...pages, ...subPackages].forEach((e) => {
  821. e.isTemp = true;
  822. const isSub = !!e.root;
  823. const d = isSub
  824. ? ctx.subPackages?.find((a) => a.root == e.root)
  825. : ctx.pages?.find((a) => a.path == e.path);
  826. if (d) {
  827. lodash.assign(d, e);
  828. }
  829. else {
  830. if (isSub) {
  831. ctx.subPackages?.unshift(e);
  832. }
  833. else {
  834. ctx.pages?.unshift(e);
  835. }
  836. }
  837. });
  838. }
  839. }
  840. // 排序后检测,避免加载顺序问题
  841. function order(d) {
  842. return {
  843. pages: lodash.orderBy(d.pages, "path"),
  844. subPackages: lodash.orderBy(d.subPackages, "root"),
  845. };
  846. }
  847. // 是否需要更新 pages.json
  848. if (!util.isDeepStrictEqual(order(ctxData), order(ctx))) {
  849. console.log("[cool-ctx] pages updated");
  850. writeFile(ctxPath, JSON.stringify(ctx, null, 4));
  851. }
  852. // appid
  853. ctx.appid = manifest.appid;
  854. }
  855. if (config.type == "admin") {
  856. const list = fs.readdirSync(rootDir("./src/modules"));
  857. ctx.modules = list.filter((e) => !e.includes("."));
  858. await axios
  859. .get(config.reqUrl + "/admin/base/comm/program", {
  860. timeout: 5000,
  861. })
  862. .then((res) => {
  863. const { code, data, message } = res.data;
  864. if (code === 1000) {
  865. ctx.serviceLang = data || "Node";
  866. }
  867. else {
  868. error(`[cool-ctx] ${message}`);
  869. }
  870. })
  871. .catch((err) => {
  872. // console.error(['[cool-ctx] ', err.message])
  873. });
  874. }
  875. return ctx;
  876. }
  877. let svgIcons = [];
  878. function findSvg(dir) {
  879. const arr = [];
  880. const dirs = fs.readdirSync(dir, {
  881. withFileTypes: true,
  882. });
  883. // 获取当前目录的模块名
  884. const moduleName = dir.match(/[/\\](?:src[/\\](?:plugins|modules)[/\\])([^/\\]+)/)?.[1] || "";
  885. for (const d of dirs) {
  886. if (d.isDirectory()) {
  887. arr.push(...findSvg(dir + d.name + "/"));
  888. }
  889. else {
  890. if (path.extname(d.name) == ".svg") {
  891. const baseName = path.basename(d.name, ".svg");
  892. // 判断是否需要跳过拼接模块名
  893. let shouldSkip = config.svg.skipNames?.includes(moduleName);
  894. // 跳过包含icon-
  895. if (baseName.includes("icon-")) {
  896. shouldSkip = true;
  897. }
  898. const iconName = shouldSkip ? baseName : `${moduleName}-${baseName}`;
  899. svgIcons.push(iconName);
  900. const svg = fs.readFileSync(dir + d.name)
  901. .toString()
  902. .replace(/(\r)|(\n)/g, "")
  903. .replace(/<svg([^>+].*?)>/, (_, $2) => {
  904. let width = 0;
  905. let height = 0;
  906. let content = $2.replace(/(width|height)="([^>+].*?)"/g, (_, s2, s3) => {
  907. if (s2 === "width") {
  908. width = s3;
  909. }
  910. else if (s2 === "height") {
  911. height = s3;
  912. }
  913. return "";
  914. });
  915. if (!/(viewBox="[^>+].*?")/g.test($2)) {
  916. content += `viewBox="0 0 ${width} ${height}"`;
  917. }
  918. return `<symbol id="icon-${iconName}" ${content}>`;
  919. })
  920. .replace("</svg>", "</symbol>");
  921. arr.push(svg);
  922. }
  923. }
  924. }
  925. return arr;
  926. }
  927. function compilerSvg() {
  928. svgIcons = [];
  929. return findSvg(rootDir("./src/"))
  930. .map((e) => {
  931. return svgo.optimize(e)?.data || e;
  932. })
  933. .join("");
  934. }
  935. async function createSvg() {
  936. const html = compilerSvg();
  937. const code = `
  938. if (typeof window !== 'undefined') {
  939. function loadSvg() {
  940. const svgDom = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  941. svgDom.style.position = 'absolute';
  942. svgDom.style.width = '0';
  943. svgDom.style.height = '0';
  944. svgDom.setAttribute('xmlns','http://www.w3.org/2000/svg');
  945. svgDom.setAttribute('xmlns:link','http://www.w3.org/1999/xlink');
  946. svgDom.innerHTML = '${html}';
  947. document.body.insertBefore(svgDom, document.body.firstChild);
  948. }
  949. loadSvg();
  950. }
  951. `;
  952. return { code, svgIcons };
  953. }
  954. async function virtual() {
  955. const virtualModuleIds = [
  956. "virtual:eps",
  957. "virtual:ctx",
  958. "virtual:svg-register",
  959. "virtual:svg-icons",
  960. ];
  961. createEps();
  962. return {
  963. name: "vite-cool-virtual",
  964. enforce: "pre",
  965. configureServer(server) {
  966. server.middlewares.use(async (req, res, next) => {
  967. // 页面刷新时触发
  968. if (req.url == "/@vite/client") {
  969. // 重新加载虚拟模块
  970. virtualModuleIds.forEach((vm) => {
  971. const mod = server.moduleGraph.getModuleById(`\0${vm}`);
  972. if (mod) {
  973. server.moduleGraph.invalidateModule(mod);
  974. }
  975. });
  976. }
  977. next();
  978. });
  979. },
  980. handleHotUpdate({ file, server }) {
  981. // 文件修改时触发
  982. if (!["pages.json", "dist", "build/cool", "eps.json", "eps.d.ts"].some((e) => file.includes(e))) {
  983. createCtx();
  984. createEps().then((data) => {
  985. if (data.isUpdate) {
  986. // 通知客户端刷新
  987. (server.hot || server.ws).send({
  988. type: "custom",
  989. event: "eps-update",
  990. data,
  991. });
  992. }
  993. });
  994. }
  995. },
  996. resolveId(id) {
  997. if (virtualModuleIds.includes(id)) {
  998. return "\0" + id;
  999. }
  1000. },
  1001. async load(id) {
  1002. if (id === "\0virtual:eps") {
  1003. const eps = await createEps();
  1004. return `
  1005. export const eps = ${JSON.stringify(eps)}
  1006. `;
  1007. }
  1008. if (id === "\0virtual:ctx") {
  1009. const ctx = await createCtx();
  1010. return `
  1011. export const ctx = ${JSON.stringify(ctx)}
  1012. `;
  1013. }
  1014. if (id == "\0virtual:svg-register") {
  1015. const { code } = await createSvg();
  1016. return code;
  1017. }
  1018. if (id == "\0virtual:svg-icons") {
  1019. const { svgIcons } = await createSvg();
  1020. return `
  1021. export const svgIcons = ${JSON.stringify(svgIcons)}
  1022. `;
  1023. }
  1024. },
  1025. };
  1026. }
  1027. // @ts-ignore
  1028. /**
  1029. * Tailwind CSS 特殊字符映射表
  1030. * 用于将类名中的特殊字符转换为安全字符,避免编译或运行时冲突
  1031. */
  1032. const TAILWIND_SAFE_CHAR_MAP = {
  1033. "[": "-",
  1034. "]": "-",
  1035. "(": "-",
  1036. ")": "-",
  1037. "#": "-h-",
  1038. "!": "-i-",
  1039. "/": "-s-",
  1040. ":": "-c-",
  1041. ",": "-2c-",
  1042. };
  1043. /**
  1044. * Tailwind CSS 常用类名前缀集合
  1045. * 按功能分类,便于维护和扩展
  1046. */
  1047. const TAILWIND_CLASS_PREFIXES = [
  1048. // 间距
  1049. "p-",
  1050. "px-",
  1051. "py-",
  1052. "pt-",
  1053. "pr-",
  1054. "pb-",
  1055. "pl-",
  1056. "m-",
  1057. "mx-",
  1058. "my-",
  1059. "mt-",
  1060. "mr-",
  1061. "mb-",
  1062. "ml-",
  1063. "gap-",
  1064. "gap-x-",
  1065. "gap-y-",
  1066. "space-x-",
  1067. "space-y-",
  1068. "inset-",
  1069. "top-",
  1070. "right-",
  1071. "bottom-",
  1072. "left-",
  1073. // 尺寸
  1074. "w-",
  1075. "h-",
  1076. "min-w-",
  1077. "min-h-",
  1078. "max-w-",
  1079. "max-h-",
  1080. // 排版
  1081. "text-",
  1082. "font-",
  1083. "leading-",
  1084. "tracking-",
  1085. "indent-",
  1086. // 边框
  1087. "border-",
  1088. "border-t-",
  1089. "border-r-",
  1090. "border-b-",
  1091. "border-l-",
  1092. "rounded-",
  1093. "rounded-t-",
  1094. "rounded-r-",
  1095. "rounded-b-",
  1096. "rounded-l-",
  1097. "rounded-tl-",
  1098. "rounded-tr-",
  1099. "rounded-br-",
  1100. "rounded-bl-",
  1101. // 效果
  1102. "shadow-",
  1103. "blur-",
  1104. "brightness-",
  1105. "contrast-",
  1106. "drop-shadow-",
  1107. "grayscale-",
  1108. "hue-rotate-",
  1109. "invert-",
  1110. "saturate-",
  1111. "sepia-",
  1112. "backdrop-blur-",
  1113. "backdrop-brightness-",
  1114. "backdrop-contrast-",
  1115. "backdrop-grayscale-",
  1116. "backdrop-hue-rotate-",
  1117. "backdrop-invert-",
  1118. "backdrop-opacity-",
  1119. "backdrop-saturate-",
  1120. "backdrop-sepia-",
  1121. // 动画
  1122. "transition-",
  1123. "duration-",
  1124. "delay-",
  1125. "animate-",
  1126. // 变换
  1127. "translate-x-",
  1128. "translate-y-",
  1129. "rotate-",
  1130. "scale-",
  1131. "scale-x-",
  1132. "scale-y-",
  1133. "skew-x-",
  1134. "skew-y-",
  1135. "origin-",
  1136. // 布局
  1137. "columns-",
  1138. "break-after-",
  1139. "break-before-",
  1140. "break-inside-",
  1141. // Flexbox 和 Grid
  1142. "basis-",
  1143. "grow-",
  1144. "shrink-",
  1145. "grid-cols-",
  1146. "grid-rows-",
  1147. "col-span-",
  1148. "row-span-",
  1149. "col-start-",
  1150. "col-end-",
  1151. "row-start-",
  1152. "row-end-",
  1153. // SVG
  1154. "stroke-",
  1155. "stroke-w-",
  1156. "fill-",
  1157. ];
  1158. /**
  1159. * Tailwind CSS 颜色变量映射
  1160. * 用于移除不需要的 CSS 变量声明
  1161. */
  1162. const TAILWIND_COLOR_VARS = {
  1163. "--tw-text-opacity": 1,
  1164. "--tw-bg-opacity": 1,
  1165. };
  1166. /**
  1167. * 转换类名中的特殊字符为安全字符
  1168. * @param value 原始类名或值
  1169. * @param isSelector 是否为选择器(true)或普通值(false)
  1170. * @returns 转换后的安全字符串
  1171. */
  1172. function toSafeTailwindClass(value, isSelector = false) {
  1173. // 处理任意值语法(如 w-[100px])
  1174. const arbitrary = value.match(/^(.+?)-\[(.*?)\]$/);
  1175. if (arbitrary) {
  1176. if (isSelector)
  1177. return value;
  1178. const [, prefix, content] = arbitrary;
  1179. const safePrefix = toSafeTailwindClass(prefix, isSelector);
  1180. const safeContent = content.replace(/[^\d.\w]/g, "-");
  1181. return `${safePrefix}-${safeContent}`;
  1182. }
  1183. let safeValue = value;
  1184. // 移除转义字符
  1185. if (safeValue.includes("\\")) {
  1186. safeValue = safeValue.replace(/\\/g, "");
  1187. }
  1188. // 替换特殊字符
  1189. for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) {
  1190. const reg = new RegExp("\\" + char, "g");
  1191. if (reg.test(safeValue)) {
  1192. safeValue = safeValue.replace(reg, rep);
  1193. }
  1194. }
  1195. return safeValue;
  1196. }
  1197. /**
  1198. * 将现代 rgb 格式(如 rgb(234 179 8 / 0.1))转换为标准 rgba 格式
  1199. * @param value rgb 字符串
  1200. * @returns 标准 rgba 字符串
  1201. */
  1202. function rgbToRgba(value) {
  1203. const match = value.match(/rgb\(([\d\s]+)\/\s*([\d.]+)\)/);
  1204. if (match) {
  1205. const [, rgb, alpha] = match;
  1206. const [r, g, b] = rgb.split(/\s+/);
  1207. return `rgba(${r}, ${g}, ${b}, ${alpha})`;
  1208. }
  1209. return value;
  1210. }
  1211. /**
  1212. * PostCSS 插件:将 rem 单位转换为 rpx,并处理 Tailwind 特殊字符
  1213. * @param options 配置项
  1214. * @returns PostCSS 插件对象
  1215. */
  1216. function postcssRemToRpx(options) {
  1217. return {
  1218. postcssPlugin: "vite-cool-uniappx-remToRpx",
  1219. prepare() {
  1220. const handledSelectors = new Set();
  1221. const { remUnit = 16, remPrecision = 6, rpxRatio = 2 } = options;
  1222. const factor = remUnit * rpxRatio;
  1223. return {
  1224. Rule(rule) {
  1225. const sel = rule.selector;
  1226. if (handledSelectors.has(sel))
  1227. return;
  1228. const safeSel = toSafeTailwindClass(sel, true);
  1229. if (safeSel !== sel) {
  1230. rule.selector = safeSel;
  1231. handledSelectors.add(sel);
  1232. }
  1233. },
  1234. Declaration(decl) {
  1235. if (decl.value.includes("/* no-rem */"))
  1236. return;
  1237. if (TAILWIND_COLOR_VARS[decl.prop]) {
  1238. decl.remove();
  1239. return;
  1240. }
  1241. if (decl.value.includes("rgb(") && decl.value.includes("/")) {
  1242. decl.value = rgbToRgba(decl.value);
  1243. }
  1244. if (decl.value.includes("rpx") && decl.parent.selector.includes("text-")) {
  1245. decl.prop = "font-size";
  1246. }
  1247. const parsed = valueParser(decl.value);
  1248. let changed = false;
  1249. parsed.walk((node) => {
  1250. if (node.type === "word") {
  1251. // rem 转 rpx
  1252. const unit = valueParser.unit(node.value);
  1253. if (unit?.unit === "rem") {
  1254. const num = unit.number;
  1255. const precision = (num.split(".")[1] || "").length;
  1256. const rpxVal = (parseFloat(num) * factor)
  1257. .toFixed(precision || remPrecision)
  1258. .replace(/\.?0+$/, "");
  1259. node.value = `${rpxVal}rpx`;
  1260. changed = true;
  1261. }
  1262. // 特殊字符处理
  1263. if (node.value.includes(".") || /[[\]()#!/:,]/.test(node.value)) {
  1264. const safe = toSafeTailwindClass(node.value, true);
  1265. if (safe !== node.value) {
  1266. node.value = safe;
  1267. changed = true;
  1268. }
  1269. }
  1270. }
  1271. // 处理 var(--tw-xxx)
  1272. if (node.type === "function" && node.value === "var") {
  1273. if (node.nodes.length > 0 && node.nodes[0].value.startsWith("--tw-")) {
  1274. node.type = "word";
  1275. node.value = TAILWIND_COLOR_VARS[node.nodes[0].value];
  1276. changed = true;
  1277. }
  1278. }
  1279. });
  1280. if (changed) {
  1281. decl.value = parsed.toString();
  1282. }
  1283. },
  1284. };
  1285. },
  1286. };
  1287. }
  1288. postcssRemToRpx.postcss = true;
  1289. /**
  1290. * Vite 插件:自动转换 .uvue 文件中的 Tailwind 类名为安全字符
  1291. * 并自动注入 rem 转 rpx 的 PostCSS 插件
  1292. * @param options 配置项
  1293. * @returns Vite 插件对象
  1294. */
  1295. function tailwindTransformPlugin(options = {}) {
  1296. const merged = {
  1297. remUnit: 16,
  1298. remPrecision: 6,
  1299. rpxRatio: 2,
  1300. ...options,
  1301. };
  1302. return {
  1303. name: "vite-cool-uniappx-tailwind",
  1304. enforce: "pre",
  1305. config() {
  1306. return {
  1307. css: {
  1308. postcss: {
  1309. plugins: [postcssRemToRpx(merged)],
  1310. },
  1311. },
  1312. };
  1313. },
  1314. transform(code, id) {
  1315. if (!id.includes(".uvue"))
  1316. return null;
  1317. let resultCode = code;
  1318. const tplMatch = resultCode.match(/<template>([\s\S]*?)<\/template>/);
  1319. if (!tplMatch?.[1])
  1320. return null;
  1321. let tpl = tplMatch[1];
  1322. const tplOrigin = tpl;
  1323. TAILWIND_CLASS_PREFIXES.forEach((prefix) => {
  1324. for (const [char, rep] of Object.entries(TAILWIND_SAFE_CHAR_MAP)) {
  1325. const reg = new RegExp(`(${prefix}[^\\s'"]*?\\${char}[^\\s'"]*?)`, "g");
  1326. const matches = [...tpl.matchAll(reg)];
  1327. matches.forEach((m) => {
  1328. const raw = m[1];
  1329. const safe = raw.replace(new RegExp("\\" + char, "g"), rep);
  1330. if (process.env.NODE_ENV === "development") {
  1331. console.log(`类名转换: ${raw} → ${safe}`);
  1332. }
  1333. tpl = tpl.replace(raw, safe);
  1334. });
  1335. }
  1336. });
  1337. if (tpl !== tplOrigin) {
  1338. resultCode = resultCode.replace(tplMatch[0], `<template>${tpl}</template>`);
  1339. return {
  1340. code: resultCode,
  1341. map: { mappings: "" },
  1342. };
  1343. }
  1344. return null;
  1345. },
  1346. };
  1347. }
  1348. /**
  1349. * uniappX 入口,自动注入 Tailwind 类名转换插件
  1350. * @param options 配置项
  1351. * @returns Vite 插件数组
  1352. */
  1353. function uniappX(options) {
  1354. if (config.type == "uniapp-x") {
  1355. return [tailwindTransformPlugin(options?.tailwind)];
  1356. }
  1357. return [];
  1358. }
  1359. function cool(options) {
  1360. // 应用类型,admin | app
  1361. config.type = options.type;
  1362. // 请求地址
  1363. config.reqUrl = getProxyTarget(options.proxy);
  1364. // 是否开启名称标签
  1365. config.nameTag = options.nameTag ?? true;
  1366. // svg
  1367. if (options.svg) {
  1368. lodash.assign(config.svg, options.svg);
  1369. }
  1370. // Eps
  1371. if (options.eps) {
  1372. const { dist, mapping, api, enable = true } = options.eps;
  1373. // 是否开启
  1374. config.eps.enable = enable;
  1375. // 类型
  1376. if (api) {
  1377. config.eps.api = api;
  1378. }
  1379. // 输出目录
  1380. if (dist) {
  1381. config.eps.dist = dist;
  1382. }
  1383. // 匹配规则
  1384. if (mapping) {
  1385. lodash.merge(config.eps.mapping, mapping);
  1386. }
  1387. }
  1388. return [base(), virtual(), uniappX(), demo(options.demo)];
  1389. }
  1390. exports.cool = cool;
  1391. }));