@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.
@@ -3,7 +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
+ import * as dotenv from "dotenv";
8
+ import { DotenvParseOutput } from "dotenv";
9
+ import * as cli from "../script/types/cli";
10
+ import { log, sdk } from "./command-executor";
7
11
 
8
12
  const g2js = require("gradle-to-js/lib/parser");
9
13
 
@@ -11,12 +15,70 @@ export function isValidVersion(version: string): boolean {
11
15
  return !!valid(version) || /^\d+\.\d+$/.test(version);
12
16
  }
13
17
 
18
+ export async function getBundleSourceMapOutput(command: cli.IReleaseReactCommand, bundleName: string, sourcemapOutputFolder: string) {
19
+ let bundleSourceMapOutput: string | undefined;
20
+ switch (command.platform) {
21
+ case "android": {
22
+ // see BundleHermesCTask -> resolvePackagerSourceMapFile
23
+ // for Hermes targeted bundles there are 2 source maps: "packager" (metro) and "compiler" (Hermes)
24
+ // Metro bundles use <bundleAssetName>.packager.map notation
25
+ const isHermes = await isHermesEnabled(command, command.platform);
26
+ if (isHermes) {
27
+ bundleSourceMapOutput = path.join(sourcemapOutputFolder, bundleName + ".packager.map");
28
+ } else {
29
+ bundleSourceMapOutput = path.join(sourcemapOutputFolder, bundleName + ".map");
30
+ }
31
+
32
+ break;
33
+ }
34
+ case "ios": {
35
+ // see react-native-xcode.sh
36
+ // to match js bundle generated by Xcode and by Revopush cli we must respect SOURCEMAP_FILE value
37
+ // because it appears as //# sourceMappingURL value in a js bundle
38
+ const xcodeDotEnvValue = getXcodeDotEnvValue("SOURCEMAP_FILE");
39
+ const sourceMapFilename = xcodeDotEnvValue ? path.basename(xcodeDotEnvValue) : bundleName + ".map";
40
+
41
+ bundleSourceMapOutput = path.join(sourcemapOutputFolder, sourceMapFilename);
42
+
43
+ break;
44
+ }
45
+ default:
46
+ throw new Error('Platform must be either "android", "ios" or "windows".');
47
+ }
48
+ return bundleSourceMapOutput;
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
+
14
74
  export async function runHermesEmitBinaryCommand(
15
- bundleName: string,
16
- outputFolder: string,
17
- sourcemapOutput: string,
18
- extraHermesFlags: string[],
19
- gradleFile: string
75
+ command: cli.IReleaseReactCommand,
76
+ bundleName: string,
77
+ outputFolder: string,
78
+ sourcemapOutputFolder: string,
79
+ extraHermesFlags: string[],
80
+ gradleFile: string,
81
+ baseBytecode?: string
20
82
  ): Promise<void> {
21
83
  const hermesArgs: string[] = [];
22
84
  const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS;
@@ -27,16 +89,21 @@ export async function runHermesEmitBinaryCommand(
27
89
 
28
90
  Array.prototype.push.apply(hermesArgs, [
29
91
  "-emit-binary",
92
+ "-O",
30
93
  "-out",
31
94
  path.join(outputFolder, bundleName + ".hbc"),
32
95
  path.join(outputFolder, bundleName),
33
96
  ...extraHermesFlags,
34
97
  ]);
35
98
 
36
- if (sourcemapOutput) {
99
+ if (sourcemapOutputFolder) {
37
100
  hermesArgs.push("-output-source-map");
38
101
  }
39
102
 
103
+ if (baseBytecode) {
104
+ hermesArgs.push("-base-bytecode", baseBytecode);
105
+ }
106
+
40
107
  console.log(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n"));
41
108
  const hermesCommand = await getHermesCommand(gradleFile);
42
109
  const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs);
@@ -72,8 +139,8 @@ export async function runHermesEmitBinaryCommand(
72
139
  });
73
140
  });
74
141
  });
