@revopush/code-push-cli 0.0.5 → 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.
@@ -1,35 +1,17 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
 
4
- import AccountManager = require("./management-sdk");
5
-
6
4
  const childProcess = require("child_process");
7
5
  import debugCommand from "./commands/debug";
8
6
  import * as fs from "fs";
9
7
  import * as chalk from "chalk";
10
-
11
- const g2js = require("gradle-to-js/lib/parser");
12
8
  import * as moment from "moment";
13
-
14
- const opener = require("opener");
15
9
  import * as os from "os";
16
10
  import * as path from "path";
17
-
18
- const plist = require("plist");
19
- const progress = require("progress");
20
- const prompt = require("prompt");
21
11
  import * as Q from "q";
22
-
23
- const rimraf = require("rimraf");
24
12
  import * as semver from "semver";
25
-
26
- const Table = require("cli-table");
27
- const which = require("which");
28
- import wordwrap = require("wordwrap");
29
13
  import * as cli from "../script/types/cli";
30
14
  import sign from "./sign";
31
-
32
- const xcode = require("xcode");
33
15
  import {
34
16
  AccessKey,
35
17
  Account,
@@ -45,14 +27,40 @@ import {
45
27
  Session,
46
28
  UpdateMetrics,
47
29
  } from "../script/types";
48
- import { getAndroidHermesEnabled, getiOSHermesEnabled, runHermesEmitBinaryCommand, isValidVersion } from "./react-native-utils";
49
- import { fileDoesNotExistOrIsDirectory, isBinaryOrZip, fileExists } from "./utils/file-utils";
30
+ import {
31
+ getBundleSourceMapOutput,
32
+ getMinifyParams,
33
+ getReactNativePackagePath,
34
+ isHermesEnabled,
35
+ isValidVersion,
36
+ runHermesEmitBinaryCommand,
37
+ takeHermesBaseBytecode,
38
+ } from "./react-native-utils";
39
+ import { fileDoesNotExistOrIsDirectory, fileExists, isBinaryOrZip } from "./utils/file-utils";
40
+
41
+ import AccountManager = require("./management-sdk");
42
+ import wordwrap = require("wordwrap");
43
+ import Promise = Q.Promise;
44
+ import { ReactNativePackageInfo } from "./types/rest-definitions";
45
+
46
+ const g2js = require("gradle-to-js/lib/parser");
47
+
48
+ const opener = require("opener");
49
+
50
+ const plist = require("plist");
51
+ const progress = require("progress");
52
+ const prompt = require("prompt");
53
+
54
+ const rimraf = require("rimraf");
55
+
56
+ const Table = require("cli-table");
57
+
58
+ const xcode = require("xcode");
50
59
 
51
60
  const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".revopush.config");
52
61
  const emailValidator = require("email-validator");
53
62
  const packageJson = require("../../package.json");
54
63
  const parseXml = Q.denodeify(require("xml2js").parseString);
55
- import Promise = Q.Promise;
56
64
 
57
65
  const properties = require("properties");
58
66
 
@@ -358,9 +366,11 @@ export const deploymentList = (command: cli.IDeploymentListCommand, showPackage:
358
366
  };
359
367
 
