react-native-ota-hot-update 1.1.2 → 1.1.4

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.
@@ -92,17 +92,21 @@ public class HotUpdateModule extends ReactContextBaseJavaModule {
92
92
  @ReactMethod
93
93
  public void setupBundlePath(String path, String extension, Promise promise) {
94
94
  if (path != null) {
95
- deleteOldBundleIfneeded();
96
95
  File file = new File(path);
97
96
  if (file.exists() && file.isFile()) {
97
+ deleteOldBundleIfneeded();
98
98
  String fileUnzip = unzip(file, extension != null ? extension : ".bundle");
99
- Log.d("setupBundlePath: ", fileUnzip);
100
99
  if (fileUnzip != null) {
100
+ Log.d("setupBundlePath: ", fileUnzip);
101
101
  file.delete();
102
102
  SharedPrefs sharedPrefs = new SharedPrefs(getReactApplicationContext());
103
103
  sharedPrefs.putString(Common.INSTANCE.getPATH(), fileUnzip);
104
104
  promise.resolve(true);
105
105
  } else {
106
+ file.delete();
107
+ deleteDirectory(file.getParentFile());
108
+ SharedPrefs sharedPrefs = new SharedPrefs(getReactApplicationContext());
109
+ sharedPrefs.putString(Common.INSTANCE.getPATH(), "");
106
110
  promise.resolve(false);
107
111
  }
108
112
  } else {
@@ -153,6 +157,13 @@ public class HotUpdateModule extends ReactContextBaseJavaModule {
153
157
  promise.resolve(true);
154
158
  }
155
159
 
160
+ @ReactMethod
161
+ public void setExactBundlePath(String path, Promise promise) {
162
+ SharedPrefs sharedPrefs = new SharedPrefs(getReactApplicationContext());
163
+ sharedPrefs.putString(Common.INSTANCE.getPATH(), path);
164
+ promise.resolve(true);
165
+ }
166
+
156
167
  @NonNull
157
168
  @Override
158
169
  public String getName() {
package/ios/RNhotupdate.m CHANGED
@@ -170,8 +170,8 @@ RCT_EXPORT_METHOD(setupBundlePath:(NSString *)path extension:(NSString *)extensi
170
170
  [self removeBundleIfNeeded];
171
171
  //Unzip file
172
172
  NSString *extractedFilePath = [self unzipFileAtPath:path extension:(extension != nil) ? extension : @".jsbundle"];
173
- NSLog(@"file extraction----- %@", extractedFilePath);
174
173
  if (extractedFilePath) {
174
+ NSLog(@"file extraction----- %@", extractedFilePath);
175
175
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
176
176
  [defaults setObject:extractedFilePath forKey:@"PATH"];
177
177
  [defaults synchronize];
@@ -211,6 +211,19 @@ RCT_EXPORT_METHOD(setCurrentVersion:(NSString *)version withResolver:(RCTPromise
211
211
  }
212
212
  }
213
213
 
214
+ RCT_EXPORT_METHOD(setExactBundlePath:(NSString *)path
215
+ resolve:(RCTPromiseResolveBlock)resolve
216
+ reject:(RCTPromiseRejectBlock)reject) {
217
+ if (path) {
218
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
219
+ [defaults setObject:path forKey:@"PATH"];
220
+ [defaults synchronize];
221
+ resolve(@(YES));
222
+ } else {
223
+ resolve(@(NO));
224
+ }
225
+ }
226
+
214
227
  - (void)loadBundle
215
228
  {
216
229
  RCTTriggerReloadCommandListeners(@"rn-hotupdate: Restart");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-ota-hot-update",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Hot update for react native",
5
5
  "main": "src/index",
6
6
  "repository": "https://github.com/vantuan88291/react-native-ota-hot-update",
@@ -10,8 +10,13 @@
10
10
  "url": "https://github.com/vantuan88291/react-native-ota-hot-update/issues"
11
11
  },
12
12
  "homepage": "https://github.com/vantuan88291/react-native-ota-hot-update",
13
+ "dependencies": {
14
+ "buffer": "^6.0.3",
15
+ "isomorphic-git": "git+https://github.com/vantuan88291/isomorphic-git.git"
16
+ },
13
17
  "peerDependencies": {
14
- "react-native": ">=0.63.4"
18
+ "react-native": ">=0.63.4",
19
+ "react-native-fs": "*"
15
20
  },
16
21
  "create-react-native-library": {
17
22
  "type": "module-legacy",
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @format
3
+ */
4
+ FileReader.prototype.readAsArrayBuffer = function (blob) {
5
+ if (this.readyState === this.LOADING) throw new Error('InvalidStateError');
6
+ this._setReadyState(this.LOADING);
7
+ this._result = null;
8
+ this._error = null;
9
+ const fr = new FileReader();
10
+ fr.onloadend = () => {
11
+ const content = atob(fr.result.replace(/data:[^;]+;base64,/, ''));
12
+ const buffer = new ArrayBuffer(content.length);
13
+ const view = new Uint8Array(buffer);
14
+ view.set(Array.from(content).map((c) => c.charCodeAt(0)));
15
+ this._result = buffer;
16
+ this._setReadyState(this.DONE);
17
+ };
18
+ fr.readAsDataURL(blob);
19
+ };
20
+
21
+ // from: https://stackoverflow.com/questions/42829838/react-native-atob-btoa-not-working-without-remote-js-debugging
22
+ const chars =
23
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
24
+ const atob = (input = '') => {
25
+ let str = input.replace(/[=]+$/, '');
26
+ let output = '';
27
+
28
+ if (str.length % 4 == 1) {
29
+ throw new Error(
30
+ "'atob' failed: The string to be decoded is not correctly encoded."
31
+ );
32
+ }
33
+ for (
34
+ let bc = 0, bs = 0, buffer, i = 0;
35
+ (buffer = str.charAt(i++));
36
+ ~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4)
37
+ ? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))))
38
+ : 0
39
+ ) {
40
+ buffer = chars.indexOf(buffer);
41
+ }
42
+
43
+ return output;
44
+ };
@@ -0,0 +1,154 @@
1
+ import { Buffer } from 'buffer';
2
+
3
+ let RNFS = {
4
+ unlink: console.log,
5
+ readdir: console.log,
6
+ mkdir: console.log,
7
+ readFile: console.log,
8
+ writeFile: console.log,
9
+ stat: console.log,
10
+ };
11
+ try {
12
+ RNFS = require('react-native-fs');
13
+ } catch {}
14
+
15
+ function Err(name: string) {
16
+ return class extends Error {
17
+ public code = name;
18
+ constructor(...args: any) {
19
+ super(...args);
20
+ if (this.message) {
21
+ this.message = name + ': ' + this.message;
22
+ } else {
23
+ this.message = name;
24
+ }
25
+ }
26
+ };
27
+ }
28
+
29
+ // const EEXIST = Err('EEXIST'); // <-- Unused because RNFS's mkdir never throws
30
+ const ENOENT = Err('ENOENT');
31
+ const ENOTDIR = Err('ENOTDIR');
32
+ // const ENOTEMPTY = Err('ENOTEMPTY'); // <-- Unused because RNFS's unlink is recursive by default
33
+
34
+ export const readdir = async (path: string) => {
35
+ try {
36
+ return await RNFS.readdir(path);
37
+ } catch (err: any) {
38
+ switch (err.message) {
39
+ case 'Attempt to get length of null array': {
40
+ throw new ENOTDIR(path);
41
+ }
42
+ case 'Folder does not exist': {
43
+ throw new ENOENT(path);
44
+ }
45
+ default:
46
+ throw err;
47
+ }
48
+ }
49
+ };
50
+
51
+ export const mkdir = async (path: string) => {
52
+ return RNFS.mkdir(path);
53
+ };
54
+
55
+ export const readFile = async (
56
+ path: string,
57
+ opts?: string | { [key: string]: string }
58
+ ) => {
59
+ let encoding;
60
+
61
+ if (typeof opts === 'string') {
62
+ encoding = opts;
63
+ } else if (typeof opts === 'object') {
64
+ encoding = opts.encoding;
65
+ }
66
+
67
+ // @ts-ignore
68
+ let result: string | Uint8Array = await RNFS.readFile(
69
+ path,
70
+ encoding || 'base64'
71
+ );
72
+
73
+ if (!encoding) {
74
+ // @ts-ignore
75
+ result = Buffer.from(result, 'base64');
76
+ }
77
+
78
+ return result;
79
+ };
80
+ export const writeFile = async (
81
+ path: string,
82
+ content: string | Uint8Array,
83
+ opts?: string | { [key: string]: string }
84
+ ) => {
85
+ let encoding;
86
+
87
+ if (typeof opts === 'string') {
88
+ encoding = opts;
89
+ } else if (typeof opts === 'object') {
90
+ encoding = opts.encoding;
91
+ }
92
+
93
+ if (typeof content === 'string') {
94
+ encoding = encoding || 'utf8';
95
+ } else {
96
+ encoding = 'base64';
97
+ content = Buffer.from(content).toString('base64');
98
+ }
99
+
100
+ await RNFS.writeFile(path, content as string, encoding);
101
+ };
102
+
103
+ export const stat = async (path: string) => {
104
+ try {
105
+ const r = await RNFS.stat(path);
106
+ // we monkeypatch the result with a `isSymbolicLink` method because isomorphic-git needs it.
107
+ // Since RNFS doesn't appear to support symlinks at all, we'll just always return false.
108
+ // @ts-ignore
109
+ r.isSymbolicLink = () => false;
110
+ return r;
111
+ } catch (err: any) {
112
+ switch (err.message) {
113
+ case 'File does not exist': {
114
+ throw new ENOENT(path);
115
+ }
116
+ default:
117
+ throw err;
118
+ }
119
+ }
120
+ };
121
+
122
+ // Since there are no symbolic links, lstat and stat are equivalent
123
+ export const lstat = stat;
124
+
125
+ export const unlink = async (path: string) => {
126
+ try {
127
+ await RNFS.unlink(path);
128
+ } catch (err: any) {
129
+ switch (err.message) {
130
+ case 'File does not exist': {
131
+ throw new ENOENT(path);
132
+ }
133
+ default:
134
+ throw err;
135
+ }
136
+ }
137
+ };
138
+
139
+ // RNFS doesn't have a separate rmdir method, so we can use unlink for deleting directories too
140
+ export const rmdir = unlink;
141
+
142
+ // These are optional, which is good because there is no equivalent in RNFS
143
+ export const readlink = async () => {
144
+ throw new Error('not implemented');
145
+ };
146
+ export const symlink = async () => {
147
+ throw new Error('not implemented');
148
+ };
149
+
150
+ // Technically we could pull this off by using `readFile` + `writeFile` with the `mode` option
151
+ // However, it's optional, because isomorphic-git will do exactly that (a readFile and a writeFile with the new mode)
152
+ export const chmod = async () => {
153
+ throw new Error('not implemented');
154
+ };
@@ -0,0 +1,118 @@
1
+ import './helper/fileReader.js';
2
+
3
+ // @ts-ignore
4
+ import git, { PromiseFsClient } from 'isomorphic-git/index.umd.min.js';
5
+ import http from 'isomorphic-git/http/web/index.js';
6
+ import * as promises from './helper/fs';
7
+ import type { CloneOption, PullOption } from '../type';
8
+
9
+ const fs: PromiseFsClient = { promises };
10
+ const getFolder = (folderName?: string) => {
11
+ try {
12
+ const { DocumentDirectoryPath } = require('react-native-fs');
13
+ return DocumentDirectoryPath + (folderName || '/git_hot_update');
14
+ } catch (e) {}
15
+ return '';
16
+ };
17
+ /**
18
+ * Should set config after clone success, otherwise cannot pull
19
+ */
20
+ const setConfig = async (
21
+ folderName?: string,
22
+ options?: {
23
+ userName?: string;
24
+ email?: string;
25
+ }
26
+ ) => {
27
+ await git.setConfig({
28
+ fs,
29
+ dir: getFolder(folderName),
30
+ path: options?.userName || 'user.name',
31
+ value: options?.email || 'hotupdate',
32
+ });
33
+ };
34
+ const cloneRepo = async (options: CloneOption) => {
35
+ try {
36
+ await git.clone({
37
+ fs,
38
+ http,
39
+ dir: getFolder(options?.folderName),
40
+ url: options?.url,
41
+ singleBranch: true,
42
+ depth: 1,
43
+ ref: options?.branch,
44
+ onProgress({ loaded, total }: { loaded: number; total: number }) {
45
+ if (options?.onProgress && total > 0) {
46
+ options?.onProgress(loaded, total);
47
+ }
48
+ },
49
+ });
50
+ await setConfig(options?.folderName, {
51
+ email: options?.email,
52
+ userName: options?.userName,
53
+ });
54
+ return {
55
+ success: true,
56
+ msg: null,
57
+ bundle: `${getFolder(options?.folderName)}/${options.bundlePath}`,
58
+ };
59
+ } catch (e: any) {
60
+ return {
61
+ success: false,
62
+ msg: e.toString(),
63
+ bundle: null,
64
+ };
65
+ }
66
+ };
67
+ const pullUpdate = async (options: PullOption) => {
68
+ try {
69
+ let count = 0;
70
+ await git.pull({
71
+ fs,
72
+ http,
73
+ dir: getFolder(options?.folderName),
74
+ ref: options?.branch,
75
+ singleBranch: true,
76
+ onProgress({ loaded, total }: { loaded: number; total: number }) {
77
+ if (total > 0) {
78
+ count = total;
79
+ if (options?.onProgress) {
80
+ options?.onProgress(loaded, total);
81
+ }
82
+ }
83
+ },
84
+ });
85
+ return {
86
+ success: count > 0,
87
+ msg: count > 0 ? 'Pull success' : 'No updated',
88
+ };
89
+ } catch (e: any) {
90
+ console.log(e.toString());
91
+ return {
92
+ success: false,
93
+ msg: e.toString(),
94
+ };
95
+ }
96
+ };
97
+ const getBranchName = async (folderName?: string) => {
98
+ try {
99
+ return await git.currentBranch({
100
+ fs,
101
+ dir: getFolder(folderName),
102
+ fullname: false,
103
+ });
104
+ } catch (e: any) {
105
+ console.log(e.toString());
106
+ return null;
107
+ }
108
+ };
109
+ const removeGitUpdate = (folderName?: string) => {
110
+ fs.promises.unlink(getFolder(folderName));
111
+ };
112
+ export default {
113
+ cloneRepo,
114
+ pullUpdate,
115
+ getBranchName,
116
+ setConfig,
117
+ removeGitUpdate,
118
+ };
package/src/index.tsx CHANGED
@@ -1,19 +1,14 @@
1
1
  import { NativeModules, Platform } from 'react-native';
2
2
  import {DownloadManager} from './download';
3
+ import { UpdateGitOption, UpdateOption } from './type';
4
+ import git from './gits';
5
+
3
6
  const LINKING_ERROR =
4
7
  'The package \'rn-hotupdate\' doesn\'t seem to be linked. Make sure: \n\n' +
5
8
  Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
6
9
  '- You rebuilt the app after installing the package\n' +
7
10
  '- You are not using Expo Go\n';
8
11
 
9
- export interface UpdateOption {
10
- headers?: object
11
- progress?(received: string, total: string): void
12
- updateSuccess?(): void
13
- updateFail?(message?: string): void
14
- restartAfterInstall?: boolean
15
- extensionBundle?: string,
16
- }
17
12
  const RNhotupdate = NativeModules.RNhotupdate
18
13
  ? NativeModules.RNhotupdate
19
14
  : new Proxy(
@@ -42,6 +37,9 @@ const downloadBundleFile = async (downloadManager: DownloadManager, uri: string,
42
37
  function setupBundlePath(path: string, extension?: string): Promise<boolean> {
43
38
  return RNhotupdate.setupBundlePath(path, extension);
44
39
  }
40
+ function setupExactBundlePath(path: string): Promise<boolean> {
41
+ return RNhotupdate.setExactBundlePath(path);
42
+ }
45
43
  function deleteBundlePath(): Promise<boolean> {
46
44
  return RNhotupdate.deleteBundle();
47
45
  }
@@ -111,12 +109,71 @@ async function downloadBundleUri(downloadManager: DownloadManager, uri: string,
111
109
  installFail(option, e);
112
110
  }
113
111
  }
114
-
112
+ const checkForGitUpdate = async (options: UpdateGitOption) => {
113
+ try {
114
+ if (!options.url || !options.bundlePath) {
115
+ throw new Error(`url or bundlePath should not be null`);
116
+ }
117
+ const branch = await git.getBranchName();
118
+ if (branch) {
119
+ const pull = await git.pullUpdate({
120
+ branch,
121
+ onProgress: options?.onProgress,
122
+ folderName: options?.folderName,
123
+ });
124
+ if (pull.success) {
125
+ options?.onPullSuccess?.();
126
+ if (options?.restartAfterInstall) {
127
+ setTimeout(() => {
128
+ resetApp();
129
+ }, 300);
130
+ }
131
+ } else {
132
+ options?.onPullFailed?.(pull.msg);
133
+ }
134
+ } else {
135
+ const clone = await git.cloneRepo({
136
+ onProgress: options?.onProgress,
137
+ folderName: options?.folderName,
138
+ url: options.url,
139
+ branch: options?.branch,
140
+ bundlePath: options.bundlePath,
141
+ });
142
+ if (clone.success) {
143
+ await git.setConfig();
144
+ if (clone.bundle) {
145
+ await setupExactBundlePath(clone.bundle);
146
+ options?.onCloneSuccess?.();
147
+ if (options?.restartAfterInstall) {
148
+ setTimeout(() => {
149
+ resetApp();
150
+ }, 300);
151
+ }
152
+ }
153
+ } else {
154
+ options?.onCloneFailed?.(clone.msg);
155
+ }
156
+ }
157
+ } catch (e: any) {
158
+ options?.onCloneFailed?.(e.toString());
159
+ } finally {
160
+ options?.onFinishProgress?.();
161
+ }
162
+ };
115
163
  export default {
116
164
  setupBundlePath,
165
+ setupExactBundlePath,
117
166
  removeUpdate: removeBundle,
118
167
  downloadBundleUri,
119
168
  resetApp,
120
169
  getCurrentVersion: getVersionAsNumber,
121
170
  setCurrentVersion,
171
+ git: {
172
+ checkForGitUpdate,
173
+ ...git,
174
+ removeGitUpdate: (folder?: string) => {
175
+ RNhotupdate.setExactBundlePath('');
176
+ git.removeGitUpdate(folder);
177
+ },
178
+ },
122
179
  };
package/src/type.ts ADDED
@@ -0,0 +1,82 @@
1
+ export interface UpdateOption {
2
+ headers?: object;
3
+ progress?(received: string, total: string): void;
4
+ updateSuccess?(): void;
5
+ updateFail?(message?: string): void;
6
+ restartAfterInstall?: boolean;
7
+ extensionBundle?: string;
8
+ }
9
+
10
+ /**
11
+ * Options for updating a Git repository.
12
+ */
13
+ export interface UpdateGitOption {
14
+ /**
15
+ * The URL of the Git repository to check update.
16
+ */
17
+ url: string;
18
+
19
+ /**
20
+ * Optional callback to monitor the progress of the update.
21
+ * @param received - The number of bytes received so far.
22
+ * @param total - The total number of bytes to be received.
23
+ */
24
+ onProgress?(received: number, total: number): void;
25
+
26
+ /**
27
+ * Optional branch name to update or switch to.
28
+ * If not specified, the default branch will be main.
29
+ */
30
+ branch?: string;
31
+
32
+ /**
33
+ * Optional name of the folder where the repository will be cloned or updated.
34
+ * If not specified, a default folder name will be git_hot_update.
35
+ */
36
+ folderName?: string;
37
+ /**
38
+ * Optional callback when pull success, should handle for case update.
39
+ */
40
+ onPullSuccess?(): void;
41
+ /**
42
+ * Optional callback when pull failed.
43
+ */
44
+ onPullFailed?(msg: string): void;
45
+ /**
46
+ * Optional callback when clone success, handle it in the first time clone.
47
+ */
48
+ onCloneSuccess?(): void;
49
+ /**
50
+ * Optional callback when clone failed.
51
+ */
52
+ onCloneFailed?(msg: string): void;
53
+ /**
54
+ * The bundle path of the Git repository, it should place at root.
55
+ * Eg: the folder name is git_hot_update, bundle file place at git_hot_update/output/main.jsbundle, so bundlePath should be: "output/main.jsbundle".
56
+ */
57
+ bundlePath: string;
58
+ /**
59
+ * Optional restart app after clone / pull success for apply the new bundle.
60
+ */
61
+ restartAfterInstall?: boolean;
62
+ /**
63
+ * Optional when all process success, use for set loading false.
64
+ */
65
+ onFinishProgress?(): void;
66
+ }
67
+
68
+ export interface CloneOption {
69
+ url: string;
70
+ folderName?: string;
71
+ onProgress?(received: number, total: number): void;
72
+ branch?: string;
73
+ bundlePath: string;
74
+ userName?: string;
75
+ email?: string;
76
+ }
77
+
78
+ export interface PullOption {
79
+ folderName?: string;
80
+ onProgress?(received: number, total: number): void;
81
+ branch: string;
82
+ }