react-native-update 10.40.0-beta.2 → 10.40.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/.claude/settings.local.json +8 -0
- package/README-CN.md +0 -12
- package/README.md +1 -30
- package/android/jni/Application.mk +1 -0
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/BundledResourceCopier.java +4 -29
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +0 -30
- package/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +0 -6
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +0 -4
- package/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +0 -4
- package/android/src/main/java/expo/modules/pushy/ExpoPushyPackage.java +1 -4
- package/cpp/patch_core/patch_core.cpp +14 -9
- package/cpp/patch_core/tests/patch_core_test.cpp +72 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +49 -45
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +27 -27
- package/harmony/pushy/src/test/DownloadTaskParams.test.ets +44 -0
- package/harmony/pushy/src/test/EventHub.test.ets +51 -0
- package/harmony/pushy/src/test/List.test.ets +11 -0
- package/harmony/pushy/src/test/ManifestParsing.test.ets +107 -0
- package/harmony/pushy/src/test/Validation.test.ets +94 -0
- package/harmony/pushy.har +0 -0
- package/package.json +1 -1
- package/src/__tests__/setup.ts +4 -0
- package/src/client.ts +8 -9
- package/src/isInRollout.ts +1 -1
- package/src/permissions.ts +2 -1
- package/src/utils.ts +9 -10
- package/package/harmony/pushy.har +0 -0
- package/react-native-update-10.40.0-beta.1.tgz +0 -0
package/README-CN.md
CHANGED
|
@@ -53,18 +53,6 @@
|
|
|
53
53
|
| **更新成功率** | ⭐⭐⭐⭐⭐ 极高(国内 CDN 优势) | ⭐⭐⭐ 中等 | ❌ **已停运** |
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
### 本地开发
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
$ git clone git@github.com:reactnativecn/react-native-update.git
|
|
61
|
-
$ cd react-native-pushy/Example/testHotUpdate
|
|
62
|
-
$ yarn
|
|
63
|
-
$ yarn start
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
本地库文件使用 yarn link 链接,因此可直接在源文件中修改,在 testHotUpdate 项目中调试。
|
|
67
|
-
|
|
68
56
|
### 关于我们
|
|
69
57
|
|
|
70
58
|
本组件由[React Native 中文网](https://reactnative.cn/)独家发布,如有定制需求可以[联系我们](https://reactnative.cn/about.html#content)。
|
package/README.md
CHANGED
|
@@ -29,38 +29,9 @@ See the docs:
|
|
|
29
29
|
|
|
30
30
|
| Category | react-native-update | expo-update | react-native-code-push |
|
|
31
31
|
|---------|---------------------|-------------|------------------------|
|
|
32
|
-
| **Price / Cost** | Free tier with multiple paid plans
|
|
32
|
+
| **Price / Cost** | Free tier with multiple paid plans, bandwidth included | Free tier with multiple paid plans, extra bandwidth charges apply | ❌ **Discontinued** (Microsoft App Center shut down on March 31, 2025) |
|
|
33
33
|
| **Package Size** | ⭐⭐⭐⭐⭐ Tens to hundreds of KB (incremental) | ⭐⭐⭐ Full bundle updates (usually tens of MB) | ❌ **Discontinued** |
|
|
34
|
-
| **iOS Support** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
|
|
35
|
-
| **Android Support** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
|
|
36
|
-
| **HarmonyOS Support** | ✅ Supported | ❌ Not supported | ❌ **Discontinued** |
|
|
37
|
-
| **Expo Support** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
|
|
38
|
-
| **RN Version Support** | ⭐⭐⭐⭐⭐ Fast support for latest stable RN versions | ⭐⭐⭐⭐ Follows Expo SDK cadence | ❌ **Discontinued** |
|
|
39
|
-
| **New Architecture** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
|
|
40
|
-
| **Hermes Support** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
|
|
41
|
-
| **Crash Rollback** | ✅ Automatic rollback | ✅ Supported | ❌ **Discontinued** |
|
|
42
|
-
| **Management UI** | ✅ CLI + Web dashboard | ✅ Expo Dashboard | ❌ **Discontinued** |
|
|
43
|
-
| **CI/CD Integration** | ✅ Supported | ✅ Supported | ❌ **Discontinued** |
|
|
44
|
-
| **API Extensibility** | ✅ Meta info + Open API | ⚠️ Limited | ❌ **Discontinued** |
|
|
45
|
-
| **Chinese Docs / Support** | ⭐⭐⭐⭐⭐ Complete Chinese docs and community support | ⭐⭐ Mostly English | ❌ **Discontinued** |
|
|
46
34
|
| **Technical Support** | ✅ Paid dedicated support | ⚠️ Community support | ❌ **Discontinued** |
|
|
47
35
|
| **Server Deployment** | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ **Discontinued** |
|
|
48
|
-
| **Update Strategy** | Flexible configuration (silent / prompted / immediate / delayed) | More fixed workflow | ❌ **Discontinued** |
|
|
49
36
|
| **Bandwidth Usage** | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ **Discontinued** |
|
|
50
37
|
|
|
51
|
-
## Local Development
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
git clone git@github.com:reactnativecn/react-native-update.git
|
|
55
|
-
cd react-native-pushy/Example/testHotUpdate
|
|
56
|
-
yarn
|
|
57
|
-
yarn start
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
The local library is linked with `yarn link`, so you can modify the source files directly and debug with the `testHotUpdate` example project.
|
|
61
|
-
|
|
62
|
-
## About
|
|
63
|
-
|
|
64
|
-
This package is published by [React Native Chinese](https://reactnative.cn/). For custom integration or service inquiries, see [Contact Us](https://reactnative.cn/about.html#content).
|
|
65
|
-
|
|
66
|
-
If you find any issues, please open a thread in [Issues](https://github.com/reactnativecn/react-native-update/issues).
|
|
@@ -5,6 +5,7 @@ APP_CFLAGS += -ffunction-sections -fdata-sections
|
|
|
5
5
|
APP_CFLAGS += -Oz -fno-unwind-tables -fno-asynchronous-unwind-tables
|
|
6
6
|
APP_CPPFLAGS += -std=c++17 -Oz -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables
|
|
7
7
|
APP_LDFLAGS += -Wl,--gc-sections -Wl,--exclude-libs,ALL
|
|
8
|
+
APP_LDFLAGS += -Wl,--icf=all
|
|
8
9
|
APP_BUILD_SCRIPT := Android.mk
|
|
9
10
|
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
|
|
10
11
|
APP_STL := c++_static
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -38,10 +38,6 @@ final class BundledResourceCopier {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
void copyFromResource(HashMap<String, ArrayList<File>> resToCopy) throws IOException {
|
|
41
|
-
if (UpdateContext.DEBUG) {
|
|
42
|
-
Log.d(UpdateContext.TAG, "copyFromResource called, resToCopy size: " + resToCopy.size());
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
ArrayList<String> apkPaths = collectApkPaths();
|
|
46
42
|
HashMap<String, ZipEntry> availableEntries = new HashMap<String, ZipEntry>();
|
|
47
43
|
HashMap<String, SafeZipFile> zipFileMap = new HashMap<String, SafeZipFile>();
|
|
@@ -73,10 +69,6 @@ final class BundledResourceCopier {
|
|
|
73
69
|
new HashMap<String, ArrayList<File>>(resToCopy);
|
|
74
70
|
|
|
75
71
|
for (String fromPath : new ArrayList<String>(remainingFiles.keySet())) {
|
|
76
|
-
if (UpdateContext.DEBUG) {
|
|
77
|
-
Log.d(UpdateContext.TAG, "Processing fromPath: " + fromPath);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
72
|
ArrayList<File> targets = remainingFiles.get(fromPath);
|
|
81
73
|
if (targets == null || targets.isEmpty()) {
|
|
82
74
|
continue;
|
|
@@ -92,9 +84,6 @@ final class BundledResourceCopier {
|
|
|
92
84
|
if (actualEntry != null) {
|
|
93
85
|
entry = availableEntries.get(actualEntry);
|
|
94
86
|
actualSourcePath = actualEntry;
|
|
95
|
-
if (UpdateContext.DEBUG) {
|
|
96
|
-
Log.d(UpdateContext.TAG, "Normalized match: " + fromPath + " -> " + actualEntry);
|
|
97
|
-
}
|
|
98
87
|
}
|
|
99
88
|
}
|
|
100
89
|
|
|
@@ -111,9 +100,6 @@ final class BundledResourceCopier {
|
|
|
111
100
|
|
|
112
101
|
File lastTarget = null;
|
|
113
102
|
for (File target : targets) {
|
|
114
|
-
if (UpdateContext.DEBUG) {
|
|
115
|
-
Log.d(UpdateContext.TAG, "Copying from resource " + actualSourcePath + " to " + target);
|
|
116
|
-
}
|
|
117
103
|
try {
|
|
118
104
|
if (lastTarget != null) {
|
|
119
105
|
UpdateFileUtils.copyFile(lastTarget, target);
|
|
@@ -146,9 +132,10 @@ final class BundledResourceCopier {
|
|
|
146
132
|
}
|
|
147
133
|
|
|
148
134
|
if (!remainingFiles.isEmpty() && UpdateContext.DEBUG) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
135
|
+
Log.w(
|
|
136
|
+
UpdateContext.TAG,
|
|
137
|
+
"Skipped " + remainingFiles.size() + " missing bundled resources"
|
|
138
|
+
);
|
|
152
139
|
}
|
|
153
140
|
} finally {
|
|
154
141
|
closeZipFiles(zipFileMap);
|
|
@@ -244,12 +231,6 @@ final class BundledResourceCopier {
|
|
|
244
231
|
resources.getValue(resourceId, typedValue, true);
|
|
245
232
|
}
|
|
246
233
|
} catch (Resources.NotFoundException e) {
|
|
247
|
-
if (UpdateContext.DEBUG) {
|
|
248
|
-
Log.d(
|
|
249
|
-
UpdateContext.TAG,
|
|
250
|
-
"Failed to resolve resource value for " + resourcePath + ": " + e.getMessage()
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
234
|
return null;
|
|
254
235
|
}
|
|
255
236
|
|
|
@@ -262,9 +243,6 @@ final class BundledResourceCopier {
|
|
|
262
243
|
assetPath = assetPath.substring(1);
|
|
263
244
|
}
|
|
264
245
|
|
|
265
|
-
if (UpdateContext.DEBUG) {
|
|
266
|
-
Log.d(UpdateContext.TAG, "Resolved resource path " + resourcePath + " -> " + assetPath);
|
|
267
|
-
}
|
|
268
246
|
return new ResolvedResourceSource(resourceId, assetPath);
|
|
269
247
|
}
|
|
270
248
|
|
|
@@ -286,9 +264,6 @@ final class BundledResourceCopier {
|
|
|
286
264
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && appInfo.splitSourceDirs != null) {
|
|
287
265
|
for (String splitPath : appInfo.splitSourceDirs) {
|
|
288
266
|
apkPaths.add(splitPath);
|
|
289
|
-
if (UpdateContext.DEBUG) {
|
|
290
|
-
Log.d(UpdateContext.TAG, "Found split APK: " + splitPath);
|
|
291
|
-
}
|
|
292
267
|
}
|
|
293
268
|
}
|
|
294
269
|
} catch (PackageManager.NameNotFoundException e) {
|
|
@@ -79,10 +79,6 @@ class DownloadTask implements Runnable {
|
|
|
79
79
|
throw new IOException("Failed to replace existing file: " + writePath);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
if (UpdateContext.DEBUG) {
|
|
83
|
-
Log.d(UpdateContext.TAG, "Downloading " + url);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
82
|
try (Response response = HTTP_CLIENT.newCall(request).execute()) {
|
|
87
83
|
if (!response.isSuccessful()) {
|
|
88
84
|
throw new IOException("Server error: " + response.code() + " " + response.message());
|
|
@@ -125,9 +121,6 @@ class DownloadTask implements Runnable {
|
|
|
125
121
|
postProgress(received, contentLength);
|
|
126
122
|
}
|
|
127
123
|
|
|
128
|
-
if (UpdateContext.DEBUG) {
|
|
129
|
-
Log.d(UpdateContext.TAG, "Download finished");
|
|
130
|
-
}
|
|
131
124
|
}
|
|
132
125
|
|
|
133
126
|
private byte[] readBytes(InputStream input) throws IOException {
|
|
@@ -251,9 +244,6 @@ class DownloadTask implements Runnable {
|
|
|
251
244
|
}
|
|
252
245
|
}
|
|
253
246
|
|
|
254
|
-
if (UpdateContext.DEBUG) {
|
|
255
|
-
Log.d(UpdateContext.TAG, "Unzip finished");
|
|
256
|
-
}
|
|
257
247
|
}
|
|
258
248
|
|
|
259
249
|
private void doPatchFromApk() throws IOException, JSONException {
|
|
@@ -295,21 +285,7 @@ class DownloadTask implements Runnable {
|
|
|
295
285
|
originBundleFile.delete();
|
|
296
286
|
}
|
|
297
287
|
|
|
298
|
-
if (UpdateContext.DEBUG) {
|
|
299
|
-
Log.d(UpdateContext.TAG, "copyList size: " + copyList.size());
|
|
300
|
-
for (String from : copyList.keySet()) {
|
|
301
|
-
Log.d(
|
|
302
|
-
UpdateContext.TAG,
|
|
303
|
-
"copyList entry: " + from + " -> " + copyList.get(from).size() + " targets"
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
288
|
bundledResourceCopier.copyFromResource(copyList);
|
|
309
|
-
|
|
310
|
-
if (UpdateContext.DEBUG) {
|
|
311
|
-
Log.d(UpdateContext.TAG, "Unzip finished");
|
|
312
|
-
}
|
|
313
289
|
}
|
|
314
290
|
|
|
315
291
|
private void doPatchFromPpk() throws IOException, JSONException {
|
|
@@ -337,15 +313,9 @@ class DownloadTask implements Runnable {
|
|
|
337
313
|
contents.deletes.toArray(new String[0])
|
|
338
314
|
);
|
|
339
315
|
|
|
340
|
-
if (UpdateContext.DEBUG) {
|
|
341
|
-
Log.d(UpdateContext.TAG, "Unzip finished");
|
|
342
|
-
}
|
|
343
316
|
}
|
|
344
317
|
|
|
345
318
|
private void doCleanUp() {
|
|
346
|
-
if (UpdateContext.DEBUG) {
|
|
347
|
-
Log.d(UpdateContext.TAG, "Start cleaning up");
|
|
348
|
-
}
|
|
349
319
|
cleanupOldEntries(
|
|
350
320
|
params.unzipDirectory.getAbsolutePath(),
|
|
351
321
|
params.hash,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
package cn.reactnative.modules.update;
|
|
2
2
|
|
|
3
|
-
import android.util.Log;
|
|
4
|
-
|
|
5
3
|
import java.io.BufferedInputStream;
|
|
6
4
|
import java.io.BufferedOutputStream;
|
|
7
5
|
import java.io.File;
|
|
@@ -66,10 +64,6 @@ public class SafeZipFile extends ZipFile {
|
|
|
66
64
|
throw new SecurityException("Illegal name: " + name);
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
if (UpdateContext.DEBUG) {
|
|
70
|
-
Log.d(UpdateContext.TAG, "Unzipping " + name);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
67
|
if (ze.isDirectory()) {
|
|
74
68
|
target.mkdirs();
|
|
75
69
|
return;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package cn.reactnative.modules.update;
|
|
2
2
|
|
|
3
|
-
import android.util.Log;
|
|
4
3
|
import androidx.annotation.Nullable;
|
|
5
4
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
6
5
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
@@ -26,9 +25,6 @@ final class UpdateEventEmitter {
|
|
|
26
25
|
static void sendEvent(String eventName, WritableMap params) {
|
|
27
26
|
ReactApplicationContext reactContext = getReactContext();
|
|
28
27
|
if (reactContext == null || !reactContext.hasActiveCatalystInstance()) {
|
|
29
|
-
if (UpdateContext.DEBUG) {
|
|
30
|
-
Log.d(UpdateContext.TAG, "Skipping event " + eventName + " because React context is unavailable");
|
|
31
|
-
}
|
|
32
28
|
return;
|
|
33
29
|
}
|
|
34
30
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package cn.reactnative.modules.update;
|
|
2
2
|
|
|
3
|
-
import android.util.Log;
|
|
4
3
|
import java.io.File;
|
|
5
4
|
import java.io.FileInputStream;
|
|
6
5
|
import java.io.FileOutputStream;
|
|
@@ -27,9 +26,6 @@ final class UpdateFileUtils {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
static void removeDirectory(File file) throws IOException {
|
|
30
|
-
if (UpdateContext.DEBUG) {
|
|
31
|
-
Log.d(UpdateContext.TAG, "Removing " + file);
|
|
32
|
-
}
|
|
33
29
|
if (file.isDirectory()) {
|
|
34
30
|
File[] files = file.listFiles();
|
|
35
31
|
if (files != null) {
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
package expo.modules.pushy;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
-
import android.util.Log;
|
|
5
4
|
import androidx.annotation.Nullable;
|
|
6
5
|
import java.util.ArrayList;
|
|
7
|
-
import java.util.HashMap;
|
|
8
6
|
import java.util.List;
|
|
9
|
-
import java.util.Map;
|
|
10
7
|
import cn.reactnative.modules.update.UpdateContext;
|
|
11
8
|
import expo.modules.core.interfaces.Package;
|
|
12
9
|
import expo.modules.core.interfaces.ReactNativeHostHandler;
|
|
@@ -24,4 +21,4 @@ public class ExpoPushyPackage implements Package {
|
|
|
24
21
|
});
|
|
25
22
|
return handlers;
|
|
26
23
|
}
|
|
27
|
-
}
|
|
24
|
+
}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
#include <unistd.h>
|
|
10
10
|
|
|
11
11
|
#include <set>
|
|
12
|
-
#include <sstream>
|
|
13
12
|
#include <vector>
|
|
14
13
|
|
|
15
14
|
extern "C" {
|
|
@@ -31,12 +30,19 @@ class HdiffBundlePatcher final : public BundlePatcher {
|
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
Status MakeErrnoStatus(const std::string& message, int err = errno) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (err != 0) {
|
|
37
|
-
stream << ": " << std::strerror(err);
|
|
33
|
+
if (err == 0) {
|
|
34
|
+
return Status::Error(message);
|
|
38
35
|
}
|
|
39
|
-
return Status::Error(
|
|
36
|
+
return Status::Error(message + ": " + std::strerror(err));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
std::string IntToString(int value) {
|
|
40
|
+
char buffer[32] = {0};
|
|
41
|
+
const int written = std::snprintf(buffer, sizeof(buffer), "%d", value);
|
|
42
|
+
if (written <= 0) {
|
|
43
|
+
return "0";
|
|
44
|
+
}
|
|
45
|
+
return std::string(buffer, static_cast<size_t>(written));
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
bool EndsWithSlash(const std::string& path) {
|
|
@@ -522,9 +528,8 @@ Status HdiffBundlePatcher::Apply(
|
|
|
522
528
|
destination_bundle_path.c_str(),
|
|
523
529
|
bundle_patch_path.c_str());
|
|
524
530
|
if (result != 0) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return Status::Error(stream.str());
|
|
531
|
+
return Status::Error(
|
|
532
|
+
"Failed to apply bundle patch, hpatch error " + IntToString(result));
|
|
528
533
|
}
|
|
529
534
|
return Status::Ok();
|
|
530
535
|
}
|
|
@@ -442,6 +442,74 @@ void TestArchivePatchCoreSupportsCustomBundlePatchEntry() {
|
|
|
442
442
|
ExpectEq(plan.merge_source_subdir, "", "custom bundle patch merge subdir mismatch");
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
void TestArchivePatchCoreHarmonyBundlePatchFromPackage() {
|
|
446
|
+
PatchManifest manifest;
|
|
447
|
+
manifest.copies.push_back(CopyOperation{"assets/a.png", "assets/b.png"});
|
|
448
|
+
|
|
449
|
+
pushy::archive_patch::ArchivePatchPlan plan;
|
|
450
|
+
Status status = pushy::archive_patch::BuildArchivePatchPlan(
|
|
451
|
+
pushy::archive_patch::ArchivePatchType::kPatchFromPackage,
|
|
452
|
+
manifest,
|
|
453
|
+
{"__diff.json", "bundle.harmony.js.patch", "assets/new.png"},
|
|
454
|
+
&plan,
|
|
455
|
+
"bundle.harmony.js.patch");
|
|
456
|
+
Expect(status.ok, status.message);
|
|
457
|
+
Expect(plan.enable_merge, "harmony package plan should enable merge");
|
|
458
|
+
ExpectEq(plan.merge_source_subdir, "assets", "harmony package merge subdir should be assets");
|
|
459
|
+
|
|
460
|
+
// ppk variant uses empty merge subdir
|
|
461
|
+
pushy::archive_patch::ArchivePatchPlan ppk_plan;
|
|
462
|
+
status = pushy::archive_patch::BuildArchivePatchPlan(
|
|
463
|
+
pushy::archive_patch::ArchivePatchType::kPatchFromPpk,
|
|
464
|
+
manifest,
|
|
465
|
+
{"__diff.json", "bundle.harmony.js.patch", "assets/new.png"},
|
|
466
|
+
&ppk_plan,
|
|
467
|
+
"bundle.harmony.js.patch");
|
|
468
|
+
Expect(status.ok, status.message);
|
|
469
|
+
Expect(ppk_plan.enable_merge, "harmony ppk plan should enable merge");
|
|
470
|
+
ExpectEq(ppk_plan.merge_source_subdir, "", "harmony ppk merge subdir should be empty");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
void TestStateCoreRollbackToEmptyVersion() {
|
|
474
|
+
State state;
|
|
475
|
+
state.current_version = "current";
|
|
476
|
+
state.last_version = "";
|
|
477
|
+
state.first_time = false;
|
|
478
|
+
state.first_time_ok = true;
|
|
479
|
+
|
|
480
|
+
State rolled = pushy::state::Rollback(state);
|
|
481
|
+
Expect(rolled.current_version.empty(), "rollback with empty last should clear current");
|
|
482
|
+
Expect(rolled.last_version.empty(), "last_version should remain empty");
|
|
483
|
+
ExpectEq(rolled.rolled_back_version, "current", "rolled_back_version should record original");
|
|
484
|
+
Expect(!rolled.first_time, "first_time should be false after rollback");
|
|
485
|
+
Expect(rolled.first_time_ok, "first_time_ok should be true after rollback");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
void TestStateCoreResolveLaunchNoCurrentVersion() {
|
|
489
|
+
State state;
|
|
490
|
+
state.current_version = "";
|
|
491
|
+
state.first_time = false;
|
|
492
|
+
state.first_time_ok = true;
|
|
493
|
+
|
|
494
|
+
LaunchDecision decision = pushy::state::ResolveLaunchState(state, false, true);
|
|
495
|
+
Expect(decision.load_version.empty(), "empty current should yield empty load_version");
|
|
496
|
+
Expect(!decision.did_rollback, "should not rollback when no current version");
|
|
497
|
+
Expect(!decision.consumed_first_time, "should not consume first_time when no current version");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
void TestStateCoreSwitchToSameVersion() {
|
|
501
|
+
State state;
|
|
502
|
+
state.package_version = "1.0.0";
|
|
503
|
+
state.current_version = "same_hash";
|
|
504
|
+
state.last_version = "old_hash";
|
|
505
|
+
|
|
506
|
+
State switched = pushy::state::SwitchVersion(state, "same_hash");
|
|
507
|
+
ExpectEq(switched.current_version, "same_hash", "current should remain same_hash");
|
|
508
|
+
ExpectEq(switched.last_version, "old_hash", "last_version should not change when switching to same");
|
|
509
|
+
Expect(switched.first_time, "first_time should be set even when switching to same");
|
|
510
|
+
Expect(!switched.first_time_ok, "first_time_ok should be false");
|
|
511
|
+
}
|
|
512
|
+
|
|
445
513
|
} // namespace
|
|
446
514
|
|
|
447
515
|
int main() {
|
|
@@ -457,6 +525,10 @@ int main() {
|
|
|
457
525
|
{"ArchivePatchCoreBuildPlanAndCopyGroups", TestArchivePatchCoreBuildPlanAndCopyGroups},
|
|
458
526
|
{"ArchivePatchCoreRejectsMissingEntries", TestArchivePatchCoreRejectsMissingEntries},
|
|
459
527
|
{"ArchivePatchCoreSupportsCustomBundlePatchEntry", TestArchivePatchCoreSupportsCustomBundlePatchEntry},
|
|
528
|
+
{"ArchivePatchCoreHarmonyBundlePatchFromPackage", TestArchivePatchCoreHarmonyBundlePatchFromPackage},
|
|
529
|
+
{"StateCoreRollbackToEmptyVersion", TestStateCoreRollbackToEmptyVersion},
|
|
530
|
+
{"StateCoreResolveLaunchNoCurrentVersion", TestStateCoreResolveLaunchNoCurrentVersion},
|
|
531
|
+
{"StateCoreSwitchToSameVersion", TestStateCoreSwitchToSameVersion},
|
|
460
532
|
};
|
|
461
533
|
|
|
462
534
|
for (const auto& test : tests) {
|
|
@@ -12,12 +12,45 @@ import NativePatchCore, {
|
|
|
12
12
|
CopyGroupResult,
|
|
13
13
|
} from './NativePatchCore';
|
|
14
14
|
|
|
15
|
-
interface PatchManifestArrays {
|
|
15
|
+
export interface PatchManifestArrays {
|
|
16
16
|
copyFroms: string[];
|
|
17
17
|
copyTos: string[];
|
|
18
18
|
deletes: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function parseManifestToArrays(
|
|
22
|
+
manifest: Record<string, any>,
|
|
23
|
+
normalizeResourceCopies: boolean,
|
|
24
|
+
): PatchManifestArrays {
|
|
25
|
+
const copyFroms: string[] = [];
|
|
26
|
+
const copyTos: string[] = [];
|
|
27
|
+
const deletesValue = manifest.deletes;
|
|
28
|
+
const deletes = Array.isArray(deletesValue)
|
|
29
|
+
? deletesValue.map(item => String(item))
|
|
30
|
+
: deletesValue && typeof deletesValue === 'object'
|
|
31
|
+
? Object.keys(deletesValue)
|
|
32
|
+
: [];
|
|
33
|
+
|
|
34
|
+
const copies = (manifest.copies || {}) as Record<string, string>;
|
|
35
|
+
for (const [to, rawFrom] of Object.entries(copies)) {
|
|
36
|
+
let from = String(rawFrom || '');
|
|
37
|
+
if (normalizeResourceCopies) {
|
|
38
|
+
from = from.replace('resources/rawfile/', '');
|
|
39
|
+
if (!from) {
|
|
40
|
+
from = to;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
copyFroms.push(from);
|
|
44
|
+
copyTos.push(to);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
copyFroms,
|
|
49
|
+
copyTos,
|
|
50
|
+
deletes,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
21
54
|
const DIFF_MANIFEST_ENTRY = '__diff.json';
|
|
22
55
|
const HARMONY_BUNDLE_PATCH_ENTRY = 'bundle.harmony.js.patch';
|
|
23
56
|
const TEMP_ORIGIN_BUNDLE_ENTRY = '.origin.bundle.harmony.js';
|
|
@@ -40,11 +73,14 @@ export class DownloadTask {
|
|
|
40
73
|
const stat = await fileIo.stat(path);
|
|
41
74
|
if (stat.isDirectory()) {
|
|
42
75
|
const files = await fileIo.listFile(path);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
|
|
77
|
+
const entries = files.filter(file => file !== '.' && file !== '..');
|
|
78
|
+
const DELETE_CONCURRENCY = 32;
|
|
79
|
+
for (let i = 0; i < entries.length; i += DELETE_CONCURRENCY) {
|
|
80
|
+
const batch = entries.slice(i, i + DELETE_CONCURRENCY);
|
|
81
|
+
await Promise.all(
|
|
82
|
+
batch.map(file => this.removeDirectory(`${path}/${file}`)),
|
|
83
|
+
);
|
|
48
84
|
}
|
|
49
85
|
await fileIo.rmdir(path);
|
|
50
86
|
} else {
|
|
@@ -166,44 +202,12 @@ export class DownloadTask {
|
|
|
166
202
|
};
|
|
167
203
|
}
|
|
168
204
|
|
|
169
|
-
return
|
|
205
|
+
return parseManifestToArrays(
|
|
170
206
|
this.parseJsonEntry(await this.readFileContent(manifestPath)),
|
|
171
207
|
normalizeResourceCopies,
|
|
172
208
|
);
|
|
173
209
|
}
|
|
174
210
|
|
|
175
|
-
private manifestToArrays(
|
|
176
|
-
manifest: Record<string, any>,
|
|
177
|
-
normalizeResourceCopies: boolean,
|
|
178
|
-
): PatchManifestArrays {
|
|
179
|
-
const copyFroms: string[] = [];
|
|
180
|
-
const copyTos: string[] = [];
|
|
181
|
-
const deletesValue = manifest.deletes;
|
|
182
|
-
const deletes = Array.isArray(deletesValue)
|
|
183
|
-
? deletesValue.map(item => String(item))
|
|
184
|
-
: deletesValue && typeof deletesValue === 'object'
|
|
185
|
-
? Object.keys(deletesValue)
|
|
186
|
-
: [];
|
|
187
|
-
|
|
188
|
-
const copies = (manifest.copies || {}) as Record<string, string>;
|
|
189
|
-
for (const [to, rawFrom] of Object.entries(copies)) {
|
|
190
|
-
let from = String(rawFrom || '');
|
|
191
|
-
if (normalizeResourceCopies) {
|
|
192
|
-
from = from.replace('resources/rawfile/', '');
|
|
193
|
-
if (!from) {
|
|
194
|
-
from = to;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
copyFroms.push(from);
|
|
198
|
-
copyTos.push(to);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
copyFroms,
|
|
203
|
-
copyTos,
|
|
204
|
-
deletes,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
211
|
|
|
208
212
|
private async applyBundlePatchFromFileSource(
|
|
209
213
|
originContent: ArrayBuffer,
|
|
@@ -525,9 +529,9 @@ export class DownloadTask {
|
|
|
525
529
|
targets.map(t => t.substring(0, t.lastIndexOf('/'))).filter(Boolean),
|
|
526
530
|
),
|
|
527
531
|
];
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
532
|
+
await Promise.all(
|
|
533
|
+
parentDirs.map(dir => this.ensureDirectory(dir)),
|
|
534
|
+
);
|
|
531
535
|
await Promise.all(
|
|
532
536
|
targets.map(target => this.writeFileContent(target, mediaBuffer.buffer)),
|
|
533
537
|
);
|
|
@@ -540,9 +544,9 @@ export class DownloadTask {
|
|
|
540
544
|
targets.map(t => t.substring(0, t.lastIndexOf('/'))).filter(Boolean),
|
|
541
545
|
),
|
|
542
546
|
];
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
547
|
+
await Promise.all(
|
|
548
|
+
parentDirs.map(dir => this.ensureDirectory(dir))
|
|
549
|
+
);
|
|
546
550
|
if (fileIo.accessSync(firstTarget)) {
|
|
547
551
|
await fileIo.unlink(firstTarget);
|
|
548
552
|
}
|
|
@@ -11,6 +11,24 @@ import { util } from '@kit.ArkTS';
|
|
|
11
11
|
|
|
12
12
|
const TAG = 'PushyTurboModule';
|
|
13
13
|
|
|
14
|
+
export function getErrorMessage(error: any): string {
|
|
15
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
16
|
+
return String(error.message);
|
|
17
|
+
}
|
|
18
|
+
return String(error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateHashInfo(info: string): void {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(info);
|
|
24
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
25
|
+
throw Error('invalid json string');
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
throw Error('invalid json string');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
export class PushyTurboModule extends UITurboModule {
|
|
15
33
|
public static readonly NAME = 'Pushy';
|
|
16
34
|
|
|
@@ -29,13 +47,6 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
29
47
|
return bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
|
|
30
48
|
}
|
|
31
49
|
|
|
32
|
-
private getErrorMessage(error: any): string {
|
|
33
|
-
if (error && typeof error === 'object' && 'message' in error) {
|
|
34
|
-
return String(error.message);
|
|
35
|
-
}
|
|
36
|
-
return String(error);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
50
|
private getPackageVersion(): string {
|
|
40
51
|
try {
|
|
41
52
|
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
|
|
@@ -70,17 +81,6 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
70
81
|
return hash;
|
|
71
82
|
}
|
|
72
83
|
|
|
73
|
-
private validateHashInfo(info: string): void {
|
|
74
|
-
try {
|
|
75
|
-
const parsed = JSON.parse(info);
|
|
76
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
77
|
-
throw Error('invalid json string');
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
throw Error('invalid json string');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
84
|
private async restartAbility(): Promise<void> {
|
|
85
85
|
const bundleInfo = await bundleManager.getBundleInfoForSelf(
|
|
86
86
|
this.getBundleFlags(),
|
|
@@ -127,14 +127,14 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
127
127
|
|
|
128
128
|
setLocalHashInfo(hash: string, info: string): boolean {
|
|
129
129
|
logger.debug(TAG, ',call setLocalHashInfo');
|
|
130
|
-
|
|
130
|
+
validateHashInfo(info);
|
|
131
131
|
this.context.setKv(`hash_${hash}`, info);
|
|
132
132
|
return true;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
getLocalHashInfo(hash: string): string {
|
|
136
136
|
const value = this.context.getKv(`hash_${hash}`);
|
|
137
|
-
|
|
137
|
+
validateHashInfo(value);
|
|
138
138
|
return value;
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -151,8 +151,8 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
151
151
|
this.context.switchVersion(hash);
|
|
152
152
|
await this.restartAbility();
|
|
153
153
|
} catch (error) {
|
|
154
|
-
logger.error(TAG, `reloadUpdate failed: ${
|
|
155
|
-
throw Error(`switchVersion failed ${
|
|
154
|
+
logger.error(TAG, `reloadUpdate failed: ${getErrorMessage(error)}`);
|
|
155
|
+
throw Error(`switchVersion failed ${getErrorMessage(error)}`);
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -161,8 +161,8 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
161
161
|
try {
|
|
162
162
|
await this.restartAbility();
|
|
163
163
|
} catch (error) {
|
|
164
|
-
logger.error(TAG, `restartApp failed: ${
|
|
165
|
-
throw Error(`restartApp failed ${
|
|
164
|
+
logger.error(TAG, `restartApp failed: ${getErrorMessage(error)}`);
|
|
165
|
+
throw Error(`restartApp failed ${getErrorMessage(error)}`);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -174,8 +174,8 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
174
174
|
this.context.switchVersion(hash);
|
|
175
175
|
return true;
|
|
176
176
|
} catch (error) {
|
|
177
|
-
logger.error(TAG, `setNeedUpdate failed: ${
|
|
178
|
-
throw Error(`switchVersionLater failed: ${
|
|
177
|
+
logger.error(TAG, `setNeedUpdate failed: ${getErrorMessage(error)}`);
|
|
178
|
+
throw Error(`switchVersionLater failed: ${getErrorMessage(error)}`);
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -185,7 +185,7 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
185
185
|
this.context.markSuccess();
|
|
186
186
|
return true;
|
|
187
187
|
} catch (error) {
|
|
188
|
-
logger.error(TAG, `markSuccess failed: ${
|
|
188
|
+
logger.error(TAG, `markSuccess failed: ${getErrorMessage(error)}`);
|
|
189
189
|
throw error;
|
|
190
190
|
}
|
|
191
191
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from '@ohos/hypium';
|
|
2
|
+
import { DownloadTaskParams } from '../main/ets/DownloadTaskParams';
|
|
3
|
+
|
|
4
|
+
export default function downloadTaskParamsTest() {
|
|
5
|
+
describe('DownloadTaskParams', () => {
|
|
6
|
+
it('has correct task type constants', 0, () => {
|
|
7
|
+
expect(DownloadTaskParams.TASK_TYPE_CLEANUP).assertEqual(0);
|
|
8
|
+
expect(DownloadTaskParams.TASK_TYPE_PATCH_FULL).assertEqual(1);
|
|
9
|
+
expect(DownloadTaskParams.TASK_TYPE_PATCH_FROM_APP).assertEqual(2);
|
|
10
|
+
expect(DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK).assertEqual(3);
|
|
11
|
+
expect(DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD).assertEqual(4);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('has correct default values', 0, () => {
|
|
15
|
+
const params = new DownloadTaskParams();
|
|
16
|
+
expect(params.type).assertEqual(DownloadTaskParams.TASK_TYPE_CLEANUP);
|
|
17
|
+
expect(params.url).assertEqual('');
|
|
18
|
+
expect(params.hash).assertEqual('');
|
|
19
|
+
expect(params.originHash).assertEqual('');
|
|
20
|
+
expect(params.targetFile).assertEqual('');
|
|
21
|
+
expect(params.unzipDirectory).assertEqual('');
|
|
22
|
+
expect(params.originDirectory).assertEqual('');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('allows setting all fields', 0, () => {
|
|
26
|
+
const params = new DownloadTaskParams();
|
|
27
|
+
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
|
|
28
|
+
params.url = 'https://example.com/patch.ppk';
|
|
29
|
+
params.hash = 'abc123';
|
|
30
|
+
params.originHash = 'def456';
|
|
31
|
+
params.targetFile = '/data/patch.ppk';
|
|
32
|
+
params.unzipDirectory = '/data/abc123';
|
|
33
|
+
params.originDirectory = '/data/def456';
|
|
34
|
+
|
|
35
|
+
expect(params.type).assertEqual(3);
|
|
36
|
+
expect(params.url).assertEqual('https://example.com/patch.ppk');
|
|
37
|
+
expect(params.hash).assertEqual('abc123');
|
|
38
|
+
expect(params.originHash).assertEqual('def456');
|
|
39
|
+
expect(params.targetFile).assertEqual('/data/patch.ppk');
|
|
40
|
+
expect(params.unzipDirectory).assertEqual('/data/abc123');
|
|
41
|
+
expect(params.originDirectory).assertEqual('/data/def456');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from '@ohos/hypium';
|
|
2
|
+
import { EventHub } from '../main/ets/EventHub';
|
|
3
|
+
|
|
4
|
+
export default function eventHubTest() {
|
|
5
|
+
describe('EventHub', () => {
|
|
6
|
+
it('is a singleton', 0, () => {
|
|
7
|
+
const a = EventHub.getInstance();
|
|
8
|
+
const b = EventHub.getInstance();
|
|
9
|
+
expect(a === b).assertEqual(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('does not throw when emitting without rnInstance', 0, () => {
|
|
13
|
+
const hub = EventHub.getInstance();
|
|
14
|
+
hub.setRNInstance(undefined);
|
|
15
|
+
let threw = false;
|
|
16
|
+
try {
|
|
17
|
+
hub.emit('test', { foo: 'bar' });
|
|
18
|
+
} catch {
|
|
19
|
+
threw = true;
|
|
20
|
+
}
|
|
21
|
+
expect(threw).assertEqual(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('calls emitDeviceEvent on rnInstance when set', 0, () => {
|
|
25
|
+
const hub = EventHub.getInstance();
|
|
26
|
+
let capturedEvent = '';
|
|
27
|
+
let capturedData: any = null;
|
|
28
|
+
const mockRnInstance = {
|
|
29
|
+
emitDeviceEvent(event: string, data: any) {
|
|
30
|
+
capturedEvent = event;
|
|
31
|
+
capturedData = data;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
hub.setRNInstance(mockRnInstance);
|
|
35
|
+
hub.emit('RCTPushyDownloadProgress', { received: 100, total: 200 });
|
|
36
|
+
expect(capturedEvent).assertEqual('RCTPushyDownloadProgress');
|
|
37
|
+
expect(capturedData.received).assertEqual(100);
|
|
38
|
+
expect(capturedData.total).assertEqual(200);
|
|
39
|
+
hub.setRNInstance(undefined);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('on/off manages listeners', 0, () => {
|
|
43
|
+
const hub = EventHub.getInstance();
|
|
44
|
+
let callCount = 0;
|
|
45
|
+
const callback = () => { callCount++; };
|
|
46
|
+
hub.on('test-event', callback);
|
|
47
|
+
hub.off('test-event', callback);
|
|
48
|
+
expect(callCount).assertEqual(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import manifestParsingTest from './ManifestParsing.test';
|
|
2
|
+
import validationTest from './Validation.test';
|
|
3
|
+
import eventHubTest from './EventHub.test';
|
|
4
|
+
import downloadTaskParamsTest from './DownloadTaskParams.test';
|
|
5
|
+
|
|
6
|
+
export default function testsuite() {
|
|
7
|
+
manifestParsingTest();
|
|
8
|
+
validationTest();
|
|
9
|
+
eventHubTest();
|
|
10
|
+
downloadTaskParamsTest();
|
|
11
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from '@ohos/hypium';
|
|
2
|
+
import { parseManifestToArrays } from '../main/ets/DownloadTask';
|
|
3
|
+
|
|
4
|
+
export default function manifestParsingTest() {
|
|
5
|
+
describe('parseManifestToArrays', () => {
|
|
6
|
+
it('returns empty arrays for empty manifest', 0, () => {
|
|
7
|
+
const result = parseManifestToArrays({}, false);
|
|
8
|
+
expect(result.copyFroms.length).assertEqual(0);
|
|
9
|
+
expect(result.copyTos.length).assertEqual(0);
|
|
10
|
+
expect(result.deletes.length).assertEqual(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('parses copies and deletes array', 0, () => {
|
|
14
|
+
const manifest = {
|
|
15
|
+
copies: {
|
|
16
|
+
'assets/icon.png': 'assets/original.png',
|
|
17
|
+
'assets/logo.png': 'assets/brand.png',
|
|
18
|
+
},
|
|
19
|
+
deletes: ['assets/old.png', 'assets/removed.txt'],
|
|
20
|
+
};
|
|
21
|
+
const result = parseManifestToArrays(manifest, false);
|
|
22
|
+
expect(result.copyFroms.length).assertEqual(2);
|
|
23
|
+
expect(result.copyTos.length).assertEqual(2);
|
|
24
|
+
expect(result.copyFroms[0]).assertEqual('assets/original.png');
|
|
25
|
+
expect(result.copyTos[0]).assertEqual('assets/icon.png');
|
|
26
|
+
expect(result.copyFroms[1]).assertEqual('assets/brand.png');
|
|
27
|
+
expect(result.copyTos[1]).assertEqual('assets/logo.png');
|
|
28
|
+
expect(result.deletes.length).assertEqual(2);
|
|
29
|
+
expect(result.deletes[0]).assertEqual('assets/old.png');
|
|
30
|
+
expect(result.deletes[1]).assertEqual('assets/removed.txt');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('parses deletes as object keys', 0, () => {
|
|
34
|
+
const manifest = {
|
|
35
|
+
deletes: {
|
|
36
|
+
'assets/old.png': true,
|
|
37
|
+
'assets/removed.txt': true,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const result = parseManifestToArrays(manifest, false);
|
|
41
|
+
expect(result.deletes.length).assertEqual(2);
|
|
42
|
+
expect(result.deletes[0]).assertEqual('assets/old.png');
|
|
43
|
+
expect(result.deletes[1]).assertEqual('assets/removed.txt');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('handles missing copies gracefully', 0, () => {
|
|
47
|
+
const manifest = {
|
|
48
|
+
deletes: ['a.txt'],
|
|
49
|
+
};
|
|
50
|
+
const result = parseManifestToArrays(manifest, false);
|
|
51
|
+
expect(result.copyFroms.length).assertEqual(0);
|
|
52
|
+
expect(result.copyTos.length).assertEqual(0);
|
|
53
|
+
expect(result.deletes.length).assertEqual(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('handles missing deletes gracefully', 0, () => {
|
|
57
|
+
const manifest = {
|
|
58
|
+
copies: { 'a.png': 'b.png' },
|
|
59
|
+
};
|
|
60
|
+
const result = parseManifestToArrays(manifest, false);
|
|
61
|
+
expect(result.deletes.length).assertEqual(0);
|
|
62
|
+
expect(result.copyFroms.length).assertEqual(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('normalizes resource copies when enabled', 0, () => {
|
|
66
|
+
const manifest = {
|
|
67
|
+
copies: {
|
|
68
|
+
'assets/icon.png': 'resources/rawfile/assets/icon.png',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const result = parseManifestToArrays(manifest, true);
|
|
72
|
+
expect(result.copyFroms[0]).assertEqual('assets/icon.png');
|
|
73
|
+
expect(result.copyTos[0]).assertEqual('assets/icon.png');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('falls back to key when normalized from is empty', 0, () => {
|
|
77
|
+
const manifest = {
|
|
78
|
+
copies: {
|
|
79
|
+
'assets/icon.png': 'resources/rawfile/',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const result = parseManifestToArrays(manifest, true);
|
|
83
|
+
expect(result.copyFroms[0]).assertEqual('assets/icon.png');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not normalize when disabled', 0, () => {
|
|
87
|
+
const manifest = {
|
|
88
|
+
copies: {
|
|
89
|
+
'assets/icon.png': 'resources/rawfile/assets/icon.png',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const result = parseManifestToArrays(manifest, false);
|
|
93
|
+
expect(result.copyFroms[0]).assertEqual('resources/rawfile/assets/icon.png');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('handles null-ish from values', 0, () => {
|
|
97
|
+
const manifest = {
|
|
98
|
+
copies: {
|
|
99
|
+
'assets/icon.png': null,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const result = parseManifestToArrays(manifest as any, false);
|
|
103
|
+
expect(result.copyFroms[0]).assertEqual('');
|
|
104
|
+
expect(result.copyTos[0]).assertEqual('assets/icon.png');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from '@ohos/hypium';
|
|
2
|
+
import { validateHashInfo, getErrorMessage } from '../main/ets/PushyTurboModule';
|
|
3
|
+
|
|
4
|
+
export default function validationTest() {
|
|
5
|
+
describe('validateHashInfo', () => {
|
|
6
|
+
it('accepts valid JSON object', 0, () => {
|
|
7
|
+
let threw = false;
|
|
8
|
+
try {
|
|
9
|
+
validateHashInfo('{"name":"v1"}');
|
|
10
|
+
} catch {
|
|
11
|
+
threw = true;
|
|
12
|
+
}
|
|
13
|
+
expect(threw).assertEqual(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('accepts empty JSON object', 0, () => {
|
|
17
|
+
let threw = false;
|
|
18
|
+
try {
|
|
19
|
+
validateHashInfo('{}');
|
|
20
|
+
} catch {
|
|
21
|
+
threw = true;
|
|
22
|
+
}
|
|
23
|
+
expect(threw).assertEqual(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rejects JSON array', 0, () => {
|
|
27
|
+
let threw = false;
|
|
28
|
+
try {
|
|
29
|
+
validateHashInfo('[1,2,3]');
|
|
30
|
+
} catch {
|
|
31
|
+
threw = true;
|
|
32
|
+
}
|
|
33
|
+
expect(threw).assertEqual(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects plain string', 0, () => {
|
|
37
|
+
let threw = false;
|
|
38
|
+
try {
|
|
39
|
+
validateHashInfo('"hello"');
|
|
40
|
+
} catch {
|
|
41
|
+
threw = true;
|
|
42
|
+
}
|
|
43
|
+
expect(threw).assertEqual(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('rejects invalid JSON', 0, () => {
|
|
47
|
+
let threw = false;
|
|
48
|
+
try {
|
|
49
|
+
validateHashInfo('{invalid}');
|
|
50
|
+
} catch {
|
|
51
|
+
threw = true;
|
|
52
|
+
}
|
|
53
|
+
expect(threw).assertEqual(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects null JSON', 0, () => {
|
|
57
|
+
let threw = false;
|
|
58
|
+
try {
|
|
59
|
+
validateHashInfo('null');
|
|
60
|
+
} catch {
|
|
61
|
+
threw = true;
|
|
62
|
+
}
|
|
63
|
+
expect(threw).assertEqual(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('getErrorMessage', () => {
|
|
68
|
+
it('extracts message from Error object', 0, () => {
|
|
69
|
+
const err = new Error('something broke');
|
|
70
|
+
expect(getErrorMessage(err)).assertEqual('something broke');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('extracts message from plain object with message property', 0, () => {
|
|
74
|
+
const err = { message: 'custom error', code: 42 };
|
|
75
|
+
expect(getErrorMessage(err)).assertEqual('custom error');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('converts string to string', 0, () => {
|
|
79
|
+
expect(getErrorMessage('raw string')).assertEqual('raw string');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('converts number to string', 0, () => {
|
|
83
|
+
expect(getErrorMessage(404)).assertEqual('404');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('converts null to string', 0, () => {
|
|
87
|
+
expect(getErrorMessage(null)).assertEqual('null');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('converts undefined to string', 0, () => {
|
|
91
|
+
expect(getErrorMessage(undefined)).assertEqual('undefined');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/__tests__/setup.ts
CHANGED
package/src/client.ts
CHANGED
|
@@ -184,7 +184,7 @@ export class Pushy {
|
|
|
184
184
|
message?: string;
|
|
185
185
|
data?: Record<string, string | number>;
|
|
186
186
|
}) => {
|
|
187
|
-
log(type
|
|
187
|
+
log(`${type} ${message}`);
|
|
188
188
|
await this.loggerPromise.promise;
|
|
189
189
|
const { logger = noop, appKey } = this.options;
|
|
190
190
|
const overridePackageVersion = this.options.overridePackageVersion;
|
|
@@ -310,12 +310,12 @@ export class Pushy {
|
|
|
310
310
|
}
|
|
311
311
|
return true;
|
|
312
312
|
};
|
|
313
|
-
markSuccess = () => {
|
|
313
|
+
markSuccess = async () => {
|
|
314
314
|
if (sharedState.marked || __DEV__ || !isFirstTime) {
|
|
315
315
|
return;
|
|
316
316
|
}
|
|
317
|
+
await Promise.resolve(PushyModule.markSuccess());
|
|
317
318
|
sharedState.marked = true;
|
|
318
|
-
PushyModule.markSuccess();
|
|
319
319
|
this.report({ type: 'markSuccess' });
|
|
320
320
|
};
|
|
321
321
|
switchVersion = async (hash: string) => {
|
|
@@ -323,7 +323,7 @@ export class Pushy {
|
|
|
323
323
|
return;
|
|
324
324
|
}
|
|
325
325
|
if (assertHash(hash) && !sharedState.applyingUpdate) {
|
|
326
|
-
log(
|
|
326
|
+
log(`switchVersion: ${hash}`);
|
|
327
327
|
sharedState.applyingUpdate = true;
|
|
328
328
|
return PushyModule.reloadUpdate({ hash });
|
|
329
329
|
}
|
|
@@ -334,7 +334,7 @@ export class Pushy {
|
|
|
334
334
|
return;
|
|
335
335
|
}
|
|
336
336
|
if (assertHash(hash)) {
|
|
337
|
-
log(
|
|
337
|
+
log(`switchVersionLater: ${hash}`);
|
|
338
338
|
return PushyModule.setNeedUpdate({ hash });
|
|
339
339
|
}
|
|
340
340
|
};
|
|
@@ -366,7 +366,7 @@ export class Pushy {
|
|
|
366
366
|
return result;
|
|
367
367
|
}
|
|
368
368
|
this.lastChecking = now;
|
|
369
|
-
const fetchBody = {
|
|
369
|
+
const fetchBody: Record<string, any> = {
|
|
370
370
|
packageVersion: this.options.overridePackageVersion || packageVersion,
|
|
371
371
|
hash: currentVersion,
|
|
372
372
|
buildTime,
|
|
@@ -374,7 +374,6 @@ export class Pushy {
|
|
|
374
374
|
...extra,
|
|
375
375
|
};
|
|
376
376
|
if (__DEV__) {
|
|
377
|
-
// @ts-ignore
|
|
378
377
|
delete fetchBody.buildTime;
|
|
379
378
|
}
|
|
380
379
|
const stringifyBody = JSON.stringify(fetchBody);
|
|
@@ -395,7 +394,7 @@ export class Pushy {
|
|
|
395
394
|
try {
|
|
396
395
|
this.report({
|
|
397
396
|
type: 'checking',
|
|
398
|
-
message: this.options.appKey
|
|
397
|
+
message: `${this.options.appKey}: ${stringifyBody}`,
|
|
399
398
|
});
|
|
400
399
|
const respJsonPromise = this.fetchCheckResult(fetchPayload);
|
|
401
400
|
this.lastRespJson = respJsonPromise;
|
|
@@ -496,7 +495,7 @@ export class Pushy {
|
|
|
496
495
|
},
|
|
497
496
|
});
|
|
498
497
|
let lastError: any;
|
|
499
|
-
|
|
498
|
+
const errorMessages: string[] = [];
|
|
500
499
|
const diffUrl = await testUrls(joinUrls(paths, diff));
|
|
501
500
|
if (diffUrl && !__DEV__) {
|
|
502
501
|
log('downloading diff');
|
package/src/isInRollout.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { cInfo } from './core';
|
|
4
4
|
|
|
5
5
|
/* eslint-disable no-bitwise */
|
|
6
|
-
function murmurhash3_32_gc(key: string, seed = 0) {
|
|
6
|
+
export function murmurhash3_32_gc(key: string, seed = 0) {
|
|
7
7
|
let remainder, bytes, h1, h1b, c1, c2, k1, i;
|
|
8
8
|
|
|
9
9
|
remainder = key.length & 3; // key.length % 4
|
package/src/permissions.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PermissionsAndroidStatic } from 'react-native';
|
|
2
2
|
import { emptyModule } from './utils';
|
|
3
3
|
|
|
4
|
-
export const PermissionsAndroid =
|
|
4
|
+
export const PermissionsAndroid =
|
|
5
|
+
emptyModule as unknown as PermissionsAndroidStatic;
|
package/src/utils.ts
CHANGED
|
@@ -39,16 +39,15 @@ export function promiseAny<T>(promises: Promise<T>[]) {
|
|
|
39
39
|
|
|
40
40
|
export const emptyObj = {};
|
|
41
41
|
export const noop = () => {};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const emptyModule = new EmptyModule();
|
|
42
|
+
const emptyModuleTarget: Record<string, typeof noop> = {};
|
|
43
|
+
export const emptyModule = new Proxy(
|
|
44
|
+
emptyModuleTarget,
|
|
45
|
+
{
|
|
46
|
+
get(_target, _prop) {
|
|
47
|
+
return noop;
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
);
|
|
52
51
|
|
|
53
52
|
const ping = isWeb
|
|
54
53
|
? Promise.resolve
|
|
Binary file
|
|
Binary file
|