@revopush/code-push-cli 0.0.8-rc.0 → 0.0.8-rc.1

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,6 +1,8 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
 
4
+ import { extractMetadataFromAndroid, extractMetadataFromIOS, getIosVersion } from "./binary-utils";
5
+
4
6
  const childProcess = require("child_process");
5
7
  import debugCommand from "./commands/debug";
6
8
  import * as fs from "fs";
@@ -12,6 +14,7 @@ import * as Q from "q";
12
14
  import * as semver from "semver";
13
15
  import * as cli from "../script/types/cli";
14
16
  import sign from "./sign";
17
+ const ApkReader = require("@devicefarmer/adbkit-apkreader");
15
18
  import {
16
19
  AccessKey,
17
20
  Account,
@@ -36,12 +39,13 @@ import {
36
39
  runHermesEmitBinaryCommand,
37
40
  takeHermesBaseBytecode,
38
41
  } from "./react-native-utils";
39
- import { fileDoesNotExistOrIsDirectory, fileExists, isBinaryOrZip } from "./utils/file-utils";
42
+ import { fileDoesNotExistOrIsDirectory, fileExists, isBinaryOrZip, extractIPA, extractAPK } from "./utils/file-utils";
40
43
 
41
44
  import AccountManager = require("./management-sdk");
42
45
  import wordwrap = require("wordwrap");
43
46
  import Promise = Q.Promise;
44
47
  import { ReactNativePackageInfo } from "./types/rest-definitions";
48
+ import { getExpoCliPath } from "./expo-utils";
45
49
 
46
50
  const g2js = require("gradle-to-js/lib/parser");
47
51
 
@@ -554,6 +558,12 @@ export function execute(command: cli.ICommand) {
554
558
  case cli.CommandType.releaseReact:
555
559
  return releaseReact(<cli.IReleaseReactCommand>command);
556
560
 
561
+ case cli.CommandType.releaseExpo:
562
+ return releaseExpo(<cli.IReleaseReactCommand>command);
563
+
564
+ case cli.CommandType.releaseNative:
565
+ return releaseNative(<cli.IReleaseNativeCommand>command);
566
+
557
567
  case cli.CommandType.rollback:
558
568
  return rollback(<cli.IRollbackCommand>command);
559
569
 
@@ -1223,6 +1233,226 @@ export const release = (command: cli.IReleaseCommand): Promise<void> => {
1223
1233
  return doRelease(command, updateMetadata);
1224
1234
  };
1225
1235
 
1236
+ export const runExpoExportEmbedCommand = async (
1237
+ command: cli.IReleaseReactCommand,
1238
+ bundleName: string,
1239
+ development: boolean,
1240
+ // entryFile: string,
1241
+ outputFolder: string,
1242
+ sourcemapOutputFolder: string,
1243
+ platform: string,
1244
+ extraBundlerOptions: string[]
1245
+ ) => {
1246
+ const expoBundleArgs: string[] = [];
1247
+ const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS;
1248
+
1249
+ if (typeof envNodeArgs !== "undefined") {
1250
+ Array.prototype.push.apply(expoBundleArgs, envNodeArgs.trim().split(/\s+/));
1251
+ }
1252
+
1253
+ const expoCliPath = getExpoCliPath();
1254
+
1255
+ Array.prototype.push.apply(expoBundleArgs, [
1256
+ expoCliPath,
1257
+ "export:embed",
1258
+ "--assets-dest",
1259
+ outputFolder,
1260
+ "--bundle-output",
1261
+ path.join(outputFolder, bundleName),
1262
+ "--dev",
1263
+ development,
1264
+ "--platform",
1265
+ platform,
1266
+ "--minify",
1267
+ false,
1268
+ "--reset-cache",
1269
+ ]);
1270
+
1271
+ if (sourcemapOutputFolder) {
1272
+ let bundleSourceMapOutput = sourcemapOutputFolder;
1273
+ if (!sourcemapOutputFolder.endsWith(".map")) {
1274
+ // user defined directory, нужно вычислить полный путь
1275
+ bundleSourceMapOutput = await getBundleSourceMapOutput(command, bundleName, sourcemapOutputFolder);
1276
+ }
1277
+
1278
+ expoBundleArgs.push("--sourcemap-output", bundleSourceMapOutput);
1279
+ }
1280
+
1281
+ // const minifyValue = await getMinifyParams(command);
1282
+ // Array.prototype.push.apply(expoBundleArgs, minifyValue);
1283
+
1284
+ if (extraBundlerOptions.length > 0) {
1285
+ expoBundleArgs.push(...extraBundlerOptions);
1286
+ }
1287
+
1288
+ log(chalk.cyan('Running "expo export:embed" command:\n'));
1289
+
1290
+ const projectRoot = process.cwd();
1291
+ expoBundleArgs.push(projectRoot);
1292
+
1293
+ log("expoBundleArgs raw:" + JSON.stringify(expoBundleArgs, null, 2));
1294
+
1295
+ const expoBundleProcess = spawn("node", expoBundleArgs);
1296
+ log(`node ${expoBundleArgs.join(" ")}`);
1297
+
1298
+ return Promise<void>((resolve, reject, notify) => {
1299
+ expoBundleProcess.stdout.on("data", (data: Buffer) => {
1300
+ log(data.toString().trim());
1301
+ });
1302
+
1303
+ expoBundleProcess.stderr.on("data", (data: Buffer) => {
1304
+ console.error(data.toString().trim());
1305
+ });
1306
+
1307
+ expoBundleProcess.on("close", (exitCode: number) => {
1308
+ if (exitCode) {
1309
+ reject(new Error(`"expo export:embed" command exited with code ${exitCode}.`));
1310
+ }
1311
+
1312
+ resolve(<void>null);
1313
+ });
1314
+ });
1315
+ };
1316
+
1317
+ export const releaseExpo = (command: cli.IReleaseReactCommand): Promise<void> => {
1318
+ let bundleName: string = command.bundleName;
1319
+ // let entryFile: string = command.entryFile;
1320
+ const outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush");
1321
+ const sourcemapOutputFolder: string = command.sourcemapOutput || path.join(os.tmpdir(), "CodePushSourceMap");
1322
+ const baseReleaseTmpFolder: string = path.join(os.tmpdir(), "CodePushBaseRelease");
1323
+ const platform: string = (command.platform = command.platform.toLowerCase());
1324
+ const releaseCommand: cli.IReleaseReactCommand = <any>command;
1325
+
1326
+ return sdk
1327
+ .getDeployment(command.appName, command.deploymentName)
1328
+ .then(async () => {
1329
+ switch (platform) {
1330
+ case "android":
1331
+ case "ios":
1332
+ if (!bundleName) {
1333
+ bundleName = platform === "ios" ? "main.jsbundle" : `index.${platform}.bundle`;
1334
+ }
1335
+ break;
1336
+
1337
+ default:
1338
+ throw new Error('Platform must be either "android" or "ios" for the "release-expo" command.');
1339
+ }
1340
+
1341
+ releaseCommand.package = outputFolder;
1342
+ releaseCommand.outputDir = outputFolder;
1343
+ releaseCommand.bundleName = bundleName;
1344
+
1345
+ let projectName: string;
1346
+
1347
+ try {
1348
+ const projectPackageJson: any = require(path.join(process.cwd(), "package.json"));
1349
+ projectName = projectPackageJson.name;
1350
+ if (!projectName) {
1351
+ throw new Error('The "package.json" file in the CWD does not have the "name" field set.');
1352
+ }
1353
+
1354
+ if (!projectPackageJson.dependencies["react-native"]) {
1355
+ throw new Error("The project in the CWD is not a React Native project.");
1356
+ }
1357
+ } catch (error) {
1358
+ throw new Error(
1359
+ 'Unable to find or read "package.json" in the CWD. The "release-expo" command must be executed in a React Native project folder.'
1360
+ );
1361
+ }
1362
+
1363
+ // TODO: do we really need entryFile for expo?
1364
+
1365
+ // if (!entryFile) {
1366
+ // entryFile = `index.${platform}.js`;
1367
+ // if (fileDoesNotExistOrIsDirectory(entryFile)) {
1368
+ // entryFile = "index.js";
1369
+ // }
1370
+ //
1371
+ // if (fileDoesNotExistOrIsDirectory(entryFile)) {
1372
+ // throw new Error(`Entry file "index.${platform}.js" or "index.js" does not exist.`);
1373
+ // }
1374
+ // } else {
1375
+ // if (fileDoesNotExistOrIsDirectory(entryFile)) {
1376
+ // throw new Error(`Entry file "${entryFile}" does not exist.`);
1377
+ // }
1378
+ // }
1379
+
1380
+ const appVersionPromise: Promise<string> = command.appStoreVersion
1381
+ ? Q(command.appStoreVersion)
1382
+ : getReactNativeProjectAppVersion(command, projectName);
1383
+
1384
+ if (!sourcemapOutputFolder.endsWith(".map") && !command.sourcemapOutput) {
1385
+ await createEmptyTempReleaseFolder(sourcemapOutputFolder);
1386
+ }
1387
+
1388
+ return appVersionPromise;
1389
+ })
1390
+ .then((appVersion: string) => {
1391
+ throwForInvalidSemverRange(appVersion);
1392
+ releaseCommand.appStoreVersion = appVersion;
1393
+
1394
+ return createEmptyTempReleaseFolder(outputFolder);
1395
+ })
1396
+ .then(() => deleteFolder(`${os.tmpdir()}/react-*`))
1397
+ .then(async () => {
1398
+ await runExpoExportEmbedCommand(
1399
+ command,
1400
+ bundleName,
1401
+ command.development || false,
1402
+ // entryFile,
1403
+ outputFolder,
1404
+ sourcemapOutputFolder,
1405
+ platform,
1406
+ command.extraBundlerOptions
1407
+ );
1408
+ })
1409
+ .then(async () => {
1410
+ const isHermes = await isHermesEnabled(command, platform);
1411
+
1412
+ if (isHermes) {
1413
+ await createEmptyTempReleaseFolder(baseReleaseTmpFolder);
1414
+ const baseBytecode = await takeHermesBaseBytecode(command, baseReleaseTmpFolder, outputFolder, bundleName);
1415
+
1416
+ log(chalk.cyan("\nRunning hermes compiler.\n"));
1417
+ await runHermesEmitBinaryCommand(
1418
+ command,
1419
+ bundleName,
1420
+ outputFolder,
1421
+ sourcemapOutputFolder,
1422
+ command.extraHermesFlags,
1423
+ command.gradleFile,
1424
+ baseBytecode
1425
+ );
1426
+ }
1427
+ })
1428
+ .then(async () => {
1429
+ if (command.privateKeyPath) {
1430
+ log(chalk.cyan("\nSigning the bundle:\n"));
1431
+ await sign(command.privateKeyPath, outputFolder);
1432
+ } else {
1433
+ console.log("private key was not provided");
1434
+ }
1435
+ })
1436
+ .then(() => {
1437
+ log(chalk.cyan("\nReleasing update contents to CodePush:\n"));
1438
+ return releaseReactNative(releaseCommand);
1439
+ })
1440
+ .then(async () => {
1441
+ if (!command.outputDir) {
1442
+ await deleteFolder(outputFolder);
1443
+ }
1444
+
1445
+ if (!command.sourcemapOutput) {
1446
+ await deleteFolder(sourcemapOutputFolder);
1447
+ }
1448
+
1449
+ await deleteFolder(baseReleaseTmpFolder);
1450
+ })
1451
+ .catch(async (err: Error) => {
1452
+ throw err;
1453
+ });
1454
+ };
1455
+
1226
1456
  export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> => {
1227
1457
  let bundleName: string = command.bundleName;
1228
1458
  let entryFile: string = command.entryFile;
@@ -1272,6 +1502,7 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
1272
1502
  );
1273
1503
  }
1274
1504
 
1505
+ // TODO: check entry file detection
1275
1506
  if (!entryFile) {
1276
1507
  entryFile = `index.${platform}.js`;
1277
1508
  if (fileDoesNotExistOrIsDirectory(entryFile)) {
@@ -1367,6 +1598,93 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise<void> =
1367
1598
  );
1368
1599
  };
1369
1600
 
1601
+ export const releaseNative = (command: cli.IReleaseNativeCommand): Promise<void> => {
1602
+ const platform: string = command.platform.toLowerCase();
1603
+ let bundleName: string = command.bundleName;
1604
+ const targetBinaryPath: string = command.targetBinary;
1605
+ const outputFolder: string = command.outputDir || path.join(os.tmpdir(), "CodePush");
1606
+ const extractFolder: string = path.join(os.tmpdir(), "CodePushBinaryExtract");
1607
+ // Validate platform
1608
+ if (platform !== "ios" && platform !== "android") {
1609
+ throw new Error('Platform must be either "ios" or "android" for the "release-native" command.');
1610
+ }
1611
+ // Validate target binary file exists
1612
+ if (!fileExists(targetBinaryPath)) {
1613
+ throw new Error(`Target binary file "${targetBinaryPath}" does not exist.`);
1614
+ }
1615
+ // Validate file extension matches platform
1616
+ if (platform === "ios" && !targetBinaryPath.toLowerCase().endsWith(".ipa")) {
1617
+ throw new Error("For iOS platform, target binary must be an .ipa file.");
1618
+ }
1619
+ if (platform === "android" && !targetBinaryPath.toLowerCase().endsWith(".apk")) {
1620
+ throw new Error("For Android platform, target binary must be an .apk file.");
1621
+ }
1622
+
1623
+ return sdk
1624
+ .getDeployment(command.appName, command.deploymentName)
1625
+ .then(async () => {
1626
+ try {
1627
+ await createEmptyTempReleaseFolder(outputFolder);
1628
+ await createEmptyTempReleaseFolder(extractFolder);
1629
+
1630
+ if (!bundleName) {
1631
+ bundleName = platform === "ios" ? "main.jsbundle" : `index.android.bundle`;
1632
+ }
1633
+ let releaseCommandPartial: Partial<cli.IReleaseReactCommand>;
1634
+
1635
+ if (platform === "ios") {
1636
+ log(chalk.cyan(`\nExtracting IPA file:\n`));
1637
+ await extractIPA(targetBinaryPath, extractFolder);
1638
+ const metadataZip = await extractMetadataFromIOS(extractFolder, outputFolder);
1639
+ const buildVersion = await getIosVersion(extractFolder)
1640
+ releaseCommandPartial = { package: metadataZip, appStoreVersion: buildVersion?.version };
1641
+ } else {
1642
+ log(chalk.cyan(`\nExtracting APK/ARR file:\n`));
1643
+ await extractAPK(targetBinaryPath, extractFolder);
1644
+
1645
+ const reader = await ApkReader.open(targetBinaryPath);
1646
+ const { versionName: appStoreVersion } = await reader.readManifest();
1647
+ const metadataZip = await extractMetadataFromAndroid(extractFolder, outputFolder);
1648
+ releaseCommandPartial = { package: metadataZip, appStoreVersion };
1649
+ }
1650
+
1651
+ const { package: metadataZip, appStoreVersion } = releaseCommandPartial;
1652
+ // Use the zip file as package for release
1653
+ const releaseCommand: cli.IReleaseReactCommand = {
1654
+ type: cli.CommandType.release,
1655
+ appName: command.appName,
1656
+ deploymentName: command.deploymentName,
1657
+ appStoreVersion: command.appStoreVersion || appStoreVersion,
1658
+ description: command.description,
1659
+ disabled: command.disabled,
1660
+ mandatory: command.mandatory,
1661
+ rollout: command.rollout,
1662
+ initial: command.initial,
1663
+ noDuplicateReleaseError: command.noDuplicateReleaseError,
1664
+ platform: platform,
1665
+ outputDir: outputFolder,
1666
+ bundleName: bundleName,
1667
+ package: metadataZip,
1668
+ };
1669
+
1670
+ return doNativeRelease(releaseCommand).then(async () => {
1671
+ // Clean up zip file
1672
+ if (fs.existsSync(releaseCommandPartial.package)) {
1673
+ fs.unlinkSync(releaseCommandPartial.package);
1674
+ }
1675
+ });
1676
+ } finally {
1677
+ try {
1678
+ await deleteFolder(extractFolder);
1679
+ await deleteFolder(outputFolder);
1680
+ } catch (ignored) {}
1681
+ }
1682
+ })
1683
+ .catch(async (err: Error) => {
1684
+ throw err;
1685
+ });
1686
+ };
1687
+
1370
1688
  const releaseReactNative = (command: cli.IReleaseReactCommand): Promise<void> => {
1371
1689
  // for initial release we explicitly define release as optional, disabled, without rollout, with a special description
1372
1690
  const updateMetadata: ReactNativePackageInfo = {
@@ -1414,6 +1732,8 @@ const doRelease = (command: cli.IReleaseCommand | cli.IReleaseReactCommand, upda
1414
1732
  return sdk
1415
1733
  .isAuthenticated(true)
1416
1734
  .then((isAuth: boolean): Promise<void> => {
1735
+ log("Release file path: " + filePath);
1736
+ log("Metadata: " + JSON.stringify(updateMetadata));
1417
1737
  return sdk.release(command.appName, command.deploymentName, filePath, updateMetadata, uploadProgress);
1418
1738
  })
1419
1739
  .then((): void => {
@@ -1432,6 +1752,57 @@ const doRelease = (command: cli.IReleaseCommand | cli.IReleaseReactCommand, upda
1432
1752
  .catch((err: CodePushError) => releaseErrorHandler(err, command));
1433
1753
  };
1434
1754
 
1755
+ const doNativeRelease = (releaseCommand: cli.IReleaseReactCommand): Promise<void> => {
1756
+ throwForInvalidSemverRange(releaseCommand.appStoreVersion);
1757
+
1758
+ const filePath: string = releaseCommand.package;
1759
+
1760
+ const updateMetadata: ReactNativePackageInfo = {
1761
+ description: releaseCommand.initial ? `Zero release for v${releaseCommand.appStoreVersion}` : releaseCommand.description,
1762
+ isDisabled: releaseCommand.initial ? true : releaseCommand.disabled,
1763
+ isMandatory: releaseCommand.initial ? false : releaseCommand.mandatory,
1764
+ isInitial: releaseCommand.initial,
1765
+ bundleName: releaseCommand.bundleName,
1766
+ outputDir: releaseCommand.outputDir,
1767
+ rollout: releaseCommand.initial ? undefined : releaseCommand.rollout,
1768
+ appVersion: releaseCommand.appStoreVersion,
1769
+ };
1770
+
1771
+ let lastTotalProgress = 0;
1772
+
1773
+ const progressBar = new progress("Upload progress:[:bar] :percent :etas", {
1774
+ complete: "=",
1775
+ incomplete: " ",
1776
+ width: 50,
1777
+ total: 100,
1778
+ });
1779
+
1780
+ const uploadProgress = (currentProgress: number): void => {
1781
+ progressBar.tick(currentProgress - lastTotalProgress);
1782
+ lastTotalProgress = currentProgress;
1783
+ };
1784
+
1785
+ return sdk
1786
+ .isAuthenticated(true)
1787
+ .then((): Promise<void> => {
1788
+ return sdk.releaseNative(releaseCommand.appName, releaseCommand.deploymentName, filePath, updateMetadata, uploadProgress);
1789
+ })
1790
+ .then((): void => {
1791
+ log(
1792
+ 'Successfully released an update containing the "' +
1793
+ releaseCommand.package +
1794
+ '" ' +
1795
+ "directory" +
1796
+ ' to the "' +
1797
+ releaseCommand.deploymentName +
1798
+ '" deployment of the "' +
1799
+ releaseCommand.appName +
1800
+ '" app.'
1801
+ );
1802
+ })
1803
+ .catch((err: CodePushError) => releaseErrorHandler(err, releaseCommand));
1804
+ };
1805
+
1435
1806
  function rollback(command: cli.IRollbackCommand): Promise<void> {
1436
1807
  return confirm().then((wasConfirmed: boolean) => {
1437
1808
  if (!wasConfirmed) {
@@ -1564,7 +1935,7 @@ function serializeConnectionInfo(accessKey: string, preserveAccessKeyOnLogout: b
1564
1935
 
1565
1936
  log(
1566
1937
  `\r\nSuccessfully logged-in. Your session file was written to ${chalk.cyan(configFilePath)}. You can run the ${chalk.cyan(
1567
- "code-push logout"
1938
+ "revopush logout"
1568
1939
  )} command at any time to delete this file and terminate your session.\r\n`
1569
1940
  );
1570
1941
  }