@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,18 +1,66 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getReactNativeVersion = exports.directoryExistsSync = exports.getiOSHermesEnabled = exports.getAndroidHermesEnabled = exports.runHermesEmitBinaryCommand = 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");
7
7
  const childProcess = require("child_process");
8
8
  const semver_1 = require("semver");
9
9
  const file_utils_1 = require("./utils/file-utils");
10
+ const dotenv = require("dotenv");
11
+ const command_executor_1 = require("./command-executor");
10
12
  const g2js = require("gradle-to-js/lib/parser");
11
13
  function isValidVersion(version) {
12
14
  return !!(0, semver_1.valid)(version) || /^\d+\.\d+$/.test(version);
13
15
  }
14
16
  exports.isValidVersion = isValidVersion;
15
- async function runHermesEmitBinaryCommand(bundleName, outputFolder, sourcemapOutput, extraHermesFlags, gradleFile) {
17
+ async function getBundleSourceMapOutput(command, bundleName, sourcemapOutputFolder) {
18
+ let bundleSourceMapOutput;
19
+ switch (command.platform) {
20
+ case "android": {
21
+ // see BundleHermesCTask -> resolvePackagerSourceMapFile
22
+ // for Hermes targeted bundles there are 2 source maps: "packager" (metro) and "compiler" (Hermes)
23
+ // Metro bundles use <bundleAssetName>.packager.map notation
24
+ const isHermes = await isHermesEnabled(command, command.platform);
25
+ if (isHermes) {
26
+ bundleSourceMapOutput = path.join(sourcemapOutputFolder, bundleName + ".packager.map");
27
+ }
28
+ else {
29
+ bundleSourceMapOutput = path.join(sourcemapOutputFolder, bundleName + ".map");
30
+ }
31
+ break;
32
+ }
33
+ case "ios": {
34
+ // see react-native-xcode.sh
35
+ // to match js bundle generated by Xcode and by Revopush cli we must respect SOURCEMAP_FILE value
36
+ // because it appears as //# sourceMappingURL value in a js bundle
37
+ const xcodeDotEnvValue = getXcodeDotEnvValue("SOURCEMAP_FILE");
38
+ const sourceMapFilename = xcodeDotEnvValue ? path.basename(xcodeDotEnvValue) : bundleName + ".map";
39
+ bundleSourceMapOutput = path.join(sourcemapOutputFolder, sourceMapFilename);
40
+ break;
41
+ }
42
+ default:
43
+ throw new Error('Platform must be either "android", "ios" or "windows".');
44
+ }
45
+ return bundleSourceMapOutput;
46
+ }
47
+ exports.getBundleSourceMapOutput = getBundleSourceMapOutput;
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) {
16
64
  const hermesArgs = [];
17
65
  const envNodeArgs = process.env.CODE_PUSH_NODE_ARGS;
18
66
  if (typeof envNodeArgs !== "undefined") {
@@ -20,14 +68,18 @@ async function runHermesEmitBinaryCommand(bundleName, outputFolder, sourcemapOut
20
68
  }
21
69
  Array.prototype.push.apply(hermesArgs, [
22
70
  "-emit-binary",
71
+ "-O",
23
72
  "-out",
24
73
  path.join(outputFolder, bundleName + ".hbc"),
25
74
  path.join(outputFolder, bundleName),
26
75
  ...extraHermesFlags,
27
76
  ]);
28
- if (sourcemapOutput) {
77
+ if (sourcemapOutputFolder) {
29
78
  hermesArgs.push("-output-source-map");
30
79
  }
80
+ if (baseBytecode) {
81
+ hermesArgs.push("-base-bytecode", baseBytecode);
82
+ }
31
83
  console.log(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n"));
32
84
  const hermesCommand = await getHermesCommand(gradleFile);
33
85
  const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs);
@@ -60,8 +112,8 @@ async function runHermesEmitBinaryCommand(bundleName, outputFolder, sourcemapOut
60
112
  });
61
113
  });
62
114
  });
