@mindbase/cli 1.0.4 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,370 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import prompts from "prompts";
4
+ import logger from "./Logger";
5
+ import {
6
+ FRONTEND_APP_TYPES,
7
+ getFrontendPackage,
8
+ getAvailableFrontendApps,
9
+ resolveFrontendDependencies,
10
+ } from "./prompts";
11
+
12
+ export interface FrontendConfig {
13
+ name: string;
14
+ description: string;
15
+ author: string;
16
+ port: number;
17
+ proxyTarget: string;
18
+ proxyPath: string;
19
+ appType: string;
20
+ }
21
+
22
+ /**
23
+ * 收集前端项目初始化配置
24
+ */
25
+ export async function collectFrontendConfig(cwd: string): Promise<FrontendConfig> {
26
+ const folderName = path.basename(cwd);
27
+ const availableApps = getAvailableFrontendApps();
28
+
29
+ const response = await prompts([
30
+ {
31
+ type: "text",
32
+ name: "name",
33
+ message: "项目名称",
34
+ initial: folderName,
35
+ validate: (value: string) => value.length > 0 || "项目名称不能为空",
36
+ },
37
+ {
38
+ type: "text",
39
+ name: "description",
40
+ message: "项目描述",
41
+ initial: "MindBase 前端应用",
42
+ },
43
+ {
44
+ type: "text",
45
+ name: "author",
46
+ message: "作者",
47
+ initial: "",
48
+ },
49
+ {
50
+ type: "text",
51
+ name: "port",
52
+ message: "开发服务器端口",
53
+ initial: "5173",
54
+ validate: (value: string) => {
55
+ const val = typeof value === "number" ? value : parseInt(value, 10);
56
+ if (isNaN(val) || val < 1 || val > 65535) {
57
+ return "端口号必须为在 1-65535 之间的数字";
58
+ }
59
+ return true;
60
+ },
61
+ },
62
+ {
63
+ type: "text",
64
+ name: "proxyTarget",
65
+ message: "API 代理目标地址",
66
+ initial: "http://localhost:3000",
67
+ },
68
+ {
69
+ type: "text",
70
+ name: "proxyPath",
71
+
72
+ message: "API 代理路径 (留空则不代理)",
73
+ initial: "/api",
74
+ },
75
+ {
76
+ type: "select",
77
+ name: "appType",
78
+ message: "选择应用形态",
79
+ choices: availableApps.map((app) => ({
80
+ title: app.description,
81
+ value: app.name,
82
+ })),
83
+ initial: 0,
84
+ },
85
+ ]);
86
+
87
+ if (Object.keys(response).length < 7) {
88
+ throw new Error("用户取消了初始化流程");
89
+ }
90
+
91
+ logger.info(`已选择应用形态: ${response.appType}`);
92
+
93
+ return response as FrontendConfig;
94
+ }
95
+
96
+ /**
97
+ * 初始化 package.json
98
+ */
99
+ export function setupFrontendPackageJson(cwd: string, config: FrontendConfig) {
100
+ const pkgPath = path.join(cwd, "package.json");
101
+ let pkg: any = {};
102
+
103
+ if (fs.existsSync(pkgPath)) {
104
+ pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
105
+ logger.info("✓ 检测到已存在的 package.json,正在更新配置...");
106
+ } else {
107
+ pkg = {
108
+ name: config.name,
109
+ version: "1.0.0",
110
+ description: config.description,
111
+ author: config.author,
112
+ private: true,
113
+ type: "module",
114
+ dependencies: {
115
+ axios: "^1.7.0",
116
+ "crypto-js": "^4.2.0",
117
+ dayjs: "^1.11.19",
118
+ "element-plus": "^2.9.0",
119
+ "element-plus/icons-vue": "^2.9.0",
120
+ "lodash-es": "^4.17.21",
121
+ pinia: "^2.2.0",
122
+ "spark-md5": "^3.0.2",
123
+ vue: "^3.5.0",
124
+ "vue-router": "^4.5.0",
125
+ },
126
+ devDependencies: {
127
+ "@vitejs/plugin-vue": "^5.2.0",
128
+ typescript: "^5.6.0",
129
+ vite: "^6.0.0",
130
+ "vue-tsc": "^2.2.0",
131
+ },
132
+ };
133
+ logger.info("✓ 正在生成 package.json...");
134
+ }
135
+
136
+ // 注入标准脚本
137
+ pkg.scripts = pkg.scripts || {};
138
+ pkg.scripts["start"] = "npm run dev";
139
+ pkg.scripts["dev"] = `vite --port ${config.port}`;
140
+ pkg.scripts["build"] = "vue-tsc -b && vite build";
141
+ pkg.scripts["preview"] = "vite preview";
142
+
143
+ // 添加依赖(包含所有依赖包)
144
+ const allPackages = resolveFrontendDependencies(config.appType);
145
+ for (const packageName of allPackages) {
146
+ pkg.dependencies = pkg.dependencies || {};
147
+ pkg.dependencies[packageName] = "^1.0.0";
148
+ }
149
+
150
+ pkg.devDependencies = pkg.devDependencies || {};
151
+ pkg.devDependencies["vue-tsc"] = "^2.2.0";
152
+
153
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
154
+ logger.info("✓ package.json 已就绪");
155
+ }
156
+
157
+ /**
158
+ * 初始化 Vite 配置 vite.config.ts
159
+ */
160
+ export function setupViteConfig(cwd: string, config: FrontendConfig) {
161
+ const viteConfigPath = path.join(cwd, "vite.config.ts");
162
+
163
+ const proxyConfig = config.proxyPath
164
+ ? `{
165
+ '${config.proxyPath}': {
166
+ target: '${config.proxyTarget}',
167
+ changeOrigin: true,
168
+ },
169
+ }`
170
+ : "undefined";
171
+
172
+ const viteConfigContent = `import { defineConfig } from "vite";
173
+ import vue from "@vitejs/plugin-vue";
174
+ import path from "path";
175
+
176
+ export default defineConfig({
177
+ plugins: [vue()],
178
+ resolve: {
179
+ alias: {
180
+ "@": path.resolve(__dirname, "./src"),
181
+ },
182
+ },
183
+ server: {
184
+ port: ${config.port},
185
+ proxy: ${proxyConfig},
186
+ },
187
+ });
188
+ `;
189
+ fs.writeFileSync(viteConfigPath, viteConfigContent);
190
+ logger.info("✓ vite.config.ts 已就绪");
191
+ }
192
+
193
+ /**
194
+ * 初始化 TypeScript 配置
195
+ */
196
+ export function setupFrontendTsConfig(cwd: string) {
197
+ // tsconfig.json
198
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
199
+ if (!fs.existsSync(tsconfigPath)) {
200
+ const tsconfigContent = {
201
+ compilerOptions: {
202
+ target: "ES2022",
203
+ module: "ESNext",
204
+ moduleResolution: "bundler",
205
+ strict: true,
206
+ esModuleInterop: true,
207
+ skipLibCheck: true,
208
+ forceConsistentCasingInFileNames: true,
209
+ resolveJsonModule: true,
210
+ isolatedModules: true,
211
+ noEmit: true,
212
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
213
+ types: ["vite/client"],
214
+ },
215
+ include: ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"],
216
+ };
217
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfigContent, null, 2));
218
+ logger.info("✓ tsconfig.json 已就绪");
219
+ }
220
+ }
221
+
222
+ /**
223
+ * 初始化 index.html
224
+ */
225
+ export function setupIndexHtml(cwd: string, config: FrontendConfig) {
226
+ const htmlPath = path.join(cwd, "index.html");
227
+
228
+ const htmlContent = `<!DOCTYPE html>
229
+ <html lang="zh-CN">
230
+ <head>
231
+ <meta charset="UTF-8" />
232
+ <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
233
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
234
+ <title>${config.name}</title>
235
+ </head>
236
+ <body>
237
+ <div id="app"></div>
238
+ <script type="module" src="/src/main.ts"></script>
239
+ </body>
240
+ </html>
241
+ `;
242
+ fs.writeFileSync(htmlPath, htmlContent);
243
+ logger.info("✓ index.html 已就绪");
244
+ }
245
+
246
+ /**
247
+ * 初始化源文件
248
+ */
249
+ export function setupAppFiles(cwd: string, config: FrontendConfig) {
250
+ const srcDir = path.join(cwd, "src");
251
+ if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir, { recursive: true });
252
+
253
+ // main.ts
254
+ const mainContent = `import {
255
+ createMindBaseApp,
256
+ createGlobComponentMap,
257
+ } from "${getFrontendPackage(config.appType)}";
258
+
259
+ const { app, pinia } = createMindBaseApp({
260
+ router: {},
261
+ componentMap: createGlobComponentMap(import.meta.glob("./views/**/*.vue"), "./views/"),
262
+ });
263
+
264
+ app.mount("#app");
265
+ `;
266
+ fs.writeFileSync(path.join(srcDir, "main.ts"), mainContent);
267
+ logger.info("✓ src/main.ts 已就绪");
268
+
269
+ // App.vue
270
+ const appContent = `<template>
271
+ <router-view />
272
+ </template>
273
+
274
+ <script setup lang="ts">
275
+ // 根组件
276
+ </script>
277
+
278
+ <style>
279
+ html,
280
+ body,
281
+ #app {
282
+ margin: 0;
283
+ padding: 0;
284
+ width: 100%;
285
+ height: 100%;
286
+ }
287
+ </style>
288
+ `;
289
+ fs.writeFileSync(path.join(srcDir, "App.vue"), appContent);
290
+ logger.info("✓ src/App.vue 已就绪");
291
+
292
+ // views 目录
293
+ const viewsDir = path.join(srcDir, "views");
294
+ if (!fs.existsSync(viewsDir)) fs.mkdirSync(viewsDir);
295
+
296
+ // 示例页面
297
+ const exampleView = `<template>
298
+ <div class="home">
299
+ <h1>欢迎使用 ${config.name}</h1>
300
+ </div>
301
+ </template>
302
+
303
+ <script setup lang="ts">
304
+ // 页面逻辑
305
+ </script>
306
+
307
+ <style scoped>
308
+ .home {
309
+ padding: 20px;
310
+ }
311
+ </style>
312
+ `;
313
+ fs.writeFileSync(path.join(viewsDir, "index.vue"), exampleView);
314
+ logger.info("✓ src/views/index.vue 已就绪");
315
+ }
316
+
317
+ /**
318
+ * 创建 public 目录
319
+ */
320
+ export function setupPublicDir(cwd: string) {
321
+ const publicDir = path.join(cwd, "public");
322
+ if (!fs.existsSync(publicDir)) {
323
+ fs.mkdirSync(publicDir, { recursive: true });
324
+ logger.info("✓ public 目录已创建");
325
+ }
326
+ }
327
+
328
+ /**
329
+ * 复制 admin 专用资源(如字体文件)
330
+ * @param cwd 项目根目录
331
+ * @param config 前端配置
332
+ */
333
+ export function copyAdminAssets(cwd: string, config: FrontendConfig) {
334
+ // 只处理 admin-app 类型
335
+ if (config.appType !== "admin-app") {
336
+ return;
337
+ }
338
+
339
+ const sourceDir = path.join(__dirname, "../assets/iconfont");
340
+
341
+ // 检查源目录是否存在
342
+ if (!fs.existsSync(sourceDir)) {
343
+ logger.warn("字体源目录不存在,跳过复制");
344
+ return;
345
+ }
346
+
347
+ // 目标目录:public/assets/mb-iconfont/
348
+ const targetDir = path.join(cwd, "public/assets/mb-iconfont");
349
+ if (!fs.existsSync(targetDir)) {
350
+ fs.mkdirSync(targetDir, { recursive: true });
351
+ }
352
+
353
+ // 复制字体文件
354
+ const fonts = ["iconfont.woff2", "iconfont.woff", "iconfont.ttf"];
355
+ let copiedCount = 0;
356
+
357
+ fonts.forEach((font) => {
358
+ const sourcePath = path.join(sourceDir, font);
359
+ const targetPath = path.join(targetDir, font);
360
+
361
+ if (fs.existsSync(sourcePath)) {
362
+ fs.copyFileSync(sourcePath, targetPath);
363
+ copiedCount++;
364
+ }
365
+ });
366
+
367
+ if (copiedCount > 0) {
368
+ logger.info(`✓ 已复制 ${copiedCount} 个字体文件到 public/assets/mb-iconfont/`);
369
+ }
370
+ }
@@ -0,0 +1,219 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
2
+
3
+ const LOG_LEVELS = {
4
+ debug: 0,
5
+ info: 1,
6
+ warn: 2,
7
+ error: 3,
8
+ silent: 4,
9
+ } as const;
10
+
11
+ let currentLogLevel: LogLevel = "info";
12
+
13
+ const COLORS: Record<LogLevel | "reset", string> = {
14
+ debug: "\x1b[36m",
15
+ info: "\x1b[32m",
16
+ warn: "\x1b[33m",
17
+ error: "\x1b[31m",
18
+ silent: "\x1b[0m",
19
+ reset: "\x1b[0m",
20
+ };
21
+
22
+ /**
23
+ * 格式化时间戳
24
+ */
25
+ function formatTimestamp(): string {
26
+ const now = new Date();
27
+ const year = now.getFullYear();
28
+ const month = String(now.getMonth() + 1).padStart(2, "0");
29
+ const day = String(now.getDate()).padStart(2, "0");
30
+ const hours = String(now.getHours()).padStart(2, "0");
31
+ const minutes = String(now.getMinutes()).padStart(2, "0");
32
+ const seconds = String(now.getSeconds()).padStart(2, "0");
33
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
34
+ }
35
+
36
+ /**
37
+ * 计算字符串在终端中的视觉宽度
38
+ * 修复:区分单宽符号 (如 ✓) 和双宽符号 (如 中文、Emoji)
39
+ */
40
+ function getVisualWidth(str: string): number {
41
+ let width = 0;
42
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
43
+ for (const { segment } of segmenter.segment(str)) {
44
+ const charCode = segment.charCodeAt(0);
45
+
46
+ // 1. 基础 ASCII
47
+ if (charCode <= 255) {
48
+ width += 1;
49
+ continue;
50
+ }
51
+
52
+ // 2. CJK 字符集范围 (常用中文、标点、全角符号)
53
+ const isCJK =
54
+ (charCode >= 0x4e00 && charCode <= 0x9fff) || // CJK Unified Ideographs
55
+ (charCode >= 0x3000 && charCode <= 0x303f) || // CJK Symbols and Punctuation
56
+ (charCode >= 0xff00 && charCode <= 0xffef); // Fullwidth Forms
57
+
58
+ // 3. Emoji 或代理对 (通常 segment.length > 1)
59
+ const isMultiByte = segment.length > 1;
60
+
61
+ if (isCJK || isMultiByte) {
62
+ width += 2;
63
+ } else {
64
+ // 4. 其他特殊符号 (如 ✓, ★, ☎) 在大多数终端占 1 格
65
+ width += 1;
66
+ }
67
+ }
68
+ return width;
69
+ }
70
+
71
+ /**
72
+ * 截断字符串以适应视觉宽度,从头部截断并保留末尾
73
+ */
74
+ function truncateToWidth(str: string, maxWidth: number): string {
75
+ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
76
+ const segments = Array.from(segmenter.segment(str)).map((s) => s.segment);
77
+
78
+ if (getVisualWidth(str) <= maxWidth) return str;
79
+
80
+ let currentWidth = 3; // 为 "..." 预留
81
+ let result = "";
82
+
83
+ // 从后往前遍历 segments
84
+ for (let i = segments.length - 1; i >= 0; i--) {
85
+ const segment = segments[i];
86
+ const segmentWidth = getVisualWidth(segment);
87
+ if (currentWidth + segmentWidth > maxWidth) break;
88
+ result = segment + result;
89
+ currentWidth += segmentWidth;
90
+ }
91
+ return "..." + result;
92
+ }
93
+
94
+ function formatMessage(level: LogLevel, message: string): string {
95
+ const timestamp = formatTimestamp();
96
+ const terminalWidth = process.stdout.columns || 80;
97
+ const timeStr = ` ${timestamp}`;
98
+ const timeWidth = getVisualWidth(timeStr);
99
+ const maxMessageWidth = terminalWidth - timeWidth - 2;
100
+
101
+ let displayMessage = message;
102
+ const messageWidth = getVisualWidth(displayMessage);
103
+
104
+ if (messageWidth > maxMessageWidth) {
105
+ displayMessage = truncateToWidth(displayMessage, maxMessageWidth);
106
+ }
107
+
108
+ const currentMsgWidth = getVisualWidth(displayMessage);
109
+ const paddingCount = Math.max(0, terminalWidth - currentMsgWidth - timeWidth);
110
+ const padding = " ".repeat(paddingCount);
111
+
112
+ return `${COLORS[level]}${displayMessage}${padding}${timeStr}${COLORS.reset}`;
113
+ }
114
+
115
+ const startupTime = Date.now();
116
+
117
+ // 标签填充函数(目标显示宽度 8,空格分散到字符间)
118
+ function padTag(tag: string, targetWidth = 8): string {
119
+ const currentWidth = getVisualWidth(tag);
120
+ const padding = Math.max(0, targetWidth - currentWidth);
121
+
122
+ if (padding === 0) return tag;
123
+
124
+ // 将空格均匀分散到字符之间
125
+ const chars = [...tag]; // 按码点分割
126
+ const gapCount = chars.length + 1; // 间隙数 = 字符数 + 1(前后和中间)
127
+
128
+ // 计算每个间隙的空格数
129
+ const baseSpaces = Math.floor(padding / gapCount);
130
+ const extraSpaces = padding % gapCount;
131
+
132
+ let result = "";
133
+ for (let i = 0; i < chars.length; i++) {
134
+ // 每个间隙的空格 = 基础空格 + 额外空格(前 extraSpaces 个间隙多1个)
135
+ const spaces = baseSpaces + (i < extraSpaces ? 1 : 0);
136
+ result += " ".repeat(spaces) + chars[i];
137
+ }
138
+ // 最后一个间隙
139
+ result += " ".repeat(baseSpaces + (chars.length < extraSpaces ? 1 : 0));
140
+
141
+ return result;
142
+ }
143
+
144
+ function startupMessage(message: string, tag?: string): string {
145
+ const diff = Date.now() - startupTime;
146
+ const timeStr = diff.toString().padStart(6, "0");
147
+ const tagStr = tag ? `【${padTag(tag)}】` : "";
148
+ return `${COLORS["warn"]}[${timeStr}] ${tagStr}${message}${COLORS["reset"]}`;
149
+ }
150
+
151
+ function shouldLog(level: LogLevel): boolean {
152
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentLogLevel];
153
+ }
154
+
155
+ function formatArgs(args: any[]): string {
156
+ return args
157
+ .map((arg) => {
158
+ if (arg instanceof Error) {
159
+ return arg.message;
160
+ }
161
+ if (typeof arg === "object") {
162
+ return JSON.stringify(arg, null, 2);
163
+ }
164
+ return String(arg);
165
+ })
166
+ .join(" ");
167
+ }
168
+
169
+ export const logger = {
170
+ debug(...args: any[]): void {
171
+ if (shouldLog("debug")) {
172
+ console.log(formatMessage("debug", formatArgs(args)));
173
+ }
174
+ },
175
+
176
+ info(...args: any[]): void {
177
+ if (shouldLog("info")) {
178
+ console.log(formatMessage("info", formatArgs(args)));
179
+ }
180
+ },
181
+
182
+ warn(...args: any[]): void {
183
+ if (shouldLog("warn")) {
184
+ console.warn(formatMessage("warn", formatArgs(args)));
185
+ }
186
+ },
187
+
188
+ error(...args: any[]): void {
189
+ if (shouldLog("error")) {
190
+ // 先打印带颜色的格式化消息
191
+ console.error(formatMessage("error", formatArgs(args)));
192
+ // 如果有 Error 对象,用 console.error 单独打印以显示堆栈
193
+ const errorArg = args.find((arg) => arg instanceof Error);
194
+ if (errorArg) {
195
+ console.error(errorArg);
196
+ }
197
+ }
198
+ },
199
+
200
+ startup(tagOrMessage: string, ...args: any[]): void {
201
+ // 如果有额外参数,第一个是标签,其余是消息
202
+ // 否则第一个参数是消息,无标签
203
+ if (args.length > 0) {
204
+ console.log(startupMessage(formatArgs(args), tagOrMessage));
205
+ } else {
206
+ console.log(startupMessage(tagOrMessage));
207
+ }
208
+ },
209
+
210
+ setLevel(level: LogLevel): void {
211
+ currentLogLevel = level;
212
+ },
213
+
214
+ getLevel(): LogLevel {
215
+ return currentLogLevel;
216
+ },
217
+ };
218
+
219
+ export default logger;