@htyf-mp/cli 0.2.0-alpha.2 → 0.2.0-alpha.4
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/README.md +1 -1
- package/package.json +20 -6
- package/src/HtyfImportsPlugin.mjs +190 -0
- package/src/build.mjs +146 -0
- package/src/clean.mjs +83 -0
- package/src/command-executor.mjs +133 -0
- package/src/config.mjs +52 -0
- package/src/constants.mjs +54 -0
- package/src/debug.mjs +132 -0
- package/src/file-system.mjs +96 -0
- package/src/index.mjs +48 -1084
- package/src/logger.mjs +72 -0
- package/src/project-initializer.mjs +247 -0
- package/src/shared-output.json +1 -2
- package/src/template-processor.mjs +137 -0
- package/src/utils-functions.mjs +76 -0
- package/src/utils.mjs +4 -63
- package/src/validators.mjs +82 -0
- package/src/webpack.config.mjs +16 -4
package/src/index.mjs
CHANGED
|
@@ -5,1103 +5,73 @@
|
|
|
5
5
|
* 用于管理多应用项目的开发、构建和部署
|
|
6
6
|
*
|
|
7
7
|
* @author CLI Team
|
|
8
|
-
* @version 2.
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
}
|
|
30
|
+
// 全局状态管理,用于优雅退出
|
|
31
|
+
let isExiting = false;
|
|
32
|
+
let currentProcess = null;
|
|
444
33
|
|
|
445
34
|
/**
|
|
446
|
-
*
|
|
35
|
+
* 优雅退出处理
|
|
447
36
|
*/
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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}`);
|
|
37
|
+
function handleGracefulExit() {
|
|
38
|
+
if (isExiting) {
|
|
39
|
+
// 如果已经在退出中,强制退出
|
|
40
|
+
Logger.warn('强制退出...');
|
|
987
41
|
process.exit(1);
|
|
988
42
|
}
|
|
989
|
-
}
|
|
990
43
|
|
|
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;
|
|
44
|
+
isExiting = true;
|
|
45
|
+
Logger.info('\n正在退出...');
|
|
1002
46
|
|
|
1003
|
-
//
|
|
1004
|
-
|
|
1005
|
-
if (!fs.existsSync(dirPath)) return 0;
|
|
1006
|
-
let size = 0;
|
|
47
|
+
// 如果有正在运行的进程,尝试关闭
|
|
48
|
+
if (currentProcess) {
|
|
1007
49
|
try {
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
size += calculateDirectorySize(itemPath);
|
|
1013
|
-
} else {
|
|
1014
|
-
size += fs.statSync(itemPath).size;
|
|
1015
|
-
}
|
|
50
|
+
if (typeof currentProcess.close === 'function') {
|
|
51
|
+
currentProcess.close();
|
|
52
|
+
} else if (typeof currentProcess.kill === 'function') {
|
|
53
|
+
currentProcess.kill('SIGTERM');
|
|
1016
54
|
}
|
|
1017
55
|
} catch (error) {
|
|
1018
|
-
Logger.
|
|
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++;
|
|
56
|
+
Logger.debug(`关闭进程时出错: ${error.message}`);
|
|
1081
57
|
}
|
|
1082
58
|
}
|
|
1083
59
|
|
|
1084
|
-
//
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
60
|
+
// 延迟退出,给清理操作一些时间
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
Logger.info('再见!');
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}, 100);
|
|
1089
65
|
}
|
|
1090
66
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
};
|
|
67
|
+
// 注册全局信号处理(使用 once 确保只注册一次)
|
|
68
|
+
// 注意:某些模块(如 debug.mjs)可能会移除这些监听器并添加自己的处理
|
|
69
|
+
process.on('SIGINT', handleGracefulExit);
|
|
70
|
+
process.on('SIGTERM', handleGracefulExit);
|
|
1102
71
|
|
|
1103
72
|
/**
|
|
1104
73
|
* 主启动函数
|
|
74
|
+
* @param {string} action - 操作类型
|
|
1105
75
|
*/
|
|
1106
76
|
async function Start(action) {
|
|
1107
77
|
const actionNames = {
|
|
@@ -1157,17 +127,7 @@ async function Start(action) {
|
|
|
1157
127
|
});
|
|
1158
128
|
|
|
1159
129
|
if (confirmClean) {
|
|
1160
|
-
|
|
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
|
-
}
|
|
130
|
+
await cleanShell(cleanType);
|
|
1171
131
|
} else {
|
|
1172
132
|
Logger.info('已取消清理操作');
|
|
1173
133
|
}
|
|
@@ -1181,6 +141,7 @@ async function Start(action) {
|
|
|
1181
141
|
Logger.error('应用配置不存在,请先在app.json中配置htyf');
|
|
1182
142
|
return;
|
|
1183
143
|
}
|
|
144
|
+
|
|
1184
145
|
const version = appInfo?.version;
|
|
1185
146
|
const incrementedVersion = version ? incrementVersion(version) : '1.0.0';
|
|
1186
147
|
const { versionName } = await inquirer.prompt([
|
|
@@ -1241,7 +202,7 @@ async function Start(action) {
|
|
|
1241
202
|
|
|
1242
203
|
// 主程序入口
|
|
1243
204
|
console.log('\n' + boxen(
|
|
1244
|
-
gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.
|
|
205
|
+
gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.2.0') + '\n' +
|
|
1245
206
|
chalk.gray('用于管理多应用项目的开发、构建和部署') + '\n' +
|
|
1246
207
|
chalk.gray(`项目目录:`) + '\n\n' +
|
|
1247
208
|
chalk.gray(`${projectPath}`) + '\n',
|
|
@@ -1255,7 +216,7 @@ console.log('\n' + boxen(
|
|
|
1255
216
|
|
|
1256
217
|
// 设置日志级别
|
|
1257
218
|
if (process.argv.includes('--debug')) {
|
|
1258
|
-
|
|
219
|
+
setLogLevel(LOG_LEVELS.DEBUG);
|
|
1259
220
|
Logger.debug('调试模式已启用');
|
|
1260
221
|
}
|
|
1261
222
|
|
|
@@ -1331,10 +292,13 @@ inquirer
|
|
|
1331
292
|
}
|
|
1332
293
|
})
|
|
1333
294
|
.catch((error) => {
|
|
295
|
+
// 如果是用户取消(Ctrl+C),不显示错误
|
|
296
|
+
if (error.isTtyError || error.name === 'AbortError') {
|
|
297
|
+
handleGracefulExit();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
1334
300
|
Logger.error(`程序启动失败: ${error.message}`);
|
|
1335
|
-
|
|
301
|
+
if (!isExiting) {
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
1336
304
|
});
|
|
1337
|
-
|
|
1338
|
-
export const getMiniAppScriptId = (appid, version) => {
|
|
1339
|
-
return `${appid}_v${version?.replace(/\./gi, '_')}`
|
|
1340
|
-
}
|