@htyf-mp/cli 0.1.3 → 0.2.0-alpha.11

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.
@@ -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
+
@@ -0,0 +1,171 @@
1
+ // 导入Node.js和第三方依赖
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import fse from 'fs-extra';
5
+ import webpack from 'webpack';
6
+ import TerserPlugin from 'terser-webpack-plugin';
7
+ import * as Repack from '@callstack/repack';
8
+ import { ExpoModulesPlugin } from '@callstack/repack-plugin-expo-modules';
9
+ import { ReanimatedPlugin } from '@callstack/repack-plugin-reanimated';
10
+ import { HtyfModulesPlugin } from './HtyfImportsPlugin.mjs';
11
+ import { getProjectRoot } from './utils.mjs';
12
+
13
+ // 获取当前文件和目录路径
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ // 微前端暴露配置,通常为JSON字符串
18
+ const appExposesOptions = process.env.APP_EXPOSES_OPTIONS;
19
+ const appRootIndexPath = process.env.APP_ROOT_INDEX_PATH;
20
+
21
+ // 读取依赖共享配置
22
+ const dependencies = fse.readJsonSync(path.join(__dirname, './shared-output.json'));
23
+
24
+ // 记录构建时间戳
25
+ const time = Date.now();
26
+
27
+ const version = process.env.APP_VERSION;
28
+ const appid = process.env.APP_ID;
29
+
30
+ // 构建Module Federation共享依赖对象
31
+ const sharedObj = {};
32
+ for (const key in dependencies) {
33
+ const p = dependencies[key];
34
+ sharedObj[key] = {
35
+ /** 是否运行为单例 */
36
+ singleton: true,
37
+ /** host模式下必须为true,微应用为false */
38
+ eager: true,
39
+ version: p.version,
40
+ };
41
+ }
42
+
43
+ const projectRoot = getProjectRoot();
44
+
45
+ /**
46
+ * Webpack配置导出函数
47
+ * @param {object} env - 构建环境变量
48
+ * @returns {object} - Webpack配置对象
49
+ */
50
+ export default (env = {}) => {
51
+ // 设置构建上下文
52
+ env.context = projectRoot || __dirname;
53
+ const { entry = appRootIndexPath, platform = 'ios' } = env;
54
+ const context = env.context || __dirname;
55
+ // 获取Repack的resolve配置
56
+ const _options = Repack.getResolveOptions();
57
+
58
+ // 解析微前端相关配置
59
+ let mpOptions = {};
60
+ if (appExposesOptions) {
61
+ mpOptions = JSON.parse(decodeURI(appExposesOptions));
62
+ }
63
+ console.log('\n');
64
+ console.log('=====mpOptions=====');
65
+ console.log('\n');
66
+ console.log('context:', projectRoot);
67
+ console.log('\n');
68
+ console.log('entry:', entry);
69
+ console.log('\n');
70
+ console.log(JSON.stringify(mpOptions, null, 2));
71
+ console.log('\n');
72
+ console.log('=====mpOptions=====');
73
+ console.log('\n');
74
+ // 基础Webpack配置
75
+ const mpConfig = {
76
+ context,
77
+ entry,
78
+ resolve: {
79
+ ..._options,
80
+ },
81
+ output: {
82
+ // 默认输出路径,host模式下会被覆盖
83
+ path: mpOptions.outputPath,
84
+ publicPath: mpOptions.outputPath,
85
+ // uniqueName: `htyf_mp_for_${mpOptions.name}`,
86
+ },
87
+ module: {
88
+ rules: [
89
+ // 处理js/ts/tsx文件,使用babel-loader
90
+ {
91
+ test: /\.[cm]?[jt]sx?$/,
92
+ use: {
93
+ loader: 'babel-loader',
94
+ // options: {
95
+ // configFile: path.resolve(__dirname, './babel.config.js'),
96
+ // }
97
+ },
98
+ type: 'javascript/auto',
99
+ },
100
+ // 处理静态资源(图片等),支持inline模式
101
+ ...Repack.getAssetTransformRules({
102
+ inline: !!mpOptions.extraChunksPath,
103
+ svg: 'xml',
104
+ }),
105
+ ],
106
+ },
107
+ optimization: {
108
+ // 压缩配置,去除注释
109
+ minimizer: [
110
+ new TerserPlugin({
111
+ test: /\.(js)?bundle(\?.*)?$/i,
112
+ extractComments: false,
113
+ terserOptions: {
114
+ format: {
115
+ comments: false,
116
+ },
117
+ },
118
+ }),
119
+ ],
120
+ chunkIds: 'named', // 便于调试
121
+ },
122
+ plugins: [
123
+ // 注入全局常量(appid、版本、构建时间)
124
+ new webpack.DefinePlugin({
125
+ __APP_DEFINE_APPID__: `"${appid}"`,
126
+ __APP_DEFINE_VERSION__: `"${version}"`,
127
+ __APP_DEFINE_BUILD_TIME__: `"${time}"`,
128
+ }),
129
+ // Repack主插件,支持extraChunks配置
130
+ new Repack.RepackPlugin({
131
+ platform,
132
+ extraChunks: [
133
+ {
134
+ include: /.*/,
135
+ type: 'remote',
136
+ outputPath: `${mpOptions.extraChunksPath}`,
137
+ },
138
+ ],
139
+ }),
140
+ // Expo模块支持
141
+ new ExpoModulesPlugin(),
142
+ // Reanimated支持
143
+ new ReanimatedPlugin(),
144
+ // Module Federation V2插件,支持host和remote两种模式
145
+ new Repack.plugins.ModuleFederationPluginV2({
146
+ dts: false,
147
+ // 微应用模式
148
+ name: mpOptions.name,
149
+ filename: mpOptions.filename,
150
+ exposes: mpOptions.exposes,
151
+ shared: {
152
+ ...sharedObj,
153
+ react: { singleton: true, eager: true },
154
+ 'react-native': { singleton: true, eager: true },
155
+ },
156
+ shareStrategy: 'loaded-first',
157
+ }),
158
+ new HtyfModulesPlugin({
159
+ codeSigning: true,
160
+ appid: mpOptions.appid,
161
+ version: mpOptions.version,
162
+ manifest: mpOptions.manifest,
163
+ }),
164
+ new webpack.optimize.LimitChunkCountPlugin({
165
+ maxChunks: 2, // 限制最大chunk数,便于远程加载
166
+ })
167
+ ],
168
+ };
169
+
170
+ return mpConfig;
171
+ };
package/cli.js DELETED
@@ -1,288 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import chalk from 'chalk'
4
- import path from 'path'
5
- import fs from 'fs'
6
- import fse from 'fs-extra'
7
- import { Command } from 'commander'
8
- import select from '@inquirer/select'
9
- import { input } from '@inquirer/prompts';
10
- import {execa, execaCommand} from 'execa'
11
- import ora from 'ora'
12
- import childProcess from 'child_process'
13
- import { v4 as uuidv4 } from 'uuid';
14
- import md5 from 'md5'
15
- import os from 'os'
16
-
17
- const v4options = {
18
- random: [
19
- 0x10, 0x91, 0x56, 0xbe, 0xc4, 0xfb, 0xc1, 0xea, 0x71, 0xb4, 0xef, 0xe1, 0x67, 0x1c, 0x58, 0x36,
20
- ],
21
- };
22
-
23
- const log = console.log
24
- const program = new Command()
25
- const green = chalk.green
26
-
27
- const repoUrl = 'https://github.com/htyf-mp-community/htyf-mp.git'
28
- const codingRepoUrl = 'https://e.coding.net/dagouzhi/hongtangyun_mobile/htyf-mp.git'
29
-
30
- const taroTempPath = 'mini-apps-template';
31
- const expoTempPath = 'mini-apps-template';
32
- const gameTempPath = 'mini-game-template';
33
-
34
- const isYarnInstalled = () => {
35
- try {
36
- childProcess.execSync('yarn --version');
37
- return true;
38
- } catch {
39
- return false;
40
- }
41
- }
42
-
43
- const isBunInstalled = () => {
44
- try {
45
- childProcess.execSync('bun --version')
46
- return true;
47
- } catch(err) {
48
- return false;
49
- }
50
- }
51
-
52
- async function main() {
53
- const spinner = ora({
54
- text: '创建代码库'
55
- })
56
- try {
57
- const kebabRegez = /^([a-z]+)(-[a-z0-9]+)*$/
58
-
59
- program
60
- .name('红糖云服APP-小程序')
61
- .description('快速创建红糖云服APP小程序')
62
-
63
- program.parse(process.argv)
64
-
65
- const args = program.args
66
- let appName = args[0]
67
-
68
- if (!appName || !kebabRegez.test(args[0])) {
69
- appName = await input({
70
- message: '输入应用程序目录名称',
71
- default: 'my-htyf-mp',
72
- validate: d => {
73
- if(!kebabRegez.test(d)) {
74
- return '请以my-app-name格式输入您的应用程序英文目录名称'
75
- }
76
- return true
77
- }
78
- })
79
- }
80
-
81
- const displayNameRegez = /^[\u4e00-\u9fa5a-zA-Z0-9]{2,4}$/
82
-
83
- let displayName = ''
84
- if (!displayName) {
85
- displayName = await input({
86
- message: '输入应用程序名称',
87
- default: '小程序',
88
- validate: d => {
89
- if(!displayNameRegez.test(d)) {
90
- return '只能中文、字母和数字, 不能包含特殊字符, 限2-4个字符'
91
- }
92
- return true
93
- }
94
- })
95
- }
96
-
97
- const tempType = await select({
98
- message: '请选择模板类型?',
99
- choices: [
100
- {
101
- name: 'Expo (https://docs.expo.dev/)',
102
- value: 'expo',
103
- },
104
- {
105
- name: 'Taro (https://docs.taro.zone/)',
106
- value: 'taro',
107
- },
108
- {
109
- name: 'Game (https://docs.cocos.com/)',
110
- value: 'game-cocos',
111
- }
112
- ]
113
- })
114
-
115
- const repoType = await select({
116
- message: '请选择模板镜像?',
117
- choices: [
118
- {
119
- name: 'github(最新)',
120
- value: repoUrl,
121
- },
122
- {
123
- name: 'coding(最快)',
124
- value: codingRepoUrl,
125
- },
126
- ]
127
- })
128
-
129
- const appid = md5(uuidv4(v4options));
130
-
131
- let config = {
132
- "type": "app",
133
- "name": appName,
134
- "projectname": displayName,
135
- "appid": appid,
136
- "appUrlConfig": "https://xxx.cos.ap-chengdu.myqcloud.com/assets/testMiniApps/htyanimation/app.json",
137
- "zipUrl": "https://xxxxx.myqcloud.com/assets/testMiniApps/xxxxx/outputs/dist.dgz"
138
- }
139
-
140
- log(`\n初始化项目. \n`)
141
-
142
-
143
- const root_path = path.join(process.cwd(), `${appName}`);
144
- const isExists = fs.existsSync(root_path);
145
- if (isExists) {
146
- console.error('目录已经存在。')
147
- return;
148
- }
149
- if (/[\u4e00-\u9fa5]/gi.test(root_path)) {
150
- console.error('禁止目录中包含中文字符')
151
- return;
152
- }
153
- spinner.start()
154
- const tmpdir = path.join(os.tmpdir(), md5(`__HTYF__`), appName);
155
- fse.emptyDirSync(tmpdir)
156
- await execa('git', ['clone', repoType, tmpdir])
157
- let appRootPath = '';
158
- const taro_path = path.join(tmpdir, `${taroTempPath}`)
159
- const expo_path = path.join(tmpdir, `${expoTempPath}`)
160
- const game_path = path.join(tmpdir, `${gameTempPath}`)
161
- const cli_path = path.join(tmpdir, `cli`)
162
- const git_path = path.join(tmpdir, `.git`)
163
- const docs_path = path.join(tmpdir, `docs`)
164
- const README_path = path.join(tmpdir, `README.md`)
165
- try {
166
- if (tempType === 'taro') {
167
- appRootPath = taro_path
168
- fse.removeSync(game_path)
169
- }
170
- if (tempType === 'expo') {
171
- appRootPath = expo_path
172
- fse.removeSync(game_path)
173
- }
174
- if (tempType === 'game-cocos') {
175
- appRootPath = game_path
176
- fse.removeSync(taro_path)
177
- fse.removeSync(expo_path)
178
- }
179
- fse.removeSync(cli_path)
180
- fse.removeSync(git_path)
181
- fse.removeSync(docs_path)
182
- fse.removeSync(README_path)
183
- } catch (err) {}
184
- const __TEMP_PATH__ = path.join(appRootPath, '__TEMP__');
185
- const pkg_path = path.join(appRootPath, './package.json');
186
- const dgzJosn = path.join(`${appRootPath}`, 'project.dgz.json')
187
- console.log(' ')
188
- console.log(tempType)
189
- console.log(__TEMP_PATH__)
190
- console.log(' ')
191
- if (tempType === 'taro') {
192
- fse.copySync(
193
- path.join(__TEMP_PATH__, 'taro_src'),
194
- path.join(appRootPath, './src')
195
- )
196
- fse.copySync(
197
- path.join(__TEMP_PATH__, 'taro.App.tsx'),
198
- path.join(appRootPath, './App.tsx')
199
- )
200
- fse.copySync(
201
- path.join(__TEMP_PATH__, 'taro.index.js'),
202
- path.join(appRootPath, './index.js')
203
- )
204
- fse.copySync(
205
- path.join(__TEMP_PATH__, 'taro.babel.config.js'),
206
- path.join(appRootPath, './babel.config.js')
207
- )
208
- fse.copySync(
209
- path.join(__TEMP_PATH__, 'taro.project.dgz.json'),
210
- path.join(appRootPath, './project.dgz.json')
211
- )
212
- const pkg_info = fse.readJSONSync(pkg_path)
213
- pkg_info['scripts'] = {
214
- ...(pkg_info['scripts'] || {}),
215
- "build:weapp": "taro build --type weapp",
216
- "build:swan": "taro build --type swan",
217
- "build:alipay": "taro build --type alipay",
218
- "build:tt": "taro build --type tt",
219
- "build:h5": "taro build --type h5",
220
- "build:rn": "taro build --type rn",
221
- "build:qq": "taro build --type qq",
222
- "build:jd": "taro build --type jd",
223
- "build:quickapp": "taro build --type quickapp",
224
- "dev:weapp": "npm run build:weapp -- --watch",
225
- "dev:swan": "npm run build:swan -- --watch",
226
- "dev:alipay": "npm run build:alipay -- --watch",
227
- "dev:tt": "npm run build:tt -- --watch",
228
- "dev:h5": "npm run build:h5 -- --watch",
229
- "dev:rn": "npm run build:rn -- --watch",
230
- "dev:qq": "npm run build:qq -- --watch",
231
- "dev:jd": "npm run build:jd -- --watch",
232
- "dev:quickapp": "npm run build:quickapp -- --watch",
233
- }
234
- delete pkg_info['scripts']['web']
235
- delete pkg_info['scripts']['web:dev']
236
- fse.writeJSONSync(pkg_path, pkg_info, {
237
- spaces: 2
238
- })
239
- }
240
- if (tempType === 'expo') {
241
- fse.copySync(
242
- path.join(__TEMP_PATH__, 'expo_src'),
243
- path.join(appRootPath, './src')
244
- )
245
- fse.copySync(
246
- path.join(__TEMP_PATH__, 'expo.App.tsx'),
247
- path.join(appRootPath, './App.tsx')
248
- )
249
- fse.copySync(
250
- path.join(__TEMP_PATH__, 'expo.index.js'),
251
- path.join(appRootPath, './index.js')
252
- )
253
- fse.copySync(
254
- path.join(__TEMP_PATH__, 'expo.babel.config.js'),
255
- path.join(appRootPath, './babel.config.js')
256
- )
257
- fse.copySync(
258
- path.join(__TEMP_PATH__, 'expo.project.dgz.json'),
259
- path.join(appRootPath, './project.dgz.json')
260
- )
261
- }
262
- if (tempType === 'game-cocos') {
263
-
264
- }
265
-
266
- fs.writeFileSync(dgzJosn, JSON.stringify(config, undefined, 2))
267
- console.log(dgzJosn)
268
-
269
- fse.removeSync(__TEMP_PATH__)
270
-
271
- spinner.text = ''
272
-
273
- fse.moveSync(appRootPath, root_path)
274
-
275
- spinner.stop()
276
- log(`${green.bold('Success!')} 创建项目 ${appName} 在 ${root_path} \n`)
277
- log(`切换到【 ${root_path} 】目录并运行开发`)
278
- log('\n')
279
- } catch (err) {
280
- console.log(err)
281
- log('\n')
282
- if (err.exitCode == 128) {
283
- log('Error: 目录已经存在。')
284
- }
285
- spinner.stop()
286
- }
287
- }
288
- main()