@htyf-mp/cli 0.2.0-alpha.2 → 0.2.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +20 -6
- package/src/HtyfImportsPlugin.mjs +190 -0
- package/src/build.mjs +146 -0
- package/src/clean.mjs +83 -0
- package/src/command-executor.mjs +133 -0
- package/src/config.mjs +52 -0
- package/src/constants.mjs +54 -0
- package/src/debug.mjs +132 -0
- package/src/file-system.mjs +96 -0
- package/src/index.mjs +48 -1084
- package/src/logger.mjs +72 -0
- package/src/project-initializer.mjs +247 -0
- package/src/shared-output.json +1 -2
- package/src/template-processor.mjs +137 -0
- package/src/utils-functions.mjs +76 -0
- package/src/utils.mjs +4 -63
- package/src/validators.mjs +82 -0
- package/src/webpack.config.mjs +16 -4
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@htyf-mp/cli",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.4",
|
|
4
4
|
"description": "红糖云服app小程序模板-cli",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "src/index.mjs",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
7
11
|
"scripts": {
|
|
8
12
|
"start": "node src/index.mjs",
|
|
9
|
-
"test": "echo \"Error: no test specified\" && exit
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 0"
|
|
10
14
|
},
|
|
11
15
|
"keywords": [
|
|
12
16
|
"hongtangyun",
|
|
17
|
+
"htyf",
|
|
13
18
|
"mp",
|
|
19
|
+
"miniprogram",
|
|
14
20
|
"taro",
|
|
15
|
-
"react-native"
|
|
21
|
+
"react-native",
|
|
22
|
+
"expo",
|
|
23
|
+
"cli",
|
|
24
|
+
"template"
|
|
16
25
|
],
|
|
17
26
|
"author": "dgz",
|
|
18
27
|
"license": "MIT",
|
|
19
28
|
"repository": {
|
|
20
29
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/htyf-mp-community/htyf-mp.git"
|
|
30
|
+
"url": "https://github.com/htyf-mp-community/htyf-mp.git",
|
|
31
|
+
"directory": "apps-cli"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0",
|
|
35
|
+
"npm": ">=9.0.0"
|
|
22
36
|
},
|
|
23
37
|
"dependencies": {
|
|
24
38
|
"@callstack/repack": "^5.1.3",
|
|
@@ -38,8 +52,8 @@
|
|
|
38
52
|
"ora": "^8.2.0",
|
|
39
53
|
"portfinder": "^1.0.37",
|
|
40
54
|
"qrcode": "^1.5.4",
|
|
41
|
-
"react-native-reanimated": "4.0.
|
|
42
|
-
"react-native-worklets": "^0.
|
|
55
|
+
"react-native-reanimated": "4.0.2",
|
|
56
|
+
"react-native-worklets": "^0.6.1",
|
|
43
57
|
"shelljs": "^0.10.0",
|
|
44
58
|
"superstatic": "^9.2.0",
|
|
45
59
|
"systeminformation": "^5.27.7",
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fse from 'fs-extra';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const sharedPkg = fse.readJSONSync(path.join(__dirname, '../shared-output.json'));
|
|
7
|
+
|
|
8
|
+
// 白名单:这些包即使有 native 代码也不需要检查(系统级或已特殊处理)
|
|
9
|
+
const WHITELIST = [];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 从模块路径中提取包名
|
|
13
|
+
* @param {string} moduleContext - 模块上下文路径
|
|
14
|
+
* @returns {string|null} 包名,如果无法提取则返回 null
|
|
15
|
+
*/
|
|
16
|
+
function extractPackageName(moduleContext) {
|
|
17
|
+
if (!moduleContext || !moduleContext.includes('node_modules')) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parts = moduleContext.split('node_modules/');
|
|
22
|
+
if (parts.length < 2) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const packagePath = parts[1];
|
|
27
|
+
const segments = packagePath.split('/').filter(Boolean);
|
|
28
|
+
|
|
29
|
+
if (segments.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 处理 scoped packages (@scope/package)
|
|
34
|
+
if (segments[0].startsWith('@') && segments.length >= 2) {
|
|
35
|
+
return `${segments[0]}/${segments[1]}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return segments[0];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 检查包是否包含 native 代码
|
|
43
|
+
* @param {string} packageName - 包名
|
|
44
|
+
* @returns {boolean} 是否包含 native 代码
|
|
45
|
+
*/
|
|
46
|
+
function hasNativeCode(packageName) {
|
|
47
|
+
const nodeModulesPath = path.join(__dirname, '../node_modules');
|
|
48
|
+
const iosPath = path.join(nodeModulesPath, packageName, 'ios');
|
|
49
|
+
const androidPath = path.join(nodeModulesPath, packageName, 'android');
|
|
50
|
+
|
|
51
|
+
return fse.pathExistsSync(iosPath) || fse.pathExistsSync(androidPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 检查包是否在共享配置中
|
|
56
|
+
* @param {string} packageName - 包名
|
|
57
|
+
* @returns {boolean} 是否在共享配置中
|
|
58
|
+
*/
|
|
59
|
+
function isInSharedConfig(packageName) {
|
|
60
|
+
return sharedPkg && sharedPkg.hasOwnProperty(packageName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 检查包是否在白名单中
|
|
65
|
+
* @param {string} packageName - 包名
|
|
66
|
+
* @returns {boolean} 是否在白名单中
|
|
67
|
+
*/
|
|
68
|
+
function isWhitelisted(packageName) {
|
|
69
|
+
return WHITELIST.includes(packageName);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 格式化错误消息
|
|
74
|
+
* @param {string[]} unsupportedPackages - 不支持的包列表
|
|
75
|
+
* @returns {string} 格式化的错误消息
|
|
76
|
+
*/
|
|
77
|
+
function formatErrorMessage(unsupportedPackages) {
|
|
78
|
+
const lines = [
|
|
79
|
+
'',
|
|
80
|
+
'═══════════════════════════════════════',
|
|
81
|
+
' 红糖云服暂不支持以下第三方 native 组件',
|
|
82
|
+
'═══════════════════════════════════════',
|
|
83
|
+
'',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
unsupportedPackages.forEach((pkg, index) => {
|
|
87
|
+
lines.push(` ${index + 1}. ${pkg}`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('═══════════════════════════════════════');
|
|
92
|
+
lines.push('');
|
|
93
|
+
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function trimTemplateStrings(strings, ...values) {
|
|
98
|
+
let fullString = strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '');
|
|
99
|
+
return fullString.replace(/^[ \t]+/gm, '');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class HtyfModulesPlugin {
|
|
103
|
+
constructor(appConfig = {}) {
|
|
104
|
+
this.config = appConfig;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
apply(compiler) {
|
|
108
|
+
const config = this.config;
|
|
109
|
+
|
|
110
|
+
// 分析第三方 native 模块
|
|
111
|
+
compiler.hooks.emit.tapAsync('AnalyzeThirdPartyModulesPlugin', (compilation, callback) => {
|
|
112
|
+
try {
|
|
113
|
+
const nativeModules = new Set();
|
|
114
|
+
|
|
115
|
+
// 收集所有包含 native 代码的第三方模块
|
|
116
|
+
compilation.modules.forEach(module => {
|
|
117
|
+
if (!module.context) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const packageName = extractPackageName(module.context);
|
|
122
|
+
if (!packageName) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (hasNativeCode(packageName)) {
|
|
127
|
+
nativeModules.add(packageName);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 检查不支持的包
|
|
132
|
+
const unsupportedPackages = Array.from(nativeModules).filter(pkg => {
|
|
133
|
+
// 跳过白名单中的包
|
|
134
|
+
if (isWhitelisted(pkg)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 如果不在共享配置中,则不支持
|
|
139
|
+
return !isInSharedConfig(pkg);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 如果有不支持的包,抛出错误
|
|
143
|
+
if (unsupportedPackages.length > 0) {
|
|
144
|
+
const errorMessage = formatErrorMessage(unsupportedPackages);
|
|
145
|
+
console.error(errorMessage);
|
|
146
|
+
|
|
147
|
+
const error = new Error(
|
|
148
|
+
`发现 ${unsupportedPackages.length} 个不支持的第三方 native 组件。\n${errorMessage}`
|
|
149
|
+
);
|
|
150
|
+
error.unsupportedPackages = unsupportedPackages;
|
|
151
|
+
callback(error);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
callback();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
callback(error);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 代码签名包装(如果启用)
|
|
162
|
+
if (config.codeSigning) {
|
|
163
|
+
compiler.hooks.emit.tapAsync('WrapCodePlugin', (compilation, callback) => {
|
|
164
|
+
try {
|
|
165
|
+
// 遍历所有编译过的资源文件
|
|
166
|
+
for (const filename in compilation.assets) {
|
|
167
|
+
if (compilation.assets.hasOwnProperty(filename)) {
|
|
168
|
+
// 获取文件内容
|
|
169
|
+
const source = compilation.assets[filename].source();
|
|
170
|
+
|
|
171
|
+
// 包裹代码
|
|
172
|
+
const wrappedSource = trimTemplateStrings`
|
|
173
|
+
${source}
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
// 更新文件内容
|
|
177
|
+
compilation.assets[filename] = {
|
|
178
|
+
source: () => wrappedSource,
|
|
179
|
+
size: () => wrappedSource.length,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
callback();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
callback(error);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/build.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import fse from 'fs-extra';
|
|
4
|
+
import AdmZip from 'adm-zip';
|
|
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 { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 压缩文件夹为zip文件
|
|
17
|
+
* @param {string} inputPath - 输入路径
|
|
18
|
+
* @param {string} outputPath - 输出路径
|
|
19
|
+
* @returns {Promise<string>} 压缩包路径
|
|
20
|
+
*/
|
|
21
|
+
export async function handleZip(inputPath, outputPath) {
|
|
22
|
+
Logger.info(`开始压缩文件夹: ${inputPath}`);
|
|
23
|
+
Logger.info(`输出文件: ${outputPath}`);
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(inputPath)) {
|
|
26
|
+
throw new Error(`输入路径不存在: ${inputPath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const stats = fs.statSync(inputPath);
|
|
30
|
+
if (!stats.isDirectory()) {
|
|
31
|
+
throw new Error(`输入路径不是目录: ${inputPath}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const files = fs.readdirSync(inputPath);
|
|
35
|
+
Logger.debug(`目录内容: ${files.length} 个文件/文件夹`);
|
|
36
|
+
|
|
37
|
+
if (files.length === 0) {
|
|
38
|
+
Logger.warn(`警告: 目录为空,将创建空的zip文件`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const zipPath = outputPath;
|
|
42
|
+
const admzip = new AdmZip();
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await admzip.addLocalFolderPromise(inputPath, {
|
|
46
|
+
filter: (filename) => {
|
|
47
|
+
if (filename.endsWith('.map')) {
|
|
48
|
+
Logger.debug(`过滤文件: ${filename}`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
Logger.debug(`添加文件: ${filename}`);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await admzip.writeZipPromise(zipPath);
|
|
57
|
+
Logger.success(`压缩完成: ${zipPath}`);
|
|
58
|
+
|
|
59
|
+
const zipStats = fs.statSync(zipPath);
|
|
60
|
+
Logger.info(`ZIP文件大小: ${FileSystemUtils.formatFileSize(zipStats.size)}`);
|
|
61
|
+
|
|
62
|
+
return zipPath;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
Logger.error(`压缩失败: ${error.message}`);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 构建小程序包
|
|
71
|
+
* @param {object} newAppInfo - 应用信息
|
|
72
|
+
* @returns {Promise<string>} 压缩包路径
|
|
73
|
+
*/
|
|
74
|
+
export async function mpBuildShell(newAppInfo) {
|
|
75
|
+
Logger.info(`开始构建小程序包...`);
|
|
76
|
+
Logger.info(`应用: ${newAppInfo.name}`);
|
|
77
|
+
Logger.info(`应用ID: ${newAppInfo.appid}`);
|
|
78
|
+
Logger.info(`版本: ${newAppInfo.version}`);
|
|
79
|
+
Logger.info(`平台: ${CONSTANTS.BUNDLE_PLATFORM}`);
|
|
80
|
+
|
|
81
|
+
// 验证必需字段
|
|
82
|
+
if (!newAppInfo.name || !newAppInfo.appid || !newAppInfo.version) {
|
|
83
|
+
throw new Error('缺少必需的应用信息: name, appid, version');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 创建临时目录
|
|
87
|
+
const tempPath = CONSTANTS.TEMP_DIR;
|
|
88
|
+
if (!FileSystemUtils.ensureDirectory(tempPath)) {
|
|
89
|
+
throw new Error('无法创建临时目录');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const mpOutputPath = CONSTANTS.MP_OUTPUT_DIR;
|
|
93
|
+
if (!FileSystemUtils.ensureDirectory(mpOutputPath)) {
|
|
94
|
+
throw new Error('无法创建输出目录');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const version = newAppInfo.version;
|
|
98
|
+
const appid = newAppInfo.appid;
|
|
99
|
+
const scriptName = getMiniAppScriptId(appid, version);
|
|
100
|
+
|
|
101
|
+
const appJson = {
|
|
102
|
+
type: 'app',
|
|
103
|
+
engines: '2.0.0',
|
|
104
|
+
name: newAppInfo.name,
|
|
105
|
+
appid: appid,
|
|
106
|
+
version: version,
|
|
107
|
+
appUrlConfig: newAppInfo.appUrlConfig || `${newAppInfo.host}/app.json`,
|
|
108
|
+
zipUrl: newAppInfo.zipUrl || `${newAppInfo.host}/dist.dgz`
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const outputPath = path.join(mpOutputPath, './dist');
|
|
112
|
+
const rootIndexPath = path.join(tempPath, '../index.js');
|
|
113
|
+
|
|
114
|
+
const mpOptions = {
|
|
115
|
+
name: scriptName,
|
|
116
|
+
filename: `${scriptName}.bundle`,
|
|
117
|
+
exposes: {
|
|
118
|
+
'App': newAppInfo.entry,
|
|
119
|
+
},
|
|
120
|
+
outputPath: path.join(mpOutputPath, './build'),
|
|
121
|
+
extraChunksPath: outputPath,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const appJsonPath = path.join(mpOutputPath, 'app.json');
|
|
125
|
+
fs.writeFileSync(appJsonPath, JSON.stringify(appJson, undefined, 2));
|
|
126
|
+
const webpackConfigPath = path.join(__dirname, 'webpack.config.mjs');
|
|
127
|
+
|
|
128
|
+
const command = `npx cross-env APP_ROOT_INDEX_PATH=${rootIndexPath} APP_EXPOSES_OPTIONS=${encodeURI(JSON.stringify(mpOptions))} webpack --config ${webpackConfigPath}`;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await CommandExecutor.execute(command, {
|
|
132
|
+
description: '构建小程序包',
|
|
133
|
+
timeout: 120000,
|
|
134
|
+
showCommand: false
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
fs.copyFileSync(appJsonPath, path.join(outputPath, `./app.json`));
|
|
138
|
+
const zipPath = await handleZip(outputPath, path.join(outputPath, `../dist.dgz`));
|
|
139
|
+
Logger.success(`压缩包已创建: ${zipPath}`);
|
|
140
|
+
return zipPath;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
Logger.error(`小程序构建失败: ${error.message}`);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
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
|
+
|