@htyf-mp/cli 0.2.0-alpha.2 → 0.2.0-alpha.3

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.
package/src/index.mjs CHANGED
@@ -5,1103 +5,31 @@
5
5
  * 用于管理多应用项目的开发、构建和部署
6
6
  *
7
7
  * @author CLI Team
8
- * @version 2.1.0
8
+ * @version 2.2.0
9
9
  */
10
10
 
11
11
  import inquirer from 'inquirer';
12
- import shell from 'shelljs';
13
- import fs from 'fs';
14
- import { fileURLToPath } from 'node:url';
15
12
  import path from 'path';
16
13
  import lodash from 'lodash';
17
14
  import fse from 'fs-extra';
18
- import superstatic from 'superstatic';
19
- import AdmZip from 'adm-zip';
20
- import QRCode from 'qrcode';
21
- import portfinder from 'portfinder';
22
- import si from 'systeminformation';
23
15
  import chalk from 'chalk';
24
- import ora from 'ora';
25
16
  import boxen from 'boxen';
26
17
  import gradient from 'gradient-string';
27
- import select from '@inquirer/select';
28
- import { input, confirm } from '@inquirer/prompts';
29
- import { execa } from 'execa';
30
- import { v4 as uuidv4 } from 'uuid';
31
- import md5 from 'md5';
32
- import os from 'os';
18
+ import { confirm } from '@inquirer/prompts';
33
19
  import { getProjectRoot } from './utils.mjs';
34
-
35
- // 获取当前文件的绝对路径和目录
36
- const __filename = fileURLToPath(import.meta.url);
37
- const __dirname = path.dirname(__filename);
38
-
39
-
20
+ import { Logger, setLogLevel } from './logger.mjs';
21
+ import { CONSTANTS, ACTION_TYPES, LOG_LEVELS } from './constants.mjs';
22
+ import { ProjectInitializer } from './project-initializer.mjs';
23
+ import { mpBuildShell } from './build.mjs';
24
+ import { mpDebugShell } from './debug.mjs';
25
+ import { cleanShell } from './clean.mjs';
26
+ import { incrementVersion, updateAppConfig } from './utils-functions.mjs';
40
27
 
41
28
  const projectPath = getProjectRoot();
42
29
 
