@hot-updater/react-native 0.30.7 → 0.30.9
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 +67 -19
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +148 -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 +75 -0
- package/lib/commonjs/wrap.spec.js.map +1 -0
- package/lib/module/index.js +68 -20
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +147 -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 +74 -0
- package/lib/module/wrap.spec.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +19 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +6 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +19 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +6 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/index.spec.ts +170 -0
- package/src/index.ts +133 -44
- package/src/wrap.spec.ts +81 -0
- package/src/wrap.tsx +36 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
addListener: vi.fn(() => () => {}),
|
|
5
|
+
checkForUpdate: vi.fn(),
|
|
6
|
+
clearCrashHistory: vi.fn(() => true),
|
|
7
|
+
createDefaultResolver: vi.fn(),
|
|
8
|
+
getAppVersion: vi.fn(() => "1.0.0"),
|
|
9
|
+
getBaseURL: vi.fn(() => null),
|
|
10
|
+
getBundleId: vi.fn(() => "bundle-id"),
|
|
11
|
+
getChannel: vi.fn(() => "production"),
|
|
12
|
+
getCohort: vi.fn(() => "123"),
|
|
13
|
+
getCrashHistory: vi.fn(() => []),
|
|
14
|
+
getDefaultChannel: vi.fn(() => "production"),
|
|
15
|
+
getFingerprintHash: vi.fn(() => null),
|
|
16
|
+
getManifest: vi.fn(() => null),
|
|
17
|
+
getMinBundleId: vi.fn(() => "min-bundle-id"),
|
|
18
|
+
init: vi.fn(),
|
|
19
|
+
isChannelSwitched: vi.fn(() => false),
|
|
20
|
+
reload: vi.fn(),
|
|
21
|
+
resetChannel: vi.fn(),
|
|
22
|
+
setCohort: vi.fn(),
|
|
23
|
+
setReloadBehavior: vi.fn(),
|
|
24
|
+
updateBundle: vi.fn(),
|
|
25
|
+
wrap: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("./DefaultResolver", () => ({
|
|
29
|
+
createDefaultResolver: mocks.createDefaultResolver,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("./checkForUpdate", () => ({
|
|
33
|
+
checkForUpdate: mocks.checkForUpdate,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("./native", () => ({
|
|
37
|
+
addListener: mocks.addListener,
|
|
38
|
+
clearCrashHistory: mocks.clearCrashHistory,
|
|
39
|
+
getAppVersion: mocks.getAppVersion,
|
|
40
|
+
getBaseURL: mocks.getBaseURL,
|
|
41
|
+
getBundleId: mocks.getBundleId,
|
|
42
|
+
getChannel: mocks.getChannel,
|
|
43
|
+
getCohort: mocks.getCohort,
|
|
44
|
+
getCrashHistory: mocks.getCrashHistory,
|
|
45
|
+
getDefaultChannel: mocks.getDefaultChannel,
|
|
46
|
+
getFingerprintHash: mocks.getFingerprintHash,
|
|
47
|
+
getManifest: mocks.getManifest,
|
|
48
|
+
getMinBundleId: mocks.getMinBundleId,
|
|
49
|
+
isChannelSwitched: mocks.isChannelSwitched,
|
|
50
|
+
reload: mocks.reload,
|
|
51
|
+
resetChannel: mocks.resetChannel,
|
|
52
|
+
setCohort: mocks.setCohort,
|
|
53
|
+
setReloadBehavior: mocks.setReloadBehavior,
|
|
54
|
+
updateBundle: mocks.updateBundle,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock("./wrap", () => ({
|
|
58
|
+
init: mocks.init,
|
|
59
|
+
wrap: mocks.wrap,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const importHotUpdater = async () => {
|
|
63
|
+
const { HotUpdater } = await import("./index");
|
|
64
|
+
return HotUpdater;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
describe("HotUpdater client initialization", () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.resetModules();
|
|
70
|
+
|
|
71
|
+
for (const mock of Object.values(mocks)) {
|
|
72
|
+
mock.mockReset();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
mocks.addListener.mockReturnValue(() => {});
|
|
76
|
+
mocks.checkForUpdate.mockResolvedValue(null);
|
|
77
|
+
mocks.createDefaultResolver.mockImplementation((baseURL: string) => ({
|
|
78
|
+
baseURL,
|
|
79
|
+
checkUpdate: vi.fn(),
|
|
80
|
+
notifyAppReady: vi.fn(),
|
|
81
|
+
}));
|
|
82
|
+
mocks.getBaseURL.mockReturnValue(null);
|
|
83
|
+
mocks.init.mockReturnValue(undefined);
|
|
84
|
+
mocks.wrap.mockReturnValue((Component: unknown) => Component);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("initializes manual update flows without wrapping a component", async () => {
|
|
88
|
+
const resolver = {
|
|
89
|
+
checkUpdate: vi.fn(),
|
|
90
|
+
notifyAppReady: vi.fn(),
|
|
91
|
+
};
|
|
92
|
+
mocks.createDefaultResolver.mockReturnValue(resolver);
|
|
93
|
+
|
|
94
|
+
const HotUpdater = await importHotUpdater();
|
|
95
|
+
|
|
96
|
+
const result = HotUpdater.init({
|
|
97
|
+
baseURL: "https://updates.example.com",
|
|
98
|
+
requestHeaders: {
|
|
99
|
+
Authorization: "Bearer token",
|
|
100
|
+
},
|
|
101
|
+
requestTimeout: 1000,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result).toBeUndefined();
|
|
105
|
+
expect(mocks.createDefaultResolver).toHaveBeenCalledWith(
|
|
106
|
+
"https://updates.example.com",
|
|
107
|
+
);
|
|
108
|
+
expect(mocks.init).toHaveBeenCalledWith({
|
|
109
|
+
requestHeaders: {
|
|
110
|
+
Authorization: "Bearer token",
|
|
111
|
+
},
|
|
112
|
+
requestTimeout: 1000,
|
|
113
|
+
resolver,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("uses init configuration for later manual update checks", async () => {
|
|
118
|
+
const resolver = {
|
|
119
|
+
checkUpdate: vi.fn(),
|
|
120
|
+
notifyAppReady: vi.fn(),
|
|
121
|
+
};
|
|
122
|
+
mocks.createDefaultResolver.mockReturnValue(resolver);
|
|
123
|
+
|
|
124
|
+
const HotUpdater = await importHotUpdater();
|
|
125
|
+
|
|
126
|
+
HotUpdater.init({
|
|
127
|
+
baseURL: "https://updates.example.com",
|
|
128
|
+
requestHeaders: {
|
|
129
|
+
Authorization: "Bearer token",
|
|
130
|
+
},
|
|
131
|
+
requestTimeout: 1000,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await HotUpdater.checkForUpdate({
|
|
135
|
+
requestHeaders: {
|
|
136
|
+
"X-Runtime": "secondary",
|
|
137
|
+
},
|
|
138
|
+
updateStrategy: "appVersion",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(mocks.checkForUpdate).toHaveBeenCalledWith({
|
|
142
|
+
requestHeaders: {
|
|
143
|
+
Authorization: "Bearer token",
|
|
144
|
+
"X-Runtime": "secondary",
|
|
145
|
+
},
|
|
146
|
+
requestTimeout: 1000,
|
|
147
|
+
resolver,
|
|
148
|
+
updateStrategy: "appVersion",
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("points users to wrap or init when methods are called too early", async () => {
|
|
153
|
+
const HotUpdater = await importHotUpdater();
|
|
154
|
+
|
|
155
|
+
expect(() =>
|
|
156
|
+
HotUpdater.checkForUpdate({
|
|
157
|
+
updateStrategy: "appVersion",
|
|
158
|
+
}),
|
|
159
|
+
).toThrow("requires HotUpdater.wrap() or HotUpdater.init() to be used");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("requires baseURL for init", async () => {
|
|
163
|
+
const HotUpdater = await importHotUpdater();
|
|
164
|
+
|
|
165
|
+
expect(() => HotUpdater.init({} as never)).toThrow(
|
|
166
|
+
"baseURL must be provided",
|
|
167
|
+
);
|
|
168
|
+
expect(mocks.init).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,14 @@ import {
|
|
|
27
27
|
} from "./native";
|
|
28
28
|
import { hotUpdaterStore } from "./store";
|
|
29
29
|
import type { HotUpdaterResolver } from "./types";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
type HotUpdaterInitOptions,
|
|
32
|
+
type HotUpdaterOptions,
|
|
33
|
+
init,
|
|
34
|
+
type InternalInitOptions,
|
|
35
|
+
type InternalWrapOptions,
|
|
36
|
+
wrap,
|
|
37
|
+
} from "./wrap";
|
|
31
38
|
|
|
32
39
|
export type {
|
|
33
40
|
CustomReloadHandler,
|
|
@@ -47,7 +54,11 @@ export {
|
|
|
47
54
|
type ResolverNotifyAppReadyParams,
|
|
48
55
|
type SignatureVerificationFailure,
|
|
49
56
|
} from "./types";
|
|
50
|
-
export type {
|
|
57
|
+
export type {
|
|
58
|
+
HotUpdaterInitOptions,
|
|
59
|
+
HotUpdaterOptions,
|
|
60
|
+
RunUpdateProcessResponse,
|
|
61
|
+
} from "./wrap";
|
|
51
62
|
|
|
52
63
|
/**
|
|
53
64
|
* Register getBaseURL to global objects for use without imports.
|
|
@@ -81,23 +92,109 @@ function createHotUpdaterClient() {
|
|
|
81
92
|
resolver: null,
|
|
82
93
|
};
|
|
83
94
|
|
|
95
|
+
const createMissingNetworkConfigError = (apiName: "init" | "wrap") => {
|
|
96
|
+
if (apiName === "init") {
|
|
97
|
+
return new Error(
|
|
98
|
+
`[HotUpdater] baseURL must be provided.\n\n` +
|
|
99
|
+
`Configure HotUpdater before calling update APIs:\n\n` +
|
|
100
|
+
` HotUpdater.init({\n` +
|
|
101
|
+
` baseURL: "<your-update-server-url>",\n` +
|
|
102
|
+
` });\n\n` +
|
|
103
|
+
`For more information, visit: https://hot-updater.dev/docs/react-native-api/init`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const baseURLExample =
|
|
108
|
+
` export default HotUpdater.wrap({\n` +
|
|
109
|
+
` baseURL: "<your-update-server-url>",\n` +
|
|
110
|
+
` updateStrategy: "appVersion",\n` +
|
|
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` +
|
|
121
|
+
` })(App);\n\n`;
|
|
122
|
+
|
|
123
|
+
return new Error(
|
|
124
|
+
`[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
|
|
125
|
+
`Option 1: Using baseURL (recommended for most cases)\n` +
|
|
126
|
+
baseURLExample +
|
|
127
|
+
`Option 2: Using custom resolver (advanced)\n` +
|
|
128
|
+
resolverExample +
|
|
129
|
+
`For more information, visit: https://hot-updater.dev/docs/react-native-api/${apiName}`,
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const normalizeOptions = (
|
|
134
|
+
options: HotUpdaterOptions,
|
|
135
|
+
): InternalWrapOptions => {
|
|
136
|
+
if ("baseURL" in options && options.baseURL) {
|
|
137
|
+
const { baseURL, ...rest } = options;
|
|
138
|
+
return {
|
|
139
|
+
...rest,
|
|
140
|
+
resolver: createDefaultResolver(baseURL),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ("resolver" in options && options.resolver) {
|
|
145
|
+
return options;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw createMissingNetworkConfigError("wrap");
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const normalizeInitOptions = (
|
|
152
|
+
options: HotUpdaterInitOptions,
|
|
153
|
+
): InternalInitOptions => {
|
|
154
|
+
const {
|
|
155
|
+
baseURL,
|
|
156
|
+
updateMode: _updateMode,
|
|
157
|
+
...rest
|
|
158
|
+
} = options as HotUpdaterInitOptions & {
|
|
159
|
+
updateMode?: unknown;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (baseURL) {
|
|
163
|
+
return {
|
|
164
|
+
...rest,
|
|
165
|
+
resolver: createDefaultResolver(baseURL),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw createMissingNetworkConfigError("init");
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const configureGlobal = (
|
|
173
|
+
normalizedOptions: InternalInitOptions | InternalWrapOptions,
|
|
174
|
+
options: HotUpdaterOptions | HotUpdaterInitOptions,
|
|
175
|
+
) => {
|
|
176
|
+
globalConfig.resolver = normalizedOptions.resolver;
|
|
177
|
+
globalConfig.requestHeaders = options.requestHeaders;
|
|
178
|
+
globalConfig.requestTimeout = options.requestTimeout;
|
|
179
|
+
};
|
|
180
|
+
|
|
84
181
|
const ensureGlobalResolver = (methodName: string) => {
|
|
85
182
|
if (!globalConfig.resolver) {
|
|
86
183
|
throw new Error(
|
|
87
|
-
`[HotUpdater] ${methodName} requires HotUpdater.wrap() to be used.\n\n` +
|
|
88
|
-
`To fix this issue,
|
|
89
|
-
`Option 1: With
|
|
184
|
+
`[HotUpdater] ${methodName} requires HotUpdater.wrap() or HotUpdater.init() to be used.\n\n` +
|
|
185
|
+
`To fix this issue, configure HotUpdater before calling ${methodName}:\n\n` +
|
|
186
|
+
`Option 1: With HotUpdater.wrap()\n` +
|
|
90
187
|
` export default HotUpdater.wrap({\n` +
|
|
91
188
|
` baseURL: "<your-update-server-url>",\n` +
|
|
92
189
|
` updateStrategy: "appVersion",\n` +
|
|
93
190
|
` updateMode: "auto"\n` +
|
|
94
191
|
` })(App);\n\n` +
|
|
95
|
-
`Option 2:
|
|
96
|
-
`
|
|
192
|
+
`Option 2: With HotUpdater.init() for custom runtimes\n` +
|
|
193
|
+
` HotUpdater.init({\n` +
|
|
97
194
|
` baseURL: "<your-update-server-url>",\n` +
|
|
98
|
-
`
|
|
99
|
-
`
|
|
100
|
-
`
|
|
195
|
+
` });\n\n` +
|
|
196
|
+
`For more information, visit: https://hot-updater.dev/docs/react-native-api/wrap ` +
|
|
197
|
+
`or https://hot-updater.dev/docs/react-native-api/init`,
|
|
101
198
|
);
|
|
102
199
|
}
|
|
103
200
|
return globalConfig.resolver;
|
|
@@ -129,44 +226,36 @@ function createHotUpdaterClient() {
|
|
|
129
226
|
* ```
|
|
130
227
|
*/
|
|
131
228
|
wrap: (options: HotUpdaterOptions) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if ("baseURL" in options && options.baseURL) {
|
|
135
|
-
const { baseURL, ...rest } = options;
|
|
136
|
-
normalizedOptions = {
|
|
137
|
-
...rest,
|
|
138
|
-
resolver: createDefaultResolver(baseURL),
|
|
139
|
-
};
|
|
140
|
-
} else if ("resolver" in options && options.resolver) {
|
|
141
|
-
normalizedOptions = options;
|
|
142
|
-
} else {
|
|
143
|
-
throw new Error(
|
|
144
|
-
`[HotUpdater] Either baseURL or resolver must be provided.\n\n` +
|
|
145
|
-
`Option 1: Using baseURL (recommended for most cases)\n` +
|
|
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`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
globalConfig.resolver = normalizedOptions.resolver;
|
|
164
|
-
globalConfig.requestHeaders = options.requestHeaders;
|
|
165
|
-
globalConfig.requestTimeout = options.requestTimeout;
|
|
229
|
+
const normalizedOptions = normalizeOptions(options);
|
|
230
|
+
configureGlobal(normalizedOptions, options);
|
|
166
231
|
|
|
167
232
|
return wrap(normalizedOptions);
|
|
168
233
|
},
|
|
169
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Initializes HotUpdater without wrapping a React component.
|
|
237
|
+
*
|
|
238
|
+
* Use this for manual update flows in runtimes where a root component HOC
|
|
239
|
+
* is not convenient. It has the same initialization effect as
|
|
240
|
+
* `HotUpdater.wrap({ updateMode: "manual" })(App)`.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```tsx
|
|
244
|
+
* HotUpdater.init({
|
|
245
|
+
* baseURL: "<your-update-server-url>",
|
|
246
|
+
* });
|
|
247
|
+
*
|
|
248
|
+
* export default App;
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
init: (options: HotUpdaterInitOptions): void => {
|
|
252
|
+
const normalizedOptions = normalizeInitOptions(options);
|
|
253
|
+
|
|
254
|
+
configureGlobal(normalizedOptions, options);
|
|
255
|
+
|
|
256
|
+
init(normalizedOptions);
|
|
257
|
+
},
|
|
258
|
+
|
|
170
259
|
/**
|
|
171
260
|
* Reloads the app.
|
|
172
261
|
*/
|
package/src/wrap.spec.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result).toBeUndefined();
|
|
64
|
+
expect(mocks.notifyAppReady).not.toHaveBeenCalled();
|
|
65
|
+
expect(resolver.notifyAppReady).not.toHaveBeenCalled();
|
|
66
|
+
|
|
67
|
+
expect(requestAnimationFrame).toHaveBeenCalled();
|
|
68
|
+
|
|
69
|
+
await vi.runOnlyPendingTimersAsync();
|
|
70
|
+
|
|
71
|
+
expect(mocks.notifyAppReady).toHaveBeenCalledWith();
|
|
72
|
+
expect(resolver.notifyAppReady).toHaveBeenCalledWith({
|
|
73
|
+
status: "STABLE",
|
|
74
|
+
crashedBundleId: undefined,
|
|
75
|
+
requestHeaders: {
|
|
76
|
+
Authorization: "Bearer token",
|
|
77
|
+
},
|
|
78
|
+
requestTimeout: 1000,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
package/src/wrap.tsx
CHANGED
|
@@ -173,6 +173,16 @@ export type ManualUpdateOptions = CommonHotUpdaterOptions &
|
|
|
173
173
|
updateMode: "manual";
|
|
174
174
|
};
|
|
175
175
|
|
|
176
|
+
type ManualBaseURLUpdateOptions = CommonHotUpdaterOptions &
|
|
177
|
+
BaseURLConfig & {
|
|
178
|
+
updateMode: "manual";
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type HotUpdaterInitOptions = Omit<
|
|
182
|
+
ManualBaseURLUpdateOptions,
|
|
183
|
+
"updateMode"
|
|
184
|
+
>;
|
|
185
|
+
|
|
176
186
|
export type HotUpdaterOptions = AutoUpdateOptions | ManualUpdateOptions;
|
|
177
187
|
|
|
178
188
|
/**
|
|
@@ -204,10 +214,30 @@ type InternalManualUpdateOptions = InternalCommonOptions & {
|
|
|
204
214
|
updateMode: "manual";
|
|
205
215
|
};
|
|
206
216
|
|
|
217
|
+
export type InternalInitOptions = InternalCommonOptions;
|
|
218
|
+
|
|
207
219
|
export type InternalWrapOptions =
|
|
208
220
|
| InternalAutoUpdateOptions
|
|
209
221
|
| InternalManualUpdateOptions;
|
|
210
222
|
|
|
223
|
+
type RequestAnimationFrame = (callback: (timestamp: number) => void) => number;
|
|
224
|
+
|
|
225
|
+
const waitForNextFrame = () =>
|
|
226
|
+
new Promise<void>((resolve) => {
|
|
227
|
+
const requestAnimationFrame = (
|
|
228
|
+
globalThis as typeof globalThis & {
|
|
229
|
+
requestAnimationFrame?: RequestAnimationFrame;
|
|
230
|
+
}
|
|
231
|
+
)?.requestAnimationFrame;
|
|
232
|
+
|
|
233
|
+
if (requestAnimationFrame) {
|
|
234
|
+
requestAnimationFrame(() => resolve());
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
void Promise.resolve().then(resolve);
|
|
239
|
+
});
|
|
240
|
+
|
|
211
241
|
/**
|
|
212
242
|
* Helper function to handle notifyAppReady flow
|
|
213
243
|
*/
|
|
@@ -217,6 +247,8 @@ const handleNotifyAppReady = async (options: {
|
|
|
217
247
|
requestTimeout?: number;
|
|
218
248
|
onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
|
|
219
249
|
}): Promise<void> => {
|
|
250
|
+
await waitForNextFrame();
|
|
251
|
+
|
|
220
252
|
try {
|
|
221
253
|
const nativeResult = nativeNotifyAppReady();
|
|
222
254
|
|
|
@@ -240,6 +272,10 @@ const handleNotifyAppReady = async (options: {
|
|
|
240
272
|
}
|
|
241
273
|
};
|
|
242
274
|
|
|
275
|
+
export function init(options: InternalInitOptions): void {
|
|
276
|
+
void handleNotifyAppReady(options);
|
|
277
|
+
}
|
|
278
|
+
|
|
243
279
|
export function wrap<P extends React.JSX.IntrinsicAttributes = object>(
|
|
244
280
|
options: InternalWrapOptions,
|
|
245
281
|
): (WrappedComponent: React.ComponentType<P>) => React.ComponentType<P> {
|