@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.
@@ -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
- return (0, exports.confirm)("Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable.").then((wasConfirmed) => {
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
- if ((0, file_utils_1.isBinaryOrZip)(command.package)) {
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
- rollout: command.rollout,
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 exports.sdk
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 (0, exports.release)(releaseCommand);
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, targetBinaryVersion, updateMetadata, uploadProgressCallback) {
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 runHermesEmitBinaryCommand(command, bundleName, outputFolder, sourcemapOutputFolder, extraHermesFlags, gradleFile) {
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.7",
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
- return confirm(
368
- "Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable."
369
- ).then((wasConfirmed: boolean): Promise<void> => {
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
- if (isBinaryOrZip(command.package)) {
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
- rollout: command.rollout,
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 sdk
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.IReleaseCommand = <any>command;
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 release(releaseCommand);
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) {
@@ -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;
@@ -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
- const nodeModulesPath = path.dirname(reactNativePath)
448
+ const nodeModulesPath = path.dirname(reactNativePath);
420
449
  if (directoryExistsSync(nodeModulesPath)) {
421
450
  return nodeModulesPath;
422
451
  }
@@ -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
@@ -13,6 +13,7 @@ export {
13
13
  PackageInfo,
14
14
  AccessKey as ServerAccessKey,
15
15
  UpdateMetrics,
16
+ BaseRelease,
16
17
  } from "../script/types/rest-definitions";
17
18
 
18
19
  export interface CodePushError {
@@ -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
+ }
@@ -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