43
- // 常量定义
44
- const CONSTANTS = {
45
- SRC_DIR: path.join(projectPath, './src'),
46
- TEMP_DIR: path.join(projectPath, './.htyf'),
47
- MP_OUTPUT_DIR: path.join(projectPath, './dist'),
48
- LOGS_DIR: path.join(projectPath, './.logs'),
49
- DEFAULT_VERSION_NAME: '1.0.0',
50
- DEFAULT_VERSION_CODE: '1',
51
- VERSION_NAME_REGEX: /^\d+(\.\d+){2}$/g,
52
- VERSION_CODE_REGEX: /^\d+$/g,
53
- WEBPACK_PORT: 8081,
54
- BUNDLE_ENTRY_FILE: 'index.js',
55
- BUNDLE_PLATFORM: 'ios',
56
- MAX_RETRY_COUNT: 3,
57
- DEFAULT_TIMEOUT: 30000,
58
- // 新增常量
59
- TEMPLATE_REPOS: {
60
- GITHUB: 'https://github.com/htyf-mp-community/htyf-mp.git',
61
- CODING: 'https://e.coding.net/dagouzhi/hongtangyun_mobile/htyf-mp.git'
62
- },
63
- TEMPLATE_TYPES: {
64
- EXPO: 'expo',
65
- }
66
- };
67
-
68
- /**
69
- * 版本号递增函数
70
- * @param {string} version - 当前版本号 (格式: x.y.z)
71
- * @returns {string} - 递增后的版本号
72
- */
73
- function incrementVersion(version) {
74
- if (!version || !CONSTANTS.VERSION_NAME_REGEX.test(version)) {
75
- return CONSTANTS.DEFAULT_VERSION_NAME;
76
- }
77
-
78
- const parts = version.split('.').map(Number);
79
- parts[2] = (parts[2] || 0) + 1; // 补丁版本号加1
80
-
81
- return parts.join('.');
82
- }
83
-
84
- // 操作类型枚举
85
- const ACTION_TYPES = {
86
- START: 'start',
87
- INIT: 'init',
88
- MP_BUILD: 'mp-build',
89
- MP_DEBUG: 'mp-debug',
90
- CLEAN: 'clean',
91
- QUIT: 'quit'
92
- };
93
-
94
- // 日志级别枚举
95
- const LOG_LEVELS = {
96
- DEBUG: 0,
97
- INFO: 1,
98
- WARN: 2,
99
- ERROR: 3
100
- };
101
-
102
- // 当前日志级别
103
- let currentLogLevel = LOG_LEVELS.INFO;
104
-
105
- /**
106
- * 日志工具类
107
- */
108
- class Logger {
109
- static debug(message, ...args) {
110
- if (currentLogLevel <= LOG_LEVELS.DEBUG) {
111
- console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
112
- }
113
- }
114
-
115
- static info(message, ...args) {
116
- if (currentLogLevel <= LOG_LEVELS.INFO) {
117
- console.log(chalk.blue(`[INFO] ${message}`), ...args);
118
- }
119
- }
120
-
121
- static warn(message, ...args) {
122
- if (currentLogLevel <= LOG_LEVELS.WARN) {
123
- console.log(chalk.yellow(`[WARN] ${message}`), ...args);
124
- }
125
- }
126
-
127
- static error(message, ...args) {
128
- if (currentLogLevel <= LOG_LEVELS.ERROR) {
129
- console.log(chalk.red(`[ERROR] ${message}`), ...args);
130
- }
131
- }
132
-
133
- static success(message, ...args) {
134
- console.log(chalk.green(`[SUCCESS] ${message}`), ...args);
135
- }
136
-
137
- static writeToFile(message, level = 'INFO') {
138
- const logDir = CONSTANTS.LOGS_DIR;
139
- const logFile = path.join(logDir, `cli-${new Date().toISOString().split('T')[0]}.log`);
140
-
141
- try {
142
- fse.ensureDirSync(logDir);
143
- const timestamp = new Date().toISOString();
144
- const logEntry = `[${timestamp}] [${level}] ${message}\n`;
145
- fs.appendFileSync(logFile, logEntry);
146
- } catch (error) {
147
- console.error('写入日志文件失败:', error.message);
148
- }
149
- }
150
- }
151
-
152
- /**
153
- * 项目初始化配置类
154
- */
155
- class ProjectConfig {
156
- constructor() {
157
- this.validators = {
158
- appName: /^([a-z]+)(-[a-z0-9]+)*$/,
159
- displayName: /^[\u4e00-\u9fa5a-zA-Z0-9]{2,4}$/
160
- };
161
-
162
- this.templates = {
163
- [CONSTANTS.TEMPLATE_TYPES.EXPO]: {
164
- name: 'Expo (https://docs.expo.dev/)',
165
- tempPath: 'mini-apps-template',
166
- files: {
167
- src: 'expo_src',
168
- app: 'expo.App.tsx',
169
- index: 'expo.index.js',
170
- babel: 'expo.babel.config.js',
171
- config: 'expo.project.dgz.json'
172
- }
173
- },
174
- };
175
- }
176
-
177
- validateAppName(name) {
178
- return this.validators.appName.test(name);
179
- }
180
-
181
- validateDisplayName(name) {
182
- return this.validators.displayName.test(name);
183
- }
184
-
185
- generateAppId() {
186
- return md5(uuidv4({
187
- random: [0x10, 0x91, 0x56, 0xbe, 0xc4, 0xfb, 0xc1, 0xea, 0x71, 0xb4, 0xef, 0xe1, 0x67, 0x1c, 0x58, 0x36]
188
- }));
189
- }
190
-
191
- createProjectConfig(appName, displayName, appid) {
192
- return {
193
- type: 'app',
194
- name: appName,
195
- projectname: displayName,
196
- appid: appid,
197
- appUrlConfig: 'https://xxx.cos.ap-chengdu.myqcloud.com/assets/testMiniApps/htyanimation/app.json',
198
- zipUrl: 'https://xxxxx.myqcloud.com/assets/testMiniApps/xxxxx/outputs/dist.dgz'
199
- };
200
- }
201
- }
202
-
203
- /**
204
- * 模板处理器类
205
- */
206
- class TemplateProcessor {
207
- constructor(config) {
208
- this.config = config;
209
- }
210
-
211
- async cloneRepository(repoUrl, tmpdir) {
212
- Logger.info('正在克隆模板仓库...');
213
- try {
214
- await execa('git', ['clone', repoUrl, tmpdir]);
215
- Logger.success('模板仓库克隆完成');
216
- } catch (error) {
217
- if (error.code === 'ENOTFOUND') {
218
- throw new Error('网络连接失败,请检查网络连接或尝试使用其他镜像源');
219
- } else if (error.code === 'ECONNREFUSED') {
220
- throw new Error('连接被拒绝,请检查防火墙设置或网络配置');
221
- } else if (error.exitCode === 128) {
222
- throw new Error('Git仓库访问失败,请检查仓库地址是否正确');
223
- } else {
224
- throw new Error(`克隆失败: ${error.message}`);
225
- }
226
- }
227
- }
228
-
229
- cleanupUnusedTemplates(tmpdir, templateType) {
230
- const pathsToRemove = [
231
- path.join(tmpdir, 'cli'),
232
- path.join(tmpdir, '.git'),
233
- path.join(tmpdir, 'docs'),
234
- path.join(tmpdir, 'README.md')
235
- ];
236
-
237
- // 根据模板类型移除不需要的模板
238
- if (templateType === CONSTANTS.TEMPLATE_TYPES.TARO || templateType === CONSTANTS.TEMPLATE_TYPES.EXPO) {
239
- pathsToRemove.push(path.join(tmpdir, 'mini-game-template'));
240
- } else if (templateType === CONSTANTS.TEMPLATE_TYPES.GAME_COCOS) {
241
- pathsToRemove.push(path.join(tmpdir, 'mini-apps-template'));
242
- }
243
-
244
- pathsToRemove.forEach(path => {
245
- try {
246
- if (fs.existsSync(path)) {
247
- fse.removeSync(path);
248
- Logger.debug(`已移除: ${path}`);
249
- }
250
- } catch (err) {
251
- Logger.warn(`移除失败: ${path}`, err.message);
252
- }
253
- });
254
- }
255
-
256
- processTemplate(appRootPath, templateType) {
257
- const template = this.config.templates[templateType];
258
- if (!template) {
259
- throw new Error(`不支持的模板类型: ${templateType}`);
260
- }
261
-
262
- const tempPath = path.join(appRootPath, '__TEMP__');
263
-
264
- if (templateType === CONSTANTS.TEMPLATE_TYPES.TARO) {
265
- this.processTaroTemplate(appRootPath, tempPath, template);
266
- } else if (templateType === CONSTANTS.TEMPLATE_TYPES.EXPO) {
267
- this.processExpoTemplate(appRootPath, tempPath, template);
268
- } else if (templateType === CONSTANTS.TEMPLATE_TYPES.GAME_COCOS) {
269
- // 预留游戏模板处理
270
- Logger.info('游戏模板处理功能开发中...');
271
- }
272
- }
273
-
274
- processTaroTemplate(appRootPath, tempPath, template) {
275
- Logger.info('正在处理 Taro 模板...');
276
-
277
- // 复制模板文件
278
- Object.entries(template.files).forEach(([key, fileName]) => {
279
- const srcPath = path.join(tempPath, fileName);
280
- const destPath = path.join(appRootPath, key === 'src' ? './src' : `./${key === 'app' ? 'App.tsx' : key === 'index' ? 'index.js' : key === 'babel' ? 'babel.config.js' : 'project.dgz.json'}`);
281
-
282
- if (fs.existsSync(srcPath)) {
283
- fse.copySync(srcPath, destPath);
284
- Logger.debug(`已复制: ${fileName} -> ${destPath}`);
285
- }
286
- });
287
-
288
- // 更新 package.json
289
- const pkgPath = path.join(appRootPath, './package.json');
290
- if (fs.existsSync(pkgPath)) {
291
- const pkgInfo = fse.readJSONSync(pkgPath);
292
- pkgInfo.scripts = {
293
- ...(pkgInfo.scripts || {}),
294
- ...template.scripts
295
- };
296
-
297
- // 移除不需要的脚本
298
- delete pkgInfo.scripts.web;
299
- delete pkgInfo.scripts['web:dev'];
300
-
301
- fse.writeJSONSync(pkgPath, pkgInfo, { spaces: 2 });
302
- Logger.info('package.json 已更新');
303
- }
304
- }
305
-
306
- processExpoTemplate(appRootPath, tempPath, template) {
307
- Logger.info('正在处理 Expo 模板...');
308
-
309
- // 复制模板文件
310
- Object.entries(template.files).forEach(([key, fileName]) => {
311
- const srcPath = path.join(tempPath, fileName);
312
- const destPath = path.join(appRootPath, key === 'src' ? './src' : `./${key === 'app' ? 'App.tsx' : key === 'index' ? 'index.js' : key === 'babel' ? 'babel.config.js' : 'project.dgz.json'}`);
313
-
314
- if (fs.existsSync(srcPath)) {
315
- fse.copySync(srcPath, destPath);
316
- Logger.debug(`已复制: ${fileName} -> ${destPath}`);
317
- }
318
- });
319
- }
320
- }
321
-
322
- /**
323
- * 配置验证工具类
324
- */
325
- class ConfigValidator {
326
- static validateAppConfig(config) {
327
- const errors = [];
328
- const warnings = [];
329
-
330
- // 必需字段检查
331
- const requiredFields = ['productName', 'versionName', 'versionCode'];
332
- requiredFields.forEach(field => {
333
- if (!config[field]) {
334
- errors.push(`缺少必需字段: ${field}`);
335
- }
336
- });
337
-
338
- // 平台配置检查
339
- if (config.iosBuild) {
340
- if (!config.iosBuild.appId) {
341
- warnings.push('iOS配置缺少appId');
342
- }
343
- }
344
-
345
- if (config.andBuild) {
346
- if (!config.andBuild.appId) {
347
- warnings.push('Android配置缺少appId');
348
- }
349
- }
350
-
351
- return { errors, warnings, isValid: errors.length === 0 };
352
- }
353
-
354
- static validateNetworkConfig() {
355
- return new Promise(async (resolve) => {
356
- try {
357
- const networkInfo = await si.networkInterfaces();
358
- const defaultNet = (lodash.isArray(networkInfo) ? networkInfo : [networkInfo])
359
- .find(i => lodash.get(i, 'default', false));
360
-
361
- if (!defaultNet || !defaultNet.ip4) {
362
- resolve({ isValid: false, error: '无法获取网络配置' });
363
- return;
364
- }
365
-
366
- resolve({
367
- isValid: true,
368
- host: defaultNet.ip4,
369
- interface: defaultNet.iface
370
- });
371
- } catch (error) {
372
- resolve({ isValid: false, error: error.message });
373
- }
374
- });
375
- }
376
- }
377
-
378
- /**
379
- * 文件系统工具类
380
- */
381
- class FileSystemUtils {
382
- static ensureDirectory(dirPath) {
383
- try {
384
- fse.ensureDirSync(dirPath);
385
- Logger.debug(`目录已确保存在: ${dirPath}`);
386
- return true;
387
- } catch (error) {
388
- Logger.error(`创建目录失败: ${dirPath}`, error.message);
389
- return false;
390
- }
391
- }
392
-
393
- static cleanDirectory(dirPath) {
394
- try {
395
- if (fs.existsSync(dirPath)) {
396
- fse.removeSync(dirPath);
397
- Logger.info(`目录已清理: ${dirPath}`);
398
- }
399
- return true;
400
- } catch (error) {
401
- Logger.error(`清理目录失败: ${dirPath}`, error.message);
402
- return false;
403
- }
404
- }
405
-
406
- static backupFile(filePath) {
407
- try {
408
- if (fs.existsSync(filePath)) {
409
- const backupPath = `${filePath}.backup.${Date.now()}`;
410
- fse.copySync(filePath, backupPath);
411
- Logger.info(`文件已备份: ${backupPath}`);
412
- return backupPath;
413
- }
414
- return null;
415
- } catch (error) {
416
- Logger.error(`备份文件失败: ${filePath}`, error.message);
417
- return null;
418
- }
419
- }
420
-
421
- static getFileSize(filePath) {
422
- try {
423
- const stats = fs.statSync(filePath);
424
- return stats.size;
425
- } catch (error) {
426
- return 0;
427
- }
428
- }
429
-
430
- static formatFileSize(bytes) {
431
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
432
- if (bytes === 0) return '0 Bytes';
433
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
434
- return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
435
- }
436
-
437
- static validatePath(path) {
438
- // if (/[ - fa5]/gi.test(path)) {
439
- // return { isValid: false, error: '路径中包含中文字符' };
440
- // }
441
- return { isValid: true };
442
- }
443
- }
444
-
445
- /**
446
- * 命令执行工具类
447
- */
448
- class CommandExecutor {
449
- static async execute(command, options = {}) {
450
- const {
451
- silent = false,
452
- fatal = true,
453
- timeout = CONSTANTS.DEFAULT_TIMEOUT,
454
- retryCount = CONSTANTS.MAX_RETRY_COUNT,
455
- description = '执行命令'
456
- } = options;
457
-
458
- const spinner = ora(`${description}: ${command}`).start();
459
-
460
- for (let attempt = 1; attempt <= retryCount; attempt++) {
461
- try {
462
- Logger.debug(`执行命令 (尝试 ${attempt}/${retryCount}): ${command}`);
463
-
464
- const result = await new Promise((resolve, reject) => {
465
- const timeoutId = setTimeout(() => {
466
- reject(new Error(`命令执行超时 (${timeout}ms)`));
467
- }, timeout);
468
-
469
- shell.exec(command, {
470
- silent: silent,
471
- fatal: false,
472
- async: false
473
- }, (code, stdout, stderr) => {
474
- clearTimeout(timeoutId);
475
- resolve({ code, stdout, stderr });
476
- });
477
- });
478
-
479
- if (result.code === 0) {
480
- spinner.succeed(`${description} 成功`);
481
- Logger.debug(`命令输出: ${result.stdout}`);
482
- return result;
483
- } else {
484
- throw new Error(`命令执行失败,退出码: ${result.code}, 错误: ${result.stderr}`);
485
- }
486
- } catch (error) {
487
- if (attempt === retryCount) {
488
- spinner.fail(`${description} 失败 (${retryCount} 次尝试后)`);
489
- Logger.error(`命令执行失败: ${error.message}`);
490
-
491
- if (fatal) {
492
- process.exit(1);
493
- }
494
- throw error;
495
- } else {
496
- Logger.warn(`命令执行失败,准备重试 (${attempt}/${retryCount}): ${error.message}`);
497
- await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // 递增延迟
498
- }
499
- }
500
- }
501
- }
502
-
503
- static async executeWithProgress(command, options = {}) {
504
- const { description = '执行命令', progressCallback } = options;
505
-
506
- return new Promise((resolve, reject) => {
507
- const child = shell.exec(command, {
508
- silent: false,
509
- fatal: false,
510
- async: true
511
- });
512
-
513
- let output = '';
514
-
515
- child.stdout?.on('data', (data) => {
516
- output += data;
517
- if (progressCallback) {
518
- progressCallback(data.toString());
519
- }
520
- });
521
-
522
- child.stderr?.on('data', (data) => {
523
- output += data;
524
- Logger.warn(`命令输出: ${data}`);
525
- });
526
-
527
- child.on('close', (code) => {
528
- if (code === 0) {
529
- Logger.success(`${description} 完成`);
530
- resolve({ code, output });
531
- } else {
532
- Logger.error(`${description} 失败,退出码: ${code}`);
533
- reject(new Error(`命令执行失败,退出码: ${code}`));
534
- }
535
- });
536
-
537
- child.on('error', (error) => {
538
- Logger.error(`${description} 出错: ${error.message}`);
539
- reject(error);
540
- });
541
- });
542
- }
543
- }
544
-
545
- /**
546
- * 项目初始化器类
547
- */
548
- class ProjectInitializer {
549
- constructor() {
550
- this.config = new ProjectConfig();
551
- this.processor = new TemplateProcessor(this.config);
552
- }
553
-
554
- async initialize() {
555
- const spinner = ora('正在初始化项目...').start();
556
-
557
- try {
558
- // 1. 获取用户输入
559
- const userInputs = await this.getUserInputs();
560
-
561
- // 2. 验证输入
562
- const validation = this.validateInputs(userInputs);
563
- if (!validation.isValid) {
564
- spinner.fail('输入验证失败');
565
- Logger.error(validation.error);
566
- return;
567
- }
568
-
569
- // 3. 检查目录
570
- const rootPath = path.join(process.cwd(), userInputs.appName);
571
- if (fs.existsSync(rootPath)) {
572
- spinner.fail('目录已存在');
573
- Logger.error(`目录已存在: ${rootPath}`);
574
- return;
575
- }
576
-
577
- // 4. 创建项目
578
- await this.createProject(userInputs, rootPath, spinner);
579
-
580
- // 5. 显示成功信息
581
- this.showSuccessInfo(userInputs.appName, rootPath);
582
-
583
- } catch (error) {
584
- spinner.fail('项目初始化失败');
585
- Logger.error('初始化失败:', error.message);
586
- if (error.exitCode === 128) {
587
- Logger.error('Error: 目录已经存在。');
588
- } else if (error.code === 'ENOTFOUND') {
589
- Logger.error('网络连接失败,请检查网络连接或尝试使用其他镜像源。');
590
- } else if (error.code === 'ECONNREFUSED') {
591
- Logger.error('连接被拒绝,请检查防火墙设置或网络配置。');
592
- }
593
- }
594
- }
595
-
596
- async getUserInputs() {
597
- Logger.info('\n开始收集项目信息...\n');
598
-
599
- const appName = await input({
600
- message: '输入应用程序目录名称',
601
- default: 'my-htyf-mp',
602
- validate: (input) => {
603
- if (!this.config.validateAppName(input)) {
604
- return '请以 my-app-name 格式输入您的应用程序英文目录名称';
605
- }
606
- return true;
607
- }
608
- });
609
-
610
- const displayName = await input({
611
- message: '输入应用程序名称',
612
- default: '小程序',
613
- validate: (input) => {
614
- if (!this.config.validateDisplayName(input)) {
615
- return '只能中文、字母和数字, 不能包含特殊字符, 限2-4个字符';
616
- }
617
- return true;
618
- }
619
- });
620
-
621
- const templateType = await select({
622
- message: '请选择模板类型?',
623
- choices: Object.entries(this.config.templates).map(([key, template]) => ({
624
- name: template.name,
625
- value: key
626
- }))
627
- });
628
-
629
- Logger.info(`已选择模板: ${this.config.templates[templateType].name}`);
630
-
631
- const repoType = await select({
632
- message: '请选择模板镜像?',
633
- choices: [
634
- { name: 'GitHub (最新)', value: CONSTANTS.TEMPLATE_REPOS.GITHUB },
635
- { name: 'Coding (最快)', value: CONSTANTS.TEMPLATE_REPOS.CODING }
636
- ]
637
- });
638
-
639
- return { appName, displayName, templateType, repoType };
640
- }
641
-
642
- validateInputs(inputs) {
643
- const pathValidation = FileSystemUtils.validatePath(inputs.appName);
644
- if (!pathValidation.isValid) {
645
- return { isValid: false, error: pathValidation.error };
646
- }
647
-
648
- return { isValid: true };
649
- }
650
-
651
- async createProject(userInputs, rootPath, spinner) {
652
- const { appName, displayName, templateType, repoType } = userInputs;
653
-
654
- // 创建临时目录
655
- spinner.text = '正在准备临时目录...';
656
- const tmpdir = path.join(os.tmpdir(), md5('__HTYF__'), appName);
657
- fse.emptyDirSync(tmpdir);
658
-
659
- try {
660
- // 克隆仓库
661
- spinner.text = '正在克隆模板仓库...';
662
- await this.processor.cloneRepository(repoType, tmpdir);
663
-
664
- // 确定应用根路径
665
- const template = this.config.templates[templateType];
666
- const appRootPath = path.join(tmpdir, template.tempPath);
667
-
668
- // 清理不需要的文件
669
- spinner.text = '正在清理模板文件...';
670
- this.processor.cleanupUnusedTemplates(tmpdir, templateType);
671
-
672
- // 处理模板
673
- spinner.text = '正在处理模板...';
674
- this.processor.processTemplate(appRootPath, templateType);
675
-
676
- // 生成配置
677
- spinner.text = '正在生成项目配置...';
678
- const appid = this.config.generateAppId();
679
- const projectConfig = this.config.createProjectConfig(appName, displayName, appid);
680
-
681
- // 写入配置文件
682
- const configPath = path.join(appRootPath, 'project.dgz.json');
683
- fs.writeFileSync(configPath, JSON.stringify(projectConfig, undefined, 2));
684
-
685
- // 清理临时文件
686
- spinner.text = '正在清理临时文件...';
687
- const tempPath = path.join(appRootPath, '__TEMP__');
688
- if (fs.existsSync(tempPath)) {
689
- fse.removeSync(tempPath);
690
- }
691
-
692
- // 移动到目标目录
693
- spinner.text = '正在创建项目目录...';
694
- fse.moveSync(appRootPath, rootPath);
695
-
696
- // 清理临时目录
697
- fse.removeSync(tmpdir);
698
-
699
- spinner.succeed('项目创建完成');
700
-
701
- } catch (error) {
702
- // 清理临时目录
703
- if (fs.existsSync(tmpdir)) {
704
- fse.removeSync(tmpdir);
705
- }
706
- throw error;
707
- }
708
- }
709
-
710
- showSuccessInfo(appName, rootPath) {
711
- console.log('\n' + boxen(
712
- gradient.rainbow('🎉 项目创建成功!') + '\n\n' +
713
- chalk.cyan('项目名称: ') + chalk.white(appName) + '\n' +
714
- chalk.cyan('项目路径: ') + chalk.white(rootPath) + '\n\n' +
715
- chalk.yellow('下一步操作:') + '\n' +
716
- chalk.white(` cd ${appName}`) + '\n' +
717
- chalk.white(' npm install') + '\n' +
718
- chalk.white(' npm run dev:weapp') + '\n\n' +
719
- chalk.blue('常用命令:') + '\n' +
720
- chalk.white(' npm run build:weapp # 构建微信小程序') + '\n' +
721
- chalk.white(' npm run build:h5 # 构建H5版本') + '\n' +
722
- chalk.white(' npm run build:rn # 构建React Native版本') + '\n\n' +
723
- chalk.gray('💡 提示: 使用 --debug 参数可以查看详细的调试信息'),
724
- {
725
- padding: 1,
726
- margin: 1,
727
- borderStyle: 'round',
728
- borderColor: 'green'
729
- }
730
- ));
731
- }
732
- }
733
-
734
- /**
735
- * 验证版本号格式
736
- */
737
- function validateVersion(versionName, versionCode) {
738
- const isVersionNameValid = CONSTANTS.VERSION_NAME_REGEX.test(versionName);
739
- const isVersionCodeValid = CONSTANTS.VERSION_CODE_REGEX.test(versionCode);
740
-
741
- if (!isVersionNameValid) {
742
- Logger.error(`版本名称格式错误: ${versionName},应为 x.y.z 格式`);
743
- }
744
-
745
- if (!isVersionCodeValid) {
746
- Logger.error(`版本代码格式错误: ${versionCode},应为数字格式`);
747
- }
748
-
749
- return isVersionNameValid && isVersionCodeValid;
750
- }
751
-
752
- /**
753
- * 更新应用配置
754
- */
755
- function updateAppConfig(newConfig) {
756
- try {
757
- const configPath = path.join(projectPath, 'app.json');
758
- const localConfig = fse.readJsonSync(configPath);
759
- const _newConfig = {
760
- ...localConfig,
761
- 'htyf': {
762
- ...localConfig.htyf,
763
- ...newConfig
764
- }
765
- }
766
-
767
- fs.writeFileSync(configPath, JSON.stringify(_newConfig, undefined, 2), 'utf-8');
768
- Logger.success(`配置文件已更新: ${configPath}`);
769
-
770
- return true;
771
- } catch (error) {
772
- Logger.error(`更新配置文件失败: ${error.message}`);
773
- return false;
774
- }
775
- }
776
-
777
- /**
778
- * 构建小程序包
779
- */
780
- async function mpBuildShell(newAppInfo) {
781
- Logger.info(`开始构建小程序包...`);
782
- Logger.info(`应用: ${newAppInfo.name}`);
783
- Logger.info(`应用ID: ${newAppInfo.appid}`);
784
- Logger.info(`版本: ${newAppInfo.version}`);
785
- Logger.info(`平台: ${CONSTANTS.BUNDLE_PLATFORM}`);
786
-
787
- // 创建临时目录
788
- const tempPath = CONSTANTS.TEMP_DIR;
789
- if (!FileSystemUtils.ensureDirectory(tempPath)) {
790
- return;
791
- }
792
-
793
- const mpOutputPath = CONSTANTS.MP_OUTPUT_DIR;
794
- if (!FileSystemUtils.ensureDirectory(mpOutputPath)) {
795
- return;
796
- }
797
-
798
- const version = newAppInfo.version;
799
- const appid = newAppInfo.appid;
800
- const scriptName = getMiniAppScriptId(appid, version);
801
-
802
- const appJson = {
803
- type: 'app',
804
- engines: '2.0.0',
805
- name: newAppInfo.name,
806
- appid: appid,
807
- version: version,
808
- appUrlConfig: newAppInfo.appUrlConfig || `${newAppInfo.host}/app.json`,
809
- zipUrl: newAppInfo.zipUrl || `${newAppInfo.host}/dist.dgz`
810
- }
811
- const outputPath = path.join(mpOutputPath, './dist');
812
- const rootIndexPath = path.join(tempPath, '../index.tsx');
813
- // fs.writeFileSync(rootIndexPath, `import '../${newAppInfo.entry}';`);
814
-
815
- const mpOptions = {
816
- name: scriptName,
817
- filename: `${scriptName}.bundle`,
818
- exposes: {
819
- 'App': newAppInfo.entry,
820
- },
821
- outputPath: path.join(mpOutputPath, './build'),
822
- extraChunksPath: outputPath,
823
- }
824
-
825
- const appJsonPath = path.join(mpOutputPath, 'app.json');
826
- fs.writeFileSync(appJsonPath, JSON.stringify(appJson, undefined, 2));
827
- const webpackConfigPath = path.join(__dirname, 'webpack.config.mjs');
828
-
829
- const command = `npx cross-env APP_ROOT_INDEX_PATH=${rootIndexPath} APP_EXPOSES_OPTIONS=${encodeURI(JSON.stringify(mpOptions))} npx webpack --config ${webpackConfigPath}`;
830
-
831
- try {
832
- await CommandExecutor.execute(command, {
833
- description: '构建小程序包',
834
- timeout: 120000
835
- });
836
-
837
- fs.copyFileSync(appJsonPath, path.join(outputPath, `./app.json`));
838
- const zipPath = await handleZip(outputPath, path.join(outputPath, `../dist.dgz`));
839
- Logger.success(`压缩包已创建: ${zipPath}`);
840
- return zipPath;
841
- } catch (error) {
842
- Logger.error(`小程序构建失败: ${error.message}`);
843
- throw error;
844
- }
845
- }
846
-
847
- /**
848
- * 压缩文件夹为zip文件
849
- */
850
- const handleZip = async (inputPath, outputPath) => {
851
- Logger.info(`开始压缩文件夹: ${inputPath}`);
852
- Logger.info(`输出文件: ${outputPath}`);
853
-
854
- if (!fs.existsSync(inputPath)) {
855
- throw new Error(`输入路径不存在: ${inputPath}`);
856
- }
857
-
858
- const stats = fs.statSync(inputPath);
859
- if (!stats.isDirectory()) {
860
- throw new Error(`输入路径不是目录: ${inputPath}`);
861
- }
862
-
863
- const files = fs.readdirSync(inputPath);
864
- Logger.debug(`目录内容: ${files.length} 个文件/文件夹`);
865
-
866
- if (files.length === 0) {
867
- Logger.warn(`警告: 目录为空,将创建空的zip文件`);
868
- }
869
-
870
- const zipPath = outputPath;
871
- const admzip = new AdmZip();
872
-
873
- try {
874
- await admzip.addLocalFolderPromise(inputPath, {
875
- filter: (filename) => {
876
- if (filename.endsWith('.map')) {
877
- Logger.debug(`过滤文件: ${filename}`);
878
- return false;
879
- }
880
- Logger.debug(`添加文件: ${filename}`);
881
- return true;
882
- }
883
- });
884
-
885
- await admzip.writeZipPromise(zipPath);
886
- Logger.success(`压缩完成: ${zipPath}`);
887
-
888
- const zipStats = fs.statSync(zipPath);
889
- Logger.info(`ZIP文件大小: ${FileSystemUtils.formatFileSize(zipStats.size)}`);
890
-
891
- return zipPath;
892
- } catch (error) {
893
- Logger.error(`压缩失败: ${error.message}`);
894
- throw error;
895
- }
896
- };
897
-
898
- /**
899
- * 小程序真机调试
900
- */
901
- async function mpDebugShell(newAppInfo) {
902
- try {
903
- const zipPath = await mpBuildShell(newAppInfo);
904
- const assetsPath = path.dirname(zipPath);
905
- const appJsonPath = path.join(assetsPath, 'app.json');
906
-
907
- Logger.info(`资源路径: ${assetsPath}`);
908
- const applocaljson = fse.readJsonSync(appJsonPath);
909
-
910
- // 验证网络配置
911
- const networkValidation = await ConfigValidator.validateNetworkConfig();
912
- if (!networkValidation.isValid) {
913
- throw new Error(`网络配置验证失败: ${networkValidation.error}`);
914
- }
915
-
916
- const host = networkValidation.host;
917
- const port = await portfinder.getPortPromise();
918
-
919
- const app = superstatic.server({
920
- port: port,
921
- address: host,
922
- cwd: assetsPath
923
- });
924
-
925
- const server = app.listen(function () {
926
- const address = server.address();
927
-
928
- const debugerAppUrlConfig = `http://${host}:${address.port}/app.json`;
929
- const debugerZip = `http://${host}:${address.port}/dist.dgz`;
930
- const args = {
931
- ...applocaljson,
932
- development: true,
933
- "name": applocaljson.name,
934
- "appid": applocaljson.appid,
935
- "version": applocaljson.version,
936
- appUrlConfig: debugerAppUrlConfig,
937
- zipUrl: debugerZip,
938
- }
939
-
940
- fse.writeJSONSync(appJsonPath, {
941
- ...fse.readJSONSync(appJsonPath),
942
- appUrlConfig: debugerAppUrlConfig,
943
- zipUrl: debugerZip,
944
- });
945
-
946
- const qrcodeUrl = `https://share.dagouzhi.com/#/pages/index/index?data=${encodeURIComponent(JSON.stringify(args))}`;
947
-
948
- // 显示调试信息
949
- console.log('\n');
950
- console.log(boxen(
951
- gradient.rainbow('小程序真机调试') + '\n\n' +
952
- chalk.cyan('服务器地址: ') + chalk.white(`${host}:${address.port}`) + '\n' +
953
- chalk.cyan('应用名称: ') + chalk.white(applocaljson.name) + '\n' +
954
- chalk.cyan('应用ID: ') + chalk.white(applocaljson.appid) + '\n' +
955
- chalk.cyan('版本: ') + chalk.white(applocaljson.version),
956
- {
957
- padding: 1,
958
- margin: 1,
959
- borderStyle: 'round',
960
- borderColor: 'cyan'
961
- }
962
- ));
963
-
964
- console.log('\n' + chalk.yellow('调试配置:'));
965
- console.log(JSON.stringify(args, undefined, 2));
966
-
967
- console.log('\n' + chalk.green('调试链接:'));
968
- console.log(qrcodeUrl);
969
-
970
- console.log('\n' + chalk.blue('二维码:'));
971
- printQrcode(qrcodeUrl);
972
-
973
- Logger.info('调试服务器已启动,按 Ctrl+C 停止');
974
- });
975
-
976
- // 优雅关闭
977
- process.on('SIGINT', () => {
978
- Logger.info('正在关闭调试服务器...');
979
- server.close(() => {
980
- Logger.success('调试服务器已关闭');
981
- process.exit(0);
982
- });
983
- });
984
-
985
- } catch (error) {
986
- Logger.error(`小程序调试失败: ${error.message}`);
987
- process.exit(1);
988
- }
989
- }
990
-
991
- /**
992
- * 清理临时文件
993
- *
994
- * @param {string} cleanType - 清理类型 ('all', 'build', 'temp', 'logs', 'cache', 或应用名称)
995
- */
996
- async function cleanShell(cleanType = 'all') {
997
- Logger.info('开始清理文件...');
998
-
999
- const pathsToClean = [];
1000
- let totalSizeBefore = 0;
1001
- let totalSizeAfter = 0;
1002
-
1003
- // 计算清理前的总大小
1004
- const calculateDirectorySize = (dirPath) => {
1005
- if (!fs.existsSync(dirPath)) return 0;
1006
- let size = 0;
1007
- try {
1008
- const items = fse.readdirSync(dirPath, { withFileTypes: true });
1009
- for (const item of items) {
1010
- const itemPath = path.join(dirPath, item.name);
1011
- if (item.isDirectory()) {
1012
- size += calculateDirectorySize(itemPath);
1013
- } else {
1014
- size += fs.statSync(itemPath).size;
1015
- }
1016
- }
1017
- } catch (error) {
1018
- Logger.warn(`计算目录大小失败: ${dirPath}`, error.message);
1019
- }
1020
- return size;
1021
- };
1022
-
1023
- if (cleanType === 'build') {
1024
- // 只清理构建输出
1025
- const buildPath = CONSTANTS.MP_OUTPUT_DIR;
1026
- pathsToClean.push(buildPath);
1027
- totalSizeBefore += calculateDirectorySize(buildPath);
1028
- Logger.info('清理构建输出文件...');
1029
- } else if (cleanType === 'temp') {
1030
- // 只清理临时文件
1031
- const tempPath = CONSTANTS.TEMP_DIR;
1032
- pathsToClean.push(tempPath);
1033
- totalSizeBefore += calculateDirectorySize(tempPath);
1034
- Logger.info('清理临时文件...');
1035
- } else if (cleanType === 'logs') {
1036
- // 清理日志文件
1037
- const logsPath = CONSTANTS.LOGS_DIR;
1038
- pathsToClean.push(logsPath);
1039
- totalSizeBefore += calculateDirectorySize(logsPath);
1040
- Logger.info('清理日志文件...');
1041
- } else if (cleanType === 'cache') {
1042
- // 清理缓存文件
1043
- const cachePaths = [
1044
- path.join(projectPath, 'node_modules/.cache'),
1045
- path.join(projectPath, '.cache'),
1046
- path.join(projectPath, 'dist/.cache')
1047
- ];
1048
- pathsToClean.push(...cachePaths);
1049
- cachePaths.forEach(cachePath => {
1050
- totalSizeBefore += calculateDirectorySize(cachePath);
1051
- });
1052
- Logger.info('清理缓存文件...');
1053
- } else if (cleanType && cleanType !== 'all') {
1054
- // 清理指定应用的文件
1055
- const appTempPath = path.join(CONSTANTS.TEMP_DIR, cleanType);
1056
- const appBuildPath = path.join(CONSTANTS.MP_OUTPUT_DIR, cleanType);
1057
- pathsToClean.push(appTempPath, appBuildPath);
1058
- totalSizeBefore += calculateDirectorySize(appTempPath) + calculateDirectorySize(appBuildPath);
1059
- Logger.info(`清理应用 ${cleanType} 的文件...`);
1060
- } else {
1061
- // 清理所有文件
1062
- const allPaths = [
1063
- path.join(CONSTANTS.TEMP_DIR),
1064
- path.join(CONSTANTS.MP_OUTPUT_DIR),
1065
- path.join(CONSTANTS.LOGS_DIR),
1066
- path.join(projectPath, 'node_modules/.cache'),
1067
- path.join(projectPath, '.cache'),
1068
- path.join(projectPath, 'dist/.cache')
1069
- ];
1070
- pathsToClean.push(...allPaths);
1071
- allPaths.forEach(cleanPath => {
1072
- totalSizeBefore += calculateDirectorySize(cleanPath);
1073
- });
1074
- Logger.info('清理所有临时文件...');
1075
- }
1076
-
1077
- let cleanedCount = 0;
1078
- for (const cleanPath of pathsToClean) {
1079
- if (FileSystemUtils.cleanDirectory(cleanPath)) {
1080
- cleanedCount++;
1081
- }
1082
- }
1083
-
1084
- // 计算清理后的总大小(应该为0)
1085
- totalSizeAfter = 0;
1086
-
1087
- const freedSpace = FileSystemUtils.formatFileSize(totalSizeBefore);
1088
- Logger.success(`清理完成,已清理 ${cleanedCount} 个目录,释放空间: ${freedSpace}`);
1089
- }
1090
-
1091
- /**
1092
- * 打印二维码
1093
- */
1094
- const printQrcode = async (url) => {
1095
- try {
1096
- let terminalStr = await QRCode.toString(url, { type: 'terminal', small: true });
1097
- console.log(terminalStr);
1098
- } catch (error) {
1099
- Logger.error(`生成二维码失败: ${error.message}`);
1100
- }
1101
- };
1102
-
1103
30
  /**
1104
31
  * 主启动函数
32
+ * @param {string} action - 操作类型
1105
33
  */