63
- }).then(() => {
64
- if (!sourcemapOutput) {
115
+ }).then(async () => {
116
+ if (!sourcemapOutputFolder) {
65
117
  // skip source map compose if source map is not enabled
66
118
  return;
67
119
  }
@@ -73,8 +125,32 @@ async function runHermesEmitBinaryCommand(bundleName, outputFolder, sourcemapOut
73
125
  if (!fs.existsSync(jsCompilerSourceMapFile)) {
74
126
  throw new Error(`sourcemap file ${jsCompilerSourceMapFile} is not found`);
75
127
  }
128
+ const platformSourceMapOutput = await getBundleSourceMapOutput(command, bundleName, sourcemapOutputFolder);
76
129
  return new Promise((resolve, reject) => {
77
- const composeSourceMapsArgs = [composeSourceMapsPath, sourcemapOutput, jsCompilerSourceMapFile, "-o", sourcemapOutput];
130
+ let bundleSourceMapOutput = sourcemapOutputFolder;
131
+ let combinedSourceMapOutput = sourcemapOutputFolder;
132
+ if (!sourcemapOutputFolder.endsWith(".map")) {
133
+ bundleSourceMapOutput = platformSourceMapOutput;
134
+ switch (command.platform) {
135
+ case "android": {
136
+ combinedSourceMapOutput = path.join(sourcemapOutputFolder, bundleName + ".map");
137
+ break;
138
+ }
139
+ case "ios": {
140
+ combinedSourceMapOutput = bundleSourceMapOutput;
141
+ break;
142
+ }
143
+ default:
144
+ throw new Error('Platform must be either "android", "ios" or "windows".');
145
+ }
146
+ }
147
+ const composeSourceMapsArgs = [
148
+ composeSourceMapsPath,
149
+ bundleSourceMapOutput,
150
+ jsCompilerSourceMapFile,
151
+ "-o",
152
+ combinedSourceMapOutput,
153
+ ];
78
154
  // https://github.com/facebook/react-native/blob/master/react.gradle#L211
79
155
  // https://github.com/facebook/react-native/blob/master/scripts/react-native-xcode.sh#L178
80
156
  // packager.sourcemap.map + hbc.sourcemap.map = sourcemap.map
@@ -103,6 +179,38 @@ async function runHermesEmitBinaryCommand(bundleName, outputFolder, sourcemapOut
103
179
  });
104
180
  }
105
181
  exports.runHermesEmitBinaryCommand = runHermesEmitBinaryCommand;
182
+ function getXcodeDotEnvValue(key) {
183
+ const xcodeEnvs = loadEnvAsMap([path.join("ios", ".xcode.env.local"), path.join("ios", ".xcode.env.local")]);
184
+ return xcodeEnvs.get(key);
185
+ }
186
+ exports.getXcodeDotEnvValue = getXcodeDotEnvValue;
187
+ async function getMinifyParams(command) {
188
+ const isHermes = await isHermesEnabled(command);
189
+ switch (command.platform) {
190
+ case "android": {
191
+ // android always explicitly pass --minify true/false
192
+ // TaskConfiguration it.minifyEnabled.set(!isHermesEnabledInThisVariant)
193
+ return ["--minify", !isHermes];
194
+ }
195
+ case "ios": {
196
+ //if [[ $USE_HERMES != false && $DEV == false ]]; then
197
+ // EXTRA_ARGS+=("--minify" "false")
198
+ // fi
199
+ // ios does pass --minify false only if Hermes enables and does pass anything otherwise
200
+ return isHermes ? ["--minify", false] : [];
201
+ }
202
+ default:
203
+ throw new Error('Platform must be either "android", "ios" or "windows".');
204
+ }
205
+ }
206
+ exports.getMinifyParams = getMinifyParams;
207
+ async function isHermesEnabled(command, platform = command.platform.toLowerCase()) {
208
+ // 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
209
+ const isAndroidHermesEnabled = await getAndroidHermesEnabled(command.gradleFile);
210
+ const isIOSHermesEnabled = getiOSHermesEnabled(command.podFile);
211
+ return command.useHermes || (platform === "android" && isAndroidHermesEnabled) || (platform === "ios" && isIOSHermesEnabled);
212
+ }
213
+ exports.isHermesEnabled = isHermesEnabled;
106
214
  function parseBuildGradleFile(gradleFile) {
107
215
  let buildGradlePath = path.join("android", "app");
108
216
  if (gradleFile) {
@@ -118,6 +226,45 @@ function parseBuildGradleFile(gradleFile) {
118
226
  throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`);
119
227
  });
120
228
  }
229
+ function parseGradlePropertiesFile(gradleFile) {
230
+ let gradlePropsPath = path.join("android", "gradle.properties");
231
+ try {
232
+ if (gradleFile) {
233
+ const base = gradleFile;
234
+ const stat = fs.lstatSync(base);
235
+ if (stat.isDirectory()) {
236
+ if (path.basename(base) === "app") {
237
+ gradlePropsPath = path.join(base, "..", "gradle.properties");
238
+ }
239
+ else {
240
+ gradlePropsPath = path.join(base, "gradle.properties");
241
+ }
242
+ }
243
+ else {
244
+ gradlePropsPath = path.join(path.dirname(base), "..", "gradle.properties");
245
+ }
246
+ }
247
+ }
248
+ catch { }
249
+ gradlePropsPath = path.normalize(gradlePropsPath);
250
+ if ((0, file_utils_1.fileDoesNotExistOrIsDirectory)(gradlePropsPath)) {
251
+ throw new Error(`Unable to find gradle.properties file "${gradlePropsPath}".`);
252
+ }
253
+ const text = fs.readFileSync(gradlePropsPath, "utf8");
254
+ const props = {};
255
+ for (const rawLine of text.split(/\r?\n/)) {
256
+ const line = rawLine.trim();
257
+ if (!line || line.startsWith("#"))
258
+ continue;
259
+ const m = line.match(/^([^=\s]+)\s*=\s*(.*)$/);
260
+ if (m) {
261
+ const key = m[1].trim();
262
+ const val = m[2].trim();
263
+ props[key] = val;
264
+ }
265
+ }
266
+ return props;
267
+ }
121
268
  async function getHermesCommandFromGradle(gradleFile) {
122
269
  const buildGradle = await parseBuildGradleFile(gradleFile);
123
270
  const hermesCommandProperty = Array.from(buildGradle["project.ext.react"] || []).find((prop) => prop.trim().startsWith("hermesCommand:"));
@@ -128,12 +275,30 @@ async function getHermesCommandFromGradle(gradleFile) {
128
275
  return "";
129
276
  }
130
277
  }
131
- function getAndroidHermesEnabled(gradleFile) {
132
- return parseBuildGradleFile(gradleFile).then((buildGradle) => {
133
- return Array.from(buildGradle["project.ext.react"] || []).some((line) => /^enableHermes\s{0,}:\s{0,}true/.test(line));
134
- });
278
+ async function getAndroidHermesEnabled(gradleFile) {
279
+ try {
280
+ const props = parseGradlePropertiesFile(gradleFile);
281
+ if (typeof props.hermesEnabled !== "undefined") {
282
+ const v = String(props.hermesEnabled).trim().toLowerCase();
283
+ if (v === "true")
284
+ return true;
285
+ if (v === "false")
286
+ return false;
287
+ }
288
+ }
289
+ catch { }
290
+ try {
291
+ const buildGradle = await parseBuildGradleFile(gradleFile);
292
+ const lines = Array.from(buildGradle["project.ext.react"] || []);
293
+ if (lines.some((l) => /\benableHermes\s*:\s*true\b/.test(l)))
294
+ return true;
295
+ if (lines.some((l) => /\benableHermes\s*:\s*false\b/.test(l)))
296
+ return false;
297
+ }
298
+ catch { }
299
+ const rnVersion = (0, semver_1.coerce)(getReactNativeVersion())?.version;
300
+ return rnVersion && (0, semver_1.compare)(rnVersion, "0.70.0") >= 0;
135
301
  }
136
- exports.getAndroidHermesEnabled = getAndroidHermesEnabled;
137
302
  function getiOSHermesEnabled(podFile) {
138
303
  let podPath = path.join("ios", "Podfile");
139
304
  if (podFile) {
@@ -144,13 +309,29 @@ function getiOSHermesEnabled(podFile) {
144
309
  }
145
310
  try {
146
311
  const podFileContents = fs.readFileSync(podPath).toString();
147
- return /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?true)/.test(podFileContents);
312
+ const hasTrue = /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?true)/.test(podFileContents);
313
+ if (hasTrue)
314
+ return true;
315
+ const hasFalse = /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?false)/.test(podFileContents);
316
+ if (hasFalse)
317
+ return false;
318
+ const rnVersion = (0, semver_1.coerce)(getReactNativeVersion())?.version;
319
+ return rnVersion && (0, semver_1.compare)(rnVersion, "0.70.0") >= 0;
148
320
  }
149
321
  catch (error) {
150
322
  throw error;
151
323
  }
152
324
  }
153
- exports.getiOSHermesEnabled = getiOSHermesEnabled;
325
+ function loadEnvAsMap(envPaths = []) {
326
+ const merged = {};
327
+ for (const envPath of envPaths) {
328
+ if (fs.existsSync(envPath)) {
329
+ Object.assign(merged, dotenv.parse(fs.readFileSync(envPath))); // later files override earlier ones
330
+ }
331
+ }
332
+ // fallback to process.env for anything missing
333
+ return new Map([...Object.entries(process.env), ...Object.entries(merged)]);
334
+ }
154
335
  function getHermesOSBin() {
155
336
  switch (process.platform) {
156
337
  case "win32":
@@ -184,7 +365,8 @@ async function getHermesCommand(gradleFile) {
184
365
  }
185
366
  };
186
367
  // Hermes is bundled with react-native since 0.69
187
- const bundledHermesEngine = path.join(getReactNativePackagePath(), "sdks", "hermesc", getHermesOSBin(), getHermesOSExe());
368
+ const reactNativePath = getReactNativePackagePath();
369
+ const bundledHermesEngine = path.join(reactNativePath, "sdks", "hermesc", getHermesOSBin(), getHermesOSExe());
188
370
  if (fileExists(bundledHermesEngine)) {
189
371
  return bundledHermesEngine;
190
372
  }
@@ -193,12 +375,13 @@ async function getHermesCommand(gradleFile) {
193
375
  return path.join("android", "app", gradleHermesCommand.replace("%OS-BIN%", getHermesOSBin()));
194
376
  }
195
377
  else {
378
+ const nodeModulesPath = getNodeModulesPath(reactNativePath);
196
379
  // assume if hermes-engine exists it should be used instead of hermesvm
197
- const hermesEngine = path.join("node_modules", "hermes-engine", getHermesOSBin(), getHermesOSExe());
380
+ const hermesEngine = path.join(nodeModulesPath, "hermes-engine", getHermesOSBin(), getHermesOSExe());
198
381
  if (fileExists(hermesEngine)) {
199
382
  return hermesEngine;
200
383
  }
201
- return path.join("node_modules", "hermesvm", getHermesOSBin(), "hermes");
384
+ return path.join(nodeModulesPath, "hermesvm", getHermesOSBin(), "hermes");
202
385
  }
203
386
  }
204
387
  function getComposeSourceMapsPath() {
@@ -209,6 +392,13 @@ function getComposeSourceMapsPath() {
209
392
  }
210
393
  return null;
211
394
  }
395
+ function getNodeModulesPath(reactNativePath) {
396
+ const nodeModulesPath = path.dirname(reactNativePath);
397
+ if (directoryExistsSync(nodeModulesPath)) {
398
+ return nodeModulesPath;
399
+ }
400
+ return path.join("node_modules");
401
+ }
212
402
  function getReactNativePackagePath() {
213
403
  const result = childProcess.spawnSync("node", ["--print", "require.resolve('react-native/package.json')"]);
214
404
  const packagePath = path.dirname(result.stdout.toString());
@@ -217,6 +407,7 @@ function getReactNativePackagePath() {
217
407
  }
218
408
  return path.join("node_modules", "react-native");
219
409
  }
410
+ exports.getReactNativePackagePath = getReactNativePackagePath;
220
411
  function directoryExistsSync(dirname) {
221
412
  try {
222
413
  return fs.statSync(dirname).isDirectory();
@@ -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.5",
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": {
@@ -26,6 +26,7 @@
26
26
  "backslash": "^0.2.0",
27
27
  "chalk": "^4.1.2",
28
28
  "cli-table": "^0.3.11",
29
+ "dotenv": "^17.2.1",
29
30
  "email-validator": "^2.0.4",
30
31
  "gradle-to-js": "2.0.1",
31
32
  "jsonwebtoken": "^9.0.2",
@@ -44,6 +45,7 @@
44
45
  "slash": "1.0.0",
45
46
  "superagent": "^8.0.9",
46
47
  "temp": "^0.9.4",
48
+ "unzipper": "^0.12.3",
47
49
  "which": "^1.2.7",
48
50
  "wordwrap": "1.0.0",
49
51
  "xcode": "^3.0.1",
@@ -58,6 +60,7 @@
58
60
  "@types/node": "^20.3.1",
59
61
  "@types/q": "^1.5.8",
60
62
  "@types/sinon": "^10.0.15",
63
+ "@types/unzipper": "^0.10.11",
61
64
  "@typescript-eslint/eslint-plugin": "^6.0.0",
62
65
  "@typescript-eslint/parser": "^6.0.0",
63
66
  "eslint": "^8.45.0",
@@ -67,8 +70,8 @@
67
70
  "prettier": "^2.8.8",
68
71
  "sinon": "15.1.2",
69
72
  "superagent-mock": "^4.0.0",
70
- "typescript": "^5.1.3",
71
73
  "ts-node": "^10.9.2",
74
+ "typescript": "^5.1.3",
72
75
  "which": "^3.0.1"
73
76
  }
74
77
  }