@bravemobile/react-native-code-push 12.3.1 → 12.4.0
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.podspec +0 -2
- package/README.md +4 -2
- package/android/app/build.gradle +0 -1
- package/android/app/proguard-rules.pro +5 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +0 -35
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +0 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +33 -9
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +1 -28
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +1 -104
- package/cli/commands/bundleCommand/bundleCodePush.ts +29 -0
- package/cli/commands/bundleCommand/index.ts +3 -0
- package/cli/commands/releaseCommand/index.ts +3 -0
- package/cli/commands/releaseCommand/release.ts +2 -1
- package/cli/dist/commands/bundleCommand/bundleCodePush.js +14 -1
- package/cli/dist/commands/bundleCommand/index.js +2 -1
- package/cli/dist/commands/releaseCommand/index.js +2 -1
- package/cli/dist/commands/releaseCommand/release.js +2 -2
- package/cli/dist/utils/hash-utils.js +1 -4
- package/cli/utils/hash-utils.ts +1 -4
- package/expo/plugin/withCodePushAndroid.js +15 -16
- package/ios/CodePush/CodePush.h +0 -16
- package/ios/CodePush/CodePush.m +0 -12
- package/ios/CodePush/CodePushConfig.m +0 -10
- package/ios/CodePush/CodePushPackage.m +0 -39
- package/ios/CodePush/CodePushUpdateUtils.m +1 -91
- package/ios/CodePush.xcodeproj/project.pbxproj +0 -324
- package/package.json +11 -28
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushInvalidPublicKeyException.java +0 -12
- package/ios/CodePush/Base64/Base64/MF_Base64Additions.h +0 -34
- package/ios/CodePush/Base64/Base64/MF_Base64Additions.m +0 -252
- package/ios/CodePush/Base64/README.md +0 -47
- package/ios/CodePush/JWT/Core/Algorithms/Base/JWTAlgorithm.h +0 -69
- package/ios/CodePush/JWT/Core/Algorithms/Base/JWTAlgorithmFactory.h +0 -16
- package/ios/CodePush/JWT/Core/Algorithms/Base/JWTAlgorithmFactory.m +0 -51
- package/ios/CodePush/JWT/Core/Algorithms/Base/JWTAlgorithmNone.h +0 -15
- package/ios/CodePush/JWT/Core/Algorithms/Base/JWTAlgorithmNone.m +0 -55
- package/ios/CodePush/JWT/Core/Algorithms/ESFamily/JWTAlgorithmESBase.h +0 -24
- package/ios/CodePush/JWT/Core/Algorithms/ESFamily/JWTAlgorithmESBase.m +0 -41
- package/ios/CodePush/JWT/Core/Algorithms/HSFamily/JWTAlgorithmHSBase.h +0 -28
- package/ios/CodePush/JWT/Core/Algorithms/HSFamily/JWTAlgorithmHSBase.m +0 -205
- package/ios/CodePush/JWT/Core/Algorithms/Holders/JWTAlgorithmDataHolder.h +0 -103
- package/ios/CodePush/JWT/Core/Algorithms/Holders/JWTAlgorithmDataHolder.m +0 -322
- package/ios/CodePush/JWT/Core/Algorithms/Holders/JWTAlgorithmDataHolderChain.h +0 -37
- package/ios/CodePush/JWT/Core/Algorithms/Holders/JWTAlgorithmDataHolderChain.m +0 -145
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/JWTAlgorithmRSBase.h +0 -35
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/JWTAlgorithmRSBase.m +0 -551
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/JWTRSAlgorithm.h +0 -23
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/RSKeys/JWTCryptoKey.h +0 -43
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/RSKeys/JWTCryptoKey.m +0 -230
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/RSKeys/JWTCryptoKeyExtractor.h +0 -31
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/RSKeys/JWTCryptoKeyExtractor.m +0 -113
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/RSKeys/JWTCryptoSecurity.h +0 -38
- package/ios/CodePush/JWT/Core/Algorithms/RSFamily/RSKeys/JWTCryptoSecurity.m +0 -500
- package/ios/CodePush/JWT/Core/ClaimSet/JWTClaim.h +0 -18
- package/ios/CodePush/JWT/Core/ClaimSet/JWTClaim.m +0 -214
- package/ios/CodePush/JWT/Core/ClaimSet/JWTClaimsSet.h +0 -23
- package/ios/CodePush/JWT/Core/ClaimSet/JWTClaimsSet.m +0 -29
- package/ios/CodePush/JWT/Core/ClaimSet/JWTClaimsSetSerializer.h +0 -19
- package/ios/CodePush/JWT/Core/ClaimSet/JWTClaimsSetSerializer.m +0 -68
- package/ios/CodePush/JWT/Core/ClaimSet/JWTClaimsSetVerifier.h +0 -18
- package/ios/CodePush/JWT/Core/ClaimSet/JWTClaimsSetVerifier.m +0 -72
- package/ios/CodePush/JWT/Core/Coding/JWTCoding+ResultTypes.h +0 -67
- package/ios/CodePush/JWT/Core/Coding/JWTCoding+ResultTypes.m +0 -111
- package/ios/CodePush/JWT/Core/Coding/JWTCoding+VersionOne.h +0 -119
- package/ios/CodePush/JWT/Core/Coding/JWTCoding+VersionOne.m +0 -307
- package/ios/CodePush/JWT/Core/Coding/JWTCoding+VersionThree.h +0 -94
- package/ios/CodePush/JWT/Core/Coding/JWTCoding+VersionThree.m +0 -619
- package/ios/CodePush/JWT/Core/Coding/JWTCoding+VersionTwo.h +0 -164
- package/ios/CodePush/JWT/Core/Coding/JWTCoding+VersionTwo.m +0 -514
- package/ios/CodePush/JWT/Core/Coding/JWTCoding.h +0 -24
- package/ios/CodePush/JWT/Core/Coding/JWTCoding.m +0 -11
- package/ios/CodePush/JWT/Core/FrameworkSupplement/JWT.h +0 -52
- package/ios/CodePush/JWT/Core/FrameworkSupplement/Map.modulemap +0 -5
- package/ios/CodePush/JWT/Core/Supplement/JWTBase64Coder.h +0 -28
- package/ios/CodePush/JWT/Core/Supplement/JWTBase64Coder.m +0 -70
- package/ios/CodePush/JWT/Core/Supplement/JWTDeprecations.h +0 -22
- package/ios/CodePush/JWT/Core/Supplement/JWTErrorDescription.h +0 -34
- package/ios/CodePush/JWT/Core/Supplement/JWTErrorDescription.m +0 -73
- package/ios/CodePush/JWT/LICENSE +0 -19
- package/ios/CodePush/JWT/README.md +0 -489
package/CodePush.podspec
CHANGED
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
### 🚀 New Architecture support
|
|
10
10
|
|
|
11
|
-
Supports React Native 0.
|
|
11
|
+
Supports React Native 0.77 ~ 0.84.
|
|
12
12
|
|
|
13
13
|
> [!NOTE]
|
|
14
14
|
> If you are using React Native 0.76 or lower, please use version `12.0.2` of this library.
|
|
@@ -17,7 +17,7 @@ Supports React Native 0.74 ~ 0.83.
|
|
|
17
17
|
|
|
18
18
|
### ✅ Requirements
|
|
19
19
|
|
|
20
|
-
- **React Native**: 0.
|
|
20
|
+
- **React Native**: 0.77 or higher
|
|
21
21
|
- **iOS**: 15.5 or higher
|
|
22
22
|
- **Android**: API level 16 or higher
|
|
23
23
|
|
|
@@ -397,6 +397,8 @@ module.exports = Config;
|
|
|
397
397
|
|
|
398
398
|
> [!TIP]
|
|
399
399
|
> You can use `--help` command to see the available commands and options.
|
|
400
|
+
>
|
|
401
|
+
> For detailed documentation, see the [CLI README](cli/README.md) ([한국어](cli/README.ko.md)).
|
|
400
402
|
|
|
401
403
|
(interactive mode not supported yet)
|
|
402
404
|
|
package/android/app/build.gradle
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
private ** jsBundleLoader;
|
|
26
26
|
}
|
|
27
27
|
# bridgeless
|
|
28
|
+
-keepclassmembers class * extends com.facebook.react.runtime.ReactHostDelegate {
|
|
29
|
+
private ** jsBundleLoader;
|
|
30
|
+
private ** _jsBundleLoader;
|
|
31
|
+
}
|
|
32
|
+
# bridgeless
|
|
28
33
|
-keepclassmembers class com.facebook.react.runtime.ReactHostImpl {
|
|
29
34
|
private final ** mReactHostDelegate; # RN < 0.81
|
|
30
35
|
private final ** reactHostDelegate; # RN 0.81+
|
|
@@ -3,10 +3,8 @@ package com.microsoft.codepush.react;
|
|
|
3
3
|
import android.content.Context;
|
|
4
4
|
import android.content.pm.PackageInfo;
|
|
5
5
|
import android.content.pm.PackageManager;
|
|
6
|
-
import android.content.res.Resources;
|
|
7
6
|
|
|
8
7
|
import com.facebook.react.ReactPackage;
|
|
9
|
-
import com.facebook.react.bridge.JavaScriptModule;
|
|
10
8
|
import com.facebook.react.bridge.NativeModule;
|
|
11
9
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
12
10
|
import com.facebook.react.uimanager.ViewManager;
|
|
@@ -40,8 +38,6 @@ public class CodePush implements ReactPackage {
|
|
|
40
38
|
private Context mContext;
|
|
41
39
|
private final boolean mIsDebugMode;
|
|
42
40
|
|
|
43
|
-
private static String mPublicKey;
|
|
44
|
-
|
|
45
41
|
private static CodePush mCurrentInstance;
|
|
46
42
|
|
|
47
43
|
public static String getServiceUrl() {
|
|
@@ -75,33 +71,12 @@ public class CodePush implements ReactPackage {
|
|
|
75
71
|
|
|
76
72
|
mCurrentInstance = this;
|
|
77
73
|
|
|
78
|
-
String publicKeyFromStrings = getCustomPropertyFromStringsIfExist("PublicKey");
|
|
79
|
-
if (publicKeyFromStrings != null) mPublicKey = publicKeyFromStrings;
|
|
80
|
-
|
|
81
74
|
String serverUrlFromStrings = getCustomPropertyFromStringsIfExist("ServerUrl");
|
|
82
75
|
if (serverUrlFromStrings != null) mServerUrl = serverUrlFromStrings;
|
|
83
76
|
|
|
84
77
|
initializeUpdateAfterRestart();
|
|
85
78
|
}
|
|
86
79
|
|
|
87
|
-
private String getPublicKeyByResourceDescriptor(int publicKeyResourceDescriptor){
|
|
88
|
-
String publicKey;
|
|
89
|
-
try {
|
|
90
|
-
publicKey = mContext.getString(publicKeyResourceDescriptor);
|
|
91
|
-
} catch (Resources.NotFoundException e) {
|
|
92
|
-
throw new CodePushInvalidPublicKeyException(
|
|
93
|
-
"Unable to get public key, related resource descriptor " +
|
|
94
|
-
publicKeyResourceDescriptor +
|
|
95
|
-
" can not be found", e
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (publicKey.isEmpty()) {
|
|
100
|
-
throw new CodePushInvalidPublicKeyException("Specified public key is empty");
|
|
101
|
-
}
|
|
102
|
-
return publicKey;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
80
|
private String getCustomPropertyFromStringsIfExist(String propertyName) {
|
|
106
81
|
String property;
|
|
107
82
|
|
|
@@ -133,10 +108,6 @@ public class CodePush implements ReactPackage {
|
|
|
133
108
|
return mAssetsBundleFileName;
|
|
134
109
|
}
|
|
135
110
|
|
|
136
|
-
public String getPublicKey() {
|
|
137
|
-
return mPublicKey;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
111
|
public String getPackageFolder() {
|
|
141
112
|
JSONObject codePushLocalPackage = mUpdateManager.getCurrentPackage();
|
|
142
113
|
if (codePushLocalPackage == null) {
|
|
@@ -338,12 +309,6 @@ public class CodePush implements ReactPackage {
|
|
|
338
309
|
nativeModules.add(dialogModule);
|
|
339
310
|
return nativeModules;
|
|
340
311
|
}
|
|
341
|
-
|
|
342
|
-
// Deprecated in RN v0.47.
|
|
343
|
-
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
|
344
|
-
return new ArrayList<>();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
312
|
@Override
|
|
348
313
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactApplicationContext) {
|
|
349
314
|
return new ArrayList<>();
|
|
@@ -24,7 +24,6 @@ public class CodePushConstants {
|
|
|
24
24
|
public static final String RELATIVE_BUNDLE_PATH_KEY = "bundlePath";
|
|
25
25
|
public static final String STATUS_FILE = "codepush.json";
|
|
26
26
|
public static final String UNZIPPED_FOLDER_NAME = "unzipped";
|
|
27
|
-
public static final String BUNDLE_JWT_FILE = ".codepushrelease";
|
|
28
27
|
public static final String LATEST_ROLLBACK_INFO_KEY = "LATEST_ROLLBACK_INFO";
|
|
29
28
|
public static final String LATEST_ROLLBACK_PACKAGE_HASH_KEY = "packageHash";
|
|
30
29
|
public static final String LATEST_ROLLBACK_TIME_KEY = "time";
|
|
@@ -151,18 +151,31 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
|
|
|
151
151
|
|
|
152
152
|
@OptIn(markerClass = UnstableReactNativeAPI.class)
|
|
153
153
|
private void setJSBundleLoaderBridgeless(ReactHost reactHost, JSBundleLoader latestJSBundleLoader) throws NoSuchFieldException, IllegalAccessException {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
reactHostDelegateField = reactHost.getClass().getDeclaredField("mReactHostDelegate");
|
|
158
|
-
} catch (NoSuchFieldException e) {
|
|
154
|
+
// RN < 0.81
|
|
155
|
+
Field reactHostDelegateField = resolveDeclaredField(reactHost.getClass(), "mReactHostDelegate");
|
|
156
|
+
if (reactHostDelegateField == null) {
|
|
159
157
|
// RN >= 0.81
|
|
160
|
-
reactHostDelegateField = reactHost.getClass()
|
|
158
|
+
reactHostDelegateField = resolveDeclaredField(reactHost.getClass(), "reactHostDelegate");
|
|
159
|
+
}
|
|
160
|
+
if (reactHostDelegateField == null) {
|
|
161
|
+
throw new NoSuchFieldException("Unable to resolve ReactHostDelegate field.");
|
|
161
162
|
}
|
|
163
|
+
|
|
162
164
|
reactHostDelegateField.setAccessible(true);
|
|
163
165
|
ReactHostDelegate reactHostDelegate = (ReactHostDelegate) reactHostDelegateField.get(reactHost);
|
|
164
166
|
assert reactHostDelegate != null;
|
|
165
|
-
|
|
167
|
+
|
|
168
|
+
// Expo ReactHost delegate keeps this mutable backing field specifically
|
|
169
|
+
// so integrations can override the bundle loader at runtime.
|
|
170
|
+
Field jsBundleLoaderField = resolveDeclaredField(reactHostDelegate.getClass(), "_jsBundleLoader");
|
|
171
|
+
if (jsBundleLoaderField == null) {
|
|
172
|
+
// Fallback for non-Expo delegates.
|
|
173
|
+
jsBundleLoaderField = resolveDeclaredField(reactHostDelegate.getClass(), "jsBundleLoader");
|
|
174
|
+
}
|
|
175
|
+
if (jsBundleLoaderField == null) {
|
|
176
|
+
throw new NoSuchFieldException("Unable to resolve JSBundleLoader field.");
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
jsBundleLoaderField.setAccessible(true);
|
|
167
180
|
jsBundleLoaderField.set(reactHostDelegate, latestJSBundleLoader);
|
|
168
181
|
}
|
|
@@ -234,13 +247,24 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
|
|
|
234
247
|
private ReactHost resolveReactHost() {
|
|
235
248
|
ReactDelegate reactDelegate = resolveReactDelegate();
|
|
236
249
|
if (reactDelegate == null) {
|
|
237
|
-
CodePushUtils.log("Unable to resolve ReactDelegate");
|
|
238
250
|
return null;
|
|
239
251
|
}
|
|
240
252
|
|
|
241
253
|
return reactDelegate.getReactHost();
|
|
242
254
|
}
|
|
243
255
|
|
|
256
|
+
private Field resolveDeclaredField(Class<?> targetClass, String fieldName) {
|
|
257
|
+
Class<?> cursor = targetClass;
|
|
258
|
+
while (cursor != null) {
|
|
259
|
+
try {
|
|
260
|
+
return cursor.getDeclaredField(fieldName);
|
|
261
|
+
} catch (NoSuchFieldException ignored) {
|
|
262
|
+
cursor = cursor.getSuperclass();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
244
268
|
private void restartAppInternal(boolean onlyIfUpdateIsPending) {
|
|
245
269
|
if (this._restartInProgress) {
|
|
246
270
|
CodePushUtils.log("Restart request queued until the current restart is completed");
|
|
@@ -360,7 +384,7 @@ public class CodePushNativeModule extends ReactContextBaseJavaModule {
|
|
|
360
384
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
361
385
|
.emit(CodePushConstants.DOWNLOAD_PROGRESS_EVENT_NAME, latestDownloadProgress.createWritableMap());
|
|
362
386
|
}
|
|
363
|
-
}
|
|
387
|
+
});
|
|
364
388
|
|
|
365
389
|
JSONObject newPackage = mUpdateManager.getPackage(CodePushUtils.tryGetString(updatePackage, CodePushConstants.PACKAGE_HASH_KEY));
|
|
366
390
|
promise.resolve(CodePushUtils.convertJsonObjectToWritable(newPackage));
|
|
@@ -144,8 +144,7 @@ public class CodePushUpdateManager {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
public void downloadPackage(JSONObject updatePackage, String expectedBundleFileName,
|
|
147
|
-
DownloadProgressCallback progressCallback
|
|
148
|
-
String stringPublicKey) throws IOException {
|
|
147
|
+
DownloadProgressCallback progressCallback) throws IOException {
|
|
149
148
|
String newUpdateHash = updatePackage.optString(CodePushConstants.PACKAGE_HASH_KEY, null);
|
|
150
149
|
String newUpdateFolderPath = getPackageFolderPath(newUpdateHash);
|
|
151
150
|
String newUpdateMetadataPath = CodePushUtils.appendPathComponent(newUpdateFolderPath, CodePushConstants.PACKAGE_FILE_NAME);
|
|
@@ -266,32 +265,6 @@ public class CodePushUpdateManager {
|
|
|
266
265
|
}
|
|
267
266
|
|
|
268
267
|
CodePushUpdateUtils.verifyFolderHash(newUpdateFolderPath, newUpdateHash);
|
|
269
|
-
|
|
270
|
-
boolean isSignatureVerificationEnabled = (stringPublicKey != null);
|
|
271
|
-
|
|
272
|
-
String signaturePath = CodePushUpdateUtils.getSignatureFilePath(newUpdateFolderPath);
|
|
273
|
-
boolean isSignatureAppearedInBundle = FileUtils.fileAtPathExists(signaturePath);
|
|
274
|
-
|
|
275
|
-
if (isSignatureVerificationEnabled) {
|
|
276
|
-
if (isSignatureAppearedInBundle) {
|
|
277
|
-
CodePushUpdateUtils.verifyUpdateSignature(newUpdateFolderPath, newUpdateHash, stringPublicKey);
|
|
278
|
-
} else {
|
|
279
|
-
throw new CodePushInvalidUpdateException(
|
|
280
|
-
"Error! Public key was provided but there is no JWT signature within app bundle to verify. " +
|
|
281
|
-
"Possible reasons, why that might happen: \n" +
|
|
282
|
-
"1. You've been released CodePush bundle update using version of CodePush CLI that is not support code signing.\n" +
|
|
283
|
-
"2. You've been released CodePush bundle update without providing --privateKeyPath option."
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
} else {
|
|
287
|
-
if (isSignatureAppearedInBundle) {
|
|
288
|
-
CodePushUtils.log(
|
|
289
|
-
"Warning! JWT signature exists in codepush update but code integrity check couldn't be performed because there is no public key configured. " +
|
|
290
|
-
"Please ensure that public key is properly configured within your application."
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
268
|
CodePushUtils.setJSONValueForKey(updatePackage, CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, relativeBundlePath);
|
|
296
269
|
}
|
|
297
270
|
} else {
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
package com.microsoft.codepush.react;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
-
import android.util.Base64;
|
|
5
|
-
|
|
6
|
-
import com.nimbusds.jose.JWSVerifier;
|
|
7
|
-
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
|
8
|
-
import com.nimbusds.jwt.SignedJWT;
|
|
9
|
-
|
|
10
|
-
import java.security.interfaces.*;
|
|
11
4
|
|
|
12
5
|
import org.json.JSONArray;
|
|
13
6
|
import org.json.JSONException;
|
|
@@ -20,14 +13,10 @@ import java.io.FileNotFoundException;
|
|
|
20
13
|
import java.io.IOException;
|
|
21
14
|
import java.io.InputStream;
|
|
22
15
|
import java.security.DigestInputStream;
|
|
23
|
-
import java.security.KeyFactory;
|
|
24
16
|
import java.security.MessageDigest;
|
|
25
17
|
import java.security.NoSuchAlgorithmException;
|
|
26
|
-
import java.security.PublicKey;
|
|
27
|
-
import java.security.spec.X509EncodedKeySpec;
|
|
28
18
|
import java.util.ArrayList;
|
|
29
19
|
import java.util.Collections;
|
|
30
|
-
import java.util.Map;
|
|
31
20
|
|
|
32
21
|
public class CodePushUpdateUtils {
|
|
33
22
|
|
|
@@ -38,13 +27,10 @@ public class CodePushUpdateUtils {
|
|
|
38
27
|
public static boolean isHashIgnored(String relativeFilePath) {
|
|
39
28
|
final String __MACOSX = "__MACOSX/";
|
|
40
29
|
final String DS_STORE = ".DS_Store";
|
|
41
|
-
final String CODEPUSH_METADATA = ".codepushrelease";
|
|
42
30
|
|
|
43
31
|
return relativeFilePath.startsWith(__MACOSX)
|
|
44
32
|
|| relativeFilePath.equals(DS_STORE)
|
|
45
|
-
|| relativeFilePath.endsWith("/" + DS_STORE)
|
|
46
|
-
|| relativeFilePath.equals(CODEPUSH_METADATA)
|
|
47
|
-
|| relativeFilePath.endsWith("/" + CODEPUSH_METADATA);
|
|
33
|
+
|| relativeFilePath.endsWith("/" + DS_STORE);
|
|
48
34
|
}
|
|
49
35
|
|
|
50
36
|
private static void addContentsOfFolderToManifest(String folderPath, String pathPrefix, ArrayList<String> manifest) {
|
|
@@ -149,11 +135,6 @@ public class CodePushUpdateUtils {
|
|
|
149
135
|
try {
|
|
150
136
|
return CodePushUtils.getStringFromInputStream(context.getAssets().open(CodePushConstants.CODE_PUSH_OLD_HASH_FILE_NAME));
|
|
151
137
|
} catch (IOException ex) {
|
|
152
|
-
if (!isDebugMode) {
|
|
153
|
-
// Only print this message in "Release" mode. In "Debug", we may not have the
|
|
154
|
-
// hash if the build skips bundling the files.
|
|
155
|
-
CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition.");
|
|
156
|
-
}
|
|
157
138
|
}
|
|
158
139
|
return null;
|
|
159
140
|
}
|
|
@@ -188,88 +169,4 @@ public class CodePushUpdateUtils {
|
|
|
188
169
|
CodePushUtils.log("The update contents succeeded the data integrity check.");
|
|
189
170
|
}
|
|
190
171
|
|
|
191
|
-
public static Map<String, Object> verifyAndDecodeJWT(String jwt, PublicKey publicKey) {
|
|
192
|
-
try {
|
|
193
|
-
SignedJWT signedJWT = SignedJWT.parse(jwt);
|
|
194
|
-
JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) publicKey);
|
|
195
|
-
if (signedJWT.verify(verifier)) {
|
|
196
|
-
Map<String, Object> claims = signedJWT.getJWTClaimsSet().getClaims();
|
|
197
|
-
CodePushUtils.log("JWT verification succeeded, payload content: " + claims.toString());
|
|
198
|
-
return claims;
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
} catch (Exception ex) {
|
|
202
|
-
CodePushUtils.log(ex.getMessage());
|
|
203
|
-
CodePushUtils.log(ex.getStackTrace().toString());
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
public static PublicKey parsePublicKey(String stringPublicKey) {
|
|
209
|
-
try {
|
|
210
|
-
//remove unnecessary "begin/end public key" entries from string
|
|
211
|
-
stringPublicKey = stringPublicKey
|
|
212
|
-
.replace("-----BEGIN PUBLIC KEY-----", "")
|
|
213
|
-
.replace("-----END PUBLIC KEY-----", "")
|
|
214
|
-
.replace(NEW_LINE, "");
|
|
215
|
-
byte[] byteKey = Base64.decode(stringPublicKey.getBytes(), Base64.DEFAULT);
|
|
216
|
-
X509EncodedKeySpec X509Key = new X509EncodedKeySpec(byteKey);
|
|
217
|
-
KeyFactory kf = KeyFactory.getInstance("RSA");
|
|
218
|
-
|
|
219
|
-
return kf.generatePublic(X509Key);
|
|
220
|
-
} catch (Exception e) {
|
|
221
|
-
CodePushUtils.log(e.getMessage());
|
|
222
|
-
CodePushUtils.log(e.getStackTrace().toString());
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
public static String getSignatureFilePath(String updateFolderPath) {
|
|
228
|
-
return CodePushUtils.appendPathComponent(
|
|
229
|
-
CodePushUtils.appendPathComponent(updateFolderPath, CodePushConstants.CODE_PUSH_FOLDER_PREFIX),
|
|
230
|
-
CodePushConstants.BUNDLE_JWT_FILE
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
public static String getSignature(String folderPath) {
|
|
235
|
-
final String signatureFilePath = getSignatureFilePath(folderPath);
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
return FileUtils.readFileToString(signatureFilePath);
|
|
239
|
-
} catch (IOException e) {
|
|
240
|
-
CodePushUtils.log(e.getMessage());
|
|
241
|
-
CodePushUtils.log(e.getStackTrace().toString());
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
public static void verifyUpdateSignature(String folderPath, String packageHash, String stringPublicKey) throws CodePushInvalidUpdateException {
|
|
247
|
-
CodePushUtils.log("Verifying signature for folder path: " + folderPath);
|
|
248
|
-
|
|
249
|
-
final PublicKey publicKey = parsePublicKey(stringPublicKey);
|
|
250
|
-
if (publicKey == null) {
|
|
251
|
-
throw new CodePushInvalidUpdateException("The update could not be verified because no public key was found.");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
final String signature = getSignature(folderPath);
|
|
255
|
-
if (signature == null) {
|
|
256
|
-
throw new CodePushInvalidUpdateException("The update could not be verified because no signature was found.");
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
final Map<String, Object> claims = verifyAndDecodeJWT(signature, publicKey);
|
|
260
|
-
if (claims == null) {
|
|
261
|
-
throw new CodePushInvalidUpdateException("The update could not be verified because it was not signed by a trusted party.");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
final String contentHash = (String) claims.get("contentHash");
|
|
265
|
-
if (contentHash == null) {
|
|
266
|
-
throw new CodePushInvalidUpdateException("The update could not be verified because the signature did not specify a content hash.");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (!contentHash.equals(packageHash)) {
|
|
270
|
-
throw new CodePushInvalidUpdateException("The update contents failed the code signing check.");
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
CodePushUtils.log("The update contents succeeded the code signing check.");
|
|
274
|
-
}
|
|
275
172
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
2
3
|
import { prepareToBundleJS } from "../../functions/prepareToBundleJS.js";
|
|
3
4
|
import { runReactNativeBundleCommand } from "../../functions/runReactNativeBundleCommand.js";
|
|
4
5
|
import { runExpoBundleCommand } from "../../functions/runExpoBundleCommand.js";
|
|
@@ -17,6 +18,7 @@ export async function bundleCodePush(
|
|
|
17
18
|
entryFile: string = ENTRY_FILE,
|
|
18
19
|
jsBundleName: string, // JS bundle file name (not CodePush bundle file)
|
|
19
20
|
bundleDirectory: string, // CodePush bundle output directory
|
|
21
|
+
outputMetroDir?: string,
|
|
20
22
|
): Promise<string> {
|
|
21
23
|
if (fs.existsSync(outputRootPath)) {
|
|
22
24
|
fs.rmSync(outputRootPath, { recursive: true });
|
|
@@ -49,6 +51,8 @@ export async function bundleCodePush(
|
|
|
49
51
|
|
|
50
52
|
console.log('log: JS bundling complete');
|
|
51
53
|
|
|
54
|
+
copyMetroOutputsIfNeeded(outputRootPath, outputMetroDir, OUTPUT_CONTENT_PATH, _jsBundleName, SOURCEMAP_OUTPUT);
|
|
55
|
+
|
|
52
56
|
await runHermesEmitBinaryCommand(
|
|
53
57
|
_jsBundleName,
|
|
54
58
|
OUTPUT_CONTENT_PATH,
|
|
@@ -61,3 +65,28 @@ export async function bundleCodePush(
|
|
|
61
65
|
|
|
62
66
|
return codePushBundleFileName;
|
|
63
67
|
}
|
|
68
|
+
|
|
69
|
+
function copyMetroOutputsIfNeeded(
|
|
70
|
+
outputRootPath: string,
|
|
71
|
+
outputMetroDir: string | undefined,
|
|
72
|
+
outputContentPath: string,
|
|
73
|
+
jsBundleName: string,
|
|
74
|
+
sourceMapOutputPath: string,
|
|
75
|
+
) {
|
|
76
|
+
if (!outputMetroDir) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const resolvedOutputMetroDir = path.join(outputRootPath, outputMetroDir);
|
|
81
|
+
|
|
82
|
+
fs.mkdirSync(resolvedOutputMetroDir, { recursive: true });
|
|
83
|
+
fs.copyFileSync(
|
|
84
|
+
path.join(outputContentPath, jsBundleName),
|
|
85
|
+
path.join(resolvedOutputMetroDir, jsBundleName),
|
|
86
|
+
);
|
|
87
|
+
fs.copyFileSync(
|
|
88
|
+
sourceMapOutputPath,
|
|
89
|
+
path.join(resolvedOutputMetroDir, path.basename(sourceMapOutputPath)),
|
|
90
|
+
);
|
|
91
|
+
console.log(`log: Metro outputs copied to: ${resolvedOutputMetroDir}`);
|
|
92
|
+
}
|
|
@@ -9,6 +9,7 @@ type Options = {
|
|
|
9
9
|
entryFile: string;
|
|
10
10
|
bundleName: string;
|
|
11
11
|
outputBundleDir: string;
|
|
12
|
+
outputMetroDir?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
program.command('bundle')
|
|
@@ -18,6 +19,7 @@ program.command('bundle')
|
|
|
18
19
|
.option('-o, --output-path <string>', 'path to output root directory', ROOT_OUTPUT_DIR)
|
|
19
20
|
.option('-e, --entry-file <string>', 'path to JS/TS entry file', ENTRY_FILE)
|
|
20
21
|
.option('-b, --bundle-name <string>', 'bundle file name (default-ios: "main.jsbundle" / default-android: "index.android.bundle")')
|
|
22
|
+
.option('--output-metro-dir <string>', 'name of directory to copy the Metro JS bundle and sourcemap before Hermes compilation')
|
|
21
23
|
.option('--output-bundle-dir <string>', 'name of directory containing the bundle file created by the "bundle" command', OUTPUT_BUNDLE_DIR)
|
|
22
24
|
.action((options: Options) => {
|
|
23
25
|
bundleCodePush(
|
|
@@ -27,5 +29,6 @@ program.command('bundle')
|
|
|
27
29
|
options.entryFile,
|
|
28
30
|
options.bundleName,
|
|
29
31
|
`${options.outputPath}/${options.outputBundleDir}`,
|
|
32
|
+
options.outputMetroDir,
|
|
30
33
|
)
|
|
31
34
|
});
|
|
@@ -19,6 +19,7 @@ type Options = {
|
|
|
19
19
|
skipBundle: boolean;
|
|
20
20
|
skipCleanup: boolean;
|
|
21
21
|
outputBundleDir: string;
|
|
22
|
+
outputMetroDir?: string;
|
|
22
23
|
hashCalc?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -39,6 +40,7 @@ program.command('release')
|
|
|
39
40
|
.option('--skip-bundle <bool>', 'skip bundle process', parseBoolean, false)
|
|
40
41
|
.option('--hash-calc <bool>', 'calculates the bundle file hash used for packageHash in the release history (Requires setting --skip-bundle to true)', parseBoolean)
|
|
41
42
|
.option('--skip-cleanup <bool>', 'skip cleanup process', parseBoolean, false)
|
|
43
|
+
.option('--output-metro-dir <string>', 'name of directory to copy the Metro JS bundle and sourcemap before Hermes compilation')
|
|
42
44
|
.option('--output-bundle-dir <string>', 'name of directory containing the bundle file created by the "bundle" command', OUTPUT_BUNDLE_DIR)
|
|
43
45
|
.action(async (options: Options) => {
|
|
44
46
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
@@ -71,6 +73,7 @@ program.command('release')
|
|
|
71
73
|
options.skipBundle,
|
|
72
74
|
options.skipCleanup,
|
|
73
75
|
`${options.outputPath}/${options.outputBundleDir}`,
|
|
76
|
+
options.outputMetroDir,
|
|
74
77
|
options.hashCalc,
|
|
75
78
|
)
|
|
76
79
|
|
|
@@ -24,11 +24,12 @@ export async function release(
|
|
|
24
24
|
skipBundle: boolean,
|
|
25
25
|
skipCleanup: boolean,
|
|
26
26
|
bundleDirectory: string,
|
|
27
|
+
outputMetroDir?: string,
|
|
27
28
|
hashCalc?: boolean,
|
|
28
29
|
): Promise<void> {
|
|
29
30
|
const bundleFileName = skipBundle
|
|
30
31
|
? readBundleFileNameFrom(bundleDirectory)
|
|
31
|
-
: await bundleCodePush(framework, platform, outputPath, entryFile, jsBundleName, bundleDirectory);
|
|
32
|
+
: await bundleCodePush(framework, platform, outputPath, entryFile, jsBundleName, bundleDirectory, outputMetroDir);
|
|
32
33
|
const bundleFilePath = `${bundleDirectory}/${bundleFileName}`;
|
|
33
34
|
|
|
34
35
|
const packageHash = await (() => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
2
3
|
import { prepareToBundleJS } from "../../functions/prepareToBundleJS.js";
|
|
3
4
|
import { runReactNativeBundleCommand } from "../../functions/runReactNativeBundleCommand.js";
|
|
4
5
|
import { runExpoBundleCommand } from "../../functions/runExpoBundleCommand.js";
|
|
@@ -10,7 +11,8 @@ import { ROOT_OUTPUT_DIR, ENTRY_FILE } from "../../constant.js";
|
|
|
10
11
|
* @return {Promise<string>} CodePush bundle file name (equals to packageHash)
|
|
11
12
|
*/
|
|
12
13
|
export async function bundleCodePush(framework, platform = 'ios', outputRootPath = ROOT_OUTPUT_DIR, entryFile = ENTRY_FILE, jsBundleName, // JS bundle file name (not CodePush bundle file)
|
|
13
|
-
bundleDirectory
|
|
14
|
+
bundleDirectory, // CodePush bundle output directory
|
|
15
|
+
outputMetroDir) {
|
|
14
16
|
if (fs.existsSync(outputRootPath)) {
|
|
15
17
|
fs.rmSync(outputRootPath, { recursive: true });
|
|
16
18
|
}
|
|
@@ -26,9 +28,20 @@ bundleDirectory) {
|
|
|
26
28
|
runReactNativeBundleCommand(_jsBundleName, OUTPUT_CONTENT_PATH, platform, SOURCEMAP_OUTPUT, entryFile);
|
|
27
29
|
}
|
|
28
30
|
console.log('log: JS bundling complete');
|
|
31
|
+
copyMetroOutputsIfNeeded(outputRootPath, outputMetroDir, OUTPUT_CONTENT_PATH, _jsBundleName, SOURCEMAP_OUTPUT);
|
|
29
32
|
await runHermesEmitBinaryCommand(_jsBundleName, OUTPUT_CONTENT_PATH, SOURCEMAP_OUTPUT);
|
|
30
33
|
console.log('log: Hermes compilation complete');
|
|
31
34
|
const { bundleFileName: codePushBundleFileName } = await makeCodePushBundle(OUTPUT_CONTENT_PATH, bundleDirectory);
|
|
32
35
|
console.log(`log: CodePush bundle created (file path: ./${bundleDirectory}/${codePushBundleFileName})`);
|
|
33
36
|
return codePushBundleFileName;
|
|
34
37
|
}
|
|
38
|
+
function copyMetroOutputsIfNeeded(outputRootPath, outputMetroDir, outputContentPath, jsBundleName, sourceMapOutputPath) {
|
|
39
|
+
if (!outputMetroDir) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const resolvedOutputMetroDir = path.join(outputRootPath, outputMetroDir);
|
|
43
|
+
fs.mkdirSync(resolvedOutputMetroDir, { recursive: true });
|
|
44
|
+
fs.copyFileSync(path.join(outputContentPath, jsBundleName), path.join(resolvedOutputMetroDir, jsBundleName));
|
|
45
|
+
fs.copyFileSync(sourceMapOutputPath, path.join(resolvedOutputMetroDir, path.basename(sourceMapOutputPath)));
|
|
46
|
+
console.log(`log: Metro outputs copied to: ${resolvedOutputMetroDir}`);
|
|
47
|
+
}
|
|
@@ -8,7 +8,8 @@ program.command('bundle')
|
|
|
8
8
|
.option('-o, --output-path <string>', 'path to output root directory', ROOT_OUTPUT_DIR)
|
|
9
9
|
.option('-e, --entry-file <string>', 'path to JS/TS entry file', ENTRY_FILE)
|
|
10
10
|
.option('-b, --bundle-name <string>', 'bundle file name (default-ios: "main.jsbundle" / default-android: "index.android.bundle")')
|
|
11
|
+
.option('--output-metro-dir <string>', 'name of directory to copy the Metro JS bundle and sourcemap before Hermes compilation')
|
|
11
12
|
.option('--output-bundle-dir <string>', 'name of directory containing the bundle file created by the "bundle" command', OUTPUT_BUNDLE_DIR)
|
|
12
13
|
.action((options) => {
|
|
13
|
-
bundleCodePush(options.framework, options.platform, options.outputPath, options.entryFile, options.bundleName, `${options.outputPath}/${options.outputBundleDir}
|
|
14
|
+
bundleCodePush(options.framework, options.platform, options.outputPath, options.entryFile, options.bundleName, `${options.outputPath}/${options.outputBundleDir}`, options.outputMetroDir);
|
|
14
15
|
});
|
|
@@ -19,6 +19,7 @@ program.command('release')
|
|
|
19
19
|
.option('--skip-bundle <bool>', 'skip bundle process', parseBoolean, false)
|
|
20
20
|
.option('--hash-calc <bool>', 'calculates the bundle file hash used for packageHash in the release history (Requires setting --skip-bundle to true)', parseBoolean)
|
|
21
21
|
.option('--skip-cleanup <bool>', 'skip cleanup process', parseBoolean, false)
|
|
22
|
+
.option('--output-metro-dir <string>', 'name of directory to copy the Metro JS bundle and sourcemap before Hermes compilation')
|
|
22
23
|
.option('--output-bundle-dir <string>', 'name of directory containing the bundle file created by the "bundle" command', OUTPUT_BUNDLE_DIR)
|
|
23
24
|
.action(async (options) => {
|
|
24
25
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
@@ -30,7 +31,7 @@ program.command('release')
|
|
|
30
31
|
console.error('--hash-calc option can be used only when --skip-bundle is set to true.');
|
|
31
32
|
process.exit(1);
|
|
32
33
|
}
|
|
33
|
-
await release(config.bundleUploader, config.getReleaseHistory, config.setReleaseHistory, options.binaryVersion, options.appVersion, options.framework, options.platform, options.identifier, options.outputPath, options.entryFile, options.bundleName, options.mandatory, options.enable, options.rollout, options.skipBundle, options.skipCleanup, `${options.outputPath}/${options.outputBundleDir}`, options.hashCalc);
|
|
34
|
+
await release(config.bundleUploader, config.getReleaseHistory, config.setReleaseHistory, options.binaryVersion, options.appVersion, options.framework, options.platform, options.identifier, options.outputPath, options.entryFile, options.bundleName, options.mandatory, options.enable, options.rollout, options.skipBundle, options.skipCleanup, `${options.outputPath}/${options.outputBundleDir}`, options.outputMetroDir, options.hashCalc);
|
|
34
35
|
console.log('🚀 Release completed.');
|
|
35
36
|
});
|
|
36
37
|
function parseBoolean(value) {
|
|
@@ -4,10 +4,10 @@ import { bundleCodePush } from "../bundleCommand/bundleCodePush.js";
|
|
|
4
4
|
import { addToReleaseHistory } from "./addToReleaseHistory.js";
|
|
5
5
|
import { generatePackageHashFromDirectory } from "../../utils/hash-utils.js";
|
|
6
6
|
import { unzip } from "../../utils/unzip.js";
|
|
7
|
-
export async function release(bundleUploader, getReleaseHistory, setReleaseHistory, binaryVersion, appVersion, framework, platform, identifier, outputPath, entryFile, jsBundleName, mandatory, enable, rollout, skipBundle, skipCleanup, bundleDirectory, hashCalc) {
|
|
7
|
+
export async function release(bundleUploader, getReleaseHistory, setReleaseHistory, binaryVersion, appVersion, framework, platform, identifier, outputPath, entryFile, jsBundleName, mandatory, enable, rollout, skipBundle, skipCleanup, bundleDirectory, outputMetroDir, hashCalc) {
|
|
8
8
|
const bundleFileName = skipBundle
|
|
9
9
|
? readBundleFileNameFrom(bundleDirectory)
|
|
10
|
-
: await bundleCodePush(framework, platform, outputPath, entryFile, jsBundleName, bundleDirectory);
|
|
10
|
+
: await bundleCodePush(framework, platform, outputPath, entryFile, jsBundleName, bundleDirectory, outputMetroDir);
|
|
11
11
|
const bundleFilePath = `${bundleDirectory}/${bundleFileName}`;
|
|
12
12
|
const packageHash = await (() => {
|
|
13
13
|
if (skipBundle && hashCalc) {
|
|
@@ -48,12 +48,9 @@ class PackageManifest {
|
|
|
48
48
|
static isIgnored(relativeFilePath) {
|
|
49
49
|
const __MACOSX = '__MACOSX/';
|
|
50
50
|
const DS_STORE = '.DS_Store';
|
|
51
|
-
const CODEPUSH_METADATA = '.codepushrelease';
|
|
52
51
|
return (relativeFilePath.startsWith(__MACOSX) ||
|
|
53
52
|
relativeFilePath === DS_STORE ||
|
|
54
|
-
relativeFilePath.endsWith('/' + DS_STORE)
|
|
55
|
-
relativeFilePath === CODEPUSH_METADATA ||
|
|
56
|
-
relativeFilePath.endsWith('/' + CODEPUSH_METADATA));
|
|
53
|
+
relativeFilePath.endsWith('/' + DS_STORE));
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
export async function generatePackageHashFromDirectory(directoryPath, basePath) {
|
package/cli/utils/hash-utils.ts
CHANGED
|
@@ -61,13 +61,10 @@ class PackageManifest {
|
|
|
61
61
|
static isIgnored(relativeFilePath: string): boolean {
|
|
62
62
|
const __MACOSX = '__MACOSX/';
|
|
63
63
|
const DS_STORE = '.DS_Store';
|
|
64
|
-
const CODEPUSH_METADATA = '.codepushrelease';
|
|
65
64
|
return (
|
|
66
65
|
relativeFilePath.startsWith(__MACOSX) ||
|
|
67
66
|
relativeFilePath === DS_STORE ||
|
|
68
|
-
relativeFilePath.endsWith('/' + DS_STORE)
|
|
69
|
-
relativeFilePath === CODEPUSH_METADATA ||
|
|
70
|
-
relativeFilePath.endsWith('/' + CODEPUSH_METADATA)
|
|
67
|
+
relativeFilePath.endsWith('/' + DS_STORE)
|
|
71
68
|
);
|
|
72
69
|
}
|
|
73
70
|
}
|