@hot-updater/react-native 0.30.7 → 0.30.8
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/lib/commonjs/index.js +41 -19
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +155 -0
- package/lib/commonjs/index.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +14 -0
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +76 -0
- package/lib/commonjs/wrap.spec.js.map +1 -0
- package/lib/module/index.js +42 -20
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +154 -0
- package/lib/module/index.spec.js.map +1 -0
- package/lib/module/wrap.js +13 -0
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +75 -0
- package/lib/module/wrap.spec.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +10 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +2 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +10 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +2 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/index.spec.ts +177 -0
- package/src/index.ts +100 -39
- package/src/wrap.spec.ts +82 -0
- package/src/wrap.tsx +26 -0
package/src/index.ts
CHANGED
|
@@ -27,7 +27,13 @@ import {
|
|
|
27
27
|
} from "./native";
|
|
28
28
|
import { hotUpdaterStore } from "./store";
|
|
29
29
|
import type { HotUpdaterResolver } from "./types";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
type HotUpdaterOptions,
|
|
32
|
+
init,
|
|
33
|
+
type InternalWrapOptions,
|
|
34
|
+
type ManualUpdateOptions,
|
|
35
|
+
wrap,
|
|
36
|
+
} from "./wrap";
|
|
31
37
|
|
|
32
38
|
export type {
|
|
33
39
|
CustomReloadHandler,
|
|
@@ -48,6 +54,7 @@ export {
|
|
|
48
54
|
type SignatureVerificationFailure,
|
|
49
55
|
} from "./types";
|
|
50
56
|
export type { HotUpdaterOptions, RunUpdateProcessResponse } from "./wrap";
|
|
57
|
+
export type { ManualUpdateOptions as HotUpdaterInitOptions } from "./wrap";
|
|
51
58
|
|
|
52
59
|
/**
|
|
53
60
|
* Register getBaseURL to global objects for use without imports.
|
|
@@ -81,23 +88,88 @@ function createHotUpdaterClient() {
|
|
|
81
88
|
resolver: null,
|
|
82
89
|
};
|
|
83
90
|
|
|
91
|
+
const normalizeOptions = (
|
|
92
|
+
options: HotUpdaterOptions,
|
|
93
|
+
apiName: "init" | "wrap",
|
|
94
|
+
): InternalWrapOptions => {
|
|
95
|
+
if ("baseURL" in options && options.baseURL) {
|
|
96
|
+
const { baseURL, ...rest } = options;
|
|
97
|
+
return {
|
|
98
|
+
...rest,
|
|
99
|
+
resolver: createDefaultResolver(baseURL),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if ("resolver" in options && options.resolver) {
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const baseURLExample =
|
|
108
|
+
apiName === "wrap"
|
|
109
|
+
? ` export default HotUpdater.wrap({\n` +
|
|
110
|
+
` baseURL: "<your-update-server-url>",\n` +
|
|
111
|
+
` updateStrategy: "appVersion",\n` +
|
|
112
|
+
` updateMode: "auto"\n` +
|
|
113
|
+
` })(App);\n\n`
|
|
114
|
+
: ` HotUpdater.init({\n` +
|
|
115
|
+
` baseURL: "<your-update-server-url>",\n` +
|
|
116
|
+
` updateMode: "manual"\n` +
|
|
117
|
+
` });\n\n`;
|
|
118
|
+
|
|
119
|
+
const resolverExample =
|
|
120
|
+
apiName === "wrap"
|
|
121
|
+
? ` export default HotUpdater.wrap({\n` +
|
|
122
|
+
` resolver: {\n` +
|
|
123
|
+
` checkUpdate: async (params) => { /* custom logic */ },\n` +
|
|
124
|
+
` notifyAppReady: async (params) => { /* custom logic */ }\n` +
|
|
125
|
+
` },\n` +
|
|
126
|
+
` updateMode: "manual"\n` +
|
|
127
|
+
` })(App);\n\n`
|
|
128
|
+
: ` HotUpdater.init({\n` +
|
|
129
|
+
` resolver: {\n` +
|
|
130
|
+
` checkUpdate: async (params) => { /* custom logic */ },\n` +
|
|
131
|
+
` notifyAppReady: async (params) => { /* custom logic */ }\n` +
|
|
132
|
+
` },\n` +
|
|
133
|
+
` updateMode: "manual"\n` +
|
|
134
|
+
` });\n\n`;
|
|
135
|
+
|
|
136
|
+
throw new Error(
|
|
137
|
+
`[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
|
|
138
|
+
`Option 1: Using baseURL (recommended for most cases)\n` +
|
|
139
|
+
baseURLExample +
|
|
140
|
+
`Option 2: Using custom resolver (advanced)\n` +
|
|
141
|
+
resolverExample +
|
|
142
|
+
`For more information, visit: https://hot-updater.dev/docs/react-native-api/${apiName}`,
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const configureGlobal = (
|
|
147
|
+
normalizedOptions: InternalWrapOptions,
|
|
148
|
+
options: HotUpdaterOptions,
|
|
149
|
+
) => {
|
|
150
|
+
globalConfig.resolver = normalizedOptions.resolver;
|
|
151
|
+
globalConfig.requestHeaders = options.requestHeaders;
|
|
152
|
+
globalConfig.requestTimeout = options.requestTimeout;
|
|
153
|
+
};
|
|
154
|
+
|
|
84
155
|
const ensureGlobalResolver = (methodName: string) => {
|
|
85
156
|
if (!globalConfig.resolver) {
|
|
86
157
|
throw new Error(
|
|
87
|
-
`[HotUpdater] ${methodName} requires HotUpdater.wrap() to be used.\n\n` +
|
|
88
|
-
`To fix this issue,
|
|
89
|
-
`Option 1: With
|
|
158
|
+
`[HotUpdater] ${methodName} requires HotUpdater.wrap() or HotUpdater.init() to be used.\n\n` +
|
|
159
|
+
`To fix this issue, configure HotUpdater before calling ${methodName}:\n\n` +
|
|
160
|
+
`Option 1: With HotUpdater.wrap()\n` +
|
|
90
161
|
` export default HotUpdater.wrap({\n` +
|
|
91
162
|
` baseURL: "<your-update-server-url>",\n` +
|
|
92
163
|
` updateStrategy: "appVersion",\n` +
|
|
93
164
|
` updateMode: "auto"\n` +
|
|
94
165
|
` })(App);\n\n` +
|
|
95
|
-
`Option 2:
|
|
96
|
-
`
|
|
166
|
+
`Option 2: With HotUpdater.init() for custom runtimes\n` +
|
|
167
|
+
` HotUpdater.init({\n` +
|
|
97
168
|
` baseURL: "<your-update-server-url>",\n` +
|
|
98
169
|
` updateMode: "manual"\n` +
|
|
99
|
-
` })
|
|
100
|
-
`For more information, visit: https://hot-updater.dev/docs/react-native-api/wrap
|
|
170
|
+
` });\n\n` +
|
|
171
|
+
`For more information, visit: https://hot-updater.dev/docs/react-native-api/wrap ` +
|
|
172
|
+
`or https://hot-updater.dev/docs/react-native-api/init`,
|
|
101
173
|
);
|
|
102
174
|
}
|
|
103
175
|
return globalConfig.resolver;
|
|
@@ -129,42 +201,31 @@ function createHotUpdaterClient() {
|
|
|
129
201
|
* ```
|
|
130
202
|
*/
|
|
131
203
|
wrap: (options: HotUpdaterOptions) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
204
|
+
const normalizedOptions = normalizeOptions(options, "wrap");
|
|
205
|
+
configureGlobal(normalizedOptions, options);
|
|
206
|
+
|
|
207
|
+
return wrap(normalizedOptions);
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Initializes HotUpdater without wrapping a React component.
|
|
212
|
+
*
|
|
213
|
+
* Use this for manual update flows in runtimes where a root component HOC
|
|
214
|
+
* is not convenient. It has the same initialization effect as
|
|
215
|
+
* `HotUpdater.wrap({ updateMode: "manual" })(App)`.
|
|
216
|
+
*/
|
|
217
|
+
init: (options: ManualUpdateOptions): void => {
|
|
218
|
+
const normalizedOptions = normalizeOptions(options, "init");
|
|
219
|
+
if (normalizedOptions.updateMode !== "manual") {
|
|
143
220
|
throw new Error(
|
|
144
|
-
`[HotUpdater]
|
|
145
|
-
`
|
|
146
|
-
` export default HotUpdater.wrap({\n` +
|
|
147
|
-
` baseURL: "<your-update-server-url>",\n` +
|
|
148
|
-
` updateStrategy: "appVersion",\n` +
|
|
149
|
-
` updateMode: "auto"\n` +
|
|
150
|
-
` })(App);\n\n` +
|
|
151
|
-
`Option 2: Using custom resolver (advanced)\n` +
|
|
152
|
-
` export default HotUpdater.wrap({\n` +
|
|
153
|
-
` resolver: {\n` +
|
|
154
|
-
` checkUpdate: async (params) => { /* custom logic */ },\n` +
|
|
155
|
-
` notifyAppReady: async (params) => { /* custom logic */ }\n` +
|
|
156
|
-
` },\n` +
|
|
157
|
-
` updateMode: "manual"\n` +
|
|
158
|
-
` })(App);\n\n` +
|
|
159
|
-
`For more information, visit: https://hot-updater.dev/docs/react-native-api/wrap`,
|
|
221
|
+
`[HotUpdater] HotUpdater.init() only supports updateMode: "manual". ` +
|
|
222
|
+
`Use HotUpdater.wrap() for automatic updates.`,
|
|
160
223
|
);
|
|
161
224
|
}
|
|
162
225
|
|
|
163
|
-
|
|
164
|
-
globalConfig.requestHeaders = options.requestHeaders;
|
|
165
|
-
globalConfig.requestTimeout = options.requestTimeout;
|
|
226
|
+
configureGlobal(normalizedOptions, options);
|
|
166
227
|
|
|
167
|
-
|
|
228
|
+
init(normalizedOptions);
|
|
168
229
|
},
|
|
169
230
|
|
|
170
231
|
/**
|
package/src/wrap.spec.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
addListener: vi.fn(() => () => {}),
|
|
5
|
+
checkForUpdate: vi.fn(),
|
|
6
|
+
getBundleId: vi.fn(() => "bundle-id"),
|
|
7
|
+
notifyAppReady: vi.fn(() => ({ status: "STABLE" })),
|
|
8
|
+
reload: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("./checkForUpdate", () => ({
|
|
12
|
+
checkForUpdate: mocks.checkForUpdate,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("./native", () => ({
|
|
16
|
+
addListener: mocks.addListener,
|
|
17
|
+
getBundleId: mocks.getBundleId,
|
|
18
|
+
notifyAppReady: mocks.notifyAppReady,
|
|
19
|
+
reload: mocks.reload,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe("HotUpdater wrap initialization", () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.resetModules();
|
|
25
|
+
vi.unstubAllGlobals();
|
|
26
|
+
vi.useRealTimers();
|
|
27
|
+
|
|
28
|
+
for (const mock of Object.values(mocks)) {
|
|
29
|
+
mock.mockReset();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mocks.checkForUpdate.mockResolvedValue(null);
|
|
33
|
+
mocks.addListener.mockReturnValue(() => {});
|
|
34
|
+
mocks.getBundleId.mockReturnValue("bundle-id");
|
|
35
|
+
mocks.notifyAppReady.mockReturnValue({ status: "STABLE" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns void from init and defers notifyAppReady to the next frame", async () => {
|
|
39
|
+
vi.useFakeTimers();
|
|
40
|
+
|
|
41
|
+
const requestAnimationFrame = vi.fn(
|
|
42
|
+
(callback: (timestamp: number) => void) => {
|
|
43
|
+
setTimeout(() => callback(0), 0);
|
|
44
|
+
return 1;
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
vi.stubGlobal("requestAnimationFrame", requestAnimationFrame);
|
|
48
|
+
|
|
49
|
+
const resolver = {
|
|
50
|
+
checkUpdate: vi.fn(),
|
|
51
|
+
notifyAppReady: vi.fn().mockResolvedValue(undefined),
|
|
52
|
+
};
|
|
53
|
+
const { init } = await import("./wrap");
|
|
54
|
+
|
|
55
|
+
const result = init({
|
|
56
|
+
resolver,
|
|
57
|
+
requestHeaders: {
|
|
58
|
+
Authorization: "Bearer token",
|
|
59
|
+
},
|
|
60
|
+
requestTimeout: 1000,
|
|
61
|
+
updateMode: "manual",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result).toBeUndefined();
|
|
65
|
+
expect(mocks.notifyAppReady).not.toHaveBeenCalled();
|
|
66
|
+
expect(resolver.notifyAppReady).not.toHaveBeenCalled();
|
|
67
|
+
|
|
68
|
+
expect(requestAnimationFrame).toHaveBeenCalled();
|
|
69
|
+
|
|
70
|
+
await vi.runOnlyPendingTimersAsync();
|
|
71
|
+
|
|
72
|
+
expect(mocks.notifyAppReady).toHaveBeenCalledWith();
|
|
73
|
+
expect(resolver.notifyAppReady).toHaveBeenCalledWith({
|
|
74
|
+
status: "STABLE",
|
|
75
|
+
crashedBundleId: undefined,
|
|
76
|
+
requestHeaders: {
|
|
77
|
+
Authorization: "Bearer token",
|
|
78
|
+
},
|
|
79
|
+
requestTimeout: 1000,
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/wrap.tsx
CHANGED
|
@@ -204,10 +204,30 @@ type InternalManualUpdateOptions = InternalCommonOptions & {
|
|
|
204
204
|
updateMode: "manual";
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
+
export type InternalInitOptions = InternalManualUpdateOptions;
|
|
208
|
+
|
|
207
209
|
export type InternalWrapOptions =
|
|
208
210
|
| InternalAutoUpdateOptions
|
|
209
211
|
| InternalManualUpdateOptions;
|
|
210
212
|
|
|
213
|
+
type RequestAnimationFrame = (callback: (timestamp: number) => void) => number;
|
|
214
|
+
|
|
215
|
+
const waitForNextFrame = () =>
|
|
216
|
+
new Promise<void>((resolve) => {
|
|
217
|
+
const requestAnimationFrame = (
|
|
218
|
+
globalThis as typeof globalThis & {
|
|
219
|
+
requestAnimationFrame?: RequestAnimationFrame;
|
|
220
|
+
}
|
|
221
|
+
)?.requestAnimationFrame;
|
|
222
|
+
|
|
223
|
+
if (requestAnimationFrame) {
|
|
224
|
+
requestAnimationFrame(() => resolve());
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
void Promise.resolve().then(resolve);
|
|
229
|
+
});
|
|
230
|
+
|
|
211
231
|
/**
|
|
212
232
|
* Helper function to handle notifyAppReady flow
|
|
213
233
|
*/
|
|
@@ -217,6 +237,8 @@ const handleNotifyAppReady = async (options: {
|
|
|
217
237
|
requestTimeout?: number;
|
|
218
238
|
onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
|
|
219
239
|
}): Promise<void> => {
|
|
240
|
+
await waitForNextFrame();
|
|
241
|
+
|
|
220
242
|
try {
|
|
221
243
|
const nativeResult = nativeNotifyAppReady();
|
|
222
244
|
|
|
@@ -240,6 +262,10 @@ const handleNotifyAppReady = async (options: {
|
|
|
240
262
|
}
|
|
241
263
|
};
|
|
242
264
|
|
|
265
|
+
export function init(options: InternalInitOptions): void {
|
|
266
|
+
void handleNotifyAppReady(options);
|
|
267
|
+
}
|
|
268
|
+
|
|
243
269
|
export function wrap<P extends React.JSX.IntrinsicAttributes = object>(
|
|
244
270
|
options: InternalWrapOptions,
|
|
245
271
|
): (WrappedComponent: React.ComponentType<P>) => React.ComponentType<P> {
|