@htyf-mp/cli 0.2.0-alpha.5 → 0.2.0-alpha.7

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/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@htyf-mp/cli",
3
- "version": "0.2.0-alpha.5",
3
+ "version": "0.2.0-alpha.7",
4
4
  "description": "红糖云服app小程序模板-cli",
5
5
  "type": "module",
6
- "bin": "src/index.mjs",
6
+ "bin": {
7
+ "htyf": "src/index.mjs",
8
+ "htyf-mp": "src/index.mjs"
9
+ },
7
10
  "files": [
8
11
  "src",
9
12
  "README.md"
@@ -40,6 +43,8 @@
40
43
  "@callstack/repack-plugin-reanimated": "^5.1.3",
41
44
  "@inquirer/prompts": "^7.8.0",
42
45
  "@inquirer/select": "^4.3.1",
46
+ "@module-federation/dts-plugin": "^0.21.6",
47
+ "@module-federation/enhanced": "^0.21.6",
43
48
  "adm-zip": "0.5.10",
44
49
  "boxen": "^8.0.1",
45
50
  "chalk": "^5.4.1",
@@ -47,6 +52,7 @@
47
52
  "fs-extra": "^11.3.0",
48
53
  "gradient-string": "^3.0.0",
49
54
  "inquirer": "^12.9.0",
55
+ "loader-utils": "^3.3.1",
50
56
  "lodash": "^4.17.21",
51
57
  "md5": "^2.3.0",
52
58
  "ora": "^8.2.0",
@@ -58,6 +64,7 @@
58
64
  "superstatic": "^9.2.0",
59
65
  "systeminformation": "^5.27.7",
60
66
  "terser-webpack-plugin": "^5.3.14",
67
+ "typescript": "^5.9.3",
61
68
  "uuid": "^9.0.1",
62
69
  "webpack": "^5.101.0",
63
70
  "webpack-cli": "^6.0.1"
@@ -1,12 +1,44 @@
1
- import path from 'path';
1
+ import path from 'node:path';
2
2
  import fse from 'fs-extra';
3
- import { fileURLToPath } from 'url';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { getOptions } from 'loader-utils';
4
5
 
6
+ // ==================== 常量定义 ====================
5
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
- const sharedPkg = fse.readJSONSync(path.join(__dirname, '../shared-output.json'));
8
+ const nodeModulesPath = path.join(__dirname, '../node_modules');
9
+
10
+ // 预编译正则表达式以提高性能
11
+ const PNPM_PACKAGE_REGEX = /node_modules\/(?:\.pnpm\/[^/]+\/node_modules\/)?((?:@[^/]+\/)?[^/]+)/;
12
+ const DEFAULT_EXPORT_REGEX = /var\s+_default\s*=\s*exports(?:\.default|\["default"\])\s*=/g;
13
+ const DEPENDENCY_PLACEHOLDER_REGEX = /__HTYF_DEPENDENCY__/g;
14
+ const LEADING_WHITESPACE_REGEX = /^[ \t]+/gm;
7
15
 
8
16
  // 白名单:这些包即使有 native 代码也不需要检查(系统级或已特殊处理)
9
- const WHITELIST = [];
17
+ const WHITELIST = new Set();
18
+
19
+ // 错误消息分隔线
20
+ const ERROR_SEPARATOR = '═══════════════════════════════════════';
21
+
22
+ // ==================== 缓存和配置 ====================
23
+ const versionCache = new Map();
24
+ const nativeCodeCache = new Map();
25
+
26
+ // 延迟加载共享配置,避免启动时错误
27
+ let sharedPkg = null;
28
+ function getSharedConfig() {
29
+ if (sharedPkg === null) {
30
+ try {
31
+ const configPath = path.join(__dirname, './shared-output.json');
32
+ sharedPkg = fse.pathExistsSync(configPath) ? fse.readJSONSync(configPath) : {};
33
+ } catch (error) {
34
+ console.warn(`[HtyfModulesPlugin] 读取共享配置失败: ${error.message}`);
35
+ sharedPkg = {};
36
+ }
37
+ }
38
+ return sharedPkg;
39
+ }
40
+
41
+ // ==================== 工具函数 ====================
10
42
 
11
43
  /**
12
44
  * 从模块路径中提取包名
@@ -14,41 +46,68 @@ const WHITELIST = [];
14
46
  * @returns {string|null} 包名,如果无法提取则返回 null
15
47
  */
16
48
  function extractPackageName(moduleContext) {
17
- if (!moduleContext || !moduleContext.includes('node_modules')) {
49
+ if (!moduleContext?.includes('node_modules')) {
18
50
  return null;
19
51
  }
20
52
 
21
- const parts = moduleContext.split('node_modules/');
22
- if (parts.length < 2) {
23
- return null;
24
- }
53
+ const match = moduleContext.match(PNPM_PACKAGE_REGEX);
54
+ return match?.[1] || null;
55
+ }
25
56
 
26
- const packagePath = parts[1];
27
- const segments = packagePath.split('/').filter(Boolean);
28
-
29
- if (segments.length === 0) {
30
- return null;
57
+ /**
58
+ * 检查包是否包含 native 代码(带缓存)
59
+ * @param {string} packageName - 包名
60
+ * @returns {boolean} 是否包含 native 代码
61
+ */
62
+ function hasNativeCode(packageName) {
63
+ if (!packageName) {
64
+ return false;
31
65
  }
32
66
 
33
- // 处理 scoped packages (@scope/package)
34
- if (segments[0].startsWith('@') && segments.length >= 2) {
35
- return `${segments[0]}/${segments[1]}`;
67
+ // 使用缓存避免重复的文件系统访问
68
+ if (nativeCodeCache.has(packageName)) {
69
+ return nativeCodeCache.get(packageName);
36
70
  }
37
71
 
38
- return segments[0];
72
+ const packagePath = path.join(nodeModulesPath, packageName);
73
+ const iosPath = path.join(packagePath, 'ios');
74
+ const androidPath = path.join(packagePath, 'android');
75
+
76
+ const hasNative = fse.pathExistsSync(iosPath) || fse.pathExistsSync(androidPath);
77
+ nativeCodeCache.set(packageName, hasNative);
78
+
79
+ return hasNative;
39
80
  }
40
81
 
41
82
  /**
42
- * 检查包是否包含 native 代码
83
+ * 获取包版本(带缓存)
43
84
  * @param {string} packageName - 包名
44
- * @returns {boolean} 是否包含 native 代码
85
+ * @returns {string|null} 包版本,如果无法获取则返回 null
45
86
  */
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);
87
+ function getPackageVersion(packageName) {
88
+ if (!packageName) {
89
+ return null;
90
+ }
91
+
92
+ if (versionCache.has(packageName)) {
93
+ return versionCache.get(packageName);
94
+ }
95
+
96
+ const pkgJsonPath = path.join(nodeModulesPath, packageName, 'package.json');
97
+ let version = null;
98
+
99
+ try {
100
+ if (fse.pathExistsSync(pkgJsonPath)) {
101
+ const pkgJson = fse.readJsonSync(pkgJsonPath);
102
+ version = pkgJson.version || null;
103
+ }
104
+ } catch (error) {
105
+ // 静默失败,返回 null
106
+ version = null;
107
+ }
108
+
109
+ versionCache.set(packageName, version);
110
+ return version;
52
111
  }
53
112
 
54
113
  /**
@@ -57,7 +116,8 @@ function hasNativeCode(packageName) {
57
116
  * @returns {boolean} 是否在共享配置中
58
117
  */
59
118
  function isInSharedConfig(packageName) {
60
- return sharedPkg && sharedPkg.hasOwnProperty(packageName);
119
+ const config = getSharedConfig();
120
+ return packageName && Object.hasOwn(config, packageName);
61
121
  }
62
122
 
63
123
  /**
@@ -66,7 +126,7 @@ function isInSharedConfig(packageName) {
66
126
  * @returns {boolean} 是否在白名单中
67
127
  */
68
128
  function isWhitelisted(packageName) {
69
- return WHITELIST.includes(packageName);
129
+ return packageName && WHITELIST.has(packageName);
70
130
  }
71
131
 
72
132
  /**
@@ -75,28 +135,66 @@ function isWhitelisted(packageName) {
75
135
  * @returns {string} 格式化的错误消息
76
136
  */
77
137
  function formatErrorMessage(unsupportedPackages) {
138
+ if (!unsupportedPackages?.length) {
139
+ return '';
140
+ }
141
+
78
142
  const lines = [
79
143
  '',
80
- '═══════════════════════════════════════',
144
+ ERROR_SEPARATOR,
81
145
  ' 红糖云服暂不支持以下第三方 native 组件',
82
- '═══════════════════════════════════════',
146
+ ERROR_SEPARATOR,
147
+ '',
148
+ ...unsupportedPackages.map((pkg, index) => ` ${index + 1}. ${pkg}`),
149
+ '',
150
+ ERROR_SEPARATOR,
83
151
  '',
84
152
  ];
85
153
 
86
- unsupportedPackages.forEach((pkg, index) => {
87
- lines.push(` ${index + 1}. ${pkg}`);
88
- });
89
-
90
- lines.push('');
91
- lines.push('═══════════════════════════════════════');
92
- lines.push('');
93
-
94
154
  return lines.join('\n');
95
155
  }
96
156
 
157
+ /**
158
+ * 去除模板字符串中的前导空白
159
+ * @param {TemplateStringsArray} strings - 模板字符串数组
160
+ * @param {...any} values - 插值变量
161
+ * @returns {string} 处理后的字符串
162
+ */
97
163
  function trimTemplateStrings(strings, ...values) {
98
- let fullString = strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '');
99
- return fullString.replace(/^[ \t]+/gm, '');
164
+ const fullString = strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '');
165
+ return fullString.replace(LEADING_WHITESPACE_REGEX, '');
166
+ }
167
+
168
+ // ==================== Webpack Loader ====================
169
+
170
+ /**
171
+ * Webpack自定义loader,用于在目标模块的源码前后插入额外的代码片段。
172
+ * 可通过options.prependCode / options.appendCode分别指定插入内容。
173
+ *
174
+ * @param {string|Buffer} source 原始模块内容
175
+ * @returns {string} 处理后的模块内容
176
+ */
177
+ export default function injectAppInfoLoader(source) {
178
+ this.cacheable?.();
179
+
180
+ // 将 Buffer 转换为字符串
181
+ const originalSource = Buffer.isBuffer(source) ? source.toString('utf-8') : source;
182
+
183
+ // 注入依赖信息属性
184
+ return `
185
+ ${originalSource}
186
+ try {
187
+ Object.defineProperty(_default, '__HTYF_MP_DEPENDENCY__', {
188
+ value: JSON.parse(__HTYF_DEPENDENCY_STRING_DATA__),
189
+ writable: false,
190
+ enumerable: false,
191
+ configurable: false
192
+ });
193
+ _default = exports.default = _default;
194
+ } catch (error) {
195
+ console.error(error);
196
+ }
197
+ `;
100
198
  }
101
199
 
102
200
  export class HtyfModulesPlugin {
@@ -104,42 +202,31 @@ export class HtyfModulesPlugin {
104
202
  this.config = appConfig;
105
203
  }
106
204
 
205
+ /**
206
+ * 应用插件到 Webpack 编译器
207
+ * @param {Compiler} compiler - Webpack 编译器实例
208
+ */
107
209
  apply(compiler) {
108
210
  const config = this.config;
109
211
 
110
- // 分析第三方 native 模块
212
+ // 分析第三方依赖并生成报告
111
213
  compiler.hooks.emit.tapAsync('AnalyzeThirdPartyModulesPlugin', (compilation, callback) => {
112
214
  try {
113
- const nativeModules = new Set();
215
+ const { dependencies, nativeModules } = this.collectDependencyInfo(compilation);
114
216
 
115
- // 收集所有包含 native 代码的第三方模块
116
- compilation.modules.forEach(module => {
117
- if (!module.context) {
118
- return;
119
- }
217
+ // 获取 manifest 路径,如果未配置则使用默认路径(webpack output 路径下的 manifest.json)
218
+ const outputPath = compilation.outputOptions.path || compiler.options.output?.path;
219
+ const manifestPath = this.config.manifest || (outputPath ? path.join(outputPath, 'manifest.json') : null);
120
220
 
121
- const packageName = extractPackageName(module.context);
122
- if (!packageName) {
123
- return;
124
- }
221
+ // 持久化依赖报告
222
+ this.persistDependencyReport(dependencies, manifestPath);
125
223
 
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
- }
224
+ // 筛选出不支持的 native 包
225
+ const unsupportedPackages = Array.from(nativeModules).filter(
226
+ pkg => !isWhitelisted(pkg) && !isInSharedConfig(pkg)
227
+ );
137
228
 
138
- // 如果不在共享配置中,则不支持
139
- return !isInSharedConfig(pkg);
140
- });
141
-
142
- // 如果有不支持的包,抛出错误
229
+ // 如果存在不支持的包,抛出错误
143
230
  if (unsupportedPackages.length > 0) {
144
231
  const errorMessage = formatErrorMessage(unsupportedPackages);
145
232
  console.error(errorMessage);
@@ -152,6 +239,8 @@ export class HtyfModulesPlugin {
152
239
  return;
153
240
  }
154
241
 
242
+ // 替换依赖占位符
243
+ this.replaceDependencyPlaceholders(compilation, dependencies);
155
244
  callback();
156
245
  } catch (error) {
157
246
  callback(error);
@@ -163,22 +252,22 @@ export class HtyfModulesPlugin {
163
252
  compiler.hooks.emit.tapAsync('WrapCodePlugin', (compilation, callback) => {
164
253
  try {
165
254
  // 遍历所有编译过的资源文件
166
- for (const filename in compilation.assets) {
167
- if (compilation.assets.hasOwnProperty(filename)) {
168
- // 获取文件内容
169
- const source = compilation.assets[filename].source();
255
+ for (const filename of Object.keys(compilation.assets)) {
256
+ const asset = compilation.assets[filename];
257
+ if (!asset || typeof asset.source !== 'function') {
258
+ continue;
259
+ }
170
260
 
171
- // 包裹代码
172
- const wrappedSource = trimTemplateStrings`
261
+ const source = asset.source();
262
+ const wrappedSource = trimTemplateStrings`
173
263
  ${source}
174
- `;
264
+ `;
175
265
 
176
- // 更新文件内容
177
- compilation.assets[filename] = {
178
- source: () => wrappedSource,
179
- size: () => wrappedSource.length,
180
- };
181
- }
266
+ // 更新文件内容
267
+ compilation.assets[filename] = {
268
+ source: () => wrappedSource,
269
+ size: () => wrappedSource.length,
270
+ };
182
271
  }
183
272
  callback();
184
273
  } catch (error) {
@@ -187,4 +276,116 @@ export class HtyfModulesPlugin {
187
276
  });
188
277
  }
189
278
  }
279
+
280
+ /**
281
+ * 收集依赖信息
282
+ * @param {Compilation} compilation - Webpack 编译对象
283
+ * @returns {{dependencies: Object, nativeModules: Set<string>}} 依赖信息
284
+ */
285
+ collectDependencyInfo(compilation) {
286
+ const dependencyMap = new Map();
287
+ const nativeModules = new Set();
288
+
289
+ for (const module of compilation.modules) {
290
+ if (!module.context) {
291
+ continue;
292
+ }
293
+
294
+ const packageName = extractPackageName(module.context);
295
+ if (!packageName) {
296
+ continue;
297
+ }
298
+
299
+ // 如果已经处理过该包,跳过
300
+ if (dependencyMap.has(packageName)) {
301
+ const existing = dependencyMap.get(packageName);
302
+ // 如果之前不是 native 模块,再次检查(可能之前检查失败)
303
+ if (!existing.nativeModule && hasNativeCode(packageName)) {
304
+ existing.nativeModule = true;
305
+ nativeModules.add(packageName);
306
+ }
307
+ continue;
308
+ }
309
+
310
+ // 首次处理该包
311
+ const nativeModule = hasNativeCode(packageName);
312
+ const dependencyInfo = {
313
+ name: packageName,
314
+ version: getPackageVersion(packageName),
315
+ nativeModule,
316
+ };
317
+
318
+ dependencyMap.set(packageName, dependencyInfo);
319
+
320
+ if (nativeModule) {
321
+ nativeModules.add(packageName);
322
+ }
323
+ }
324
+
325
+ // 将 Map 转换为普通对象
326
+ const dependencies = Object.fromEntries(dependencyMap);
327
+
328
+ return {
329
+ dependencies,
330
+ nativeModules,
331
+ };
332
+ }
333
+
334
+ /**
335
+ * 持久化依赖报告到文件
336
+ * @param {Object} dependencies - 依赖信息对象
337
+ * @param {string|null} manifestPath - manifest 文件路径,如果未提供则使用配置中的 manifest
338
+ */
339
+ persistDependencyReport(dependencies = {}, manifestPath = null) {
340
+ const manifest = manifestPath || this.config.manifest;
341
+ if (!manifest) {
342
+ return;
343
+ }
344
+
345
+ try {
346
+ const reportDir = path.dirname(manifest);
347
+ fse.ensureDirSync(reportDir);
348
+ fse.writeJsonSync(
349
+ manifest,
350
+ { packages: dependencies },
351
+ { spaces: 2 }
352
+ );
353
+ } catch (error) {
354
+ console.warn(`[HtyfModulesPlugin] 写入依赖报告失败: ${error.message}`);
355
+ }
356
+ }
357
+
358
+ /**
359
+ * 替换依赖占位符
360
+ * @param {Compilation} compilation - Webpack 编译对象
361
+ * @param {Object} dependencies - 依赖信息对象
362
+ */
363
+ replaceDependencyPlaceholders(compilation, dependencies = {}) {
364
+ // 将依赖信息序列化为 JSON 字符串,并进行安全的字符串转义
365
+ const jsonString = JSON.stringify(dependencies);
366
+ // 转义反斜杠和单引号,用于嵌入到单引号字符串中
367
+ const escapedString = jsonString
368
+ .replace(/\\/g, '\\\\') // 转义反斜杠
369
+ .replace(/'/g, "\\'"); // 转义单引号
370
+ const stringLiteral = `'${escapedString}'`;
371
+
372
+ // 遍历所有资源文件,替换占位符
373
+ for (const filename of Object.keys(compilation.assets)) {
374
+ const asset = compilation.assets[filename];
375
+ if (!asset || typeof asset.source !== 'function') {
376
+ continue;
377
+ }
378
+
379
+ const source = asset.source().toString();
380
+ if (!source.includes('__HTYF_DEPENDENCY_STRING_DATA__')) {
381
+ continue;
382
+ }
383
+
384
+ const replacedSource = source.replace(DEPENDENCY_PLACEHOLDER_REGEX, stringLiteral);
385
+ compilation.assets[filename] = {
386
+ source: () => replacedSource,
387
+ size: () => replacedSource.length,
388
+ };
389
+ }
390
+ }
190
391
  }
package/src/build.mjs CHANGED
@@ -119,6 +119,7 @@ export async function mpBuildShell(newAppInfo) {
119
119
  },
120
120
  outputPath: path.join(mpOutputPath, './build'),
121
121
  extraChunksPath: outputPath,
122
+ manifest: path.join(mpOutputPath, './build'),
122
123
  };
123
124
 
124
125
  const appJsonPath = path.join(mpOutputPath, 'app.json');
@@ -156,7 +156,10 @@ export default (env = {}) => {
156
156
  shareStrategy: 'loaded-first',
157
157
  }),
158
158
  new HtyfModulesPlugin({
159
- codeSigning: true
159
+ codeSigning: true,
160
+ appid: mpOptions.appid,
161
+ version: mpOptions.version,
162
+ manifest: mpOptions.manifest,
160
163
  }),
161
164
  new webpack.optimize.LimitChunkCountPlugin({
162
165
  maxChunks: 2, // 限制最大chunk数,便于远程加载