@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.
- package/INTEGRATION.md +371 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/package.json +86 -0
- package/scripts/build-multi-bundle.js +483 -0
- package/scripts/release-codepush-dev.sh +90 -0
- package/scripts/release-codepush.js +420 -0
- package/scripts/sync-bundles-to-assets.js +252 -0
- package/src/index.ts +40 -0
- package/src/multi-bundle/DevModeConfig.ts +23 -0
- package/src/multi-bundle/HMRClient.ts +157 -0
- package/src/multi-bundle/LocalBundleManager.ts +155 -0
- package/src/multi-bundle/ModuleErrorFallback.tsx +85 -0
- package/src/multi-bundle/ModuleLoaderMock.ts +169 -0
- package/src/multi-bundle/ModuleLoadingPlaceholder.tsx +34 -0
- package/src/multi-bundle/ModuleRegistry.ts +295 -0
- package/src/multi-bundle/README.md +343 -0
- package/src/multi-bundle/config.ts +141 -0
- package/src/multi-bundle/createModuleLoader.tsx +92 -0
- package/src/multi-bundle/createModuleRouteLoader.tsx +31 -0
- package/src/multi-bundle/devUtils.ts +48 -0
- package/src/multi-bundle/init.ts +131 -0
- package/src/multi-bundle/metro-config-helper.js +140 -0
- package/src/multi-bundle/preloadModule.ts +33 -0
- package/src/multi-bundle/routeRegistry.ts +118 -0
- package/src/types/global.d.ts +14 -0
- package/templates/metro.config.js.template +45 -0
- package/templates/multi-bundle.config.json.template +31 -0
- package/templates/native/android/ModuleLoaderModule.kt +227 -0
- package/templates/native/android/ModuleLoaderPackage.kt +26 -0
- package/templates/native/ios/ModuleLoaderModule.h +13 -0
- package/templates/native/ios/ModuleLoaderModule.m +60 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 多 Bundle 构建脚本
|
|
5
|
+
* 使用 React Native CLI 的 bundle 命令分别构建主 bundle 和各个模块 bundle
|
|
6
|
+
*
|
|
7
|
+
* 使用方式:
|
|
8
|
+
* 1. 作为 npm 包使用:node node_modules/@bm-fe/react-native-multi-bundle/scripts/build-multi-bundle.js <platform> [--env <env>]
|
|
9
|
+
* 2. 全局安装后:multi-bundle-build <platform> [--env <env>]
|
|
10
|
+
*
|
|
11
|
+
* 参数:
|
|
12
|
+
* - platform: 'ios' 或 'android'
|
|
13
|
+
* - --env: 'development' | 'staging' | 'production' (默认: 'production')
|
|
14
|
+
*
|
|
15
|
+
* 环境变量:
|
|
16
|
+
* - PROJECT_ROOT: 项目根目录(可选,默认从脚本位置推断)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { execSync } = require('child_process');
|
|
22
|
+
|
|
23
|
+
// 支持从任意项目目录运行
|
|
24
|
+
// 如果作为 npm 包使用,PROJECT_ROOT 应该是调用脚本的项目根目录
|
|
25
|
+
// 如果直接运行,PROJECT_ROOT 是脚本所在目录的父目录
|
|
26
|
+
const PROJECT_ROOT = process.env.PROJECT_ROOT || path.join(__dirname, '..');
|
|
27
|
+
const CONFIG_FILE = path.join(PROJECT_ROOT, 'multi-bundle.config.json');
|
|
28
|
+
const OUTPUT_DIR = path.join(PROJECT_ROOT, 'build/bundles');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ANSI 颜色工具函数
|
|
32
|
+
*/
|
|
33
|
+
const colors = {
|
|
34
|
+
reset: '\x1b[0m',
|
|
35
|
+
bright: '\x1b[1m',
|
|
36
|
+
dim: '\x1b[2m',
|
|
37
|
+
red: '\x1b[31m',
|
|
38
|
+
green: '\x1b[32m',
|
|
39
|
+
yellow: '\x1b[33m',
|
|
40
|
+
blue: '\x1b[34m',
|
|
41
|
+
magenta: '\x1b[35m',
|
|
42
|
+
cyan: '\x1b[36m',
|
|
43
|
+
gray: '\x1b[90m',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function green(text) {
|
|
47
|
+
return `${colors.green}${text}${colors.reset}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function yellow(text) {
|
|
51
|
+
return `${colors.yellow}${text}${colors.reset}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function blue(text) {
|
|
55
|
+
return `${colors.blue}${text}${colors.reset}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function red(text) {
|
|
59
|
+
return `${colors.red}${text}${colors.reset}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function cyan(text) {
|
|
63
|
+
return `${colors.cyan}${text}${colors.reset}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function gray(text) {
|
|
67
|
+
return `${colors.gray}${text}${colors.reset}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function bold(text) {
|
|
71
|
+
return `${colors.bright}${text}${colors.reset}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 格式化文件大小
|
|
76
|
+
*/
|
|
77
|
+
function formatSize(bytes) {
|
|
78
|
+
if (bytes < 1024) {
|
|
79
|
+
return `${bytes} B`;
|
|
80
|
+
} else if (bytes < 1024 * 1024) {
|
|
81
|
+
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
82
|
+
} else {
|
|
83
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 格式化文件路径(相对项目根目录)
|
|
89
|
+
*/
|
|
90
|
+
function formatPath(filePath) {
|
|
91
|
+
return path.relative(PROJECT_ROOT, filePath).replace(/\\/g, '/');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 格式化时间(毫秒转秒或保持毫秒)
|
|
96
|
+
*/
|
|
97
|
+
function formatDuration(ms) {
|
|
98
|
+
if (ms < 1000) {
|
|
99
|
+
return `${ms.toFixed(0)}ms`;
|
|
100
|
+
} else {
|
|
101
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 显示构建进度
|
|
107
|
+
*/
|
|
108
|
+
function showProgress(current, total, name) {
|
|
109
|
+
return `[${cyan(`${current}/${total}`)}] ${bold('Building')} ${cyan(name)}...`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 获取主 bundle 文件名(根据平台)
|
|
114
|
+
*/
|
|
115
|
+
function getMainBundleFileName(platform) {
|
|
116
|
+
if (platform === 'ios') {
|
|
117
|
+
return 'main.jsbundle';
|
|
118
|
+
} else if (platform === 'android') {
|
|
119
|
+
return 'index.android.bundle';
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取模块 bundle 文件名(根据平台和模块 ID)
|
|
126
|
+
*/
|
|
127
|
+
function getModuleBundleFileName(moduleId, platform) {
|
|
128
|
+
if (platform === 'ios') {
|
|
129
|
+
return `${moduleId}.jsbundle`;
|
|
130
|
+
} else if (platform === 'android') {
|
|
131
|
+
return `${moduleId}.bundle`;
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 读取 multi-bundle.config.json 配置文件
|
|
138
|
+
* 该配置文件用于:
|
|
139
|
+
* 1. 定义主 bundle 和模块 bundle 的入口文件
|
|
140
|
+
* 2. 定义模块的版本、依赖关系等元数据
|
|
141
|
+
* 3. 生成 bundle-manifest.json
|
|
142
|
+
*/
|
|
143
|
+
function loadMultiBundleConfig() {
|
|
144
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
145
|
+
throw new Error(`Config file not found: ${CONFIG_FILE}`);
|
|
146
|
+
}
|
|
147
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 从模块 bundle 中移除 prelude 代码
|
|
152
|
+
* Prelude 代码从 bundle 开头到第一个 __d( 之前
|
|
153
|
+
*/
|
|
154
|
+
function removePreludeFromBundle(bundlePath) {
|
|
155
|
+
try {
|
|
156
|
+
const bundleContent = fs.readFileSync(bundlePath, 'utf-8');
|
|
157
|
+
const firstModuleDefIndex = bundleContent.indexOf('__d(');
|
|
158
|
+
|
|
159
|
+
if (firstModuleDefIndex > 0) {
|
|
160
|
+
// 移除 prelude:从开头到第一个 __d( 之前的所有内容
|
|
161
|
+
const codeWithoutPrelude = bundleContent.substring(firstModuleDefIndex);
|
|
162
|
+
fs.writeFileSync(bundlePath, codeWithoutPrelude, 'utf-8');
|
|
163
|
+
console.log(` ${gray('→')} ${gray('Removed')} ${gray(`${firstModuleDefIndex} characters of prelude`)}`);
|
|
164
|
+
} else {
|
|
165
|
+
console.warn(` ${yellow('⚠')} ${yellow('Warning:')} ${yellow('__d( not found in bundle, prelude not removed')}`);
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(` ${red('✗')} ${red('Failed to remove prelude:')} ${red(error.message)}`);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 确保输出目录存在
|
|
175
|
+
*/
|
|
176
|
+
function ensureOutputDir(platform) {
|
|
177
|
+
const platformDir = path.join(OUTPUT_DIR, platform);
|
|
178
|
+
const modulesDir = path.join(platformDir, 'modules');
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(platformDir)) {
|
|
181
|
+
fs.mkdirSync(platformDir, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
if (!fs.existsSync(modulesDir)) {
|
|
184
|
+
fs.mkdirSync(modulesDir, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return platformDir;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 构建单个 bundle
|
|
192
|
+
*/
|
|
193
|
+
function buildBundle(entryFile, outputFile, platform, env = 'development', progressInfo = null) {
|
|
194
|
+
const isProduction = env === 'production';
|
|
195
|
+
const dev = !isProduction;
|
|
196
|
+
const startTime = Date.now();
|
|
197
|
+
|
|
198
|
+
// 显示进度信息
|
|
199
|
+
if (progressInfo) {
|
|
200
|
+
const { current, total, name } = progressInfo;
|
|
201
|
+
console.log(showProgress(current, total, name));
|
|
202
|
+
} else {
|
|
203
|
+
console.log(`${bold('Building')} ${cyan(formatPath(outputFile))} ${gray(`[${env}]`)}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const entry = path.relative(PROJECT_ROOT, entryFile);
|
|
207
|
+
const out = path.relative(PROJECT_ROOT, outputFile);
|
|
208
|
+
|
|
209
|
+
// ⭐ 关键:判断是否是模块 bundle(通过检查 entry 路径)
|
|
210
|
+
const isModuleBundle = entry.includes('src/modules/');
|
|
211
|
+
|
|
212
|
+
const commandParts = [
|
|
213
|
+
// 主 bundle 和模块 bundle 都设置 RN_MULTI_BUNDLE_BUILD,但使用不同的 RN_BUILD_TYPE
|
|
214
|
+
'RN_MULTI_BUNDLE_BUILD=true',
|
|
215
|
+
isModuleBundle ? 'RN_BUILD_TYPE=module' : 'RN_BUILD_TYPE=main',
|
|
216
|
+
'npx react-native bundle',
|
|
217
|
+
`--entry-file ${entry}`,
|
|
218
|
+
`--bundle-output ${out}`,
|
|
219
|
+
`--platform ${platform}`,
|
|
220
|
+
`--dev ${dev}`,
|
|
221
|
+
'--reset-cache',
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
// 生产环境添加 sourcemap 输出
|
|
225
|
+
if (isProduction) {
|
|
226
|
+
const sourcemapOutput = `${out}.map`;
|
|
227
|
+
commandParts.push(`--sourcemap-output ${sourcemapOutput}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const command = commandParts.join(' ');
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
execSync(command, {
|
|
234
|
+
cwd: PROJECT_ROOT,
|
|
235
|
+
stdio: 'inherit',
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const duration = Date.now() - startTime;
|
|
239
|
+
|
|
240
|
+
// 检查文件是否生成
|
|
241
|
+
if (fs.existsSync(outputFile)) {
|
|
242
|
+
// ⭐ 如果是模块 bundle,移除 prelude 代码
|
|
243
|
+
if (isModuleBundle) {
|
|
244
|
+
removePreludeFromBundle(outputFile);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const stats = fs.statSync(outputFile);
|
|
248
|
+
const relativePath = formatPath(outputFile);
|
|
249
|
+
let logMessage = ` ${green('✓')} ${bold(relativePath)} ${gray(`(${formatSize(stats.size)})`)}`;
|
|
250
|
+
|
|
251
|
+
// 检查 sourcemap 是否生成
|
|
252
|
+
let sourcemapSize = 0;
|
|
253
|
+
if (isProduction) {
|
|
254
|
+
const sourcemapFile = `${outputFile}.map`;
|
|
255
|
+
if (fs.existsSync(sourcemapFile)) {
|
|
256
|
+
const sourcemapStats = fs.statSync(sourcemapFile);
|
|
257
|
+
sourcemapSize = sourcemapStats.size;
|
|
258
|
+
logMessage += ` ${gray(`+ ${formatPath(sourcemapFile)} (${formatSize(sourcemapSize)})`)}`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
logMessage += ` ${gray(`[${formatDuration(duration)}]`)}`;
|
|
263
|
+
console.log(logMessage);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
file: relativePath,
|
|
268
|
+
size: stats.size,
|
|
269
|
+
sourcemapSize: sourcemapSize,
|
|
270
|
+
duration: duration,
|
|
271
|
+
};
|
|
272
|
+
} else {
|
|
273
|
+
throw new Error(`Bundle file not generated: ${outputFile}`);
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const duration = Date.now() - startTime;
|
|
277
|
+
console.error(` ${red('✗')} ${red('Failed')} ${formatPath(entryFile)} ${gray(`[${formatDuration(duration)}]`)}`);
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 生成 manifest
|
|
284
|
+
* 基于 multi-bundle.config.json 生成 bundle-manifest.json
|
|
285
|
+
*
|
|
286
|
+
* @param {Object} config - 从 multi-bundle.config.json 读取的配置
|
|
287
|
+
* @param {string} platform - 平台 ('ios' 或 'android')
|
|
288
|
+
* @returns {Object} 生成的 manifest 对象
|
|
289
|
+
*/
|
|
290
|
+
function generateManifest(config, platform) {
|
|
291
|
+
// 从 multi-bundle.config.json 读取 formatVersion
|
|
292
|
+
const mainFileName = getMainBundleFileName(platform);
|
|
293
|
+
const manifest = {
|
|
294
|
+
formatVersion: config.formatVersion || 1,
|
|
295
|
+
main: {
|
|
296
|
+
// 主 bundle 文件名根据平台自动生成
|
|
297
|
+
file: mainFileName,
|
|
298
|
+
// 版本号从 multi-bundle.config.json 读取
|
|
299
|
+
version: config.main.version,
|
|
300
|
+
},
|
|
301
|
+
// 模块列表从 multi-bundle.config.json 读取并转换
|
|
302
|
+
modules: config.modules.map((module) => ({
|
|
303
|
+
id: module.id, // 从 multi-bundle.config.json 读取
|
|
304
|
+
// bundle 文件名根据平台和模块 ID 自动生成
|
|
305
|
+
file: `modules/${getModuleBundleFileName(module.id, platform)}`,
|
|
306
|
+
version: module.version, // 从 multi-bundle.config.json 读取
|
|
307
|
+
dependencies: module.dependencies || [], // 从 multi-bundle.config.json 读取
|
|
308
|
+
lazy: module.lazy !== false, // 从 multi-bundle.config.json 读取
|
|
309
|
+
})),
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const manifestPath = path.join(OUTPUT_DIR, platform, 'bundle-manifest.json');
|
|
313
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
314
|
+
const relativePath = formatPath(manifestPath);
|
|
315
|
+
console.log(` ${green('✓')} ${bold('Generated')} ${cyan(relativePath)} ${gray('(from multi-bundle.config.json)')}`);
|
|
316
|
+
|
|
317
|
+
return manifest;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 打印构建摘要表格
|
|
322
|
+
*/
|
|
323
|
+
function printBuildSummary(results, platform, env, totalDuration) {
|
|
324
|
+
console.log('\n' + bold('Build Summary'));
|
|
325
|
+
console.log(gray('─'.repeat(80)));
|
|
326
|
+
|
|
327
|
+
// 表头
|
|
328
|
+
const header = [
|
|
329
|
+
'File'.padEnd(35),
|
|
330
|
+
'Size'.padEnd(12),
|
|
331
|
+
'Sourcemap'.padEnd(12),
|
|
332
|
+
'Time'.padEnd(10),
|
|
333
|
+
].join(' | ');
|
|
334
|
+
console.log(bold(header));
|
|
335
|
+
console.log(gray('─'.repeat(80)));
|
|
336
|
+
|
|
337
|
+
let totalSize = 0;
|
|
338
|
+
let totalSourcemapSize = 0;
|
|
339
|
+
|
|
340
|
+
// 表格内容
|
|
341
|
+
for (const result of results) {
|
|
342
|
+
if (result.success) {
|
|
343
|
+
const file = result.file.padEnd(35);
|
|
344
|
+
const size = formatSize(result.size).padEnd(12);
|
|
345
|
+
const sourcemap = result.sourcemapSize > 0
|
|
346
|
+
? formatSize(result.sourcemapSize).padEnd(12)
|
|
347
|
+
: gray('-').padEnd(12);
|
|
348
|
+
const time = formatDuration(result.duration).padEnd(10);
|
|
349
|
+
|
|
350
|
+
console.log(`${file} | ${size} | ${sourcemap} | ${time}`);
|
|
351
|
+
|
|
352
|
+
totalSize += result.size;
|
|
353
|
+
totalSourcemapSize += result.sourcemapSize;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log(gray('─'.repeat(80)));
|
|
358
|
+
|
|
359
|
+
// 总计行
|
|
360
|
+
const totalFile = bold('Total').padEnd(35);
|
|
361
|
+
const totalSizeStr = bold(formatSize(totalSize)).padEnd(12);
|
|
362
|
+
const totalSourcemapStr = totalSourcemapSize > 0
|
|
363
|
+
? bold(formatSize(totalSourcemapSize)).padEnd(12)
|
|
364
|
+
: gray('-').padEnd(12);
|
|
365
|
+
const totalTimeStr = bold(formatDuration(totalDuration)).padEnd(10);
|
|
366
|
+
|
|
367
|
+
console.log(`${totalFile} | ${totalSizeStr} | ${totalSourcemapStr} | ${totalTimeStr}`);
|
|
368
|
+
console.log(gray('─'.repeat(80)));
|
|
369
|
+
|
|
370
|
+
// 输出目录信息
|
|
371
|
+
const outputPath = formatPath(path.join(OUTPUT_DIR, platform));
|
|
372
|
+
console.log(`\n${bold('Output:')} ${cyan(outputPath)}`);
|
|
373
|
+
console.log(`${bold('Platform:')} ${blue(platform)} | ${bold('Environment:')} ${yellow(env)}\n`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* 主构建函数
|
|
378
|
+
*/
|
|
379
|
+
function buildMultiBundle(platform, env = 'development') {
|
|
380
|
+
const startTime = Date.now();
|
|
381
|
+
|
|
382
|
+
// 构建开始日志
|
|
383
|
+
console.log('\n' + bold('🚀 Building Multi-Bundle'));
|
|
384
|
+
console.log(`${bold('Platform:')} ${blue(platform)} | ${bold('Environment:')} ${yellow(env)}\n`);
|
|
385
|
+
|
|
386
|
+
const config = loadMultiBundleConfig();
|
|
387
|
+
const outputDir = ensureOutputDir(platform);
|
|
388
|
+
|
|
389
|
+
const results = [];
|
|
390
|
+
const totalBundles = 1 + config.modules.length; // main + modules
|
|
391
|
+
let currentIndex = 0;
|
|
392
|
+
|
|
393
|
+
// 1. 构建主 bundle
|
|
394
|
+
currentIndex++;
|
|
395
|
+
const mainEntry = path.join(PROJECT_ROOT, config.main.entry);
|
|
396
|
+
const mainFileName = getMainBundleFileName(platform);
|
|
397
|
+
const mainOutput = path.join(outputDir, mainFileName);
|
|
398
|
+
const mainResult = buildBundle(
|
|
399
|
+
mainEntry,
|
|
400
|
+
mainOutput,
|
|
401
|
+
platform,
|
|
402
|
+
env,
|
|
403
|
+
{ current: currentIndex, total: totalBundles, name: mainFileName }
|
|
404
|
+
);
|
|
405
|
+
results.push(mainResult);
|
|
406
|
+
|
|
407
|
+
// 2. 构建各个模块 bundle
|
|
408
|
+
for (const module of config.modules) {
|
|
409
|
+
currentIndex++;
|
|
410
|
+
const moduleEntry = path.join(PROJECT_ROOT, module.entry);
|
|
411
|
+
const moduleFileName = getModuleBundleFileName(module.id, platform);
|
|
412
|
+
const moduleOutput = path.join(outputDir, 'modules', moduleFileName);
|
|
413
|
+
const moduleResult = buildBundle(
|
|
414
|
+
moduleEntry,
|
|
415
|
+
moduleOutput,
|
|
416
|
+
platform,
|
|
417
|
+
env,
|
|
418
|
+
{ current: currentIndex, total: totalBundles, name: moduleFileName }
|
|
419
|
+
);
|
|
420
|
+
results.push(moduleResult);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 3. 生成 manifest
|
|
424
|
+
generateManifest(config, platform);
|
|
425
|
+
|
|
426
|
+
const totalDuration = Date.now() - startTime;
|
|
427
|
+
|
|
428
|
+
// 显示构建摘要
|
|
429
|
+
printBuildSummary(results, platform, env, totalDuration);
|
|
430
|
+
|
|
431
|
+
console.log(green(bold('✅ Build completed successfully!')) + '\n');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* CLI 入口
|
|
436
|
+
*/
|
|
437
|
+
function main() {
|
|
438
|
+
const args = process.argv.slice(2);
|
|
439
|
+
const platform = args[0] || 'ios';
|
|
440
|
+
|
|
441
|
+
// 解析环境参数
|
|
442
|
+
// ⭐ 默认使用 production 环境,打包命令应该默认是生产模式
|
|
443
|
+
let env = 'production';
|
|
444
|
+
const envIndex = args.indexOf('--env');
|
|
445
|
+
if (envIndex !== -1 && args[envIndex + 1]) {
|
|
446
|
+
env = args[envIndex + 1];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 验证平台参数
|
|
450
|
+
if (!['ios', 'android'].includes(platform)) {
|
|
451
|
+
console.error(`\n${red('✗')} ${red(bold('Invalid platform'))}`);
|
|
452
|
+
console.error(` ${gray('Use:')} ${cyan('ios')} ${gray('or')} ${cyan('android')}\n`);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 验证环境参数
|
|
457
|
+
const validEnvs = ['development', 'staging', 'production'];
|
|
458
|
+
if (!validEnvs.includes(env)) {
|
|
459
|
+
console.error(`\n${red('✗')} ${red(bold('Invalid environment'))}`);
|
|
460
|
+
console.error(` ${gray('Use one of:')} ${validEnvs.map(e => cyan(e)).join(', ')}\n`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
buildMultiBundle(platform, env);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
console.error(`\n${red('✗')} ${red(bold('Build failed'))}`);
|
|
468
|
+
if (error.message) {
|
|
469
|
+
console.error(` ${red(error.message)}`);
|
|
470
|
+
} else {
|
|
471
|
+
console.error(` ${red(String(error))}`);
|
|
472
|
+
}
|
|
473
|
+
console.error('');
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (require.main === module) {
|
|
479
|
+
main();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = { buildMultiBundle };
|
|
483
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# CodePush 发布脚本 - 开发环境配置
|
|
4
|
+
# 用法: ./scripts/release-codepush-dev.sh [参数]
|
|
5
|
+
# 示例: ./scripts/release-codepush-dev.sh --platform ios --version 1.0.0
|
|
6
|
+
|
|
7
|
+
# 获取脚本所在目录
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
10
|
+
|
|
11
|
+
# 切换到项目根目录
|
|
12
|
+
cd "$PROJECT_ROOT"
|
|
13
|
+
|
|
14
|
+
# ============================================
|
|
15
|
+
# 环境变量配置(开发环境默认值)
|
|
16
|
+
# ============================================
|
|
17
|
+
# 请根据实际情况修改以下环境变量
|
|
18
|
+
|
|
19
|
+
# CodePush 服务器地址(必填)
|
|
20
|
+
export CODE_PUSH_SERVER_URL="${CODE_PUSH_SERVER_URL:-https://code-push.bminfraaws-prod.com}"
|
|
21
|
+
|
|
22
|
+
# CodePush 访问密钥(必填)
|
|
23
|
+
export ACCESS_KEY="${ACCESS_KEY:-5u9un34l5A8fYIeHpPtwUEYc8QRg4ksvOXqog}"
|
|
24
|
+
|
|
25
|
+
# CodePush 应用名称(可选,可通过命令行参数覆盖)
|
|
26
|
+
export CODE_PUSH_APP="${CODE_PUSH_APP:-multiple-bundle-demo-android}"
|
|
27
|
+
|
|
28
|
+
# 部署环境(可选,可通过命令行参数覆盖)
|
|
29
|
+
# 可选值: Staging, Production
|
|
30
|
+
export CODE_PUSH_DEPLOYMENT="${CODE_PUSH_DEPLOYMENT:-Staging}"
|
|
31
|
+
|
|
32
|
+
# 平台(可选,可通过命令行参数覆盖)
|
|
33
|
+
# 可选值: ios, android
|
|
34
|
+
export CODE_PUSH_PLATFORM="${CODE_PUSH_PLATFORM:-android}"
|
|
35
|
+
|
|
36
|
+
# 原生应用版本号(可选,可通过命令行参数覆盖)
|
|
37
|
+
export CODE_PUSH_VERSION="${CODE_PUSH_VERSION:-1.0.0}"
|
|
38
|
+
|
|
39
|
+
# 发布描述(可选)
|
|
40
|
+
export CODE_PUSH_DESCRIPTION="${CODE_PUSH_DESCRIPTION:-Development release}"
|
|
41
|
+
|
|
42
|
+
# 是否强制更新(可选,默认 false)
|
|
43
|
+
# 可选值: true, false, 1, 0
|
|
44
|
+
export CODE_PUSH_MANDATORY="${CODE_PUSH_MANDATORY:-false}"
|
|
45
|
+
|
|
46
|
+
# 灰度百分比(可选,默认 100)
|
|
47
|
+
# 范围: 1-100
|
|
48
|
+
export CODE_PUSH_ROLLOUT="${CODE_PUSH_ROLLOUT:-0}"
|
|
49
|
+
|
|
50
|
+
# 是否禁用(可选,默认 false)
|
|
51
|
+
# 可选值: true, false, 1, 0
|
|
52
|
+
export CODE_PUSH_IS_DISABLED="${CODE_PUSH_IS_DISABLED:-false}"
|
|
53
|
+
|
|
54
|
+
# ============================================
|
|
55
|
+
# 执行发布脚本
|
|
56
|
+
# ============================================
|
|
57
|
+
|
|
58
|
+
echo "=========================================="
|
|
59
|
+
echo "CodePush Release - Development Environment"
|
|
60
|
+
echo "=========================================="
|
|
61
|
+
echo ""
|
|
62
|
+
echo "环境变量配置:"
|
|
63
|
+
echo " CODE_PUSH_SERVER_URL: $CODE_PUSH_SERVER_URL"
|
|
64
|
+
echo " CODE_PUSH_APP: $CODE_PUSH_APP"
|
|
65
|
+
echo " CODE_PUSH_DEPLOYMENT: $CODE_PUSH_DEPLOYMENT"
|
|
66
|
+
echo " CODE_PUSH_PLATFORM: $CODE_PUSH_PLATFORM"
|
|
67
|
+
echo " CODE_PUSH_VERSION: $CODE_PUSH_VERSION"
|
|
68
|
+
echo " CODE_PUSH_DESCRIPTION: $CODE_PUSH_DESCRIPTION"
|
|
69
|
+
echo " CODE_PUSH_MANDATORY: $CODE_PUSH_MANDATORY"
|
|
70
|
+
echo " CODE_PUSH_ROLLOUT: $CODE_PUSH_ROLLOUT"
|
|
71
|
+
echo " CODE_PUSH_IS_DISABLED: $CODE_PUSH_IS_DISABLED"
|
|
72
|
+
echo ""
|
|
73
|
+
echo "=========================================="
|
|
74
|
+
echo ""
|
|
75
|
+
|
|
76
|
+
# 检查 Node.js 是否安装
|
|
77
|
+
if ! command -v node &> /dev/null; then
|
|
78
|
+
echo "错误: 未找到 Node.js,请先安装 Node.js"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# 检查 release-codepush.js 是否存在
|
|
83
|
+
if [ ! -f "$SCRIPT_DIR/release-codepush.js" ]; then
|
|
84
|
+
echo "错误: 未找到 release-codepush.js"
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# 执行发布脚本,传递所有命令行参数
|
|
89
|
+
node "$SCRIPT_DIR/release-codepush.js" "$@"
|
|
90
|
+
|