75
- }).then(() => {
76
- if (!sourcemapOutput) {
142
+ }).then(async () => {
143
+ if (!sourcemapOutputFolder) {
77
144
  // skip source map compose if source map is not enabled
78
145
  return;
79
146
  }
@@ -88,8 +155,33 @@ export async function runHermesEmitBinaryCommand(
88
155
  throw new Error(`sourcemap file ${jsCompilerSourceMapFile} is not found`);
89
156
  }
90
157
 
158
+ const platformSourceMapOutput = await getBundleSourceMapOutput(command, bundleName, sourcemapOutputFolder);
91
159
  return new Promise((resolve, reject) => {
92
- const composeSourceMapsArgs = [composeSourceMapsPath, sourcemapOutput, jsCompilerSourceMapFile, "-o", sourcemapOutput];
160
+ let bundleSourceMapOutput = sourcemapOutputFolder;
161
+ let combinedSourceMapOutput = sourcemapOutputFolder;
162
+
163
+ if (!sourcemapOutputFolder.endsWith(".map")) {
164
+ bundleSourceMapOutput = platformSourceMapOutput;
165
+ switch (command.platform) {
166
+ case "android": {
167
+ combinedSourceMapOutput = path.join(sourcemapOutputFolder, bundleName + ".map");
168
+ break;
169
+ }
170
+ case "ios": {
171
+ combinedSourceMapOutput = bundleSourceMapOutput;
172
+ break;
173
+ }
174
+ default:
175
+ throw new Error('Platform must be either "android", "ios" or "windows".');
176
+ }
177
+ }
178
+ const composeSourceMapsArgs = [
179
+ composeSourceMapsPath,
180
+ bundleSourceMapOutput,
181
+ jsCompilerSourceMapFile,
182
+ "-o",
183
+ combinedSourceMapOutput,
184
+ ];
93
185
 
94
186
  // https://github.com/facebook/react-native/blob/master/react.gradle#L211
95
187
  // https://github.com/facebook/react-native/blob/master/scripts/react-native-xcode.sh#L178
@@ -124,6 +216,40 @@ export async function runHermesEmitBinaryCommand(
124
216
  });
125
217
  }
126
218
 
219
+ export function getXcodeDotEnvValue(key: string): string | undefined {
220
+ const xcodeEnvs = loadEnvAsMap([path.join("ios", ".xcode.env.local"), path.join("ios", ".xcode.env.local")]);
221
+ return xcodeEnvs.get(key);
222
+ }
223
+
224
+ export async function getMinifyParams(command: cli.IReleaseReactCommand) {
225
+ const isHermes = await isHermesEnabled(command);
226
+
227
+ switch (command.platform) {
228
+ case "android": {
229
+ // android always explicitly pass --minify true/false
230
+ // TaskConfiguration it.minifyEnabled.set(!isHermesEnabledInThisVariant)
231
+ return ["--minify", !isHermes];
232
+ }
233
+ case "ios": {
234
+ //if [[ $USE_HERMES != false && $DEV == false ]]; then
235
+ // EXTRA_ARGS+=("--minify" "false")
236
+ // fi
237
+ // ios does pass --minify false only if Hermes enables and does pass anything otherwise
238
+ return isHermes ? ["--minify", false] : [];
239
+ }
240
+ default:
241
+ throw new Error('Platform must be either "android", "ios" or "windows".');
242
+ }
243
+ }
244
+
245
+ export async function isHermesEnabled(command: cli.IReleaseReactCommand, platform: string = command.platform.toLowerCase()) {
246
+ // 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
247
+ const isAndroidHermesEnabled = await getAndroidHermesEnabled(command.gradleFile);
248
+ const isIOSHermesEnabled = getiOSHermesEnabled(command.podFile);
249
+
250
+ return command.useHermes || (platform === "android" && isAndroidHermesEnabled) || (platform === "ios" && isIOSHermesEnabled);
251
+ }
252
+
127
253
  function parseBuildGradleFile(gradleFile: string) {
128
254
  let buildGradlePath: string = path.join("android", "app");
129
255
  if (gradleFile) {
@@ -142,6 +268,47 @@ function parseBuildGradleFile(gradleFile: string) {
142
268
  });
143
269
  }
144
270
 
271
+ function parseGradlePropertiesFile(gradleFile: string): Record<string, string> {
272
+ let gradlePropsPath: string = path.join("android", "gradle.properties");
273
+
274
+ try {
275
+ if (gradleFile) {
276
+ const base = gradleFile;
277
+ const stat = fs.lstatSync(base);
278
+
279
+ if (stat.isDirectory()) {
280
+ if (path.basename(base) === "app") {
281
+ gradlePropsPath = path.join(base, "..", "gradle.properties");
282
+ } else {
283
+ gradlePropsPath = path.join(base, "gradle.properties");
284
+ }
285
+ } else {
286
+ gradlePropsPath = path.join(path.dirname(base), "..", "gradle.properties");
287
+ }
288
+ }
289
+ } catch {}
290
+
291
+ gradlePropsPath = path.normalize(gradlePropsPath);
292
+
293
+ if (fileDoesNotExistOrIsDirectory(gradlePropsPath)) {
294
+ throw new Error(`Unable to find gradle.properties file "${gradlePropsPath}".`);
295
+ }
296
+
297
+ const text = fs.readFileSync(gradlePropsPath, "utf8");
298
+ const props: Record<string, string> = {};
299
+ for (const rawLine of text.split(/\r?\n/)) {
300
+ const line = rawLine.trim();
301
+ if (!line || line.startsWith("#")) continue;
302
+ const m = line.match(/^([^=\s]+)\s*=\s*(.*)$/);
303
+ if (m) {
304
+ const key = m[1].trim();
305
+ const val = m[2].trim();
306
+ props[key] = val;
307
+ }
308
+ }
309
+ return props;
310
+ }
311
+
145
312
  async function getHermesCommandFromGradle(gradleFile: string): Promise<string> {
146
313
  const buildGradle: any = await parseBuildGradleFile(gradleFile);
147
314
  const hermesCommandProperty: any = Array.from(buildGradle["project.ext.react"] || []).find((prop: string) =>
@@ -154,13 +321,28 @@ async function getHermesCommandFromGradle(gradleFile: string): Promise<string> {
154
321
  }
155
322
  }
156
323
 
157
- export function getAndroidHermesEnabled(gradleFile: string): boolean {
158
- return parseBuildGradleFile(gradleFile).then((buildGradle: any) => {
159
- return Array.from(buildGradle["project.ext.react"] || []).some((line: string) => /^enableHermes\s{0,}:\s{0,}true/.test(line));
160
- });
324
+ async function getAndroidHermesEnabled(gradleFile: string): Promise<boolean> {
325
+ try {
326
+ const props = parseGradlePropertiesFile(gradleFile);
327
+ if (typeof props.hermesEnabled !== "undefined") {
328
+ const v = String(props.hermesEnabled).trim().toLowerCase();
329
+ if (v === "true") return true;
330
+ if (v === "false") return false;
331
+ }
332
+ } catch {}
333
+
334
+ try {
335
+ const buildGradle: any = await parseBuildGradleFile(gradleFile);
336
+ const lines: string[] = Array.from(buildGradle["project.ext.react"] || []);
337
+ if (lines.some((l) => /\benableHermes\s*:\s*true\b/.test(l))) return true;
338
+ if (lines.some((l) => /\benableHermes\s*:\s*false\b/.test(l))) return false;
339
+ } catch {}
340
+
341
+ const rnVersion = coerce(getReactNativeVersion())?.version;
342
+ return rnVersion && compare(rnVersion, "0.70.0") >= 0;
161
343
  }
162
344
 
163
- export function getiOSHermesEnabled(podFile: string): boolean {
345
+ function getiOSHermesEnabled(podFile: string): boolean {
164
346
  let podPath = path.join("ios", "Podfile");
165
347
  if (podFile) {
166
348
  podPath = podFile;
@@ -171,12 +353,33 @@ export function getiOSHermesEnabled(podFile: string): boolean {
171
353
 
172
354
  try {
173
355
  const podFileContents = fs.readFileSync(podPath).toString();
174
- return /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?true)/.test(podFileContents);
356
+
357
+ const hasTrue = /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?true)/.test(podFileContents);
358
+ if (hasTrue) return true;
359
+
360
+ const hasFalse = /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?false)/.test(podFileContents);
361
+ if (hasFalse) return false;
362
+
363
+ const rnVersion = coerce(getReactNativeVersion())?.version;
364
+ return rnVersion && compare(rnVersion, "0.70.0") >= 0;
175
365
  } catch (error) {
176
366
  throw error;
177
367
  }
178
368
  }
179
369
 
370
+ function loadEnvAsMap(envPaths = []): Map<string, string | undefined> {
371
+ const merged: DotenvParseOutput = {};
372
+
373
+ for (const envPath of envPaths) {
374
+ if (fs.existsSync(envPath)) {
375
+ Object.assign(merged, dotenv.parse(fs.readFileSync(envPath))); // later files override earlier ones
376
+ }
377
+ }
378
+
379
+ // fallback to process.env for anything missing
380
+ return new Map([...Object.entries(process.env), ...Object.entries(merged)]);
381
+ }
382
+
180
383
  function getHermesOSBin(): string {
181
384
  switch (process.platform) {
182
385
  case "win32":
@@ -211,7 +414,8 @@ async function getHermesCommand(gradleFile: string): Promise<string> {
211
414
  }
212
415
  };
213
416
  // Hermes is bundled with react-native since 0.69
214
- const bundledHermesEngine = path.join(getReactNativePackagePath(), "sdks", "hermesc", getHermesOSBin(), getHermesOSExe());
417
+ const reactNativePath = getReactNativePackagePath();
418
+ const bundledHermesEngine = path.join(reactNativePath, "sdks", "hermesc", getHermesOSBin(), getHermesOSExe());
215
419
  if (fileExists(bundledHermesEngine)) {
216
420
  return bundledHermesEngine;
217
421
  }
@@ -220,12 +424,14 @@ async function getHermesCommand(gradleFile: string): Promise<string> {
220
424
  if (gradleHermesCommand) {
221
425
  return path.join("android", "app", gradleHermesCommand.replace("%OS-BIN%", getHermesOSBin()));
222
426
  } else {
427
+ const nodeModulesPath = getNodeModulesPath(reactNativePath);
428
+
223
429
  // assume if hermes-engine exists it should be used instead of hermesvm
224
- const hermesEngine = path.join("node_modules", "hermes-engine", getHermesOSBin(), getHermesOSExe());
430
+ const hermesEngine = path.join(nodeModulesPath, "hermes-engine", getHermesOSBin(), getHermesOSExe());
225
431
  if (fileExists(hermesEngine)) {
226
432
  return hermesEngine;
227
433
  }
228
- return path.join("node_modules", "hermesvm", getHermesOSBin(), "hermes");
434
+ return path.join(nodeModulesPath, "hermesvm", getHermesOSBin(), "hermes");
229
435
  }
230
436
  }
231
437
 
@@ -238,7 +444,16 @@ function getComposeSourceMapsPath(): string {
238
444
  return null;
239
445
  }
240
446
 
241
- function getReactNativePackagePath(): string {
447
+ function getNodeModulesPath(reactNativePath: string): string {
448
+ const nodeModulesPath = path.dirname(reactNativePath);
449
+ if (directoryExistsSync(nodeModulesPath)) {
450
+ return nodeModulesPath;
451
+ }
452
+
453
+ return path.join("node_modules");
454
+ }
455
+
456
+ export function getReactNativePackagePath(): string {
242
457
  const result = childProcess.spawnSync("node", ["--print", "require.resolve('react-native/package.json')"]);
243
458
  const packagePath = path.dirname(result.stdout.toString());
244
459
  if (result.status === 0 && directoryExistsSync(packagePath)) {
@@ -280,4 +495,4 @@ export function getReactNativeVersion(): string {
280
495
  (projectPackageJson.dependencies && projectPackageJson.dependencies["react-native"]) ||
281
496
  (projectPackageJson.devDependencies && projectPackageJson.devDependencies["react-native"])
282
497
  );
283
- }
498
+ }
@@ -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