@htyf-mp/cli 0.1.3 → 0.2.0-beta.1
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/package.json +22 -5
- package/src/ListImportsPlugin.mjs +98 -0
- package/src/index.mjs +825 -0
- package/src/shared-output.json +82 -0
- package/src/webpack.config.mjs +161 -0
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@htyf-mp/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.0-beta.1",
|
|
4
4
|
"description": "红糖云服app小程序模板-cli",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": "
|
|
6
|
+
"bin": "src/index.mjs",
|
|
7
7
|
"scripts": {
|
|
8
|
+
"start": "node src/index.mjs",
|
|
8
9
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
@@ -20,14 +21,30 @@
|
|
|
20
21
|
"url": "https://github.com/htyf-mp-community/htyf-mp.git"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
24
|
+
"@callstack/repack": "^5.1.3",
|
|
25
|
+
"@callstack/repack-plugin-expo-modules": "^5.1.3",
|
|
26
|
+
"@callstack/repack-plugin-reanimated": "^5.1.3",
|
|
23
27
|
"@inquirer/prompts": "^3.1.1",
|
|
24
|
-
"
|
|
28
|
+
"adm-zip": "0.5.10",
|
|
29
|
+
"boxen": "^8.0.1",
|
|
30
|
+
"chalk": "^5.4.1",
|
|
25
31
|
"cli-spinners": "^2.9.0",
|
|
26
32
|
"commander": "^11.0.0",
|
|
33
|
+
"cross-env": "^7.0.3",
|
|
27
34
|
"execa": "^8.0.1",
|
|
28
|
-
"fs-extra": "^11.
|
|
35
|
+
"fs-extra": "^11.3.0",
|
|
36
|
+
"gradient-string": "^3.0.0",
|
|
37
|
+
"inquirer": "^12.8.2",
|
|
38
|
+
"lodash": "^4.17.21",
|
|
29
39
|
"md5": "^2.3.0",
|
|
30
40
|
"ora": "^7.0.1",
|
|
31
|
-
"
|
|
41
|
+
"portfinder": "^1.0.37",
|
|
42
|
+
"qrcode": "^1.5.4",
|
|
43
|
+
"shelljs": "^0.10.0",
|
|
44
|
+
"superstatic": "^9.2.0",
|
|
45
|
+
"systeminformation": "^5.27.7",
|
|
46
|
+
"terser-webpack-plugin": "^5.3.14",
|
|
47
|
+
"uuid": "^9.0.1",
|
|
48
|
+
"webpack": "^5.100.2"
|
|
32
49
|
}
|
|
33
50
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const sharedPkg = fse.readJSONSync(path.join(__dirname, 'shared-output.json'))
|
|
8
|
+
|
|
9
|
+
function trimTemplateStrings(strings, ...values) {
|
|
10
|
+
let fullString = strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '');
|
|
11
|
+
return fullString.replace(/^[ \t]+/gm, '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class AnalyzeThirdPartyModulesPlugin {
|
|
15
|
+
constructor(appConfig) {
|
|
16
|
+
this.config = appConfig;
|
|
17
|
+
}
|
|
18
|
+
apply(compiler) {
|
|
19
|
+
const config = this.config;
|
|
20
|
+
compiler.hooks.emit.tapAsync('AnalyzeThirdPartyModulesPlugin', (compilation, callback) => {
|
|
21
|
+
let thirdPartyModules = new Set();
|
|
22
|
+
|
|
23
|
+
compilation.modules.forEach(module => {
|
|
24
|
+
if (module.context && module.context.includes('node_modules')) {
|
|
25
|
+
let packageName = module.context.split('node_modules/')[1].split('/')[0];
|
|
26
|
+
if (packageName.startsWith('@')) {
|
|
27
|
+
packageName = module.context.split('node_modules/')[1].split('/').splice(0,2).join('/');
|
|
28
|
+
}
|
|
29
|
+
const ios_path = path.join(__dirname, '../node_modules/', packageName, 'ios');
|
|
30
|
+
const and_path = path.join(__dirname, '../node_modules/', packageName, 'android');
|
|
31
|
+
const isNative = fse.pathExistsSync(ios_path) || fse.pathExistsSync(and_path);
|
|
32
|
+
if (isNative) {
|
|
33
|
+
thirdPartyModules.add(packageName);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
const nonsupport = []
|
|
38
|
+
thirdPartyModules = Array.from(thirdPartyModules);
|
|
39
|
+
for (const key in thirdPartyModules) {
|
|
40
|
+
const pkg = thirdPartyModules[key];
|
|
41
|
+
if (!sharedPkg[pkg]) {
|
|
42
|
+
if (
|
|
43
|
+
pkg !== 'expo-splash-screen' &&
|
|
44
|
+
pkg !== '@callstack/repack' &&
|
|
45
|
+
pkg !== 'react-native-iap'
|
|
46
|
+
) {
|
|
47
|
+
nonsupport.push(pkg)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (nonsupport.length) {
|
|
52
|
+
const msgArr = ['红糖云服暂不支持以上第三方native组件']
|
|
53
|
+
console.log(' ')
|
|
54
|
+
console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
|
|
55
|
+
console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
|
|
56
|
+
console.log(' ')
|
|
57
|
+
console.log('红糖云服暂不支持以下第三方native组件: ')
|
|
58
|
+
for (const key in nonsupport) {
|
|
59
|
+
const element = nonsupport[key];
|
|
60
|
+
const _msg = `${Number(key) + 1}. ${element}`;
|
|
61
|
+
console.log(_msg)
|
|
62
|
+
msgArr.push(_msg)
|
|
63
|
+
}
|
|
64
|
+
console.log(' ')
|
|
65
|
+
console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
|
|
66
|
+
console.log('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
|
|
67
|
+
console.log(' ')
|
|
68
|
+
console.log(' ')
|
|
69
|
+
callback(msgArr.join(`\n`));
|
|
70
|
+
} else {
|
|
71
|
+
callback();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (config.codeSigning) {
|
|
75
|
+
compiler.hooks.emit.tapAsync('WrapCodePlugin', (compilation, callback) => {
|
|
76
|
+
// 遍历所有编译过的资源文件
|
|
77
|
+
for (const filename in compilation.assets) {
|
|
78
|
+
if (compilation.assets.hasOwnProperty(filename)) {
|
|
79
|
+
// 获取文件内容
|
|
80
|
+
let source = compilation.assets[filename].source();
|
|
81
|
+
|
|
82
|
+
// 包裹代码
|
|
83
|
+
const wrappedSource = trimTemplateStrings`
|
|
84
|
+
${source}
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
// 更新文件内容
|
|
88
|
+
compilation.assets[filename] = {
|
|
89
|
+
source: () => wrappedSource,
|
|
90
|
+
size: () => wrappedSource.length
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
callback();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React Native 应用 CLI 工具
|
|
5
|
+
* 用于管理多应用项目的开发、构建和部署
|
|
6
|
+
*
|
|
7
|
+
* @author CLI Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import inquirer from 'inquirer';
|
|
12
|
+
import shell from 'shelljs';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import lodash from 'lodash';
|
|
17
|
+
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
|
+
import chalk from 'chalk';
|
|
24
|
+
import ora from 'ora';
|
|
25
|
+
import boxen from 'boxen';
|
|
26
|
+
import gradient from 'gradient-string';
|
|
27
|
+
|
|
28
|
+
// 获取当前文件的绝对路径和目录
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
|
|
32
|
+
const projectPath = path.resolve(__dirname, '../../../../');
|
|
33
|
+
|
|
34
|
+
// 常量定义
|
|
35
|
+
const CONSTANTS = {
|
|
36
|
+
SRC_DIR: '../src',
|
|
37
|
+
TEMP_DIR: './.temp',
|
|
38
|
+
MP_OUTPUT_DIR: './.mp-output',
|
|
39
|
+
LOGS_DIR: './.logs',
|
|
40
|
+
DEFAULT_VERSION_NAME: '1.0.0',
|
|
41
|
+
DEFAULT_VERSION_CODE: '1',
|
|
42
|
+
VERSION_NAME_REGEX: /^\d+(\.\d+){2}$/g,
|
|
43
|
+
VERSION_CODE_REGEX: /^\d+$/g,
|
|
44
|
+
WEBPACK_PORT: 8081,
|
|
45
|
+
BUNDLE_ENTRY_FILE: 'index.js',
|
|
46
|
+
BUNDLE_PLATFORM: 'ios',
|
|
47
|
+
MAX_RETRY_COUNT: 3,
|
|
48
|
+
DEFAULT_TIMEOUT: 30000
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 操作类型枚举
|
|
52
|
+
const ACTION_TYPES = {
|
|
53
|
+
START: 'start',
|
|
54
|
+
MP_BUILD: 'mp-build',
|
|
55
|
+
MP_DEBUG: 'mp-debug',
|
|
56
|
+
CLEAN: 'clean',
|
|
57
|
+
QUIT: 'quit'
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 日志级别枚举
|
|
61
|
+
const LOG_LEVELS = {
|
|
62
|
+
DEBUG: 0,
|
|
63
|
+
INFO: 1,
|
|
64
|
+
WARN: 2,
|
|
65
|
+
ERROR: 3
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 当前日志级别
|
|
69
|
+
let currentLogLevel = LOG_LEVELS.INFO;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 日志工具类
|
|
73
|
+
*/
|
|
74
|
+
class Logger {
|
|
75
|
+
static debug(message, ...args) {
|
|
76
|
+
if (currentLogLevel <= LOG_LEVELS.DEBUG) {
|
|
77
|
+
console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static info(message, ...args) {
|
|
82
|
+
if (currentLogLevel <= LOG_LEVELS.INFO) {
|
|
83
|
+
console.log(chalk.blue(`[INFO] ${message}`), ...args);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static warn(message, ...args) {
|
|
88
|
+
if (currentLogLevel <= LOG_LEVELS.WARN) {
|
|
89
|
+
console.log(chalk.yellow(`[WARN] ${message}`), ...args);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static error(message, ...args) {
|
|
94
|
+
if (currentLogLevel <= LOG_LEVELS.ERROR) {
|
|
95
|
+
console.log(chalk.red(`[ERROR] ${message}`), ...args);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static success(message, ...args) {
|
|
100
|
+
console.log(chalk.green(`[SUCCESS] ${message}`), ...args);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static writeToFile(message, level = 'INFO') {
|
|
104
|
+
const logDir = path.join(__dirname, CONSTANTS.LOGS_DIR);
|
|
105
|
+
const logFile = path.join(logDir, `cli-${new Date().toISOString().split('T')[0]}.log`);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
fse.ensureDirSync(logDir);
|
|
109
|
+
const timestamp = new Date().toISOString();
|
|
110
|
+
const logEntry = `[${timestamp}] [${level}] ${message}\n`;
|
|
111
|
+
fs.appendFileSync(logFile, logEntry);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('写入日志文件失败:', error.message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 配置验证工具类
|
|
120
|
+
*/
|
|
121
|
+
class ConfigValidator {
|
|
122
|
+
static validateAppConfig(config) {
|
|
123
|
+
const errors = [];
|
|
124
|
+
const warnings = [];
|
|
125
|
+
|
|
126
|
+
// 必需字段检查
|
|
127
|
+
const requiredFields = ['productName', 'versionName', 'versionCode'];
|
|
128
|
+
requiredFields.forEach(field => {
|
|
129
|
+
if (!config[field]) {
|
|
130
|
+
errors.push(`缺少必需字段: ${field}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 版本格式检查
|
|
135
|
+
// if (config.versionName && !CONSTANTS.VERSION_NAME_REGEX.test(`${config.versionName}`)) {
|
|
136
|
+
// errors.push(`版本名称格式错误: ${config.versionName},应为 x.y.z 格式`);
|
|
137
|
+
// }
|
|
138
|
+
// if (config.versionCode && !CONSTANTS.VERSION_CODE_REGEX.test(`${config.versionCode}`)) {
|
|
139
|
+
// errors.push(`版本代码格式错误: ${config.versionCode},应为数字格式`);
|
|
140
|
+
// }
|
|
141
|
+
|
|
142
|
+
// 平台配置检查
|
|
143
|
+
if (config.iosBuild) {
|
|
144
|
+
if (!config.iosBuild.appId) {
|
|
145
|
+
warnings.push('iOS配置缺少appId');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (config.andBuild) {
|
|
150
|
+
if (!config.andBuild.appId) {
|
|
151
|
+
warnings.push('Android配置缺少appId');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { errors, warnings, isValid: errors.length === 0 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static validateNetworkConfig() {
|
|
159
|
+
return new Promise(async (resolve) => {
|
|
160
|
+
try {
|
|
161
|
+
const networkInfo = await si.networkInterfaces();
|
|
162
|
+
const defaultNet = (lodash.isArray(networkInfo) ? networkInfo : [networkInfo])
|
|
163
|
+
.find(i => lodash.get(i, 'default', false));
|
|
164
|
+
|
|
165
|
+
if (!defaultNet || !defaultNet.ip4) {
|
|
166
|
+
resolve({ isValid: false, error: '无法获取网络配置' });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
resolve({
|
|
171
|
+
isValid: true,
|
|
172
|
+
host: defaultNet.ip4,
|
|
173
|
+
interface: defaultNet.iface
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
resolve({ isValid: false, error: error.message });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 文件系统工具类
|
|
184
|
+
*/
|
|
185
|
+
class FileSystemUtils {
|
|
186
|
+
static ensureDirectory(dirPath) {
|
|
187
|
+
try {
|
|
188
|
+
fse.ensureDirSync(dirPath);
|
|
189
|
+
Logger.debug(`目录已确保存在: ${dirPath}`);
|
|
190
|
+
return true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
Logger.error(`创建目录失败: ${dirPath}`, error.message);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static cleanDirectory(dirPath) {
|
|
198
|
+
try {
|
|
199
|
+
if (fs.existsSync(dirPath)) {
|
|
200
|
+
fse.removeSync(dirPath);
|
|
201
|
+
Logger.info(`目录已清理: ${dirPath}`);
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
Logger.error(`清理目录失败: ${dirPath}`, error.message);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static backupFile(filePath) {
|
|
211
|
+
try {
|
|
212
|
+
if (fs.existsSync(filePath)) {
|
|
213
|
+
const backupPath = `${filePath}.backup.${Date.now()}`;
|
|
214
|
+
fse.copySync(filePath, backupPath);
|
|
215
|
+
Logger.info(`文件已备份: ${backupPath}`);
|
|
216
|
+
return backupPath;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
Logger.error(`备份文件失败: ${filePath}`, error.message);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static getFileSize(filePath) {
|
|
226
|
+
try {
|
|
227
|
+
const stats = fs.statSync(filePath);
|
|
228
|
+
return stats.size;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static formatFileSize(bytes) {
|
|
235
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
236
|
+
if (bytes === 0) return '0 Bytes';
|
|
237
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
238
|
+
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 命令执行工具类
|
|
244
|
+
*/
|
|
245
|
+
class CommandExecutor {
|
|
246
|
+
static async execute(command, options = {}) {
|
|
247
|
+
const {
|
|
248
|
+
silent = false,
|
|
249
|
+
fatal = true,
|
|
250
|
+
timeout = CONSTANTS.DEFAULT_TIMEOUT,
|
|
251
|
+
retryCount = CONSTANTS.MAX_RETRY_COUNT,
|
|
252
|
+
description = '执行命令'
|
|
253
|
+
} = options;
|
|
254
|
+
|
|
255
|
+
const spinner = ora(`${description}: ${command}`).start();
|
|
256
|
+
|
|
257
|
+
for (let attempt = 1; attempt <= retryCount; attempt++) {
|
|
258
|
+
try {
|
|
259
|
+
Logger.debug(`执行命令 (尝试 ${attempt}/${retryCount}): ${command}`);
|
|
260
|
+
|
|
261
|
+
const result = await new Promise((resolve, reject) => {
|
|
262
|
+
const timeoutId = setTimeout(() => {
|
|
263
|
+
reject(new Error(`命令执行超时 (${timeout}ms)`));
|
|
264
|
+
}, timeout);
|
|
265
|
+
|
|
266
|
+
shell.exec(command, {
|
|
267
|
+
silent: silent,
|
|
268
|
+
fatal: false,
|
|
269
|
+
async: false
|
|
270
|
+
}, (code, stdout, stderr) => {
|
|
271
|
+
clearTimeout(timeoutId);
|
|
272
|
+
resolve({ code, stdout, stderr });
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (result.code === 0) {
|
|
277
|
+
spinner.succeed(`${description} 成功`);
|
|
278
|
+
Logger.debug(`命令输出: ${result.stdout}`);
|
|
279
|
+
return result;
|
|
280
|
+
} else {
|
|
281
|
+
throw new Error(`命令执行失败,退出码: ${result.code}, 错误: ${result.stderr}`);
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (attempt === retryCount) {
|
|
285
|
+
spinner.fail(`${description} 失败 (${retryCount} 次尝试后)`);
|
|
286
|
+
Logger.error(`命令执行失败: ${error.message}`);
|
|
287
|
+
|
|
288
|
+
if (fatal) {
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
} else {
|
|
293
|
+
Logger.warn(`命令执行失败,准备重试 (${attempt}/${retryCount}): ${error.message}`);
|
|
294
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // 递增延迟
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
static async executeWithProgress(command, options = {}) {
|
|
301
|
+
const { description = '执行命令', progressCallback } = options;
|
|
302
|
+
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
const child = shell.exec(command, {
|
|
305
|
+
silent: false,
|
|
306
|
+
fatal: false,
|
|
307
|
+
async: true
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
let output = '';
|
|
311
|
+
|
|
312
|
+
child.stdout?.on('data', (data) => {
|
|
313
|
+
output += data;
|
|
314
|
+
if (progressCallback) {
|
|
315
|
+
progressCallback(data.toString());
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
child.stderr?.on('data', (data) => {
|
|
320
|
+
output += data;
|
|
321
|
+
Logger.warn(`命令输出: ${data}`);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
child.on('close', (code) => {
|
|
325
|
+
if (code === 0) {
|
|
326
|
+
Logger.success(`${description} 完成`);
|
|
327
|
+
resolve({ code, output });
|
|
328
|
+
} else {
|
|
329
|
+
Logger.error(`${description} 失败,退出码: ${code}`);
|
|
330
|
+
reject(new Error(`命令执行失败,退出码: ${code}`));
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
child.on('error', (error) => {
|
|
335
|
+
Logger.error(`${description} 出错: ${error.message}`);
|
|
336
|
+
reject(error);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 验证版本号格式
|
|
344
|
+
*
|
|
345
|
+
* @param {string} versionName - 版本名称 (格式: x.y.z)
|
|
346
|
+
* @param {string} versionCode - 版本代码 (格式: 数字)
|
|
347
|
+
* @returns {boolean} 验证结果
|
|
348
|
+
*/
|
|
349
|
+
function validateVersion(versionName, versionCode) {
|
|
350
|
+
const isVersionNameValid = CONSTANTS.VERSION_NAME_REGEX.test(versionName);
|
|
351
|
+
const isVersionCodeValid = CONSTANTS.VERSION_CODE_REGEX.test(versionCode);
|
|
352
|
+
|
|
353
|
+
if (!isVersionNameValid) {
|
|
354
|
+
Logger.error(`版本名称格式错误: ${versionName},应为 x.y.z 格式`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!isVersionCodeValid) {
|
|
358
|
+
Logger.error(`版本代码格式错误: ${versionCode},应为数字格式`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return isVersionNameValid && isVersionCodeValid;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 更新应用配置
|
|
366
|
+
*
|
|
367
|
+
* @param {string} appName - 应用名称
|
|
368
|
+
* @param {Object} newConfig - 新配置
|
|
369
|
+
* @returns {boolean} 更新是否成功
|
|
370
|
+
*/
|
|
371
|
+
function updateAppConfig(newConfig) {
|
|
372
|
+
try {
|
|
373
|
+
const configPath = path.join(projectPath, 'app.json');
|
|
374
|
+
const localConfig = fse.readJsonSync(configPath);
|
|
375
|
+
const _newConfig = {
|
|
376
|
+
...localConfig,
|
|
377
|
+
'htyf': {
|
|
378
|
+
...localConfig.htyf,
|
|
379
|
+
...newConfig
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// 备份原配置文件
|
|
383
|
+
// FileSystemUtils.backupFile(configPath);
|
|
384
|
+
|
|
385
|
+
// 写入新配置
|
|
386
|
+
fs.writeFileSync(configPath, JSON.stringify(_newConfig, undefined, 2), 'utf-8');
|
|
387
|
+
Logger.success(`配置文件已更新: ${configPath}`);
|
|
388
|
+
|
|
389
|
+
return true;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
Logger.error(`更新配置文件失败: ${error.message}`);
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 构建小程序包
|
|
398
|
+
*
|
|
399
|
+
* @param {string} appName - 应用名称
|
|
400
|
+
*/
|
|
401
|
+
async function mpBuildShell(newAppInfo) {
|
|
402
|
+
Logger.info(`开始构建小程序包...`);
|
|
403
|
+
Logger.info(`应用: ${newAppInfo.name}`);
|
|
404
|
+
Logger.info(`平台: ${CONSTANTS.BUNDLE_PLATFORM}`);
|
|
405
|
+
|
|
406
|
+
// 创建临时目录
|
|
407
|
+
const tempPath = path.join(__dirname, CONSTANTS.TEMP_DIR, newAppInfo.name);
|
|
408
|
+
if (!FileSystemUtils.ensureDirectory(tempPath)) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const mpOutputPath = path.join(__dirname, CONSTANTS.MP_OUTPUT_DIR, newAppInfo.name, 'dist');
|
|
413
|
+
if (!FileSystemUtils.ensureDirectory(mpOutputPath)) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const version = newAppInfo.version;
|
|
417
|
+
const appid = newAppInfo.appid;
|
|
418
|
+
const scriptName = getMiniAppScriptId(appid, version);
|
|
419
|
+
|
|
420
|
+
const appJson = {
|
|
421
|
+
type: 'app',
|
|
422
|
+
engines: '2.0.0',
|
|
423
|
+
name: newAppInfo.name,
|
|
424
|
+
appid: appid,
|
|
425
|
+
version: version,
|
|
426
|
+
appUrlConfig: newAppInfo.appUrlConfig || `${newAppInfo.host}/app.json`,
|
|
427
|
+
zipUrl: newAppInfo.zipUrl || `${newAppInfo.host}/dist.dgz`
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const mpOptions = {
|
|
431
|
+
name: scriptName,
|
|
432
|
+
filename: `${scriptName}.bundle`,
|
|
433
|
+
exposes: {
|
|
434
|
+
'App': `./src/${newAppInfo.name}/index.tsx`,
|
|
435
|
+
},
|
|
436
|
+
outputPath: path.join(mpOutputPath, '../build'),
|
|
437
|
+
extraChunksPath: path.join(mpOutputPath),
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
const appJsonPath = path.join(mpOutputPath, '../', `./app.json`);
|
|
442
|
+
fs.writeFileSync(appJsonPath, JSON.stringify(appJson, undefined, 2));
|
|
443
|
+
const webpackConfigPath = path.join(__dirname, 'webpack.config.mjs');
|
|
444
|
+
const command = `npx cross-env APP_USE_ENTRY_DEV=${newAppInfo.name} APP_EXPOSES_OPTIONS=${encodeURI(JSON.stringify(mpOptions))} APP_EXPOSES_FILENAME=${mpOptions.filename} APP_EXPOSES_EXPOSES=${JSON.stringify(mpOptions.exposes)} npx webpack --config ${webpackConfigPath}`;
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
await CommandExecutor.execute(command, {
|
|
448
|
+
description: '构建小程序包',
|
|
449
|
+
timeout: 120000
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
fs.copyFileSync(appJsonPath, path.join(mpOutputPath, '../dist', `./app.json`));
|
|
453
|
+
const zipPath = await handleZip(mpOutputPath, path.join(mpOutputPath, '../', `./dist.dgz`));
|
|
454
|
+
Logger.success(`压缩包已创建: ${zipPath}`);
|
|
455
|
+
return zipPath;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
Logger.error(`小程序构建失败: ${error.message}`);
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* 压缩文件夹为zip文件
|
|
464
|
+
*
|
|
465
|
+
* @param {string} inputPath - 输入文件夹路径
|
|
466
|
+
* @param {string} outputPath - 输出zip文件路径
|
|
467
|
+
* @returns {Promise<string>} zip文件路径
|
|
468
|
+
*/
|
|
469
|
+
const handleZip = async (inputPath, outputPath) => {
|
|
470
|
+
Logger.info(`开始压缩文件夹: ${inputPath}`);
|
|
471
|
+
Logger.info(`输出文件: ${outputPath}`);
|
|
472
|
+
|
|
473
|
+
// 检查输入路径是否存在
|
|
474
|
+
if (!fs.existsSync(inputPath)) {
|
|
475
|
+
throw new Error(`输入路径不存在: ${inputPath}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 检查输入路径是否为目录
|
|
479
|
+
const stats = fs.statSync(inputPath);
|
|
480
|
+
if (!stats.isDirectory()) {
|
|
481
|
+
throw new Error(`输入路径不是目录: ${inputPath}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 列出目录内容
|
|
485
|
+
const files = fs.readdirSync(inputPath);
|
|
486
|
+
Logger.debug(`目录内容: ${files.length} 个文件/文件夹`);
|
|
487
|
+
files.forEach(file => {
|
|
488
|
+
const filePath = path.join(inputPath, file);
|
|
489
|
+
const fileStats = fs.statSync(filePath);
|
|
490
|
+
Logger.debug(` - ${file} (${fileStats.isDirectory() ? '目录' : '文件'})`);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (files.length === 0) {
|
|
494
|
+
Logger.warn(`警告: 目录为空,将创建空的zip文件`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const zipPath = outputPath;
|
|
498
|
+
const admzip = new AdmZip();
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
await admzip.addLocalFolderPromise(inputPath, {
|
|
502
|
+
filter: (filename) => {
|
|
503
|
+
// 过滤掉 .map 文件
|
|
504
|
+
if (filename.endsWith('.map')) {
|
|
505
|
+
Logger.debug(`过滤文件: ${filename}`);
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
Logger.debug(`添加文件: ${filename}`);
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await admzip.writeZipPromise(zipPath);
|
|
514
|
+
Logger.success(`压缩完成: ${zipPath}`);
|
|
515
|
+
|
|
516
|
+
// 检查生成的zip文件大小
|
|
517
|
+
const zipStats = fs.statSync(zipPath);
|
|
518
|
+
Logger.info(`ZIP文件大小: ${FileSystemUtils.formatFileSize(zipStats.size)}`);
|
|
519
|
+
|
|
520
|
+
return zipPath;
|
|
521
|
+
} catch (error) {
|
|
522
|
+
Logger.error(`压缩失败: ${error.message}`);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* 小程序真机调试
|
|
529
|
+
*
|
|
530
|
+
* @param {string} appName - 应用名称
|
|
531
|
+
*/
|
|
532
|
+
async function mpDebugShell(newAppInfo) {
|
|
533
|
+
try {
|
|
534
|
+
const zipPath = await mpBuildShell(newAppInfo);
|
|
535
|
+
const assetsPath = path.dirname(zipPath);
|
|
536
|
+
const appJsonPath = path.join(assetsPath, 'app.json');
|
|
537
|
+
|
|
538
|
+
Logger.info(`资源路径: ${assetsPath}`);
|
|
539
|
+
const applocaljson = fse.readJsonSync(appJsonPath);
|
|
540
|
+
|
|
541
|
+
// 验证网络配置
|
|
542
|
+
const networkValidation = await ConfigValidator.validateNetworkConfig();
|
|
543
|
+
if (!networkValidation.isValid) {
|
|
544
|
+
throw new Error(`网络配置验证失败: ${networkValidation.error}`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const host = networkValidation.host;
|
|
548
|
+
const port = await portfinder.getPortPromise();
|
|
549
|
+
|
|
550
|
+
const app = superstatic.server({
|
|
551
|
+
port: port,
|
|
552
|
+
address: host,
|
|
553
|
+
cwd: assetsPath
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const server = app.listen(function () {
|
|
557
|
+
const address = server.address();
|
|
558
|
+
|
|
559
|
+
const debugerAppUrlConfig = `http://${host}:${address.port}/app.json`;
|
|
560
|
+
const debugerZip = `http://${host}:${address.port}/dist.dgz`;
|
|
561
|
+
const args = {
|
|
562
|
+
...applocaljson,
|
|
563
|
+
development: true,
|
|
564
|
+
"name": applocaljson.name,
|
|
565
|
+
"appid": applocaljson.appid,
|
|
566
|
+
"version": applocaljson.version,
|
|
567
|
+
appUrlConfig: debugerAppUrlConfig,
|
|
568
|
+
zipUrl: debugerZip,
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
fse.writeJSONSync(appJsonPath, {
|
|
572
|
+
...fse.readJSONSync(appJsonPath),
|
|
573
|
+
appUrlConfig: debugerAppUrlConfig,
|
|
574
|
+
zipUrl: debugerZip,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const qrcodeUrl = `https://share.dagouzhi.com/#/pages/index/index?data=${encodeURIComponent(JSON.stringify(args))}`;
|
|
578
|
+
|
|
579
|
+
// 显示调试信息
|
|
580
|
+
console.log('\n');
|
|
581
|
+
console.log(boxen(
|
|
582
|
+
gradient.rainbow('小程序真机调试') + '\n\n' +
|
|
583
|
+
chalk.cyan('服务器地址: ') + chalk.white(`${host}:${address.port}`) + '\n' +
|
|
584
|
+
chalk.cyan('应用名称: ') + chalk.white(applocaljson.name) + '\n' +
|
|
585
|
+
chalk.cyan('应用ID: ') + chalk.white(applocaljson.appid) + '\n' +
|
|
586
|
+
chalk.cyan('版本: ') + chalk.white(applocaljson.version),
|
|
587
|
+
{
|
|
588
|
+
padding: 1,
|
|
589
|
+
margin: 1,
|
|
590
|
+
borderStyle: 'round',
|
|
591
|
+
borderColor: 'cyan'
|
|
592
|
+
}
|
|
593
|
+
));
|
|
594
|
+
|
|
595
|
+
console.log('\n' + chalk.yellow('调试配置:'));
|
|
596
|
+
console.log(JSON.stringify(args, undefined, 2));
|
|
597
|
+
|
|
598
|
+
console.log('\n' + chalk.green('调试链接:'));
|
|
599
|
+
console.log(qrcodeUrl);
|
|
600
|
+
|
|
601
|
+
console.log('\n' + chalk.blue('二维码:'));
|
|
602
|
+
printQrcode(qrcodeUrl);
|
|
603
|
+
|
|
604
|
+
Logger.info('调试服务器已启动,按 Ctrl+C 停止');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// 优雅关闭
|
|
608
|
+
process.on('SIGINT', () => {
|
|
609
|
+
Logger.info('正在关闭调试服务器...');
|
|
610
|
+
server.close(() => {
|
|
611
|
+
Logger.success('调试服务器已关闭');
|
|
612
|
+
process.exit(0);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
} catch (error) {
|
|
617
|
+
Logger.error(`小程序调试失败: ${error.message}`);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 清理临时文件
|
|
624
|
+
*
|
|
625
|
+
* @param {string} appName - 应用名称(可选)
|
|
626
|
+
*/
|
|
627
|
+
async function cleanShell(appName = null) {
|
|
628
|
+
Logger.info('开始清理临时文件...');
|
|
629
|
+
|
|
630
|
+
const pathsToClean = [];
|
|
631
|
+
|
|
632
|
+
if (appName) {
|
|
633
|
+
// 清理指定应用的临时文件
|
|
634
|
+
pathsToClean.push(
|
|
635
|
+
path.join(__dirname, CONSTANTS.TEMP_DIR, appName),
|
|
636
|
+
path.join(__dirname, CONSTANTS.MP_OUTPUT_DIR, appName)
|
|
637
|
+
);
|
|
638
|
+
} else {
|
|
639
|
+
// 清理所有临时文件
|
|
640
|
+
pathsToClean.push(
|
|
641
|
+
path.join(__dirname, CONSTANTS.TEMP_DIR),
|
|
642
|
+
path.join(__dirname, CONSTANTS.MP_OUTPUT_DIR)
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let cleanedCount = 0;
|
|
647
|
+
for (const path of pathsToClean) {
|
|
648
|
+
if (FileSystemUtils.cleanDirectory(path)) {
|
|
649
|
+
cleanedCount++;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
Logger.success(`清理完成,已清理 ${cleanedCount} 个目录`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 打印二维码
|
|
658
|
+
*
|
|
659
|
+
* @param {string} url - 要生成二维码的URL
|
|
660
|
+
*/
|
|
661
|
+
const printQrcode = async (url) => {
|
|
662
|
+
try {
|
|
663
|
+
let terminalStr = await QRCode.toString(url, { type: 'terminal', small: true });
|
|
664
|
+
console.log(terminalStr);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
Logger.error(`生成二维码失败: ${error.message}`);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* 主启动函数
|
|
672
|
+
* 处理用户选择的操作类型
|
|
673
|
+
*
|
|
674
|
+
* @param {string} action - 操作类型
|
|
675
|
+
*/
|
|
676
|
+
async function Start(action) {
|
|
677
|
+
const actionNames = {
|
|
678
|
+
[ACTION_TYPES.MP_BUILD]: '小程序构建',
|
|
679
|
+
[ACTION_TYPES.MP_DEBUG]: '小程序调试',
|
|
680
|
+
[ACTION_TYPES.CLEAN]: '清理模式',
|
|
681
|
+
[ACTION_TYPES.QUIT]: '退出'
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
Logger.info(`当前操作: ${actionNames[action] || '未知操作'}`);
|
|
685
|
+
|
|
686
|
+
// 如果是退出操作,直接返回
|
|
687
|
+
if (action === ACTION_TYPES.QUIT) {
|
|
688
|
+
Logger.info('再见!');
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 如果是清理或状态操作,不需要选择应用
|
|
693
|
+
if (action === ACTION_TYPES.CLEAN) {
|
|
694
|
+
const { cleanAll } = await inquirer.prompt([
|
|
695
|
+
{
|
|
696
|
+
type: 'confirm',
|
|
697
|
+
name: 'cleanAll',
|
|
698
|
+
message: '是否清理所有应用的临时文件?',
|
|
699
|
+
default: false
|
|
700
|
+
}
|
|
701
|
+
]);
|
|
702
|
+
|
|
703
|
+
if (cleanAll) {
|
|
704
|
+
await cleanShell();
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
// 显示应用选择界面
|
|
711
|
+
try {
|
|
712
|
+
const appInfo = fse.readJsonSync(path.join(projectPath, 'app.json')).htyf;
|
|
713
|
+
if (!appInfo) {
|
|
714
|
+
Logger.error('应用配置不存在,请先在app.json中配置htyf');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const { versionName } = await inquirer.prompt([
|
|
718
|
+
{
|
|
719
|
+
type: 'input',
|
|
720
|
+
name: 'versionName',
|
|
721
|
+
message: '请输入应用版本名称 (格式: x.y.z):',
|
|
722
|
+
default: appInfo?.version,
|
|
723
|
+
validate: (input) => {
|
|
724
|
+
return CONSTANTS.VERSION_NAME_REGEX.test(input) || '请输入正确的版本名称格式 (x.y.z)';
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
]);
|
|
728
|
+
|
|
729
|
+
Logger.info(`用户选择:`);
|
|
730
|
+
Logger.info(`版本名称: ${versionName}`);
|
|
731
|
+
|
|
732
|
+
// 验证版本格式
|
|
733
|
+
// if (!validateVersion(versionName, versionCode)) {
|
|
734
|
+
// return;
|
|
735
|
+
// }
|
|
736
|
+
|
|
737
|
+
// 合并新配置
|
|
738
|
+
const newAppInfo = lodash.merge({}, appInfo, {
|
|
739
|
+
version: versionName,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// 更新配置文件
|
|
743
|
+
if (!updateAppConfig(newAppInfo)) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// 显示操作信息
|
|
748
|
+
console.log('\n' + boxen(
|
|
749
|
+
chalk.cyan('应用信息') + '\n\n' +
|
|
750
|
+
chalk.white('名称: ') + chalk.yellow(`${newAppInfo.name})`) + '\n' +
|
|
751
|
+
{
|
|
752
|
+
padding: 1,
|
|
753
|
+
margin: 1,
|
|
754
|
+
borderStyle: 'round',
|
|
755
|
+
borderColor: 'yellow'
|
|
756
|
+
}
|
|
757
|
+
));
|
|
758
|
+
|
|
759
|
+
// 执行初始化
|
|
760
|
+
Logger.info('执行初始化...');
|
|
761
|
+
|
|
762
|
+
// 根据操作类型执行相应命令
|
|
763
|
+
switch (action) {
|
|
764
|
+
case ACTION_TYPES.MP_BUILD:
|
|
765
|
+
await mpBuildShell(newAppInfo);
|
|
766
|
+
break;
|
|
767
|
+
case ACTION_TYPES.MP_DEBUG:
|
|
768
|
+
await mpDebugShell(newAppInfo);
|
|
769
|
+
break;
|
|
770
|
+
default:
|
|
771
|
+
Logger.error(`未知的操作类型: ${action}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
} catch (error) {
|
|
775
|
+
Logger.error(`操作失败: ${error.message}`);
|
|
776
|
+
Logger.error(`错误详情: ${error.stack}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// 主程序入口
|
|
781
|
+
console.log('\n' + boxen(
|
|
782
|
+
gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.0.0') + '\n' +
|
|
783
|
+
chalk.gray('用于管理多应用项目的开发、构建和部署'),
|
|
784
|
+
{
|
|
785
|
+
padding: 1,
|
|
786
|
+
margin: 1,
|
|
787
|
+
borderStyle: 'round',
|
|
788
|
+
borderColor: 'cyan'
|
|
789
|
+
}
|
|
790
|
+
));
|
|
791
|
+
|
|
792
|
+
// 设置日志级别
|
|
793
|
+
if (process.argv.includes('--debug')) {
|
|
794
|
+
currentLogLevel = LOG_LEVELS.DEBUG;
|
|
795
|
+
Logger.debug('调试模式已启用');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 记录启动日志
|
|
799
|
+
Logger.writeToFile('CLI 工具启动', 'INFO');
|
|
800
|
+
|
|
801
|
+
inquirer
|
|
802
|
+
.prompt([
|
|
803
|
+
{
|
|
804
|
+
type: 'rawlist',
|
|
805
|
+
name: 'index',
|
|
806
|
+
message: '请选择你想要执行的操作:',
|
|
807
|
+
choices: [
|
|
808
|
+
{ name: '🔍 小程序 - 打包小程序', value: ACTION_TYPES.MP_BUILD },
|
|
809
|
+
{ name: '📦 小程序 - 真机调试', value: ACTION_TYPES.MP_DEBUG },
|
|
810
|
+
{ name: '🧹 清理模式 - 清理临时文件', value: ACTION_TYPES.CLEAN },
|
|
811
|
+
{ name: '👋 退出', value: ACTION_TYPES.QUIT },
|
|
812
|
+
],
|
|
813
|
+
},
|
|
814
|
+
])
|
|
815
|
+
.then(({ index }) => {
|
|
816
|
+
Start(index);
|
|
817
|
+
})
|
|
818
|
+
.catch((error) => {
|
|
819
|
+
Logger.error(`程序启动失败: ${error.message}`);
|
|
820
|
+
process.exit(1);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
export const getMiniAppScriptId = (appid, version) => {
|
|
824
|
+
return `${appid}_v${version?.replace(/\./gi, '_')}`
|
|
825
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@bam.tech/react-native-image-resizer": "3.0.11",
|
|
3
|
+
"@dr.pogodin/react-native-fs": "2.34.0",
|
|
4
|
+
"@dr.pogodin/react-native-static-server": "0.22.0",
|
|
5
|
+
"@react-native-async-storage/async-storage": "2.2.0",
|
|
6
|
+
"@react-native-camera-roll/camera-roll": "7.10.1",
|
|
7
|
+
"@react-native-clipboard/clipboard": "1.16.3",
|
|
8
|
+
"@react-native-community/blur": "4.4.1",
|
|
9
|
+
"@react-native-community/checkbox": "0.5.20",
|
|
10
|
+
"@react-native-community/datetimepicker": "8.4.2",
|
|
11
|
+
"@react-native-community/geolocation": "3.4.0",
|
|
12
|
+
"@react-native-community/netinfo": "11.4.1",
|
|
13
|
+
"@react-native-community/slider": "4.5.7",
|
|
14
|
+
"@react-native-cookies/cookies": "6.2.1",
|
|
15
|
+
"@react-native-documents/picker": "10.1.5",
|
|
16
|
+
"@react-native-masked-view/masked-view": "0.3.2",
|
|
17
|
+
"@react-native-picker/picker": "2.11.1",
|
|
18
|
+
"@react-native-segmented-control/segmented-control": "2.5.7",
|
|
19
|
+
"@sayem314/react-native-keep-awake": "1.3.1",
|
|
20
|
+
"@shopify/flash-list": "1.8.3",
|
|
21
|
+
"@shopify/react-native-skia": "2.1.1",
|
|
22
|
+
"expo": "53.0.20",
|
|
23
|
+
"expo-asset": "11.1.7",
|
|
24
|
+
"expo-audio": "0.4.8",
|
|
25
|
+
"expo-brightness": "13.1.4",
|
|
26
|
+
"expo-camera": "16.1.11",
|
|
27
|
+
"expo-clipboard": "7.1.5",
|
|
28
|
+
"expo-constants": "17.1.7",
|
|
29
|
+
"expo-crypto": "14.1.5",
|
|
30
|
+
"expo-font": "13.3.2",
|
|
31
|
+
"expo-image": "2.4.0",
|
|
32
|
+
"expo-linking": "7.1.7",
|
|
33
|
+
"expo-router": "5.1.4",
|
|
34
|
+
"expo-system-ui": "5.0.10",
|
|
35
|
+
"expo-video": "2.2.2",
|
|
36
|
+
"jcore-react-native": "2.3.1",
|
|
37
|
+
"jpush-react-native": "3.2.0",
|
|
38
|
+
"llama.rn": "0.6.2",
|
|
39
|
+
"lottie-react-native": "7.2.4",
|
|
40
|
+
"open-im-sdk-rn": "3.8.3-patch.3",
|
|
41
|
+
"react-airplay": "1.2.0",
|
|
42
|
+
"react-native-avoid-softinput": "7.0.2",
|
|
43
|
+
"react-native-background-actions": "4.0.1",
|
|
44
|
+
"react-native-ble-manager": "12.1.5",
|
|
45
|
+
"react-native-blob-util": "0.22.2",
|
|
46
|
+
"react-native-bootsplash": "6.3.10",
|
|
47
|
+
"react-native-compressor": "1.12.0",
|
|
48
|
+
"react-native-config": "1.5.5",
|
|
49
|
+
"react-native-device-info": "14.0.4",
|
|
50
|
+
"react-native-edge-to-edge": "1.6.2",
|
|
51
|
+
"react-native-fast-image": "8.6.3",
|
|
52
|
+
"react-native-gesture-handler": "2.27.2",
|
|
53
|
+
"react-native-get-random-values": "1.11.0",
|
|
54
|
+
"react-native-google-mobile-ads": "15.4.0",
|
|
55
|
+
"react-native-haptic-feedback": "2.3.3",
|
|
56
|
+
"react-native-iap": "13.0.1",
|
|
57
|
+
"react-native-image-picker": "8.2.1",
|
|
58
|
+
"react-native-keyboard-controller": "1.18.0",
|
|
59
|
+
"react-native-linear-gradient": "2.8.3",
|
|
60
|
+
"react-native-localize": "3.5.1",
|
|
61
|
+
"react-native-mmkv": "3.3.0",
|
|
62
|
+
"react-native-orientation-locker": "1.7.0",
|
|
63
|
+
"react-native-permissions": "5.4.2",
|
|
64
|
+
"react-native-quick-base64": "2.2.1",
|
|
65
|
+
"react-native-quick-crypto": "0.7.15",
|
|
66
|
+
"react-native-reanimated": "3.19.0",
|
|
67
|
+
"react-native-restart": "0.0.27",
|
|
68
|
+
"react-native-safe-area-context": "5.5.2",
|
|
69
|
+
"react-native-screens": "4.13.1",
|
|
70
|
+
"react-native-share": "12.1.0",
|
|
71
|
+
"react-native-svg": "15.12.0",
|
|
72
|
+
"react-native-track-player": "4.1.1",
|
|
73
|
+
"react-native-vector-icons": "10.2.0",
|
|
74
|
+
"react-native-video": "6.16.1",
|
|
75
|
+
"react-native-view-shot": "4.0.3",
|
|
76
|
+
"react-native-vlc-media-player": "1.0.89",
|
|
77
|
+
"react-native-volume-manager": "2.0.8",
|
|
78
|
+
"react-native-webview": "13.15.0",
|
|
79
|
+
"react-native-wgpu": "0.2.0",
|
|
80
|
+
"react-native-wifi-reborn": "4.13.6",
|
|
81
|
+
"react-native-zip-archive": "7.0.2"
|
|
82
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// 导入Node.js和第三方依赖
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import fse from 'fs-extra';
|
|
5
|
+
import webpack from 'webpack';
|
|
6
|
+
import TerserPlugin from 'terser-webpack-plugin';
|
|
7
|
+
import * as Repack from '@callstack/repack';
|
|
8
|
+
import { ExpoModulesPlugin } from '@callstack/repack-plugin-expo-modules';
|
|
9
|
+
import { ReanimatedPlugin } from '@callstack/repack-plugin-reanimated';
|
|
10
|
+
|
|
11
|
+
// 获取当前文件和目录路径
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const projectPath = path.resolve(__dirname, '../../../');
|
|
15
|
+
|
|
16
|
+
// 微前端暴露配置,通常为JSON字符串
|
|
17
|
+
const appExposesOptions = process.env.APP_EXPOSES_OPTIONS;
|
|
18
|
+
|
|
19
|
+
// 读取package.json,获取应用基本信息
|
|
20
|
+
const appConfig = fse.readJsonSync(path.join(projectPath, './app.json'));
|
|
21
|
+
// 读取依赖共享配置
|
|
22
|
+
const dependencies = fse.readJsonSync(path.join(__dirname, './shared-output.json'));
|
|
23
|
+
const htyf = appConfig.htyf || {}
|
|
24
|
+
const htyfHostConfig = {
|
|
25
|
+
"version": htyf.version,
|
|
26
|
+
"name": htyf.name,
|
|
27
|
+
"appid": htyf.appid,
|
|
28
|
+
"appUrlConfig": htyf.appUrlConfig,
|
|
29
|
+
"zipUrl": htyf.zipUrl
|
|
30
|
+
}
|
|
31
|
+
// 记录构建时间戳
|
|
32
|
+
const time = Date.now();
|
|
33
|
+
|
|
34
|
+
// 构建Module Federation共享依赖对象
|
|
35
|
+
const sharedObj = {};
|
|
36
|
+
for (const key in dependencies) {
|
|
37
|
+
const p = dependencies[key];
|
|
38
|
+
sharedObj[key] = {
|
|
39
|
+
/** 是否运行为单例 */
|
|
40
|
+
singleton: true,
|
|
41
|
+
/** host模式下必须为true,微应用为false */
|
|
42
|
+
eager: false,
|
|
43
|
+
requiredVersion: p.version,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Webpack配置导出函数
|
|
49
|
+
* @param {object} env - 构建环境变量
|
|
50
|
+
* @returns {object} - Webpack配置对象
|
|
51
|
+
*/
|
|
52
|
+
export default (env = {}) => {
|
|
53
|
+
// 设置构建上下文
|
|
54
|
+
env.context = __dirname;
|
|
55
|
+
const { entry = './index.js', platform = 'ios' } = env;
|
|
56
|
+
const context = env.context || projectPath;
|
|
57
|
+
// 获取Repack的resolve配置
|
|
58
|
+
const _options = Repack.getResolveOptions();
|
|
59
|
+
|
|
60
|
+
// 解析微前端相关配置
|
|
61
|
+
let mpOptions = {};
|
|
62
|
+
if (appExposesOptions) {
|
|
63
|
+
mpOptions = JSON.parse(decodeURI(appExposesOptions));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 基础Webpack配置
|
|
67
|
+
const mpConfig = {
|
|
68
|
+
context,
|
|
69
|
+
entry,
|
|
70
|
+
resolve: {
|
|
71
|
+
..._options,
|
|
72
|
+
},
|
|
73
|
+
output: {
|
|
74
|
+
// 默认输出路径,host模式下会被覆盖
|
|
75
|
+
path: mpOptions.outputPath,
|
|
76
|
+
uniqueName: mpOptions.uniqueName,
|
|
77
|
+
},
|
|
78
|
+
module: {
|
|
79
|
+
rules: [
|
|
80
|
+
// 处理js/ts/tsx文件,使用babel-loader
|
|
81
|
+
{
|
|
82
|
+
test: /\.[cm]?[jt]sx?$/,
|
|
83
|
+
use: 'babel-loader',
|
|
84
|
+
type: 'javascript/auto',
|
|
85
|
+
},
|
|
86
|
+
// 处理静态资源(图片等),支持inline模式
|
|
87
|
+
...Repack.getAssetTransformRules({
|
|
88
|
+
inline: true,
|
|
89
|
+
}),
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
optimization: {
|
|
93
|
+
// 压缩配置,去除注释
|
|
94
|
+
minimizer: [
|
|
95
|
+
new TerserPlugin({
|
|
96
|
+
test: /\.(js)?bundle(\?.*)?$/i,
|
|
97
|
+
extractComments: false,
|
|
98
|
+
terserOptions: {
|
|
99
|
+
format: {
|
|
100
|
+
comments: false,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
chunkIds: 'named', // 便于调试
|
|
106
|
+
},
|
|
107
|
+
plugins: [
|
|
108
|
+
// 注入全局常量(appid、版本、构建时间)
|
|
109
|
+
new webpack.DefinePlugin({
|
|
110
|
+
__APP_DEFINE_APPID__: `"${htyfHostConfig.name}"`,
|
|
111
|
+
__APP_DEFINE_VERSION__: `"${htyfHostConfig.version}"`,
|
|
112
|
+
__APP_DEFINE_BUILD_TIME__: `"${time}"`,
|
|
113
|
+
}),
|
|
114
|
+
// Repack主插件,支持extraChunks配置
|
|
115
|
+
new Repack.RepackPlugin(mpOptions.extraChunksPath ? {
|
|
116
|
+
platform,
|
|
117
|
+
extraChunks: [
|
|
118
|
+
{
|
|
119
|
+
include: /.*/,
|
|
120
|
+
type: 'remote',
|
|
121
|
+
outputPath: `${mpOptions.extraChunksPath}`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
} : {
|
|
125
|
+
platform,
|
|
126
|
+
}),
|
|
127
|
+
// Expo模块支持
|
|
128
|
+
new ExpoModulesPlugin(),
|
|
129
|
+
// Reanimated支持
|
|
130
|
+
new ReanimatedPlugin(),
|
|
131
|
+
// Module Federation V2插件,支持host和remote两种模式
|
|
132
|
+
new Repack.plugins.ModuleFederationPluginV2(mpOptions.exposes ? {
|
|
133
|
+
// 微应用模式
|
|
134
|
+
name: mpOptions.name,
|
|
135
|
+
filename: mpOptions.filename,
|
|
136
|
+
exposes: mpOptions.exposes,
|
|
137
|
+
shared: {
|
|
138
|
+
...sharedObj,
|
|
139
|
+
react: { singleton: true, eager: true },
|
|
140
|
+
'react-native': { singleton: true, eager: true },
|
|
141
|
+
},
|
|
142
|
+
shareStrategy: 'loaded-first',
|
|
143
|
+
} : {
|
|
144
|
+
// host模式
|
|
145
|
+
name: 'HTYF_HOST_APP',
|
|
146
|
+
remotes: {},
|
|
147
|
+
shared: {
|
|
148
|
+
...sharedObj,
|
|
149
|
+
react: { singleton: true, eager: true },
|
|
150
|
+
'react-native': { singleton: true, eager: true },
|
|
151
|
+
},
|
|
152
|
+
shareStrategy: 'loaded-first',
|
|
153
|
+
}),
|
|
154
|
+
new webpack.optimize.LimitChunkCountPlugin({
|
|
155
|
+
maxChunks: 2, // 限制最大chunk数,便于远程加载
|
|
156
|
+
}),
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return mpConfig;
|
|
161
|
+
};
|