@hot-updater/react-native 0.27.1 → 0.29.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/android/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +9 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +143 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +325 -210
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
- package/android/src/main/java/com/hotupdater/CohortService.kt +73 -0
- package/android/src/main/java/com/hotupdater/DecompressService.kt +28 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +1 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +51 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +533 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryReceiver.kt +14 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +55 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +19 -7
- package/android/src/newarch/HotUpdaterModule.kt +16 -25
- package/android/src/oldarch/HotUpdaterModule.kt +20 -26
- package/android/src/oldarch/HotUpdaterSpec.kt +12 -2
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +340 -232
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
- package/ios/HotUpdater/Internal/CohortService.swift +63 -0
- package/ios/HotUpdater/Internal/DecompressService.swift +53 -30
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +376 -70
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +321 -9
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +24 -8
- package/lib/commonjs/DefaultResolver.js +3 -5
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +2 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +211 -39
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +443 -0
- package/lib/commonjs/native.spec.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +4 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/DefaultResolver.js +3 -5
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +14 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +204 -34
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +442 -0
- package/lib/module/native.spec.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +5 -6
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +14 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +43 -23
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +6 -3
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +3 -6
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +14 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +43 -23
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +6 -3
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +3 -6
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DefaultResolver.ts +4 -4
- package/src/checkForUpdate.ts +4 -0
- package/src/index.ts +21 -0
- package/src/native.spec.ts +480 -0
- package/src/native.ts +285 -39
- package/src/specs/NativeHotUpdater.ts +36 -6
- package/src/types.ts +7 -3
- package/src/wrap.tsx +8 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -120,14 +120,14 @@
|
|
|
120
120
|
"react-native": "0.79.1",
|
|
121
121
|
"react-native-builder-bob": "^0.40.10",
|
|
122
122
|
"typescript": "^5.8.3",
|
|
123
|
-
"hot-updater": "0.
|
|
123
|
+
"hot-updater": "0.29.0"
|
|
124
124
|
},
|
|
125
125
|
"dependencies": {
|
|
126
126
|
"use-sync-external-store": "1.5.0",
|
|
127
|
-
"@hot-updater/cli-tools": "0.
|
|
128
|
-
"@hot-updater/core": "0.
|
|
129
|
-
"@hot-updater/
|
|
130
|
-
"@hot-updater/
|
|
127
|
+
"@hot-updater/cli-tools": "0.29.0",
|
|
128
|
+
"@hot-updater/core": "0.29.0",
|
|
129
|
+
"@hot-updater/js": "0.29.0",
|
|
130
|
+
"@hot-updater/plugin-core": "0.29.0"
|
|
131
131
|
},
|
|
132
132
|
"scripts": {
|
|
133
133
|
"build": "bob build && tsc -p plugin/tsconfig.build.json",
|
package/src/DefaultResolver.ts
CHANGED
|
@@ -14,18 +14,18 @@ export function createDefaultResolver(baseURL: string): HotUpdaterResolver {
|
|
|
14
14
|
checkUpdate: async (
|
|
15
15
|
params: ResolverCheckUpdateParams,
|
|
16
16
|
): Promise<AppUpdateInfo | null> => {
|
|
17
|
-
// Build URL based on strategy (existing buildUpdateUrl logic)
|
|
18
17
|
let url: string;
|
|
18
|
+
const cohortPath = `/${encodeURIComponent(params.cohort)}`;
|
|
19
|
+
|
|
19
20
|
if (params.updateStrategy === "fingerprint") {
|
|
20
21
|
if (!params.fingerprintHash) {
|
|
21
22
|
throw new Error("Fingerprint hash is required");
|
|
22
23
|
}
|
|
23
|
-
url = `${baseURL}/fingerprint/${params.platform}/${params.fingerprintHash}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
24
|
+
url = `${baseURL}/fingerprint/${params.platform}/${params.fingerprintHash}/${params.channel}/${params.minBundleId}/${params.bundleId}${cohortPath}`;
|
|
24
25
|
} else {
|
|
25
|
-
url = `${baseURL}/app-version/${params.platform}/${params.appVersion}/${params.channel}/${params.minBundleId}/${params.bundleId}`;
|
|
26
|
+
url = `${baseURL}/app-version/${params.platform}/${params.appVersion}/${params.channel}/${params.minBundleId}/${params.bundleId}${cohortPath}`;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
// Use existing fetchUpdateInfo
|
|
29
29
|
return fetchUpdateInfo({
|
|
30
30
|
url,
|
|
31
31
|
requestHeaders: params.requestHeaders,
|
package/src/checkForUpdate.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getAppVersion,
|
|
6
6
|
getBundleId,
|
|
7
7
|
getChannel,
|
|
8
|
+
getCohort,
|
|
8
9
|
getDefaultChannel,
|
|
9
10
|
getFingerprintHash,
|
|
10
11
|
getMinBundleId,
|
|
@@ -92,6 +93,8 @@ export async function checkForUpdate(
|
|
|
92
93
|
? minBundleId
|
|
93
94
|
: currentBundleId;
|
|
94
95
|
|
|
96
|
+
const cohort = getCohort();
|
|
97
|
+
|
|
95
98
|
if (!currentAppVersion) {
|
|
96
99
|
options.onError?.(new HotUpdaterError("Failed to get app version"));
|
|
97
100
|
return null;
|
|
@@ -122,6 +125,7 @@ export async function checkForUpdate(
|
|
|
122
125
|
appVersion: currentAppVersion,
|
|
123
126
|
bundleId: requestBundleId,
|
|
124
127
|
minBundleId,
|
|
128
|
+
cohort,
|
|
125
129
|
channel: targetChannel,
|
|
126
130
|
updateStrategy: options.updateStrategy,
|
|
127
131
|
fingerprintHash,
|
package/src/index.ts
CHANGED
|
@@ -11,13 +11,16 @@ import {
|
|
|
11
11
|
getBaseURL,
|
|
12
12
|
getBundleId,
|
|
13
13
|
getChannel,
|
|
14
|
+
getCohort,
|
|
14
15
|
getCrashHistory,
|
|
15
16
|
getDefaultChannel,
|
|
16
17
|
getFingerprintHash,
|
|
18
|
+
getManifest,
|
|
17
19
|
getMinBundleId,
|
|
18
20
|
isChannelSwitched,
|
|
19
21
|
reload,
|
|
20
22
|
resetChannel,
|
|
23
|
+
setCohort,
|
|
21
24
|
setReloadBehavior,
|
|
22
25
|
type UpdateParams,
|
|
23
26
|
updateBundle,
|
|
@@ -29,6 +32,8 @@ import { type HotUpdaterOptions, type InternalWrapOptions, wrap } from "./wrap";
|
|
|
29
32
|
export type {
|
|
30
33
|
CustomReloadHandler,
|
|
31
34
|
HotUpdaterEvent,
|
|
35
|
+
Manifest,
|
|
36
|
+
ManifestAsset,
|
|
32
37
|
NotifyAppReadyResult,
|
|
33
38
|
ReloadBehavior,
|
|
34
39
|
ReloadBehaviorSetting,
|
|
@@ -220,6 +225,11 @@ function createHotUpdaterClient() {
|
|
|
220
225
|
*/
|
|
221
226
|
getMinBundleId,
|
|
222
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Fetches the current manifest for the active bundle.
|
|
230
|
+
*/
|
|
231
|
+
getManifest,
|
|
232
|
+
|
|
223
233
|
/**
|
|
224
234
|
* Fetches the current channel of the app.
|
|
225
235
|
*
|
|
@@ -256,6 +266,17 @@ function createHotUpdaterClient() {
|
|
|
256
266
|
*/
|
|
257
267
|
isChannelSwitched,
|
|
258
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Sets the persisted cohort used for rollout calculations.
|
|
271
|
+
* Call `getCohort()` first if you need to restore the initial value later.
|
|
272
|
+
*/
|
|
273
|
+
setCohort,
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Gets the persisted cohort used for rollout calculations.
|
|
277
|
+
*/
|
|
278
|
+
getCohort,
|
|
279
|
+
|
|
259
280
|
/**
|
|
260
281
|
* Adds a listener to HotUpdater events.
|
|
261
282
|
*
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { INVALID_COHORT_ERROR_MESSAGE } from "@hot-updater/core";
|
|
2
|
+
import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const nativeModuleMock = vi.hoisted(() => {
|
|
5
|
+
const getManifest = vi.fn<() => Record<string, unknown> | string>();
|
|
6
|
+
const getCrashHistory = vi.fn<() => string[] | string>(() => []);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
clearCrashHistory: vi.fn(() => true),
|
|
10
|
+
getBaseURL: vi.fn<() => string | null>(() => null),
|
|
11
|
+
getBundleId: vi.fn<() => string | null>(() => "bundle-id"),
|
|
12
|
+
getCohort: vi.fn<() => string>(() => "123"),
|
|
13
|
+
getManifest,
|
|
14
|
+
getCrashHistory,
|
|
15
|
+
getConstants: vi.fn(() => ({
|
|
16
|
+
APP_VERSION: null,
|
|
17
|
+
CHANNEL: "production",
|
|
18
|
+
DEFAULT_CHANNEL: "production",
|
|
19
|
+
FINGERPRINT_HASH: null,
|
|
20
|
+
MIN_BUNDLE_ID: "min-bundle-id",
|
|
21
|
+
})),
|
|
22
|
+
notifyAppReady: vi.fn(),
|
|
23
|
+
reload: vi.fn(),
|
|
24
|
+
resetChannel: vi.fn(),
|
|
25
|
+
setCohort: vi.fn(),
|
|
26
|
+
setBundleURL: vi.fn(),
|
|
27
|
+
switchChannel: vi.fn(),
|
|
28
|
+
updateBundle: vi.fn(),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
vi.mock("react-native", () => ({
|
|
33
|
+
NativeEventEmitter: class {
|
|
34
|
+
addListener() {
|
|
35
|
+
return { remove: () => {} };
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
Platform: {
|
|
39
|
+
OS: "ios",
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
vi.mock("./specs/NativeHotUpdater", () => ({
|
|
44
|
+
default: nativeModuleMock,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
describe("notifyAppReady", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.resetModules();
|
|
50
|
+
nativeModuleMock.notifyAppReady.mockReset();
|
|
51
|
+
nativeModuleMock.getBaseURL.mockReset();
|
|
52
|
+
nativeModuleMock.getBundleId.mockReset();
|
|
53
|
+
nativeModuleMock.getCrashHistory.mockReset();
|
|
54
|
+
nativeModuleMock.getConstants.mockReturnValue({
|
|
55
|
+
APP_VERSION: null,
|
|
56
|
+
CHANNEL: "production",
|
|
57
|
+
DEFAULT_CHANNEL: "production",
|
|
58
|
+
FINGERPRINT_HASH: null,
|
|
59
|
+
MIN_BUNDLE_ID: "min-bundle-id",
|
|
60
|
+
});
|
|
61
|
+
nativeModuleMock.getBundleId.mockReturnValue("bundle-id");
|
|
62
|
+
nativeModuleMock.getBaseURL.mockReturnValue(null);
|
|
63
|
+
nativeModuleMock.getCrashHistory.mockReturnValue([]);
|
|
64
|
+
nativeModuleMock.getCohort.mockReset();
|
|
65
|
+
nativeModuleMock.getCohort.mockReturnValue("123");
|
|
66
|
+
nativeModuleMock.getManifest.mockReset();
|
|
67
|
+
nativeModuleMock.getManifest.mockReturnValue({
|
|
68
|
+
assets: {
|
|
69
|
+
"index.android.bundle": {
|
|
70
|
+
fileHash: "hash-123",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
bundleId: "bundle-id",
|
|
74
|
+
});
|
|
75
|
+
nativeModuleMock.resetChannel.mockReset();
|
|
76
|
+
nativeModuleMock.setCohort.mockReset();
|
|
77
|
+
nativeModuleMock.updateBundle.mockReset();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("normalizes legacy PROMOTED launch reports to STABLE", async () => {
|
|
81
|
+
nativeModuleMock.notifyAppReady.mockReturnValue(
|
|
82
|
+
JSON.stringify({ status: "PROMOTED" }),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const { notifyAppReady } = await import("./native");
|
|
86
|
+
|
|
87
|
+
expect(notifyAppReady()).toEqual({ status: "STABLE" });
|
|
88
|
+
expect(nativeModuleMock.notifyAppReady).toHaveBeenCalledWith();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns RECOVERED launch reports unchanged", async () => {
|
|
92
|
+
nativeModuleMock.notifyAppReady.mockReturnValue({
|
|
93
|
+
crashedBundleId: "bundle-123",
|
|
94
|
+
status: "RECOVERED",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const { notifyAppReady } = await import("./native");
|
|
98
|
+
|
|
99
|
+
expect(notifyAppReady()).toEqual({
|
|
100
|
+
crashedBundleId: "bundle-123",
|
|
101
|
+
status: "RECOVERED",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("falls back to STABLE for malformed old-arch payloads", async () => {
|
|
106
|
+
nativeModuleMock.notifyAppReady.mockReturnValue("{");
|
|
107
|
+
|
|
108
|
+
const { notifyAppReady } = await import("./native");
|
|
109
|
+
|
|
110
|
+
expect(notifyAppReady()).toEqual({ status: "STABLE" });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns the native bundle id when available", async () => {
|
|
114
|
+
nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
|
|
115
|
+
|
|
116
|
+
const { getBundleId } = await import("./native");
|
|
117
|
+
|
|
118
|
+
expect(getBundleId()).toBe("bundle-123");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("throws when native SDK does not expose getBundleId", async () => {
|
|
122
|
+
const nativeModule = nativeModuleMock as typeof nativeModuleMock & {
|
|
123
|
+
getBundleId?: typeof nativeModuleMock.getBundleId;
|
|
124
|
+
};
|
|
125
|
+
const originalGetBundleId = nativeModule.getBundleId;
|
|
126
|
+
nativeModule.getBundleId = null as unknown as Mock<() => string | null>;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const { getBundleId } = await import("./native");
|
|
130
|
+
|
|
131
|
+
expect(() => getBundleId()).toThrow(
|
|
132
|
+
"Native module is missing 'getBundleId()'",
|
|
133
|
+
);
|
|
134
|
+
} finally {
|
|
135
|
+
nativeModule.getBundleId = originalGetBundleId;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("falls back to MIN_BUNDLE_ID when native reports an empty bundle id", async () => {
|
|
140
|
+
nativeModuleMock.getBundleId.mockReturnValue("");
|
|
141
|
+
|
|
142
|
+
const { getBundleId } = await import("./native");
|
|
143
|
+
|
|
144
|
+
expect(getBundleId()).toBe("min-bundle-id");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("falls back to MIN_BUNDLE_ID when native bundle id is null", async () => {
|
|
148
|
+
nativeModuleMock.getBundleId.mockReturnValue(null);
|
|
149
|
+
|
|
150
|
+
const { getBundleId } = await import("./native");
|
|
151
|
+
|
|
152
|
+
expect(getBundleId()).toBe("min-bundle-id");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("falls back to MIN_BUNDLE_ID for legacy NIL_UUID bundle ids", async () => {
|
|
156
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
157
|
+
"00000000-0000-0000-0000-000000000000",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const { getBundleId } = await import("./native");
|
|
161
|
+
|
|
162
|
+
expect(getBundleId()).toBe("min-bundle-id");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("returns manifest from native objects", async () => {
|
|
166
|
+
nativeModuleMock.getManifest.mockReturnValue({
|
|
167
|
+
assets: {
|
|
168
|
+
"assets/logo.png": {
|
|
169
|
+
fileHash: "hash-logo",
|
|
170
|
+
},
|
|
171
|
+
"index.android.bundle": {
|
|
172
|
+
fileHash: "hash-bundle",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
bundleId: "bundle-123",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const { getManifest } = await import("./native");
|
|
179
|
+
|
|
180
|
+
expect(getManifest()).toEqual({
|
|
181
|
+
assets: {
|
|
182
|
+
"assets/logo.png": {
|
|
183
|
+
fileHash: "hash-logo",
|
|
184
|
+
},
|
|
185
|
+
"index.android.bundle": {
|
|
186
|
+
fileHash: "hash-bundle",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
bundleId: "bundle-123",
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("normalizes legacy manifest asset entries from native objects", async () => {
|
|
194
|
+
nativeModuleMock.getManifest.mockReturnValue({
|
|
195
|
+
assets: {
|
|
196
|
+
"assets/logo.png": "hash-logo",
|
|
197
|
+
},
|
|
198
|
+
bundleId: "bundle-123",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const { getManifest } = await import("./native");
|
|
202
|
+
|
|
203
|
+
expect(getManifest()).toEqual({
|
|
204
|
+
assets: {
|
|
205
|
+
"assets/logo.png": {
|
|
206
|
+
fileHash: "hash-logo",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
bundleId: "bundle-123",
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("parses manifest from old-arch JSON payloads", async () => {
|
|
214
|
+
nativeModuleMock.getManifest.mockReturnValue(
|
|
215
|
+
JSON.stringify({
|
|
216
|
+
assets: {
|
|
217
|
+
"assets/logo.png": {
|
|
218
|
+
fileHash: "hash-logo",
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
bundleId: "bundle-123",
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const { getManifest } = await import("./native");
|
|
226
|
+
|
|
227
|
+
expect(getManifest()).toEqual({
|
|
228
|
+
assets: {
|
|
229
|
+
"assets/logo.png": {
|
|
230
|
+
fileHash: "hash-logo",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
bundleId: "bundle-123",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("normalizes legacy manifest asset entries from old-arch JSON payloads", async () => {
|
|
238
|
+
nativeModuleMock.getManifest.mockReturnValue(
|
|
239
|
+
JSON.stringify({
|
|
240
|
+
assets: {
|
|
241
|
+
"assets/logo.png": "hash-logo",
|
|
242
|
+
},
|
|
243
|
+
bundleId: "bundle-123",
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const { getManifest } = await import("./native");
|
|
248
|
+
|
|
249
|
+
expect(getManifest()).toEqual({
|
|
250
|
+
assets: {
|
|
251
|
+
"assets/logo.png": {
|
|
252
|
+
fileHash: "hash-logo",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
bundleId: "bundle-123",
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("returns an empty-assets manifest for malformed payloads", async () => {
|
|
260
|
+
nativeModuleMock.getManifest.mockReturnValue("{");
|
|
261
|
+
|
|
262
|
+
const { getManifest } = await import("./native");
|
|
263
|
+
|
|
264
|
+
expect(getManifest()).toEqual({
|
|
265
|
+
assets: {},
|
|
266
|
+
bundleId: "bundle-id",
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("caches active bundle getters within a JS runtime", async () => {
|
|
271
|
+
nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
|
|
272
|
+
nativeModuleMock.getManifest.mockReturnValue({
|
|
273
|
+
assets: {
|
|
274
|
+
"assets/logo.png": {
|
|
275
|
+
fileHash: "hash-logo",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
bundleId: "bundle-123",
|
|
279
|
+
});
|
|
280
|
+
nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-123");
|
|
281
|
+
|
|
282
|
+
const { getBaseURL, getBundleId, getManifest } = await import("./native");
|
|
283
|
+
|
|
284
|
+
expect(getBundleId()).toBe("bundle-123");
|
|
285
|
+
expect(getBundleId()).toBe("bundle-123");
|
|
286
|
+
expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(1);
|
|
287
|
+
|
|
288
|
+
const firstManifest = getManifest();
|
|
289
|
+
firstManifest.assets["assets/logo.png"] = {
|
|
290
|
+
fileHash: "mutated-hash",
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
expect(getManifest()).toEqual({
|
|
294
|
+
assets: {
|
|
295
|
+
"assets/logo.png": {
|
|
296
|
+
fileHash: "hash-logo",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
bundleId: "bundle-123",
|
|
300
|
+
});
|
|
301
|
+
expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(1);
|
|
302
|
+
|
|
303
|
+
expect(getBaseURL()).toBe("file:///bundle-123");
|
|
304
|
+
expect(getBaseURL()).toBe("file:///bundle-123");
|
|
305
|
+
expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(1);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("invalidates cached bundle getters after updateBundle succeeds", async () => {
|
|
309
|
+
nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
|
|
310
|
+
nativeModuleMock.getManifest.mockReturnValue({
|
|
311
|
+
assets: {},
|
|
312
|
+
bundleId: "bundle-123",
|
|
313
|
+
});
|
|
314
|
+
nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-123");
|
|
315
|
+
nativeModuleMock.updateBundle.mockResolvedValue(true);
|
|
316
|
+
|
|
317
|
+
const { getBaseURL, getBundleId, getManifest, updateBundle } = await import(
|
|
318
|
+
"./native"
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
expect(getBundleId()).toBe("bundle-123");
|
|
322
|
+
expect(getManifest()).toEqual({
|
|
323
|
+
assets: {},
|
|
324
|
+
bundleId: "bundle-123",
|
|
325
|
+
});
|
|
326
|
+
expect(getBaseURL()).toBe("file:///bundle-123");
|
|
327
|
+
|
|
328
|
+
nativeModuleMock.getBundleId.mockReturnValue("bundle-456");
|
|
329
|
+
nativeModuleMock.getManifest.mockReturnValue({
|
|
330
|
+
assets: {},
|
|
331
|
+
bundleId: "bundle-456",
|
|
332
|
+
});
|
|
333
|
+
nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-456");
|
|
334
|
+
|
|
335
|
+
await updateBundle({
|
|
336
|
+
bundleId: "bundle-456",
|
|
337
|
+
fileHash: null,
|
|
338
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
339
|
+
status: "UPDATE",
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(getBundleId()).toBe("bundle-456");
|
|
343
|
+
expect(getManifest()).toEqual({
|
|
344
|
+
assets: {},
|
|
345
|
+
bundleId: "bundle-456",
|
|
346
|
+
});
|
|
347
|
+
expect(getBaseURL()).toBe("file:///bundle-456");
|
|
348
|
+
expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(2);
|
|
349
|
+
expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
|
|
350
|
+
expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("invalidates cached bundle getters after resetChannel succeeds", async () => {
|
|
354
|
+
nativeModuleMock.getConstants.mockReturnValue({
|
|
355
|
+
APP_VERSION: null,
|
|
356
|
+
CHANNEL: "beta",
|
|
357
|
+
DEFAULT_CHANNEL: "production",
|
|
358
|
+
FINGERPRINT_HASH: null,
|
|
359
|
+
MIN_BUNDLE_ID: "min-bundle-id",
|
|
360
|
+
});
|
|
361
|
+
nativeModuleMock.getBundleId.mockReturnValue("bundle-beta");
|
|
362
|
+
nativeModuleMock.getManifest.mockReturnValue({
|
|
363
|
+
assets: {},
|
|
364
|
+
bundleId: "bundle-beta",
|
|
365
|
+
});
|
|
366
|
+
nativeModuleMock.getBaseURL.mockReturnValue("file:///bundle-beta");
|
|
367
|
+
nativeModuleMock.resetChannel.mockResolvedValue(true);
|
|
368
|
+
|
|
369
|
+
const { getBaseURL, getBundleId, getManifest, resetChannel } = await import(
|
|
370
|
+
"./native"
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(getBundleId()).toBe("bundle-beta");
|
|
374
|
+
expect(getManifest()).toEqual({
|
|
375
|
+
assets: {},
|
|
376
|
+
bundleId: "bundle-beta",
|
|
377
|
+
});
|
|
378
|
+
expect(getBaseURL()).toBe("file:///bundle-beta");
|
|
379
|
+
|
|
380
|
+
nativeModuleMock.getBundleId.mockReturnValue(null);
|
|
381
|
+
nativeModuleMock.getManifest.mockReturnValue({});
|
|
382
|
+
nativeModuleMock.getBaseURL.mockReturnValue("");
|
|
383
|
+
|
|
384
|
+
await expect(resetChannel()).resolves.toBe(true);
|
|
385
|
+
|
|
386
|
+
expect(getBundleId()).toBe("min-bundle-id");
|
|
387
|
+
expect(getManifest()).toEqual({
|
|
388
|
+
assets: {},
|
|
389
|
+
bundleId: "min-bundle-id",
|
|
390
|
+
});
|
|
391
|
+
expect(getBaseURL()).toBeNull();
|
|
392
|
+
expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(2);
|
|
393
|
+
expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
|
|
394
|
+
expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("parses crash history from legacy JSON payloads", async () => {
|
|
398
|
+
nativeModuleMock.getCrashHistory.mockReturnValue(
|
|
399
|
+
JSON.stringify(["bundle-1", "bundle-2"]),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const { getCrashHistory } = await import("./native");
|
|
403
|
+
|
|
404
|
+
expect(getCrashHistory()).toEqual(["bundle-1", "bundle-2"]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("falls back to an empty crash history for malformed payloads", async () => {
|
|
408
|
+
nativeModuleMock.getCrashHistory.mockReturnValue("{");
|
|
409
|
+
|
|
410
|
+
const { getCrashHistory } = await import("./native");
|
|
411
|
+
|
|
412
|
+
expect(getCrashHistory()).toEqual([]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("passes normalized cohort overrides to native", async () => {
|
|
416
|
+
const { setCohort } = await import("./native");
|
|
417
|
+
|
|
418
|
+
setCohort(" QA-Group ");
|
|
419
|
+
|
|
420
|
+
expect(nativeModuleMock.setCohort).toHaveBeenCalledWith("qa-group");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("returns the most recently set cohort before native reads catch up", async () => {
|
|
424
|
+
nativeModuleMock.getCohort.mockReturnValue("123");
|
|
425
|
+
|
|
426
|
+
const { getCohort, setCohort } = await import("./native");
|
|
427
|
+
|
|
428
|
+
setCohort(" QA-Group ");
|
|
429
|
+
|
|
430
|
+
expect(getCohort()).toBe("qa-group");
|
|
431
|
+
expect(nativeModuleMock.getCohort).not.toHaveBeenCalled();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("throws when attempting to clear the cohort with an empty value", async () => {
|
|
435
|
+
const { setCohort } = await import("./native");
|
|
436
|
+
|
|
437
|
+
expect(() => setCohort("")).toThrow(INVALID_COHORT_ERROR_MESSAGE);
|
|
438
|
+
expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("throws for invalid cohort overrides", async () => {
|
|
442
|
+
const { setCohort } = await import("./native");
|
|
443
|
+
|
|
444
|
+
expect(() => setCohort("Bad Cohort")).toThrow(INVALID_COHORT_ERROR_MESSAGE);
|
|
445
|
+
expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("throws for cohort overrides longer than the limit", async () => {
|
|
449
|
+
const { setCohort } = await import("./native");
|
|
450
|
+
|
|
451
|
+
expect(() => setCohort("a".repeat(65))).toThrow(
|
|
452
|
+
INVALID_COHORT_ERROR_MESSAGE,
|
|
453
|
+
);
|
|
454
|
+
expect(nativeModuleMock.setCohort).not.toHaveBeenCalled();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("returns the cohort reported by native", async () => {
|
|
458
|
+
nativeModuleMock.getCohort.mockReturnValue("qa-group");
|
|
459
|
+
|
|
460
|
+
const { getCohort } = await import("./native");
|
|
461
|
+
|
|
462
|
+
expect(getCohort()).toBe("qa-group");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("normalizes the cohort reported by native", async () => {
|
|
466
|
+
nativeModuleMock.getCohort.mockReturnValue(" QA-GROUP ");
|
|
467
|
+
|
|
468
|
+
const { getCohort } = await import("./native");
|
|
469
|
+
|
|
470
|
+
expect(getCohort()).toBe("qa-group");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("throws when native reports an invalid cohort", async () => {
|
|
474
|
+
nativeModuleMock.getCohort.mockReturnValue("1001");
|
|
475
|
+
|
|
476
|
+
const { getCohort } = await import("./native");
|
|
477
|
+
|
|
478
|
+
expect(() => getCohort()).toThrow(INVALID_COHORT_ERROR_MESSAGE);
|
|
479
|
+
});
|
|
480
|
+
});
|