react-native-update 10.37.13 → 10.37.16
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-CN.md +72 -0
- package/README.md +61 -49
- package/android/build.gradle +47 -17
- package/bunfig.toml +2 -0
- package/error.js +1609 -0
- package/harmony/har-wrapper/AppScope/app.json5 +8 -0
- package/harmony/har-wrapper/build-profile.json5 +35 -0
- package/harmony/har-wrapper/hvigor/hvigor-config.json5 +5 -0
- package/harmony/har-wrapper/hvigorfile.ts +6 -0
- package/harmony/har-wrapper/oh-package.json5 +4 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-77b153ce45aba0ed28ef.json +1415 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-b65a07793384e0ce3e08.json +809 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-ce0e89410afd8bf3a057.json +60 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json +14 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-03-16T14-00-08-0134.json +89 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-rnupdate-Release-267153624504c9c3ffdd.json +222 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.ninja_deps +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.ninja_log +8 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeCache.txt +415 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake +74 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake +85 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake +15 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c +880 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +869 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeConfigureLog.yaml +388 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt +3 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache +1 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/HDiffPatch/file_for_patch.c.o +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/HDiffPatch/libHDiffPatch/HPatch/patch.c.o +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/hpatch.c.o +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/lzma/C/Lzma2Dec.c.o +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/lzma/C/LzmaDec.c.o +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/pushy.c.o +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rules.ninja +64 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/additional_project_files.txt +0 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/build.ninja +206 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/build_file_index.txt +1 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/cmake_install.cmake +54 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/compile_commands.json +38 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/configure_fingerprint.json +1 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/hvigor_native_config.json +1 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/metadata_generation_command.txt +17 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/native_work_dir.txt +1 -0
- package/harmony/pushy/.cxx/default/default/release/arm64-v8a/output.log +14 -0
- package/harmony/pushy/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake +0 -0
- package/harmony/pushy/BuildProfile.ets +3 -5
- package/harmony/pushy/build-profile.json5 +8 -1
- package/harmony/pushy/oh-package-lock.json5 +1 -1
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +42 -30
- package/harmony/pushy/src/main/ets/DownloadTask.ts +145 -106
- package/harmony/pushy/src/main/ets/Logger.ts +24 -7
- package/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +1 -1
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +9 -73
- package/harmony/pushy/src/main/ets/UpdateContext.ts +206 -70
- package/harmony/pushy.har +0 -0
- package/package.json +9 -6
- package/scripts/build-harmony-har.js +427 -0
- package/scripts/prepublish.ts +113 -0
- package/scripts/read.js +188 -0
- package/src/__tests__/core.test.ts +103 -0
- package/src/__tests__/setup.ts +37 -0
- package/src/__tests__/utils.test.ts +36 -0
- package/src/client.ts +17 -10
- package/src/core.ts +5 -5
- package/src/locales/en.ts +1 -0
- package/src/locales/zh.ts +1 -0
- package/src/utils.ts +13 -1
package/scripts/read.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 从 Hermes 字节码文件中读取 metadata
|
|
5
|
+
* 支持两种方式:
|
|
6
|
+
* 1. 从文件末尾的自定义 Meta 块读取(新方式,推荐)
|
|
7
|
+
* 2. 从 Hermes -meta 参数注入的数据读取(旧方式,备用)
|
|
8
|
+
*/
|
|
9
|
+
function readBundleMetadata(filePath) {
|
|
10
|
+
const buffer = fs.readFileSync(filePath);
|
|
11
|
+
|
|
12
|
+
// 优先尝试从文件末尾的自定义 Meta 块读取
|
|
13
|
+
const metaFromFooter = readMetadataFromHBCFooter(buffer);
|
|
14
|
+
if (metaFromFooter) {
|
|
15
|
+
return metaFromFooter;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 如果末尾没有 Meta 块,尝试从 Hermes -meta 参数注入的数据读取(备用方案)
|
|
19
|
+
console.log('No metadata footer found, trying to read from Hermes -meta parameter...');
|
|
20
|
+
return readMetadataFromHermesMeta(buffer);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 从 HBC 文件末尾的自定义 Meta 块读取元数据
|
|
25
|
+
* 格式: [原始HBC][MAGIC_START][LENGTH][JSON_DATA][MAGIC_END]
|
|
26
|
+
*/
|
|
27
|
+
function readMetadataFromHBCFooter(buffer) {
|
|
28
|
+
const MAGIC = Buffer.from('RNUPDATE', 'utf8'); // 8 bytes
|
|
29
|
+
const MAGIC_SIZE = 8;
|
|
30
|
+
const LENGTH_SIZE = 4;
|
|
31
|
+
|
|
32
|
+
console.log(`\n[DEBUG] Reading metadata from HBC footer...`);
|
|
33
|
+
console.log(`[DEBUG] File size: ${buffer.length} bytes`);
|
|
34
|
+
console.log(`[DEBUG] Last 100 bytes (hex): ${buffer.slice(-100).toString('hex')}`);
|
|
35
|
+
console.log(`[DEBUG] Last 50 bytes (utf8): ${buffer.slice(-50).toString('utf8', 0, 50).replace(/[^\x20-\x7E]/g, '.')}`);
|
|
36
|
+
|
|
37
|
+
// 检查文件是否足够大以包含 meta 块
|
|
38
|
+
// 最小大小: MAGIC_START(8) + LENGTH(4) + JSON(至少2字节"{}") + MAGIC_END(8) = 22 bytes
|
|
39
|
+
if (buffer.length < 22) {
|
|
40
|
+
console.log(`[DEBUG] File too small: ${buffer.length} < 22`);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 从文件末尾向前查找最后一个 MAGIC_END
|
|
45
|
+
const lastMagicIndex = buffer.lastIndexOf(MAGIC);
|
|
46
|
+
|
|
47
|
+
console.log(`[DEBUG] MAGIC string: "${MAGIC.toString('utf8')}"`);
|
|
48
|
+
console.log(`[DEBUG] Last MAGIC index: ${lastMagicIndex}`);
|
|
49
|
+
|
|
50
|
+
if (lastMagicIndex === -1) {
|
|
51
|
+
console.log(`[DEBUG] MAGIC not found in file`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// MAGIC_END 应该在文件末尾
|
|
56
|
+
console.log(`[DEBUG] Expected MAGIC_END at: ${buffer.length - MAGIC_SIZE}`);
|
|
57
|
+
console.log(`[DEBUG] Found MAGIC_END at: ${lastMagicIndex}`);
|
|
58
|
+
|
|
59
|
+
if (lastMagicIndex + MAGIC_SIZE !== buffer.length) {
|
|
60
|
+
console.warn(`⚠️ Found MAGIC but not at file end (expected ${buffer.length - MAGIC_SIZE}, found ${lastMagicIndex})`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 计算 MAGIC_START 的位置
|
|
65
|
+
// 从 MAGIC_END 向前:MAGIC_END(8) + JSON(?) + LENGTH(4) + MAGIC_START(8)
|
|
66
|
+
const magicEndStart = lastMagicIndex;
|
|
67
|
+
|
|
68
|
+
// 读取长度字段(在 MAGIC_END 之前的 4 字节)
|
|
69
|
+
const lengthStart = magicEndStart - LENGTH_SIZE;
|
|
70
|
+
if (lengthStart < MAGIC_SIZE) {
|
|
71
|
+
console.log(`[DEBUG] lengthStart too small: ${lengthStart} < ${MAGIC_SIZE}`);
|
|
72
|
+
return null; // 文件太小
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const jsonLength = buffer.readUInt32LE(lengthStart);
|
|
76
|
+
console.log(`[DEBUG] JSON length from buffer: ${jsonLength}`);
|
|
77
|
+
|
|
78
|
+
// 验证长度是否合理(JSON 数据应该小于 10KB)
|
|
79
|
+
if (jsonLength > 10240 || jsonLength < 2) {
|
|
80
|
+
console.warn(`⚠️ Invalid JSON length: ${jsonLength}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 计算 JSON 数据的起始位置
|
|
85
|
+
const jsonStart = lengthStart - jsonLength;
|
|
86
|
+
console.log(`[DEBUG] JSON start position: ${jsonStart}`);
|
|
87
|
+
if (jsonStart < MAGIC_SIZE) {
|
|
88
|
+
console.log(`[DEBUG] jsonStart too small: ${jsonStart} < ${MAGIC_SIZE}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 验证 MAGIC_START
|
|
93
|
+
const magicStartPos = jsonStart - MAGIC_SIZE;
|
|
94
|
+
console.log(`[DEBUG] MAGIC_START position: ${magicStartPos}`);
|
|
95
|
+
const magicStart = buffer.slice(magicStartPos, jsonStart);
|
|
96
|
+
console.log(`[DEBUG] MAGIC_START bytes: ${magicStart.toString('hex')}`);
|
|
97
|
+
console.log(`[DEBUG] Expected MAGIC bytes: ${MAGIC.toString('hex')}`);
|
|
98
|
+
|
|
99
|
+
if (!magicStart.equals(MAGIC)) {
|
|
100
|
+
console.warn('⚠️ MAGIC_START mismatch');
|
|
101
|
+
console.warn(` Expected: ${MAGIC.toString('hex')}`);
|
|
102
|
+
console.warn(` Got: ${magicStart.toString('hex')}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 读取 JSON 数据
|
|
107
|
+
try {
|
|
108
|
+
const jsonBuffer = buffer.slice(jsonStart, lengthStart);
|
|
109
|
+
const jsonString = jsonBuffer.toString('utf8');
|
|
110
|
+
console.log(`[DEBUG] JSON string: ${jsonString}`);
|
|
111
|
+
const metadata = JSON.parse(jsonString);
|
|
112
|
+
|
|
113
|
+
console.log(`✅ Found metadata in HBC footer: contentHash=${metadata.contentHash?.slice(0, 16)}...`);
|
|
114
|
+
console.log(` Metadata: ${JSON.stringify(metadata, null, 2)}`);
|
|
115
|
+
|
|
116
|
+
return metadata;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('❌ Failed to parse metadata JSON:', error);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 从 Hermes -meta 参数注入的数据读取(备用方案)
|
|
125
|
+
*/
|
|
126
|
+
function readMetadataFromHermesMeta(buffer) {
|
|
127
|
+
const metadata = {};
|
|
128
|
+
const searchPattern = Buffer.from('contentHash=', 'utf8');
|
|
129
|
+
|
|
130
|
+
// 在整个文件中查找模式
|
|
131
|
+
let index = buffer.indexOf(searchPattern);
|
|
132
|
+
|
|
133
|
+
if (index !== -1) {
|
|
134
|
+
// 找到了 "contentHash=",读取后面的 hash 值
|
|
135
|
+
const hashStart = index + searchPattern.length;
|
|
136
|
+
|
|
137
|
+
// Hash 应该是 64 个十六进制字符 (SHA256)
|
|
138
|
+
let hashEnd = hashStart;
|
|
139
|
+
while (hashEnd < buffer.length && hashEnd < hashStart + 64) {
|
|
140
|
+
const byte = buffer[hashEnd];
|
|
141
|
+
// 检查是否是有效的十六进制字符 (0-9, a-f, A-F)
|
|
142
|
+
if ((byte >= 48 && byte <= 57) || // 0-9
|
|
143
|
+
(byte >= 97 && byte <= 102) || // a-f
|
|
144
|
+
(byte >= 65 && byte <= 70)) { // A-F
|
|
145
|
+
hashEnd++;
|
|
146
|
+
} else {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (hashEnd > hashStart) {
|
|
152
|
+
metadata.contentHash = buffer.toString('utf8', hashStart, hashEnd);
|
|
153
|
+
console.log(`✅ Found contentHash in Hermes -meta: ${metadata.contentHash.slice(0, 16)}...`);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
console.warn('⚠️ contentHash not found in Hermes -meta parameter');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return metadata;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 从普通 JS bundle 的注释中读取 metadata
|
|
164
|
+
*/
|
|
165
|
+
function readMetadataFromJSBundle(buffer) {
|
|
166
|
+
const content = buffer.toString('utf8');
|
|
167
|
+
const metadata = {};
|
|
168
|
+
|
|
169
|
+
// 查找 //# BUNDLE_METADATA {...} 注释
|
|
170
|
+
const metaMatch = content.match(/\/\/# BUNDLE_METADATA\s+(\{[^}]+\})/);
|
|
171
|
+
if (metaMatch) {
|
|
172
|
+
try {
|
|
173
|
+
const metaObj = JSON.parse(metaMatch[1]);
|
|
174
|
+
metadata.contentHash = metaObj.contentHash;
|
|
175
|
+
console.log(`✅ Found contentHash in JS bundle comment: ${metadata.contentHash.slice(0, 16)}...`);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error('Failed to parse BUNDLE_METADATA comment:', e);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
console.warn('⚠️ BUNDLE_METADATA comment not found in JS bundle');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return metadata;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 测试读取
|
|
187
|
+
const metadata = readBundleMetadata('./index.bundlejs');
|
|
188
|
+
console.log('Final metadata:', metadata);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, test, mock } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
// In Bun, top-level imports are cached.
|
|
4
|
+
// We can use mock.module to change the implementation of a module,
|
|
5
|
+
// but if a module has already been executed (like core.ts),
|
|
6
|
+
// re-importing it might not re-run the top-level code unless we use some tricks
|
|
7
|
+
// or run tests in isolation.
|
|
8
|
+
// Actually, bun test runs each file in its own environment usually,
|
|
9
|
+
// BUT if we run multiple test files in one process, they might share the cache.
|
|
10
|
+
const importFreshCore = (cacheKey: string) => import(`../core?${cacheKey}`);
|
|
11
|
+
|
|
12
|
+
describe('core info parsing', () => {
|
|
13
|
+
test('should call error when currentVersionInfo is invalid JSON', async () => {
|
|
14
|
+
const mockError = mock(() => {});
|
|
15
|
+
|
|
16
|
+
mock.module('react-native', () => ({
|
|
17
|
+
Platform: {
|
|
18
|
+
OS: 'ios',
|
|
19
|
+
Version: 13,
|
|
20
|
+
},
|
|
21
|
+
NativeModules: {
|
|
22
|
+
Pushy: {
|
|
23
|
+
currentVersionInfo: '{invalid}',
|
|
24
|
+
downloadRootDir: '/tmp',
|
|
25
|
+
packageVersion: '1.0.0',
|
|
26
|
+
currentVersion: 'hash1',
|
|
27
|
+
isFirstTime: false,
|
|
28
|
+
rolledBackVersion: '',
|
|
29
|
+
buildTime: '2023-01-01',
|
|
30
|
+
uuid: 'existing-uuid',
|
|
31
|
+
setLocalHashInfo: mock(() => {}),
|
|
32
|
+
getLocalHashInfo: mock(() => Promise.resolve('{}')),
|
|
33
|
+
setUuid: mock(() => {}),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
NativeEventEmitter: class {
|
|
37
|
+
addListener = mock(() => ({ remove: mock(() => {}) }));
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
mock.module('react-native/Libraries/Core/ReactNativeVersion', () => ({
|
|
42
|
+
version: { major: 0, minor: 73, patch: 0 },
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module('nanoid/non-secure', () => ({
|
|
46
|
+
nanoid: () => 'mock-uuid',
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
mock.module('../utils', () => ({
|
|
50
|
+
error: mockError,
|
|
51
|
+
log: mock(() => {}),
|
|
52
|
+
emptyModule: {},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Use a unique query parameter to bypass cache if supported, or just rely on fresh environment per file.
|
|
56
|
+
// In Bun, you can sometimes use a cache buster if it's dynamic import.
|
|
57
|
+
await importFreshCore('error');
|
|
58
|
+
|
|
59
|
+
expect(mockError).toHaveBeenCalledWith(
|
|
60
|
+
expect.stringContaining('error_parse_version_info')
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('should not call error when currentVersionInfo is valid JSON', async () => {
|
|
65
|
+
const mockError = mock(() => {});
|
|
66
|
+
const mockSetLocalHashInfo = mock(() => {});
|
|
67
|
+
|
|
68
|
+
mock.module('react-native', () => ({
|
|
69
|
+
Platform: {
|
|
70
|
+
OS: 'ios',
|
|
71
|
+
Version: 13,
|
|
72
|
+
},
|
|
73
|
+
NativeModules: {
|
|
74
|
+
Pushy: {
|
|
75
|
+
currentVersionInfo: JSON.stringify({ name: 'v1', debugChannel: true }),
|
|
76
|
+
downloadRootDir: '/tmp',
|
|
77
|
+
packageVersion: '1.0.0',
|
|
78
|
+
currentVersion: 'hash1',
|
|
79
|
+
isFirstTime: false,
|
|
80
|
+
rolledBackVersion: '',
|
|
81
|
+
buildTime: '2023-01-01',
|
|
82
|
+
uuid: 'existing-uuid',
|
|
83
|
+
setLocalHashInfo: mockSetLocalHashInfo,
|
|
84
|
+
getLocalHashInfo: mock(() => Promise.resolve('{}')),
|
|
85
|
+
setUuid: mock(() => {}),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
NativeEventEmitter: class {
|
|
89
|
+
addListener = mock(() => ({ remove: mock(() => {}) }));
|
|
90
|
+
},
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
mock.module('../utils', () => ({
|
|
94
|
+
error: mockError,
|
|
95
|
+
log: mock(() => {}),
|
|
96
|
+
emptyModule: {},
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
await importFreshCore('success');
|
|
100
|
+
|
|
101
|
+
expect(mockError).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mock } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
mock.module('react-native', () => {
|
|
4
|
+
return {
|
|
5
|
+
Platform: {
|
|
6
|
+
OS: 'ios',
|
|
7
|
+
Version: 13,
|
|
8
|
+
},
|
|
9
|
+
NativeModules: {
|
|
10
|
+
Pushy: {
|
|
11
|
+
currentVersionInfo: '{}',
|
|
12
|
+
downloadRootDir: '/tmp',
|
|
13
|
+
packageVersion: '1.0.0',
|
|
14
|
+
currentVersion: 'hash',
|
|
15
|
+
isFirstTime: false,
|
|
16
|
+
rolledBackVersion: '',
|
|
17
|
+
buildTime: '2023-01-01',
|
|
18
|
+
uuid: 'uuid',
|
|
19
|
+
setLocalHashInfo: () => {},
|
|
20
|
+
getLocalHashInfo: () => Promise.resolve('{}'),
|
|
21
|
+
setUuid: () => {},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
NativeEventEmitter: class {
|
|
25
|
+
addListener = () => ({ remove: () => {} });
|
|
26
|
+
removeAllListeners = () => {};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
mock.module('../i18n', () => {
|
|
32
|
+
return {
|
|
33
|
+
default: {
|
|
34
|
+
t: (key: string, params?: any) => `${key}${params ? JSON.stringify(params) : ''}`,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test, mock } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
mock.module('react-native', () => {
|
|
4
|
+
return {
|
|
5
|
+
Platform: {
|
|
6
|
+
OS: 'ios',
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
mock.module('../i18n', () => {
|
|
12
|
+
return {
|
|
13
|
+
default: {
|
|
14
|
+
t: (key: string) => key,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
import { joinUrls } from '../utils';
|
|
20
|
+
|
|
21
|
+
describe('joinUrls', () => {
|
|
22
|
+
test('returns undefined when fileName is not provided', () => {
|
|
23
|
+
expect(joinUrls(['example.com'])).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('returns an empty array when paths is empty', () => {
|
|
27
|
+
expect(joinUrls([], 'file.txt')).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('maps over paths and prepends https:// with fileName', () => {
|
|
31
|
+
expect(joinUrls(['example.com', 'test.org'], 'file.txt')).toEqual([
|
|
32
|
+
'https://example.com/file.txt',
|
|
33
|
+
'https://test.org/file.txt',
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
assertWeb,
|
|
23
23
|
emptyObj,
|
|
24
24
|
enhancedFetch,
|
|
25
|
+
info,
|
|
25
26
|
joinUrls,
|
|
26
27
|
log,
|
|
27
28
|
noop,
|
|
@@ -189,7 +190,7 @@ export class Pushy {
|
|
|
189
190
|
};
|
|
190
191
|
assertDebug = (matter: string) => {
|
|
191
192
|
if (__DEV__ && !this.options.debug) {
|
|
192
|
-
|
|
193
|
+
info(this.t('dev_debug_disabled', { matter }));
|
|
193
194
|
return false;
|
|
194
195
|
}
|
|
195
196
|
return true;
|
|
@@ -323,9 +324,10 @@ export class Pushy {
|
|
|
323
324
|
this.throwIfEnabled(Error('errorChecking: ' + errorMessage));
|
|
324
325
|
return this.lastRespJson ? await this.lastRespJson : emptyObj;
|
|
325
326
|
}
|
|
326
|
-
|
|
327
|
+
const respJsonPromise = resp.json() as Promise<CheckResult>;
|
|
328
|
+
this.lastRespJson = respJsonPromise;
|
|
327
329
|
|
|
328
|
-
const result: CheckResult = await
|
|
330
|
+
const result: CheckResult = await respJsonPromise;
|
|
329
331
|
|
|
330
332
|
log('checking result:', result);
|
|
331
333
|
|
|
@@ -344,9 +346,14 @@ export class Pushy {
|
|
|
344
346
|
const remoteEndpoints = await resp.json();
|
|
345
347
|
log('fetch endpoints:', remoteEndpoints);
|
|
346
348
|
if (Array.isArray(remoteEndpoints)) {
|
|
347
|
-
server.backups
|
|
348
|
-
|
|
349
|
-
)
|
|
349
|
+
const backups = server.backups || [];
|
|
350
|
+
const set = new Set(backups);
|
|
351
|
+
for (const endpoint of remoteEndpoints) {
|
|
352
|
+
set.add(endpoint);
|
|
353
|
+
}
|
|
354
|
+
if (set.size !== backups.length) {
|
|
355
|
+
server.backups = Array.from(set);
|
|
356
|
+
}
|
|
350
357
|
}
|
|
351
358
|
} catch (e: any) {
|
|
352
359
|
log('failed to fetch endpoints from: ', server.queryUrls);
|
|
@@ -355,7 +362,7 @@ export class Pushy {
|
|
|
355
362
|
return server.backups;
|
|
356
363
|
};
|
|
357
364
|
downloadUpdate = async (
|
|
358
|
-
|
|
365
|
+
updateInfo: CheckResult,
|
|
359
366
|
onDownloadProgress?: (data: ProgressData) => void,
|
|
360
367
|
) => {
|
|
361
368
|
const {
|
|
@@ -367,15 +374,15 @@ export class Pushy {
|
|
|
367
374
|
name,
|
|
368
375
|
description = '',
|
|
369
376
|
metaInfo,
|
|
370
|
-
} =
|
|
377
|
+
} = updateInfo;
|
|
371
378
|
if (
|
|
372
379
|
this.options.beforeDownloadUpdate &&
|
|
373
|
-
(await this.options.beforeDownloadUpdate(
|
|
380
|
+
(await this.options.beforeDownloadUpdate(updateInfo)) === false
|
|
374
381
|
) {
|
|
375
382
|
log('beforeDownloadUpdate returned false, skipping download');
|
|
376
383
|
return;
|
|
377
384
|
}
|
|
378
|
-
if (!
|
|
385
|
+
if (!updateInfo.update || !hash) {
|
|
379
386
|
return;
|
|
380
387
|
}
|
|
381
388
|
if (rolledBackVersion === hash) {
|
package/src/core.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
|
2
|
-
import { emptyModule, log } from './utils';
|
|
2
|
+
import { emptyModule, error, log } from './utils';
|
|
3
|
+
import i18n from './i18n';
|
|
3
4
|
const {
|
|
4
5
|
version: v,
|
|
5
6
|
} = require('react-native/Libraries/Core/ReactNativeVersion');
|
|
@@ -46,10 +47,9 @@ if (currentVersionInfoString) {
|
|
|
46
47
|
delete _currentVersionInfo.debugChannel;
|
|
47
48
|
setLocalHashInfo(currentVersion, _currentVersionInfo);
|
|
48
49
|
}
|
|
49
|
-
} catch (
|
|
50
|
-
|
|
51
|
-
'
|
|
52
|
-
currentVersionInfoString,
|
|
50
|
+
} catch (err) {
|
|
51
|
+
error(
|
|
52
|
+
i18n.t('error_parse_version_info', { info: currentVersionInfoString }),
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/locales/en.ts
CHANGED
|
@@ -44,6 +44,7 @@ export default {
|
|
|
44
44
|
|
|
45
45
|
// Error messages
|
|
46
46
|
error_appkey_required: 'appKey is required',
|
|
47
|
+
error_parse_version_info: 'Failed to parse currentVersionInfo: {{info}}',
|
|
47
48
|
error_update_check_failed: 'Update check failed',
|
|
48
49
|
error_cannot_connect_server:
|
|
49
50
|
'Can not connect to update server. Please check your network.',
|
package/src/locales/zh.ts
CHANGED
|
@@ -42,6 +42,7 @@ export default {
|
|
|
42
42
|
|
|
43
43
|
// Error messages
|
|
44
44
|
error_appkey_required: '需要提供 appKey',
|
|
45
|
+
error_parse_version_info: '解析 currentVersionInfo 失败: {{info}}',
|
|
45
46
|
error_update_check_failed: '更新检查失败',
|
|
46
47
|
error_cannot_connect_server: '无法连接到更新服务器。请检查网络连接。',
|
|
47
48
|
error_cannot_connect_backup:
|
package/src/utils.ts
CHANGED
|
@@ -5,6 +5,18 @@ export function log(...args: any[]) {
|
|
|
5
5
|
console.log(i18n.t('dev_log_prefix'), ...args);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export function info(...args: any[]) {
|
|
9
|
+
console.info(i18n.t('dev_log_prefix'), ...args);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function warn(...args: any[]) {
|
|
13
|
+
console.warn(i18n.t('dev_log_prefix'), ...args);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function error(...args: any[]) {
|
|
17
|
+
console.error(i18n.t('dev_log_prefix'), ...args);
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
export const isWeb = Platform.OS === 'web';
|
|
9
21
|
|
|
10
22
|
export function promiseAny<T>(promises: Promise<T>[]) {
|
|
@@ -93,7 +105,7 @@ export const testUrls = async (urls?: string[]) => {
|
|
|
93
105
|
|
|
94
106
|
export const assertWeb = () => {
|
|
95
107
|
if (isWeb) {
|
|
96
|
-
|
|
108
|
+
warn(i18n.t('dev_web_not_supported'));
|
|
97
109
|
return false;
|
|
98
110
|
}
|
|
99
111
|
return true;
|