react-native-update 10.37.0 → 10.37.2

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/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ### 优势
12
12
 
13
13
  1. 对中国用户使用阿里云高速 CDN 分发,对比其他服务器在国外的热更新服务,分发更稳定,更新成功率极高。对国外用户则智能分流至 cloudflare,同样享受高速更新服务。
14
- 2. 基于 bsdiff/hdiff 算法创建的**超小更新包**,通常版本迭代后在几十 KB 级别(其他全量热更新服务所需流量通常在几十 MB 级别)。
14
+ 2. 基于 bsdiff/hdiff 算法创建的**超小更新包**,通常版本迭代后在几十至几百 KB 级别(其他全量热更新服务所需流量通常在几十 MB 级别)。
15
15
  3. 始终跟进 RN 最新正式版本,第一时间提供支持。支持 hermes 字节码格式。支持新架构(注:安卓 0.73.0 ~ 0.76.0 的新架构因官方 bug 不支持,0.73 以下或 0.76.1 以上的新架构可用)。
16
16
  4. 跨越多个版本进行更新时,只需要下载**一个更新包**,不需要逐版本依次更新。
17
17
  5. 命令行工具 & 网页双端管理,版本发布过程简单便捷,完全可以集成 CI。
@@ -24,7 +24,7 @@
24
24
  | 对比维度 | react-native-update | expo-update | react-native-code-push |
25
25
  |---------|---------------------|-------------|------------------------|
26
26
  | **价格/成本** | 提供免费额度,多级梯度付费(最低约 66 元/月),流量不单独计费 | 提供免费额度,多级梯度付费(最低约 136 元/月),超出流量额外计费 | ❌ **已停运**(Microsoft App Center 已于 2025 年 3 月 31 日停止服务) |
27
- | **更新包大小** | ⭐⭐⭐⭐⭐ 几十 KB(增量更新) | ⭐⭐⭐ 全量更新(通常几十 MB) | ❌ **已停运** |
27
+ | **更新包大小** | ⭐⭐⭐⭐⭐ 几十至几百 KB(增量更新) | ⭐⭐⭐ 全量更新(通常几十 MB) | ❌ **已停运** |
28
28
  | **中国地区访问速度** | ⭐⭐⭐⭐⭐ 阿里云 CDN,速度极快 | ⭐⭐ 国外服务器,可能较慢 | ❌ **已停运** |
29
29
  | **iOS 支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
30
30
  | **Android 支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
@@ -134,13 +134,21 @@ public class UpdateModuleImpl {
134
134
  }
135
135
  }
136
136
 