360
368
  function deploymentRemove(command: cli.IDeploymentRemoveCommand): Promise<void> {
361
- return confirm(
362
- "Are you sure you want to remove this deployment? Note that its deployment key will be PERMANENTLY unrecoverable."
363
- ).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> => {
364
374
  if (wasConfirmed) {
365
375
  return sdk.removeDeployment(command.appName, command.deploymentName).then((): void => {
366
376
  log('Successfully removed the "' + command.deploymentName + '" deployment from the "' + command.appName + '" app.');
@@ -1200,75 +1210,33 @@ function patch(command: cli.IPatchCommand): Promise<void> {
1200
1210
  }
1201
1211
 
1202
1212
  export const release = (command: cli.IReleaseCommand): Promise<void> => {
1203
- if (isBinaryOrZip(command.package)) {
1204
- throw new Error(
1205
- "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)."
1206
- );
1207
- }
1208
-
1209
- throwForInvalidSemverRange(command.appStoreVersion);
1210
- const filePath: string = command.package;
1211
- let isSingleFilePackage: boolean = true;
1212
-
1213
- if (fs.lstatSync(filePath).isDirectory()) {
1214
- isSingleFilePackage = false;
1215
- }
1216
-
1217
- let lastTotalProgress = 0;
1218
- const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
1219
- complete: "=",
1220
- incomplete: " ",
1221
- width: 50,
1222
- total: 100,
1223
- });
1224
-
1225
- const uploadProgress = (currentProgress: number): void => {
1226
- progressBar.tick(currentProgress - lastTotalProgress);
1227
- lastTotalProgress = currentProgress;
1228
- };
1229
-
1213
+ // for initial release we explicitly define release as optional, disabled, without rollout, with a special description
1230
1214
  const updateMetadata: PackageInfo = {
1231
- description: command.description,
1232
- isDisabled: command.disabled,
1233
- isMandatory: command.mandatory,
1234
- 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,
1235
1221
  };
1236
1222
 
1237
- return sdk
1238
- .isAuthenticated(true)
1239
- .then((isAuth: boolean): Promise<void> => {
1240
- return sdk.release(command.appName, command.deploymentName, filePath, command.appStoreVersion, updateMetadata, uploadProgress);
1241
- })
1242
- .then((): void => {
1243
- log(
1244
- 'Successfully released an update containing the "' +
1245
- command.package +
1246
- '" ' +
1247
- (isSingleFilePackage ? "file" : "directory") +
1248
- ' to the "' +
1249
- command.deploymentName +
1250
- '" deployment of the "' +
1251
- command.appName +
1252
- '" app.'
1253
- );
1254
- })
1255
- .catch((err: CodePushError) => releaseErrorHandler(err, command));
1223
+ return doRelease(command, updateMetadata);
1256
1224
  };
1257
1225
 
1258
1226
  export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> => {
1259
1227
  let bundleName: string = command.bundleName;
1260
1228
  let entryFile: string = command.entryFile;
1261
1229
  const outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush");
1230
+ const sourcemapOutputFolder: string = command.sourcemapOutput || path.join(os.tmpdir(), "CodePushSourceMap");
1231
+ const baseReleaseTmpFolder: string = path.join(os.tmpdir(), "CodePushBaseRelease");
1262
1232
  const platform: string = (command.platform = command.platform.toLowerCase());
1263
- const releaseCommand: cli.IReleaseCommand = <any>command;
1233
+ const releaseCommand: cli.IReleaseReactCommand = <any>command;
1264
1234
  // Check for app and deployment exist before releasing an update.
1265
1235
  // This validation helps to save about 1 minute or more in case user has typed wrong app or deployment name.
1266
1236
  return (
1267
1237
  sdk
1268
1238
  .getDeployment(command.appName, command.deploymentName)
1269
- .then((): any => {
1270
- releaseCommand.package = outputFolder;
1271
-
1239
+ .then(async () => {
1272
1240
  switch (platform) {
1273
1241
  case "android":
1274
1242
  case "ios":
@@ -1282,6 +1250,10 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
1282
1250
  throw new Error('Platform must be either "android", "ios" or "windows".');
1283
1251
  }
1284
1252
 
1253
+ releaseCommand.package = outputFolder;
1254
+ releaseCommand.outputDir = outputFolder;
1255
+ releaseCommand.bundleName = bundleName;
1256
+
1285
1257
  let projectName: string;
1286
1258
 
1287
1259
  try {
@@ -1319,8 +1291,9 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
1319
1291
  ? Q(command.appStoreVersion)
1320
1292
  : getReactNativeProjectAppVersion(command, projectName);
1321
1293
 
1322
- if (command.sourcemapOutput && !command.sourcemapOutput.endsWith(".map")) {
1323
- command.sourcemapOutput = path.join(command.sourcemapOutput, bundleName + ".map");
1294
+ if (!sourcemapOutputFolder.endsWith(".map") && !command.sourcemapOutput) {
1295
+ // create tmp dir only if no dir was given by user. User must crete a directory if --sourcemapOutput is passes
1296
+ await createEmptyTempReleaseFolder(sourcemapOutputFolder);
1324
1297
  }
1325
1298
 
1326
1299
  return appVersionPromise;
@@ -1334,31 +1307,34 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
1334
1307
  // This is needed to clear the react native bundler cache:
1335
1308
  // https://github.com/facebook/react-native/issues/4289
1336
1309
  .then(() => deleteFolder(`${os.tmpdir()}/react-*`))
1337
- .then(() =>
1338
- runReactNativeBundleCommand(
1310
+ .then(async () => {
1311
+ await runReactNativeBundleCommand(
1312
+ command,
1339
1313
  bundleName,
1340
1314
  command.development || false,
1341
1315
  entryFile,
1342
1316
  outputFolder,
1317
+ sourcemapOutputFolder,
1343
1318
  platform,
1344
- command.sourcemapOutput,
1345
1319
  command.extraBundlerOptions
1346
- )
1347
- )
1320
+ );
1321
+ })
1348
1322
  .then(async () => {
1349
- const isHermesEnabled =
1350
- command.useHermes ||
1351
- (platform === "android" && (await getAndroidHermesEnabled(command.gradleFile))) || // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in build.gradle and we're releasing an Android build
1352
- (platform === "ios" && (await getiOSHermesEnabled(command.podFile))); // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in Podfile and we're releasing an iOS build
1323
+ const isHermes = await isHermesEnabled(command, platform);
1324
+
1325
+ if (isHermes) {
1326
+ await createEmptyTempReleaseFolder(baseReleaseTmpFolder);
1327
+ const baseBytecode = await takeHermesBaseBytecode(command, baseReleaseTmpFolder, outputFolder, bundleName);
1353
1328
 
1354
- if (isHermesEnabled) {
1355
1329
  log(chalk.cyan("\nRunning hermes compiler...\n"));
1356
1330
  await runHermesEmitBinaryCommand(
1331
+ command,
1357
1332
  bundleName,
1358
1333
  outputFolder,
1359
- command.sourcemapOutput,
1334
+ sourcemapOutputFolder,
1360
1335
  command.extraHermesFlags,
1361
- command.gradleFile
1336
+ command.gradleFile,
1337
+ baseBytecode
1362
1338
  );
1363
1339
  }
1364
1340
  })
@@ -1372,20 +1348,90 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
1372
1348
  })
1373
1349
  .then(() => {
1374
1350
  log(chalk.cyan("\nReleasing update contents to CodePush:\n"));
1375
- return release(releaseCommand);
1351
+ return releaseReactNative(releaseCommand);
1376
1352
  })
1377
- .then(() => {
1353
+ .then(async () => {
1378
1354
  if (!command.outputDir) {
1379
- deleteFolder(outputFolder);
1355
+ await deleteFolder(outputFolder);
1380
1356
  }
1357
+
1358
+ if (!command.sourcemapOutput) {
1359
+ await deleteFolder(sourcemapOutputFolder);
1360
+ }
1361
+
1362
+ await deleteFolder(baseReleaseTmpFolder);
1381
1363
  })
1382
- .catch((err: Error) => {
1383
- deleteFolder(outputFolder);
1364
+ .catch(async (err: Error) => {
1384
1365
  throw err;
1385
1366
  })
1386
1367
  );
1387
1368
  };
1388
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
+
1389
1435
  function rollback(command: cli.IRollbackCommand): Promise<void> {
1390
1436
  return confirm().then((wasConfirmed: boolean) => {
1391
1437
  if (!wasConfirmed) {
@@ -1427,15 +1473,16 @@ function requestAccessKey(): Promise<string> {
1427
1473
  });
1428
1474
  }
1429
1475
 
1430
- export const runReactNativeBundleCommand = (
1476
+ export const runReactNativeBundleCommand = async (
1477
+ command: cli.IReleaseReactCommand,
1431
1478
  bundleName: string,
1432
1479
  development: boolean,
1433
1480
  entryFile: string,
1434
1481
  outputFolder: string,
1482
+ sourcemapOutputFolder: string,
1435
1483
  platform: string,
1436
- sourcemapOutput: string,
1437
1484
  extraBundlerOptions: string[]
1438
- ): Promise<void> => {
1485
+ ) => {
1439
1486
  const reactNativeBundleArgs: string[] = [];
1440
1487
  const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS;
1441
1488
 
@@ -1443,10 +1490,12 @@ export const runReactNativeBundleCommand = (
1443
1490
  Array.prototype.push.apply(reactNativeBundleArgs, envNodeArgs.trim().split(/\s+/));
1444
1491
  }
1445
1492
 
1446
- const isOldCLI = fs.existsSync(path.join("node_modules", "react-native", "local-cli", "cli.js"));
1493
+ const reactNativePackagePath = getReactNativePackagePath();
1494
+ const oldCliPath = path.join(reactNativePackagePath, "local-cli", "cli.js");
1495
+ const cliPath = fs.existsSync(oldCliPath) ? oldCliPath : path.join(reactNativePackagePath, "cli.js");
1447
1496
 
1448
1497
  Array.prototype.push.apply(reactNativeBundleArgs, [
1449
- isOldCLI ? path.join("node_modules", "react-native", "local-cli", "cli.js") : path.join("node_modules", "react-native", "cli.js"),
1498
+ cliPath,
1450
1499
  "bundle",
1451
1500
  "--assets-dest",
1452
1501
  outputFolder,
@@ -1458,12 +1507,22 @@ export const runReactNativeBundleCommand = (
1458
1507
  entryFile,
1459
1508
  "--platform",
1460
1509
  platform,
1510
+ "--reset-cache",
1461
1511
  ]);
1462
1512
 
1463
- if (sourcemapOutput) {
1464
- reactNativeBundleArgs.push("--sourcemap-output", sourcemapOutput);
1513
+ if (sourcemapOutputFolder) {
1514
+ let bundleSourceMapOutput = sourcemapOutputFolder;
1515
+ if (!sourcemapOutputFolder.endsWith(".map")) {
1516
+ // user defined full path to source map. let's use that instead
1517
+ bundleSourceMapOutput = await getBundleSourceMapOutput(command, bundleName, sourcemapOutputFolder);
1518
+ }
1519
+
1520
+ reactNativeBundleArgs.push("--sourcemap-output", bundleSourceMapOutput);
1465
1521
  }
1466
1522
 
1523
+ const minifyValue = await getMinifyParams(command);
1524
+ Array.prototype.push.apply(reactNativeBundleArgs, minifyValue);
1525
+
1467
1526
  if (extraBundlerOptions.length > 0) {
1468
1527
  reactNativeBundleArgs.push(...extraBundlerOptions);
1469
1528
  }
@@ -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