@htyf-mp/cli 0.2.0-alpha.9 → 0.2.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 +1 -1
- package/src/HtyfImportsPlugin.mjs +0 -1
- package/src/build.mjs +116 -18
- package/src/debug.mjs +61 -14
- package/src/export_godot.mjs +166 -20
- package/src/index.mjs +56 -15
- package/src/webpack.config.mjs +21 -1
- package/src/ListImportsPlugin.mjs +0 -105
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fse from 'fs-extra';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { getOptions } from 'loader-utils';
|
|
5
4
|
|
|
6
5
|
// ==================== 常量定义 ====================
|
|
7
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
package/src/build.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 构建模块
|
|
3
|
+
* 负责小程序和 Godot 游戏的打包构建
|
|
4
|
+
*
|
|
5
|
+
* @module build
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import path from 'path';
|
|
2
9
|
import fse from 'fs-extra';
|
|
3
10
|
import AdmZip from 'adm-zip';
|
|
@@ -10,29 +17,40 @@ import { getMiniAppScriptId } from './utils-functions.mjs';
|
|
|
10
17
|
import { getProjectRoot, guessGodotProjectDir } from './utils.mjs';
|
|
11
18
|
import { exportGodot, promptGodotOptions } from './export_godot.mjs';
|
|
12
19
|
|
|
20
|
+
// 获取当前文件所在目录
|
|
13
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
22
|
const __dirname = path.dirname(__filename);
|
|
15
23
|
const PROJECT_ROOT = getProjectRoot();
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
|
-
* 压缩文件夹为
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
26
|
+
* 压缩文件夹为 ZIP 文件
|
|
27
|
+
*
|
|
28
|
+
* 将指定目录压缩为 ZIP 格式,自动过滤 .map 文件(源码映射文件)
|
|
29
|
+
*
|
|
30
|
+
* @param {string} inputPath - 要压缩的目录路径
|
|
31
|
+
* @param {string} outputPath - 输出的 ZIP 文件路径
|
|
32
|
+
* @returns {Promise<string>} 压缩包完整路径
|
|
33
|
+
* @throws {Error} 当输入路径不存在或不是目录时抛出错误
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const zipPath = await handleZip('./dist', './dist.dgz');
|
|
22
37
|
*/
|
|
23
38
|
export async function handleZip(inputPath, outputPath) {
|
|
24
39
|
Logger.info(`开始压缩文件夹: ${inputPath}`);
|
|
25
40
|
Logger.info(`输出文件: ${outputPath}`);
|
|
26
41
|
|
|
42
|
+
// 验证输入路径是否存在
|
|
27
43
|
if (!(await fse.pathExists(inputPath))) {
|
|
28
44
|
throw new Error(`输入路径不存在: ${inputPath}`);
|
|
29
45
|
}
|
|
30
46
|
|
|
47
|
+
// 验证是否为目录
|
|
31
48
|
const stats = await fse.stat(inputPath);
|
|
32
49
|
if (!stats.isDirectory()) {
|
|
33
50
|
throw new Error(`输入路径不是目录: ${inputPath}`);
|
|
34
51
|
}
|
|
35
52
|
|
|
53
|
+
// 检查目录是否为空
|
|
36
54
|
const files = await fse.readdir(inputPath);
|
|
37
55
|
Logger.debug(`目录内容: ${files.length} 个文件/文件夹`);
|
|
38
56
|
|
|
@@ -44,6 +62,7 @@ export async function handleZip(inputPath, outputPath) {
|
|
|
44
62
|
const admzip = new AdmZip();
|
|
45
63
|
|
|
46
64
|
try {
|
|
65
|
+
// 添加目录到 ZIP,过滤 .map 文件(源码映射文件,不需要打包)
|
|
47
66
|
await admzip.addLocalFolderPromise(inputPath, {
|
|
48
67
|
filter: (filename) => {
|
|
49
68
|
if (filename.endsWith('.map')) {
|
|
@@ -55,9 +74,11 @@ export async function handleZip(inputPath, outputPath) {
|
|
|
55
74
|
}
|
|
56
75
|
});
|
|
57
76
|
|
|
77
|
+
// 写入 ZIP 文件
|
|
58
78
|
await admzip.writeZipPromise(zipPath);
|
|
59
79
|
Logger.success(`压缩完成: ${zipPath}`);
|
|
60
80
|
|
|
81
|
+
// 显示压缩包大小
|
|
61
82
|
const zipStats = await fse.stat(zipPath);
|
|
62
83
|
Logger.info(`ZIP文件大小: ${FileSystemUtils.formatFileSize(zipStats.size)}`);
|
|
63
84
|
|
|
@@ -70,37 +91,64 @@ export async function handleZip(inputPath, outputPath) {
|
|
|
70
91
|
|
|
71
92
|
/**
|
|
72
93
|
* 构建小程序包
|
|
73
|
-
*
|
|
74
|
-
*
|
|
94
|
+
*
|
|
95
|
+
* 支持两种类型的项目构建:
|
|
96
|
+
* 1. 普通小程序项目:使用 webpack 打包
|
|
97
|
+
* 2. Godot 游戏项目:使用 Godot 导出工具打包
|
|
98
|
+
*
|
|
99
|
+
* @param {object} newAppInfo - 应用信息对象
|
|
100
|
+
* @param {string} newAppInfo.name - 应用名称
|
|
101
|
+
* @param {string} newAppInfo.appid - 应用ID
|
|
102
|
+
* @param {string} newAppInfo.version - 应用版本
|
|
103
|
+
* @param {string} [newAppInfo.entry] - 入口文件(普通小程序必需)
|
|
104
|
+
* @param {string} [newAppInfo.host] - 服务器地址
|
|
105
|
+
* @param {string} [newAppInfo.appUrlConfig] - 应用配置URL
|
|
106
|
+
* @param {string} [newAppInfo.zipUrl] - 压缩包URL
|
|
107
|
+
* @param {boolean} [isGodot=false] - 是否为 Godot 项目
|
|
108
|
+
* @returns {Promise<string>} 压缩包完整路径
|
|
109
|
+
* @throws {Error} 当必需字段缺失或构建失败时抛出错误
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* const zipPath = await mpBuildShell({
|
|
113
|
+
* name: '我的应用',
|
|
114
|
+
* appid: 'com.example.app',
|
|
115
|
+
* version: '1.0.0',
|
|
116
|
+
* entry: './src/index.js'
|
|
117
|
+
* }, false);
|
|
75
118
|
*/
|
|
76
|
-
export async function mpBuildShell(newAppInfo, isGodot) {
|
|
119
|
+
export async function mpBuildShell(newAppInfo, isGodot = false) {
|
|
77
120
|
Logger.info(`开始构建小程序包...`);
|
|
78
121
|
Logger.info(`应用: ${newAppInfo.name}`);
|
|
79
122
|
Logger.info(`应用ID: ${newAppInfo.appid}`);
|
|
80
123
|
Logger.info(`版本: ${newAppInfo.version}`);
|
|
81
124
|
Logger.info(`平台: ${CONSTANTS.BUNDLE_PLATFORM}`);
|
|
125
|
+
Logger.info(`项目类型: ${isGodot ? 'Godot 游戏' : '普通小程序'}`);
|
|
82
126
|
|
|
83
|
-
//
|
|
127
|
+
// ========== 参数验证 ==========
|
|
84
128
|
if (!newAppInfo.name || !newAppInfo.appid || !newAppInfo.version) {
|
|
85
129
|
throw new Error('缺少必需的应用信息: name, appid, version');
|
|
86
130
|
}
|
|
87
131
|
|
|
88
|
-
//
|
|
132
|
+
// ========== 目录准备 ==========
|
|
133
|
+
// 创建临时目录(用于存放构建过程中的临时文件)
|
|
89
134
|
const tempPath = CONSTANTS.TEMP_DIR;
|
|
90
135
|
if (!FileSystemUtils.ensureDirectory(tempPath)) {
|
|
91
136
|
throw new Error('无法创建临时目录');
|
|
92
137
|
}
|
|
93
138
|
|
|
139
|
+
// 创建输出目录(最终构建产物存放位置)
|
|
94
140
|
const mpOutputPath = CONSTANTS.MP_OUTPUT_DIR;
|
|
95
141
|
if (!FileSystemUtils.ensureDirectory(mpOutputPath)) {
|
|
96
142
|
throw new Error('无法创建输出目录');
|
|
97
143
|
}
|
|
98
144
|
|
|
145
|
+
// ========== 应用配置准备 ==========
|
|
99
146
|
const { version, appid } = newAppInfo;
|
|
100
147
|
const scriptName = getMiniAppScriptId(appid, version);
|
|
101
148
|
|
|
149
|
+
// 构建 app.json 配置
|
|
102
150
|
const appJson = {
|
|
103
|
-
type: 'app',
|
|
151
|
+
type: isGodot ? 'game' : 'app', // Godot 项目类型为 'game',普通小程序为 'app'
|
|
104
152
|
engines: '2.0.0',
|
|
105
153
|
name: newAppInfo.name,
|
|
106
154
|
appid,
|
|
@@ -109,55 +157,105 @@ export async function mpBuildShell(newAppInfo, isGodot) {
|
|
|
109
157
|
zipUrl: newAppInfo.zipUrl || `${newAppInfo.host}/dist.dgz`
|
|
110
158
|
};
|
|
111
159
|
|
|
112
|
-
|
|
113
|
-
|
|
160
|
+
// 输出路径配置
|
|
161
|
+
let outputPath = path.join(mpOutputPath, 'dist'); // 最终输出目录
|
|
162
|
+
let distPackagePath = path.join(mpOutputPath, 'dist.dgz'); // 最终压缩包路径
|
|
114
163
|
const rootIndexPath = path.join(tempPath, '..', 'index.js');
|
|
115
|
-
|
|
164
|
+
// Webpack 构建选项(仅普通小程序使用)
|
|
116
165
|
const mpOptions = {
|
|
117
166
|
name: scriptName,
|
|
118
167
|
filename: `${scriptName}.bundle`,
|
|
119
168
|
exposes: {
|
|
120
169
|
'App': newAppInfo.entry,
|
|
121
170
|
},
|
|
122
|
-
outputPath: path.join(mpOutputPath, 'build'),
|
|
171
|
+
outputPath: path.join(mpOutputPath, 'build'), // webpack 构建输出目录
|
|
123
172
|
extraChunksPath: outputPath,
|
|
124
|
-
manifest: path.join(mpOutputPath, '
|
|
173
|
+
manifest: path.join(mpOutputPath, 'manifest.json'),
|
|
125
174
|
};
|
|
175
|
+
Logger.info('mpOptions', mpOptions);
|
|
126
176
|
|
|
177
|
+
// 保存 app.json 到输出目录
|
|
127
178
|
const appJsonPath = path.join(mpOutputPath, 'app.json');
|
|
128
179
|
await fse.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
129
180
|
const webpackConfigPath = path.join(__dirname, 'webpack.config.mjs');
|
|
130
181
|
|
|
182
|
+
// ========== 清空输出目录 ==========
|
|
183
|
+
// 确保每次构建都是干净的状态,避免旧文件残留
|
|
184
|
+
Logger.info('清空输出目录...');
|
|
185
|
+
await fse.ensureDir(outputPath);
|
|
186
|
+
await fse.emptyDir(outputPath);
|
|
187
|
+
|
|
188
|
+
// 清空 webpack 构建目录
|
|
189
|
+
// 如果 buildPath 是一个文件,先删除它,然后创建目录
|
|
190
|
+
const buildPath = path.join(mpOutputPath, 'build');
|
|
191
|
+
if (await fse.pathExists(buildPath)) {
|
|
192
|
+
const stats = await fse.stat(buildPath);
|
|
193
|
+
if (stats.isFile()) {
|
|
194
|
+
// 如果是文件,删除它
|
|
195
|
+
await fse.remove(buildPath);
|
|
196
|
+
Logger.info(`已删除文件: ${buildPath}`);
|
|
197
|
+
}
|
|
198
|
+
// 确保是目录并清空
|
|
199
|
+
await fse.ensureDir(buildPath);
|
|
200
|
+
await fse.emptyDir(buildPath);
|
|
201
|
+
} else {
|
|
202
|
+
// 如果不存在,直接创建目录
|
|
203
|
+
await fse.ensureDir(buildPath);
|
|
204
|
+
}
|
|
131
205
|
|
|
206
|
+
// ========== 执行构建 ==========
|
|
132
207
|
try {
|
|
133
208
|
if (!isGodot) {
|
|
209
|
+
// ========== 普通小程序构建流程 ==========
|
|
210
|
+
Logger.info('使用 Webpack 构建小程序包...');
|
|
134
211
|
const command = `npx cross-env APP_ROOT_INDEX_PATH=${rootIndexPath} APP_EXPOSES_OPTIONS=${encodeURI(JSON.stringify(mpOptions))} webpack --config ${webpackConfigPath}`;
|
|
135
212
|
await CommandExecutor.execute(command, {
|
|
136
213
|
description: '构建小程序包',
|
|
137
|
-
timeout: 120000,
|
|
214
|
+
timeout: 120000, // 2 分钟超时
|
|
138
215
|
showCommand: false
|
|
139
216
|
});
|
|
140
217
|
} else {
|
|
218
|
+
// ========== Godot 游戏构建流程 ==========
|
|
141
219
|
Logger.info('检测到 Godot 项目,开始执行 Godot 导出流程...');
|
|
220
|
+
|
|
221
|
+
// 根据平台常量确定默认平台
|
|
142
222
|
const defaultPlatform = ['android', 'ios'].includes(CONSTANTS.BUNDLE_PLATFORM)
|
|
143
223
|
? CONSTANTS.BUNDLE_PLATFORM
|
|
144
224
|
: 'ios';
|
|
225
|
+
|
|
226
|
+
// 准备 Godot 导出选项的默认值
|
|
145
227
|
const godotDefaults = {
|
|
146
228
|
targetBaseDir: outputPath,
|
|
147
229
|
projectDir: guessGodotProjectDir(PROJECT_ROOT),
|
|
148
|
-
name: newAppInfo.
|
|
230
|
+
name: newAppInfo.appid || 'GodotApp',
|
|
149
231
|
preset: defaultPlatform === 'android' ? 'Android' : 'iOS',
|
|
150
232
|
platform: defaultPlatform
|
|
151
233
|
};
|
|
234
|
+
|
|
235
|
+
// 提示用户输入或确认 Godot 导出选项
|
|
152
236
|
const godotOptions = await promptGodotOptions(godotDefaults);
|
|
153
237
|
outputPath = path.resolve(godotOptions.targetBaseDir);
|
|
154
238
|
await fse.ensureDir(outputPath);
|
|
155
|
-
|
|
239
|
+
|
|
240
|
+
// 执行 Godot 导出
|
|
156
241
|
await exportGodot(godotOptions);
|
|
242
|
+
|
|
243
|
+
// Godot 项目使用平台特定的文件名(如 dist.ios.dgz 或 dist.android.dgz)
|
|
244
|
+
const platformName = godotOptions.platform === 'ios' ? 'ios' : 'android';
|
|
245
|
+
distPackagePath = path.join(mpOutputPath, `dist.${platformName}.dgz`);
|
|
246
|
+
|
|
247
|
+
// 更新 app.json 中的 zipUrl
|
|
248
|
+
appJson.zipUrl = newAppInfo.zipUrl || `${newAppInfo.host}/dist.${platformName}.dgz`;
|
|
249
|
+
await fse.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
157
250
|
}
|
|
158
251
|
|
|
252
|
+
// ========== 打包最终产物 ==========
|
|
253
|
+
// 将 app.json 复制到输出目录
|
|
159
254
|
await fse.ensureDir(outputPath);
|
|
160
255
|
await fse.copy(appJsonPath, path.join(outputPath, 'app.json'));
|
|
256
|
+
fse.copyFileSync(path.join(outputPath, '../manifest.json'), path.join(outputPath, 'manifest.json'));
|
|
257
|
+
|
|
258
|
+
// 压缩输出目录为最终包
|
|
161
259
|
const zipPath = await handleZip(outputPath, distPackagePath);
|
|
162
260
|
Logger.success(`压缩包已创建: ${zipPath}`);
|
|
163
261
|
return zipPath;
|
package/src/debug.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 调试模块
|
|
3
|
+
* 负责启动小程序真机调试服务器
|
|
4
|
+
*
|
|
5
|
+
* @module debug
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import path from 'path';
|
|
2
9
|
import fse from 'fs-extra';
|
|
3
10
|
import superstatic from 'superstatic';
|
|
@@ -12,45 +19,76 @@ import { printQrcode } from './utils-functions.mjs';
|
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* 小程序真机调试
|
|
22
|
+
*
|
|
23
|
+
* 功能流程:
|
|
24
|
+
* 1. 构建小程序包
|
|
25
|
+
* 2. 验证网络配置
|
|
26
|
+
* 3. 启动本地调试服务器
|
|
27
|
+
* 4. 生成调试二维码
|
|
28
|
+
*
|
|
15
29
|
* @param {object} newAppInfo - 应用信息
|
|
30
|
+
* @param {string} newAppInfo.name - 应用名称
|
|
31
|
+
* @param {string} newAppInfo.appid - 应用ID
|
|
32
|
+
* @param {string} newAppInfo.version - 应用版本
|
|
33
|
+
* @param {string} [newAppInfo.entry] - 入口文件(普通小程序必需,Godot 项目不需要)
|
|
34
|
+
* @param {boolean} [isGodot=false] - 是否为 Godot 项目
|
|
35
|
+
* @throws {Error} 当必需字段缺失或网络配置验证失败时抛出错误
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* await mpDebugShell({
|
|
39
|
+
* name: '我的应用',
|
|
40
|
+
* appid: 'com.example.app',
|
|
41
|
+
* version: '1.0.0',
|
|
42
|
+
* entry: './src/index.js'
|
|
43
|
+
* }, false);
|
|
16
44
|
*/
|
|
17
|
-
export async function mpDebugShell(newAppInfo) {
|
|
45
|
+
export async function mpDebugShell(newAppInfo, isGodot = false) {
|
|
18
46
|
try {
|
|
19
|
-
//
|
|
20
|
-
|
|
47
|
+
// ========== 参数验证 ==========
|
|
48
|
+
// Godot 项目不需要 entry 字段,普通小程序需要
|
|
49
|
+
if (!isGodot && !newAppInfo.entry) {
|
|
21
50
|
throw new Error('缺少必需的应用信息: entry');
|
|
22
51
|
}
|
|
23
52
|
|
|
24
|
-
|
|
53
|
+
// ========== 构建小程序包 ==========
|
|
54
|
+
Logger.info('开始构建小程序包...');
|
|
55
|
+
const zipPath = await mpBuildShell(newAppInfo, isGodot);
|
|
25
56
|
const assetsPath = path.dirname(zipPath);
|
|
26
57
|
const appJsonPath = path.join(assetsPath, 'app.json');
|
|
27
58
|
|
|
28
59
|
Logger.info(`资源路径: ${assetsPath}`);
|
|
29
60
|
const applocaljson = fse.readJsonSync(appJsonPath);
|
|
30
61
|
|
|
31
|
-
//
|
|
62
|
+
// ========== 网络配置验证 ==========
|
|
32
63
|
const networkValidation = await ConfigValidator.validateNetworkConfig();
|
|
33
64
|
if (!networkValidation.isValid) {
|
|
34
65
|
throw new Error(`网络配置验证失败: ${networkValidation.error}`);
|
|
35
66
|
}
|
|
36
67
|
|
|
68
|
+
// ========== 启动调试服务器 ==========
|
|
37
69
|
const host = networkValidation.host;
|
|
38
|
-
const port = await portfinder.getPortPromise();
|
|
70
|
+
const port = await portfinder.getPortPromise(); // 自动查找可用端口
|
|
39
71
|
|
|
72
|
+
// 创建静态文件服务器
|
|
40
73
|
const app = superstatic.server({
|
|
41
74
|
port: port,
|
|
42
75
|
address: host,
|
|
43
|
-
cwd: assetsPath
|
|
76
|
+
cwd: assetsPath // 服务器根目录
|
|
44
77
|
});
|
|
45
78
|
|
|
79
|
+
// 启动服务器
|
|
46
80
|
const server = app.listen(function () {
|
|
47
81
|
const address = server.address();
|
|
48
82
|
|
|
83
|
+
// ========== 生成调试配置 ==========
|
|
84
|
+
// 构建调试用的 URL
|
|
49
85
|
const debugerAppUrlConfig = `http://${host}:${address.port}/app.json`;
|
|
50
86
|
const debugerZip = `http://${host}:${address.port}/dist.dgz`;
|
|
87
|
+
|
|
88
|
+
// 构建调试参数
|
|
51
89
|
const args = {
|
|
52
90
|
...applocaljson,
|
|
53
|
-
development: true,
|
|
91
|
+
development: true, // 标记为开发模式
|
|
54
92
|
"name": applocaljson.name,
|
|
55
93
|
"appid": applocaljson.appid,
|
|
56
94
|
"version": applocaljson.version,
|
|
@@ -58,15 +96,17 @@ export async function mpDebugShell(newAppInfo) {
|
|
|
58
96
|
zipUrl: debugerZip,
|
|
59
97
|
};
|
|
60
98
|
|
|
99
|
+
// 更新 app.json 文件,写入调试 URL
|
|
61
100
|
fse.writeJSONSync(appJsonPath, {
|
|
62
101
|
...fse.readJSONSync(appJsonPath),
|
|
63
102
|
appUrlConfig: debugerAppUrlConfig,
|
|
64
103
|
zipUrl: debugerZip,
|
|
65
104
|
});
|
|
66
105
|
|
|
106
|
+
// 生成调试二维码 URL
|
|
67
107
|
const qrcodeUrl = `https://share.dagouzhi.com/#/pages/index/index?data=${encodeURIComponent(JSON.stringify(args))}`;
|
|
68
108
|
|
|
69
|
-
// 显示调试信息
|
|
109
|
+
// ========== 显示调试信息 ==========
|
|
70
110
|
console.log('\n');
|
|
71
111
|
console.log(boxen(
|
|
72
112
|
gradient.rainbow('小程序真机调试') + '\n\n' +
|
|
@@ -82,23 +122,29 @@ export async function mpDebugShell(newAppInfo) {
|
|
|
82
122
|
}
|
|
83
123
|
));
|
|
84
124
|
|
|
125
|
+
// 显示调试配置 JSON
|
|
85
126
|
console.log('\n' + chalk.yellow('调试配置:'));
|
|
86
127
|
console.log(JSON.stringify(args, undefined, 2));
|
|
87
128
|
|
|
129
|
+
// 显示调试链接
|
|
88
130
|
console.log('\n' + chalk.green('调试链接:'));
|
|
89
131
|
console.log(qrcodeUrl);
|
|
90
132
|
|
|
133
|
+
// 显示二维码
|
|
91
134
|
console.log('\n' + chalk.blue('二维码:'));
|
|
92
135
|
printQrcode(qrcodeUrl);
|
|
93
136
|
|
|
94
137
|
Logger.info('调试服务器已启动,按 Ctrl+C 停止');
|
|
95
138
|
});
|
|
96
139
|
|
|
97
|
-
//
|
|
140
|
+
// ========== 优雅关闭处理 ==========
|
|
141
|
+
// 移除全局的信号处理,添加专用的服务器关闭处理
|
|
142
|
+
// 这样可以确保服务器能够正确关闭,避免资源泄漏
|
|
98
143
|
const handleShutdown = () => {
|
|
99
144
|
Logger.info('\n正在关闭调试服务器...');
|
|
100
145
|
let isClosed = false;
|
|
101
146
|
|
|
147
|
+
// 尝试关闭服务器
|
|
102
148
|
server.close(() => {
|
|
103
149
|
if (!isClosed) {
|
|
104
150
|
isClosed = true;
|
|
@@ -108,6 +154,7 @@ export async function mpDebugShell(newAppInfo) {
|
|
|
108
154
|
});
|
|
109
155
|
|
|
110
156
|
// 如果服务器在 3 秒内没有关闭,强制退出
|
|
157
|
+
// 防止服务器关闭卡死导致进程无法退出
|
|
111
158
|
setTimeout(() => {
|
|
112
159
|
if (!isClosed) {
|
|
113
160
|
isClosed = true;
|
|
@@ -117,12 +164,12 @@ export async function mpDebugShell(newAppInfo) {
|
|
|
117
164
|
}, 3000);
|
|
118
165
|
};
|
|
119
166
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
167
|
+
// 注册信号处理器
|
|
168
|
+
// 移除可能存在的全局处理器,避免冲突
|
|
122
169
|
process.removeAllListeners('SIGINT');
|
|
123
170
|
process.removeAllListeners('SIGTERM');
|
|
124
|
-
process.once('SIGINT', handleShutdown);
|
|
125
|
-
process.once('SIGTERM', handleShutdown);
|
|
171
|
+
process.once('SIGINT', handleShutdown); // Ctrl+C
|
|
172
|
+
process.once('SIGTERM', handleShutdown); // 终止信号
|
|
126
173
|
|
|
127
174
|
} catch (error) {
|
|
128
175
|
Logger.error(`小程序调试失败: ${error.message}`);
|
package/src/export_godot.mjs
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Godot 导出模块
|
|
5
|
+
* 负责处理 Godot 游戏项目的导出和打包
|
|
6
|
+
*
|
|
7
|
+
* @module export_godot
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
import path from 'node:path';
|
|
4
11
|
import { pathToFileURL } from 'node:url';
|
|
5
12
|
import { accessSync, constants as fsConstants } from 'node:fs';
|
|
@@ -10,38 +17,71 @@ import AdmZip from 'adm-zip';
|
|
|
10
17
|
import { execa } from 'execa';
|
|
11
18
|
import inquirer from 'inquirer';
|
|
12
19
|
|
|
20
|
+
// ========== 常量定义 ==========
|
|
21
|
+
|
|
22
|
+
/** Godot 编辑器默认路径(macOS) */
|
|
13
23
|
const DEFAULT_GODOT_BIN = '/Applications/Godot.app/Contents/MacOS/Godot';
|
|
24
|
+
|
|
25
|
+
/** Godot 配置缓存目录 */
|
|
14
26
|
const GODOT_CACHE_DIR = path.join(homedir() || process.cwd(), '.htyf');
|
|
27
|
+
|
|
28
|
+
/** Godot 配置缓存文件路径 */
|
|
15
29
|
const GODOT_CACHE_FILE = path.join(GODOT_CACHE_DIR, 'godot-config.json');
|
|
16
30
|
|
|
31
|
+
/** 支持的平台枚举 */
|
|
17
32
|
const PLATFORMS = {
|
|
18
33
|
IOS: 'ios',
|
|
19
34
|
ANDROID: 'android'
|
|
20
35
|
};
|
|
21
36
|
|
|
37
|
+
/** 平台预设名称映射 */
|
|
22
38
|
const PRESET_NAMES = {
|
|
23
39
|
[PLATFORMS.IOS]: 'iOS',
|
|
24
40
|
[PLATFORMS.ANDROID]: 'Android'
|
|
25
41
|
};
|
|
26
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 提示用户输入或确认 Godot 导出选项
|
|
45
|
+
*
|
|
46
|
+
* 如果所有必需参数都已提供,则直接返回,不进行交互式询问
|
|
47
|
+
* 否则通过命令行交互获取用户输入
|
|
48
|
+
*
|
|
49
|
+
* @param {object} [defaults={}] - 默认选项
|
|
50
|
+
* @param {string} [defaults.targetBaseDir] - 导出目标根目录
|
|
51
|
+
* @param {string} [defaults.projectDir] - Godot 项目目录
|
|
52
|
+
* @param {string} [defaults.appid] - 应用ID(用作导出文件名)
|
|
53
|
+
* @param {string} [defaults.preset] - Godot 导出预设名称
|
|
54
|
+
* @param {string} [defaults.platform] - 导出平台('ios' 或 'android')
|
|
55
|
+
* @returns {Promise<object>} Godot 导出选项对象
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const options = await promptGodotOptions({
|
|
59
|
+
* targetBaseDir: './dist',
|
|
60
|
+
* projectDir: './game',
|
|
61
|
+
* appid: 'com.example.game',
|
|
62
|
+
* preset: 'iOS',
|
|
63
|
+
* platform: 'ios'
|
|
64
|
+
* });
|
|
65
|
+
*/
|
|
27
66
|
export async function promptGodotOptions(defaults = {}) {
|
|
28
|
-
//
|
|
67
|
+
// 如果所有必需的值都已提供,直接返回,不进行询问(用于自动化场景)
|
|
29
68
|
if (
|
|
30
69
|
defaults.targetBaseDir &&
|
|
31
70
|
defaults.projectDir &&
|
|
32
|
-
defaults.
|
|
71
|
+
defaults.appid &&
|
|
33
72
|
defaults.preset &&
|
|
34
73
|
defaults.platform
|
|
35
74
|
) {
|
|
36
75
|
return {
|
|
37
76
|
targetBaseDir: String(defaults.targetBaseDir).trim(),
|
|
38
77
|
projectDir: String(defaults.projectDir).trim(),
|
|
39
|
-
name: String(defaults.
|
|
78
|
+
name: String(defaults.appid).trim(),
|
|
40
79
|
preset: String(defaults.preset).trim(),
|
|
41
80
|
platform: defaults.platform
|
|
42
81
|
};
|
|
43
82
|
}
|
|
44
83
|
|
|
84
|
+
// 通过交互式命令行获取用户输入
|
|
45
85
|
const answers = await inquirer.prompt([
|
|
46
86
|
{
|
|
47
87
|
type: 'input',
|
|
@@ -92,6 +132,12 @@ export async function promptGodotOptions(defaults = {}) {
|
|
|
92
132
|
};
|
|
93
133
|
}
|
|
94
134
|
|
|
135
|
+
/**
|
|
136
|
+
* 检查文件是否可执行
|
|
137
|
+
*
|
|
138
|
+
* @param {string} filePath - 文件路径
|
|
139
|
+
* @returns {boolean} 文件是否可执行
|
|
140
|
+
*/
|
|
95
141
|
function isExecutable(filePath) {
|
|
96
142
|
if (!filePath) return false;
|
|
97
143
|
if (!fs.existsSync(filePath)) return false;
|
|
@@ -104,6 +150,11 @@ function isExecutable(filePath) {
|
|
|
104
150
|
}
|
|
105
151
|
}
|
|
106
152
|
|
|
153
|
+
/**
|
|
154
|
+
* 读取缓存的 Godot 编辑器路径
|
|
155
|
+
*
|
|
156
|
+
* @returns {string} 缓存的路径,如果不存在则返回空字符串
|
|
157
|
+
*/
|
|
107
158
|
function readCachedGodotPath() {
|
|
108
159
|
try {
|
|
109
160
|
if (!fs.existsSync(GODOT_CACHE_FILE)) {
|
|
@@ -116,15 +167,26 @@ function readCachedGodotPath() {
|
|
|
116
167
|
}
|
|
117
168
|
}
|
|
118
169
|
|
|
170
|
+
/**
|
|
171
|
+
* 保存 Godot 编辑器路径到缓存
|
|
172
|
+
*
|
|
173
|
+
* @param {string} godotPath - Godot 编辑器路径
|
|
174
|
+
*/
|
|
119
175
|
async function writeCachedGodotPath(godotPath) {
|
|
120
176
|
try {
|
|
121
177
|
await fs.ensureDir(GODOT_CACHE_DIR);
|
|
122
178
|
await fs.writeJson(GODOT_CACHE_FILE, { godotPath }, { spaces: 2 });
|
|
123
179
|
} catch {
|
|
124
|
-
//
|
|
180
|
+
// 缓存失败不应该阻塞执行
|
|
125
181
|
}
|
|
126
182
|
}
|
|
127
183
|
|
|
184
|
+
/**
|
|
185
|
+
* 提示用户输入 Godot 编辑器路径
|
|
186
|
+
*
|
|
187
|
+
* @param {string} defaultPath - 默认路径
|
|
188
|
+
* @returns {Promise<string>} 用户输入的路径
|
|
189
|
+
*/
|
|
128
190
|
async function promptForGodotBinaryPath(defaultPath) {
|
|
129
191
|
const { godotPath } = await inquirer.prompt([
|
|
130
192
|
{
|
|
@@ -139,69 +201,142 @@ async function promptForGodotBinaryPath(defaultPath) {
|
|
|
139
201
|
return godotPath.trim();
|
|
140
202
|
}
|
|
141
203
|
|
|
204
|
+
/**
|
|
205
|
+
* 解析并获取 Godot 编辑器可执行文件路径
|
|
206
|
+
*
|
|
207
|
+
* 优先级:环境变量 > 缓存 > 默认路径
|
|
208
|
+
* 总是会询问用户,但将缓存值或默认值作为默认输入
|
|
209
|
+
*
|
|
210
|
+
* @returns {Promise<string>} Godot 编辑器可执行文件路径
|
|
211
|
+
* @throws {Error} 当路径不可执行时抛出错误
|
|
212
|
+
*/
|
|
142
213
|
async function resolveGodotBinary() {
|
|
214
|
+
// 获取可能的默认值(优先级:环境变量 > 缓存 > 默认路径)
|
|
143
215
|
const override = process.env.GODOT_EDITOR ? process.env.GODOT_EDITOR.trim() : '';
|
|
144
|
-
if (override && isExecutable(override)) {
|
|
145
|
-
return override;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
216
|
const cached = readCachedGodotPath();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const fallbackDefault = override || cached || DEFAULT_GODOT_BIN;
|
|
159
|
-
const godotPath = await promptForGodotBinaryPath(fallbackDefault);
|
|
160
|
-
|
|
217
|
+
|
|
218
|
+
// 确定默认值
|
|
219
|
+
let defaultPath = override || cached || DEFAULT_GODOT_BIN;
|
|
220
|
+
|
|
221
|
+
// 总是询问用户,但将缓存值或默认值作为默认输入
|
|
222
|
+
const godotPath = await promptForGodotBinaryPath(defaultPath);
|
|
223
|
+
|
|
224
|
+
// 验证路径是否可执行
|
|
161
225
|
if (!isExecutable(godotPath)) {
|
|
162
226
|
throw new Error(`Godot 二进制文件不可执行: ${godotPath}`);
|
|
163
227
|
}
|
|
164
228
|
|
|
229
|
+
// 保存用户输入作为下次的默认值
|
|
165
230
|
await writeCachedGodotPath(godotPath);
|
|
166
231
|
return godotPath;
|
|
167
232
|
}
|
|
168
233
|
|
|
234
|
+
/**
|
|
235
|
+
* 执行 Godot 命令行命令
|
|
236
|
+
*
|
|
237
|
+
* @param {string} godotBin - Godot 编辑器可执行文件路径
|
|
238
|
+
* @param {string[]} args - 命令参数
|
|
239
|
+
*/
|
|
169
240
|
async function runGodotCommand(godotBin, args) {
|
|
170
241
|
await execa(godotBin, args, { stdio: 'inherit' });
|
|
171
242
|
}
|
|
172
243
|
|
|
244
|
+
/**
|
|
245
|
+
* 处理 Android 平台导出
|
|
246
|
+
*
|
|
247
|
+
* Android 导出流程:
|
|
248
|
+
* 1. 使用 Godot 导出为 ZIP 包
|
|
249
|
+
* 2. 解压 ZIP 包到指定目录
|
|
250
|
+
* 3. 清理临时 ZIP 文件
|
|
251
|
+
*
|
|
252
|
+
* @param {object} options - 导出选项
|
|
253
|
+
* @param {string} options.godotBin - Godot 编辑器路径
|
|
254
|
+
* @param {string} options.projectDir - Godot 项目目录
|
|
255
|
+
* @param {string} options.preset - 导出预设名称
|
|
256
|
+
* @param {string} options.targetBaseDir - 目标根目录
|
|
257
|
+
* @param {string} options.name - 导出文件名(不含扩展名)
|
|
258
|
+
*/
|
|
173
259
|
async function handleAndroidExport({ godotBin, projectDir, preset, targetBaseDir, name }) {
|
|
260
|
+
// Android 导出路径:targetBaseDir/android/app/src/main/assets/
|
|
174
261
|
const assetsBaseDir = path.join(targetBaseDir, 'android', 'app', 'src', 'main', 'assets');
|
|
175
262
|
await fs.ensureDir(assetsBaseDir);
|
|
176
263
|
|
|
264
|
+
// 使用 Godot 导出为 ZIP 包
|
|
177
265
|
const zipPath = path.join(assetsBaseDir, `${name}.zip`);
|
|
178
266
|
await runGodotCommand(godotBin, ['--headless', '--path', projectDir, '--export-pack', preset, zipPath]);
|
|
179
267
|
|
|
268
|
+
// 解压 ZIP 包到目标目录
|
|
180
269
|
const targetDir = path.join(assetsBaseDir, name);
|
|
181
270
|
await fs.remove(targetDir);
|
|
182
271
|
await fs.ensureDir(targetDir);
|
|
183
272
|
|
|
184
273
|
const zip = new AdmZip(zipPath);
|
|
185
274
|
zip.extractAllTo(targetDir, true);
|
|
275
|
+
|
|
276
|
+
// 清理临时 ZIP 文件
|
|
186
277
|
await fs.remove(zipPath);
|
|
187
278
|
}
|
|
188
279
|
|
|
280
|
+
/**
|
|
281
|
+
* 处理 iOS 平台导出
|
|
282
|
+
*
|
|
283
|
+
* iOS 导出流程:
|
|
284
|
+
* 1. 使用 Godot 直接导出为 PCK 文件
|
|
285
|
+
*
|
|
286
|
+
* @param {object} options - 导出选项
|
|
287
|
+
* @param {string} options.godotBin - Godot 编辑器路径
|
|
288
|
+
* @param {string} options.projectDir - Godot 项目目录
|
|
289
|
+
* @param {string} options.preset - 导出预设名称
|
|
290
|
+
* @param {string} options.targetBaseDir - 目标根目录
|
|
291
|
+
* @param {string} options.name - 导出文件名(不含扩展名)
|
|
292
|
+
*/
|
|
189
293
|
async function handleIosExport({ godotBin, projectDir, preset, targetBaseDir, name }) {
|
|
294
|
+
// iOS 导出路径:targetBaseDir/ios/
|
|
190
295
|
const assetsBaseDir = path.join(targetBaseDir, 'ios');
|
|
191
296
|
await fs.ensureDir(assetsBaseDir);
|
|
297
|
+
|
|
298
|
+
// 使用 Godot 直接导出为 PCK 文件
|
|
192
299
|
const outputPath = path.join(assetsBaseDir, `${name}.pck`);
|
|
193
300
|
await runGodotCommand(godotBin, ['--headless', '--path', projectDir, '--export-pack', preset, outputPath]);
|
|
194
301
|
}
|
|
195
302
|
|
|
303
|
+
/**
|
|
304
|
+
* 导出 Godot 项目
|
|
305
|
+
*
|
|
306
|
+
* 主要流程:
|
|
307
|
+
* 1. 解析并获取 Godot 编辑器路径
|
|
308
|
+
* 2. 执行资源导入(执行两次以确保资源正确导入)
|
|
309
|
+
* 3. 根据平台执行相应的导出流程
|
|
310
|
+
*
|
|
311
|
+
* @param {object} options - 导出选项
|
|
312
|
+
* @param {string} options.platform - 导出平台('ios' 或 'android')
|
|
313
|
+
* @param {string} options.projectDir - Godot 项目目录
|
|
314
|
+
* @param {string} options.preset - Godot 导出预设名称
|
|
315
|
+
* @param {string} options.targetBaseDir - 目标根目录
|
|
316
|
+
* @param {string} options.name - 导出文件名(不含扩展名)
|
|
317
|
+
* @throws {Error} 当平台不支持时抛出错误
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* await exportGodot({
|
|
321
|
+
* platform: 'ios',
|
|
322
|
+
* projectDir: './game',
|
|
323
|
+
* preset: 'iOS',
|
|
324
|
+
* targetBaseDir: './dist',
|
|
325
|
+
* name: 'MyGame'
|
|
326
|
+
* });
|
|
327
|
+
*/
|
|
196
328
|
export async function exportGodot(options) {
|
|
329
|
+
// 解析并获取 Godot 编辑器路径
|
|
197
330
|
const godotBin = await resolveGodotBinary();
|
|
198
331
|
const { platform, projectDir, preset, targetBaseDir, name } = options;
|
|
199
332
|
|
|
200
333
|
// 执行两次 import 以确保资源正确导入
|
|
334
|
+
// 某些情况下,第一次导入可能不完整,执行两次可以确保所有资源都被正确导入
|
|
201
335
|
const importArgs = ['--headless', '--path', projectDir, '--import'];
|
|
202
336
|
await runGodotCommand(godotBin, importArgs);
|
|
203
337
|
await runGodotCommand(godotBin, importArgs);
|
|
204
338
|
|
|
339
|
+
// 根据平台执行相应的导出流程
|
|
205
340
|
switch (platform) {
|
|
206
341
|
case PLATFORMS.IOS:
|
|
207
342
|
await handleIosExport({ godotBin, projectDir, preset, targetBaseDir, name });
|
|
@@ -214,11 +349,21 @@ export async function exportGodot(options) {
|
|
|
214
349
|
}
|
|
215
350
|
}
|
|
216
351
|
|
|
352
|
+
/**
|
|
353
|
+
* 主函数(当文件作为 CLI 直接执行时)
|
|
354
|
+
*
|
|
355
|
+
* 如果该文件被直接执行(而非作为模块导入),则运行主函数
|
|
356
|
+
*/
|
|
217
357
|
async function main() {
|
|
218
358
|
const options = await promptGodotOptions();
|
|
219
359
|
await exportGodot(options);
|
|
220
360
|
}
|
|
221
361
|
|
|
362
|
+
/**
|
|
363
|
+
* 检查当前文件是否作为 CLI 直接执行
|
|
364
|
+
*
|
|
365
|
+
* @returns {boolean} 是否为 CLI 执行
|
|
366
|
+
*/
|
|
222
367
|
const isCliExecution = (() => {
|
|
223
368
|
if (!process.argv[1]) {
|
|
224
369
|
return false;
|
|
@@ -227,6 +372,7 @@ const isCliExecution = (() => {
|
|
|
227
372
|
return cliUrl === import.meta.url;
|
|
228
373
|
})();
|
|
229
374
|
|
|
375
|
+
// 如果作为 CLI 直接执行,运行主函数
|
|
230
376
|
if (isCliExecution) {
|
|
231
377
|
main().catch((error) => {
|
|
232
378
|
console.error(error.message || error);
|
package/src/index.mjs
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 红糖云服应用 CLI 工具主入口
|
|
5
|
+
*
|
|
6
|
+
* 功能:
|
|
7
|
+
* - 初始化新小程序项目
|
|
8
|
+
* - 小程序打包构建
|
|
9
|
+
* - 小程序真机调试
|
|
10
|
+
* - 清理临时文件
|
|
11
|
+
*
|
|
12
|
+
* 支持项目类型:
|
|
13
|
+
* - 普通小程序项目(使用 webpack 构建)
|
|
14
|
+
* - Godot 游戏项目(使用 Godot 导出工具)
|
|
6
15
|
*
|
|
7
16
|
* @author CLI Team
|
|
8
17
|
* @version 2.2.0
|
|
18
|
+
* @module index
|
|
9
19
|
*/
|
|
10
20
|
|
|
11
21
|
import inquirer from 'inquirer';
|
|
@@ -27,12 +37,18 @@ import { incrementVersion, updateAppConfig } from './utils-functions.mjs';
|
|
|
27
37
|
|
|
28
38
|
const projectPath = getProjectRoot();
|
|
29
39
|
|
|
30
|
-
//
|
|
40
|
+
// ========== 全局状态管理 ==========
|
|
41
|
+
// 用于优雅退出,避免重复退出操作
|
|
31
42
|
let isExiting = false;
|
|
32
43
|
let currentProcess = null;
|
|
33
44
|
|
|
34
45
|
/**
|
|
35
|
-
*
|
|
46
|
+
* 优雅退出处理函数
|
|
47
|
+
*
|
|
48
|
+
* 当收到退出信号(如 Ctrl+C)时:
|
|
49
|
+
* 1. 尝试关闭正在运行的进程
|
|
50
|
+
* 2. 延迟退出,给清理操作一些时间
|
|
51
|
+
* 3. 如果已经在退出中,强制退出
|
|
36
52
|
*/
|
|
37
53
|
function handleGracefulExit() {
|
|
38
54
|
if (isExiting) {
|
|
@@ -64,14 +80,22 @@ function handleGracefulExit() {
|
|
|
64
80
|
}, 100);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
//
|
|
83
|
+
// ========== 注册全局信号处理 ==========
|
|
84
|
+
// 使用 once 确保只注册一次
|
|
68
85
|
// 注意:某些模块(如 debug.mjs)可能会移除这些监听器并添加自己的处理
|
|
69
|
-
process.on('SIGINT', handleGracefulExit);
|
|
70
|
-
process.on('SIGTERM', handleGracefulExit);
|
|
86
|
+
process.on('SIGINT', handleGracefulExit); // Ctrl+C
|
|
87
|
+
process.on('SIGTERM', handleGracefulExit); // 终止信号
|
|
71
88
|
|
|
72
89
|
/**
|
|
73
90
|
* 主启动函数
|
|
74
|
-
*
|
|
91
|
+
*
|
|
92
|
+
* 根据操作类型执行相应的功能:
|
|
93
|
+
* - MP_BUILD: 构建小程序包
|
|
94
|
+
* - MP_DEBUG: 启动真机调试服务器
|
|
95
|
+
* - CLEAN: 清理临时文件
|
|
96
|
+
* - QUIT: 退出程序
|
|
97
|
+
*
|
|
98
|
+
* @param {string} action - 操作类型(ACTION_TYPES 中的值)
|
|
75
99
|
*/
|
|
76
100
|
async function Start(action) {
|
|
77
101
|
const actionNames = {
|
|
@@ -83,11 +107,13 @@ async function Start(action) {
|
|
|
83
107
|
|
|
84
108
|
Logger.info(`当前操作: ${actionNames[action] || '未知操作'}`);
|
|
85
109
|
|
|
110
|
+
// ========== 退出操作 ==========
|
|
86
111
|
if (action === ACTION_TYPES.QUIT) {
|
|
87
112
|
Logger.info('再见!');
|
|
88
113
|
return;
|
|
89
114
|
}
|
|
90
115
|
|
|
116
|
+
// ========== 清理操作 ==========
|
|
91
117
|
if (action === ACTION_TYPES.CLEAN) {
|
|
92
118
|
const { cleanType } = await inquirer.prompt([
|
|
93
119
|
{
|
|
@@ -134,17 +160,23 @@ async function Start(action) {
|
|
|
134
160
|
return;
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
//
|
|
163
|
+
// ========== 构建/调试操作 ==========
|
|
138
164
|
try {
|
|
165
|
+
// 读取应用配置
|
|
139
166
|
const appConfigPath = path.join(projectPath, 'app.json');
|
|
140
167
|
const appInfo = fse.readJsonSync(appConfigPath).htyf;
|
|
168
|
+
|
|
169
|
+
// 检测是否为 Godot 项目(通过检查是否存在 project.godot 文件)
|
|
141
170
|
const isGodot = fse.existsSync(path.join(projectPath, 'project.godot'));
|
|
142
|
-
Logger.info(
|
|
171
|
+
Logger.info(`项目类型: ${isGodot ? 'Godot 游戏项目' : '普通小程序项目'}`);
|
|
172
|
+
|
|
143
173
|
if (!appInfo) {
|
|
144
174
|
Logger.error('应用配置不存在,请先在app.json中配置htyf');
|
|
145
175
|
return;
|
|
146
176
|
}
|
|
147
177
|
|
|
178
|
+
// ========== 版本处理 ==========
|
|
179
|
+
// 自动递增版本号作为默认值
|
|
148
180
|
const version = appInfo?.version;
|
|
149
181
|
const incrementedVersion = version ? incrementVersion(version) : '1.0.0';
|
|
150
182
|
const { versionName } = await inquirer.prompt([
|
|
@@ -163,15 +195,18 @@ async function Start(action) {
|
|
|
163
195
|
Logger.info(`用户选择:`);
|
|
164
196
|
Logger.info(`版本名称: ${versionName}`);
|
|
165
197
|
|
|
198
|
+
// ========== 更新应用配置 ==========
|
|
199
|
+
// 合并用户输入的版本号到应用信息中
|
|
166
200
|
const newAppInfo = lodash.merge({}, appInfo, {
|
|
167
201
|
version: versionName,
|
|
168
202
|
});
|
|
169
203
|
|
|
204
|
+
// 更新 app.json 文件
|
|
170
205
|
if (!updateAppConfig(newAppInfo)) {
|
|
171
206
|
return;
|
|
172
207
|
}
|
|
173
208
|
|
|
174
|
-
//
|
|
209
|
+
// ========== 显示应用信息 ==========
|
|
175
210
|
console.log('\n' + boxen(
|
|
176
211
|
chalk.cyan('应用信息') + '\n\n' +
|
|
177
212
|
chalk.white('名称: ') + chalk.yellow(`${newAppInfo.name}`) + '\n' +
|
|
@@ -184,13 +219,16 @@ async function Start(action) {
|
|
|
184
219
|
}
|
|
185
220
|
));
|
|
186
221
|
|
|
187
|
-
Logger.info('
|
|
222
|
+
Logger.info('开始执行操作...');
|
|
188
223
|
|
|
224
|
+
// ========== 执行相应操作 ==========
|
|
189
225
|
switch (action) {
|
|
190
226
|
case ACTION_TYPES.MP_BUILD:
|
|
227
|
+
// 构建小程序包
|
|
191
228
|
await mpBuildShell(newAppInfo, isGodot);
|
|
192
229
|
break;
|
|
193
230
|
case ACTION_TYPES.MP_DEBUG:
|
|
231
|
+
// 启动真机调试服务器
|
|
194
232
|
await mpDebugShell(newAppInfo, isGodot);
|
|
195
233
|
break;
|
|
196
234
|
default:
|
|
@@ -203,7 +241,8 @@ async function Start(action) {
|
|
|
203
241
|
}
|
|
204
242
|
}
|
|
205
243
|
|
|
206
|
-
// 主程序入口
|
|
244
|
+
// ========== 主程序入口 ==========
|
|
245
|
+
// 显示欢迎信息
|
|
207
246
|
console.log('\n' + boxen(
|
|
208
247
|
gradient.rainbow('🎯 红糖云服 应用 CLI 工具 v2.2.0') + '\n' +
|
|
209
248
|
chalk.gray('用于管理多应用项目的开发、构建和部署') + '\n' +
|
|
@@ -217,7 +256,9 @@ console.log('\n' + boxen(
|
|
|
217
256
|
}
|
|
218
257
|
));
|
|
219
258
|
|
|
220
|
-
//
|
|
259
|
+
// ========== 命令行参数处理 ==========
|
|
260
|
+
|
|
261
|
+
// 处理调试模式参数
|
|
221
262
|
if (process.argv.includes('--debug')) {
|
|
222
263
|
setLogLevel(LOG_LEVELS.DEBUG);
|
|
223
264
|
Logger.debug('调试模式已启用');
|
|
@@ -226,7 +267,7 @@ if (process.argv.includes('--debug')) {
|
|
|
226
267
|
// 记录启动日志
|
|
227
268
|
Logger.writeToFile('CLI 工具启动', 'INFO');
|
|
228
269
|
|
|
229
|
-
//
|
|
270
|
+
// 处理清理参数(命令行直接清理,不进入交互界面)
|
|
230
271
|
if (process.argv.includes('--clean')) {
|
|
231
272
|
const cleanIndex = process.argv.indexOf('--clean');
|
|
232
273
|
const cleanType = process.argv[cleanIndex + 1] || 'all';
|
package/src/webpack.config.mjs
CHANGED
|
@@ -60,13 +60,31 @@ export default (env = {}) => {
|
|
|
60
60
|
if (appExposesOptions) {
|
|
61
61
|
mpOptions = JSON.parse(decodeURI(appExposesOptions));
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
// 处理 exposes 路径,确保路径正确解析
|
|
65
|
+
// Module Federation 需要相对于 context 的路径,如果路径不以 ./ 开头,需要添加
|
|
66
|
+
if (mpOptions.exposes) {
|
|
67
|
+
const processedExposes = {};
|
|
68
|
+
for (const [key, value] of Object.entries(mpOptions.exposes)) {
|
|
69
|
+
// 如果路径不是绝对路径且不以 ./ 开头,添加 ./ 前缀
|
|
70
|
+
if (value && typeof value === 'string' && !path.isAbsolute(value) && !value.startsWith('./') && !value.startsWith('../')) {
|
|
71
|
+
processedExposes[key] = `./${value}`;
|
|
72
|
+
} else {
|
|
73
|
+
processedExposes[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
mpOptions.exposes = processedExposes;
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
console.log('\n');
|
|
64
|
-
console.log('=====mpOptions=====');
|
|
80
|
+
console.log('=====mpOptions (after processing)=====');
|
|
65
81
|
console.log('\n');
|
|
66
82
|
console.log('context:', projectRoot);
|
|
67
83
|
console.log('\n');
|
|
68
84
|
console.log('entry:', entry);
|
|
69
85
|
console.log('\n');
|
|
86
|
+
console.log('exposes (processed):', JSON.stringify(mpOptions.exposes, null, 2));
|
|
87
|
+
console.log('\n');
|
|
70
88
|
console.log(JSON.stringify(mpOptions, null, 2));
|
|
71
89
|
console.log('\n');
|
|
72
90
|
console.log('=====mpOptions=====');
|
|
@@ -77,6 +95,8 @@ export default (env = {}) => {
|
|
|
77
95
|
entry,
|
|
78
96
|
resolve: {
|
|
79
97
|
..._options,
|
|
98
|
+
// 确保扩展名解析正确
|
|
99
|
+
extensions: ['.tsx', '.ts', '.jsx', '.js', '.json', ...(_options.extensions || [])],
|
|
80
100
|
},
|
|
81
101
|
output: {
|
|
82
102
|
// 默认输出路径,host模式下会被覆盖
|
|
@@ -1,105 +0,0 @@
|
|
|
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 HtyfModulesPlugin {
|
|
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
|
-
process.exit(1);
|
|
71
|
-
} else {
|
|
72
|
-
callback();
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
if (config.codeSigning) {
|
|
76
|
-
compiler.hooks.emit.tapAsync('WrapCodePlugin', (compilation, callback) => {
|
|
77
|
-
// 遍历所有编译过的资源文件
|
|
78
|
-
for (const filename in compilation.assets) {
|
|
79
|
-
if (compilation.assets.hasOwnProperty(filename)) {
|
|
80
|
-
// 获取文件内容
|
|
81
|
-
let source = compilation.assets[filename].source();
|
|
82
|
-
|
|
83
|
-
// 包裹代码
|
|
84
|
-
const wrappedSource = trimTemplateStrings`
|
|
85
|
-
try {
|
|
86
|
-
${source}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
const that = globalThis || global || self || window;
|
|
89
|
-
that.__htyf_error_callback__ && that.__htyf_error_callback__(error, that)
|
|
90
|
-
console.error(error)
|
|
91
|
-
}
|
|
92
|
-
`;
|
|
93
|
-
|
|
94
|
-
// 更新文件内容
|
|
95
|
-
compilation.assets[filename] = {
|
|
96
|
-
source: () => wrappedSource,
|
|
97
|
-
size: () => wrappedSource.length
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
callback();
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|