137
- public static void reloadUpdate(final UpdateContext updateContext,final ReactApplicationContext mContext, final ReadableMap options, final Promise promise) {
137
+ public static void reloadUpdate(final UpdateContext updateContext, final ReactApplicationContext mContext, final ReadableMap options, final Promise promise) {
138
138
  final String hash = options.getString("hash");
139
+ restartApp(updateContext, mContext, hash, promise);
140
+ }
141
+
142
+
143
+ public static void restartApp(final UpdateContext updateContext, final ReactApplicationContext mContext, final String hash, final Promise promise) {
139
144
  UiThreadUtil.runOnUiThread(new Runnable() {
140
145
  @Override
141
146
  public void run() {
142
-
143
- updateContext.switchVersion(hash);
147
+ // 如果提供了 hash,则切换版本
148
+ if (hash != null && updateContext != null) {
149
+ updateContext.switchVersion(hash);
150
+ }
151
+
144
152
  final Context application = mContext.getApplicationContext();
145
153
  JSBundleLoader loader = JSBundleLoader.createFileLoader(UpdateContext.getBundleUrl(application));
146
154
  try {
@@ -166,6 +174,7 @@ public class UpdateModuleImpl {
166
174
  } catch (Throwable err) {
167
175
  final Activity currentActivity = mContext.getCurrentActivity();
168
176
  if (currentActivity == null) {
177
+ promise.reject(err);
169
178
  return;
170
179
  }
171
180
  try {
@@ -203,6 +212,7 @@ public class UpdateModuleImpl {
203
212
 
204
213
  // Invoke the reload method with a reason
205
214
  reloadMethod.invoke(reactHost, "react-native-update");
215
+ promise.resolve(true);
206
216
  } catch (Throwable e) {
207
217
  currentActivity.runOnUiThread(new Runnable() {
208
218
  @Override
@@ -210,42 +220,17 @@ public class UpdateModuleImpl {
210
220
  currentActivity.recreate();
211
221
  }
212
222
  });
223
+ promise.resolve(true);
213
224
  }
214
225
  }
215
- promise.resolve(true);
216
226
  }
217
227
  });
218
228
  }
219
229
 
230
+
220
231
  public static void restartApp(final ReactApplicationContext mContext, final Promise promise) {
221
- UiThreadUtil.runOnUiThread(new Runnable() {
222
- @Override
223
- public void run() {
224
- try {
225
- final Context application = mContext.getApplicationContext();
226
- ReactInstanceManager instanceManager = ((ReactApplication) application).getReactNativeHost().getReactInstanceManager();
227
-
228
- instanceManager.recreateReactContextInBackground();
229
- promise.resolve(true);
230
-
231
- } catch (Throwable err) {
232
- promise.reject("restartApp failed: "+err.getMessage());
233
- Log.e("pushy", "restartApp failed", err);
234
-
235
- final Activity currentActivity = mContext.getCurrentActivity();
236
- if (currentActivity == null) {
237
- return;
238
- }
239
- currentActivity.runOnUiThread(new Runnable() {
240
- @Override
241
- public void run() {
242
- currentActivity.recreate();
243
- }
244
- });
245
- }
246
- }
247
- });
248
- }
232
+ restartApp(null, mContext, null, promise);
233
+ }
249
234
 
250
235
  public static void setNeedUpdate(final UpdateContext updateContext, final ReadableMap options, final Promise promise) {
251
236
  final String hash = options.getString("hash");
@@ -320,3 +305,4 @@ public class UpdateModuleImpl {
320
305
  }
321
306
 
322
307
  }
308
+
@@ -104,27 +104,27 @@ public class UpdateModule extends NativePushySpec {
104
104
 
105
105
  @Override
106
106
  public void downloadPatchFromPpk(ReadableMap options, final Promise promise) {
107
- UpdateModuleImpl.downloadPatchFromPpk(updateContext,options,promise);
107
+ UpdateModuleImpl.downloadPatchFromPpk(updateContext, options, promise);
108
108
  }
109
109
 
110
110
  @Override
111
111
  public void reloadUpdate(ReadableMap options,Promise promise) {
112
- UpdateModuleImpl.reloadUpdate(updateContext, mContext, options,promise);
112
+ UpdateModuleImpl.reloadUpdate(updateContext, mContext, options, promise);
113
113
  }
114
114
 
115
115
  @Override
116
116
  public void restartApp(Promise promise) {
117
- UpdateModuleImpl.restartApp(mContext, promise);
117
+ UpdateModuleImpl.restartApp(updateContext, mContext, null, promise);
118
118
  }
119
119
 
120
120
  @Override
121
121
  public void setNeedUpdate(ReadableMap options,Promise promise) {
122
- UpdateModuleImpl.setNeedUpdate(updateContext, options,promise);
122
+ UpdateModuleImpl.setNeedUpdate(updateContext, options, promise);
123
123
  }
124
124
 
125
125
  @Override
126
126
  public void markSuccess(Promise promise) {
127
- UpdateModuleImpl.markSuccess(updateContext,promise);
127
+ UpdateModuleImpl.markSuccess(updateContext, promise);
128
128
  }
129
129
 
130
130
  @Override
@@ -4,36 +4,31 @@ import path from 'path';
4
4
  export function reactNativeUpdatePlugin() {
5
5
  return {
6
6
  pluginId: 'reactNativeUpdatePlugin',
7
- apply(node) {
8
- node.registerTask({
9
- name: 'reactNativeUpdatePlugin',
10
- run: () => {
11
- const cwd = process.cwd();
12
- const metaFilePath = path.resolve(
13
- cwd,
14
- 'entry/src/main/resources/rawfile/meta.json',
15
- );
16
- fs.mkdirSync(path.dirname(metaFilePath), { recursive: true });
7
+ apply(_node) {
8
+ const cwd = process.cwd();
9
+ const metaFilePath = path.resolve(
10
+ cwd,
11
+ 'entry/src/main/resources/rawfile/meta.json',
12
+ );
13
+ fs.mkdirSync(path.dirname(metaFilePath), { recursive: true });
17
14
 
18
- const moduleJsonPath = path.resolve(cwd, 'AppScope/app.json5');
19
- let versionName = '';
20
- if (fs.existsSync(moduleJsonPath)) {
21
- const content = fs.readFileSync(moduleJsonPath, 'utf-8');
22
- const match = content.match(
23
- /(?:"versionName"|versionName):\s*["']([^"']+)["']/,
24
- );
25
- versionName = match?.[1] || '';
26
- }
15
+ const moduleJsonPath = path.resolve(cwd, 'AppScope/app.json5');
16
+ let versionName = '';
17
+ if (fs.existsSync(moduleJsonPath)) {
18
+ const content = fs.readFileSync(moduleJsonPath, 'utf-8');
19
+ const match = content.match(
20
+ /(?:"versionName"|versionName):\s*["']([^"']+)["']/,
21
+ );
22
+ versionName = match?.[1] || '';
23
+ }
27
24
 
28
- const metaContent = {
29
- pushy_build_time: new Date().toISOString(),
30
- versionName,
31
- };
25
+ const metaContent = {
26
+ pushy_build_time: Date.now(),
27
+ versionName,
28
+ };
32
29
 
33
- fs.writeFileSync(metaFilePath, JSON.stringify(metaContent, null, 2));
34
- console.log(`Build time written to ${metaFilePath}`);
35
- },
36
- });
30
+ fs.writeFileSync(metaFilePath, JSON.stringify(metaContent, null, 2));
31
+ console.log(`Build time written to ${metaFilePath}`);
37
32
  },
38
33
  };
39
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.37.0",
3
+ "version": "10.37.2",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "scripts": {
@@ -0,0 +1,184 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const PROJECT_ROOT = path.resolve(__dirname, '../../..');
5
+ console.log(`[Bundle Metadata] Project root: ${PROJECT_ROOT}`);
6
+
7
+ function calculateContentHash(bundleCode) {
8
+ const hash = crypto.createHash('sha256');
9
+ hash.update(bundleCode, 'utf8');
10
+ return hash.digest('hex');
11
+ }
12
+
13
+ function generateMetadataInjection(contentHash) {
14
+ return `// Auto-injected bundle metadata by Metro plugin
15
+ var __BUNDLE_METADATA__ = {
16
+ contentHash: '${contentHash}'
17
+ };
18
+ `;
19
+ }
20
+
21
+ function generateMetadataComment(contentHash) {
22
+ return `\n//# BUNDLE_METADATA ${JSON.stringify({
23
+ contentHash
24
+ })}`;
25
+ }
26
+
27
+ function setupSingleHermesc(hermescPath, locationName) {
28
+ const hermescDir = path.dirname(hermescPath);
29
+ const backupHermescPath = path.join(hermescDir, '_hermesc');
30
+ const wrapperSourcePath = path.join(__dirname, 'hermesc-wrapper.js');
31
+
32
+ if (fs.existsSync(backupHermescPath)) {
33
+ console.log(`⏭️ [Hermesc Setup] ${locationName} already configured, skipping...`);
34
+ return true;
35
+ }
36
+
37
+ if (!fs.existsSync(hermescPath)) {
38
+ console.log(`ℹ️ [Hermesc Setup] ${locationName} hermesc not found at: ${hermescPath}`);
39
+ return false;
40
+ }
41
+
42
+ if (!fs.existsSync(wrapperSourcePath)) {
43
+ console.error(`❌ [Hermesc Setup] Wrapper script not found at: ${wrapperSourcePath}`);
44
+ return false;
45
+ }
46
+
47
+ try {
48
+ console.log(`🔧 [Hermesc Setup] Setting up hermesc wrapper for ${locationName}...`);
49
+
50
+ fs.renameSync(hermescPath, backupHermescPath);
51
+ console.log(`✅ [Hermesc Setup] ${locationName}: Renamed hermesc -> _hermesc`);
52
+
53
+ const shellScript = `#!/bin/bash
54
+ # Hermesc wrapper script - auto-generated
55
+ # This script calls the Node.js wrapper which handles post-processing
56
+
57
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
58
+ WRAPPER_SCRIPT="${wrapperSourcePath}"
59
+
60
+ find_node() {
61
+ if command -v node >/dev/null 2>&1; then
62
+ command -v node
63
+ return 0
64
+ fi
65
+
66
+ local NODE_PATHS=(
67
+ "/usr/local/bin/node"
68
+ "/opt/homebrew/bin/node"
69
+ "$HOME/.nvm/versions/node/$(ls -t "$HOME/.nvm/versions/node" 2>/dev/null | head -1)/bin/node"
70
+ "/usr/bin/node"
71
+ )
72
+
73
+ for node_path in "\${NODE_PATHS[@]}"; do
74
+ if [ -x "$node_path" ]; then
75
+ echo "$node_path"
76
+ return 0
77
+ fi
78
+ done
79
+
80
+ echo "Error: node executable not found" >&2
81
+ echo "Please ensure Node.js is installed and accessible" >&2
82
+ exit 1
83
+ }
84
+
85
+ NODE_BIN=$(find_node)
86
+ exec "$NODE_BIN" "$WRAPPER_SCRIPT" "$@"
87
+ `;
88
+
89
+ fs.writeFileSync(hermescPath, shellScript, { mode: 0o755 });
90
+ console.log(`✅ [Hermesc Setup] ${locationName}: Created hermesc wrapper shell script`);
91
+
92
+ console.log(`🎉 [Hermesc Setup] ${locationName} configured successfully!`);
93
+ console.log(`📋 [Hermesc Setup] ${locationName} details:`);
94
+ console.log(` - Original: ${backupHermescPath}`);
95
+ console.log(` - Wrapper: ${hermescPath}`);
96
+ console.log(` - Handler: ${wrapperSourcePath}`);
97
+
98
+ return true;
99
+ } catch (error) {
100
+ console.error(`❌ [Hermesc Setup] Failed to setup hermesc wrapper for ${locationName}:`, error);
101
+
102
+ if (fs.existsSync(backupHermescPath) && !fs.existsSync(hermescPath)) {
103
+ try {
104
+ fs.renameSync(backupHermescPath, hermescPath);
105
+ console.log(`🔄 [Hermesc Setup] ${locationName}: Rolled back changes`);
106
+ } catch (rollbackError) {
107
+ console.error(`❌ [Hermesc Setup] ${locationName}: Rollback failed:`, rollbackError);
108
+ }
109
+ }
110
+
111
+ return false;
112
+ }
113
+ }
114
+
115
+ function setupHermescWrapper() {
116
+ const wrapperSourcePath = path.join(__dirname, 'hermesc-wrapper.js');
117
+
118
+ if (!fs.existsSync(wrapperSourcePath)) {
119
+ console.error(`❌ [Hermesc Setup] Wrapper script not found at: ${wrapperSourcePath}`);
120
+ return;
121
+ }
122
+
123
+ try {
124
+ fs.chmodSync(wrapperSourcePath, 0o755);
125
+ } catch (error) {
126
+ console.error('❌ [Hermesc Setup] Failed to set execute permissions on wrapper:', error);
127
+ }
128
+
129
+ console.log('🔧 [Hermesc Setup] Starting hermesc wrapper setup...');
130
+
131
+ const hermescLocations = [
132
+ {
133
+ path: path.join(PROJECT_ROOT, 'node_modules/react-native/sdks/hermesc/osx-bin/hermesc'),
134
+ name: 'Node Modules'
135
+ },
136
+ {
137
+ path: path.join(PROJECT_ROOT, 'ios/Pods/hermes-engine/destroot/bin/hermesc'),
138
+ name: 'iOS Pods'
139
+ }
140
+ ];
141
+
142
+ let successCount = 0;
143
+ let totalProcessed = 0;
144
+
145
+ for (const location of hermescLocations) {
146
+ const success = setupSingleHermesc(location.path, location.name);
147
+ if (success) {
148
+ successCount++;
149
+ }
150
+ totalProcessed++;
151
+ }
152
+
153
+ console.log(`\n📊 [Hermesc Setup] Summary: ${successCount}/${totalProcessed} locations configured successfully`);
154
+ }
155
+
156
+ function metadataSerializer(entryPoint, preModules, graph, options) {
157
+ setupHermescWrapper();
158
+ const baseJSBundle = require('metro/src/DeltaBundler/Serializers/baseJSBundle');
159
+ const bundleToString = require('metro/src/lib/bundleToString');
160
+ const bundle = baseJSBundle(entryPoint, preModules, graph, options);
161
+ const { code: bundleCode } = bundleToString(bundle);
162
+ const contentHash = calculateContentHash(bundleCode);
163
+ const metadataInjection = generateMetadataInjection(contentHash);
164
+ const metadataComment = generateMetadataComment(contentHash);
165
+ const hashFilePath = path.join(PROJECT_ROOT, 'bundle-hash.json');
166
+
167
+ try {
168
+ const hashData = {
169
+ contentHash,
170
+ timestamp: new Date().toISOString(),
171
+ };
172
+ fs.writeFileSync(hashFilePath, JSON.stringify(hashData, null, 2));
173
+ console.log(`✅ [Metro] Saved hash to: ${hashFilePath}`);
174
+ console.log(`🔐 [Metro] Hash: ${contentHash.slice(0, 16)}...`);
175
+ } catch (error) {
176
+ console.error('❌ [Metro] Failed to save hash file:', error);
177
+ }
178
+
179
+ return bundleCode + metadataInjection + metadataComment;
180
+ }
181
+
182
+ module.exports = {
183
+ metadataSerializer,
184
+ };
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const PROJECT_ROOT = path.resolve(__dirname, '../../..');
7
+ const realHermescPath = path.join(PROJECT_ROOT, 'node_modules/react-native/sdks/hermesc/osx-bin/_hermesc');
8
+ const args = process.argv.slice(2);
9
+
10
+ console.log(`[Hermesc Wrapper] Executing Hermes compilation...`);
11
+ console.log(`[Hermesc Wrapper] Args:`, args.join(' '));
12
+
13
+ const isCompileOperation = args.includes('-emit-binary');
14
+ let outputFile = null;
15
+
16
+ const outIndex = args.indexOf('-out');
17
+ if (outIndex !== -1 && outIndex + 1 < args.length) {
18
+ outputFile = args[outIndex + 1];
19
+ }
20
+
21
+ const hermesc = spawn(realHermescPath, args, {
22
+ stdio: 'inherit',
23
+ env: process.env
24
+ });
25
+
26
+ hermesc.on('error', (error) => {
27
+ console.error(`[Hermesc Wrapper] ❌ Failed to start hermesc:`, error);
28
+ process.exit(1);
29
+ });
30
+
31
+ hermesc.on('close', (code) => {
32
+ console.log(`[Hermesc Wrapper] Hermes compilation completed with code: ${code}`);
33
+
34
+ if (code === 0 && isCompileOperation && outputFile) {
35
+ console.log(`[Hermesc Wrapper] 🔄 Post-processing HBC file: ${outputFile}`);
36
+
37
+ setTimeout(() => {
38
+ processHBCFile(outputFile);
39
+ }, 500);
40
+ } else {
41
+ process.exit(code);
42
+ }
43
+ });
44
+
45
+ function processHBCFile(hbcFilePath) {
46
+ const hashFilePath = path.join(PROJECT_ROOT, 'bundle-hash.json');
47
+
48
+ if (!fs.existsSync(hashFilePath)) {
49
+ console.warn(`[Hermesc Wrapper] ⚠️ Hash file not found: ${hashFilePath}`);
50
+ console.warn(`[Hermesc Wrapper] Skipping metadata injection.`);
51
+ process.exit(0);
52
+ return;
53
+ }
54
+
55
+ if (!fs.existsSync(hbcFilePath)) {
56
+ console.warn(`[Hermesc Wrapper] ⚠️ HBC file not found: ${hbcFilePath}`);
57
+ console.warn(`[Hermesc Wrapper] Skipping metadata injection.`);
58
+ process.exit(0);
59
+ return;
60
+ }
61
+
62
+ try {
63
+ const hashData = JSON.parse(fs.readFileSync(hashFilePath, 'utf8'));
64
+ const { contentHash } = hashData;
65
+
66
+ console.log(`[Hermesc Wrapper] 📝 Injecting metadata into HBC...`);
67
+ console.log(`[Hermesc Wrapper] Hash: ${contentHash.slice(0, 16)}...`);
68
+
69
+ const hbcBuffer = fs.readFileSync(hbcFilePath);
70
+
71
+ const metadata = { contentHash };
72
+ const metadataJson = JSON.stringify(metadata);
73
+
74
+ const MAGIC = Buffer.from('RNUPDATE', 'utf8');
75
+ const jsonBuffer = Buffer.from(metadataJson, 'utf8');
76
+ const lengthBuffer = Buffer.alloc(4);
77
+ lengthBuffer.writeUInt32LE(jsonBuffer.length);
78
+
79
+ const finalBuffer = Buffer.concat([
80
+ hbcBuffer,
81
+ MAGIC,
82
+ jsonBuffer,
83
+ lengthBuffer,
84
+ MAGIC,
85
+ ]);
86
+
87
+ fs.writeFileSync(hbcFilePath, finalBuffer);
88
+
89
+ console.log(`[Hermesc Wrapper] ✅ Successfully injected metadata into: ${hbcFilePath}`);
90
+ console.log(`[Hermesc Wrapper] 🧹 Cleaning up hash file...`);
91
+
92
+ process.exit(0);
93
+ } catch (error) {
94
+ console.error(`[Hermesc Wrapper] ❌ Failed to process HBC file:`, error);
95
+ process.exit(1);
96
+ }
97
+ }