@htyf-mp/cli 0.2.19 → 0.2.20
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 +104 -11
- package/package.json +1 -1
- package/src/build.mjs +116 -5
- package/src/debug.mjs +69 -2
package/README.md
CHANGED
|
@@ -1,21 +1,114 @@
|
|
|
1
|
-
|
|
1
|
+
## 红糖云服小程序模板 CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
用于创建和管理 **红糖云服 App 小程序** 项目(应用 / 游戏 / Web / 插件),包含:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- 初始化小程序项目模版
|
|
6
|
+
- 构建小程序资源包(打包)
|
|
7
|
+
- 启动真机调试服务
|
|
8
|
+
- 清理构建产物与临时文件
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
- 官网:[`https://mp.dagouzhi.com/`](https://mp.dagouzhi.com/)
|
|
11
|
+
- GitHub:[`https://github.com/htyf-mp-community`](https://github.com/htyf-mp-community)
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## 安装与快速开始
|
|
14
|
+
|
|
15
|
+
建议直接使用 npx:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
10
18
|
npx @htyf-mp/cli
|
|
11
19
|
```
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
按照交互提示完成:
|
|
22
|
+
|
|
23
|
+
1. **输入应用目录名**(例如 `my-htyf-mp`,用于创建项目文件夹)。
|
|
24
|
+
2. **输入应用显示名称**(2–4 个中文/字母/数字)。
|
|
25
|
+
3. **选择模板类型**:
|
|
26
|
+
- `app-template`:React Native 应用小程序(对应 `type: 'app'`)。
|
|
27
|
+
- `game-template`:Godot 游戏小程序(对应 `type: 'game'`)。
|
|
28
|
+
- `web-template`:Web/H5 小程序(对应 `type: 'web'`)。
|
|
29
|
+
4. 选择模板仓库镜像(当前默认使用 GitHub)。
|
|
30
|
+
|
|
31
|
+
CLI 会自动:
|
|
32
|
+
|
|
33
|
+
- 克隆官方模版仓库到临时目录。
|
|
34
|
+
- 清理无关模版,仅保留所选模板。
|
|
35
|
+
- 生成 `app.json` 中的 `htyf` 配置(包含 `appid`、`name`、`zipUrl`、`appUrlConfig` 等)。
|
|
36
|
+
- 将模板拷贝到你指定的项目目录中。
|
|
37
|
+
|
|
38
|
+
> 各模板的具体用法,请参考生成项目中 `_apps_temp_` / `_game_temp_` / `_web_temp_` 目录下的 `README.md`。
|
|
39
|
+
|
|
40
|
+
## 项目操作命令
|
|
41
|
+
|
|
42
|
+
进入生成好的项目根目录后,直接运行 CLI(支持 `node` 或已全局安装的命令):
|
|
43
|
+
|
|
44
|
+
### 交互式主菜单
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node src/index.mjs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
在交互菜单中可以选择:
|
|
51
|
+
|
|
52
|
+
- 🆕 初始化新小程序项目(`init`)
|
|
53
|
+
- 🔍 小程序 - 打包小程序(`mp-build`)
|
|
54
|
+
- 📦 小程序 - 真机调试(`mp-debug`)
|
|
55
|
+
- 🧹 清理模式 - 清理临时文件(`clean`)
|
|
56
|
+
- 👋 退出
|
|
57
|
+
|
|
58
|
+
### 构建小程序(打包)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
node src/index.mjs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
在菜单中选择 **“小程序 - 打包小程序”**:
|
|
65
|
+
|
|
66
|
+
1. CLI 会读取项目根目录下 `app.json` 中的 `htyf` 配置。
|
|
67
|
+
2. 自动检测是否为 Godot 项目(是否存在 `project.godot`):
|
|
68
|
+
- 存在:按 **Godot 游戏** 流程打包。
|
|
69
|
+
- 不存在:按 **普通小程序(React Native / Web)** 流程打包。
|
|
70
|
+
3. 提示输入版本号(默认在当前版本基础上自动 +1,例如 `1.0.0 -> 1.0.1`)。
|
|
71
|
+
4. 更新 `app.json` 中的版本号并执行构建,将资源输出到 `dist` 目录。
|
|
72
|
+
|
|
73
|
+
构建完成后,可以将产物上传到 `zipUrl` 指定的地址,供红糖云服 App 拉取与更新。
|
|
74
|
+
|
|
75
|
+
### 真机调试
|
|
76
|
+
|
|
77
|
+
在主菜单中选择 **“小程序 - 真机调试”**:
|
|
78
|
+
|
|
79
|
+
- CLI 会启动调试服务器,更新 `app.json` 中的调试地址。
|
|
80
|
+
- 在红糖云服 App 中配置好对应的调试入口,即可在真机上实时预览与调试。
|
|
81
|
+
|
|
82
|
+
> 具体调试行为(如 QR 码、连接方式)以红糖云服 App 当前版本为准。
|
|
14
83
|
|
|
15
|
-
###
|
|
84
|
+
### 清理命令
|
|
16
85
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
|
|
86
|
+
支持通过参数直接清理,无需进入交互界面:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
node src/index.mjs --clean all # 清理所有临时文件
|
|
90
|
+
node src/index.mjs --clean build # 清理构建输出
|
|
91
|
+
node src/index.mjs --clean temp # 清理临时目录
|
|
92
|
+
node src/index.mjs --clean logs # 清理日志文件
|
|
93
|
+
node src/index.mjs --clean cache # 清理缓存文件
|
|
21
94
|
```
|
|
95
|
+
|
|
96
|
+
也可以在交互菜单中选择 **“清理模式 - 清理临时文件”**,按提示选择清理范围。
|
|
97
|
+
|
|
98
|
+
## app.json 与 htyf 配置
|
|
99
|
+
|
|
100
|
+
项目根目录下会有一个 `app.json`,其中包含 `htyf` 字段,CLI 会从这里读取并更新应用配置:
|
|
101
|
+
|
|
102
|
+
- 重要字段包括:
|
|
103
|
+
- `type`: `'app' | 'game' | 'web' | 'plugin'`
|
|
104
|
+
- `appid`: 小程序唯一 ID
|
|
105
|
+
- `name`: 应用名称
|
|
106
|
+
- `zipUrl`: 构建产物 zip 包地址
|
|
107
|
+
- `appUrlConfig`: 线上配置地址
|
|
108
|
+
- `version`: 版本号(由 CLI 帮你自动递增或手动修改)
|
|
109
|
+
|
|
110
|
+
> 不同模板下的 `app.json` 字段说明和推荐配置,可在各自模板目录的 README 中查看:
|
|
111
|
+
> - React Native 应用小程序:`_apps_temp_/README.md`
|
|
112
|
+
> - Godot 游戏小程序:`_game_temp_/README.md`
|
|
113
|
+
> - Web/H5 小程序:`_web_temp_/RADME.md`
|
|
114
|
+
|
package/package.json
CHANGED
package/src/build.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import fse from 'fs-extra';
|
|
10
10
|
import AdmZip from 'adm-zip';
|
|
11
|
+
import QRCode from 'qrcode';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
import { Logger } from './logger.mjs';
|
|
13
14
|
import { CONSTANTS } from './constants.mjs';
|
|
@@ -22,6 +23,98 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
22
23
|
const __dirname = path.dirname(__filename);
|
|
23
24
|
const PROJECT_ROOT = getProjectRoot();
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 复制目录内容(不包含最外层目录名)
|
|
28
|
+
* @param {string} srcDir
|
|
29
|
+
* @param {string} destDir
|
|
30
|
+
*/
|
|
31
|
+
async function copyDirContents(srcDir, destDir) {
|
|
32
|
+
await fse.ensureDir(destDir);
|
|
33
|
+
const entries = await fse.readdir(srcDir);
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
await fse.copy(path.join(srcDir, entry), path.join(destDir, entry), { overwrite: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 执行 Web 端 H5 构建
|
|
41
|
+
* @param {'build'|'dev'} mode
|
|
42
|
+
*/
|
|
43
|
+
async function runWebH5Build(mode = 'build') {
|
|
44
|
+
const cmd = mode === 'dev' ? 'npm run dev:h5' : 'npm run build:h5';
|
|
45
|
+
Logger.info(`开始执行 Web H5 构建: ${cmd}`);
|
|
46
|
+
await CommandExecutor.execute(cmd, {
|
|
47
|
+
description: `Web H5 ${mode === 'dev' ? '开发' : '构建'}`,
|
|
48
|
+
timeout: mode === 'dev' ? 120000 : 300000,
|
|
49
|
+
showCommand: false,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 将 taro build:h5 的输出同步到最终 zip 目录
|
|
55
|
+
*
|
|
56
|
+
* 说明:
|
|
57
|
+
* - taro 的 outputRoot 在模板里通常是 ./dist
|
|
58
|
+
* - CLI 最终会把内容打包到 ./dist/dist(outputPath)
|
|
59
|
+
* - 因此这里需要把 ./dist 下的 h5 静态产物复制到 ./dist/dist
|
|
60
|
+
*
|
|
61
|
+
* @param {string} projectDistRoot - taro 输出目录(一般为 ./dist)
|
|
62
|
+
* @param {string} zipInputDir - zip 输入目录(一般为 ./dist/dist)
|
|
63
|
+
*/
|
|
64
|
+
async function syncWebH5Output(projectDistRoot, zipInputDir) {
|
|
65
|
+
// 先清空 zip 输入目录,避免残留
|
|
66
|
+
await fse.ensureDir(zipInputDir);
|
|
67
|
+
await fse.emptyDir(zipInputDir);
|
|
68
|
+
|
|
69
|
+
const h5Dir = path.join(projectDistRoot, 'h5');
|
|
70
|
+
if (await fse.pathExists(h5Dir)) {
|
|
71
|
+
Logger.info('检测到 taro H5 输出目录 ./dist/h5,复制其内容...');
|
|
72
|
+
await copyDirContents(h5Dir, zipInputDir);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Logger.info('未检测到 ./dist/h5,尝试复制 ./dist 下的 H5 静态产物...');
|
|
77
|
+
const entries = await fse.readdir(projectDistRoot);
|
|
78
|
+
|
|
79
|
+
const excludeNames = new Set([
|
|
80
|
+
'dist', // 避免递归复制 ./dist/dist
|
|
81
|
+
'build', // RN bundle 输出目录
|
|
82
|
+
'app.json',
|
|
83
|
+
'manifest.json',
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (excludeNames.has(entry)) continue;
|
|
88
|
+
if (entry.endsWith('.dgz')) continue;
|
|
89
|
+
|
|
90
|
+
await fse.copy(path.join(projectDistRoot, entry), path.join(zipInputDir, entry), { overwrite: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 在构建输出目录生成二维码图片
|
|
96
|
+
*
|
|
97
|
+
* 优先使用 zipUrl,若为空则回退到 appUrlConfig。
|
|
98
|
+
*
|
|
99
|
+
* @param {string} outputPath - 构建输出目录
|
|
100
|
+
* @param {object} appJson - 应用配置对象
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
*/
|
|
103
|
+
async function generateBuildQrCode(outputPath, appJson) {
|
|
104
|
+
const qrContent = appJson?.zipUrl || appJson?.appUrlConfig;
|
|
105
|
+
if (!qrContent) {
|
|
106
|
+
Logger.warn('未配置 zipUrl/appUrlConfig,跳过二维码生成');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const qrFilePath = path.join(outputPath, 'qrcode.png');
|
|
111
|
+
await QRCode.toFile(qrFilePath, qrContent, {
|
|
112
|
+
width: 320,
|
|
113
|
+
margin: 2
|
|
114
|
+
});
|
|
115
|
+
Logger.success(`二维码已生成: ${qrFilePath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
25
118
|
/**
|
|
26
119
|
* 压缩文件夹为 ZIP 文件
|
|
27
120
|
*
|
|
@@ -116,13 +209,13 @@ export async function handleZip(inputPath, outputPath) {
|
|
|
116
209
|
* entry: './src/index.js'
|
|
117
210
|
* }, false);
|
|
118
211
|
*/
|
|
119
|
-
export async function mpBuildShell(newAppInfo, isGodot = false) {
|
|
212
|
+
export async function mpBuildShell(newAppInfo, isGodot = false, buildOptions = {}) {
|
|
120
213
|
Logger.info(`开始构建小程序包...`);
|
|
121
214
|
Logger.info(`应用: ${newAppInfo.name}`);
|
|
122
215
|
Logger.info(`应用ID: ${newAppInfo.appid}`);
|
|
123
216
|
Logger.info(`版本: ${newAppInfo.version}`);
|
|
124
217
|
Logger.info(`平台: ${CONSTANTS.BUNDLE_PLATFORM}`);
|
|
125
|
-
Logger.info(`项目类型: ${isGodot ? 'Godot 游戏' : '普通小程序'}`);
|
|
218
|
+
Logger.info(`项目类型: ${isGodot ? 'Godot 游戏' : (newAppInfo?.type === 'web' ? 'Web 应用' : '普通小程序')}`);
|
|
126
219
|
|
|
127
220
|
// ========== 参数验证 ==========
|
|
128
221
|
if (!newAppInfo.name || !newAppInfo.appid || !newAppInfo.version) {
|
|
@@ -148,6 +241,7 @@ export async function mpBuildShell(newAppInfo, isGodot = false) {
|
|
|
148
241
|
|
|
149
242
|
// 构建 app.json 配置
|
|
150
243
|
const appType = newAppInfo.type || (isGodot ? 'game' : 'app');
|
|
244
|
+
const isWeb = appType === 'web';
|
|
151
245
|
const appJson = {
|
|
152
246
|
...newAppInfo,
|
|
153
247
|
rotate: newAppInfo.rotate || 'portrait',
|
|
@@ -177,9 +271,11 @@ export async function mpBuildShell(newAppInfo, isGodot = false) {
|
|
|
177
271
|
};
|
|
178
272
|
Logger.info('mpOptions', mpOptions);
|
|
179
273
|
|
|
180
|
-
// 保存 app.json 到输出目录
|
|
181
274
|
const appJsonPath = path.join(mpOutputPath, 'app.json');
|
|
182
|
-
|
|
275
|
+
// 注意:Web H5 构建会清理 ./dist,因此 app.json 写入要尽量晚一点
|
|
276
|
+
if (!isWeb) {
|
|
277
|
+
await fse.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
278
|
+
}
|
|
183
279
|
const webpackConfigPath = path.join(__dirname, 'webpack.config.mjs');
|
|
184
280
|
|
|
185
281
|
// ========== 清空输出目录 ==========
|
|
@@ -208,7 +304,6 @@ export async function mpBuildShell(newAppInfo, isGodot = false) {
|
|
|
208
304
|
|
|
209
305
|
// ========== 执行构建 ==========
|
|
210
306
|
try {
|
|
211
|
-
const isWeb = appType === 'web';
|
|
212
307
|
if (!isGodot && !isWeb) {
|
|
213
308
|
// ========== 普通小程序构建流程 ==========
|
|
214
309
|
Logger.info('使用 React Native 构建小程序包...');
|
|
@@ -282,6 +377,21 @@ export async function mpBuildShell(newAppInfo, isGodot = false) {
|
|
|
282
377
|
// 更新 app.json 中的 zipUrl
|
|
283
378
|
appJson.zipUrl = newAppInfo.zipUrl || `${newAppInfo.host}/dist.${platformName}.dgz`;
|
|
284
379
|
await fse.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
380
|
+
} else if (isWeb) {
|
|
381
|
+
// ========== Web 应用构建流程 ==========
|
|
382
|
+
// 正式打包走 build:h5;真机调试可通过 buildOptions.skipWebBuild 跳过
|
|
383
|
+
if (!buildOptions.skipWebBuild) {
|
|
384
|
+
await runWebH5Build('build');
|
|
385
|
+
await syncWebH5Output(mpOutputPath, outputPath);
|
|
386
|
+
} else {
|
|
387
|
+
Logger.info('跳过 Web H5 构建(debug 模式),仅打包 app.json');
|
|
388
|
+
// 保证 zipInputDir 存在,后续还要写 app.json/qrcode
|
|
389
|
+
await fse.ensureDir(outputPath);
|
|
390
|
+
await fse.emptyDir(outputPath);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Web 构建后再写 app.json,避免被 taro 清理掉
|
|
394
|
+
await fse.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
285
395
|
}
|
|
286
396
|
|
|
287
397
|
// ========== 打包最终产物 ==========
|
|
@@ -291,6 +401,7 @@ export async function mpBuildShell(newAppInfo, isGodot = false) {
|
|
|
291
401
|
if (!isGodot && appType !== 'web') {
|
|
292
402
|
fse.copyFileSync(path.join(outputPath, '../manifest.json'), path.join(outputPath, 'manifest.json'));
|
|
293
403
|
}
|
|
404
|
+
await generateBuildQrCode(outputPath, appJson);
|
|
294
405
|
|
|
295
406
|
// 压缩输出目录为最终包
|
|
296
407
|
const zipPath = await handleZip(outputPath, distPackagePath);
|
package/src/debug.mjs
CHANGED
|
@@ -13,9 +13,38 @@ import chalk from 'chalk';
|
|
|
13
13
|
import boxen from 'boxen';
|
|
14
14
|
import gradient from 'gradient-string';
|
|
15
15
|
import { Logger } from './logger.mjs';
|
|
16
|
+
import { CONSTANTS } from './constants.mjs';
|
|
16
17
|
import { ConfigValidator } from './validators.mjs';
|
|
17
18
|
import { mpBuildShell } from './build.mjs';
|
|
18
19
|
import { printQrcode } from './utils-functions.mjs';
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 等待 Web H5 首次编译产物就绪,避免 taro watch 后续清理 dist
|
|
24
|
+
* 导致最终缺少 dist/app.json 与 dist/dist.dgz。
|
|
25
|
+
*
|
|
26
|
+
* @param {{timeoutMs?: number, intervalMs?: number}} [options]
|
|
27
|
+
* @returns {Promise<boolean>}
|
|
28
|
+
*/
|
|
29
|
+
async function waitForWebH5Output(options = {}) {
|
|
30
|
+
const { timeoutMs = 120000, intervalMs = 1000 } = options;
|
|
31
|
+
const h5Dir = path.join(CONSTANTS.MP_OUTPUT_DIR, 'h5'); // ./dist/h5
|
|
32
|
+
|
|
33
|
+
const start = Date.now();
|
|
34
|
+
while (Date.now() - start < timeoutMs) {
|
|
35
|
+
if (await fse.pathExists(h5Dir)) {
|
|
36
|
+
try {
|
|
37
|
+
const entries = await fse.readdir(h5Dir);
|
|
38
|
+
if (entries.length > 0) return true;
|
|
39
|
+
} catch (_) {
|
|
40
|
+
// directory may be in flux
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
19
48
|
|
|
20
49
|
/**
|
|
21
50
|
* 小程序真机调试
|
|
@@ -51,10 +80,27 @@ export async function mpDebugShell(newAppInfo, isGodot = false) {
|
|
|
51
80
|
if (!isGodot && !isWeb && !newAppInfo.entry) {
|
|
52
81
|
throw new Error('缺少必需的应用信息: entry');
|
|
53
82
|
}
|
|
54
|
-
|
|
83
|
+
let devH5Process = null;
|
|
84
|
+
if (isWeb) {
|
|
85
|
+
// devH5Process = spawn('npm', ['run', 'dev:h5'], {
|
|
86
|
+
// stdio: 'inherit',
|
|
87
|
+
// shell: true,
|
|
88
|
+
// });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Web 真机调试:等待 taro 首次编译产物就绪
|
|
92
|
+
// 这样后续生成的 dist/app.json 与 dist/dist.dgz 不容易被覆盖/删除。
|
|
93
|
+
if (isWeb) {
|
|
94
|
+
Logger.info('等待 Web H5 首次编译完成...');
|
|
95
|
+
const ok = await waitForWebH5Output();
|
|
96
|
+
if (!ok) {
|
|
97
|
+
Logger.warn('等待 Web H5 产物超时,将继续生成调试用 dist 资源');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
55
101
|
// ========== 构建小程序包 ==========
|
|
56
102
|
Logger.info('开始构建小程序包...');
|
|
57
|
-
const zipPath = await mpBuildShell(newAppInfo, isGodot);
|
|
103
|
+
const zipPath = await mpBuildShell(newAppInfo, isGodot, { skipWebBuild: isWeb });
|
|
58
104
|
const assetsPath = path.dirname(zipPath);
|
|
59
105
|
const appJsonPath = path.join(assetsPath, 'app.json');
|
|
60
106
|
|
|
@@ -71,6 +117,11 @@ export async function mpDebugShell(newAppInfo, isGodot = false) {
|
|
|
71
117
|
const host = networkValidation.host;
|
|
72
118
|
const port = await portfinder.getPortPromise(); // 自动查找可用端口
|
|
73
119
|
|
|
120
|
+
// Web 真机调试:启动 H5 dev server,并把固定地址写入打包后的 app.json 的 webUrl
|
|
121
|
+
// 注意:dev:h5 由 taro 侧负责启动静态服务(这里只启动命令并在 app.json 写死 URL)
|
|
122
|
+
const webDevUrl = process.env.WEB_H5_DEV_URL || 'http://192.168.1.28:10086';
|
|
123
|
+
|
|
124
|
+
|
|
74
125
|
// 创建静态文件服务器
|
|
75
126
|
const app = superstatic.server({
|
|
76
127
|
port: port,
|
|
@@ -96,6 +147,7 @@ export async function mpDebugShell(newAppInfo, isGodot = false) {
|
|
|
96
147
|
"version": applocaljson.version,
|
|
97
148
|
appUrlConfig: debugerAppUrlConfig,
|
|
98
149
|
zipUrl: debugerZip,
|
|
150
|
+
...(isWeb ? { webUrl: webDevUrl } : {}),
|
|
99
151
|
};
|
|
100
152
|
|
|
101
153
|
// 更新 app.json 文件,写入调试 URL
|
|
@@ -103,6 +155,7 @@ export async function mpDebugShell(newAppInfo, isGodot = false) {
|
|
|
103
155
|
...fse.readJSONSync(appJsonPath),
|
|
104
156
|
appUrlConfig: debugerAppUrlConfig,
|
|
105
157
|
zipUrl: debugerZip,
|
|
158
|
+
...(isWeb ? { webUrl: webDevUrl } : {}),
|
|
106
159
|
});
|
|
107
160
|
|
|
108
161
|
// 生成调试二维码 URL
|
|
@@ -145,6 +198,20 @@ export async function mpDebugShell(newAppInfo, isGodot = false) {
|
|
|
145
198
|
const handleShutdown = () => {
|
|
146
199
|
Logger.info('\n正在关闭调试服务器...');
|
|
147
200
|
let isClosed = false;
|
|
201
|
+
|
|
202
|
+
// 优先停止 Web H5 dev server(debug 进程)
|
|
203
|
+
if (devH5Process) {
|
|
204
|
+
try {
|
|
205
|
+
// detached 后它会成为新的进程组 leader,使用负 pid 可杀整个组
|
|
206
|
+
process.kill(-devH5Process.pid, 'SIGTERM');
|
|
207
|
+
} catch (e) {
|
|
208
|
+
try {
|
|
209
|
+
devH5Process.kill('SIGTERM');
|
|
210
|
+
} catch (_) {
|
|
211
|
+
// ignore
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
148
215
|
|
|
149
216
|
// 尝试关闭服务器
|
|
150
217
|
server.close(() => {
|