@bm-fe/react-native-multi-bundle 1.0.0-beta.0

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.
Files changed (32) hide show
  1. package/INTEGRATION.md +371 -0
  2. package/LICENSE +21 -0
  3. package/README.md +240 -0
  4. package/package.json +86 -0
  5. package/scripts/build-multi-bundle.js +483 -0
  6. package/scripts/release-codepush-dev.sh +90 -0
  7. package/scripts/release-codepush.js +420 -0
  8. package/scripts/sync-bundles-to-assets.js +252 -0
  9. package/src/index.ts +40 -0
  10. package/src/multi-bundle/DevModeConfig.ts +23 -0
  11. package/src/multi-bundle/HMRClient.ts +157 -0
  12. package/src/multi-bundle/LocalBundleManager.ts +155 -0
  13. package/src/multi-bundle/ModuleErrorFallback.tsx +85 -0
  14. package/src/multi-bundle/ModuleLoaderMock.ts +169 -0
  15. package/src/multi-bundle/ModuleLoadingPlaceholder.tsx +34 -0
  16. package/src/multi-bundle/ModuleRegistry.ts +295 -0
  17. package/src/multi-bundle/README.md +343 -0
  18. package/src/multi-bundle/config.ts +141 -0
  19. package/src/multi-bundle/createModuleLoader.tsx +92 -0
  20. package/src/multi-bundle/createModuleRouteLoader.tsx +31 -0
  21. package/src/multi-bundle/devUtils.ts +48 -0
  22. package/src/multi-bundle/init.ts +131 -0
  23. package/src/multi-bundle/metro-config-helper.js +140 -0
  24. package/src/multi-bundle/preloadModule.ts +33 -0
  25. package/src/multi-bundle/routeRegistry.ts +118 -0
  26. package/src/types/global.d.ts +14 -0
  27. package/templates/metro.config.js.template +45 -0
  28. package/templates/multi-bundle.config.json.template +31 -0
  29. package/templates/native/android/ModuleLoaderModule.kt +227 -0
  30. package/templates/native/android/ModuleLoaderPackage.kt +26 -0
  31. package/templates/native/ios/ModuleLoaderModule.h +13 -0
  32. package/templates/native/ios/ModuleLoaderModule.m +60 -0
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CodePush 发布脚本
5
+ * 将打包产物上传到 CodePush 服务器
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execSync } = require('child_process');
11
+
12
+ const OUTPUT_DIR = path.join(__dirname, '../build/bundles');
13
+ const PROJECT_ROOT = path.join(__dirname, '..');
14
+
15
+ /**
16
+ * ANSI 颜色工具函数
17
+ */
18
+ const colors = {
19
+ reset: '\x1b[0m',
20
+ bright: '\x1b[1m',
21
+ dim: '\x1b[2m',
22
+ red: '\x1b[31m',
23
+ green: '\x1b[32m',
24
+ yellow: '\x1b[33m',
25
+ blue: '\x1b[34m',
26
+ magenta: '\x1b[35m',
27
+ cyan: '\x1b[36m',
28
+ gray: '\x1b[90m',
29
+ };
30
+
31
+ function green(text) {
32
+ return `${colors.green}${text}${colors.reset}`;
33
+ }
34
+
35
+ function yellow(text) {
36
+ return `${colors.yellow}${text}${colors.reset}`;
37
+ }
38
+
39
+ function blue(text) {
40
+ return `${colors.blue}${text}${colors.reset}`;
41
+ }
42
+
43
+ function red(text) {
44
+ return `${colors.red}${text}${colors.reset}`;
45
+ }
46
+
47
+ function cyan(text) {
48
+ return `${colors.cyan}${text}${colors.reset}`;
49
+ }
50
+
51
+ function gray(text) {
52
+ return `${colors.gray}${text}${colors.reset}`;
53
+ }
54
+
55
+ function bold(text) {
56
+ return `${colors.bright}${text}${colors.reset}`;
57
+ }
58
+
59
+ /**
60
+ * 格式化文件大小
61
+ */
62
+ function formatSize(bytes) {
63
+ if (bytes < 1024) {
64
+ return `${bytes} B`;
65
+ } else if (bytes < 1024 * 1024) {
66
+ return `${(bytes / 1024).toFixed(2)} KB`;
67
+ } else {
68
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 格式化文件路径(相对项目根目录)
74
+ */
75
+ function formatPath(filePath) {
76
+ return path.relative(PROJECT_ROOT, filePath).replace(/\\/g, '/');
77
+ }
78
+
79
+ /**
80
+ * 解析命令行参数,支持从环境变量读取
81
+ */
82
+ function parseArgs() {
83
+ const args = process.argv.slice(2);
84
+ const params = {
85
+ app: null,
86
+ deployment: null,
87
+ platform: null,
88
+ version: null,
89
+ description: '',
90
+ mandatory: false,
91
+ rollout: 100,
92
+ isDisabled: false,
93
+ };
94
+
95
+ // 先解析命令行参数
96
+ for (let i = 0; i < args.length; i++) {
97
+ const arg = args[i];
98
+ if (arg.startsWith('--')) {
99
+ const key = arg.slice(2);
100
+ const value = args[i + 1];
101
+
102
+ if (key === 'mandatory') {
103
+ params.mandatory = value === 'true' || value === '1';
104
+ i++;
105
+ } else if (key === 'isDisabled') {
106
+ params.isDisabled = value === 'true' || value === '1';
107
+ i++;
108
+ } else if (key === 'rollout') {
109
+ params.rollout = parseInt(value, 10);
110
+ i++;
111
+ } else if (key in params) {
112
+ params[key] = value;
113
+ i++;
114
+ }
115
+ }
116
+ }
117
+
118
+ // 如果命令行参数缺失,从环境变量读取
119
+ // 环境变量命名:CODE_PUSH_<PARAM_NAME>
120
+ // 命令行参数优先级高于环境变量
121
+ if (!params.app && process.env.CODE_PUSH_APP) {
122
+ params.app = process.env.CODE_PUSH_APP;
123
+ }
124
+ if (!params.deployment && process.env.CODE_PUSH_DEPLOYMENT) {
125
+ params.deployment = process.env.CODE_PUSH_DEPLOYMENT;
126
+ }
127
+ if (!params.platform && process.env.CODE_PUSH_PLATFORM) {
128
+ params.platform = process.env.CODE_PUSH_PLATFORM;
129
+ }
130
+ if (!params.version && process.env.CODE_PUSH_VERSION) {
131
+ params.version = process.env.CODE_PUSH_VERSION;
132
+ }
133
+ // description 如果命令行没有提供且环境变量存在,则使用环境变量
134
+ if (params.description === '' && process.env.CODE_PUSH_DESCRIPTION) {
135
+ params.description = process.env.CODE_PUSH_DESCRIPTION;
136
+ }
137
+ // mandatory 如果命令行没有提供且环境变量存在,则使用环境变量
138
+ if (!args.includes('--mandatory') && process.env.CODE_PUSH_MANDATORY) {
139
+ params.mandatory = process.env.CODE_PUSH_MANDATORY === 'true' || process.env.CODE_PUSH_MANDATORY === '1';
140
+ }
141
+ // rollout 如果命令行没有提供且环境变量存在,则使用环境变量
142
+ if (!args.includes('--rollout') && process.env.CODE_PUSH_ROLLOUT) {
143
+ const rollout = parseInt(process.env.CODE_PUSH_ROLLOUT, 10);
144
+ if (!isNaN(rollout) && rollout >= 1 && rollout <= 100) {
145
+ params.rollout = rollout;
146
+ }
147
+ }
148
+ // isDisabled 如果命令行没有提供且环境变量存在,则使用环境变量
149
+ if (!args.includes('--isDisabled') && process.env.CODE_PUSH_IS_DISABLED) {
150
+ params.isDisabled = process.env.CODE_PUSH_IS_DISABLED === 'true' || process.env.CODE_PUSH_IS_DISABLED === '1';
151
+ }
152
+
153
+ return params;
154
+ }
155
+
156
+ /**
157
+ * 验证必需参数和环境变量
158
+ */
159
+ function validateParams(params) {
160
+ const errors = [];
161
+
162
+ if (!params.app) {
163
+ errors.push('--app 参数是必需的');
164
+ }
165
+ if (!params.deployment) {
166
+ errors.push('--deployment 参数是必需的');
167
+ }
168
+ if (!params.platform) {
169
+ errors.push('--platform 参数是必需的');
170
+ }
171
+ if (!['ios', 'android'].includes(params.platform)) {
172
+ errors.push('--platform 必须是 ios 或 android');
173
+ }
174
+ if (!params.version) {
175
+ errors.push('--version 参数是必需的');
176
+ }
177
+
178
+ if (!process.env.CODE_PUSH_SERVER_URL) {
179
+ errors.push('环境变量 CODE_PUSH_SERVER_URL 是必需的');
180
+ }
181
+ if (!process.env.ACCESS_KEY) {
182
+ errors.push('环境变量 ACCESS_KEY 是必需的');
183
+ }
184
+
185
+ if (params.rollout < 1 || params.rollout > 100) {
186
+ errors.push('--rollout 必须在 1-100 之间');
187
+ }
188
+
189
+ if (errors.length > 0) {
190
+ console.error(`\n${red('✗')} ${red(bold('参数验证失败'))}`);
191
+ errors.forEach(error => {
192
+ console.error(` ${red(error)}`);
193
+ });
194
+ console.error('');
195
+ process.exit(1);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * 检查打包产物目录是否存在
201
+ */
202
+ function validateBuildOutput(platform) {
203
+ const platformDir = path.join(OUTPUT_DIR, platform);
204
+
205
+ if (!fs.existsSync(platformDir)) {
206
+ console.error(`\n${red('✗')} ${red(bold('打包产物目录不存在'))}`);
207
+ console.error(` ${gray('路径:')} ${formatPath(platformDir)}`);
208
+ console.error(` ${yellow('提示:')} 请先运行 ${cyan('npm run bundle:' + platform)} 进行打包\n`);
209
+ process.exit(1);
210
+ }
211
+
212
+ const manifestPath = path.join(platformDir, 'bundle-manifest.json');
213
+ if (!fs.existsSync(manifestPath)) {
214
+ console.error(`\n${red('✗')} ${red(bold('bundle-manifest.json 不存在'))}`);
215
+ console.error(` ${gray('路径:')} ${formatPath(manifestPath)}\n`);
216
+ process.exit(1);
217
+ }
218
+
219
+ return platformDir;
220
+ }
221
+
222
+ /**
223
+ * 创建 zip 包
224
+ */
225
+ function createZipPackage(platformDir, platform, version) {
226
+ const timestamp = Date.now();
227
+ const zipFileName = `${platform}-${version}-${timestamp}.zip`;
228
+ const zipPath = path.join(OUTPUT_DIR, zipFileName);
229
+
230
+ console.log(`${bold('📦')} ${bold('创建 zip 包')}...`);
231
+ console.log(` ${gray('源目录:')} ${formatPath(platformDir)}`);
232
+ console.log(` ${gray('输出文件:')} ${formatPath(zipPath)}`);
233
+
234
+ try {
235
+ // 使用系统 zip 命令创建压缩包
236
+ // 进入平台目录内部,压缩所有内容(不包含平台目录本身)
237
+ // 使用 -j 选项可以忽略目录结构,但我们需要保留 modules/ 目录结构
238
+ // 所以使用 cd 进入目录,然后压缩当前目录的所有内容
239
+ execSync(
240
+ `cd "${platformDir}" && zip -r "${zipPath}" .`,
241
+ { stdio: 'inherit' }
242
+ );
243
+
244
+ if (!fs.existsSync(zipPath)) {
245
+ throw new Error('Zip 文件创建失败');
246
+ }
247
+
248
+ const stats = fs.statSync(zipPath);
249
+ console.log(` ${green('✓')} ${bold('创建成功')} ${gray(`(${formatSize(stats.size)})`)}\n`);
250
+
251
+ return zipPath;
252
+ } catch (error) {
253
+ console.error(`\n${red('✗')} ${red(bold('创建 zip 包失败'))}`);
254
+ console.error(` ${red(error.message)}\n`);
255
+ process.exit(1);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * 生成 packageInfo JSON
261
+ */
262
+ function generatePackageInfo(params, platformDir) {
263
+ const manifestPath = path.join(platformDir, 'bundle-manifest.json');
264
+ let manifest = {};
265
+
266
+ try {
267
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
268
+ } catch (error) {
269
+ console.warn(`${yellow('⚠')} ${yellow('无法读取 manifest,使用默认值')}`);
270
+ }
271
+
272
+ const packageInfo = {
273
+ appVersion: params.version,
274
+ description: params.description || '',
275
+ mandatory: params.mandatory,
276
+ rollout: params.rollout,
277
+ isDisabled: params.isDisabled,
278
+ };
279
+
280
+ return JSON.stringify(packageInfo);
281
+ }
282
+
283
+ /**
284
+ * 上传到 CodePush 服务器
285
+ */
286
+ async function uploadToCodePush(zipPath, packageInfo, params) {
287
+ const serverUrl = process.env.CODE_PUSH_SERVER_URL;
288
+ const accessKey = process.env.ACCESS_KEY;
289
+ const apiUrl = `${serverUrl}/api/apps/${params.app}/deployments/${params.deployment}/release`;
290
+
291
+ console.log(`${bold('🚀')} ${bold('上传到 CodePush 服务器')}...`);
292
+ console.log(` ${gray('服务器:')} ${cyan(serverUrl)}`);
293
+ console.log(` ${gray('应用:')} ${cyan(params.app)}`);
294
+ console.log(` ${gray('部署环境:')} ${cyan(params.deployment)}`);
295
+ console.log(` ${gray('平台:')} ${cyan(params.platform)}`);
296
+ console.log(` ${gray('版本:')} ${cyan(params.version)}\n`);
297
+
298
+ try {
299
+ // 读取 zip 文件
300
+ const zipBuffer = fs.readFileSync(zipPath);
301
+ const zipFileName = path.basename(zipPath);
302
+
303
+ // 创建 FormData(使用 Node.js 18+ 内置的 FormData)
304
+ const formData = new FormData();
305
+
306
+ // 添加 zip 文件(使用 Blob)
307
+ const zipBlob = new Blob([zipBuffer], { type: 'application/zip' });
308
+ formData.append('package', zipBlob, zipFileName);
309
+
310
+ // 添加 packageInfo
311
+ formData.append('packageInfo', packageInfo);
312
+
313
+ // 使用 fetch 上传
314
+ const response = await fetch(apiUrl, {
315
+ method: 'POST',
316
+ headers: {
317
+ 'Authorization': `Bearer ${accessKey}`,
318
+ // 不要手动设置 Content-Type,让 fetch 自动设置 multipart/form-data boundary
319
+ },
320
+ body: formData,
321
+ });
322
+
323
+ // 解析响应
324
+ const responseText = await response.text();
325
+ let releaseInfo = {};
326
+
327
+ if (response.ok) {
328
+ // 成功
329
+ try {
330
+ releaseInfo = JSON.parse(responseText);
331
+ } catch (e) {
332
+ // 如果响应不是 JSON,直接显示原始响应
333
+ releaseInfo = { response: responseText };
334
+ }
335
+
336
+ console.log(`${green('✓')} ${green(bold('上传成功'))}\n`);
337
+ console.log(bold('Release 信息:'));
338
+ console.log(gray('─'.repeat(60)));
339
+
340
+ if (releaseInfo.label) {
341
+ console.log(` ${bold('Label:')} ${cyan(releaseInfo.label)}`);
342
+ }
343
+ if (releaseInfo.packageHash) {
344
+ console.log(` ${bold('Package Hash:')} ${cyan(releaseInfo.packageHash)}`);
345
+ }
346
+ if (releaseInfo.appVersion) {
347
+ console.log(` ${bold('App Version:')} ${cyan(releaseInfo.appVersion)}`);
348
+ }
349
+ if (releaseInfo.description) {
350
+ console.log(` ${bold('Description:')} ${cyan(releaseInfo.description)}`);
351
+ }
352
+ if (releaseInfo.mandatory !== undefined) {
353
+ console.log(` ${bold('Mandatory:')} ${cyan(releaseInfo.mandatory ? 'Yes' : 'No')}`);
354
+ }
355
+ if (releaseInfo.rollout !== undefined) {
356
+ console.log(` ${bold('Rollout:')} ${cyan(`${releaseInfo.rollout}%`)}`);
357
+ }
358
+
359
+ console.log(gray('─'.repeat(60)) + '\n');
360
+
361
+ return releaseInfo;
362
+ } else {
363
+ // 错误
364
+ throw new Error(`HTTP ${response.status}: ${responseText}`);
365
+ }
366
+ } catch (error) {
367
+ console.error(`\n${red('✗')} ${red(bold('上传失败'))}`);
368
+
369
+ if (error.response) {
370
+ console.error(` ${red(`HTTP ${error.response.status}`)}`);
371
+ }
372
+
373
+ if (error.message) {
374
+ console.error(` ${red(error.message)}`);
375
+ }
376
+
377
+ console.error('');
378
+ process.exit(1);
379
+ }
380
+ }
381
+
382
+ /**
383
+ * 主函数
384
+ */
385
+ async function main() {
386
+ console.log('\n' + bold('📤 CodePush Release'));
387
+ console.log(gray('─'.repeat(60)) + '\n');
388
+
389
+ // 解析参数
390
+ const params = parseArgs();
391
+
392
+ // 验证参数
393
+ validateParams(params);
394
+
395
+ // 验证打包产物
396
+ const platformDir = validateBuildOutput(params.platform);
397
+
398
+ // 创建 zip 包
399
+ const zipPath = createZipPackage(platformDir, params.platform, params.version);
400
+
401
+ // 生成 packageInfo
402
+ const packageInfo = generatePackageInfo(params, platformDir);
403
+
404
+ // 上传到 CodePush
405
+ await uploadToCodePush(zipPath, packageInfo, params);
406
+
407
+ console.log(green(bold('✅ 发布完成!')) + '\n');
408
+ }
409
+
410
+ if (require.main === module) {
411
+ main().catch((error) => {
412
+ console.error(`\n${red('✗')} ${red(bold('执行失败'))}`);
413
+ console.error(` ${red(error.message)}`);
414
+ console.error('');
415
+ process.exit(1);
416
+ });
417
+ }
418
+
419
+ module.exports = { main };
420
+
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 同步 Bundle 到 Assets 目录脚本
5
+ * 将构建好的 bundle 文件同步到 Android/iOS 的 assets 目录
6
+ *
7
+ * Android: android/app/src/main/assets/bundles/
8
+ * iOS: ios/DemoProject/RNModules/Bundles/
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const PROJECT_ROOT = path.join(__dirname, '..');
15
+ const BUILD_DIR = path.join(PROJECT_ROOT, 'build/bundles');
16
+ const ANDROID_ASSETS_DIR = path.join(PROJECT_ROOT, 'android/app/src/main/assets');
17
+ const IOS_ASSETS_DIR = path.join(PROJECT_ROOT, 'ios/DemoProject/RNModules/Bundles');
18
+
19
+ /**
20
+ * ANSI 颜色工具函数
21
+ */
22
+ const colors = {
23
+ reset: '\x1b[0m',
24
+ bright: '\x1b[1m',
25
+ red: '\x1b[31m',
26
+ green: '\x1b[32m',
27
+ yellow: '\x1b[33m',
28
+ blue: '\x1b[34m',
29
+ cyan: '\x1b[36m',
30
+ gray: '\x1b[90m',
31
+ };
32
+
33
+ function green(text) {
34
+ return `${colors.green}${text}${colors.reset}`;
35
+ }
36
+
37
+ function yellow(text) {
38
+ return `${colors.yellow}${text}${colors.reset}`;
39
+ }
40
+
41
+ function blue(text) {
42
+ return `${colors.blue}${text}${colors.reset}`;
43
+ }
44
+
45
+ function red(text) {
46
+ return `${colors.red}${text}${colors.reset}`;
47
+ }
48
+
49
+ function cyan(text) {
50
+ return `${colors.cyan}${text}${colors.reset}`;
51
+ }
52
+
53
+ function gray(text) {
54
+ return `${colors.gray}${text}${colors.reset}`;
55
+ }
56
+
57
+ function bold(text) {
58
+ return `${colors.bright}${text}${colors.reset}`;
59
+ }
60
+
61
+ /**
62
+ * 格式化文件路径(相对项目根目录)
63
+ */
64
+ function formatPath(filePath) {
65
+ return path.relative(PROJECT_ROOT, filePath).replace(/\\/g, '/');
66
+ }
67
+
68
+ /**
69
+ * 格式化文件大小
70
+ */
71
+ function formatSize(bytes) {
72
+ if (bytes < 1024) {
73
+ return `${bytes} B`;
74
+ } else if (bytes < 1024 * 1024) {
75
+ return `${(bytes / 1024).toFixed(2)} KB`;
76
+ } else {
77
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * 复制文件
83
+ */
84
+ function copyFile(src, dest) {
85
+ // 确保目标目录存在
86
+ const destDir = path.dirname(dest);
87
+ if (!fs.existsSync(destDir)) {
88
+ fs.mkdirSync(destDir, { recursive: true });
89
+ }
90
+
91
+ // 复制文件
92
+ fs.copyFileSync(src, dest);
93
+
94
+ const stats = fs.statSync(dest);
95
+ return stats.size;
96
+ }
97
+
98
+ /**
99
+ * 同步单个平台的 bundle
100
+ */
101
+ function syncPlatformBundles(platform, sourceDir, targetDir) {
102
+ console.log(`\n${bold('Syncing')} ${cyan(platform)} ${gray('bundles...')}\n`);
103
+
104
+ if (!fs.existsSync(sourceDir)) {
105
+ console.error(` ${red('✗')} Source directory not found: ${formatPath(sourceDir)}`);
106
+ console.error(` ${gray('Please run')} ${cyan(`npm run bundle:${platform}`)} ${gray('first')}\n`);
107
+ return { success: false, syncedFiles: [], failedFiles: [] };
108
+ }
109
+
110
+ // 读取 manifest
111
+ const manifestPath = path.join(sourceDir, 'bundle-manifest.json');
112
+ if (!fs.existsSync(manifestPath)) {
113
+ console.error(` ${red('✗')} Manifest not found: ${formatPath(manifestPath)}`);
114
+ return { success: false, syncedFiles: [], failedFiles: [] };
115
+ }
116
+
117
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
118
+ const syncedFiles = [];
119
+ const failedFiles = [];
120
+ let totalSize = 0;
121
+
122
+ // 确保目标目录存在
123
+ if (!fs.existsSync(targetDir)) {
124
+ fs.mkdirSync(targetDir, { recursive: true });
125
+ }
126
+
127
+ // 1. 复制 manifest
128
+ try {
129
+ const destManifest = path.join(targetDir, 'bundle-manifest.json');
130
+ const size = copyFile(manifestPath, destManifest);
131
+ syncedFiles.push('bundle-manifest.json');
132
+ totalSize += size;
133
+ console.log(` ${green('✓')} ${bold('bundle-manifest.json')} ${gray(`(${formatSize(size)})`)}`);
134
+ } catch (error) {
135
+ failedFiles.push('bundle-manifest.json');
136
+ console.error(` ${red('✗')} Failed to copy manifest: ${error.message}`);
137
+ }
138
+
139
+ // 2. 复制主 bundle
140
+ try {
141
+ const mainSource = path.join(sourceDir, manifest.main.file);
142
+ const mainDest = path.join(targetDir, manifest.main.file);
143
+ const size = copyFile(mainSource, mainDest);
144
+ syncedFiles.push(manifest.main.file);
145
+ totalSize += size;
146
+ console.log(` ${green('✓')} ${bold(manifest.main.file)} ${gray(`(${formatSize(size)})`)}`);
147
+ } catch (error) {
148
+ failedFiles.push(manifest.main.file);
149
+ console.error(` ${red('✗')} Failed to copy main bundle: ${error.message}`);
150
+ }
151
+
152
+ // 3. 复制模块 bundle
153
+ for (const module of manifest.modules) {
154
+ try {
155
+ const moduleSource = path.join(sourceDir, module.file);
156
+ const moduleDest = path.join(targetDir, module.file);
157
+
158
+ // 确保模块目录存在
159
+ const moduleDir = path.dirname(moduleDest);
160
+ if (!fs.existsSync(moduleDir)) {
161
+ fs.mkdirSync(moduleDir, { recursive: true });
162
+ }
163
+
164
+ const size = copyFile(moduleSource, moduleDest);
165
+ syncedFiles.push(module.file);
166
+ totalSize += size;
167
+ console.log(` ${green('✓')} ${bold(module.file)} ${gray(`(${formatSize(size)})`)}`);
168
+ } catch (error) {
169
+ failedFiles.push(module.file);
170
+ console.error(` ${red('✗')} Failed to copy ${module.file}: ${error.message}`);
171
+ }
172
+ }
173
+
174
+ // 显示摘要
175
+ console.log(`\n ${bold('Summary:')}`);
176
+ console.log(` ${gray('Synced:')} ${green(syncedFiles.length)} files ${gray(`(${formatSize(totalSize)})`)}`);
177
+ if (failedFiles.length > 0) {
178
+ console.log(` ${gray('Failed:')} ${red(failedFiles.length)} files`);
179
+ }
180
+ console.log(` ${gray('Target:')} ${cyan(formatPath(targetDir))}\n`);
181
+
182
+ return {
183
+ success: failedFiles.length === 0,
184
+ syncedFiles,
185
+ failedFiles,
186
+ totalSize,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * 主函数
192
+ */
193
+ function main() {
194
+ const args = process.argv.slice(2);
195
+ const platform = args[0]; // 'android', 'ios', 或 'all'
196
+
197
+ console.log('\n' + bold('📦 Syncing Bundles to Assets'));
198
+ console.log(gray('─'.repeat(60)) + '\n');
199
+
200
+ if (!platform || !['android', 'ios', 'all'].includes(platform)) {
201
+ console.error(` ${red('✗')} ${red(bold('Invalid platform'))}`);
202
+ console.error(` ${gray('Use:')} ${cyan('android')}, ${cyan('ios')}, ${cyan('or')} ${cyan('all')}\n`);
203
+ process.exit(1);
204
+ }
205
+
206
+ const results = [];
207
+
208
+ if (platform === 'android' || platform === 'all') {
209
+ const androidSource = path.join(BUILD_DIR, 'android');
210
+ const result = syncPlatformBundles('Android', androidSource, ANDROID_ASSETS_DIR);
211
+ results.push({ platform: 'android', ...result });
212
+ }
213
+
214
+ if (platform === 'ios' || platform === 'all') {
215
+ const iosSource = path.join(BUILD_DIR, 'ios');
216
+ const result = syncPlatformBundles('iOS', iosSource, IOS_ASSETS_DIR);
217
+ results.push({ platform: 'ios', ...result });
218
+ }
219
+
220
+ // 显示最终摘要
221
+ console.log(bold('Final Summary'));
222
+ console.log(gray('─'.repeat(60)));
223
+
224
+ let totalSynced = 0;
225
+ let totalFailed = 0;
226
+ let totalSize = 0;
227
+
228
+ for (const result of results) {
229
+ const status = result.success ? green('✓') : red('✗');
230
+ console.log(` ${status} ${bold(result.platform)}: ${green(result.syncedFiles.length)} synced, ${result.failedFiles.length > 0 ? red(result.failedFiles.length) : gray('0')} failed`);
231
+ totalSynced += result.syncedFiles.length;
232
+ totalFailed += result.failedFiles.length;
233
+ totalSize += result.totalSize || 0;
234
+ }
235
+
236
+ console.log(gray('─'.repeat(60)));
237
+ console.log(` ${bold('Total:')} ${green(totalSynced)} files synced ${gray(`(${formatSize(totalSize)})`)}, ${totalFailed > 0 ? red(totalFailed) : gray('0')} failed\n`);
238
+
239
+ if (totalFailed > 0) {
240
+ console.error(red(bold('⚠️ Some files failed to sync\n')));
241
+ process.exit(1);
242
+ } else {
243
+ console.log(green(bold('✅ Sync completed successfully!\n')));
244
+ }
245
+ }
246
+
247
+ if (require.main === module) {
248
+ main();
249
+ }
250
+
251
+ module.exports = { syncPlatformBundles };
252
+
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * React Native 多 Bundle 系统 - npm 包入口
3
+ * 导出所有公共 API
4
+ */
5
+
6
+ // 核心初始化
7
+ export { initMultiBundle, type InitResult } from './multi-bundle/init';
8
+
9
+ // 配置相关
10
+ export {
11
+ type MultiBundleConfig,
12
+ type DevServerConfig,
13
+ type ManifestProvider,
14
+ type ModuleLoader,
15
+ mergeConfig,
16
+ getGlobalConfig,
17
+ setGlobalConfig,
18
+ } from './multi-bundle/config';
19
+
20
+ // 模块加载器
21
+ export { createModuleLoader, type CreateModuleLoaderOptions } from './multi-bundle/createModuleLoader';
22
+ export { createModuleRouteLoader } from './multi-bundle/createModuleRouteLoader';
23
+
24
+ // 模块预加载
25
+ export { preloadModule } from './multi-bundle/preloadModule';
26
+
27
+ // ModuleRegistry
28
+ export { ModuleRegistry, type BundleManifest } from './multi-bundle/ModuleRegistry';
29
+
30
+ // LocalBundleManager
31
+ export { LocalBundleManager } from './multi-bundle/LocalBundleManager';
32
+
33
+ // 路由注册(可选,用于兼容旧代码)
34
+ export { registerModuleRoute, getModuleRoute } from './multi-bundle/routeRegistry';
35
+
36
+ // 类型定义
37
+ export type {
38
+ ModuleState,
39
+ ModuleMeta,
40
+ } from './multi-bundle/ModuleRegistry';