@htyf-mp/cli 0.2.0-alpha.2 → 0.2.0-alpha.3

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/src/logger.mjs ADDED
@@ -0,0 +1,72 @@
1
+ import chalk from 'chalk';
2
+ import fse from 'fs-extra';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { CONSTANTS, LOG_LEVELS } from './constants.mjs';
6
+
7
+ // 当前日志级别
8
+ let currentLogLevel = LOG_LEVELS.INFO;
9
+
10
+ /**
11
+ * 设置日志级别
12
+ * @param {number} level - 日志级别
13
+ */
14
+ export function setLogLevel(level) {
15
+ currentLogLevel = level;
16
+ }
17
+
18
+ /**
19
+ * 获取当前日志级别
20
+ * @returns {number} 当前日志级别
21
+ */
22
+ export function getLogLevel() {
23
+ return currentLogLevel;
24
+ }
25
+
26
+ /**
27
+ * 日志工具类
28
+ */
29
+ export class Logger {
30
+ static debug(message, ...args) {
31
+ if (currentLogLevel <= LOG_LEVELS.DEBUG) {
32
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
33
+ }
34
+ }
35
+
36
+ static info(message, ...args) {
37
+ if (currentLogLevel <= LOG_LEVELS.INFO) {
38
+ console.log(chalk.blue(`[INFO] ${message}`), ...args);
39
+ }
40
+ }
41
+
42
+ static warn(message, ...args) {
43
+ if (currentLogLevel <= LOG_LEVELS.WARN) {
44
+ console.log(chalk.yellow(`[WARN] ${message}`), ...args);
45
+ }
46
+ }
47
+
48
+ static error(message, ...args) {
49
+ if (currentLogLevel <= LOG_LEVELS.ERROR) {
50
+ console.log(chalk.red(`[ERROR] ${message}`), ...args);
51
+ }
52
+ }
53
+
54
+ static success(message, ...args) {
55
+ console.log(chalk.green(`[SUCCESS] ${message}`), ...args);
56
+ }
57
+
58
+ static writeToFile(message, level = 'INFO') {
59
+ const logDir = CONSTANTS.LOGS_DIR;
60
+ const logFile = path.join(logDir, `cli-${new Date().toISOString().split('T')[0]}.log`);
61
+
62
+ try {
63
+ fse.ensureDirSync(logDir);
64
+ const timestamp = new Date().toISOString();
65
+ const logEntry = `[${timestamp}] [${level}] ${message}\n`;
66
+ fs.appendFileSync(logFile, logEntry);
67
+ } catch (error) {
68
+ console.error('写入日志文件失败:', error.message);
69
+ }
70
+ }
71
+ }
72
+
@@ -0,0 +1,207 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fse from 'fs-extra';
5
+ import md5 from 'md5';
6
+ import lodash from 'lodash';
7
+ import ora from 'ora';
8
+ import boxen from 'boxen';
9
+ import chalk from 'chalk';
10
+ import gradient from 'gradient-string';
11
+ import { input, confirm } from '@inquirer/prompts';
12
+ import select from '@inquirer/select';
13
+ import { Logger } from './logger.mjs';
14
+ import { CONSTANTS } from './constants.mjs';
15
+ import { ProjectConfig } from './config.mjs';
16
+ import { TemplateProcessor } from './template-processor.mjs';
17
+ import { FileSystemUtils } from './file-system.mjs';
18
+
19
+ /**
20
+ * 项目初始化器类
21
+ */
22
+ export class ProjectInitializer {
23
+ constructor() {
24
+ this.config = new ProjectConfig();
25
+ this.processor = new TemplateProcessor(this.config);
26
+ }
27
+
28
+ async initialize() {
29
+ const spinner = ora('正在初始化项目...').start();
30
+
31
+ try {
32
+ // 1. 获取用户输入
33
+ const userInputs = await this.getUserInputs();
34
+
35
+ // 2. 验证输入
36
+ const validation = this.validateInputs(userInputs);
37
+ if (!validation.isValid) {
38
+ spinner.fail('输入验证失败');
39
+ Logger.error(validation.error);
40
+ return;
41
+ }
42
+
43
+ // 3. 检查目录
44
+ const rootPath = path.join(process.cwd(), userInputs.appName);
45
+ if (fs.existsSync(rootPath)) {
46
+ spinner.fail('目录已存在');
47
+ Logger.error(`目录已存在: ${rootPath}`);
48
+ return;
49
+ }
50
+
51
+ // 4. 创建项目
52
+ await this.createProject(userInputs, rootPath, spinner);
53
+
54
+ // 5. 显示成功信息
55
+ this.showSuccessInfo(userInputs.appName, rootPath);
56
+
57
+ } catch (error) {
58
+ spinner.fail('项目初始化失败');
59
+ Logger.error('初始化失败:', error.message);
60
+ if (error.exitCode === 128) {
61
+ Logger.error('Error: 目录已经存在。');
62
+ } else if (error.code === 'ENOTFOUND') {
63
+ Logger.error('网络连接失败,请检查网络连接或尝试使用其他镜像源。');
64
+ } else if (error.code === 'ECONNREFUSED') {
65
+ Logger.error('连接被拒绝,请检查防火墙设置或网络配置。');
66
+ }
67
+ }
68
+ }
69
+
70
+ async getUserInputs() {
71
+ Logger.info('\n开始收集项目信息...\n');
72
+
73
+ const appName = await input({
74
+ message: '输入应用程序目录名称',
75
+ default: 'my-htyf-mp',
76
+ validate: (input) => {
77
+ if (!this.config.validateAppName(input)) {
78
+ return '请以 my-app-name 格式输入您的应用程序英文目录名称';
79
+ }
80
+ return true;
81
+ }
82
+ });
83
+
84
+ const displayName = await input({
85
+ message: '输入应用程序名称',
86
+ default: '小程序',
87
+ validate: (input) => {
88
+ if (!this.config.validateDisplayName(input)) {
89
+ return '只能中文、字母和数字, 不能包含特殊字符, 限2-4个字符';
90
+ }
91
+ return true;
92
+ }
93
+ });
94
+
95
+ const templateType = await select({
96
+ message: '请选择模板类型?',
97
+ choices: Object.entries(this.config.templates).map(([key, template]) => ({
98
+ name: template.name,
99
+ value: key
100
+ }))
101
+ });
102
+
103
+ Logger.info(`已选择模板: ${this.config.templates[templateType].name}`);
104
+
105
+ const repoType = await select({
106
+ message: '请选择模板镜像?',
107
+ choices: [
108
+ { name: 'GitHub (最新)', value: CONSTANTS.TEMPLATE_REPOS.GITHUB },
109
+ { name: 'Coding (最快)', value: CONSTANTS.TEMPLATE_REPOS.CODING }
110
+ ]
111
+ });
112
+
113
+ return { appName, displayName, templateType, repoType };
114
+ }
115
+
116
+ validateInputs(inputs) {
117
+ const pathValidation = FileSystemUtils.validatePath(inputs.appName);
118
+ if (!pathValidation.isValid) {
119
+ return { isValid: false, error: pathValidation.error };
120
+ }
121
+
122
+ return { isValid: true };
123
+ }
124
+
125
+ async createProject(userInputs, rootPath, spinner) {
126
+ const { appName, displayName, templateType, repoType } = userInputs;
127
+
128
+ // 创建临时目录
129
+ spinner.text = '正在准备临时目录...';
130
+ const tmpdir = path.join(os.tmpdir(), md5('__HTYF__'), appName);
131
+ fse.emptyDirSync(tmpdir);
132
+
133
+ try {
134
+ // 克隆仓库
135
+ spinner.text = '正在克隆模板仓库...';
136
+ await this.processor.cloneRepository(repoType, tmpdir);
137
+
138
+ // 确定应用根路径
139
+ const template = this.config.templates[templateType];
140
+ const appRootPath = path.join(tmpdir, template.tempPath);
141
+
142
+ // 清理不需要的文件
143
+ spinner.text = '正在清理模板文件...';
144
+ this.processor.cleanupUnusedTemplates(tmpdir, templateType);
145
+
146
+ // 处理模板
147
+ spinner.text = '正在处理模板...';
148
+ this.processor.processTemplate(appRootPath, templateType);
149
+
150
+ // 生成配置
151
+ spinner.text = '正在生成项目配置...';
152
+ const appid = this.config.generateAppId();
153
+ const projectConfig = this.config.createProjectConfig(appName, displayName, appid);
154
+
155
+ // 写入配置文件
156
+ const configPath = path.join(appRootPath, 'project.dgz.json');
157
+ fs.writeFileSync(configPath, JSON.stringify(projectConfig, undefined, 2));
158
+
159
+ // 清理临时文件
160
+ spinner.text = '正在清理临时文件...';
161
+ const tempPath = path.join(appRootPath, '__TEMP__');
162
+ if (fs.existsSync(tempPath)) {
163
+ fse.removeSync(tempPath);
164
+ }
165
+
166
+ // 移动到目标目录
167
+ spinner.text = '正在创建项目目录...';
168
+ fse.moveSync(appRootPath, rootPath);
169
+
170
+ // 清理临时目录
171
+ fse.removeSync(tmpdir);
172
+
173
+ spinner.succeed('项目创建完成');
174
+
175
+ } catch (error) {
176
+ // 清理临时目录
177
+ if (fs.existsSync(tmpdir)) {
178
+ fse.removeSync(tmpdir);
179
+ }
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ showSuccessInfo(appName, rootPath) {
185
+ console.log('\n' + boxen(
186
+ gradient.rainbow('🎉 项目创建成功!') + '\n\n' +
187
+ chalk.cyan('项目名称: ') + chalk.white(appName) + '\n' +
188
+ chalk.cyan('项目路径: ') + chalk.white(rootPath) + '\n\n' +
189
+ chalk.yellow('下一步操作:') + '\n' +
190
+ chalk.white(` cd ${appName}`) + '\n' +
191
+ chalk.white(' npm install') + '\n' +
192
+ chalk.white(' npm run dev:weapp') + '\n\n' +
193
+ chalk.blue('常用命令:') + '\n' +
194
+ chalk.white(' npm run build:weapp # 构建微信小程序') + '\n' +
195
+ chalk.white(' npm run build:h5 # 构建H5版本') + '\n' +
196
+ chalk.white(' npm run build:rn # 构建React Native版本') + '\n\n' +
197
+ chalk.gray('💡 提示: 使用 --debug 参数可以查看详细的调试信息'),
198
+ {
199
+ padding: 1,
200
+ margin: 1,
201
+ borderStyle: 'round',
202
+ borderColor: 'green'
203
+ }
204
+ ));
205
+ }
206
+ }
207
+
@@ -16,7 +16,6 @@
16
16
  "@react-native-masked-view/masked-view": "0.3.2",
17
17
  "@react-native-picker/picker": "2.11.1",
18
18
  "@react-native-segmented-control/segmented-control": "2.5.7",
19
- "@sayem314/react-native-keep-awake": "1.3.1",
20
19
  "@shopify/flash-list": "1.8.3",
21
20
  "@shopify/react-native-skia": "2.1.1",
22
21
  "expo": "53.0.20",
@@ -68,7 +67,7 @@
68
67
  "react-native-safe-area-context": "5.5.2",
69
68
  "react-native-screens": "4.13.1",
70
69
  "react-native-share": "12.1.0",
71
- "react-native-svg": "15.12.0",
70
+ "react-native-svg": "15.13.0",
72
71
  "react-native-track-player": "4.1.1",
73
72
  "react-native-vector-icons": "10.2.0",
74
73
  "react-native-video": "6.16.1",
@@ -0,0 +1,137 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import fse from 'fs-extra';
4
+ import { execa } from 'execa';
5
+ import { Logger } from './logger.mjs';
6
+ import { CONSTANTS } from './constants.mjs';
7
+
8
+ /**
9
+ * 模板处理器类
10
+ */
11
+ export class TemplateProcessor {
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+
16
+ async cloneRepository(repoUrl, tmpdir) {
17
+ Logger.info('正在克隆模板仓库...');
18
+ Logger.debug(`仓库地址: ${repoUrl}`);
19
+ Logger.debug(`目标目录: ${tmpdir}`);
20
+
21
+ try {
22
+ await execa('git', ['clone', '--depth', '1', repoUrl, tmpdir], {
23
+ timeout: 60000 // 60秒超时
24
+ });
25
+ Logger.success('模板仓库克隆完成');
26
+ } catch (error) {
27
+ if (error.code === 'ENOTFOUND') {
28
+ throw new Error('网络连接失败,请检查网络连接或尝试使用其他镜像源');
29
+ } else if (error.code === 'ECONNREFUSED') {
30
+ throw new Error('连接被拒绝,请检查防火墙设置或网络配置');
31
+ } else if (error.exitCode === 128) {
32
+ throw new Error('Git仓库访问失败,请检查仓库地址是否正确');
33
+ } else if (error.signal === 'SIGTERM' || error.timedOut) {
34
+ throw new Error('克隆操作超时,请检查网络连接或稍后重试');
35
+ } else {
36
+ throw new Error(`克隆失败: ${error.message}`);
37
+ }
38
+ }
39
+ }
40
+
41
+ cleanupUnusedTemplates(tmpdir, templateType) {
42
+ const pathsToRemove = [
43
+ path.join(tmpdir, 'cli'),
44
+ path.join(tmpdir, '.git'),
45
+ path.join(tmpdir, 'docs'),
46
+ path.join(tmpdir, 'README.md')
47
+ ];
48
+
49
+ // 根据模板类型移除不需要的模板
50
+ if (templateType === CONSTANTS.TEMPLATE_TYPES.TARO || templateType === CONSTANTS.TEMPLATE_TYPES.EXPO) {
51
+ pathsToRemove.push(path.join(tmpdir, 'mini-game-template'));
52
+ } else if (templateType === CONSTANTS.TEMPLATE_TYPES.GAME_COCOS) {
53
+ pathsToRemove.push(path.join(tmpdir, 'mini-apps-template'));
54
+ }
55
+
56
+ pathsToRemove.forEach(pathToRemove => {
57
+ try {
58
+ if (fs.existsSync(pathToRemove)) {
59
+ fse.removeSync(pathToRemove);
60
+ Logger.debug(`已移除: ${pathToRemove}`);
61
+ }
62
+ } catch (err) {
63
+ Logger.warn(`移除失败: ${pathToRemove}`, err.message);
64
+ }
65
+ });
66
+ }
67
+
68
+ processTemplate(appRootPath, templateType) {
69
+ const template = this.config.templates[templateType];
70
+ if (!template) {
71
+ throw new Error(`不支持的模板类型: ${templateType}`);
72
+ }
73
+
74
+ const tempPath = path.join(appRootPath, '__TEMP__');
75
+
76
+ if (templateType === CONSTANTS.TEMPLATE_TYPES.TARO) {
77
+ this.processTaroTemplate(appRootPath, tempPath, template);
78
+ } else if (templateType === CONSTANTS.TEMPLATE_TYPES.EXPO) {
79
+ this.processExpoTemplate(appRootPath, tempPath, template);
80
+ } else if (templateType === CONSTANTS.TEMPLATE_TYPES.GAME_COCOS) {
81
+ // 预留游戏模板处理
82
+ Logger.info('游戏模板处理功能开发中...');
83
+ }
84
+ }
85
+
86
+ processTaroTemplate(appRootPath, tempPath, template) {
87
+ Logger.info('正在处理 Taro 模板...');
88
+
89
+ // 复制模板文件
90
+ Object.entries(template.files).forEach(([key, fileName]) => {
91
+ const srcPath = path.join(tempPath, fileName);
92
+ const destPath = path.join(appRootPath, key === 'src' ? './src' : `./${key === 'app' ? 'App.tsx' : key === 'index' ? 'index.js' : key === 'babel' ? 'babel.config.js' : 'project.dgz.json'}`);
93
+
94
+ if (fs.existsSync(srcPath)) {
95
+ fse.copySync(srcPath, destPath);
96
+ Logger.debug(`已复制: ${fileName} -> ${destPath}`);
97
+ }
98
+ });
99
+
100
+ // 更新 package.json
101
+ const pkgPath = path.join(appRootPath, './package.json');
102
+ if (fs.existsSync(pkgPath)) {
103
+ const pkgInfo = fse.readJSONSync(pkgPath);
104
+ if (template.scripts) {
105
+ pkgInfo.scripts = {
106
+ ...(pkgInfo.scripts || {}),
107
+ ...template.scripts
108
+ };
109
+ }
110
+
111
+ // 移除不需要的脚本
112
+ if (pkgInfo.scripts) {
113
+ delete pkgInfo.scripts.web;
114
+ delete pkgInfo.scripts['web:dev'];
115
+ }
116
+
117
+ fse.writeJSONSync(pkgPath, pkgInfo, { spaces: 2 });
118
+ Logger.info('package.json 已更新');
119
+ }
120
+ }
121
+
122
+ processExpoTemplate(appRootPath, tempPath, template) {
123
+ Logger.info('正在处理 Expo 模板...');
124
+
125
+ // 复制模板文件
126
+ Object.entries(template.files).forEach(([key, fileName]) => {
127
+ const srcPath = path.join(tempPath, fileName);
128
+ const destPath = path.join(appRootPath, key === 'src' ? './src' : `./${key === 'app' ? 'App.tsx' : key === 'index' ? 'index.js' : key === 'babel' ? 'babel.config.js' : 'project.dgz.json'}`);
129
+
130
+ if (fs.existsSync(srcPath)) {
131
+ fse.copySync(srcPath, destPath);
132
+ Logger.debug(`已复制: ${fileName} -> ${destPath}`);
133
+ }
134
+ });
135
+ }
136
+ }
137
+
@@ -0,0 +1,76 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import fse from 'fs-extra';
4
+ import { Logger } from './logger.mjs';
5
+ import { CONSTANTS } from './constants.mjs';
6
+ import { getProjectRoot } from './utils.mjs';
7
+
8
+ const projectPath = getProjectRoot();
9
+
10
+ /**
11
+ * 版本号递增函数
12
+ * @param {string} version - 当前版本号 (格式: x.y.z)
13
+ * @returns {string} - 递增后的版本号
14
+ */
15
+ export function incrementVersion(version) {
16
+ if (!version || !CONSTANTS.VERSION_NAME_REGEX.test(version)) {
17
+ return CONSTANTS.DEFAULT_VERSION_NAME;
18
+ }
19
+
20
+ const parts = version.split('.').map(Number);
21
+ parts[2] = (parts[2] || 0) + 1; // 补丁版本号加1
22
+
23
+ return parts.join('.');
24
+ }
25
+
26
+ /**
27
+ * 更新应用配置
28
+ * @param {object} newConfig - 新配置对象
29
+ * @returns {boolean} 是否成功
30
+ */
31
+ export function updateAppConfig(newConfig) {
32
+ try {
33
+ const configPath = path.join(projectPath, 'app.json');
34
+ const localConfig = fse.readJsonSync(configPath);
35
+ const _newConfig = {
36
+ ...localConfig,
37
+ 'htyf': {
38
+ ...localConfig.htyf,
39
+ ...newConfig
40
+ }
41
+ }
42
+
43
+ fs.writeFileSync(configPath, JSON.stringify(_newConfig, undefined, 2), 'utf-8');
44
+ Logger.success(`配置文件已更新: ${configPath}`);
45
+
46
+ return true;
47
+ } catch (error) {
48
+ Logger.error(`更新配置文件失败: ${error.message}`);
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 获取小程序脚本ID
55
+ * @param {string} appid - 应用ID
56
+ * @param {string} version - 版本号
57
+ * @returns {string} 脚本ID
58
+ */
59
+ export function getMiniAppScriptId(appid, version) {
60
+ return `${appid}_v${version?.replace(/\./gi, '_')}`;
61
+ }
62
+
63
+ /**
64
+ * 打印二维码
65
+ * @param {string} url - 二维码URL
66
+ */
67
+ export async function printQrcode(url) {
68
+ try {
69
+ const QRCode = await import('qrcode');
70
+ let terminalStr = await QRCode.toString(url, { type: 'terminal', small: true });
71
+ console.log(terminalStr);
72
+ } catch (error) {
73
+ Logger.error(`生成二维码失败: ${error.message}`);
74
+ }
75
+ }
76
+
package/src/utils.mjs CHANGED
@@ -1,73 +1,14 @@
1
1
  import path from 'path';
2
- import fs from 'fs';
3
- import { fileURLToPath } from 'url'
2
+ import { fileURLToPath } from 'url';
4
3
 
5
4
  const __filename = fileURLToPath(import.meta.url);
6
5
  const __dirname = path.dirname(__filename);
6
+
7
7
  /**
8
8
  * 获取项目根目录
9
9
  * @returns {string} 项目根目录路径
10
10
  */
11
11
  export function getProjectRoot() {
12
- let currentPath = __dirname;
13
-
14
- // 调试信息
15
- if (process.argv.includes('--debug')) {
16
- console.log('开始查找项目根目录...');
17
- console.log('当前文件路径:', __dirname);
18
- }
19
-
20
- // 如果当前在 node_modules 中,向上查找项目根目录
21
- while (currentPath.includes('node_modules')) {
22
- if (process.argv.includes('--debug')) {
23
- console.log('在 node_modules 中,向上查找:', currentPath);
24
- }
25
- currentPath = path.dirname(currentPath);
26
- }
27
-
28
- // 继续向上查找,直到找到包含 app.json 的目录
29
- while (currentPath !== path.dirname(currentPath)) {
30
- const appJsonPath = path.join(currentPath, 'app.json');
31
- const packageJsonPath = path.join(currentPath, 'package.json');
32
- const pnpmWorkspacePath = path.join(currentPath, 'pnpm-workspace.yaml');
33
-
34
- if (process.argv.includes('--debug')) {
35
- console.log('检查路径:', currentPath);
36
- console.log(' app.json 存在:', fs.existsSync(appJsonPath));
37
- console.log(' package.json 存在:', fs.existsSync(packageJsonPath));
38
- console.log(' pnpm-workspace.yaml 存在:', fs.existsSync(pnpmWorkspacePath));
39
- }
40
-
41
- // 检查是否是项目根目录(包含 app.json、package.json 和 pnpm-workspace.yaml)
42
- if (fs.existsSync(appJsonPath) &&
43
- fs.existsSync(packageJsonPath) &&
44
- fs.existsSync(pnpmWorkspacePath)) {
45
-
46
- // 验证 app.json 内容
47
- try {
48
- const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8'));
49
- if (appJson.htyf) {
50
- if (process.argv.includes('--debug')) {
51
- console.log('找到项目根目录:', currentPath);
52
- console.log('app.json 内容:', JSON.stringify(appJson, null, 2));
53
- }
54
- return currentPath;
55
- }
56
- } catch (error) {
57
- // 忽略 JSON 解析错误
58
- if (process.argv.includes('--debug')) {
59
- console.log('app.json 解析失败:', error.message);
60
- }
61
- }
62
- }
63
-
64
- currentPath = path.dirname(currentPath);
65
- }
66
-
67
- // 如果没找到,尝试使用原来的方法
68
- const fallbackPath = path.resolve(__dirname, '../../../../');
69
- if (process.argv.includes('--debug')) {
70
- console.log('使用备用路径:', fallbackPath);
71
- }
72
- return fallbackPath;
12
+ let currentPath = process.cwd() || __dirname;
13
+ return currentPath;
73
14
  }
@@ -0,0 +1,82 @@
1
+ import lodash from 'lodash';
2
+ import si from 'systeminformation';
3
+ import { Logger } from './logger.mjs';
4
+
5
+ /**
6
+ * 配置验证工具类
7
+ */
8
+ export class ConfigValidator {
9
+ static validateAppConfig(config) {
10
+ const errors = [];
11
+ const warnings = [];
12
+
13
+ // 必需字段检查
14
+ const requiredFields = ['productName', 'versionName', 'versionCode'];
15
+ requiredFields.forEach(field => {
16
+ if (!config[field]) {
17
+ errors.push(`缺少必需字段: ${field}`);
18
+ }
19
+ });
20
+
21
+ // 平台配置检查
22
+ if (config.iosBuild) {
23
+ if (!config.iosBuild.appId) {
24
+ warnings.push('iOS配置缺少appId');
25
+ }
26
+ }
27
+
28
+ if (config.andBuild) {
29
+ if (!config.andBuild.appId) {
30
+ warnings.push('Android配置缺少appId');
31
+ }
32
+ }
33
+
34
+ return { errors, warnings, isValid: errors.length === 0 };
35
+ }
36
+
37
+ static async validateNetworkConfig() {
38
+ try {
39
+ const networkInfo = await si.networkInterfaces();
40
+ const defaultNet = (lodash.isArray(networkInfo) ? networkInfo : [networkInfo])
41
+ .find(i => lodash.get(i, 'default', false));
42
+
43
+ if (!defaultNet || !defaultNet.ip4) {
44
+ return { isValid: false, error: '无法获取网络配置' };
45
+ }
46
+
47
+ return {
48
+ isValid: true,
49
+ host: defaultNet.ip4,
50
+ interface: defaultNet.iface
51
+ };
52
+ } catch (error) {
53
+ Logger.error(`网络配置验证失败: ${error.message}`);
54
+ return { isValid: false, error: error.message };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 验证版本号格式
60
+ * @param {string} versionName - 版本名称
61
+ * @param {string} versionCode - 版本代码
62
+ * @returns {boolean} 是否有效
63
+ */
64
+ static validateVersion(versionName, versionCode) {
65
+ const VERSION_NAME_REGEX = /^\d+(\.\d+){2}$/g;
66
+ const VERSION_CODE_REGEX = /^\d+$/g;
67
+
68
+ const isVersionNameValid = VERSION_NAME_REGEX.test(versionName);
69
+ const isVersionCodeValid = VERSION_CODE_REGEX.test(versionCode);
70
+
71
+ if (!isVersionNameValid) {
72
+ Logger.error(`版本名称格式错误: ${versionName},应为 x.y.z 格式`);
73
+ }
74
+
75
+ if (!isVersionCodeValid) {
76
+ Logger.error(`版本代码格式错误: ${versionCode},应为数字格式`);
77
+ }
78
+
79
+ return isVersionNameValid && isVersionCodeValid;
80
+ }
81
+ }
82
+