@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.
@@ -743,6 +743,207 @@ yargs
743
743
  return checkValidReleaseOptions(argv);
744
744
  });
745
745
  addCommonConfiguration(yargs);
746
+ })
747
+ .command("release-expo", "Release an Expo / React Native update to an app deployment", (yargs) => {
748
+ isValidCommand = true;
749
+ yargs
750
+ .usage(USAGE_PREFIX + " release-expo <appName> <platform> [options]")
751
+ .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments
752
+ .example("release-expo MyApp ios", 'Releases the Expo-managed iOS project in the current working directory to the "MyApp" app\'s "Staging" deployment')
753
+ .example("release-expo MyApp android -d Production", 'Releases the Expo-managed Android project in the current working directory to the "MyApp" app\'s "Production" deployment')
754
+ .option("bundleName", {
755
+ alias: "b",
756
+ default: null,
757
+ demand: false,
758
+ description: 'Name of the generated JS bundle file. If unspecified, the standard bundle name will be used, depending on the specified platform: "main.jsbundle" (iOS), "index.android.bundle" (Android) or "index.windows.bundle" (Windows)',
759
+ type: "string",
760
+ })
761
+ .option("deploymentName", {
762
+ alias: "d",
763
+ default: "Staging",
764
+ demand: false,
765
+ description: "Deployment to release the update to",
766
+ type: "string",
767
+ })
768
+ .option("description", {
769
+ alias: "des",
770
+ default: null,
771
+ demand: false,
772
+ description: "Description of the changes made to the app with this release",
773
+ type: "string",
774
+ })
775
+ .option("development", {
776
+ alias: "dev",
777
+ default: false,
778
+ demand: false,
779
+ description: "Specifies whether to generate a dev or release build",
780
+ type: "boolean",
781
+ })
782
+ .option("disabled", {
783
+ alias: "x",
784
+ default: false,
785
+ demand: false,
786
+ description: "Specifies whether this release should be immediately downloadable",
787
+ type: "boolean",
788
+ })
789
+ .option("entryFile", {
790
+ alias: "e",
791
+ default: null,
792
+ demand: false,
793
+ description: 'Path to the app\'s entry Javascript file. If omitted, "index.<platform>.js" and then "index.js" will be used (if they exist)',
794
+ type: "string",
795
+ })
796
+ .option("gradleFile", {
797
+ alias: "g",
798
+ default: null,
799
+ demand: false,
800
+ description: "Path to the gradle file which specifies the binary version you want to target this release at (android only).",
801
+ })
802
+ .option("initial", {
803
+ alias: "i",
804
+ default: false,
805
+ demand: false,
806
+ description: "Specifies whether release is initial (base) for given targetBinaryVersion.",
807
+ type: "boolean",
808
+ })
809
+ .option("mandatory", {
810
+ alias: "m",
811
+ default: false,
812
+ demand: false,
813
+ description: "Specifies whether this release should be considered mandatory",
814
+ type: "boolean",
815
+ })
816
+ .option("noDuplicateReleaseError", {
817
+ default: false,
818
+ demand: false,
819
+ description: "When this flag is set, releasing a package that is identical to the latest release will produce a warning instead of an error",
820
+ type: "boolean",
821
+ })
822
+ .option("plistFile", {
823
+ alias: "p",
824
+ default: null,
825
+ demand: false,
826
+ description: "Path to the plist file which specifies the binary version you want to target this release at (iOS only).",
827
+ })
828
+ .option("plistFilePrefix", {
829
+ alias: "pre",
830
+ default: null,
831
+ demand: false,
832
+ description: "Prefix to append to the file name when attempting to find your app's Info.plist file (iOS only).",
833
+ })
834
+ .option("rollout", {
835
+ alias: "r",
836
+ default: "100%",
837
+ demand: false,
838
+ description: "Percentage of users this release should be immediately available to",
839
+ type: "string",
840
+ })
841
+ .option("sourcemapOutput", {
842
+ alias: "s",
843
+ default: null,
844
+ demand: false,
845
+ description: "Path to where the sourcemap for the resulting bundle should be written. If omitted, a sourcemap will not be generated.",
846
+ type: "string",
847
+ })
848
+ .option("targetBinaryVersion", {
849
+ alias: "t",
850
+ default: null,
851
+ demand: false,
852
+ description: 'Semver expression that specifies the binary app version(s) this release is targeting (e.g. 1.1.0, ~1.2.3). If omitted, the release will target the exact version specified in the "Info.plist" (iOS), "build.gradle" (Android) or "Package.appxmanifest" (Windows) files.',
853
+ type: "string",
854
+ })
855
+ .option("outputDir", {
856
+ alias: "o",
857
+ default: null,
858
+ demand: false,
859
+ description: "Path to where the bundle and sourcemap should be written. If omitted, a bundle and sourcemap will not be written.",
860
+ type: "string",
861
+ })
862
+ .option("useHermes", {
863
+ alias: "h",
864
+ default: false,
865
+ demand: false,
866
+ description: "Enable hermes and bypass automatic checks",
867
+ type: "boolean",
868
+ })
869
+ .option("podFile", {
870
+ alias: "pod",
871
+ default: null,
872
+ demand: false,
873
+ description: "Path to the cocopods config file (iOS only).",
874
+ type: "string",
875
+ })
876
+ .option("extraHermesFlags", {
877
+ alias: "hf",
878
+ default: [],
879
+ demand: false,
880
+ description: "Flags that get passed to Hermes, JavaScript to bytecode compiler. Can be specified multiple times.",
881
+ type: "array",
882
+ })
883
+ .option("privateKeyPath", {
884
+ alias: "k",
885
+ default: null,
886
+ demand: false,
887
+ description: "Path to private key used for code signing.",
888
+ type: "string",
889
+ })
890
+ .option("xcodeProjectFile", {
891
+ alias: "xp",
892
+ default: null,
893
+ demand: false,
894
+ description: "Path to the Xcode project or project.pbxproj file",
895
+ type: "string",
896
+ })
897
+ .option("xcodeTargetName", {
898
+ alias: "xt",
899
+ default: undefined,
900
+ demand: false,
901
+ description: "Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only)",
902
+ type: "string",
903
+ })
904
+ .option("buildConfigurationName", {
905
+ alias: "c",
906
+ default: undefined,
907
+ demand: false,
908
+ description: "Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only)",
909
+ type: "string",
910
+ })
911
+ .option("extraBundlerOption", {
912
+ alias: "eo",
913
+ default: [],
914
+ demand: false,
915
+ description: "Option that gets passed to react-native bundler. Can be specified multiple times.",
916
+ type: "array",
917
+ })
918
+ .check((argv) => {
919
+ return checkValidReleaseOptions(argv);
920
+ });
921
+ addCommonConfiguration(yargs);
922
+ })
923
+ .command("release-native", "Release a binary update to an app deployment", (yargs) => {
924
+ yargs
925
+ .usage(USAGE_PREFIX + " release-native <appName> <platform> <targetBinary> [options]")
926
+ .demand(/*count*/ 3, /*max*/ 3) // Require exactly three non-option arguments
927
+ .example("release-native MyApp ios ./app.ipa", 'Releases the React Native iOS project from "./app.ipa" to the "MyApp" app\'s "Staging" deployment')
928
+ .example("release-native MyApp android ./app.apk -d Production", 'Releases the React Native Android project from "./app.apk" to the "MyApp" app\'s "Production" deployment')
929
+ .option("deploymentName", {
930
+ alias: "d",
931
+ default: "Staging",
932
+ demand: false,
933
+ description: "Deployment to release the update to",
934
+ type: "string",
935
+ })
936
+ .option("targetBinaryVersion", {
937
+ alias: "t",
938
+ default: null,
939
+ demand: false,
940
+ description: "Semver expression that specifies the binary app version(s) this release is targeting (e.g. 1.1.0, ~1.2.3).",
941
+ type: "string",
942
+ })
943
+ .check((argv, aliases) => {
944
+ return checkValidReleaseOptions(argv);
945
+ });
946
+ addCommonConfiguration(yargs);
746
947
  })