1106
34
  async function Start(action) {
1107
35
  const actionNames = {
@@ -1157,17 +85,7 @@ async function Start(action) {
1157
85
  });
1158
86
 
1159
87
  if (confirmClean) {
1160
- if (cleanType === 'all') {
1161
- await cleanShell();
1162
- } else if (cleanType === 'build') {
1163
- await cleanShell('build');
1164
- } else if (cleanType === 'temp') {
1165
- await cleanShell('temp');
1166
- } else if (cleanType === 'logs') {
1167
- await cleanShell('logs');
1168
- } else if (cleanType === 'cache') {
1169
- await cleanShell('cache');
1170
- }
88
+ await cleanShell(cleanType);
1171
89
  } else {
1172
90
  Logger.info('已取消清理操作');
1173
91
  }
@@ -1181,6 +99,7 @@ async function Start(action) {
1181
99
  Logger.error('应用配置不存在,请先在app.json中配置htyf');
1182
100
  return;
1183
101
  }
102
+
1184
103
  const version = appInfo?.version;
1185
104
  const incrementedVersion = version ? incrementVersion(version) : '1.0.0';
1186
105
  const { versionName } = await inquirer.prompt([
@@ -1241,7 +160,7 @@ async function Start(action) {
1241
160
 
1242
161
  // 主程序入口
1243
162
  console.log('\n' + boxen(
1244
- gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.1.0') + '\n' +
163
+ gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.2.0') + '\n' +
1245
164
  chalk.gray('用于管理多应用项目的开发、构建和部署') + '\n' +
1246
165
  chalk.gray(`项目目录:`) + '\n\n' +
1247
166
  chalk.gray(`${projectPath}`) + '\n',
@@ -1255,7 +174,7 @@ console.log('\n' + boxen(
1255
174
 
1256
175
  // 设置日志级别
1257
176
  if (process.argv.includes('--debug')) {
1258
- currentLogLevel = LOG_LEVELS.DEBUG;
177
+ setLogLevel(LOG_LEVELS.DEBUG);
1259
178
  Logger.debug('调试模式已启用');
1260
179
  }
1261
180
 
@@ -1334,7 +253,3 @@ inquirer
1334
253
  Logger.error(`程序启动失败: ${error.message}`);
1335
254
  process.exit(1);
1336
255
  });
1337
-
1338
- export const getMiniAppScriptId = (appid, version) => {
1339
- return `${appid}_v${version?.replace(/\./gi, '_')}`
1340
- }