-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
140 lines (133 loc) · 5.58 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/**
* React Native Hot Patching.
* Downloads bundles from a server and reloads bundle on app restart.
*/
import axios from 'axios';
import { Platform } from 'react-native';
import { getActiveBundle, getBundles, registerBundle, reloadBundle, setActiveBundle, unregisterBundle } from 'react-native-dynamic-bundle';
import RNFS from 'react-native-fs';
import { unzip } from 'react-native-zip-archive'
import semver from 'semver';
/**
* Returns current active app version.
* Ex: app version from package.json: 1.0.0, bundle version: 1.0.1 => returns 1.0.1
* Ex: app version from package.json: 1.0.0, bundle version: null => returns 1.0.0
* Ex: app version from package.json: 1.0.1, bundle version: 1.0.0 => returns 1.0.1
* @param {string} appVersion app version from package.json
* @return {string} currently active app version
*/
async function getCurrentAppVersion(appVersion) {
const bundleVersion = await getActiveBundle();
if(!bundleVersion) {
// if bundle is null then return app version from package.json
return appVersion;
} else {
// return version that is greater
return semver.gt(bundleVersion, appVersion) ? bundleVersion : appVersion;
}
};
/**
* Initializes hot patching
* @param {Object} options init params
*/
async function init(options = {}) {
// validate options
if(!options.url) throw new Error('RNHotPatching.init(): url can not be null');
if(!options.appVersion) throw new Error('RNHotPatching.init(): appVersion can not be null');
try {
// get latest bundle version
const resp = await axios.get(`${options.url}/api/v1/bundles/latest/${Platform.OS}`);
// if response is empty then there are no bundles yet, return
if(Object.keys(resp.data).length === 0) return;
// check if we should download and activate bundle
const currentAppVersion = await getCurrentAppVersion(options.appVersion);
if(isActivationRequired(currentAppVersion, resp.data)) {
// create bundle folder
await RNFS.mkdir(`${RNFS.DocumentDirectoryPath}/bundles/${resp.data.version}`, {NSURLIsExcludedFromBackupKey: true});
// download bundle
const zippedBundlePath = `${RNFS.DocumentDirectoryPath}/bundles/${resp.data.version}/${Platform.OS}.bundle.zip`;
await RNFS.downloadFile({
fromUrl: resp.data.url,
toFile: zippedBundlePath
}).promise;
// unzip bundle
await unzip(zippedBundlePath, `${RNFS.DocumentDirectoryPath}/bundles/${resp.data.version}`);
// delete zipped bundle
await RNFS.unlink(zippedBundlePath);
// if bundle was downloaded then set it as active
const bundleExists = await RNFS.exists(`${RNFS.DocumentDirectoryPath}/bundles/${resp.data.version}/${Platform.OS}.bundle`);
if(bundleExists) {
registerBundle(resp.data.version, `bundles/${resp.data.version}/${Platform.OS}.bundle`);
setActiveBundle(resp.data.version);
}
}
// remove bundles that are not used anymore
await removeStaleBundles(options.appVersion);
// if there are no more existing bundles then set active bundle to default (this may happen on next app store update after hotpatched version)
const bundles = await getBundles();
if(Object.keys(bundles).length === 0) setActiveBundle(null);
} catch(err) {
console.log('Error on RNHotPatching.init()');
console.log(err);
throw err;
}
};
/**
* Checks whether plugin should download a bundle and set it as active.
* Returns true only if ALL of the following conditions are met:
* - remote bundle's is_update_required property is true
* - app version >= remote bundle's apply_from_version property
* - app version < remote bundle's version property
* NOTICE: we need bundle's apply_from_version property because only none native bundle updates will be applied.
* So you should track that active bundle does not have any native code changes.
* @param {string} currentAppVersion current app version
* @param {Object} remoteBundleData remote bundle data
* @return {boolean} whether bundle should be downloaded and activated
*/
function isActivationRequired(currentAppVersion, remoteBundleData) {
const isRemoteBundleUpdateRequired = remoteBundleData.is_update_required;
// if "apply_from_version" exists and is in valid semver format then check that it is >= than "apply_from_version" field
const isGreaterThanMin = semver.valid(remoteBundleData.apply_from_version) ? semver.gte(currentAppVersion, remoteBundleData.apply_from_version) : false;
const isLessThanMax = semver.lt(currentAppVersion, remoteBundleData.version);
return isRemoteBundleUpdateRequired && isGreaterThanMin && isLessThanMax;
};
/**
* Removes stale bundles
* @param {string} appVersion app version from package.json
*/
async function removeStaleBundles(appVersion) {
const bundles = await getBundles();
for(let version of Object.keys(bundles)) {
// if bundle version is less than app version from package.json
if(semver.lt(version, appVersion)) {
unregisterBundle(version);
// if bundle folder exists then delete it
const bundleExists = await RNFS.exists(`${RNFS.DocumentDirectoryPath}/bundles/${version}`);
if(bundleExists) {
await RNFS.unlink(`${RNFS.DocumentDirectoryPath}/bundles/${version}`);
}
}
}
};
/**
* Removes all bundles and resets app bundle to the one from app store
*/
async function reset() {
const bundles = await getBundles();
for(let version of Object.keys(bundles)) {
unregisterBundle(version);
const bundleExists = await RNFS.exists(`${RNFS.DocumentDirectoryPath}/bundles/${version}`);
if(bundleExists) {
await RNFS.unlink(`${RNFS.DocumentDirectoryPath}/bundles/${version}`);
}
}
setActiveBundle(null);
reloadBundle();
};
module.exports = {
getCurrentAppVersion,
init,
isActivationRequired,
removeStaleBundles,
reset
};