747
948
  .command("rollback", "Rollback the latest release for an app deployment", (yargs) => {
748
949
  yargs
@@ -1070,6 +1271,54 @@ function createCommand() {
1070
1271
  releaseReactCommand.extraBundlerOptions = argv["extraBundlerOption"];
1071
1272
  }
1072
1273
  break;
1274
+ case "release-expo":
1275
+ if (arg1 && arg2) {
1276
+ cmd = { type: cli.CommandType.releaseExpo };
1277
+ const releaseExpoCommand = cmd;
1278
+ releaseExpoCommand.appName = arg1;
1279
+ releaseExpoCommand.platform = arg2;
1280
+ releaseExpoCommand.appStoreVersion = argv["targetBinaryVersion"];
1281
+ releaseExpoCommand.bundleName = argv["bundleName"];
1282
+ releaseExpoCommand.deploymentName = argv["deploymentName"];
1283
+ releaseExpoCommand.disabled = argv["disabled"];
1284
+ releaseExpoCommand.description = argv["description"] ? backslash(argv["description"]) : "";
1285
+ releaseExpoCommand.development = argv["development"];
1286
+ releaseExpoCommand.entryFile = argv["entryFile"];
1287
+ releaseExpoCommand.gradleFile = argv["gradleFile"];
1288
+ releaseExpoCommand.mandatory = argv["mandatory"];
1289
+ releaseExpoCommand.initial = argv["initial"];
1290
+ releaseExpoCommand.noDuplicateReleaseError = argv["noDuplicateReleaseError"];
1291
+ releaseExpoCommand.plistFile = argv["plistFile"];
1292
+ releaseExpoCommand.plistFilePrefix = argv["plistFilePrefix"];
1293
+ releaseExpoCommand.rollout = getRolloutValue(argv["rollout"]);
1294
+ releaseExpoCommand.sourcemapOutput = argv["sourcemapOutput"];
1295
+ releaseExpoCommand.outputDir = argv["outputDir"];
1296
+ releaseExpoCommand.useHermes = argv["useHermes"];
1297
+ releaseExpoCommand.extraHermesFlags = argv["extraHermesFlags"];
1298
+ releaseExpoCommand.podFile = argv["podFile"];
1299
+ releaseExpoCommand.privateKeyPath = argv["privateKeyPath"];
1300
+ releaseExpoCommand.xcodeProjectFile = argv["xcodeProjectFile"];
1301
+ releaseExpoCommand.xcodeTargetName = argv["xcodeTargetName"];
1302
+ releaseExpoCommand.buildConfigurationName = argv["buildConfigurationName"];
1303
+ releaseExpoCommand.extraBundlerOptions = argv["extraBundlerOption"];
1304
+ }
1305
+ break;
1306
+ case "release-native":
1307
+ if (arg1 && arg2 && arg3) {
1308
+ cmd = { type: cli.CommandType.releaseNative };
1309
+ const releaseBinaryCommand = cmd;
1310
+ releaseBinaryCommand.appName = arg1;
1311
+ releaseBinaryCommand.platform = arg2;
1312
+ releaseBinaryCommand.targetBinary = arg3;
1313
+ releaseBinaryCommand.deploymentName = argv["deploymentName"];
1314
+ releaseBinaryCommand.appStoreVersion = argv["targetBinaryVersion"];
1315
+ releaseBinaryCommand.initial = true;
1316
+ releaseBinaryCommand.disabled = true;
1317
+ releaseBinaryCommand.mandatory = false;
1318
+ // TODO add support for releaseBinaryCommand.bundleName
1319
+ // TODO add support for releaseBinaryCommand.outputDir
1320
+ }
1321
+ break;
1073
1322
  case "rollback":
1074
1323
  if (arg1 && arg2) {
1075
1324
  cmd = { type: cli.CommandType.rollback };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getExpoCliPath = void 0;
4
+ const childProcess = require("child_process");
5
+ function getExpoCliPath() {
6
+ const result = childProcess.spawnSync("node", ["--print", "require.resolve('@expo/cli')"]);
7
+ const cliPath = result.stdout.toString().trim();
8
+ if (result.status === 0 && cliPath) {
9
+ return cliPath;
10
+ }
11
+ throw new Error('Unable to resolve "@expo/cli". Please make sure it is installed in your project (e.g. "npm install --save-dev @expo/cli").');
12
+ }
13
+ exports.getExpoCliPath = getExpoCliPath;
@@ -268,6 +268,51 @@ class AccountManager {
268
268
  });
269
269
  });
