@bravemobile/react-native-code-push 12.0.0-beta.2 → 12.0.0-beta.5
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/CodePush.js +113 -49
- package/README.md +46 -23
- package/cli/commands/initCommand/index.js +14 -0
- package/cli/commands/initCommand/initAndroid.js +48 -0
- package/cli/commands/initCommand/initIos.js +128 -0
- package/cli/commands/initCommand/test/initAndroid.test.js +85 -0
- package/cli/commands/initCommand/test/initIos.test.js +98 -0
- package/cli/commands/releaseCommand/addToReleaseHistory.js +6 -0
- package/cli/commands/releaseCommand/index.js +8 -0
- package/cli/commands/releaseCommand/release.js +3 -0
- package/cli/commands/updateHistoryCommand/index.js +3 -0
- package/cli/commands/updateHistoryCommand/updateReleaseHistory.js +3 -0
- package/cli/index.js +6 -0
- package/expo/plugin/withCodePushAndroid.js +1 -1
- package/expo/plugin/withCodePushIos.js +2 -2
- package/package.json +3 -2
- package/typings/react-native-code-push.d.ts +5 -0
- package/versioning/BaseVersioning.js +12 -1
- package/versioning/SemverVersioning.test.js +78 -0
package/CodePush.js
CHANGED
|
@@ -9,6 +9,54 @@ const PackageMixins = require("./package-mixins")(NativeCodePush);
|
|
|
9
9
|
|
|
10
10
|
const DEPLOYMENT_KEY = 'deprecated_deployment_key';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @param deviceId {string}
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function hashDeviceId(deviceId) {
|
|
17
|
+
let hash = 0;
|
|
18
|
+
for (let i = 0; i < deviceId.length; i++) {
|
|
19
|
+
hash = ((hash << 5) - hash) + deviceId.charCodeAt(i);
|
|
20
|
+
hash |= 0; // Convert to 32bit int
|
|
21
|
+
}
|
|
22
|
+
return Math.abs(hash);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param clientId {string}
|
|
27
|
+
* @param packageHash {string}
|
|
28
|
+
* @returns {number}
|
|
29
|
+
*/
|
|
30
|
+
function getBucket(clientId, packageHash) {
|
|
31
|
+
const hash = hashDeviceId(`${clientId ?? ''}_${packageHash ?? ''}`);
|
|
32
|
+
return (Math.abs(hash) % 100);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Note that the `clientUniqueId` value may not guarantee the same value if the app is deleted and re-installed.
|
|
37
|
+
* In other words, if a user re-installs the app, the result of this function may change.
|
|
38
|
+
* @returns {Promise<boolean>}
|
|
39
|
+
*/
|
|
40
|
+
async function decideLatestReleaseIsInRollout(versioning, clientId, onRolloutSkipped) {
|
|
41
|
+
const [latestVersion, latestReleaseInfo] = versioning.findLatestRelease();
|
|
42
|
+
|
|
43
|
+
if (latestReleaseInfo.rollout === undefined || latestReleaseInfo.rollout >= 100) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const bucket = getBucket(clientId, latestReleaseInfo.packageHash);
|
|
48
|
+
const inRollout = bucket < latestReleaseInfo.rollout;
|
|
49
|
+
|
|
50
|
+
log(`Bucket: ${bucket}, rollout: ${latestReleaseInfo.rollout} → ${inRollout ? 'IN' : 'OUT'}`);
|
|
51
|
+
|
|
52
|
+
if (!inRollout) {
|
|
53
|
+
log(`Skipping update due to rollout. Bucket ${bucket} is not smaller than rollout range ${latestReleaseInfo.rollout}.`);
|
|
54
|
+
onRolloutSkipped?.(latestVersion);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return inRollout;
|
|
58
|
+
}
|
|
59
|
+
|
|
12
60
|
async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
|
|
13
61
|
/*
|
|
14
62
|
* Before we ask the server if an update exists, we
|
|
@@ -42,6 +90,9 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
|
|
|
42
90
|
}
|
|
43
91
|
}
|
|
44
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @type {RemotePackage|null|undefined}
|
|
95
|
+
*/
|
|
45
96
|
const update = await (async () => {
|
|
46
97
|
try {
|
|
47
98
|
const updateRequest = {
|
|
@@ -58,8 +109,8 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
|
|
|
58
109
|
*/
|
|
59
110
|
const updateChecker = sharedCodePushOptions.updateChecker;
|
|
60
111
|
if (updateChecker) {
|
|
112
|
+
// We do not provide rollout functionality. This could be implemented in the `updateChecker`.
|
|
61
113
|
const { update_info } = await updateChecker(updateRequest);
|
|
62
|
-
|
|
63
114
|
return mapToRemotePackageMetadata(update_info);
|
|
64
115
|
} else {
|
|
65
116
|
/**
|
|
@@ -77,6 +128,9 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
|
|
|
77
128
|
|
|
78
129
|
const versioning = new SemverVersioning(releaseHistory);
|
|
79
130
|
|
|
131
|
+
const isInRollout = await decideLatestReleaseIsInRollout(versioning, nativeConfig.clientUniqueId, sharedCodePushOptions?.onRolloutSkipped);
|
|
132
|
+
versioning.setIsLatestReleaseInRollout(isInRollout);
|
|
133
|
+
|
|
80
134
|
const shouldRollbackToBinary = versioning.shouldRollbackToBinary(runtimeVersion)
|
|
81
135
|
if (shouldRollbackToBinary) {
|
|
82
136
|
// Reset to latest major version and restart
|
|
@@ -217,7 +271,7 @@ async function getCurrentPackage() {
|
|
|
217
271
|
async function getUpdateMetadata(updateState) {
|
|
218
272
|
let updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING);
|
|
219
273
|
if (updateMetadata) {
|
|
220
|
-
updateMetadata = {...PackageMixins.local, ...updateMetadata};
|
|
274
|
+
updateMetadata = { ...PackageMixins.local, ...updateMetadata };
|
|
221
275
|
updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash);
|
|
222
276
|
updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash);
|
|
223
277
|
}
|
|
@@ -424,47 +478,47 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
424
478
|
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
|
|
425
479
|
minimumBackgroundDuration: 0,
|
|
426
480
|
updateDialog: null,
|
|
427
|
-
...options
|
|
481
|
+
...options,
|
|
428
482
|
};
|
|
429
483
|
|
|
430
484
|
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
|
|
431
485
|
? syncStatusChangeCallback
|
|
432
486
|
: (syncStatus) => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
487
|
+
switch (syncStatus) {
|
|
488
|
+
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
|
|
489
|
+
log("Checking for update.");
|
|
490
|
+
break;
|
|
491
|
+
case CodePush.SyncStatus.AWAITING_USER_ACTION:
|
|
492
|
+
log("Awaiting user action.");
|
|
493
|
+
break;
|
|
494
|
+
case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
|
|
495
|
+
log("Downloading package.");
|
|
496
|
+
break;
|
|
497
|
+
case CodePush.SyncStatus.INSTALLING_UPDATE:
|
|
498
|
+
log("Installing update.");
|
|
499
|
+
break;
|
|
500
|
+
case CodePush.SyncStatus.UP_TO_DATE:
|
|
501
|
+
log("App is up to date.");
|
|
502
|
+
break;
|
|
503
|
+
case CodePush.SyncStatus.UPDATE_IGNORED:
|
|
504
|
+
log("User cancelled the update.");
|
|
505
|
+
break;
|
|
506
|
+
case CodePush.SyncStatus.UPDATE_INSTALLED:
|
|
507
|
+
if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
|
|
508
|
+
log("Update is installed and will be run on the next app restart.");
|
|
509
|
+
} else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
|
|
510
|
+
if (syncOptions.minimumBackgroundDuration > 0) {
|
|
511
|
+
log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
|
|
512
|
+
} else {
|
|
513
|
+
log("Update is installed and will be run when the app next resumes.");
|
|
461
514
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
517
|
+
case CodePush.SyncStatus.UNKNOWN_ERROR:
|
|
518
|
+
log("An unknown error occurred.");
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
468
522
|
|
|
469
523
|
let remotePackageLabel;
|
|
470
524
|
try {
|
|
@@ -497,7 +551,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
497
551
|
|
|
498
552
|
if (!remotePackage || updateShouldBeIgnored) {
|
|
499
553
|
if (updateShouldBeIgnored) {
|
|
500
|
-
|
|
554
|
+
log("An update is available, but it is being ignored due to having been previously rolled back.");
|
|
501
555
|
}
|
|
502
556
|
|
|
503
557
|
const currentPackage = await CodePush.getCurrentPackage();
|
|
@@ -536,7 +590,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
536
590
|
onPress: () => {
|
|
537
591
|
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_IGNORED);
|
|
538
592
|
resolve(CodePush.SyncStatus.UPDATE_IGNORED);
|
|
539
|
-
}
|
|
593
|
+
},
|
|
540
594
|
});
|
|
541
595
|
}
|
|
542
596
|
|
|
@@ -544,10 +598,10 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
544
598
|
// right of any other button, add it last
|
|
545
599
|
dialogButtons.push({
|
|
546
600
|
text: installButtonText,
|
|
547
|
-
onPress:() => {
|
|
601
|
+
onPress: () => {
|
|
548
602
|
doDownloadAndInstall()
|
|
549
603
|
.then(resolve, reject);
|
|
550
|
-
}
|
|
604
|
+
},
|
|
551
605
|
})
|
|
552
606
|
|
|
553
607
|
// If the update has a description, and the developer
|
|
@@ -609,6 +663,9 @@ let CodePush;
|
|
|
609
663
|
*
|
|
610
664
|
* onSyncError: (label: string, error: Error) => void | undefined,
|
|
611
665
|
* setOnSyncError(onSyncErrorFunction: (label: string, error: Error) => void | undefined): void,
|
|
666
|
+
*
|
|
667
|
+
* onRolloutSkipped: (label: string, error: Error) => void | undefined,
|
|
668
|
+
* setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | undefined): void,
|
|
612
669
|
* }}
|
|
613
670
|
*/
|
|
614
671
|
const sharedCodePushOptions = {
|
|
@@ -653,6 +710,12 @@ const sharedCodePushOptions = {
|
|
|
653
710
|
if (typeof onSyncErrorFunction !== 'function') throw new Error('Please pass a function to onSyncError');
|
|
654
711
|
this.onSyncError = onSyncErrorFunction;
|
|
655
712
|
},
|
|
713
|
+
onRolloutSkipped: undefined,
|
|
714
|
+
setOnRolloutSkipped(onRolloutSkippedFunction) {
|
|
715
|
+
if (!onRolloutSkippedFunction) return;
|
|
716
|
+
if (typeof onRolloutSkippedFunction !== 'function') throw new Error('Please pass a function to onRolloutSkipped');
|
|
717
|
+
this.onRolloutSkipped = onRolloutSkippedFunction;
|
|
718
|
+
},
|
|
656
719
|
}
|
|
657
720
|
|
|
658
721
|
function codePushify(options = {}) {
|
|
@@ -671,7 +734,7 @@ function codePushify(options = {}) {
|
|
|
671
734
|
throw new Error(
|
|
672
735
|
`Unable to find the "Component" class, please either:
|
|
673
736
|
1. Upgrade to a newer version of React Native that supports it, or
|
|
674
|
-
2. Call the codePush.sync API in your component instead of using the @codePush decorator
|
|
737
|
+
2. Call the codePush.sync API in your component instead of using the @codePush decorator`,
|
|
675
738
|
);
|
|
676
739
|
}
|
|
677
740
|
|
|
@@ -688,6 +751,7 @@ function codePushify(options = {}) {
|
|
|
688
751
|
sharedCodePushOptions.setOnDownloadStart(options.onDownloadStart);
|
|
689
752
|
sharedCodePushOptions.setOnDownloadSuccess(options.onDownloadSuccess);
|
|
690
753
|
sharedCodePushOptions.setOnSyncError(options.onSyncError);
|
|
754
|
+
sharedCodePushOptions.setOnRolloutSkipped(options.onRolloutSkipped);
|
|
691
755
|
|
|
692
756
|
const decorator = (RootComponent) => {
|
|
693
757
|
class CodePushComponent extends React.Component {
|
|
@@ -730,7 +794,7 @@ function codePushify(options = {}) {
|
|
|
730
794
|
}
|
|
731
795
|
|
|
732
796
|
render() {
|
|
733
|
-
const props = {...this.props};
|
|
797
|
+
const props = { ...this.props };
|
|
734
798
|
|
|
735
799
|
// We can set ref property on class components only (not stateless)
|
|
736
800
|
// Check it by render method
|
|
@@ -777,7 +841,7 @@ if (NativeCodePush) {
|
|
|
777
841
|
IMMEDIATE: NativeCodePush.codePushInstallModeImmediate, // Restart the app immediately
|
|
778
842
|
ON_NEXT_RESTART: NativeCodePush.codePushInstallModeOnNextRestart, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart
|
|
779
843
|
ON_NEXT_RESUME: NativeCodePush.codePushInstallModeOnNextResume, // Restart the app the next time it is resumed from the background
|
|
780
|
-
ON_NEXT_SUSPEND: NativeCodePush.codePushInstallModeOnNextSuspend // Restart the app _while_ it is in the background,
|
|
844
|
+
ON_NEXT_SUSPEND: NativeCodePush.codePushInstallModeOnNextSuspend, // Restart the app _while_ it is in the background,
|
|
781
845
|
// but only after it has been in the background for "minimumBackgroundDuration" seconds (0 by default),
|
|
782
846
|
// so that user context isn't lost unless the app suspension is long enough to not matter
|
|
783
847
|
},
|
|
@@ -790,17 +854,17 @@ if (NativeCodePush) {
|
|
|
790
854
|
CHECKING_FOR_UPDATE: 5,
|
|
791
855
|
AWAITING_USER_ACTION: 6,
|
|
792
856
|
DOWNLOADING_PACKAGE: 7,
|
|
793
|
-
INSTALLING_UPDATE: 8
|
|
857
|
+
INSTALLING_UPDATE: 8,
|
|
794
858
|
},
|
|
795
859
|
CheckFrequency: {
|
|
796
860
|
ON_APP_START: 0,
|
|
797
861
|
ON_APP_RESUME: 1,
|
|
798
|
-
MANUAL: 2
|
|
862
|
+
MANUAL: 2,
|
|
799
863
|
},
|
|
800
864
|
UpdateState: {
|
|
801
865
|
RUNNING: NativeCodePush.codePushUpdateStateRunning,
|
|
802
866
|
PENDING: NativeCodePush.codePushUpdateStatePending,
|
|
803
|
-
LATEST: NativeCodePush.codePushUpdateStateLatest
|
|
867
|
+
LATEST: NativeCodePush.codePushUpdateStateLatest,
|
|
804
868
|
},
|
|
805
869
|
DeploymentStatus: {
|
|
806
870
|
FAILED: "DeploymentFailed",
|
|
@@ -814,11 +878,11 @@ if (NativeCodePush) {
|
|
|
814
878
|
optionalIgnoreButtonLabel: "Ignore",
|
|
815
879
|
optionalInstallButtonLabel: "Install",
|
|
816
880
|
optionalUpdateMessage: "An update is available. Would you like to install it?",
|
|
817
|
-
title: "Update available"
|
|
881
|
+
title: "Update available",
|
|
818
882
|
},
|
|
819
883
|
DEFAULT_ROLLBACK_RETRY_OPTIONS: {
|
|
820
884
|
delayInHours: 24,
|
|
821
|
-
maxRetryAttempts: 1
|
|
885
|
+
maxRetryAttempts: 1,
|
|
822
886
|
},
|
|
823
887
|
});
|
|
824
888
|
} else {
|
package/README.md
CHANGED
|
@@ -21,11 +21,13 @@ npm remove react-native-code-push
|
|
|
21
21
|
npm install @bravemobile/react-native-code-push
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
1. Edit `android/app/build.gradle` file to remove the `apply from: "../../node_modules/..../codepush.gradle"` line.
|
|
25
25
|
|
|
26
|
-
The following changes are optional but recommended for cleaning up the old configuration:
|
|
27
|
-
- Since the deployment key is no longer needed due to the retirement of AppCenter, it is recommended to remove it from your `Info.plist`, `strings.xml`, or JavaScript code.
|
|
28
|
-
- Thanks to Auto Linking, you can remove the `react-native-code-push` module settings from `settings.gradle`.
|
|
26
|
+
2. The following changes are optional but recommended for cleaning up the old configuration:
|
|
27
|
+
- Since the deployment key is no longer needed due to the retirement of AppCenter, it is recommended to remove it from your `Info.plist`, `strings.xml`, or JavaScript code.
|
|
28
|
+
- Thanks to Auto Linking, you can remove the `react-native-code-push` module settings from `settings.gradle`.
|
|
29
|
+
|
|
30
|
+
3. Follow the installation guide starting from **'4. "CodePush-ify" your app'**.
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
## ⚙️ Installation
|
|
@@ -35,7 +37,31 @@ The following changes are optional but recommended for cleaning up the old confi
|
|
|
35
37
|
npm install @bravemobile/react-native-code-push
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
### 2.
|
|
40
|
+
### 2. Run init command
|
|
41
|
+
|
|
42
|
+
For React Native CLI projects, you can use the automatic setup command to configure your project for CodePush.
|
|
43
|
+
|
|
44
|
+
This command will automatically edit your `AppDelegate` and `MainApplication` files to integrate CodePush.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx code-push init
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
And run the following command to install CocoaPods dependencies for iOS:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd ios && pod install && cd ..
|
|
54
|
+
```
|
|
55
|
+
(or `npx pod-install`, `bundle exec pod install --project-directory=./ios`, ..)
|
|
56
|
+
|
|
57
|
+
### 2-1. Manual Setup
|
|
58
|
+
|
|
59
|
+
If you prefer manual setup or if the automatic configuration fails, you can follow the manual setup instructions below.
|
|
60
|
+
|
|
61
|
+
<details><summary>Click to see the manual setup instructions.</summary>
|
|
62
|
+
<p>
|
|
63
|
+
|
|
64
|
+
### iOS Manual Setup
|
|
39
65
|
|
|
40
66
|
#### (1) Install CocoaPods Dependencies
|
|
41
67
|
|
|
@@ -54,7 +80,7 @@ Run `cd ios && pod install && cd ..`
|
|
|
54
80
|
|
|
55
81
|
1. Open your project with Xcode (e.g. CodePushDemoApp.xcworkspace)
|
|
56
82
|
2. File → New → File from Template
|
|
57
|
-
3. Select 'Objective-C File' and click 'Next' and write any name as you like.
|
|
83
|
+
3. Select 'Objective-C File' and click 'Next' and write any name as you like.
|
|
58
84
|
4. Then Xcode will ask you to create a bridging header file. Click 'Create'.
|
|
59
85
|
5. Delete the file created in step 3.
|
|
60
86
|
|
|
@@ -110,17 +136,9 @@ Then, edit `AppDelegate.swift` like below.
|
|
|
110
136
|
```
|
|
111
137
|
|
|
112
138
|
|
|
113
|
-
###
|
|
114
|
-
|
|
115
|
-
#### (1) Edit `android/app/build.gradle`
|
|
116
|
-
|
|
117
|
-
Add the following line to the end of the file.
|
|
118
|
-
```diff
|
|
119
|
-
// ...
|
|
120
|
-
+ apply from: "../../node_modules/@bravemobile/react-native-code-push/android/codepush.gradle"
|
|
121
|
-
```
|
|
139
|
+
### Android Manual Setup
|
|
122
140
|
|
|
123
|
-
####
|
|
141
|
+
#### Edit `MainApplication` Code
|
|
124
142
|
|
|
125
143
|
**If you have `MainApplication.kt` (>= RN 0.73)**
|
|
126
144
|
|
|
@@ -161,7 +179,11 @@ Add the following line to the end of the file.
|
|
|
161
179
|
}
|
|
162
180
|
```
|
|
163
181
|
|
|
164
|
-
|
|
182
|
+
</p>
|
|
183
|
+
</details>
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
### 3. Expo Setup
|
|
165
187
|
For Expo projects, you can use the automated config plugin instead of manual setup.
|
|
166
188
|
|
|
167
189
|
**Add plugin to your Expo configuration:**
|
|
@@ -185,7 +207,8 @@ npx expo prebuild
|
|
|
185
207
|
**Requirements**
|
|
186
208
|
Expo SDK: 50.0.0 or higher
|
|
187
209
|
|
|
188
|
-
|
|
210
|
+
|
|
211
|
+
### 4. "CodePush-ify" Your App
|
|
189
212
|
|
|
190
213
|
The root component of your app should be wrapped with a higher-order component.
|
|
191
214
|
|
|
@@ -199,8 +222,8 @@ At runtime, the library fetches this information to keep the app up to date.
|
|
|
199
222
|
|
|
200
223
|
```typescript
|
|
201
224
|
import CodePush, {
|
|
202
|
-
|
|
203
|
-
|
|
225
|
+
ReleaseHistoryInterface,
|
|
226
|
+
UpdateCheckRequest,
|
|
204
227
|
} from "@bravemobile/react-native-code-push";
|
|
205
228
|
|
|
206
229
|
// ... MyApp Component
|
|
@@ -229,7 +252,7 @@ export default CodePush({
|
|
|
229
252
|
> The URL for fetching the release history should point to the resource location generated by the CLI tool.
|
|
230
253
|
|
|
231
254
|
|
|
232
|
-
####
|
|
255
|
+
#### 4-1. Telemetry Callbacks
|
|
233
256
|
|
|
234
257
|
Please refer to the [CodePushOptions](https://github.com/Soomgo-Mobile/react-native-code-push/blob/f0d26f7614af41c6dd4daecd9f7146e2383b2b0d/typings/react-native-code-push.d.ts#L76-L95) type for more details.
|
|
235
258
|
- **onUpdateSuccess:** Triggered when the update bundle is executed successfully.
|
|
@@ -239,7 +262,7 @@ Please refer to the [CodePushOptions](https://github.com/Soomgo-Mobile/react-nat
|
|
|
239
262
|
- **onSyncError:** Triggered when an unknown error occurs during the update process. (`CodePush.SyncStatus.UNKNOWN_ERROR` status)
|
|
240
263
|
|
|
241
264
|
|
|
242
|
-
###
|
|
265
|
+
### 5. Configure the CLI Tool
|
|
243
266
|
|
|
244
267
|
> [!TIP]
|
|
245
268
|
> For a more detailed and practical example, refer to the `CodePushDemoApp` in `example` directory. ([link](https://github.com/Soomgo-Mobile/react-native-code-push/tree/master/Examples/CodePushDemoApp))
|
|
@@ -352,7 +375,7 @@ Create a new release history for a specific binary app version.
|
|
|
352
375
|
This ensures that the library runtime recognizes the binary app as the latest version and determines that no CodePush update is available for it.
|
|
353
376
|
|
|
354
377
|
**Example:**
|
|
355
|
-
- Create a new release history for the binary app version `1.0.0`.
|
|
378
|
+
- Create a new release history for the binary app version `1.0.0`.
|
|
356
379
|
|
|
357
380
|
```bash
|
|
358
381
|
npx code-push create-history --binary-version 1.0.0 --platform ios --identifier staging
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { initAndroid } = require('./initAndroid');
|
|
2
|
+
const { initIos } = require('./initIos');
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
|
|
5
|
+
program
|
|
6
|
+
.command('init')
|
|
7
|
+
.description('Automatically performs iOS/Android native configurations to initialize the CodePush project.')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
console.log('log: Start initializing CodePush...');
|
|
10
|
+
await initAndroid();
|
|
11
|
+
await initIos();
|
|
12
|
+
console.log('log: CodePush has been successfully initialized.');
|
|
13
|
+
});
|
|
14
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { EOL } = require('os');
|
|
4
|
+
|
|
5
|
+
function modifyMainApplicationKt(mainApplicationContent) {
|
|
6
|
+
if (mainApplicationContent.includes('CodePush.getJSBundleFile()')) {
|
|
7
|
+
console.log('log: MainApplication.kt already has CodePush initialized.');
|
|
8
|
+
return mainApplicationContent;
|
|
9
|
+
}
|
|
10
|
+
return mainApplicationContent
|
|
11
|
+
.replace('import com.facebook.react.ReactApplication', `import com.facebook.react.ReactApplication${EOL}import com.microsoft.codepush.react.CodePush`)
|
|
12
|
+
.replace('override fun getJSMainModuleName(): String = "index"', `override fun getJSMainModuleName(): String = "index"${EOL} override fun getJSBundleFile(): String = CodePush.getJSBundleFile()`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function initAndroid() {
|
|
16
|
+
console.log('log: Running Android setup...');
|
|
17
|
+
await applyMainApplication();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function applyMainApplication() {
|
|
21
|
+
const mainApplicationPath = await findMainApplication();
|
|
22
|
+
if (!mainApplicationPath) {
|
|
23
|
+
console.log('log: Could not find MainApplication.kt');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (mainApplicationPath.endsWith('.java')) {
|
|
28
|
+
console.log('log: MainApplication.java is not supported. Please migrate to MainApplication.kt.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const mainApplicationContent = fs.readFileSync(mainApplicationPath, 'utf-8');
|
|
33
|
+
const newContent = modifyMainApplicationKt(mainApplicationContent);
|
|
34
|
+
fs.writeFileSync(mainApplicationPath, newContent);
|
|
35
|
+
console.log('log: Successfully updated MainApplication.kt.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function findMainApplication() {
|
|
39
|
+
const searchPath = path.join(process.cwd(), 'android', 'app', 'src', 'main', 'java');
|
|
40
|
+
const files = fs.readdirSync(searchPath, { recursive: true });
|
|
41
|
+
const mainApplicationFile = files.find(file => file.endsWith('MainApplication.java') || file.endsWith('MainApplication.kt'));
|
|
42
|
+
return mainApplicationFile ? path.join(searchPath, mainApplicationFile) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
initAndroid: initAndroid,
|
|
47
|
+
modifyMainApplicationKt: modifyMainApplicationKt,
|
|
48
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const xcode = require('xcode');
|
|
4
|
+
|
|
5
|
+
async function initIos() {
|
|
6
|
+
console.log('log: Running iOS setup...');
|
|
7
|
+
const projectDir = path.join(process.cwd(), 'ios');
|
|
8
|
+
const files = fs.readdirSync(projectDir);
|
|
9
|
+
const xcodeprojFile = files.find(file => file.endsWith('.xcodeproj'));
|
|
10
|
+
if (!xcodeprojFile) {
|
|
11
|
+
console.log('log: Could not find .xcodeproj file in ios directory');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const projectName = xcodeprojFile.replace('.xcodeproj', '');
|
|
15
|
+
const appDelegatePath = findAppDelegate(path.join(projectDir, projectName));
|
|
16
|
+
|
|
17
|
+
if (!appDelegatePath) {
|
|
18
|
+
console.log('log: Could not find AppDelegate file');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (appDelegatePath.endsWith('.swift')) {
|
|
23
|
+
await setupSwift(appDelegatePath, projectDir, projectName);
|
|
24
|
+
} else {
|
|
25
|
+
await setupObjectiveC(appDelegatePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('log: Please run `cd ios && pod install` to complete the setup.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findAppDelegate(searchPath) {
|
|
32
|
+
if (!fs.existsSync(searchPath)) return null;
|
|
33
|
+
const files = fs.readdirSync(searchPath);
|
|
34
|
+
const appDelegateFile = files.find(file => file.startsWith('AppDelegate') && (file.endsWith('.m') || file.endsWith('.mm') || file.endsWith('.swift')));
|
|
35
|
+
return appDelegateFile ? path.join(searchPath, appDelegateFile) : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function modifyObjectiveCAppDelegate(appDelegateContent) {
|
|
39
|
+
const IMPORT_STATEMENT = '#import <CodePush/CodePush.h>';
|
|
40
|
+
if (appDelegateContent.includes(IMPORT_STATEMENT)) {
|
|
41
|
+
console.log('log: AppDelegate already has CodePush imported.');
|
|
42
|
+
return appDelegateContent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return appDelegateContent
|
|
46
|
+
.replace('#import "AppDelegate.h"\n', `#import "AppDelegate.h"\n${IMPORT_STATEMENT}\n`)
|
|
47
|
+
.replace('[[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];', '[CodePush bundleURL];');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function modifySwiftAppDelegate(appDelegateContent) {
|
|
51
|
+
const CODEPUSH_CALL_STATEMENT = 'CodePush.bundleURL()';
|
|
52
|
+
if (appDelegateContent.includes(CODEPUSH_CALL_STATEMENT)) {
|
|
53
|
+
console.log('log: AppDelegate.swift already configured for CodePush.');
|
|
54
|
+
return appDelegateContent;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return appDelegateContent
|
|
58
|
+
.replace('Bundle.main.url(forResource: "main", withExtension: "jsbundle")', CODEPUSH_CALL_STATEMENT);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function setupObjectiveC(appDelegatePath) {
|
|
62
|
+
const appDelegateContent = fs.readFileSync(appDelegatePath, 'utf-8');
|
|
63
|
+
const newContent = modifyObjectiveCAppDelegate(appDelegateContent);
|
|
64
|
+
fs.writeFileSync(appDelegatePath, newContent);
|
|
65
|
+
console.log('log: Successfully updated AppDelegate.m/mm.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function setupSwift(appDelegatePath, projectDir, projectName) {
|
|
69
|
+
const bridgingHeaderPath = await ensureBridgingHeader(projectDir, projectName);
|
|
70
|
+
if (!bridgingHeaderPath) {
|
|
71
|
+
console.log('log: Failed to create or find bridging header.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const appDelegateContent = fs.readFileSync(appDelegatePath, 'utf-8');
|
|
76
|
+
const newContent = modifySwiftAppDelegate(appDelegateContent);
|
|
77
|
+
fs.writeFileSync(appDelegatePath, newContent);
|
|
78
|
+
console.log('log: Successfully updated AppDelegate.swift.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function ensureBridgingHeader(projectDir, projectName) {
|
|
82
|
+
const projectPath = path.join(projectDir, `${projectName}.xcodeproj`, 'project.pbxproj');
|
|
83
|
+
const myProj = xcode.project(projectPath);
|
|
84
|
+
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
myProj.parse(function (err) {
|
|
87
|
+
if (err) {
|
|
88
|
+
console.error(`Error parsing Xcode project: ${err}`);
|
|
89
|
+
return reject(err);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const bridgingHeaderRelativePath = `${projectName}/${projectName}-Bridging-Header.h`;
|
|
93
|
+
const bridgingHeaderAbsolutePath = path.join(projectDir, bridgingHeaderRelativePath);
|
|
94
|
+
|
|
95
|
+
const configurations = myProj.pbxXCBuildConfigurationSection();
|
|
96
|
+
for (const name in configurations) {
|
|
97
|
+
const config = configurations[name];
|
|
98
|
+
if (config.buildSettings) {
|
|
99
|
+
config.buildSettings.SWIFT_OBJC_BRIDGING_HEADER = `"${bridgingHeaderRelativePath}"`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!fs.existsSync(bridgingHeaderAbsolutePath)) {
|
|
104
|
+
fs.mkdirSync(path.dirname(bridgingHeaderAbsolutePath), { recursive: true });
|
|
105
|
+
fs.writeFileSync(bridgingHeaderAbsolutePath, '#import <CodePush/CodePush.h>\n');
|
|
106
|
+
console.log(`log: Created bridging header at ${bridgingHeaderAbsolutePath}`);
|
|
107
|
+
const groupKey = myProj.findPBXGroupKey({ name: projectName });
|
|
108
|
+
myProj.addHeaderFile(bridgingHeaderRelativePath, { public: true }, groupKey);
|
|
109
|
+
} else {
|
|
110
|
+
const headerContent = fs.readFileSync(bridgingHeaderAbsolutePath, 'utf-8');
|
|
111
|
+
if (!headerContent.includes('#import <CodePush/CodePush.h>')) {
|
|
112
|
+
fs.appendFileSync(bridgingHeaderAbsolutePath, '\n#import <CodePush/CodePush.h>\n');
|
|
113
|
+
console.log(`log: Updated bridging header at ${bridgingHeaderAbsolutePath}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(projectPath, myProj.writeSync());
|
|
118
|
+
console.log('log: Updated Xcode project with bridging header path.');
|
|
119
|
+
resolve(bridgingHeaderAbsolutePath);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
initIos: initIos,
|
|
126
|
+
modifyObjectiveCAppDelegate: modifyObjectiveCAppDelegate,
|
|
127
|
+
modifySwiftAppDelegate: modifySwiftAppDelegate,
|
|
128
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { initAndroid, modifyMainApplicationKt } = require('../initAndroid');
|
|
4
|
+
|
|
5
|
+
const tempDir = path.join(__dirname, 'temp');
|
|
6
|
+
|
|
7
|
+
// https://github.com/react-native-community/template/blob/0.80.2/template/android/app/src/main/java/com/helloworld/MainApplication.kt
|
|
8
|
+
const ktTemplate = `
|
|
9
|
+
package com.helloworld
|
|
10
|
+
|
|
11
|
+
import android.app.Application
|
|
12
|
+
import com.facebook.react.PackageList
|
|
13
|
+
import com.facebook.react.ReactApplication
|
|
14
|
+
import com.facebook.react.ReactHost
|
|
15
|
+
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
|
16
|
+
import com.facebook.react.ReactNativeHost
|
|
17
|
+
import com.facebook.react.ReactPackage
|
|
18
|
+
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
|
19
|
+
import com.facebook.react.defaults.DefaultReactNativeHost
|
|
20
|
+
|
|
21
|
+
class MainApplication : Application(), ReactApplication {
|
|
22
|
+
|
|
23
|
+
override val reactNativeHost: ReactNativeHost =
|
|
24
|
+
object : DefaultReactNativeHost(this) {
|
|
25
|
+
override fun getPackages(): List<ReactPackage> =
|
|
26
|
+
PackageList(this).packages.apply {
|
|
27
|
+
// Packages that cannot be autolinked yet can be added manually here, for example:
|
|
28
|
+
// add(MyReactNativePackage())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override fun getJSMainModuleName(): String = "index"
|
|
32
|
+
|
|
33
|
+
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
|
34
|
+
|
|
35
|
+
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
|
36
|
+
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override val reactHost: ReactHost
|
|
40
|
+
get() = getDefaultReactHost(applicationContext, reactNativeHost)
|
|
41
|
+
|
|
42
|
+
override fun onCreate() {
|
|
43
|
+
super.onCreate()
|
|
44
|
+
loadReactNative(this)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
describe('Android init command', () => {
|
|
50
|
+
it('should correctly modify Kotlin MainApplication content', () => {
|
|
51
|
+
const modifiedContent = modifyMainApplicationKt(ktTemplate);
|
|
52
|
+
expect(modifiedContent).toContain('import com.microsoft.codepush.react.CodePush');
|
|
53
|
+
expect(modifiedContent).toContain('override fun getJSBundleFile(): String = CodePush.getJSBundleFile()');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should log a message and exit if MainApplication.java is found', async () => {
|
|
57
|
+
const originalCwd = process.cwd();
|
|
58
|
+
|
|
59
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
60
|
+
process.chdir(tempDir);
|
|
61
|
+
|
|
62
|
+
// Arrange
|
|
63
|
+
const javaAppDir = path.join(tempDir, 'android', 'app', 'src', 'main', 'java', 'com', 'helloworld');
|
|
64
|
+
fs.mkdirSync(javaAppDir, { recursive: true });
|
|
65
|
+
const javaFilePath = path.join(javaAppDir, 'MainApplication.java');
|
|
66
|
+
const originalContent = '// Java file content';
|
|
67
|
+
fs.writeFileSync(javaFilePath, originalContent);
|
|
68
|
+
|
|
69
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
await initAndroid();
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
expect(consoleSpy).toHaveBeenCalledWith('MainApplication.java is not supported. Please migrate to MainApplication.kt.');
|
|
77
|
+
const finalContent = fs.readFileSync(javaFilePath, 'utf-8');
|
|
78
|
+
expect(finalContent).toBe(originalContent); // Ensure file is not modified
|
|
79
|
+
|
|
80
|
+
consoleSpy.mockRestore();
|
|
81
|
+
|
|
82
|
+
process.chdir(originalCwd);
|
|
83
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { modifyObjectiveCAppDelegate, modifySwiftAppDelegate } = require('../initIos');
|
|
2
|
+
|
|
3
|
+
// https://github.com/react-native-community/template/blob/0.80.2/template/ios/HelloWorld/AppDelegate.swift
|
|
4
|
+
const swiftTemplate = `
|
|
5
|
+
import UIKit
|
|
6
|
+
import React
|
|
7
|
+
import React_RCTAppDelegate
|
|
8
|
+
import ReactAppDependencyProvider
|
|
9
|
+
|
|
10
|
+
@main
|
|
11
|
+
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
12
|
+
var window: UIWindow?
|
|
13
|
+
var reactNativeDelegate: ReactNativeDelegate?
|
|
14
|
+
var reactNativeFactory: RCTReactNativeFactory?
|
|
15
|
+
|
|
16
|
+
func application(
|
|
17
|
+
_ application: UIApplication,
|
|
18
|
+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
|
19
|
+
) -> Bool {
|
|
20
|
+
let delegate = ReactNativeDelegate()
|
|
21
|
+
let factory = RCTReactNativeFactory(delegate: delegate)
|
|
22
|
+
delegate.dependencyProvider = RCTAppDependencyProvider()
|
|
23
|
+
reactNativeDelegate = delegate
|
|
24
|
+
reactNativeFactory = factory
|
|
25
|
+
window = UIWindow(frame: UIScreen.main.bounds)
|
|
26
|
+
factory.startReactNative(
|
|
27
|
+
withModuleName: "HelloWorld",
|
|
28
|
+
in: window,
|
|
29
|
+
launchOptions: launchOptions
|
|
30
|
+
)
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
|
|
36
|
+
override func sourceURL(for bridge: RCTBridge) -> URL? {
|
|
37
|
+
self.bundleURL()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override func bundleURL() -> URL? {
|
|
41
|
+
#if DEBUG
|
|
42
|
+
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
|
|
43
|
+
#else
|
|
44
|
+
Bundle.main.url(forResource: "main", withExtension: "jsbundle")
|
|
45
|
+
#endif
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
// https://github.com/react-native-community/template/blob/0.76.9/template/ios/HelloWorld/AppDelegate.mm
|
|
51
|
+
const objcTemplate = `
|
|
52
|
+
#import "AppDelegate.h"
|
|
53
|
+
|
|
54
|
+
#import <React/RCTBundleURLProvider.h>
|
|
55
|
+
|
|
56
|
+
@implementation AppDelegate
|
|
57
|
+
|
|
58
|
+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
|
59
|
+
{
|
|
60
|
+
self.moduleName = @"HelloWorld";
|
|
61
|
+
// You can add your custom initial props in the dictionary below.
|
|
62
|
+
// They will be passed down to the ViewController used by React Native.
|
|
63
|
+
self.initialProps = @{};
|
|
64
|
+
|
|
65
|
+
return [super application:application didFinishLaunchingWithOptions:launchOptions];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
|
69
|
+
{
|
|
70
|
+
return [self bundleURL];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
- (NSURL *)bundleURL
|
|
74
|
+
{
|
|
75
|
+
#if DEBUG
|
|
76
|
+
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
|
|
77
|
+
#else
|
|
78
|
+
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
79
|
+
#endif
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@end
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
describe('iOS init command - pure functions', () => {
|
|
86
|
+
it('should correctly modify Swift AppDelegate content', () => {
|
|
87
|
+
const modifiedContent = modifySwiftAppDelegate(swiftTemplate);
|
|
88
|
+
expect(modifiedContent).toContain('CodePush.bundleURL()');
|
|
89
|
+
expect(modifiedContent).not.toContain('Bundle.main.url(forResource: "main", withExtension: "jsbundle")');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should correctly modify Objective-C AppDelegate content', () => {
|
|
93
|
+
const modifiedContent = modifyObjectiveCAppDelegate(objcTemplate);
|
|
94
|
+
expect(modifiedContent).toContain('#import <CodePush/CodePush.h>');
|
|
95
|
+
expect(modifiedContent).toContain('[CodePush bundleURL]');
|
|
96
|
+
expect(modifiedContent).not.toContain('[[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -25,6 +25,7 @@ const fs = require("fs");
|
|
|
25
25
|
* @param identifier {string?}
|
|
26
26
|
* @param mandatory {boolean?}
|
|
27
27
|
* @param enable {boolean?}
|
|
28
|
+
* @param rollout {number?}
|
|
28
29
|
* @returns {Promise<void>}
|
|
29
30
|
*/
|
|
30
31
|
async function addToReleaseHistory(
|
|
@@ -38,6 +39,7 @@ async function addToReleaseHistory(
|
|
|
38
39
|
identifier,
|
|
39
40
|
mandatory,
|
|
40
41
|
enable,
|
|
42
|
+
rollout
|
|
41
43
|
) {
|
|
42
44
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
43
45
|
|
|
@@ -54,6 +56,10 @@ async function addToReleaseHistory(
|
|
|
54
56
|
mandatory: mandatory,
|
|
55
57
|
downloadUrl: bundleDownloadUrl,
|
|
56
58
|
packageHash: packageHash,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (typeof rollout === 'number') {
|
|
62
|
+
newReleaseHistory[appVersion].rollout = rollout;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
try {
|
|
@@ -16,6 +16,7 @@ program.command('release')
|
|
|
16
16
|
.option('-j, --js-bundle-name <string>', 'JS bundle file name (default-ios: "main.jsbundle" / default-android: "index.android.bundle")')
|
|
17
17
|
.option('-m, --mandatory <bool>', 'make the release to be mandatory', parseBoolean, false)
|
|
18
18
|
.option('--enable <bool>', 'make the release to be enabled', parseBoolean, true)
|
|
19
|
+
.option('--rollout <number>', 'rollout percentage (0-100)', parseFloat)
|
|
19
20
|
.option('--skip-bundle <bool>', 'skip bundle process', parseBoolean, false)
|
|
20
21
|
.option('--skip-cleanup <bool>', 'skip cleanup process', parseBoolean, false)
|
|
21
22
|
.option('--output-bundle-dir <string>', 'name of directory containing the bundle file created by the "bundle" command', OUTPUT_BUNDLE_DIR)
|
|
@@ -32,6 +33,7 @@ program.command('release')
|
|
|
32
33
|
* @param {string} options.bundleName
|
|
33
34
|
* @param {string} options.mandatory
|
|
34
35
|
* @param {string} options.enable
|
|
36
|
+
* @param {number} options.rollout
|
|
35
37
|
* @param {string} options.skipBundle
|
|
36
38
|
* @param {string} options.skipCleanup
|
|
37
39
|
* @param {string} options.outputBundleDir
|
|
@@ -40,6 +42,11 @@ program.command('release')
|
|
|
40
42
|
.action(async (options) => {
|
|
41
43
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
42
44
|
|
|
45
|
+
if (typeof options.rollout === 'number' && (options.rollout < 0 || options.rollout > 100)) {
|
|
46
|
+
console.error('Rollout percentage number must be between 0 and 100 (inclusive).');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
await release(
|
|
44
51
|
config.bundleUploader,
|
|
45
52
|
config.getReleaseHistory,
|
|
@@ -54,6 +61,7 @@ program.command('release')
|
|
|
54
61
|
options.bundleName,
|
|
55
62
|
options.mandatory,
|
|
56
63
|
options.enable,
|
|
64
|
+
options.rollout,
|
|
57
65
|
options.skipBundle,
|
|
58
66
|
options.skipCleanup,
|
|
59
67
|
`${options.outputPath}/${options.outputBundleDir}`,
|
|
@@ -33,6 +33,7 @@ const { addToReleaseHistory } = require("./addToReleaseHistory");
|
|
|
33
33
|
* @param jsBundleName {string}
|
|
34
34
|
* @param mandatory {boolean}
|
|
35
35
|
* @param enable {boolean}
|
|
36
|
+
* @param rollout {number}
|
|
36
37
|
* @param skipBundle {boolean}
|
|
37
38
|
* @param skipCleanup {boolean}
|
|
38
39
|
* @param bundleDirectory {string}
|
|
@@ -52,6 +53,7 @@ async function release(
|
|
|
52
53
|
jsBundleName,
|
|
53
54
|
mandatory,
|
|
54
55
|
enable,
|
|
56
|
+
rollout,
|
|
55
57
|
skipBundle,
|
|
56
58
|
skipCleanup,
|
|
57
59
|
bundleDirectory,
|
|
@@ -82,6 +84,7 @@ async function release(
|
|
|
82
84
|
identifier,
|
|
83
85
|
mandatory,
|
|
84
86
|
enable,
|
|
87
|
+
rollout,
|
|
85
88
|
)
|
|
86
89
|
|
|
87
90
|
if (!skipCleanup) {
|
|
@@ -12,6 +12,7 @@ program.command('update-history')
|
|
|
12
12
|
.option('-c, --config <path>', 'set config file name (JS/TS)', CONFIG_FILE_NAME)
|
|
13
13
|
.option('-m, --mandatory <bool>', 'make the release to be mandatory', parseBoolean, undefined)
|
|
14
14
|
.option('-e, --enable <bool>', 'make the release to be enabled', parseBoolean, undefined)
|
|
15
|
+
.option('--rollout <number>', 'rollout percentage (0-100)', parseFloat, undefined)
|
|
15
16
|
/**
|
|
16
17
|
* @param {Object} options
|
|
17
18
|
* @param {string} options.appVersion
|
|
@@ -21,6 +22,7 @@ program.command('update-history')
|
|
|
21
22
|
* @param {string} options.config
|
|
22
23
|
* @param {string} options.mandatory
|
|
23
24
|
* @param {string} options.enable
|
|
25
|
+
* @param {number} options.rollout
|
|
24
26
|
* @return {void}
|
|
25
27
|
*/
|
|
26
28
|
.action(async (options) => {
|
|
@@ -40,6 +42,7 @@ program.command('update-history')
|
|
|
40
42
|
options.identifier,
|
|
41
43
|
options.mandatory,
|
|
42
44
|
options.enable,
|
|
45
|
+
options.rollout
|
|
43
46
|
)
|
|
44
47
|
});
|
|
45
48
|
|
|
@@ -23,6 +23,7 @@ const path = require('path');
|
|
|
23
23
|
* @param identifier {string?}
|
|
24
24
|
* @param mandatory {boolean?}
|
|
25
25
|
* @param enable {boolean?}
|
|
26
|
+
* @param rollout {number?}
|
|
26
27
|
* @returns {Promise<void>}
|
|
27
28
|
*/
|
|
28
29
|
async function updateReleaseHistory(
|
|
@@ -34,6 +35,7 @@ async function updateReleaseHistory(
|
|
|
34
35
|
identifier,
|
|
35
36
|
mandatory,
|
|
36
37
|
enable,
|
|
38
|
+
rollout
|
|
37
39
|
) {
|
|
38
40
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
39
41
|
|
|
@@ -42,6 +44,7 @@ async function updateReleaseHistory(
|
|
|
42
44
|
|
|
43
45
|
if (typeof mandatory === "boolean") updateInfo.mandatory = mandatory;
|
|
44
46
|
if (typeof enable === "boolean") updateInfo.enabled = enable;
|
|
47
|
+
if (typeof rollout === "number") updateInfo.rollout = rollout;
|
|
45
48
|
|
|
46
49
|
try {
|
|
47
50
|
const JSON_FILE_NAME = `${binaryVersion}.json`;
|
package/cli/index.js
CHANGED
|
@@ -21,7 +21,7 @@ function androidMainApplicationApplyImplementation(
|
|
|
21
21
|
Please add "${add.replace(/\n/g, '').trim()}" to the MainApplication.kt.
|
|
22
22
|
Supported format: Expo SDK default template.
|
|
23
23
|
|
|
24
|
-
Android manual setup: https://github.com/Soomgo-Mobile/react-native-code-push#
|
|
24
|
+
Android manual setup: https://github.com/Soomgo-Mobile/react-native-code-push#2-1-manual-setup
|
|
25
25
|
`,
|
|
26
26
|
);
|
|
27
27
|
|
|
@@ -24,7 +24,7 @@ function iosApplyImplementation(
|
|
|
24
24
|
Please ${replace ? 'replace' : 'add'} "${add.replace(/\n/g, '').trim()}" to the AppDelegate.(m|swift).
|
|
25
25
|
Supported format: Expo SDK default template.
|
|
26
26
|
|
|
27
|
-
iOS manual setup: https://github.com/Soomgo-Mobile/react-native-code-push#2-
|
|
27
|
+
iOS manual setup: https://github.com/Soomgo-Mobile/react-native-code-push#2-1-manual-setup
|
|
28
28
|
`,
|
|
29
29
|
);
|
|
30
30
|
|
|
@@ -126,7 +126,7 @@ const withIosBridgingHeader = (config) => {
|
|
|
126
126
|
#import <CodePush/CodePush.h>
|
|
127
127
|
|
|
128
128
|
Supported format: Expo SDK default template.
|
|
129
|
-
iOS manual setup: https://github.com/Soomgo-Mobile/react-native-code-push#2-
|
|
129
|
+
iOS manual setup: https://github.com/Soomgo-Mobile/react-native-code-push#2-1-manual-setup
|
|
130
130
|
`
|
|
131
131
|
);
|
|
132
132
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bravemobile/react-native-code-push",
|
|
3
|
-
"version": "12.0.0-beta.
|
|
3
|
+
"version": "12.0.0-beta.5",
|
|
4
4
|
"description": "React Native plugin for the CodePush service",
|
|
5
5
|
"main": "CodePush.js",
|
|
6
6
|
"typings": "typings/react-native-code-push.d.ts",
|
|
@@ -89,7 +89,8 @@
|
|
|
89
89
|
"ts-node": "^10.9.2",
|
|
90
90
|
"tslint": "^6.1.3",
|
|
91
91
|
"typescript": "5.0.4",
|
|
92
|
-
"typescript-eslint": "^8.11.0"
|
|
92
|
+
"typescript-eslint": "^8.11.0",
|
|
93
|
+
"xcode": "^3.0.1"
|
|
93
94
|
},
|
|
94
95
|
"engines": {
|
|
95
96
|
"node": ">=18"
|
|
@@ -29,6 +29,7 @@ export interface ReleaseInfo {
|
|
|
29
29
|
mandatory: boolean;
|
|
30
30
|
downloadUrl: string;
|
|
31
31
|
packageHash: string;
|
|
32
|
+
rollout?: number;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// from code-push SDK
|
|
@@ -93,6 +94,10 @@ export interface CodePushOptions extends SyncOptions {
|
|
|
93
94
|
* Callback function that is called when sync process failed.
|
|
94
95
|
*/
|
|
95
96
|
onSyncError?: (label: string, error: Error) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Callback function that is called when rollout is skipped.
|
|
99
|
+
*/
|
|
100
|
+
onRolloutSkipped?: (label: string, error: Error) => void;
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
export interface DownloadProgress {
|
|
@@ -24,13 +24,19 @@ class BaseVersioning {
|
|
|
24
24
|
this.sortedReleaseHistory = Object.entries(releaseHistory).sort(
|
|
25
25
|
([a], [b]) => this.sortingMethod(a, b)
|
|
26
26
|
);
|
|
27
|
+
|
|
28
|
+
/** @type {boolean} */
|
|
29
|
+
this.isLatestReleaseInRollout = true;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
33
|
* @return {[ReleaseVersion, ReleaseInfo][]}
|
|
31
34
|
*/
|
|
32
35
|
get sortedEnabledReleaseHistory() {
|
|
33
|
-
|
|
36
|
+
const list = this.sortedReleaseHistory.filter(([_, bundle]) => bundle.enabled)
|
|
37
|
+
// If the latest release is not a rollout target, exclude it from the list.
|
|
38
|
+
// - This also means that rollout deployment can only be applied to the latest release.
|
|
39
|
+
return this.isLatestReleaseInRollout ? list : list.slice(1);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -42,6 +48,11 @@ class BaseVersioning {
|
|
|
42
48
|
);
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/** @param isLatestReleaseInRollout {boolean} */
|
|
52
|
+
setIsLatestReleaseInRollout(isLatestReleaseInRollout) {
|
|
53
|
+
this.isLatestReleaseInRollout = isLatestReleaseInRollout;
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
/**
|
|
46
57
|
* find latest release in releaseHistory
|
|
47
58
|
* @return {[ReleaseVersion, ReleaseInfo]}
|
|
@@ -48,6 +48,36 @@ describe('Semver Versioning Test', () => {
|
|
|
48
48
|
{ enabled: true, mandatory: true, downloadUrl: 'R3', packageHash: 'P3' }
|
|
49
49
|
])
|
|
50
50
|
})
|
|
51
|
+
|
|
52
|
+
it('should return the previous enabled release if the latest release is not a rollout target', () => {
|
|
53
|
+
const RELEASED_BUNDLES = {
|
|
54
|
+
'1.0.0': { enabled: true, mandatory: false, ...MOCK_INFOS },
|
|
55
|
+
'1.1.0': { enabled: false, mandatory: false, ...MOCK_INFOS }, // not enabled
|
|
56
|
+
'1.2.0': { enabled: true, mandatory: false, ...MOCK_INFOS, rollout: 30 },
|
|
57
|
+
};
|
|
58
|
+
const LATEST_IS_ROLLOUT_TARGET = false;
|
|
59
|
+
|
|
60
|
+
const versioning = new SemverVersioning(RELEASED_BUNDLES);
|
|
61
|
+
versioning.setIsLatestReleaseInRollout(LATEST_IS_ROLLOUT_TARGET);
|
|
62
|
+
|
|
63
|
+
const [latestVersion] = versioning.findLatestRelease();
|
|
64
|
+
expect(latestVersion).toEqual('1.0.0');
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should return the latest release if the latest release is a rollout target', () => {
|
|
68
|
+
const RELEASED_BUNDLES = {
|
|
69
|
+
'1.0.0': { enabled: true, mandatory: false, ...MOCK_INFOS },
|
|
70
|
+
'1.1.0': { enabled: true, mandatory: false, ...MOCK_INFOS },
|
|
71
|
+
'1.2.0': { enabled: true, mandatory: false, ...MOCK_INFOS, rollout: 30 },
|
|
72
|
+
};
|
|
73
|
+
const LATEST_IS_ROLLOUT_TARGET = true;
|
|
74
|
+
|
|
75
|
+
const versioning = new SemverVersioning(RELEASED_BUNDLES);
|
|
76
|
+
versioning.setIsLatestReleaseInRollout(LATEST_IS_ROLLOUT_TARGET);
|
|
77
|
+
|
|
78
|
+
const [latestVersion] = versioning.findLatestRelease();
|
|
79
|
+
expect(latestVersion).toEqual('1.2.0');
|
|
80
|
+
})
|
|
51
81
|
})
|
|
52
82
|
|
|
53
83
|
describe('checkIsMandatory', () => {
|
|
@@ -159,6 +189,26 @@ describe('Semver Versioning Test', () => {
|
|
|
159
189
|
expect(new SemverVersioning(RELEASED_BUNDLES).checkIsMandatory('1.1.1')).toBe(false);
|
|
160
190
|
expect(new SemverVersioning(RELEASED_BUNDLES).checkIsMandatory('1.2.0')).toBe(true);
|
|
161
191
|
});
|
|
192
|
+
|
|
193
|
+
it('When having not-enabled releases and latest mandatory version is not a rollout target', () => {
|
|
194
|
+
const RELEASED_BUNDLES = {
|
|
195
|
+
'1.0.0': FIRST_RELEASE_INFO,
|
|
196
|
+
'1.0.1': { enabled: true, mandatory: true, ...MOCK_INFOS },
|
|
197
|
+
'1.1.0': { enabled: false, mandatory: false, ...MOCK_INFOS },
|
|
198
|
+
'1.1.1': { enabled: true, mandatory: true, ...MOCK_INFOS },
|
|
199
|
+
'1.2.0': { enabled: true, mandatory: true, ...MOCK_INFOS, rollout: 30 },
|
|
200
|
+
};
|
|
201
|
+
const LATEST_IS_ROLLOUT_TARGET = false;
|
|
202
|
+
|
|
203
|
+
const versioning = new SemverVersioning(RELEASED_BUNDLES);
|
|
204
|
+
versioning.setIsLatestReleaseInRollout(LATEST_IS_ROLLOUT_TARGET);
|
|
205
|
+
|
|
206
|
+
expect(versioning.checkIsMandatory('1.0.0')).toBe(true); // update to 1.1.1
|
|
207
|
+
expect(versioning.checkIsMandatory('1.0.1')).toBe(true); // update to 1.1.1
|
|
208
|
+
expect(versioning.checkIsMandatory('1.1.0')).toBe(true); // update to 1.1.1
|
|
209
|
+
expect(versioning.checkIsMandatory('1.1.1')).toBe(false); // up to date
|
|
210
|
+
expect(versioning.checkIsMandatory('1.2.0')).toBe(true); // rollback target is treated as mandatory
|
|
211
|
+
})
|
|
162
212
|
})
|
|
163
213
|
})
|
|
164
214
|
|
|
@@ -201,5 +251,33 @@ describe('Semver Versioning Test', () => {
|
|
|
201
251
|
expect(new SemverVersioning(RELEASED_BUNDLES_1).shouldRollbackToBinary('1.2.0')).toBe(false)
|
|
202
252
|
expect(new SemverVersioning(RELEASED_BUNDLES_2).shouldRollbackToBinary('1.2.0-rc.2')).toBe(false)
|
|
203
253
|
})
|
|
254
|
+
|
|
255
|
+
it ('should return true if the latest release is not a rollout target', () => {
|
|
256
|
+
const RELEASED_BUNDLES = {
|
|
257
|
+
'1.0.0': { enabled: true, mandatory: false, ...MOCK_INFOS },
|
|
258
|
+
'1.1.0': { enabled: true, mandatory: false, ...MOCK_INFOS, rollout: 30 },
|
|
259
|
+
};
|
|
260
|
+
const LATEST_IS_ROLLOUT_TARGET = false;
|
|
261
|
+
|
|
262
|
+
const versioning = new SemverVersioning(RELEASED_BUNDLES);
|
|
263
|
+
versioning.setIsLatestReleaseInRollout(LATEST_IS_ROLLOUT_TARGET);
|
|
264
|
+
|
|
265
|
+
// If the latest release was a rollout target and the rollout value is later reduced, the app might run on version 1.1.0.
|
|
266
|
+
expect(versioning.shouldRollbackToBinary('1.1.0')).toBe(true);
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it ('should return false if the latest release and the previous release are both rollout targets', () => {
|
|
270
|
+
const RELEASED_BUNDLES = {
|
|
271
|
+
'1.0.0': { enabled: true, mandatory: false, ...MOCK_INFOS },
|
|
272
|
+
'1.1.0': { enabled: true, mandatory: false, ...MOCK_INFOS, rollout: 30 },
|
|
273
|
+
'1.2.0': { enabled: true, mandatory: false, ...MOCK_INFOS, rollout: 30 },
|
|
274
|
+
};
|
|
275
|
+
const LATEST_IS_ROLLOUT_TARGET = true;
|
|
276
|
+
|
|
277
|
+
const versioning = new SemverVersioning(RELEASED_BUNDLES);
|
|
278
|
+
versioning.setIsLatestReleaseInRollout(LATEST_IS_ROLLOUT_TARGET);
|
|
279
|
+
|
|
280
|
+
expect(versioning.shouldRollbackToBinary('1.0.0')).toBe(false); // update to 1.1.0
|
|
281
|
+
})
|
|
204
282
|
})
|
|
205
283
|
})
|