@htyf-mp/cli 0.1.3 → 0.2.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +52 -13
- package/src/HtyfImportsPlugin.mjs +391 -0
- package/src/ListImportsPlugin.mjs +105 -0
- package/src/babel.config.js +10 -0
- package/src/build.mjs +169 -0
- package/src/clean.mjs +83 -0
- package/src/command-executor.mjs +133 -0
- package/src/config.mjs +52 -0
- package/src/constants.mjs +54 -0
- package/src/debug.mjs +132 -0
- package/src/export_godot.mjs +236 -0
- package/src/file-system.mjs +114 -0
- package/src/index.mjs +307 -0
- package/src/logger.mjs +72 -0
- package/src/project-initializer.mjs +273 -0
- package/src/shared-output.json +81 -0
- package/src/template-processor.mjs +137 -0
- package/src/utils-functions.mjs +76 -0
- package/src/utils.mjs +37 -0
- package/src/validators.mjs +82 -0
- package/src/webpack.config.mjs +171 -0
- package/cli.js +0 -288
package/src/build.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fse from 'fs-extra';
|
|
3
|
+
import AdmZip from 'adm-zip';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { Logger } from './logger.mjs';
|
|
6
|
+
import { CONSTANTS } from './constants.mjs';
|
|
7
|
+
import { FileSystemUtils } from './file-system.mjs';
|
|
8
|
+
import { CommandExecutor } from './command-executor.mjs';
|
|
9
|
+
import { getMiniAppScriptId } from './utils-functions.mjs';
|
|
10
|
+
import { getProjectRoot, guessGodotProjectDir } from './utils.mjs';
|
|
11
|
+
import { exportGodot, promptGodotOptions } from './export_godot.mjs';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 压缩文件夹为zip文件
|
|
19
|
+
* @param {string} inputPath - 输入路径
|
|
20
|
+
* @param {string} outputPath - 输出路径
|
|
21
|
+
* @returns {Promise<string>} 压缩包路径
|
|
22
|
+
*/
|
|
23
|
+
export async function handleZip(inputPath, outputPath) {
|
|
24
|
+
Logger.info(`开始压缩文件夹: ${inputPath}`);
|
|
25
|
+
Logger.info(`输出文件: ${outputPath}`);
|
|
26
|
+
|
|
27
|
+
if (!(await fse.pathExists(inputPath))) {
|
|
28
|
+
throw new Error(`输入路径不存在: ${inputPath}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const stats = await fse.stat(inputPath);
|
|
32
|
+
if (!stats.isDirectory()) {
|
|
33
|
+
throw new Error(`输入路径不是目录: ${inputPath}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const files = await fse.readdir(inputPath);
|
|
37
|
+
Logger.debug(`目录内容: ${files.length} 个文件/文件夹`);
|
|
38
|
+
|
|
39
|
+
if (files.length === 0) {
|
|
40
|
+
Logger.warn(`警告: 目录为空,将创建空的zip文件`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const zipPath = outputPath;
|
|
44
|
+
const admzip = new AdmZip();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await admzip.addLocalFolderPromise(inputPath, {
|
|
48
|
+
filter: (filename) => {
|
|
49
|
+
if (filename.endsWith('.map')) {
|
|
50
|
+
Logger.debug(`过滤文件: ${filename}`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
Logger.debug(`添加文件: ${filename}`);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await admzip.writeZipPromise(zipPath);
|
|
59
|
+
Logger.success(`压缩完成: ${zipPath}`);
|
|
60
|
+
|
|
61
|
+
const zipStats = await fse.stat(zipPath);
|
|
62
|
+
Logger.info(`ZIP文件大小: ${FileSystemUtils.formatFileSize(zipStats.size)}`);
|
|
63
|
+
|
|
64
|
+
return zipPath;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
Logger.error(`压缩失败: ${error.message}`);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 构建小程序包
|
|
73
|
+
* @param {object} newAppInfo - 应用信息
|
|
74
|
+
* @returns {Promise<string>} 压缩包路径
|
|
75
|
+
*/
|
|
76
|
+
export async function mpBuildShell(newAppInfo, isGodot) {
|
|
77
|
+
Logger.info(`开始构建小程序包...`);
|
|
78
|
+
Logger.info(`应用: ${newAppInfo.name}`);
|
|
79
|
+
Logger.info(`应用ID: ${newAppInfo.appid}`);
|
|
80
|
+
Logger.info(`版本: ${newAppInfo.version}`);
|
|
81
|
+
Logger.info(`平台: ${CONSTANTS.BUNDLE_PLATFORM}`);
|
|
82
|
+
|
|
83
|
+
// 验证必需字段
|
|
84
|
+
if (!newAppInfo.name || !newAppInfo.appid || !newAppInfo.version) {
|
|
85
|
+
throw new Error('缺少必需的应用信息: name, appid, version');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 创建临时目录
|
|
89
|
+
const tempPath = CONSTANTS.TEMP_DIR;
|
|
90
|
+
if (!FileSystemUtils.ensureDirectory(tempPath)) {
|
|
91
|
+
throw new Error('无法创建临时目录');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mpOutputPath = CONSTANTS.MP_OUTPUT_DIR;
|
|
95
|
+
if (!FileSystemUtils.ensureDirectory(mpOutputPath)) {
|
|
96
|
+
throw new Error('无法创建输出目录');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { version, appid } = newAppInfo;
|
|
100
|
+
const scriptName = getMiniAppScriptId(appid, version);
|
|
101
|
+
|
|
102
|
+
const appJson = {
|
|
103
|
+
type: 'app',
|
|
104
|
+
engines: '2.0.0',
|
|
105
|
+
name: newAppInfo.name,
|
|
106
|
+
appid,
|
|
107
|
+
version,
|
|
108
|
+
appUrlConfig: newAppInfo.appUrlConfig || `${newAppInfo.host}/app.json`,
|
|
109
|
+
zipUrl: newAppInfo.zipUrl || `${newAppInfo.host}/dist.dgz`
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
let outputPath = path.join(mpOutputPath, 'dist');
|
|
113
|
+
const distPackagePath = path.join(mpOutputPath, 'dist.dgz');
|
|
114
|
+
const rootIndexPath = path.join(tempPath, '..', 'index.js');
|
|
115
|
+
|
|
116
|
+
const mpOptions = {
|
|
117
|
+
name: scriptName,
|
|
118
|
+
filename: `${scriptName}.bundle`,
|
|
119
|
+
exposes: {
|
|
120
|
+
'App': newAppInfo.entry,
|
|
121
|
+
},
|
|
122
|
+
outputPath: path.join(mpOutputPath, 'build'),
|
|
123
|
+
extraChunksPath: outputPath,
|
|
124
|
+
manifest: path.join(mpOutputPath, 'build'),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const appJsonPath = path.join(mpOutputPath, 'app.json');
|
|
128
|
+
await fse.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
129
|
+
const webpackConfigPath = path.join(__dirname, 'webpack.config.mjs');
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
if (!isGodot) {
|
|
134
|
+
const command = `npx cross-env APP_ROOT_INDEX_PATH=${rootIndexPath} APP_EXPOSES_OPTIONS=${encodeURI(JSON.stringify(mpOptions))} webpack --config ${webpackConfigPath}`;
|
|
135
|
+
await CommandExecutor.execute(command, {
|
|
136
|
+
description: '构建小程序包',
|
|
137
|
+
timeout: 120000,
|
|
138
|
+
showCommand: false
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
Logger.info('检测到 Godot 项目,开始执行 Godot 导出流程...');
|
|
142
|
+
const defaultPlatform = ['android', 'ios'].includes(CONSTANTS.BUNDLE_PLATFORM)
|
|
143
|
+
? CONSTANTS.BUNDLE_PLATFORM
|
|
144
|
+
: 'ios';
|
|
145
|
+
const godotDefaults = {
|
|
146
|
+
targetBaseDir: outputPath,
|
|
147
|
+
projectDir: guessGodotProjectDir(PROJECT_ROOT),
|
|
148
|
+
name: newAppInfo.appid || 'GodotApp',
|
|
149
|
+
preset: defaultPlatform === 'android' ? 'Android' : 'iOS',
|
|
150
|
+
platform: defaultPlatform
|
|
151
|
+
};
|
|
152
|
+
const godotOptions = await promptGodotOptions(godotDefaults);
|
|
153
|
+
outputPath = path.resolve(godotOptions.targetBaseDir);
|
|
154
|
+
await fse.ensureDir(outputPath);
|
|
155
|
+
await fse.emptyDir(outputPath);
|
|
156
|
+
await exportGodot(godotOptions);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
await fse.ensureDir(outputPath);
|
|
160
|
+
await fse.copy(appJsonPath, path.join(outputPath, 'app.json'));
|
|
161
|
+
const zipPath = await handleZip(outputPath, distPackagePath);
|
|
162
|
+
Logger.success(`压缩包已创建: ${zipPath}`);
|
|
163
|
+
return zipPath;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
Logger.error(`小程序构建失败: ${error.message}`);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
package/src/clean.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { Logger } from './logger.mjs';
|
|
3
|
+
import { CONSTANTS } from './constants.mjs';
|
|
4
|
+
import { FileSystemUtils } from './file-system.mjs';
|
|
5
|
+
import { getProjectRoot } from './utils.mjs';
|
|
6
|
+
|
|
7
|
+
const projectPath = getProjectRoot();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 清理临时文件
|
|
11
|
+
* @param {string} cleanType - 清理类型 ('all', 'build', 'temp', 'logs', 'cache', 或应用名称)
|
|
12
|
+
*/
|
|
13
|
+
export async function cleanShell(cleanType = 'all') {
|
|
14
|
+
Logger.info('开始清理文件...');
|
|
15
|
+
|
|
16
|
+
const pathsToClean = [];
|
|
17
|
+
let totalSizeBefore = 0;
|
|
18
|
+
|
|
19
|
+
if (cleanType === 'build') {
|
|
20
|
+
// 只清理构建输出
|
|
21
|
+
const buildPath = CONSTANTS.MP_OUTPUT_DIR;
|
|
22
|
+
pathsToClean.push(buildPath);
|
|
23
|
+
totalSizeBefore += FileSystemUtils.calculateDirectorySize(buildPath);
|
|
24
|
+
Logger.info('清理构建输出文件...');
|
|
25
|
+
} else if (cleanType === 'temp') {
|
|
26
|
+
// 只清理临时文件
|
|
27
|
+
const tempPath = CONSTANTS.TEMP_DIR;
|
|
28
|
+
pathsToClean.push(tempPath);
|
|
29
|
+
totalSizeBefore += FileSystemUtils.calculateDirectorySize(tempPath);
|
|
30
|
+
Logger.info('清理临时文件...');
|
|
31
|
+
} else if (cleanType === 'logs') {
|
|
32
|
+
// 清理日志文件
|
|
33
|
+
const logsPath = CONSTANTS.LOGS_DIR;
|
|
34
|
+
pathsToClean.push(logsPath);
|
|
35
|
+
totalSizeBefore += FileSystemUtils.calculateDirectorySize(logsPath);
|
|
36
|
+
Logger.info('清理日志文件...');
|
|
37
|
+
} else if (cleanType === 'cache') {
|
|
38
|
+
// 清理缓存文件
|
|
39
|
+
const cachePaths = [
|
|
40
|
+
path.join(projectPath, 'node_modules/.cache'),
|
|
41
|
+
path.join(projectPath, '.cache'),
|
|
42
|
+
path.join(projectPath, 'dist/.cache')
|
|
43
|
+
];
|
|
44
|
+
pathsToClean.push(...cachePaths);
|
|
45
|
+
cachePaths.forEach(cachePath => {
|
|
46
|
+
totalSizeBefore += FileSystemUtils.calculateDirectorySize(cachePath);
|
|
47
|
+
});
|
|
48
|
+
Logger.info('清理缓存文件...');
|
|
49
|
+
} else if (cleanType && cleanType !== 'all') {
|
|
50
|
+
// 清理指定应用的文件
|
|
51
|
+
const appTempPath = path.join(CONSTANTS.TEMP_DIR, cleanType);
|
|
52
|
+
const appBuildPath = path.join(CONSTANTS.MP_OUTPUT_DIR, cleanType);
|
|
53
|
+
pathsToClean.push(appTempPath, appBuildPath);
|
|
54
|
+
totalSizeBefore += FileSystemUtils.calculateDirectorySize(appTempPath) + FileSystemUtils.calculateDirectorySize(appBuildPath);
|
|
55
|
+
Logger.info(`清理应用 ${cleanType} 的文件...`);
|
|
56
|
+
} else {
|
|
57
|
+
// 清理所有文件
|
|
58
|
+
const allPaths = [
|
|
59
|
+
path.join(CONSTANTS.TEMP_DIR),
|
|
60
|
+
path.join(CONSTANTS.MP_OUTPUT_DIR),
|
|
61
|
+
path.join(CONSTANTS.LOGS_DIR),
|
|
62
|
+
path.join(projectPath, 'node_modules/.cache'),
|
|
63
|
+
path.join(projectPath, '.cache'),
|
|
64
|
+
path.join(projectPath, 'dist/.cache')
|
|
65
|
+
];
|
|
66
|
+
pathsToClean.push(...allPaths);
|
|
67
|
+
allPaths.forEach(cleanPath => {
|
|
68
|
+
totalSizeBefore += FileSystemUtils.calculateDirectorySize(cleanPath);
|
|
69
|
+
});
|
|
70
|
+
Logger.info('清理所有临时文件...');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let cleanedCount = 0;
|
|
74
|
+
for (const cleanPath of pathsToClean) {
|
|
75
|
+
if (FileSystemUtils.cleanDirectory(cleanPath)) {
|
|
76
|
+
cleanedCount++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const freedSpace = FileSystemUtils.formatFileSize(totalSizeBefore);
|
|
81
|
+
Logger.success(`清理完成,已清理 ${cleanedCount} 个目录,释放空间: ${freedSpace}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import shell from 'shelljs';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { Logger } from './logger.mjs';
|
|
4
|
+
import { CONSTANTS } from './constants.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 命令执行工具类
|
|
8
|
+
*/
|
|
9
|
+
export class CommandExecutor {
|
|
10
|
+
static async execute(command, options = {}) {
|
|
11
|
+
const {
|
|
12
|
+
silent = false,
|
|
13
|
+
fatal = true,
|
|
14
|
+
timeout = CONSTANTS.DEFAULT_TIMEOUT,
|
|
15
|
+
retryCount = CONSTANTS.MAX_RETRY_COUNT,
|
|
16
|
+
description = '执行命令',
|
|
17
|
+
showCommand = false
|
|
18
|
+
} = options;
|
|
19
|
+
|
|
20
|
+
const displayCommand = showCommand ? command : description;
|
|
21
|
+
const spinner = ora(displayCommand).start();
|
|
22
|
+
|
|
23
|
+
// 信号处理:当收到退出信号时,停止 spinner
|
|
24
|
+
const cleanup = () => {
|
|
25
|
+
spinner.stop();
|
|
26
|
+
};
|
|
27
|
+
process.once('SIGINT', cleanup);
|
|
28
|
+
process.once('SIGTERM', cleanup);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
for (let attempt = 1; attempt <= retryCount; attempt++) {
|
|
32
|
+
try {
|
|
33
|
+
Logger.debug(`执行命令 (尝试 ${attempt}/${retryCount}): ${command}`);
|
|
34
|
+
|
|
35
|
+
const result = await new Promise((resolve, reject) => {
|
|
36
|
+
const timeoutId = setTimeout(() => {
|
|
37
|
+
reject(new Error(`命令执行超时 (${timeout}ms)`));
|
|
38
|
+
}, timeout);
|
|
39
|
+
|
|
40
|
+
shell.exec(command, {
|
|
41
|
+
silent: silent,
|
|
42
|
+
fatal: false,
|
|
43
|
+
async: false
|
|
44
|
+
}, (code, stdout, stderr) => {
|
|
45
|
+
clearTimeout(timeoutId);
|
|
46
|
+
resolve({ code, stdout, stderr });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (result.code === 0) {
|
|
51
|
+
process.removeListener('SIGINT', cleanup);
|
|
52
|
+
process.removeListener('SIGTERM', cleanup);
|
|
53
|
+
spinner.succeed(`${description} 成功`);
|
|
54
|
+
Logger.debug(`命令输出: ${result.stdout}`);
|
|
55
|
+
return result;
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(`命令执行失败,退出码: ${result.code}, 错误: ${result.stderr}`);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// 如果是用户中断,直接退出
|
|
61
|
+
if (error.message && (error.message.includes('SIGINT') || error.message.includes('SIGTERM'))) {
|
|
62
|
+
process.removeListener('SIGINT', cleanup);
|
|
63
|
+
process.removeListener('SIGTERM', cleanup);
|
|
64
|
+
spinner.stop();
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (attempt === retryCount) {
|
|
69
|
+
process.removeListener('SIGINT', cleanup);
|
|
70
|
+
process.removeListener('SIGTERM', cleanup);
|
|
71
|
+
spinner.fail(`${description} 失败 (${retryCount} 次尝试后)`);
|
|
72
|
+
Logger.error(`命令执行失败: ${error.message}`);
|
|
73
|
+
|
|
74
|
+
if (fatal) {
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
} else {
|
|
79
|
+
Logger.warn(`命令执行失败,准备重试 (${attempt}/${retryCount}): ${error.message}`);
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // 递增延迟
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
process.removeListener('SIGINT', cleanup);
|
|
86
|
+
process.removeListener('SIGTERM', cleanup);
|
|
87
|
+
spinner.stop();
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static async executeWithProgress(command, options = {}) {
|
|
93
|
+
const { description = '执行命令', progressCallback } = options;
|
|
94
|
+
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const child = shell.exec(command, {
|
|
97
|
+
silent: false,
|
|
98
|
+
fatal: false,
|
|
99
|
+
async: true
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let output = '';
|
|
103
|
+
|
|
104
|
+
child.stdout?.on('data', (data) => {
|
|
105
|
+
output += data;
|
|
106
|
+
if (progressCallback) {
|
|
107
|
+
progressCallback(data.toString());
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.stderr?.on('data', (data) => {
|
|
112
|
+
output += data;
|
|
113
|
+
Logger.warn(`命令输出: ${data}`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
child.on('close', (code) => {
|
|
117
|
+
if (code === 0) {
|
|
118
|
+
Logger.success(`${description} 完成`);
|
|
119
|
+
resolve({ code, output });
|
|
120
|
+
} else {
|
|
121
|
+
Logger.error(`${description} 失败,退出码: ${code}`);
|
|
122
|
+
reject(new Error(`命令执行失败,退出码: ${code}`));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
child.on('error', (error) => {
|
|
127
|
+
Logger.error(`${description} 出错: ${error.message}`);
|
|
128
|
+
reject(error);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import md5 from 'md5';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { CONSTANTS } from './constants.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 项目初始化配置类
|
|
7
|
+
*/
|
|
8
|
+
export class ProjectConfig {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.validators = {
|
|
11
|
+
appName: /^([a-z]+)(-[a-z0-9]+)*$/,
|
|
12
|
+
displayName: /^[\u4e00-\u9fa5a-zA-Z0-9]{2,4}$/
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
this.templates = {
|
|
16
|
+
[CONSTANTS.TEMPLATE_TYPES.APP_TEMPLATE]: {
|
|
17
|
+
name: 'app-template',
|
|
18
|
+
tempPath: '_apps_temp_'
|
|
19
|
+
},
|
|
20
|
+
[CONSTANTS.TEMPLATE_TYPES.GAME_TEMPLATE]: {
|
|
21
|
+
name: 'game-template',
|
|
22
|
+
tempPath: '_games_temp_'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
validateAppName(name) {
|
|
28
|
+
return this.validators.appName.test(name);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
validateDisplayName(name) {
|
|
32
|
+
return this.validators.displayName.test(name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
generateAppId() {
|
|
36
|
+
return md5(uuidv4({
|
|
37
|
+
random: [0x10, 0x91, 0x56, 0xbe, 0xc4, 0xfb, 0xc1, 0xea, 0x71, 0xb4, 0xef, 0xe1, 0x67, 0x1c, 0x58, 0x36]
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
createProjectConfig(appName, displayName, appid) {
|
|
42
|
+
return {
|
|
43
|
+
type: 'app',
|
|
44
|
+
name: appName,
|
|
45
|
+
projectname: displayName,
|
|
46
|
+
appid: appid,
|
|
47
|
+
appUrlConfig: 'https://xxx.cos.ap-chengdu.myqcloud.com/assets/testMiniApps/htyanimation/app.json',
|
|
48
|
+
zipUrl: 'https://xxxxx.myqcloud.com/assets/testMiniApps/xxxxx/outputs/dist.dgz'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { getProjectRoot } from './utils.mjs';
|
|
3
|
+
|
|
4
|
+
const projectPath = getProjectRoot();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CLI 工具常量定义
|
|
8
|
+
*/
|
|
9
|
+
export const CONSTANTS = {
|
|
10
|
+
SRC_DIR: path.join(projectPath, './src'),
|
|
11
|
+
TEMP_DIR: path.join(projectPath, './.htyf'),
|
|
12
|
+
MP_OUTPUT_DIR: path.join(projectPath, './dist'),
|
|
13
|
+
LOGS_DIR: path.join(projectPath, './.htyf/.logs'),
|
|
14
|
+
DEFAULT_VERSION_NAME: '1.0.0',
|
|
15
|
+
DEFAULT_VERSION_CODE: '1',
|
|
16
|
+
VERSION_NAME_REGEX: /^\d+(\.\d+){2}$/g,
|
|
17
|
+
VERSION_CODE_REGEX: /^\d+$/g,
|
|
18
|
+
WEBPACK_PORT: 8081,
|
|
19
|
+
BUNDLE_ENTRY_FILE: 'index.js',
|
|
20
|
+
BUNDLE_PLATFORM: 'ios',
|
|
21
|
+
MAX_RETRY_COUNT: 3,
|
|
22
|
+
DEFAULT_TIMEOUT: 30000,
|
|
23
|
+
TEMPLATE_REPOS: {
|
|
24
|
+
GITHUB: 'https://github.com/htyf-mp-community/htyf-mp.git',
|
|
25
|
+
CODING: 'https://e.coding.net/dagouzhi/hongtangyun_mobile/htyf-mp.git'
|
|
26
|
+
},
|
|
27
|
+
TEMPLATE_TYPES: {
|
|
28
|
+
APP_TEMPLATE: 'app-template',
|
|
29
|
+
GAME_TEMPLATE: 'game-template',
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 操作类型枚举
|
|
35
|
+
*/
|
|
36
|
+
export const ACTION_TYPES = {
|
|
37
|
+
START: 'start',
|
|
38
|
+
INIT: 'init',
|
|
39
|
+
MP_BUILD: 'mp-build',
|
|
40
|
+
MP_DEBUG: 'mp-debug',
|
|
41
|
+
CLEAN: 'clean',
|
|
42
|
+
QUIT: 'quit'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 日志级别枚举
|
|
47
|
+
*/
|
|
48
|
+
export const LOG_LEVELS = {
|
|
49
|
+
DEBUG: 0,
|
|
50
|
+
INFO: 1,
|
|
51
|
+
WARN: 2,
|
|
52
|
+
ERROR: 3
|
|
53
|
+
};
|
|
54
|
+
|
package/src/debug.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fse from 'fs-extra';
|
|
3
|
+
import superstatic from 'superstatic';
|
|
4
|
+
import portfinder from 'portfinder';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import boxen from 'boxen';
|
|
7
|
+
import gradient from 'gradient-string';
|
|
8
|
+
import { Logger } from './logger.mjs';
|
|
9
|
+
import { ConfigValidator } from './validators.mjs';
|
|
10
|
+
import { mpBuildShell } from './build.mjs';
|
|
11
|
+
import { printQrcode } from './utils-functions.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 小程序真机调试
|
|
15
|
+
* @param {object} newAppInfo - 应用信息
|
|
16
|
+
*/
|
|
17
|
+
export async function mpDebugShell(newAppInfo) {
|
|
18
|
+
try {
|
|
19
|
+
// 验证必需字段
|
|
20
|
+
if (!newAppInfo.entry) {
|
|
21
|
+
throw new Error('缺少必需的应用信息: entry');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const zipPath = await mpBuildShell(newAppInfo);
|
|
25
|
+
const assetsPath = path.dirname(zipPath);
|
|
26
|
+
const appJsonPath = path.join(assetsPath, 'app.json');
|
|
27
|
+
|
|
28
|
+
Logger.info(`资源路径: ${assetsPath}`);
|
|
29
|
+
const applocaljson = fse.readJsonSync(appJsonPath);
|
|
30
|
+
|
|
31
|
+
// 验证网络配置
|
|
32
|
+
const networkValidation = await ConfigValidator.validateNetworkConfig();
|
|
33
|
+
if (!networkValidation.isValid) {
|
|
34
|
+
throw new Error(`网络配置验证失败: ${networkValidation.error}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const host = networkValidation.host;
|
|
38
|
+
const port = await portfinder.getPortPromise();
|
|
39
|
+
|
|
40
|
+
const app = superstatic.server({
|
|
41
|
+
port: port,
|
|
42
|
+
address: host,
|
|
43
|
+
cwd: assetsPath
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const server = app.listen(function () {
|
|
47
|
+
const address = server.address();
|
|
48
|
+
|
|
49
|
+
const debugerAppUrlConfig = `http://${host}:${address.port}/app.json`;
|
|
50
|
+
const debugerZip = `http://${host}:${address.port}/dist.dgz`;
|
|
51
|
+
const args = {
|
|
52
|
+
...applocaljson,
|
|
53
|
+
development: true,
|
|
54
|
+
"name": applocaljson.name,
|
|
55
|
+
"appid": applocaljson.appid,
|
|
56
|
+
"version": applocaljson.version,
|
|
57
|
+
appUrlConfig: debugerAppUrlConfig,
|
|
58
|
+
zipUrl: debugerZip,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
fse.writeJSONSync(appJsonPath, {
|
|
62
|
+
...fse.readJSONSync(appJsonPath),
|
|
63
|
+
appUrlConfig: debugerAppUrlConfig,
|
|
64
|
+
zipUrl: debugerZip,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const qrcodeUrl = `https://share.dagouzhi.com/#/pages/index/index?data=${encodeURIComponent(JSON.stringify(args))}`;
|
|
68
|
+
|
|
69
|
+
// 显示调试信息
|
|
70
|
+
console.log('\n');
|
|
71
|
+
console.log(boxen(
|
|
72
|
+
gradient.rainbow('小程序真机调试') + '\n\n' +
|
|
73
|
+
chalk.cyan('服务器地址: ') + chalk.white(`${host}:${address.port}`) + '\n' +
|
|
74
|
+
chalk.cyan('应用名称: ') + chalk.white(applocaljson.name) + '\n' +
|
|
75
|
+
chalk.cyan('应用ID: ') + chalk.white(applocaljson.appid) + '\n' +
|
|
76
|
+
chalk.cyan('版本: ') + chalk.white(applocaljson.version),
|
|
77
|
+
{
|
|
78
|
+
padding: 1,
|
|
79
|
+
margin: 1,
|
|
80
|
+
borderStyle: 'round',
|
|
81
|
+
borderColor: 'cyan'
|
|
82
|
+
}
|
|
83
|
+
));
|
|
84
|
+
|
|
85
|
+
console.log('\n' + chalk.yellow('调试配置:'));
|
|
86
|
+
console.log(JSON.stringify(args, undefined, 2));
|
|
87
|
+
|
|
88
|
+
console.log('\n' + chalk.green('调试链接:'));
|
|
89
|
+
console.log(qrcodeUrl);
|
|
90
|
+
|
|
91
|
+
console.log('\n' + chalk.blue('二维码:'));
|
|
92
|
+
printQrcode(qrcodeUrl);
|
|
93
|
+
|
|
94
|
+
Logger.info('调试服务器已启动,按 Ctrl+C 停止');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 优雅关闭 - 移除全局处理,添加专用的服务器关闭处理
|
|
98
|
+
const handleShutdown = () => {
|
|
99
|
+
Logger.info('\n正在关闭调试服务器...');
|
|
100
|
+
let isClosed = false;
|
|
101
|
+
|
|
102
|
+
server.close(() => {
|
|
103
|
+
if (!isClosed) {
|
|
104
|
+
isClosed = true;
|
|
105
|
+
Logger.success('调试服务器已关闭');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 如果服务器在 3 秒内没有关闭,强制退出
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
if (!isClosed) {
|
|
113
|
+
isClosed = true;
|
|
114
|
+
Logger.warn('强制退出...');
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
}, 3000);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// 移除全局的信号处理,添加专用的服务器关闭处理
|
|
121
|
+
// 这样可以确保服务器能够正确关闭
|
|
122
|
+
process.removeAllListeners('SIGINT');
|
|
123
|
+
process.removeAllListeners('SIGTERM');
|
|
124
|
+
process.once('SIGINT', handleShutdown);
|
|
125
|
+
process.once('SIGTERM', handleShutdown);
|
|
126
|
+
|
|
127
|
+
} catch (error) {
|
|
128
|
+
Logger.error(`小程序调试失败: ${error.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|