270
270
  }
271
+ releaseNative(appName, deploymentName, filePath, updateMetadata, uploadProgressCallback) {
272
+ return Promise((resolve, reject, notify) => {
273
+ const request = superagent.post(this._serverUrl + urlEncode([`/apps/${appName}/deployments/${deploymentName}/nativerelease`]));
274
+ this.attachCredentials(request);
275
+ const file = fs.createReadStream(filePath);
276
+ request
277
+ .attach("package", file)
278
+ .field("packageInfo", JSON.stringify(updateMetadata))
279
+ .on("progress", (event) => {
280
+ if (uploadProgressCallback && event && event.total > 0) {
281
+ const currentProgress = (event.loaded / event.total) * 100;
282
+ uploadProgressCallback(currentProgress);
283
+ }
284
+ })
285
+ .end((err, res) => {
286
+ fs.unlinkSync(filePath);
287
+ if (err) {
288
+ reject(this.getCodePushError(err, res));
289
+ return;
290
+ }
291
+ if (res.ok) {
292
+ resolve(null);
293
+ }
294
+ else {
295
+ let body;
296
+ try {
297
+ body = JSON.parse(res.text);
298
+ }
299
+ catch (err) { }
300
+ if (body) {
301
+ reject({
302
+ message: body.message,
303
+ statusCode: res && res.status,
304
+ });
305
+ }
306
+ else {
307
+ reject({
308
+ message: res.text,
309
+ statusCode: res && res.status,
310
+ });
311
+ }
312
+ }
313
+ });
314
+ });
315
+ }
271
316
  patchRelease(appName, deploymentName, label, updateMetadata) {
272
317
  updateMetadata.label = label;
273
318
  const requestBody = JSON.stringify({ packageInfo: updateMetadata });
@@ -51,7 +51,7 @@ async function takeHermesBaseBytecode(command, baseReleaseTmpFolder, outputFolde
51
51
  return null;
52
52
  }
53
53
  const baseReleaseArchive = await (0, file_utils_1.downloadBlob)(bundleBlobUrl, baseReleaseTmpFolder);
54
- await (0, file_utils_1.extract)(baseReleaseArchive, baseReleaseTmpFolder);
54
+ await (0, file_utils_1.extractIPA)(baseReleaseArchive, baseReleaseTmpFolder);
55
55
  const baseReleaseBundle = path.join(baseReleaseTmpFolder, path.basename(outputFolder), bundleName);
56
56
  if (!fs.existsSync(baseReleaseBundle)) {
57
57
  (0, command_executor_1.log)(chalk.cyan("\nNo base release available...\n"));
@@ -72,6 +72,8 @@ async function runHermesEmitBinaryCommand(command, bundleName, outputFolder, sou
72
72
  "-out",
73
73
  path.join(outputFolder, bundleName + ".hbc"),
74
74
  path.join(outputFolder, bundleName),
75
+ "-w",
76
+ "-max-diagnostic-width=80",
75
77
  ...extraHermesFlags,
76
78
  ]);
77
79
  if (sourcemapOutputFolder) {
@@ -37,4 +37,6 @@ var CommandType;
37
37
  CommandType[CommandType["sessionList"] = 29] = "sessionList";
38
38
  CommandType[CommandType["sessionRemove"] = 30] = "sessionRemove";
39
39
  CommandType[CommandType["whoami"] = 31] = "whoami";
40
+ CommandType[CommandType["releaseExpo"] = 32] = "releaseExpo";
41
+ CommandType[CommandType["releaseNative"] = 33] = "releaseNative";
40
42
  })(CommandType || (exports.CommandType = CommandType = {}));
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extract = exports.downloadBlob = exports.normalizePath = exports.fileDoesNotExistOrIsDirectory = exports.copyFileToTmpDir = exports.fileExists = exports.isDirectory = exports.isBinaryOrZip = void 0;
3
+ exports.extractAPK = exports.extractIPA = 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
8
  const unzipper = require("unzipper");
