@revopush/code-push-cli 0.0.7 → 0.0.8-rc.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/bin/script/command-executor.js +76 -46
- package/bin/script/command-parser.js +16 -1
- package/bin/script/management-sdk.js +5 -3
- package/bin/script/react-native-utils.js +21 -2
- package/bin/script/utils/file-utils.js +27 -2
- package/bin/test/management-sdk.js +7 -0
- package/package.json +4 -2
- package/script/command-executor.ts +94 -58
- package/script/command-parser.ts +17 -3
- package/script/management-sdk.ts +8 -3
- package/script/react-native-utils.ts +32 -3
- package/script/types/cli.ts +3 -0
- package/script/types/rest-definitions.ts +12 -0
- package/script/types.ts +1 -0
- package/script/utils/file-utils.ts +30 -1
- package/test/management-sdk.ts +9 -0
|
@@ -236,11 +236,12 @@ function deploymentHistoryClear(command) {
|
|
|
236
236
|
const deploymentList = (command, showPackage = true) => {
|
|
237
237
|
throwForInvalidOutputFormat(command.format);
|
|
238
238
|
let deployments;
|
|
239
|
+
const DEPLOYMENTS_MAX_LENGTH = 10; // do not take metrics if number of deployment higher than this
|
|
239
240
|
return exports.sdk
|
|
240
241
|
.getDeployments(command.appName)
|
|
241
242
|
.then((retrievedDeployments) => {
|
|
242
243
|
deployments = retrievedDeployments;
|
|
243
|
-
if (showPackage) {
|
|
244
|
+
if (showPackage && deployments.length < DEPLOYMENTS_MAX_LENGTH) {
|
|
244
245
|
const metricsPromises = deployments.map((deployment) => {
|
|
245
246
|
if (deployment.package) {
|
|
246
247
|
return exports.sdk.getDeploymentMetrics(command.appName, deployment.name).then((metrics) => {
|
|
@@ -269,7 +270,10 @@ const deploymentList = (command, showPackage = true) => {
|
|
|
269
270
|
};
|
|
270
271
|
exports.deploymentList = deploymentList;
|
|
271
272
|
function deploymentRemove(command) {
|
|
272
|
-
|
|
273
|
+
const confirmation = command.isForce
|
|
274
|
+
? Q.resolve(true)
|
|
275
|
+
: (0, exports.confirm)("Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable.");
|
|
276
|
+
return confirmation.then((wasConfirmed) => {
|
|
273
277
|
if (wasConfirmed) {
|
|
274
278
|
return exports.sdk.removeDeployment(command.appName, command.deploymentName).then(() => {
|
|
275
279
|
(0, exports.log)('Successfully removed the "' + command.deploymentName + '" deployment from the "' + command.appName + '" app.');
|
|
@@ -969,49 +973,16 @@ function patch(command) {
|
|
|
969
973
|
throw new Error("At least one property must be specified to patch a release.");
|
|
970
974
|
}
|
|
971
975
|
const release = (command) => {
|
|
972
|
-
|
|
973
|
-
throw new Error("It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle).");
|
|
974
|
-
}
|
|
975
|
-
throwForInvalidSemverRange(command.appStoreVersion);
|
|
976
|
-
const filePath = command.package;
|
|
977
|
-
let isSingleFilePackage = true;
|
|
978
|
-
if (fs.lstatSync(filePath).isDirectory()) {
|
|
979
|
-
isSingleFilePackage = false;
|
|
980
|
-
}
|
|
981
|
-
let lastTotalProgress = 0;
|
|
982
|
-
const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
|
|
983
|
-
complete: "=",
|
|
984
|
-
incomplete: " ",
|
|
985
|
-
width: 50,
|
|
986
|
-
total: 100,
|
|
987
|
-
});
|
|
988
|
-
const uploadProgress = (currentProgress) => {
|
|
989
|
-
progressBar.tick(currentProgress - lastTotalProgress);
|
|
990
|
-
lastTotalProgress = currentProgress;
|
|
991
|
-
};
|
|
976
|
+
// for initial release we explicitly define release as optional, disabled, without rollout, with a special description
|
|
992
977
|
const updateMetadata = {
|
|
993
|
-
description: command.description,
|
|
994
|
-
isDisabled: command.disabled,
|
|
995
|
-
isMandatory: command.mandatory,
|
|
996
|
-
|
|
978
|
+
description: command.initial ? `Zero release for v${command.appStoreVersion}` : command.description,
|
|
979
|
+
isDisabled: command.initial ? true : command.disabled,
|
|
980
|
+
isMandatory: command.initial ? false : command.mandatory,
|
|
981
|
+
isInitial: command.initial,
|
|
982
|
+
rollout: command.initial ? undefined : command.rollout,
|
|
983
|
+
appVersion: command.appStoreVersion,
|
|
997
984
|
};
|
|
998
|
-
return
|
|
999
|
-
.isAuthenticated(true)
|
|
1000
|
-
.then((isAuth) => {
|
|
1001
|
-
return exports.sdk.release(command.appName, command.deploymentName, filePath, command.appStoreVersion, updateMetadata, uploadProgress);
|
|
1002
|
-
})
|
|
1003
|
-
.then(() => {
|
|
1004
|
-
(0, exports.log)('Successfully released an update containing the "' +
|
|
1005
|
-
command.package +
|
|
1006
|
-
'" ' +
|
|
1007
|
-
(isSingleFilePackage ? "file" : "directory") +
|
|
1008
|
-
' to the "' +
|
|
1009
|
-
command.deploymentName +
|
|
1010
|
-
'" deployment of the "' +
|
|
1011
|
-
command.appName +
|
|
1012
|
-
'" app.');
|
|
1013
|
-
})
|
|
1014
|
-
.catch((err) => releaseErrorHandler(err, command));
|
|
985
|
+
return doRelease(command, updateMetadata);
|
|
1015
986
|
};
|
|
1016
987
|
exports.release = release;
|
|
1017
988
|
const releaseReact = (command) => {
|
|
@@ -1019,6 +990,7 @@ const releaseReact = (command) => {
|
|
|
1019
990
|
let entryFile = command.entryFile;
|
|
1020
991
|
const outputFolder = command.outputDir || path.join(os.tmpdir(), "CodePush");
|
|
1021
992
|
const sourcemapOutputFolder = command.sourcemapOutput || path.join(os.tmpdir(), "CodePushSourceMap");
|
|
993
|
+
const baseReleaseTmpFolder = path.join(os.tmpdir(), "CodePushBaseRelease");
|
|
1022
994
|
const platform = (command.platform = command.platform.toLowerCase());
|
|
1023
995
|
const releaseCommand = command;
|
|
1024
996
|
// Check for app and deployment exist before releasing an update.
|
|
@@ -1026,7 +998,6 @@ const releaseReact = (command) => {
|
|
|
1026
998
|
return (exports.sdk
|
|
1027
999
|
.getDeployment(command.appName, command.deploymentName)
|
|
1028
1000
|
.then(async () => {
|
|
1029
|
-
releaseCommand.package = outputFolder;
|
|
1030
1001
|
switch (platform) {
|
|
1031
1002
|
case "android":
|
|
1032
1003
|
case "ios":
|
|
@@ -1038,6 +1009,9 @@ const releaseReact = (command) => {
|
|
|
1038
1009
|
default:
|
|
1039
1010
|
throw new Error('Platform must be either "android", "ios" or "windows".');
|
|
1040
1011
|
}
|
|
1012
|
+
releaseCommand.package = outputFolder;
|
|
1013
|
+
releaseCommand.outputDir = outputFolder;
|
|
1014
|
+
releaseCommand.bundleName = bundleName;
|
|
1041
1015
|
let projectName;
|
|
1042
1016
|
try {
|
|
1043
1017
|
const projectPackageJson = require(path.join(process.cwd(), "package.json"));
|
|
@@ -1089,8 +1063,10 @@ const releaseReact = (command) => {
|
|
|
1089
1063
|
.then(async () => {
|
|
1090
1064
|
const isHermes = await (0, react_native_utils_1.isHermesEnabled)(command, platform);
|
|
1091
1065
|
if (isHermes) {
|
|
1066
|
+
await (0, exports.createEmptyTempReleaseFolder)(baseReleaseTmpFolder);
|
|
1067
|
+
const baseBytecode = await (0, react_native_utils_1.takeHermesBaseBytecode)(command, baseReleaseTmpFolder, outputFolder, bundleName);
|
|
1092
1068
|
(0, exports.log)(chalk.cyan("\nRunning hermes compiler...\n"));
|
|
1093
|
-
await (0, react_native_utils_1.runHermesEmitBinaryCommand)(command, bundleName, outputFolder, sourcemapOutputFolder, command.extraHermesFlags, command.gradleFile);
|
|
1069
|
+
await (0, react_native_utils_1.runHermesEmitBinaryCommand)(command, bundleName, outputFolder, sourcemapOutputFolder, command.extraHermesFlags, command.gradleFile, baseBytecode);
|
|
1094
1070
|
}
|
|
1095
1071
|
})
|
|
1096
1072
|
.then(async () => {
|
|
@@ -1104,7 +1080,7 @@ const releaseReact = (command) => {
|
|
|
1104
1080
|
})
|
|
1105
1081
|
.then(() => {
|
|
1106
1082
|
(0, exports.log)(chalk.cyan("\nReleasing update contents to CodePush:\n"));
|
|
1107
|
-
return (
|
|
1083
|
+
return releaseReactNative(releaseCommand);
|
|
1108
1084
|
})
|
|
1109
1085
|
.then(async () => {
|
|
1110
1086
|
if (!command.outputDir) {
|
|
@@ -1113,12 +1089,66 @@ const releaseReact = (command) => {
|
|
|
1113
1089
|
if (!command.sourcemapOutput) {
|
|
1114
1090
|
await deleteFolder(sourcemapOutputFolder);
|
|
1115
1091
|
}
|
|
1092
|
+
await deleteFolder(baseReleaseTmpFolder);
|
|
1116
1093
|
})
|
|
1117
1094
|
.catch(async (err) => {
|
|
1118
1095
|
throw err;
|
|
1119
1096
|
}));
|
|
1120
1097
|
};
|
|
1121
1098
|
exports.releaseReact = releaseReact;
|
|
1099
|
+
const releaseReactNative = (command) => {
|
|
1100
|
+
// for initial release we explicitly define release as optional, disabled, without rollout, with a special description
|
|
1101
|
+
const updateMetadata = {
|
|
1102
|
+
description: command.initial ? `Zero release for v${command.appStoreVersion}` : command.description,
|
|
1103
|
+
isDisabled: command.initial ? true : command.disabled,
|
|
1104
|
+
isMandatory: command.initial ? false : command.mandatory,
|
|
1105
|
+
isInitial: command.initial,
|
|
1106
|
+
bundleName: command.bundleName,
|
|
1107
|
+
outputDir: command.outputDir,
|
|
1108
|
+
rollout: command.initial ? undefined : command.rollout,
|
|
1109
|
+
appVersion: command.appStoreVersion,
|
|
1110
|
+
};
|
|
1111
|
+
return doRelease(command, updateMetadata);
|
|
1112
|
+
};
|
|
1113
|
+
const doRelease = (command, updateMetadata) => {
|
|
1114
|
+
if ((0, file_utils_1.isBinaryOrZip)(command.package)) {
|
|
1115
|
+
throw new Error("It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle).");
|
|
1116
|
+
}
|
|
1117
|
+
throwForInvalidSemverRange(command.appStoreVersion);
|
|
1118
|
+
const filePath = command.package;
|
|
1119
|
+
let isSingleFilePackage = true;
|
|
1120
|
+
if (fs.lstatSync(filePath).isDirectory()) {
|
|
1121
|
+
isSingleFilePackage = false;
|
|
1122
|
+
}
|
|
1123
|
+
let lastTotalProgress = 0;
|
|
1124
|
+
const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
|
|
1125
|
+
complete: "=",
|
|
1126
|
+
incomplete: " ",
|
|
1127
|
+
width: 50,
|
|
1128
|
+
total: 100,
|
|
1129
|
+
});
|
|
1130
|
+
const uploadProgress = (currentProgress) => {
|
|
1131
|
+
progressBar.tick(currentProgress - lastTotalProgress);
|
|
1132
|
+
lastTotalProgress = currentProgress;
|
|
1133
|
+
};
|
|
1134
|
+
return exports.sdk
|
|
1135
|
+
.isAuthenticated(true)
|
|
1136
|
+
.then((isAuth) => {
|
|
1137
|
+
return exports.sdk.release(command.appName, command.deploymentName, filePath, updateMetadata, uploadProgress);
|
|
1138
|
+
})
|
|
1139
|
+
.then(() => {
|
|
1140
|
+
(0, exports.log)('Successfully released an update containing the "' +
|
|
1141
|
+
command.package +
|
|
1142
|
+
'" ' +
|
|
1143
|
+
(isSingleFilePackage ? "file" : "directory") +
|
|
1144
|
+
' to the "' +
|
|
1145
|
+
command.deploymentName +
|
|
1146
|
+
'" deployment of the "' +
|
|
1147
|
+
command.appName +
|
|
1148
|
+
'" app.');
|
|
1149
|
+
})
|
|
1150
|
+
.catch((err) => releaseErrorHandler(err, command));
|
|
1151
|
+
};
|
|
1122
1152
|
function rollback(command) {
|
|
1123
1153
|
return (0, exports.confirm)().then((wasConfirmed) => {
|
|
1124
1154
|
if (!wasConfirmed) {
|
|
@@ -203,7 +203,13 @@ function deploymentRemove(commandName, yargs) {
|
|
|
203
203
|
yargs
|
|
204
204
|
.usage(USAGE_PREFIX + " deployment " + commandName + " <appName> <deploymentName>")
|
|
205
205
|
.demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments
|
|
206
|
-
.example("deployment " + commandName + " MyApp MyDeployment", 'Removes deployment "MyDeployment" from app "MyApp"')
|
|
206
|
+
.example("deployment " + commandName + " MyApp MyDeployment", 'Removes deployment "MyDeployment" from app "MyApp"')
|
|
207
|
+
.option("force", {
|
|
208
|
+
default: false,
|
|
209
|
+
demand: false,
|
|
210
|
+
description: "Bypass confirmation when removing deployments",
|
|
211
|
+
type: "boolean",
|
|
212
|
+
});
|
|
207
213
|
addCommonConfiguration(yargs);
|
|
208
214
|
}
|
|
209
215
|
function deploymentHistory(commandName, yargs) {
|
|
@@ -616,6 +622,13 @@ yargs
|
|
|
616
622
|
default: null,
|
|
617
623
|
demand: false,
|
|
618
624
|
description: "Path to the gradle file which specifies the binary version you want to target this release at (android only).",
|
|
625
|
+
})
|
|
626
|
+
.option("initial", {
|
|
627
|
+
alias: "i",
|
|
628
|
+
default: false,
|
|
629
|
+
demand: false,
|
|
630
|
+
description: "Specifies whether release is initial (base) for given targetBinaryVersion.",
|
|
631
|
+
type: "boolean",
|
|
619
632
|
})
|
|
620
633
|
.option("mandatory", {
|
|
621
634
|
alias: "m",
|
|
@@ -933,6 +946,7 @@ function createCommand() {
|
|
|
933
946
|
const deploymentRemoveCommand = cmd;
|
|
934
947
|
deploymentRemoveCommand.appName = arg2;
|
|
935
948
|
deploymentRemoveCommand.deploymentName = arg3;
|
|
949
|
+
deploymentRemoveCommand.isForce = argv["force"];
|
|
936
950
|
}
|
|
937
951
|
break;
|
|
938
952
|
case "rename":
|
|
@@ -1039,6 +1053,7 @@ function createCommand() {
|
|
|
1039
1053
|
releaseReactCommand.entryFile = argv["entryFile"];
|
|
1040
1054
|
releaseReactCommand.gradleFile = argv["gradleFile"];
|
|
1041
1055
|
releaseReactCommand.mandatory = argv["mandatory"];
|
|
1056
|
+
releaseReactCommand.initial = argv["initial"];
|
|
1042
1057
|
releaseReactCommand.noDuplicateReleaseError = argv["noDuplicateReleaseError"];
|
|
1043
1058
|
releaseReactCommand.plistFile = argv["plistFile"];
|
|
1044
1059
|
releaseReactCommand.plistFilePrefix = argv["plistFilePrefix"];
|
|
@@ -195,6 +195,9 @@ class AccountManager {
|
|
|
195
195
|
getDeployment(appName, deploymentName) {
|
|
196
196
|
return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}`])).then((res) => res.body.deployment);
|
|
197
197
|
}
|
|
198
|
+
getBaseRelease(appName, deploymentName, appVerison) {
|
|
199
|
+
return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}/basebundle?appVersion=${appVerison}`])).then((res) => res.body.basebundle);
|
|
200
|
+
}
|
|
198
201
|
renameDeployment(appName, oldDeploymentName, newDeploymentName) {
|
|
199
202
|
return this.patch(urlEncode([`/apps/${appName}/deployments/${oldDeploymentName}`]), JSON.stringify({ name: newDeploymentName })).then(() => null);
|
|
200
203
|
}
|
|
@@ -207,9 +210,8 @@ class AccountManager {
|
|
|
207
210
|
getDeploymentHistory(appName, deploymentName) {
|
|
208
211
|
return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}/history`])).then((res) => res.body.history);
|
|
209
212
|
}
|
|
210
|
-
release(appName, deploymentName, filePath,
|
|
213
|
+
release(appName, deploymentName, filePath, updateMetadata, uploadProgressCallback) {
|
|
211
214
|
return Promise((resolve, reject, notify) => {
|
|
212
|
-
updateMetadata.appVersion = targetBinaryVersion;
|
|
213
215
|
const request = superagent.post(this._serverUrl + urlEncode([`/apps/${appName}/deployments/${deploymentName}/release`]));
|
|
214
216
|
this.attachCredentials(request);
|
|
215
217
|
const getPackageFilePromise = Q.Promise((resolve, reject) => {
|
|
@@ -414,7 +416,7 @@ class AccountManager {
|
|
|
414
416
|
}
|
|
415
417
|
request.set("Accept", `application/vnd.code-push.v${AccountManager.API_VERSION}+json`);
|
|
416
418
|
request.set("Authorization", `Bearer ${this._accessKey}`);
|
|
417
|
-
request.set("X-CodePush-SDK-Version", packageJson.version);
|
|
419
|
+
request.set("X-CodePush-SDK-Version", packageJson.version); // TODO get version differently without require("../../package.json");
|
|
418
420
|
}
|
|
419
421
|
}
|
|
420
422
|
module.exports = AccountManager;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getReactNativeVersion = exports.directoryExistsSync = exports.getReactNativePackagePath = exports.isHermesEnabled = exports.getMinifyParams = exports.getXcodeDotEnvValue = exports.runHermesEmitBinaryCommand = exports.getBundleSourceMapOutput = exports.isValidVersion = void 0;
|
|
3
|
+
exports.getReactNativeVersion = exports.directoryExistsSync = exports.getReactNativePackagePath = exports.isHermesEnabled = exports.getMinifyParams = exports.getXcodeDotEnvValue = exports.runHermesEmitBinaryCommand = exports.takeHermesBaseBytecode = exports.getBundleSourceMapOutput = exports.isValidVersion = void 0;
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const chalk = require("chalk");
|
|
6
6
|
const path = require("path");
|
|
@@ -8,6 +8,7 @@ const childProcess = require("child_process");
|
|
|
8
8
|
const semver_1 = require("semver");
|
|
9
9
|
const file_utils_1 = require("./utils/file-utils");
|
|
10
10
|
const dotenv = require("dotenv");
|
|
11
|
+
const command_executor_1 = require("./command-executor");
|
|
11
12
|
const g2js = require("gradle-to-js/lib/parser");
|
|
12
13
|
function isValidVersion(version) {
|
|
13
14
|
return !!(0, semver_1.valid)(version) || /^\d+\.\d+$/.test(version);
|
|
@@ -44,7 +45,22 @@ async function getBundleSourceMapOutput(command, bundleName, sourcemapOutputFold
|
|
|
44
45
|
return bundleSourceMapOutput;
|
|
45
46
|
}
|
|
46
47
|
exports.getBundleSourceMapOutput = getBundleSourceMapOutput;
|
|
47
|
-
async function
|
|
48
|
+
async function takeHermesBaseBytecode(command, baseReleaseTmpFolder, outputFolder, bundleName) {
|
|
49
|
+
const { bundleBlobUrl } = await command_executor_1.sdk.getBaseRelease(command.appName, command.deploymentName, command.appStoreVersion);
|
|
50
|
+
if (!bundleBlobUrl) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const baseReleaseArchive = await (0, file_utils_1.downloadBlob)(bundleBlobUrl, baseReleaseTmpFolder);
|
|
54
|
+
await (0, file_utils_1.extract)(baseReleaseArchive, baseReleaseTmpFolder);
|
|
55
|
+
const baseReleaseBundle = path.join(baseReleaseTmpFolder, path.basename(outputFolder), bundleName);
|
|
56
|
+
if (!fs.existsSync(baseReleaseBundle)) {
|
|
57
|
+
(0, command_executor_1.log)(chalk.cyan("\nNo base release available...\n"));
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return baseReleaseBundle;
|
|
61
|
+
}
|
|
62
|
+
exports.takeHermesBaseBytecode = takeHermesBaseBytecode;
|
|
63
|
+
async function runHermesEmitBinaryCommand(command, bundleName, outputFolder, sourcemapOutputFolder, extraHermesFlags, gradleFile, baseBytecode) {
|
|
48
64
|
const hermesArgs = [];
|
|
49
65
|
const envNodeArgs = process.env.CODE_PUSH_NODE_ARGS;
|
|
50
66
|
if (typeof envNodeArgs !== "undefined") {
|
|
@@ -61,6 +77,9 @@ async function runHermesEmitBinaryCommand(command, bundleName, outputFolder, sou
|
|
|
61
77
|
if (sourcemapOutputFolder) {
|
|
62
78
|
hermesArgs.push("-output-source-map");
|
|
63
79
|
}
|
|
80
|
+
if (baseBytecode) {
|
|
81
|
+
hermesArgs.push("-base-bytecode", baseBytecode);
|
|
82
|
+
}
|
|
64
83
|
console.log(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n"));
|
|
65
84
|
const hermesCommand = await getHermesCommand(gradleFile);
|
|
66
85
|
const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizePath = exports.fileDoesNotExistOrIsDirectory = exports.copyFileToTmpDir = exports.fileExists = exports.isDirectory = exports.isBinaryOrZip = void 0;
|
|
3
|
+
exports.extract = exports.downloadBlob = exports.normalizePath = exports.fileDoesNotExistOrIsDirectory = exports.copyFileToTmpDir = exports.fileExists = exports.isDirectory = exports.isBinaryOrZip = void 0;
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const rimraf = require("rimraf");
|
|
7
7
|
const temp = require("temp");
|
|
8
|
+
const unzipper = require("unzipper");
|
|
9
|
+
const superagent = require("superagent");
|
|
8
10
|
function isBinaryOrZip(path) {
|
|
9
11
|
return path.search(/\.zip$/i) !== -1 || path.search(/\.apk$/i) !== -1 || path.search(/\.ipa$/i) !== -1;
|
|
10
12
|
}
|
|
@@ -22,7 +24,6 @@ function fileExists(file) {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
exports.fileExists = fileExists;
|
|
25
|
-
;
|
|
26
27
|
function copyFileToTmpDir(filePath) {
|
|
27
28
|
if (!isDirectory(filePath)) {
|
|
28
29
|
const outputFolderPath = temp.mkdirSync("code-push");
|
|
@@ -48,3 +49,27 @@ function normalizePath(filePath) {
|
|
|
48
49
|
return filePath.replace(/\\/g, "/");
|
|
49
50
|
}
|
|
50
51
|
exports.normalizePath = normalizePath;
|
|
52
|
+
async function downloadBlob(url, folder, filename = "blob.zip") {
|
|
53
|
+
const destination = path.join(folder, filename);
|
|
54
|
+
const writeStream = fs.createWriteStream(destination);
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
writeStream.on("finish", () => resolve(destination));
|
|
57
|
+
writeStream.on("error", reject);
|
|
58
|
+
superagent
|
|
59
|
+
.get(url)
|
|
60
|
+
.ok((res) => res.status < 400)
|
|
61
|
+
.on("error", (err) => {
|
|
62
|
+
writeStream.destroy();
|
|
63
|
+
reject(err);
|
|
64
|
+
})
|
|
65
|
+
.pipe(writeStream);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
exports.downloadBlob = downloadBlob;
|
|
69
|
+
async function extract(zipPath, extractTo) {
|
|
70
|
+
const extractStream = unzipper.Extract({ path: extractTo });
|
|
71
|
+
await new Promise((resolve, reject) => {
|
|
72
|
+
fs.createReadStream(zipPath).pipe(extractStream).on("close", resolve).on("error", reject);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
exports.extract = extract;
|
|
@@ -154,6 +154,13 @@ describe("Management SDK", () => {
|
|
|
154
154
|
done();
|
|
155
155
|
}, rejectHandler);
|
|
156
156
|
});
|
|
157
|
+
it("getBaseBundle handles JSON response", (done) => {
|
|
158
|
+
mockReturn(JSON.stringify({ basebundle: { bundleBlobUrl: "https://test.test/release.zip" } }), 200, {});
|
|
159
|
+
manager.getBaseRelease("appName", "deploymentName", "1.2.3").done((obj) => {
|
|
160
|
+
assert.ok(obj);
|
|
161
|
+
done();
|
|
162
|
+
}, rejectHandler);
|
|
163
|
+
});
|
|
157
164
|
it("getDeployments handles JSON response", (done) => {
|
|
158
165
|
mockReturn(JSON.stringify({ deployments: [] }), 200, {});
|
|
159
166
|
manager.getDeployments("appName").done((obj) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revopush/code-push-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8-rc.0",
|
|
4
4
|
"description": "Management CLI for the CodePush service",
|
|
5
5
|
"main": "./script/cli.js",
|
|
6
6
|
"scripts": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"slash": "1.0.0",
|
|
46
46
|
"superagent": "^8.0.9",
|
|
47
47
|
"temp": "^0.9.4",
|
|
48
|
+
"unzipper": "^0.12.3",
|
|
48
49
|
"which": "^1.2.7",
|
|
49
50
|
"wordwrap": "1.0.0",
|
|
50
51
|
"xcode": "^3.0.1",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"@types/node": "^20.3.1",
|
|
60
61
|
"@types/q": "^1.5.8",
|
|
61
62
|
"@types/sinon": "^10.0.15",
|
|
63
|
+
"@types/unzipper": "^0.10.11",
|
|
62
64
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
63
65
|
"@typescript-eslint/parser": "^6.0.0",
|
|
64
66
|
"eslint": "^8.45.0",
|
|
@@ -68,8 +70,8 @@
|
|
|
68
70
|
"prettier": "^2.8.8",
|
|
69
71
|
"sinon": "15.1.2",
|
|
70
72
|
"superagent-mock": "^4.0.0",
|
|
71
|
-
"typescript": "^5.1.3",
|
|
72
73
|
"ts-node": "^10.9.2",
|
|
74
|
+
"typescript": "^5.1.3",
|
|
73
75
|
"which": "^3.0.1"
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -34,12 +34,14 @@ import {
|
|
|
34
34
|
isHermesEnabled,
|
|
35
35
|
isValidVersion,
|
|
36
36
|
runHermesEmitBinaryCommand,
|
|
37
|
+
takeHermesBaseBytecode,
|
|
37
38
|
} from "./react-native-utils";
|
|
38
39
|
import { fileDoesNotExistOrIsDirectory, fileExists, isBinaryOrZip } from "./utils/file-utils";
|
|
39
40
|
|
|
40
41
|
import AccountManager = require("./management-sdk");
|
|
41
42
|
import wordwrap = require("wordwrap");
|
|
42
43
|
import Promise = Q.Promise;
|
|
44
|
+
import { ReactNativePackageInfo } from "./types/rest-definitions";
|
|
43
45
|
|
|
44
46
|
const g2js = require("gradle-to-js/lib/parser");
|
|
45
47
|
|
|
@@ -364,9 +366,11 @@ export const deploymentList = (command: cli.IDeploymentListCommand, showPackage:
|
|
|
364
366
|
};
|
|
365
367
|
|
|
366
368
|
function deploymentRemove(command: cli.IDeploymentRemoveCommand): Promise<void> {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
369
|
+
const confirmation = command.isForce
|
|
370
|
+
? Q.resolve(true)
|
|
371
|
+
: confirm("Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable.");
|
|
372
|
+
|
|
373
|
+
return confirmation.then((wasConfirmed: boolean): Promise<void> => {
|
|
370
374
|
if (wasConfirmed) {
|
|
371
375
|
return sdk.removeDeployment(command.appName, command.deploymentName).then((): void => {
|
|
372
376
|
log('Successfully removed the "' + command.deploymentName + '" deployment from the "' + command.appName + '" app.');
|
|
@@ -1206,59 +1210,17 @@ function patch(command: cli.IPatchCommand): Promise<void> {
|
|
|
1206
1210
|
}
|
|
1207
1211
|
|
|
1208
1212
|
export const release = (command: cli.IReleaseCommand): Promise<void> => {
|
|
1209
|
-
|
|
1210
|
-
throw new Error(
|
|
1211
|
-
"It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle)."
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
throwForInvalidSemverRange(command.appStoreVersion);
|
|
1216
|
-
const filePath: string = command.package;
|
|
1217
|
-
let isSingleFilePackage: boolean = true;
|
|
1218
|
-
|
|
1219
|
-
if (fs.lstatSync(filePath).isDirectory()) {
|
|
1220
|
-
isSingleFilePackage = false;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
let lastTotalProgress = 0;
|
|
1224
|
-
const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
|
|
1225
|
-
complete: "=",
|
|
1226
|
-
incomplete: " ",
|
|
1227
|
-
width: 50,
|
|
1228
|
-
total: 100,
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
const uploadProgress = (currentProgress: number): void => {
|
|
1232
|
-
progressBar.tick(currentProgress - lastTotalProgress);
|
|
1233
|
-
lastTotalProgress = currentProgress;
|
|
1234
|
-
};
|
|
1235
|
-
|
|
1213
|
+
// for initial release we explicitly define release as optional, disabled, without rollout, with a special description
|
|
1236
1214
|
const updateMetadata: PackageInfo = {
|
|
1237
|
-
description: command.description,
|
|
1238
|
-
isDisabled: command.disabled,
|
|
1239
|
-
isMandatory: command.mandatory,
|
|
1240
|
-
|
|
1215
|
+
description: command.initial ? `Zero release for v${command.appStoreVersion}` : command.description,
|
|
1216
|
+
isDisabled: command.initial ? true : command.disabled,
|
|
1217
|
+
isMandatory: command.initial ? false : command.mandatory,
|
|
1218
|
+
isInitial: command.initial,
|
|
1219
|
+
rollout: command.initial ? undefined : command.rollout,
|
|
1220
|
+
appVersion: command.appStoreVersion,
|
|
1241
1221
|
};
|
|
1242
1222
|
|
|
1243
|
-
return
|
|
1244
|
-
.isAuthenticated(true)
|
|
1245
|
-
.then((isAuth: boolean): Promise<void> => {
|
|
1246
|
-
return sdk.release(command.appName, command.deploymentName, filePath, command.appStoreVersion, updateMetadata, uploadProgress);
|
|
1247
|
-
})
|
|
1248
|
-
.then((): void => {
|
|
1249
|
-
log(
|
|
1250
|
-
'Successfully released an update containing the "' +
|
|
1251
|
-
command.package +
|
|
1252
|
-
'" ' +
|
|
1253
|
-
(isSingleFilePackage ? "file" : "directory") +
|
|
1254
|
-
' to the "' +
|
|
1255
|
-
command.deploymentName +
|
|
1256
|
-
'" deployment of the "' +
|
|
1257
|
-
command.appName +
|
|
1258
|
-
'" app.'
|
|
1259
|
-
);
|
|
1260
|
-
})
|
|
1261
|
-
.catch((err: CodePushError) => releaseErrorHandler(err, command));
|
|
1223
|
+
return doRelease(command, updateMetadata);
|
|
1262
1224
|
};
|
|
1263
1225
|
|
|
1264
1226
|
export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> => {
|
|
@@ -1266,16 +1228,15 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1266
1228
|
let entryFile: string = command.entryFile;
|
|
1267
1229
|
const outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush");
|
|
1268
1230
|
const sourcemapOutputFolder: string = command.sourcemapOutput || path.join(os.tmpdir(), "CodePushSourceMap");
|
|
1231
|
+
const baseReleaseTmpFolder: string = path.join(os.tmpdir(), "CodePushBaseRelease");
|
|
1269
1232
|
const platform: string = (command.platform = command.platform.toLowerCase());
|
|
1270
|
-
const releaseCommand: cli.
|
|
1233
|
+
const releaseCommand: cli.IReleaseReactCommand = <any>command;
|
|
1271
1234
|
// Check for app and deployment exist before releasing an update.
|
|
1272
1235
|
// This validation helps to save about 1 minute or more in case user has typed wrong app or deployment name.
|
|
1273
1236
|
return (
|
|
1274
1237
|
sdk
|
|
1275
1238
|
.getDeployment(command.appName, command.deploymentName)
|
|
1276
1239
|
.then(async () => {
|
|
1277
|
-
releaseCommand.package = outputFolder;
|
|
1278
|
-
|
|
1279
1240
|
switch (platform) {
|
|
1280
1241
|
case "android":
|
|
1281
1242
|
case "ios":
|
|
@@ -1289,6 +1250,10 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1289
1250
|
throw new Error('Platform must be either "android", "ios" or "windows".');
|
|
1290
1251
|
}
|
|
1291
1252
|
|
|
1253
|
+
releaseCommand.package = outputFolder;
|
|
1254
|
+
releaseCommand.outputDir = outputFolder;
|
|
1255
|
+
releaseCommand.bundleName = bundleName;
|
|
1256
|
+
|
|
1292
1257
|
let projectName: string;
|
|
1293
1258
|
|
|
1294
1259
|
try {
|
|
@@ -1358,6 +1323,9 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1358
1323
|
const isHermes = await isHermesEnabled(command, platform);
|
|
1359
1324
|
|
|
1360
1325
|
if (isHermes) {
|
|
1326
|
+
await createEmptyTempReleaseFolder(baseReleaseTmpFolder);
|
|
1327
|
+
const baseBytecode = await takeHermesBaseBytecode(command, baseReleaseTmpFolder, outputFolder, bundleName);
|
|
1328
|
+
|
|
1361
1329
|
log(chalk.cyan("\nRunning hermes compiler...\n"));
|
|
1362
1330
|
await runHermesEmitBinaryCommand(
|
|
1363
1331
|
command,
|
|
@@ -1365,7 +1333,8 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1365
1333
|
outputFolder,
|
|
1366
1334
|
sourcemapOutputFolder,
|
|
1367
1335
|
command.extraHermesFlags,
|
|
1368
|
-
command.gradleFile
|
|
1336
|
+
command.gradleFile,
|
|
1337
|
+
baseBytecode
|
|
1369
1338
|
);
|
|
1370
1339
|
}
|
|
1371
1340
|
})
|
|
@@ -1379,7 +1348,7 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1379
1348
|
})
|
|
1380
1349
|
.then(() => {
|
|
1381
1350
|
log(chalk.cyan("\nReleasing update contents to CodePush:\n"));
|
|
1382
|
-
return
|
|
1351
|
+
return releaseReactNative(releaseCommand);
|
|
1383
1352
|
})
|
|
1384
1353
|
.then(async () => {
|
|
1385
1354
|
if (!command.outputDir) {
|
|
@@ -1389,6 +1358,8 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1389
1358
|
if (!command.sourcemapOutput) {
|
|
1390
1359
|
await deleteFolder(sourcemapOutputFolder);
|
|
1391
1360
|
}
|
|
1361
|
+
|
|
1362
|
+
await deleteFolder(baseReleaseTmpFolder);
|
|
1392
1363
|
})
|
|
1393
1364
|
.catch(async (err: Error) => {
|
|
1394
1365
|
throw err;
|
|
@@ -1396,6 +1367,71 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
|
|
|
1396
1367
|
);
|
|
1397
1368
|
};
|
|
1398
1369
|
|
|
1370
|
+
const releaseReactNative = (command: cli.IReleaseReactCommand): Promise<void> => {
|
|
1371
|
+
// for initial release we explicitly define release as optional, disabled, without rollout, with a special description
|
|
1372
|
+
const updateMetadata: ReactNativePackageInfo = {
|
|
1373
|
+
description: command.initial ? `Zero release for v${command.appStoreVersion}` : command.description,
|
|
1374
|
+
isDisabled: command.initial ? true : command.disabled,
|
|
1375
|
+
isMandatory: command.initial ? false : command.mandatory,
|
|
1376
|
+
isInitial: command.initial,
|
|
1377
|
+
bundleName: command.bundleName,
|
|
1378
|
+
outputDir: command.outputDir,
|
|
1379
|
+
rollout: command.initial ? undefined : command.rollout,
|
|
1380
|
+
appVersion: command.appStoreVersion,
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
return doRelease(command, updateMetadata);
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
const doRelease = (command: cli.IReleaseCommand | cli.IReleaseReactCommand, updateMetadata: PackageInfo): Promise<void> => {
|
|
1387
|
+
if (isBinaryOrZip(command.package)) {
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
"It is unnecessary to package releases in a .zip or binary file. Please specify the direct path to the update content's directory (e.g. /platforms/ios/www) or file (e.g. main.jsbundle)."
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
throwForInvalidSemverRange(command.appStoreVersion);
|
|
1394
|
+
const filePath: string = command.package;
|
|
1395
|
+
let isSingleFilePackage: boolean = true;
|
|
1396
|
+
|
|
1397
|
+
if (fs.lstatSync(filePath).isDirectory()) {
|
|
1398
|
+
isSingleFilePackage = false;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
let lastTotalProgress = 0;
|
|
1402
|
+
const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
|
|
1403
|
+
complete: "=",
|
|
1404
|
+
incomplete: " ",
|
|
1405
|
+
width: 50,
|
|
1406
|
+
total: 100,
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
const uploadProgress = (currentProgress: number): void => {
|
|
1410
|
+
progressBar.tick(currentProgress - lastTotalProgress);
|
|
1411
|
+
lastTotalProgress = currentProgress;
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
return sdk
|
|
1415
|
+
.isAuthenticated(true)
|
|
1416
|
+
.then((isAuth: boolean): Promise<void> => {
|
|
1417
|
+
return sdk.release(command.appName, command.deploymentName, filePath, updateMetadata, uploadProgress);
|
|
1418
|
+
})
|
|
1419
|
+
.then((): void => {
|
|
1420
|
+
log(
|
|
1421
|
+
'Successfully released an update containing the "' +
|
|
1422
|
+
command.package +
|
|
1423
|
+
'" ' +
|
|
1424
|
+
(isSingleFilePackage ? "file" : "directory") +
|
|
1425
|
+
' to the "' +
|
|
1426
|
+
command.deploymentName +
|
|
1427
|
+
'" deployment of the "' +
|
|
1428
|
+
command.appName +
|
|
1429
|
+
'" app.'
|
|
1430
|
+
);
|
|
1431
|
+
})
|
|
1432
|
+
.catch((err: CodePushError) => releaseErrorHandler(err, command));
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1399
1435
|
function rollback(command: cli.IRollbackCommand): Promise<void> {
|
|
1400
1436
|
return confirm().then((wasConfirmed: boolean) => {
|
|
1401
1437
|
if (!wasConfirmed) {
|
package/script/command-parser.ts
CHANGED
|
@@ -247,7 +247,13 @@ function deploymentRemove(commandName: string, yargs: yargs.Argv): void {
|
|
|
247
247
|
yargs
|
|
248
248
|
.usage(USAGE_PREFIX + " deployment " + commandName + " <appName> <deploymentName>")
|
|
249
249
|
.demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments
|
|
250
|
-
.example("deployment " + commandName + " MyApp MyDeployment", 'Removes deployment "MyDeployment" from app "MyApp"')
|
|
250
|
+
.example("deployment " + commandName + " MyApp MyDeployment", 'Removes deployment "MyDeployment" from app "MyApp"')
|
|
251
|
+
.option("force", {
|
|
252
|
+
default: false,
|
|
253
|
+
demand: false,
|
|
254
|
+
description: "Bypass confirmation when removing deployments",
|
|
255
|
+
type: "boolean",
|
|
256
|
+
});
|
|
251
257
|
|
|
252
258
|
addCommonConfiguration(yargs);
|
|
253
259
|
}
|
|
@@ -732,6 +738,13 @@ yargs
|
|
|
732
738
|
demand: false,
|
|
733
739
|
description: "Path to the gradle file which specifies the binary version you want to target this release at (android only).",
|
|
734
740
|
})
|
|
741
|
+
.option("initial", {
|
|
742
|
+
alias: "i",
|
|
743
|
+
default: false,
|
|
744
|
+
demand: false,
|
|
745
|
+
description: "Specifies whether release is initial (base) for given targetBinaryVersion.",
|
|
746
|
+
type: "boolean",
|
|
747
|
+
})
|
|
735
748
|
.option("mandatory", {
|
|
736
749
|
alias: "m",
|
|
737
750
|
default: false,
|
|
@@ -844,8 +857,7 @@ yargs
|
|
|
844
857
|
alias: "eo",
|
|
845
858
|
default: [],
|
|
846
859
|
demand: false,
|
|
847
|
-
description:
|
|
848
|
-
"Option that gets passed to react-native bundler. Can be specified multiple times.",
|
|
860
|
+
description: "Option that gets passed to react-native bundler. Can be specified multiple times.",
|
|
849
861
|
type: "array",
|
|
850
862
|
})
|
|
851
863
|
.check((argv: any, aliases: { [aliases: string]: string }): any => {
|
|
@@ -1107,6 +1119,7 @@ export function createCommand(): cli.ICommand {
|
|
|
1107
1119
|
|
|
1108
1120
|
deploymentRemoveCommand.appName = arg2;
|
|
1109
1121
|
deploymentRemoveCommand.deploymentName = arg3;
|
|
1122
|
+
deploymentRemoveCommand.isForce = argv["force"] as any;
|
|
1110
1123
|
}
|
|
1111
1124
|
break;
|
|
1112
1125
|
|
|
@@ -1240,6 +1253,7 @@ export function createCommand(): cli.ICommand {
|
|
|
1240
1253
|
releaseReactCommand.entryFile = argv["entryFile"] as any;
|
|
1241
1254
|
releaseReactCommand.gradleFile = argv["gradleFile"] as any;
|
|
1242
1255
|
releaseReactCommand.mandatory = argv["mandatory"] as any;
|
|
1256
|
+
releaseReactCommand.initial = argv["initial"] as any;
|
|
1243
1257
|
releaseReactCommand.noDuplicateReleaseError = argv["noDuplicateReleaseError"] as any;
|
|
1244
1258
|
releaseReactCommand.plistFile = argv["plistFile"] as any;
|
|
1245
1259
|
releaseReactCommand.plistFilePrefix = argv["plistFilePrefix"] as any;
|
package/script/management-sdk.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
PackageInfo,
|
|
27
27
|
ServerAccessKey,
|
|
28
28
|
Session,
|
|
29
|
+
BaseRelease,
|
|
29
30
|
} from "./types";
|
|
30
31
|
|
|
31
32
|
const packageJson = require("../../package.json");
|
|
@@ -273,6 +274,12 @@ class AccountManager {
|
|
|
273
274
|
return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}`])).then((res: JsonResponse) => res.body.deployment);
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
public getBaseRelease(appName: string, deploymentName: string, appVerison: string): Promise<BaseRelease> {
|
|
278
|
+
return this.get(urlEncode([`/apps/${appName}/deployments/${deploymentName}/basebundle?appVersion=${appVerison}`])).then(
|
|
279
|
+
(res: JsonResponse) => res.body.basebundle
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
276
283
|
public renameDeployment(appName: string, oldDeploymentName: string, newDeploymentName: string): Promise<void> {
|
|
277
284
|
return this.patch(
|
|
278
285
|
urlEncode([`/apps/${appName}/deployments/${oldDeploymentName}`]),
|
|
@@ -300,12 +307,10 @@ class AccountManager {
|
|
|
300
307
|
appName: string,
|
|
301
308
|
deploymentName: string,
|
|
302
309
|
filePath: string,
|
|
303
|
-
targetBinaryVersion: string,
|
|
304
310
|
updateMetadata: PackageInfo,
|
|
305
311
|
uploadProgressCallback?: (progress: number) => void
|
|
306
312
|
): Promise<void> {
|
|
307
313
|
return Promise<void>((resolve, reject, notify) => {
|
|
308
|
-
updateMetadata.appVersion = targetBinaryVersion;
|
|
309
314
|
const request: superagent.Request<any> = superagent.post(
|
|
310
315
|
this._serverUrl + urlEncode([`/apps/${appName}/deployments/${deploymentName}/release`])
|
|
311
316
|
);
|
|
@@ -569,7 +574,7 @@ class AccountManager {
|
|
|
569
574
|
|
|
570
575
|
request.set("Accept", `application/vnd.code-push.v${AccountManager.API_VERSION}+json`);
|
|
571
576
|
request.set("Authorization", `Bearer ${this._accessKey}`);
|
|
572
|
-
request.set("X-CodePush-SDK-Version", packageJson.version);
|
|
577
|
+
request.set("X-CodePush-SDK-Version", packageJson.version); // TODO get version differently without require("../../package.json");
|
|
573
578
|
}
|
|
574
579
|
}
|
|
575
580
|
|
|
@@ -3,10 +3,11 @@ import * as chalk from "chalk";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as childProcess from "child_process";
|
|
5
5
|
import { coerce, compare, valid } from "semver";
|
|
6
|
-
import { fileDoesNotExistOrIsDirectory } from "./utils/file-utils";
|
|
6
|
+
import { downloadBlob, extract, fileDoesNotExistOrIsDirectory } from "./utils/file-utils";
|
|
7
7
|
import * as dotenv from "dotenv";
|
|
8
8
|
import { DotenvParseOutput } from "dotenv";
|
|
9
9
|
import * as cli from "../script/types/cli";
|
|
10
|
+
import { log, sdk } from "./command-executor";
|
|
10
11
|
|
|
11
12
|
const g2js = require("gradle-to-js/lib/parser");
|
|
12
13
|
|
|
@@ -47,13 +48,37 @@ export async function getBundleSourceMapOutput(command: cli.IReleaseReactCommand
|
|
|
47
48
|
return bundleSourceMapOutput;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
export async function takeHermesBaseBytecode(
|
|
52
|
+
command: cli.IReleaseReactCommand,
|
|
53
|
+
baseReleaseTmpFolder: string,
|
|
54
|
+
outputFolder: string,
|
|
55
|
+
bundleName: string
|
|
56
|
+
): Promise<string | null> {
|
|
57
|
+
const { bundleBlobUrl } = await sdk.getBaseRelease(command.appName, command.deploymentName, command.appStoreVersion);
|
|
58
|
+
if (!bundleBlobUrl) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const baseReleaseArchive = await downloadBlob(bundleBlobUrl, baseReleaseTmpFolder);
|
|
63
|
+
await extract(baseReleaseArchive, baseReleaseTmpFolder);
|
|
64
|
+
const baseReleaseBundle = path.join(baseReleaseTmpFolder, path.basename(outputFolder), bundleName);
|
|
65
|
+
|
|
66
|
+
if (!fs.existsSync(baseReleaseBundle)) {
|
|
67
|
+
log(chalk.cyan("\nNo base release available...\n"));
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return baseReleaseBundle;
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
export async function runHermesEmitBinaryCommand(
|
|
51
75
|
command: cli.IReleaseReactCommand,
|
|
52
76
|
bundleName: string,
|
|
53
77
|
outputFolder: string,
|
|
54
78
|
sourcemapOutputFolder: string,
|
|
55
79
|
extraHermesFlags: string[],
|
|
56
|
-
gradleFile: string
|
|
80
|
+
gradleFile: string,
|
|
81
|
+
baseBytecode?: string
|
|
57
82
|
): Promise<void> {
|
|
58
83
|
const hermesArgs: string[] = [];
|
|
59
84
|
const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS;
|
|
@@ -75,6 +100,10 @@ export async function runHermesEmitBinaryCommand(
|
|
|
75
100
|
hermesArgs.push("-output-source-map");
|
|
76
101
|
}
|
|
77
102
|
|
|
103
|
+
if (baseBytecode) {
|
|
104
|
+
hermesArgs.push("-base-bytecode", baseBytecode);
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
console.log(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n"));
|
|
79
108
|
const hermesCommand = await getHermesCommand(gradleFile);
|
|
80
109
|
const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs);
|
|
@@ -416,7 +445,7 @@ function getComposeSourceMapsPath(): string {
|
|
|
416
445
|
}
|
|
417
446
|
|
|
418
447
|
function getNodeModulesPath(reactNativePath: string): string {
|
|
419
|
-
|
|
448
|
+
const nodeModulesPath = path.dirname(reactNativePath);
|
|
420
449
|
if (directoryExistsSync(nodeModulesPath)) {
|
|
421
450
|
return nodeModulesPath;
|
|
422
451
|
}
|
package/script/types/cli.ts
CHANGED
|
@@ -132,6 +132,7 @@ export interface IDeploymentListCommand extends ICommand {
|
|
|
132
132
|
export interface IDeploymentRemoveCommand extends ICommand {
|
|
133
133
|
appName: string;
|
|
134
134
|
deploymentName: string;
|
|
135
|
+
isForce?: boolean;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
export interface IDeploymentRenameCommand extends ICommand {
|
|
@@ -156,6 +157,7 @@ export interface IPackageInfo {
|
|
|
156
157
|
disabled?: boolean;
|
|
157
158
|
mandatory?: boolean;
|
|
158
159
|
rollout?: number;
|
|
160
|
+
initial?: boolean;
|
|
159
161
|
}
|
|
160
162
|
|
|
161
163
|
export interface IPatchCommand extends ICommand, IPackageInfo {
|
|
@@ -190,6 +192,7 @@ export interface IReleaseCommand extends IReleaseBaseCommand {
|
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
export interface IReleaseReactCommand extends IReleaseBaseCommand {
|
|
195
|
+
package?: string;
|
|
193
196
|
bundleName?: string;
|
|
194
197
|
development?: boolean;
|
|
195
198
|
entryFile?: string;
|
|
@@ -49,11 +49,18 @@ export interface PackageInfo {
|
|
|
49
49
|
description?: string;
|
|
50
50
|
isDisabled?: boolean;
|
|
51
51
|
isMandatory?: boolean;
|
|
52
|
+
isInitial?: boolean;
|
|
52
53
|
/*generated*/ label?: string;
|
|
53
54
|
/*generated*/ packageHash?: string;
|
|
54
55
|
rollout?: number;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/*inout*/
|
|
59
|
+
export interface ReactNativePackageInfo extends PackageInfo {
|
|
60
|
+
bundleName?: string;
|
|
61
|
+
outputDir?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
/*out*/
|
|
58
65
|
export interface UpdateCheckResponse extends PackageInfo {
|
|
59
66
|
target_binary_range?: string;
|
|
@@ -126,6 +133,11 @@ export interface Deployment {
|
|
|
126
133
|
/*generated*/ package?: Package;
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
/*inout*/
|
|
137
|
+
export interface BaseRelease {
|
|
138
|
+
bundleBlobUrl?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
/*out*/
|
|
130
142
|
export interface BlobInfo {
|
|
131
143
|
size: number;
|
package/script/types.ts
CHANGED
|
@@ -2,6 +2,9 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as rimraf from "rimraf";
|
|
4
4
|
import * as temp from "temp";
|
|
5
|
+
import * as unzipper from "unzipper";
|
|
6
|
+
|
|
7
|
+
import superagent = require("superagent");
|
|
5
8
|
|
|
6
9
|
export function isBinaryOrZip(path: string): boolean {
|
|
7
10
|
return path.search(/\.zip$/i) !== -1 || path.search(/\.apk$/i) !== -1 || path.search(/\.ipa$/i) !== -1;
|
|
@@ -17,7 +20,7 @@ export function fileExists(file: string): boolean {
|
|
|
17
20
|
} catch (e) {
|
|
18
21
|
return false;
|
|
19
22
|
}
|
|
20
|
-
}
|
|
23
|
+
}
|
|
21
24
|
|
|
22
25
|
export function copyFileToTmpDir(filePath: string): string {
|
|
23
26
|
if (!isDirectory(filePath)) {
|
|
@@ -44,3 +47,29 @@ export function normalizePath(filePath: string): string {
|
|
|
44
47
|
//replace all backslashes coming from cli running on windows machines by slashes
|
|
45
48
|
return filePath.replace(/\\/g, "/");
|
|
46
49
|
}
|
|
50
|
+
|
|
51
|
+
export async function downloadBlob(url: string, folder: string, filename: string = "blob.zip"): Promise<string> {
|
|
52
|
+
const destination = path.join(folder, filename);
|
|
53
|
+
const writeStream = fs.createWriteStream(destination);
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
writeStream.on("finish", () => resolve(destination));
|
|
57
|
+
writeStream.on("error", reject);
|
|
58
|
+
|
|
59
|
+
superagent
|
|
60
|
+
.get(url)
|
|
61
|
+
.ok((res) => res.status < 400)
|
|
62
|
+
.on("error", (err) => {
|
|
63
|
+
writeStream.destroy();
|
|
64
|
+
reject(err);
|
|
65
|
+
})
|
|
66
|
+
.pipe(writeStream);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function extract(zipPath: string, extractTo: string) {
|
|
71
|
+
const extractStream = unzipper.Extract({ path: extractTo });
|
|
72
|
+
await new Promise<void>((resolve, reject) => {
|
|
73
|
+
fs.createReadStream(zipPath).pipe(extractStream).on("close", resolve).on("error", reject);
|
|
74
|
+
});
|
|
75
|
+
}
|
package/test/management-sdk.ts
CHANGED
|
@@ -196,6 +196,15 @@ describe("Management SDK", () => {
|
|
|
196
196
|
}, rejectHandler);
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
+
it("getBaseBundle handles JSON response", (done: Mocha.Done) => {
|
|
200
|
+
mockReturn(JSON.stringify({ basebundle: { bundleBlobUrl: "https://test.test/release.zip" } }), 200, {});
|
|
201
|
+
|
|
202
|
+
manager.getBaseRelease("appName", "deploymentName", "1.2.3").done((obj: any) => {
|
|
203
|
+
assert.ok(obj);
|
|
204
|
+
done();
|
|
205
|
+
}, rejectHandler);
|
|
206
|
+
});
|
|
207
|
+
|
|
199
208
|
it("getDeployments handles JSON response", (done: Mocha.Done) => {
|
|
200
209
|
mockReturn(JSON.stringify({ deployments: [] }), 200, {});
|
|
201
210
|
|