@hot-updater/react-native 0.30.11 → 0.31.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/HotUpdater.podspec +1 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
- package/android/src/newarch/HotUpdater.kt +14 -2
- package/android/src/newarch/HotUpdaterModule.kt +112 -1
- package/android/src/oldarch/HotUpdater.kt +14 -2
- package/android/src/oldarch/HotUpdaterModule.kt +107 -3
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
- package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
- package/lib/commonjs/DefaultResolver.js +18 -4
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/DefaultResolver.spec.js +136 -0
- package/lib/commonjs/DefaultResolver.spec.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +6 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +53 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +133 -1
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +40 -16
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +119 -3
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +8 -0
- package/lib/commonjs/sdkVersion.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +72 -5
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.spec.js +265 -8
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +19 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +74 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/DefaultResolver.js +18 -4
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/DefaultResolver.spec.js +135 -0
- package/lib/module/DefaultResolver.spec.js.map +1 -0
- package/lib/module/checkForUpdate.js +6 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +53 -18
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +133 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +40 -16
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +119 -3
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +4 -0
- package/lib/module/sdkVersion.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +72 -5
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.spec.js +265 -8
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +19 -5
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +74 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts +4 -3
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +37 -9
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +56 -3
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +5 -0
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +1 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +48 -32
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts +4 -3
- package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +2 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +37 -9
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +56 -3
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +2 -0
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +5 -0
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +1 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +48 -32
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +17 -9
- package/src/DefaultResolver.spec.ts +178 -0
- package/src/DefaultResolver.ts +32 -6
- package/src/checkForUpdate.ts +9 -2
- package/src/index.spec.ts +170 -2
- package/src/index.ts +100 -40
- package/src/native.spec.ts +139 -1
- package/src/native.ts +145 -31
- package/src/sdkVersion.ts +1 -0
- package/src/specs/NativeHotUpdater.ts +28 -0
- package/src/store.spec.ts +324 -9
- package/src/store.ts +133 -7
- package/src/types.ts +2 -0
- package/src/wrap.spec.ts +91 -0
- package/src/wrap.tsx +84 -45
package/src/checkForUpdate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppUpdateInfo } from "@hot-updater/core";
|
|
1
|
+
import type { AppUpdateAvailableInfo, AppUpdateInfo } from "@hot-updater/core";
|
|
2
2
|
import { Platform } from "react-native";
|
|
3
3
|
|
|
4
4
|
import { HotUpdaterError } from "./error";
|
|
@@ -42,7 +42,7 @@ export interface CheckForUpdateOptions {
|
|
|
42
42
|
requestTimeout?: number;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export type CheckForUpdateResult =
|
|
45
|
+
export type CheckForUpdateResult = AppUpdateAvailableInfo & {
|
|
46
46
|
/**
|
|
47
47
|
* Updates the bundle.
|
|
48
48
|
* This method is equivalent to `HotUpdater.updateBundle()` but with all required arguments pre-filled.
|
|
@@ -142,6 +142,10 @@ export async function checkForUpdate(
|
|
|
142
142
|
return null;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
if (updateInfo.status === "UP_TO_DATE") {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
145
149
|
if (
|
|
146
150
|
explicitChannel &&
|
|
147
151
|
explicitChannel !== defaultChannel &&
|
|
@@ -168,8 +172,11 @@ export async function checkForUpdate(
|
|
|
168
172
|
return updateBundle({
|
|
169
173
|
bundleId: updateInfo.id,
|
|
170
174
|
channel: runtimeChannel,
|
|
175
|
+
changedAssets: updateInfo.changedAssets ?? null,
|
|
171
176
|
fileUrl: updateInfo.fileUrl,
|
|
172
177
|
fileHash: updateInfo.fileHash,
|
|
178
|
+
manifestFileHash: updateInfo.manifestFileHash ?? null,
|
|
179
|
+
manifestUrl: updateInfo.manifestUrl ?? null,
|
|
173
180
|
status: updateInfo.status,
|
|
174
181
|
shouldSkipCurrentBundleIdCheck: isFirstRuntimeChannelSwitchAttempt,
|
|
175
182
|
});
|
package/src/index.spec.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
import type { HotUpdater as HotUpdaterValue } from "./index";
|
|
4
|
+
import type { HotUpdaterInitOptions, HotUpdaterOptions } from "./wrap";
|
|
5
|
+
|
|
3
6
|
const mocks = vi.hoisted(() => ({
|
|
4
7
|
addListener: vi.fn(() => () => {}),
|
|
5
8
|
checkForUpdate: vi.fn(),
|
|
@@ -74,7 +77,7 @@ describe("HotUpdater client initialization", () => {
|
|
|
74
77
|
|
|
75
78
|
mocks.addListener.mockReturnValue(() => {});
|
|
76
79
|
mocks.checkForUpdate.mockResolvedValue(null);
|
|
77
|
-
mocks.createDefaultResolver.mockImplementation((baseURL:
|
|
80
|
+
mocks.createDefaultResolver.mockImplementation((baseURL: unknown) => ({
|
|
78
81
|
baseURL,
|
|
79
82
|
checkUpdate: vi.fn(),
|
|
80
83
|
notifyAppReady: vi.fn(),
|
|
@@ -114,6 +117,171 @@ describe("HotUpdater client initialization", () => {
|
|
|
114
117
|
});
|
|
115
118
|
});
|
|
116
119
|
|
|
120
|
+
it("accepts dynamic baseURL resolvers for manual update flows", async () => {
|
|
121
|
+
const resolver = {
|
|
122
|
+
checkUpdate: vi.fn(),
|
|
123
|
+
notifyAppReady: vi.fn(),
|
|
124
|
+
};
|
|
125
|
+
const resolveBaseURL = vi.fn(() => "https://updates.example.com");
|
|
126
|
+
mocks.createDefaultResolver.mockReturnValue(resolver);
|
|
127
|
+
|
|
128
|
+
const HotUpdater = await importHotUpdater();
|
|
129
|
+
|
|
130
|
+
HotUpdater.init({
|
|
131
|
+
baseURL: resolveBaseURL,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(mocks.createDefaultResolver).toHaveBeenCalledWith(resolveBaseURL);
|
|
135
|
+
expect(mocks.init).toHaveBeenCalledWith({
|
|
136
|
+
resolver,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("accepts custom resolvers for manual update flows", async () => {
|
|
141
|
+
const resolver = {
|
|
142
|
+
checkUpdate: vi.fn(),
|
|
143
|
+
notifyAppReady: vi.fn(),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const HotUpdater = await importHotUpdater();
|
|
147
|
+
|
|
148
|
+
HotUpdater.init({
|
|
149
|
+
resolver,
|
|
150
|
+
requestHeaders: {
|
|
151
|
+
Authorization: "Bearer token",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(mocks.createDefaultResolver).not.toHaveBeenCalled();
|
|
156
|
+
expect(mocks.init).toHaveBeenCalledWith({
|
|
157
|
+
resolver,
|
|
158
|
+
requestHeaders: {
|
|
159
|
+
Authorization: "Bearer token",
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("accepts onError during init and uses it for later manual update checks", async () => {
|
|
165
|
+
const resolver = {
|
|
166
|
+
checkUpdate: vi.fn(),
|
|
167
|
+
notifyAppReady: vi.fn(),
|
|
168
|
+
};
|
|
169
|
+
const onError = vi.fn();
|
|
170
|
+
mocks.createDefaultResolver.mockReturnValue(resolver);
|
|
171
|
+
|
|
172
|
+
const HotUpdater = await importHotUpdater();
|
|
173
|
+
|
|
174
|
+
HotUpdater.init({
|
|
175
|
+
baseURL: "https://updates.example.com",
|
|
176
|
+
onError,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await HotUpdater.checkForUpdate({
|
|
180
|
+
updateStrategy: "appVersion",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(mocks.init).toHaveBeenCalledWith({
|
|
184
|
+
onError,
|
|
185
|
+
resolver,
|
|
186
|
+
});
|
|
187
|
+
expect(mocks.checkForUpdate).toHaveBeenCalledWith({
|
|
188
|
+
onError,
|
|
189
|
+
requestHeaders: {},
|
|
190
|
+
requestTimeout: undefined,
|
|
191
|
+
resolver,
|
|
192
|
+
updateStrategy: "appVersion",
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("lets checkForUpdate override the init onError handler", async () => {
|
|
197
|
+
const resolver = {
|
|
198
|
+
checkUpdate: vi.fn(),
|
|
199
|
+
notifyAppReady: vi.fn(),
|
|
200
|
+
};
|
|
201
|
+
const initOnError = vi.fn();
|
|
202
|
+
const checkOnError = vi.fn();
|
|
203
|
+
mocks.createDefaultResolver.mockReturnValue(resolver);
|
|
204
|
+
|
|
205
|
+
const HotUpdater = await importHotUpdater();
|
|
206
|
+
|
|
207
|
+
HotUpdater.init({
|
|
208
|
+
baseURL: "https://updates.example.com",
|
|
209
|
+
onError: initOnError,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await HotUpdater.checkForUpdate({
|
|
213
|
+
onError: checkOnError,
|
|
214
|
+
updateStrategy: "appVersion",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(mocks.checkForUpdate).toHaveBeenCalledWith({
|
|
218
|
+
onError: checkOnError,
|
|
219
|
+
requestHeaders: {},
|
|
220
|
+
requestTimeout: undefined,
|
|
221
|
+
resolver,
|
|
222
|
+
updateStrategy: "appVersion",
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("defaults wrap to automatic update mode", async () => {
|
|
227
|
+
const resolver = {
|
|
228
|
+
checkUpdate: vi.fn(),
|
|
229
|
+
notifyAppReady: vi.fn(),
|
|
230
|
+
};
|
|
231
|
+
mocks.createDefaultResolver.mockReturnValue(resolver);
|
|
232
|
+
|
|
233
|
+
const HotUpdater = await importHotUpdater();
|
|
234
|
+
|
|
235
|
+
HotUpdater.wrap({
|
|
236
|
+
baseURL: "https://updates.example.com",
|
|
237
|
+
updateStrategy: "appVersion",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(mocks.wrap).toHaveBeenCalledWith({
|
|
241
|
+
resolver,
|
|
242
|
+
updateMode: "auto",
|
|
243
|
+
updateStrategy: "appVersion",
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("keeps deprecated manual wrap calls working", async () => {
|
|
248
|
+
const resolver = {
|
|
249
|
+
checkUpdate: vi.fn(),
|
|
250
|
+
notifyAppReady: vi.fn(),
|
|
251
|
+
};
|
|
252
|
+
mocks.createDefaultResolver.mockReturnValue(resolver);
|
|
253
|
+
|
|
254
|
+
const HotUpdater = await importHotUpdater();
|
|
255
|
+
|
|
256
|
+
HotUpdater.wrap({
|
|
257
|
+
baseURL: "https://updates.example.com",
|
|
258
|
+
updateMode: "manual",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(mocks.wrap).toHaveBeenCalledWith({
|
|
262
|
+
resolver,
|
|
263
|
+
updateMode: "manual",
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("types public wrap to accept pre-typed option unions", () => {
|
|
268
|
+
const compileOnly = (
|
|
269
|
+
wrap: typeof HotUpdaterValue.wrap,
|
|
270
|
+
options: HotUpdaterOptions,
|
|
271
|
+
) => wrap(options);
|
|
272
|
+
|
|
273
|
+
expect(compileOnly).toBeTypeOf("function");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("types public init options with onError", () => {
|
|
277
|
+
const options = {
|
|
278
|
+
baseURL: "https://updates.example.com",
|
|
279
|
+
onError: vi.fn(),
|
|
280
|
+
} satisfies HotUpdaterInitOptions;
|
|
281
|
+
|
|
282
|
+
expect(options.baseURL).toBe("https://updates.example.com");
|
|
283
|
+
});
|
|
284
|
+
|
|
117
285
|
it("uses init configuration for later manual update checks", async () => {
|
|
118
286
|
const resolver = {
|
|
119
287
|
checkUpdate: vi.fn(),
|
|
@@ -163,7 +331,7 @@ describe("HotUpdater client initialization", () => {
|
|
|
163
331
|
const HotUpdater = await importHotUpdater();
|
|
164
332
|
|
|
165
333
|
expect(() => HotUpdater.init({} as never)).toThrow(
|
|
166
|
-
"baseURL must be provided",
|
|
334
|
+
"Either baseURL or resolver must be provided",
|
|
167
335
|
);
|
|
168
336
|
expect(mocks.init).not.toHaveBeenCalled();
|
|
169
337
|
});
|
package/src/index.ts
CHANGED
|
@@ -28,17 +28,21 @@ import {
|
|
|
28
28
|
import { hotUpdaterStore } from "./store";
|
|
29
29
|
import type { HotUpdaterResolver } from "./types";
|
|
30
30
|
import {
|
|
31
|
+
type AutoUpdateOptions,
|
|
31
32
|
type HotUpdaterInitOptions,
|
|
32
33
|
type HotUpdaterOptions,
|
|
33
34
|
init,
|
|
34
35
|
type InternalInitOptions,
|
|
35
36
|
type InternalWrapOptions,
|
|
37
|
+
type ManualUpdateOptions,
|
|
36
38
|
wrap,
|
|
37
39
|
} from "./wrap";
|
|
38
40
|
|
|
39
41
|
export type {
|
|
40
42
|
CustomReloadHandler,
|
|
41
43
|
HotUpdaterEvent,
|
|
44
|
+
HotUpdaterProgressArtifactType,
|
|
45
|
+
HotUpdaterProgressEvent,
|
|
42
46
|
Manifest,
|
|
43
47
|
ManifestAsset,
|
|
44
48
|
NotifyAppReadyResult,
|
|
@@ -48,6 +52,7 @@ export type {
|
|
|
48
52
|
export * from "./store";
|
|
49
53
|
export {
|
|
50
54
|
extractSignatureFailure,
|
|
55
|
+
type HotUpdaterBaseURL,
|
|
51
56
|
type HotUpdaterResolver,
|
|
52
57
|
isSignatureVerificationError,
|
|
53
58
|
type ResolverCheckUpdateParams,
|
|
@@ -55,8 +60,10 @@ export {
|
|
|
55
60
|
type SignatureVerificationFailure,
|
|
56
61
|
} from "./types";
|
|
57
62
|
export type {
|
|
63
|
+
HotUpdaterFallbackComponentProps,
|
|
58
64
|
HotUpdaterInitOptions,
|
|
59
65
|
HotUpdaterOptions,
|
|
66
|
+
ManualUpdateOptions,
|
|
60
67
|
RunUpdateProcessResponse,
|
|
61
68
|
} from "./wrap";
|
|
62
69
|
|
|
@@ -78,6 +85,33 @@ const registerGlobalGetBaseURL = () => {
|
|
|
78
85
|
// Call registration immediately on module load
|
|
79
86
|
registerGlobalGetBaseURL();
|
|
80
87
|
|
|
88
|
+
type HotUpdaterWrap = {
|
|
89
|
+
(options: AutoUpdateOptions): ReturnType<typeof wrap>;
|
|
90
|
+
/**
|
|
91
|
+
* @deprecated Replace manual `HotUpdater.wrap` with `HotUpdater.init`.
|
|
92
|
+
*
|
|
93
|
+
* ```tsx
|
|
94
|
+
* import { HotUpdater } from "@hot-updater/react-native";
|
|
95
|
+
*
|
|
96
|
+
* HotUpdater.init({
|
|
97
|
+
* baseURL: "<your-update-server-url>",
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* export default App;
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* Then call `HotUpdater.checkForUpdate(...)` from your manual update flow.
|
|
104
|
+
* See https://hot-updater.dev/docs/guides/custom-update
|
|
105
|
+
*/
|
|
106
|
+
(options: ManualUpdateOptions): ReturnType<typeof wrap>;
|
|
107
|
+
(options: HotUpdaterOptions): ReturnType<typeof wrap>;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isManualWrapOptions = (
|
|
111
|
+
options: HotUpdaterOptions,
|
|
112
|
+
): options is ManualUpdateOptions =>
|
|
113
|
+
"updateMode" in options && options.updateMode === "manual";
|
|
114
|
+
|
|
81
115
|
/**
|
|
82
116
|
* Creates a HotUpdater client instance with all update management methods.
|
|
83
117
|
* This function is called once on module initialization to create a singleton instance.
|
|
@@ -88,6 +122,7 @@ function createHotUpdaterClient() {
|
|
|
88
122
|
resolver: HotUpdaterResolver | null;
|
|
89
123
|
requestHeaders?: Record<string, string>;
|
|
90
124
|
requestTimeout?: number;
|
|
125
|
+
onError?: (error: unknown) => void;
|
|
91
126
|
} = {
|
|
92
127
|
resolver: null,
|
|
93
128
|
};
|
|
@@ -95,54 +130,67 @@ function createHotUpdaterClient() {
|
|
|
95
130
|
const createMissingNetworkConfigError = (apiName: "init" | "wrap") => {
|
|
96
131
|
if (apiName === "init") {
|
|
97
132
|
return new Error(
|
|
98
|
-
`[HotUpdater] baseURL must be provided.\n\n` +
|
|
99
|
-
`Configure HotUpdater before calling update APIs:\n\n` +
|
|
133
|
+
`[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
|
|
134
|
+
`Configure HotUpdater before calling update APIs with the standard baseURL setup:\n\n` +
|
|
100
135
|
` HotUpdater.init({\n` +
|
|
101
136
|
` baseURL: "<your-update-server-url>",\n` +
|
|
102
137
|
` });\n\n` +
|
|
103
|
-
`For
|
|
138
|
+
`For manual update flows, visit: https://hot-updater.dev/docs/guides/custom-update`,
|
|
104
139
|
);
|
|
105
140
|
}
|
|
106
141
|
|
|
107
142
|
const baseURLExample =
|
|
108
143
|
` export default HotUpdater.wrap({\n` +
|
|
109
144
|
` baseURL: "<your-update-server-url>",\n` +
|
|
110
|
-
` updateStrategy: "appVersion"
|
|
111
|
-
` updateMode: "auto"\n` +
|
|
112
|
-
` })(App);\n\n`;
|
|
113
|
-
|
|
114
|
-
const resolverExample =
|
|
115
|
-
` export default HotUpdater.wrap({\n` +
|
|
116
|
-
` resolver: {\n` +
|
|
117
|
-
` checkUpdate: async (params) => { /* custom logic */ },\n` +
|
|
118
|
-
` notifyAppReady: async (params) => { /* custom logic */ }\n` +
|
|
119
|
-
` },\n` +
|
|
120
|
-
` updateMode: "manual"\n` +
|
|
145
|
+
` updateStrategy: "appVersion"\n` +
|
|
121
146
|
` })(App);\n\n`;
|
|
122
147
|
|
|
123
148
|
return new Error(
|
|
124
149
|
`[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
|
|
125
|
-
`
|
|
150
|
+
`Configure HotUpdater.wrap with the standard baseURL setup:\n\n` +
|
|
126
151
|
baseURLExample +
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
`For more information, visit: https://hot-updater.dev/docs/react-native-api/${apiName}`,
|
|
152
|
+
`For manual update flows, use HotUpdater.init() and visit: ` +
|
|
153
|
+
`https://hot-updater.dev/docs/guides/custom-update`,
|
|
130
154
|
);
|
|
131
155
|
};
|
|
132
156
|
|
|
133
157
|
const normalizeOptions = (
|
|
134
158
|
options: HotUpdaterOptions,
|
|
135
159
|
): InternalWrapOptions => {
|
|
136
|
-
if (
|
|
137
|
-
|
|
160
|
+
if (isManualWrapOptions(options)) {
|
|
161
|
+
if ("baseURL" in options && options.baseURL) {
|
|
162
|
+
const { baseURL, ...rest } = options;
|
|
163
|
+
return {
|
|
164
|
+
...rest,
|
|
165
|
+
resolver: createDefaultResolver(baseURL),
|
|
166
|
+
updateMode: "manual",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if ("resolver" in options && options.resolver) {
|
|
171
|
+
return {
|
|
172
|
+
...options,
|
|
173
|
+
updateMode: "manual",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const autoOptions = options as AutoUpdateOptions;
|
|
179
|
+
|
|
180
|
+
if ("baseURL" in autoOptions && autoOptions.baseURL) {
|
|
181
|
+
const { baseURL, ...rest } = autoOptions;
|
|
138
182
|
return {
|
|
139
183
|
...rest,
|
|
140
184
|
resolver: createDefaultResolver(baseURL),
|
|
185
|
+
updateMode: "auto",
|
|
141
186
|
};
|
|
142
187
|
}
|
|
143
188
|
|
|
144
|
-
if ("resolver" in
|
|
145
|
-
return
|
|
189
|
+
if ("resolver" in autoOptions && autoOptions.resolver) {
|
|
190
|
+
return {
|
|
191
|
+
...autoOptions,
|
|
192
|
+
updateMode: "auto",
|
|
193
|
+
};
|
|
146
194
|
}
|
|
147
195
|
|
|
148
196
|
throw createMissingNetworkConfigError("wrap");
|
|
@@ -151,21 +199,23 @@ function createHotUpdaterClient() {
|
|
|
151
199
|
const normalizeInitOptions = (
|
|
152
200
|
options: HotUpdaterInitOptions,
|
|
153
201
|
): InternalInitOptions => {
|
|
154
|
-
const {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (baseURL) {
|
|
202
|
+
const { updateMode: _updateMode, ...rest } =
|
|
203
|
+
options as HotUpdaterInitOptions & {
|
|
204
|
+
updateMode?: unknown;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if ("baseURL" in rest && rest.baseURL) {
|
|
208
|
+
const { baseURL, ...baseURLRest } = rest;
|
|
163
209
|
return {
|
|
164
|
-
...
|
|
210
|
+
...baseURLRest,
|
|
165
211
|
resolver: createDefaultResolver(baseURL),
|
|
166
212
|
};
|
|
167
213
|
}
|
|
168
214
|
|
|
215
|
+
if ("resolver" in rest && rest.resolver) {
|
|
216
|
+
return rest;
|
|
217
|
+
}
|
|
218
|
+
|
|
169
219
|
throw createMissingNetworkConfigError("init");
|
|
170
220
|
};
|
|
171
221
|
|
|
@@ -176,6 +226,7 @@ function createHotUpdaterClient() {
|
|
|
176
226
|
globalConfig.resolver = normalizedOptions.resolver;
|
|
177
227
|
globalConfig.requestHeaders = options.requestHeaders;
|
|
178
228
|
globalConfig.requestTimeout = options.requestTimeout;
|
|
229
|
+
globalConfig.onError = options.onError;
|
|
179
230
|
};
|
|
180
231
|
|
|
181
232
|
const ensureGlobalResolver = (methodName: string) => {
|
|
@@ -186,15 +237,13 @@ function createHotUpdaterClient() {
|
|
|
186
237
|
`Option 1: With HotUpdater.wrap()\n` +
|
|
187
238
|
` export default HotUpdater.wrap({\n` +
|
|
188
239
|
` baseURL: "<your-update-server-url>",\n` +
|
|
189
|
-
` updateStrategy: "appVersion"
|
|
190
|
-
` updateMode: "auto"\n` +
|
|
240
|
+
` updateStrategy: "appVersion"\n` +
|
|
191
241
|
` })(App);\n\n` +
|
|
192
242
|
`Option 2: With HotUpdater.init() for custom runtimes\n` +
|
|
193
243
|
` HotUpdater.init({\n` +
|
|
194
244
|
` baseURL: "<your-update-server-url>",\n` +
|
|
195
245
|
` });\n\n` +
|
|
196
|
-
`For
|
|
197
|
-
`or https://hot-updater.dev/docs/react-native-api/init`,
|
|
246
|
+
`For manual update flows, visit: https://hot-updater.dev/docs/guides/custom-update`,
|
|
198
247
|
);
|
|
199
248
|
}
|
|
200
249
|
return globalConfig.resolver;
|
|
@@ -218,7 +267,6 @@ function createHotUpdaterClient() {
|
|
|
218
267
|
* export default HotUpdater.wrap({
|
|
219
268
|
* baseURL: "<your-update-server-url>",
|
|
220
269
|
* updateStrategy: "appVersion",
|
|
221
|
-
* updateMode: "auto",
|
|
222
270
|
* requestHeaders: {
|
|
223
271
|
* "Authorization": "Bearer <your-access-token>",
|
|
224
272
|
* },
|
|
@@ -236,8 +284,8 @@ function createHotUpdaterClient() {
|
|
|
236
284
|
* Initializes HotUpdater without wrapping a React component.
|
|
237
285
|
*
|
|
238
286
|
* Use this for manual update flows in runtimes where a root component HOC
|
|
239
|
-
* is not convenient.
|
|
240
|
-
* `HotUpdater.wrap({ updateMode: "manual" })(App)
|
|
287
|
+
* is not convenient. Replace deprecated manual
|
|
288
|
+
* `HotUpdater.wrap({ updateMode: "manual" })(App)` usage with this API.
|
|
241
289
|
*
|
|
242
290
|
* @example
|
|
243
291
|
* ```tsx
|
|
@@ -419,6 +467,7 @@ function createHotUpdaterClient() {
|
|
|
419
467
|
...config.requestHeaders,
|
|
420
468
|
},
|
|
421
469
|
requestTimeout: config.requestTimeout ?? globalConfig.requestTimeout,
|
|
470
|
+
onError: config.onError ?? globalConfig.onError,
|
|
422
471
|
};
|
|
423
472
|
|
|
424
473
|
return checkForUpdate(mergedConfig);
|
|
@@ -471,8 +520,12 @@ function createHotUpdaterClient() {
|
|
|
471
520
|
const ok = await resetChannel();
|
|
472
521
|
if (ok) {
|
|
473
522
|
hotUpdaterStore.setState({
|
|
523
|
+
artifactType: null,
|
|
524
|
+
details: null,
|
|
525
|
+
downloadedBytes: undefined,
|
|
474
526
|
isUpdateDownloaded: false,
|
|
475
527
|
progress: 0,
|
|
528
|
+
totalBytes: undefined,
|
|
476
529
|
});
|
|
477
530
|
}
|
|
478
531
|
return ok;
|
|
@@ -521,4 +574,11 @@ function createHotUpdaterClient() {
|
|
|
521
574
|
};
|
|
522
575
|
}
|
|
523
576
|
|
|
524
|
-
|
|
577
|
+
type HotUpdaterClient = Omit<
|
|
578
|
+
ReturnType<typeof createHotUpdaterClient>,
|
|
579
|
+
"wrap"
|
|
580
|
+
> & {
|
|
581
|
+
wrap: HotUpdaterWrap;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
export const HotUpdater: HotUpdaterClient = createHotUpdaterClient();
|
package/src/native.spec.ts
CHANGED
|
@@ -167,6 +167,7 @@ describe("notifyAppReady", () => {
|
|
|
167
167
|
assets: {
|
|
168
168
|
"assets/logo.png": {
|
|
169
169
|
fileHash: "hash-logo",
|
|
170
|
+
signature: "sig-logo",
|
|
170
171
|
},
|
|
171
172
|
"index.android.bundle": {
|
|
172
173
|
fileHash: "hash-bundle",
|
|
@@ -181,6 +182,7 @@ describe("notifyAppReady", () => {
|
|
|
181
182
|
assets: {
|
|
182
183
|
"assets/logo.png": {
|
|
183
184
|
fileHash: "hash-logo",
|
|
185
|
+
signature: "sig-logo",
|
|
184
186
|
},
|
|
185
187
|
"index.android.bundle": {
|
|
186
188
|
fileHash: "hash-bundle",
|
|
@@ -337,11 +339,147 @@ describe("notifyAppReady", () => {
|
|
|
337
339
|
bundleId: "bundle-123",
|
|
338
340
|
});
|
|
339
341
|
expect(getBaseURL()).toBe("file:///bundle-123");
|
|
340
|
-
expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(
|
|
342
|
+
expect(nativeModuleMock.getBundleId).toHaveBeenCalledTimes(3);
|
|
341
343
|
expect(nativeModuleMock.getManifest).toHaveBeenCalledTimes(2);
|
|
342
344
|
expect(nativeModuleMock.getBaseURL).toHaveBeenCalledTimes(2);
|
|
343
345
|
});
|
|
344
346
|
|
|
347
|
+
it("reinstalls a session-installed bundle after native resets to built-in", async () => {
|
|
348
|
+
nativeModuleMock.getConstants.mockReturnValue({
|
|
349
|
+
APP_VERSION: null,
|
|
350
|
+
CHANNEL: "production",
|
|
351
|
+
DEFAULT_CHANNEL: "production",
|
|
352
|
+
FINGERPRINT_HASH: null,
|
|
353
|
+
MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
|
|
354
|
+
});
|
|
355
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
356
|
+
"00000000-0000-0000-0000-000000000002",
|
|
357
|
+
);
|
|
358
|
+
nativeModuleMock.updateBundle.mockResolvedValue(true);
|
|
359
|
+
|
|
360
|
+
const { getBundleId, updateBundle } = await import("./native");
|
|
361
|
+
|
|
362
|
+
await updateBundle({
|
|
363
|
+
bundleId: "00000000-0000-0000-0000-000000000003",
|
|
364
|
+
fileHash: null,
|
|
365
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
366
|
+
status: "UPDATE",
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
370
|
+
"00000000-0000-0000-0000-000000000003",
|
|
371
|
+
);
|
|
372
|
+
expect(getBundleId()).toBe("00000000-0000-0000-0000-000000000003");
|
|
373
|
+
|
|
374
|
+
nativeModuleMock.getBundleId.mockReturnValue(null);
|
|
375
|
+
|
|
376
|
+
await expect(
|
|
377
|
+
updateBundle({
|
|
378
|
+
bundleId: "00000000-0000-0000-0000-000000000003",
|
|
379
|
+
fileHash: null,
|
|
380
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
381
|
+
status: "UPDATE",
|
|
382
|
+
}),
|
|
383
|
+
).resolves.toBe(true);
|
|
384
|
+
|
|
385
|
+
expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(2);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("skips a session-installed bundle when native still reports it active", async () => {
|
|
389
|
+
nativeModuleMock.getConstants.mockReturnValue({
|
|
390
|
+
APP_VERSION: null,
|
|
391
|
+
CHANNEL: "production",
|
|
392
|
+
DEFAULT_CHANNEL: "production",
|
|
393
|
+
FINGERPRINT_HASH: null,
|
|
394
|
+
MIN_BUNDLE_ID: "00000000-0000-0000-0000-000000000001",
|
|
395
|
+
});
|
|
396
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
397
|
+
"00000000-0000-0000-0000-000000000002",
|
|
398
|
+
);
|
|
399
|
+
nativeModuleMock.updateBundle.mockResolvedValue(true);
|
|
400
|
+
|
|
401
|
+
const { updateBundle } = await import("./native");
|
|
402
|
+
|
|
403
|
+
await updateBundle({
|
|
404
|
+
bundleId: "00000000-0000-0000-0000-000000000003",
|
|
405
|
+
fileHash: null,
|
|
406
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
407
|
+
status: "UPDATE",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
nativeModuleMock.getBundleId.mockReturnValue(
|
|
411
|
+
"00000000-0000-0000-0000-000000000003",
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
await expect(
|
|
415
|
+
updateBundle({
|
|
416
|
+
bundleId: "00000000-0000-0000-0000-000000000003",
|
|
417
|
+
fileHash: null,
|
|
418
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
419
|
+
status: "UPDATE",
|
|
420
|
+
}),
|
|
421
|
+
).resolves.toBe(true);
|
|
422
|
+
|
|
423
|
+
expect(nativeModuleMock.updateBundle).toHaveBeenCalledTimes(1);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("forwards manifest artifact parameters to native updateBundle", async () => {
|
|
427
|
+
nativeModuleMock.getBundleId.mockReturnValue("bundle-123");
|
|
428
|
+
nativeModuleMock.updateBundle.mockResolvedValue(true);
|
|
429
|
+
|
|
430
|
+
const { updateBundle } = await import("./native");
|
|
431
|
+
|
|
432
|
+
await updateBundle({
|
|
433
|
+
bundleId: "bundle-789",
|
|
434
|
+
changedAssets: {
|
|
435
|
+
"index.ios.bundle": {
|
|
436
|
+
file: {
|
|
437
|
+
compression: "br",
|
|
438
|
+
url: "https://example.com/files/index.ios.bundle.br",
|
|
439
|
+
},
|
|
440
|
+
fileHash: "hash-next",
|
|
441
|
+
patch: {
|
|
442
|
+
algorithm: "bsdiff",
|
|
443
|
+
baseBundleId: "bundle-123",
|
|
444
|
+
baseFileHash: "hash-prev",
|
|
445
|
+
patchFileHash: "hash-patch",
|
|
446
|
+
patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
fileHash: "sig:archive",
|
|
451
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
452
|
+
manifestFileHash: "sig:manifest",
|
|
453
|
+
manifestUrl: "https://example.com/manifest.json",
|
|
454
|
+
status: "UPDATE",
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
expect(nativeModuleMock.updateBundle).toHaveBeenCalledWith({
|
|
458
|
+
bundleId: "bundle-789",
|
|
459
|
+
changedAssets: {
|
|
460
|
+
"index.ios.bundle": {
|
|
461
|
+
file: {
|
|
462
|
+
compression: "br",
|
|
463
|
+
url: "https://example.com/files/index.ios.bundle.br",
|
|
464
|
+
},
|
|
465
|
+
fileHash: "hash-next",
|
|
466
|
+
patch: {
|
|
467
|
+
algorithm: "bsdiff",
|
|
468
|
+
baseBundleId: "bundle-123",
|
|
469
|
+
baseFileHash: "hash-prev",
|
|
470
|
+
patchFileHash: "hash-patch",
|
|
471
|
+
patchUrl: "https://example.com/files/index.ios.bundle.bsdiff",
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
channel: undefined,
|
|
476
|
+
fileHash: "sig:archive",
|
|
477
|
+
fileUrl: "https://example.com/bundle.zip",
|
|
478
|
+
manifestFileHash: "sig:manifest",
|
|
479
|
+
manifestUrl: "https://example.com/manifest.json",
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
345
483
|
it("invalidates cached bundle getters after resetChannel succeeds", async () => {
|
|
346
484
|
nativeModuleMock.getConstants.mockReturnValue({
|
|
347
485
|
APP_VERSION: null,
|