9
+ const AdmZip = require("adm-zip");
9
10
  const superagent = require("superagent");
10
11
  function isBinaryOrZip(path) {
11
12
  return path.search(/\.zip$/i) !== -1 || path.search(/\.apk$/i) !== -1 || path.search(/\.ipa$/i) !== -1;
@@ -66,10 +67,15 @@ async function downloadBlob(url, folder, filename = "blob.zip") {
66
67
  });
67
68
  }
68
69
  exports.downloadBlob = downloadBlob;
69
- async function extract(zipPath, extractTo) {
70
+ async function extractIPA(zipPath, extractTo) {
70
71
  const extractStream = unzipper.Extract({ path: extractTo });
71
72
  await new Promise((resolve, reject) => {
72
73
  fs.createReadStream(zipPath).pipe(extractStream).on("close", resolve).on("error", reject);
73
74
  });
74
75
  }
75
- exports.extract = extract;
76
+ exports.extractIPA = extractIPA;
77
+ async function extractAPK(zipPath, extractTo) {
78
+ const zip = new AdmZip(zipPath);
79
+ zip.extractAllTo(extractTo, true);
80
+ }
81
+ exports.extractAPK = extractAPK;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revopush/code-push-cli",
3
- "version": "0.0.8-rc.0",
3
+ "version": "0.0.8-rc.1",
4
4
  "description": "Management CLI for the CodePush service",
5
5
  "main": "./script/cli.js",
6
6
  "scripts": {
@@ -23,7 +23,10 @@
23
23
  "Revopush"
24
24
  ],
25
25
  "dependencies": {
26
+ "@devicefarmer/adbkit-apkreader": "^3.2.4",
27
+ "adm-zip": "^0.5.16",
26
28
  "backslash": "^0.2.0",
29
+ "bplist-parser": "^0.3.2",
27
30
  "chalk": "^4.1.2",
28
31
  "cli-table": "^0.3.11",
29
32
  "dotenv": "^17.2.1",
@@ -33,7 +36,7 @@
33
36
  "moment": "^2.29.4",
34
37
  "opener": "^1.5.2",
35
38
  "parse-duration": "1.1.0",
36
- "plist": "^3.0.6",
39
+ "plist": "^3.1.0",
37
40
  "progress": "^2.0.3",
38
41
  "prompt": "^1.3.0",
39
42
  "properties": "^1.2.1",
@@ -0,0 +1,209 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs";
3
+ import * as chalk from "chalk";
4
+ import { log } from "./command-executor";
5
+ import { hashFile } from "./hash-utils";
6
+ import * as os from "os";
7
+ import * as Q from "q";
8
+ import * as yazl from "yazl";
9
+ import { readFile } from "node:fs/promises";
10
+ import plist from "plist"
11
+ import * as bplist from "bplist-parser";
12
+
13
+ export async function extractMetadataFromAndroid(extractFolder, outputFolder) {
14
+ const assetsFolder = path.join(extractFolder, "assets");
15
+ if (!fs.existsSync(assetsFolder)) {
16
+ throw new Error("Invalid APK structure: assets folder not found.");
17
+ }
18
+
19
+ const codepushMetadata = path.join(assetsFolder, "CodePushMetadata");
20
+
21
+ let fileHashes: { [key: string]: string } = {};
22
+ if (fs.existsSync(codepushMetadata)) {
23
+ fileHashes = await takeHashesFromMetadata(codepushMetadata);
24
+ } else {
25
+ log(chalk.yellow(`\nWarning: CodepushMetadata file not found in APK. Check used version of SDK\n`));
26
+ }
27
+
28
+ // Get index.android.bundle from root of app folder
29
+ const mainJsBundlePath = path.join(assetsFolder, "index.android.bundle");
30
+ if (fs.existsSync(mainJsBundlePath)) {
31
+ // Copy bundle to output folder
32
+ const outputCodePushFolder = path.join(outputFolder, "CodePush");
33
+ fs.mkdirSync(outputCodePushFolder, { recursive: true });
34
+ const outputBundlePath = path.join(outputCodePushFolder, "index.android.bundle");
35
+ fs.copyFileSync(mainJsBundlePath, outputBundlePath);
36
+ } else {
37
+ throw new Error("index.android.bundle not found in APK root folder.");
38
+ }
39
+
40
+ // Save packageManifest.json
41
+ const manifestPath = path.join(outputFolder, "packageManifest.json");
42
+ fs.writeFileSync(manifestPath, JSON.stringify(fileHashes, null, 2));
43
+ log(chalk.cyan(`\nSaved packageManifest.json with ${Object.keys(fileHashes).length} entries.\n`));
44
+
45
+ // Create zip archive with packageManifest.json and bundle file
46
+ const zipPath = path.join(os.tmpdir(), `CodePushBinary-${Date.now()}.zip`);
47
+ await createZipArchive(outputFolder, zipPath, ["packageManifest.json", "CodePush/index.android.bundle"]);
48
+
49
+ return zipPath;
50
+ }
51
+
52
+ export async function extractMetadataFromIOS(extractFolder, outputFolder) {
53
+ const payloadFolder = path.join(extractFolder, "Payload");
54
+ if (!fs.existsSync(payloadFolder)) {
55
+ throw new Error("Invalid IPA structure: Payload folder not found.");
56
+ }
57
+
58
+ const appFolders = fs.readdirSync(payloadFolder).filter((item) => {
59
+ const itemPath = path.join(payloadFolder, item);
60
+ return fs.statSync(itemPath).isDirectory() && item.endsWith(".app");
61
+ });
62
+
63
+ if (appFolders.length === 0) {
64
+ throw new Error("Invalid IPA structure: No .app folder found in Payload.");
65
+ }
66
+
67
+ const appFolder = path.join(payloadFolder, appFolders[0]);
68
+ const codePushFolder = path.join(appFolder, "assets");
69
+
70
+ const fileHashes: { [key: string]: string } = {};
71
+
72
+ if (fs.existsSync(codePushFolder)) {
73
+ await calculateHashesForDirectory(codePushFolder, appFolder, fileHashes);
74
+ } else {
75
+ log(chalk.yellow(`\nWarning: CodePush folder not found in IPA.\n`));
76
+ }
77
+
78
+ const mainJsBundlePath = path.join(appFolder, "main.jsbundle");
79
+ if (fs.existsSync(mainJsBundlePath)) {
80
+ log(chalk.cyan(`\nFound main.jsbundle, calculating hash:\n`));
81
+ const bundleHash = await hashFile(mainJsBundlePath);
82
+ fileHashes["CodePush/main.jsbundle"] = bundleHash;
83
+
84
+ // Copy bundle to output folder
85
+ const outputCodePushFolder = path.join(outputFolder, "CodePush");
86
+ fs.mkdirSync(outputCodePushFolder, { recursive: true });
87
+ const outputBundlePath = path.join(outputCodePushFolder, "main.jsbundle");
88
+ fs.copyFileSync(mainJsBundlePath, outputBundlePath);
89
+ } else {
90
+ throw new Error("main.jsbundle not found in IPA root folder.");
91
+ }
92
+
93
+ // Save packageManifest.json
94
+ const manifestPath = path.join(outputFolder, "packageManifest.json");
95
+ fs.writeFileSync(manifestPath, JSON.stringify(fileHashes, null, 2));
96
+ log(chalk.cyan(`\nSaved packageManifest.json with ${Object.keys(fileHashes).length} entries.\n`));
97
+
98
+ // Create zip archive with packageManifest.json and bundle file
99
+ const zipPath = path.join(os.tmpdir(), `CodePushBinary-${Date.now()}.zip`);
100
+ await createZipArchive(outputFolder, zipPath, ["packageManifest.json", "CodePush/main.jsbundle"]);
101
+
102
+ return zipPath;
103
+ }
104
+
105
+ async function calculateHashesForDirectory(
106
+ directoryPath: string,
107
+ basePath: string,
108
+ fileHashes: { [key: string]: string }
109
+ ) {
110
+ const items = fs.readdirSync(directoryPath);
111
+
112
+ for (const item of items) {
113
+ const itemPath = path.join(directoryPath, item);
114
+ const stat = fs.statSync(itemPath);
115
+
116
+ if (stat.isDirectory()) {
117
+ await calculateHashesForDirectory(itemPath, basePath, fileHashes);
118
+ } else {
119
+ // Calculate relative path from basePath (app folder) to the file
120
+ const relativePath = path.relative(basePath, itemPath).replace(/\\/g, "/");
121
+ const hash = await hashFile(itemPath);
122
+ const hashKey = `CodePush/${relativePath}`
123
+ fileHashes[hashKey] = hash;
124
+ log(chalk.gray(` ${relativePath}:${hash.substring(0, 8)}...\n`));
125
+ }
126
+ }
127
+ }
128
+
129
+
130
+ type BinaryHashes = { [p: string]: string };
131
+
132
+ async function takeHashesFromMetadata(metadataPath: string): Promise<BinaryHashes> {
133
+ const content = await readFile(metadataPath, "utf-8");
134
+ const metadata = JSON.parse(content);
135
+ if (!metadata || !metadata.manifest) {
136
+ throw new Error("Failed to take manifest from metadata file of APK");
137
+ }
138
+
139
+ return Object.fromEntries(metadata.manifest.map((item) => item.split(":")));
140
+ }
141
+
142
+ function createZipArchive(sourceFolder: string, zipPath: string, filesToInclude: string[]): Q.Promise<void> {
143
+ return Q.Promise<void>((resolve, reject) => {
144
+ const zipFile = new yazl.ZipFile();
145
+ const writeStream = fs.createWriteStream(zipPath);
146
+
147
+ zipFile.outputStream
148
+ .pipe(writeStream)
149
+ .on("error", (error: Error) => {
150
+ reject(error);
151
+ })
152
+ .on("close", () => {
153
+ resolve();
154
+ });
155
+
156
+ for (const file of filesToInclude) {
157
+ const filePath = path.join(sourceFolder, file);
158
+ if (fs.existsSync(filePath)) {
159
+ zipFile.addFile(filePath, file);
160
+ }
161
+ }
162
+
163
+ zipFile.end();
164
+ });
165
+ }
166
+
167
+ function parseAnyPlistFile(plistPath: string): any {
168
+ const buf = fs.readFileSync(plistPath);
169
+
170
+ if (buf.slice(0, 6).toString("ascii") === "bplist") {
171
+ const arr = bplist.parseBuffer(buf);
172
+ if (!arr?.length) throw new Error("Empty binary plist");
173
+ return arr[0];
174
+ }
175
+
176
+ const xml = buf.toString("utf8");
177
+ return plist.parse(xml);
178
+ }
179
+
180
+ export async function getIosVersion(extractFolder: string) {
181
+ const payloadFolder = path.join(extractFolder, "Payload");
182
+ if (!fs.existsSync(payloadFolder)) {
183
+ throw new Error("Invalid IPA structure: Payload folder not found.");
184
+ }
185
+
186
+ const appFolders = fs.readdirSync(payloadFolder).filter((item) => {
187
+ const itemPath = path.join(payloadFolder, item);
188
+ return fs.statSync(itemPath).isDirectory() && item.endsWith(".app");
189
+ });
190
+
191
+ if (appFolders.length === 0) {
192
+ throw new Error("Invalid IPA structure: No .app folder found in Payload.");
193
+ }
194
+
195
+ const appFolder = path.join(payloadFolder, appFolders[0]);
196
+
197
+ const plistPath = path.join(appFolder, "Info.plist");
198
+
199
+ const data = parseAnyPlistFile(plistPath);
200
+
201
+ console.log('App Version (Short):', data.CFBundleShortVersionString);
202
+ console.log('Build Number:', data.CFBundleVersion);
203
+ console.log('Bundle ID:', data.CFBundleIdentifier);
204
+
205
+ return {
206
+ version: data.CFBundleShortVersionString,
207
+ build: data.CFBundleVersion
208
+ };
209
+ }