@bravemobile/react-native-code-push 13.1.0-beta.3 → 13.1.0-beta.4

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.
package/README.md CHANGED
@@ -351,9 +351,10 @@ Diff archives are published only when all three of these hold:
351
351
  - `--diff-base-count` is greater than `0` (it defaults to `3`).
352
352
 
353
353
  ```ts
354
- bundleDownloader: async (downloadUrl) => {
355
- const downloadedFilePath = path.join(os.tmpdir(), path.basename(new URL(downloadUrl).pathname));
356
- // fetch the archive from your storage (S3, GCS, ...) to downloadedFilePath
354
+ bundleDownloader: async (archive, platform, identifier = 'staging') => {
355
+ const downloadedFilePath = path.join(os.tmpdir(), archive.packageHash);
356
+ const storageKey = `bundles/${platform}/${identifier}/full-bundle/${archive.packageHash}`;
357
+ // fetch storageKey from your storage (S3, Supabase, ...) to downloadedFilePath
357
358
  return { downloadedFilePath };
358
359
  },
359
360
  ```
@@ -389,6 +390,7 @@ const Config: CliConfigInterface = {
389
390
  source: string,
390
391
  platform: "ios" | "android",
391
392
  identifier,
393
+ artifact,
392
394
  ): Promise<{downloadUrl: string}> => {
393
395
  // ...
394
396
  },
@@ -413,7 +415,7 @@ const Config: CliConfigInterface = {
413
415
 
414
416
  // Only needed to publish asset diff archives (see "4-2. Asset Diff Archives").
415
417
  // bundleDownloader: async (
416
- // downloadUrl: string,
418
+ // archive,
417
419
  // platform: "ios" | "android",
418
420
  // identifier,
419
421
  // ): Promise<{downloadedFilePath: string}> => {
@@ -427,6 +429,9 @@ module.exports = Config;
427
429
 
428
430
  **`bundleUploader`**
429
431
  - Implements a function to upload the bundle file.
432
+ - The optional `artifact` argument identifies the full bundle, binary patch, or asset diff
433
+ archive. The `release` command always provides it, including the target binary version and
434
+ the package hashes needed to construct a storage key without parsing a file name.
430
435
  - The `downloadUrl` returned by this function is recorded in `ReleaseHistoryInterface` data
431
436
  and is used by the library runtime to download the bundle file from this URL.
432
437
  - Used in the following cases:
@@ -17,7 +17,7 @@ import {
17
17
  import { applyPatch } from "../../utils/binaryPatch.js";
18
18
  import { generatePackageHashFromDirectory } from "../../utils/hash-utils.js";
19
19
  import { unzip } from "../../utils/unzip.js";
20
- import type { CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
20
+ import type { BundleUploadArtifact, CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
21
21
 
22
22
  /**
23
23
  * Covers the release flow around the binary patch option with `--skip-bundle`, which is
@@ -92,15 +92,23 @@ class ProcessExitError extends Error {
92
92
  }
93
93
  }
94
94
 
95
- type Uploads = { filePath: string, downloadUrl: string, logCountBeforeUpload: number }[];
96
-
97
- function recordingUploader(uploads: Uploads, failOn?: (filePath: string) => boolean) {
98
- return async (filePath: string) => {
95
+ type Uploads = {
96
+ artifact: BundleUploadArtifact | undefined;
97
+ downloadUrl: string;
98
+ filePath: string;
99
+ logCountBeforeUpload: number;
100
+ }[];
101
+
102
+ function recordingUploader(
103
+ uploads: Uploads,
104
+ failOn?: (filePath: string) => boolean,
105
+ ): CliConfigInterface['bundleUploader'] {
106
+ return async (filePath, _platform, _identifier, artifact) => {
99
107
  if (failOn?.(filePath)) {
100
108
  throw new Error(`upload rejected: ${path.basename(filePath)}`);
101
109
  }
102
110
  const downloadUrl = `https://cdn.example.com/${path.basename(filePath)}`;
103
- uploads.push({ filePath, downloadUrl, logCountBeforeUpload: logs.length });
111
+ uploads.push({ artifact, filePath, downloadUrl, logCountBeforeUpload: logs.length });
104
112
  return { downloadUrl };
105
113
  };
106
114
  }
@@ -204,6 +212,17 @@ afterEach(() => {
204
212
  });
205
213
 
206
214
  describe("release without --binary-bundle-path", () => {
215
+ it("keeps existing three-argument uploaders callable", async () => {
216
+ const legacyUploader = async (
217
+ source: string,
218
+ _platform: 'ios' | 'android',
219
+ _identifier?: string,
220
+ ) => ({ downloadUrl: source });
221
+ const uploader: CliConfigInterface['bundleUploader'] = legacyUploader;
222
+
223
+ await expect(uploader('bundle.zip', 'ios')).resolves.toEqual({ downloadUrl: 'bundle.zip' });
224
+ });
225
+
207
226
  it("uploads the full bundle only, exactly as before binary patches existed", async () => {
208
227
  const staged = await stageBundleOutput("full-only");
209
228
 
@@ -211,6 +230,11 @@ describe("release without --binary-bundle-path", () => {
211
230
 
212
231
  expect(uploads).toHaveLength(1);
213
232
  expect(uploads[0].filePath).toBe(`${staged.bundleDirectory}/${staged.bundleFileName}`);
233
+ expect(uploads[0].artifact).toEqual({
234
+ type: 'full-bundle',
235
+ targetBinaryVersion: BINARY_VERSION,
236
+ packageHash: staged.bundleFileName,
237
+ });
214
238
  expect(fs.readdirSync(staged.bundleDirectory)).toEqual([staged.bundleFileName]);
215
239
  expect(releaseHistories[0][APP_VERSION]).toEqual({
216
240
  enabled: true,
@@ -658,11 +682,11 @@ describe("release with asset diff bases", () => {
658
682
 
659
683
  /** Serves the staged base archives, and records which releases were asked for. */
660
684
  function recordingDownloader(bases: StagedBaseRelease[], downloads: string[]): CliConfigInterface['bundleDownloader'] {
661
- return async (downloadUrl: string) => {
662
- downloads.push(downloadUrl);
663
- const base = bases.find((candidate) => candidate.downloadUrl === downloadUrl);
685
+ return async (archive) => {
686
+ downloads.push(archive.downloadUrl);
687
+ const base = bases.find((candidate) => candidate.downloadUrl === archive.downloadUrl);
664
688
  if (!base) {
665
- throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
689
+ throw new Error(`the downloader was called with an unexpected url: ${archive.downloadUrl}`);
666
690
  }
667
691
  return { downloadedFilePath: base.archivePath };
668
692
  };
@@ -687,6 +711,24 @@ describe("release with asset diff bases", () => {
687
711
  `${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
688
712
  diffFileName,
689
713
  ]);
714
+ expect(uploads.map(({ artifact }) => artifact)).toEqual([
715
+ {
716
+ type: 'full-bundle',
717
+ targetBinaryVersion: BINARY_VERSION,
718
+ packageHash: staged.bundleFileName,
719
+ },
720
+ {
721
+ type: 'binary-patch',
722
+ targetBinaryVersion: BINARY_VERSION,
723
+ packageHash: staged.bundleFileName,
724
+ },
725
+ {
726
+ type: 'asset-diff',
727
+ targetBinaryVersion: BINARY_VERSION,
728
+ packageHash: staged.bundleFileName,
729
+ basePackageHash: base.packageHash,
730
+ },
731
+ ]);
690
732
  expect(downloads).toEqual([base.downloadUrl]);
691
733
  // Every artifact is uploaded before the history points at any of them.
692
734
  expect(uploadCountsWhenHistorySaved).toEqual([3]);
@@ -2,7 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { bundleCodePush, resolveJsBundleName } from "../bundleCommand/bundleCodePush.js";
4
4
  import { addToReleaseHistory } from "./addToReleaseHistory.js";
5
- import type { CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
5
+ import type { BundleUploadArtifact, CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
6
6
  import { generatePackageHashFromDirectory } from "../../utils/hash-utils.js";
7
7
  import { unzip } from "../../utils/unzip.js";
8
8
  import { ASSET_DIFF_ARCHIVE_INFIX, makeAssetDiffBundle } from "../../functions/makeAssetDiffBundle.js";
@@ -95,6 +95,7 @@ export async function release(
95
95
  bundleDownloader,
96
96
  bundleDirectory,
97
97
  packageHash,
98
+ targetBinaryVersion: binaryVersion,
98
99
  platform,
99
100
  identifier,
100
101
  })
@@ -102,10 +103,32 @@ export async function release(
102
103
 
103
104
  // Every artifact is uploaded before the release history is touched, so a failed
104
105
  // upload leaves the history describing only updates that can actually be downloaded.
105
- const downloadUrl = await uploadArtifact(bundleUploader, bundleFilePath, platform, identifier, 'bundle');
106
+ const downloadUrl = await uploadArtifact(
107
+ bundleUploader,
108
+ bundleFilePath,
109
+ platform,
110
+ identifier,
111
+ {
112
+ type: 'full-bundle',
113
+ targetBinaryVersion: binaryVersion,
114
+ packageHash,
115
+ },
116
+ 'bundle',
117
+ );
106
118
  let patchDownloadUrl: string | undefined;
107
119
  if (binaryPatch) {
108
- patchDownloadUrl = await uploadArtifact(bundleUploader, binaryPatch.patchBundleFilePath, platform, identifier, 'binary patch bundle');
120
+ patchDownloadUrl = await uploadArtifact(
121
+ bundleUploader,
122
+ binaryPatch.patchBundleFilePath,
123
+ platform,
124
+ identifier,
125
+ {
126
+ type: 'binary-patch',
127
+ targetBinaryVersion: binaryVersion,
128
+ packageHash,
129
+ },
130
+ 'binary patch bundle',
131
+ );
109
132
  console.log(`log: Binary patch archive uploaded (download url: ${patchDownloadUrl})`);
110
133
  }
111
134
  const diffPackages: Record<string, string> = {};
@@ -114,7 +137,12 @@ export async function release(
114
137
  // one that cannot be uploaded is left out of the history instead of failing the
115
138
  // release the other two artifacts are ready for.
116
139
  try {
117
- const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier);
140
+ const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier, {
141
+ type: 'asset-diff',
142
+ targetBinaryVersion: binaryVersion,
143
+ packageHash,
144
+ basePackageHash,
145
+ });
118
146
  diffPackages[basePackageHash] = diffDownloadUrl;
119
147
  console.log(`log: Asset diff archive against ${basePackageHash} uploaded (download url: ${diffDownloadUrl})`);
120
148
  } catch (error) {
@@ -289,6 +317,7 @@ async function makeAssetDiffArtifacts({
289
317
  bundleDownloader,
290
318
  bundleDirectory,
291
319
  packageHash,
320
+ targetBinaryVersion,
292
321
  platform,
293
322
  identifier,
294
323
  }: {
@@ -298,6 +327,7 @@ async function makeAssetDiffArtifacts({
298
327
  bundleDownloader: NonNullable<CliConfigInterface['bundleDownloader']>;
299
328
  bundleDirectory: string;
300
329
  packageHash: string;
330
+ targetBinaryVersion: string;
301
331
  platform: 'ios' | 'android';
302
332
  identifier: string | undefined;
303
333
  }): Promise<AssetDiffArtifact[]> {
@@ -307,6 +337,7 @@ async function makeAssetDiffArtifacts({
307
337
  bundleDownloader,
308
338
  platform,
309
339
  identifier,
340
+ targetBinaryVersion,
310
341
  });
311
342
 
312
343
  const artifacts: AssetDiffArtifact[] = [];
@@ -421,10 +452,11 @@ async function uploadArtifact(
421
452
  filePath: string,
422
453
  platform: 'ios' | 'android',
423
454
  identifier: string | undefined,
455
+ artifact: BundleUploadArtifact,
424
456
  artifactName: string,
425
457
  ): Promise<string> {
426
458
  try {
427
- return await uploadFile(bundleUploader, filePath, platform, identifier);
459
+ return await uploadFile(bundleUploader, filePath, platform, identifier, artifact);
428
460
  } catch (error) {
429
461
  console.error(`Failed to upload the ${artifactName} file. Exiting the program.\n`, error)
430
462
  process.exit(1)
@@ -436,7 +468,8 @@ async function uploadFile(
436
468
  filePath: string,
437
469
  platform: 'ios' | 'android',
438
470
  identifier: string | undefined,
471
+ artifact: BundleUploadArtifact,
439
472
  ): Promise<string> {
440
- const { downloadUrl } = await bundleUploader(filePath, platform, identifier);
473
+ const { downloadUrl } = await bundleUploader(filePath, platform, identifier, artifact);
441
474
  return downloadUrl
442
475
  }
@@ -51,16 +51,25 @@ export async function release(bundleUploader, getReleaseHistory, setReleaseHisto
51
51
  bundleDownloader,
52
52
  bundleDirectory,
53
53
  packageHash,
54
+ targetBinaryVersion: binaryVersion,
54
55
  platform,
55
56
  identifier,
56
57
  })
57
58
  : [];
58
59
  // Every artifact is uploaded before the release history is touched, so a failed
59
60
  // upload leaves the history describing only updates that can actually be downloaded.
60
- const downloadUrl = await uploadArtifact(bundleUploader, bundleFilePath, platform, identifier, 'bundle');
61
+ const downloadUrl = await uploadArtifact(bundleUploader, bundleFilePath, platform, identifier, {
62
+ type: 'full-bundle',
63
+ targetBinaryVersion: binaryVersion,
64
+ packageHash,
65
+ }, 'bundle');
61
66
  let patchDownloadUrl;
62
67
  if (binaryPatch) {
63
- patchDownloadUrl = await uploadArtifact(bundleUploader, binaryPatch.patchBundleFilePath, platform, identifier, 'binary patch bundle');
68
+ patchDownloadUrl = await uploadArtifact(bundleUploader, binaryPatch.patchBundleFilePath, platform, identifier, {
69
+ type: 'binary-patch',
70
+ targetBinaryVersion: binaryVersion,
71
+ packageHash,
72
+ }, 'binary patch bundle');
64
73
  console.log(`log: Binary patch archive uploaded (download url: ${patchDownloadUrl})`);
65
74
  }
66
75
  const diffPackages = {};
@@ -69,7 +78,12 @@ export async function release(bundleUploader, getReleaseHistory, setReleaseHisto
69
78
  // one that cannot be uploaded is left out of the history instead of failing the
70
79
  // release the other two artifacts are ready for.
71
80
  try {
72
- const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier);
81
+ const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier, {
82
+ type: 'asset-diff',
83
+ targetBinaryVersion: binaryVersion,
84
+ packageHash,
85
+ basePackageHash,
86
+ });
73
87
  diffPackages[basePackageHash] = diffDownloadUrl;
74
88
  console.log(`log: Asset diff archive against ${basePackageHash} uploaded (download url: ${diffDownloadUrl})`);
75
89
  }
@@ -180,13 +194,14 @@ async function makeBinaryPatchArtifact({ baseBundlePath, contentsPath, bundleFil
180
194
  *
181
195
  * @return {Promise<AssetDiffArtifact[]>} The diff archives to upload, each naming the package it was built against.
182
196
  */
183
- async function makeAssetDiffArtifacts({ patchBundleFilePath, releaseHistory, diffBaseCount, bundleDownloader, bundleDirectory, packageHash, platform, identifier, }) {
197
+ async function makeAssetDiffArtifacts({ patchBundleFilePath, releaseHistory, diffBaseCount, bundleDownloader, bundleDirectory, packageHash, targetBinaryVersion, platform, identifier, }) {
184
198
  const bases = await resolveAssetDiffBases({
185
199
  releaseHistory,
186
200
  diffBaseCount,
187
201
  bundleDownloader,
188
202
  platform,
189
203
  identifier,
204
+ targetBinaryVersion,
190
205
  });
191
206
  const artifacts = [];
192
207
  const builtAgainst = new Set();
@@ -278,16 +293,16 @@ function warnOnBaseBundleMismatch(outputPath, baseBundlePath) {
278
293
  }
279
294
  }
280
295
  /** Uploads an artifact the release cannot go out without, so a failure ends the program. */
281
- async function uploadArtifact(bundleUploader, filePath, platform, identifier, artifactName) {
296
+ async function uploadArtifact(bundleUploader, filePath, platform, identifier, artifact, artifactName) {
282
297
  try {
283
- return await uploadFile(bundleUploader, filePath, platform, identifier);
298
+ return await uploadFile(bundleUploader, filePath, platform, identifier, artifact);
284
299
  }
285
300
  catch (error) {
286
301
  console.error(`Failed to upload the ${artifactName} file. Exiting the program.\n`, error);
287
302
  process.exit(1);
288
303
  }
289
304
  }
290
- async function uploadFile(bundleUploader, filePath, platform, identifier) {
291
- const { downloadUrl } = await bundleUploader(filePath, platform, identifier);
305
+ async function uploadFile(bundleUploader, filePath, platform, identifier, artifact) {
306
+ const { downloadUrl } = await bundleUploader(filePath, platform, identifier, artifact);
292
307
  return downloadUrl;
293
308
  }
@@ -66,12 +66,12 @@ class ProcessExitError extends Error {
66
66
  }
67
67
  }
68
68
  function recordingUploader(uploads, failOn) {
69
- return async (filePath) => {
69
+ return async (filePath, _platform, _identifier, artifact) => {
70
70
  if (failOn?.(filePath)) {
71
71
  throw new Error(`upload rejected: ${path.basename(filePath)}`);
72
72
  }
73
73
  const downloadUrl = `https://cdn.example.com/${path.basename(filePath)}`;
74
- uploads.push({ filePath, downloadUrl, logCountBeforeUpload: logs.length });
74
+ uploads.push({ artifact, filePath, downloadUrl, logCountBeforeUpload: logs.length });
75
75
  return { downloadUrl };
76
76
  };
77
77
  }
@@ -124,11 +124,21 @@ afterEach(() => {
124
124
  fs.rmSync(path.resolve(process.cwd(), `${BINARY_VERSION}.json`), { force: true });
125
125
  });
126
126
  describe("release without --binary-bundle-path", () => {
127
+ it("keeps existing three-argument uploaders callable", async () => {
128
+ const legacyUploader = async (source, _platform, _identifier) => ({ downloadUrl: source });
129
+ const uploader = legacyUploader;
130
+ await expect(uploader('bundle.zip', 'ios')).resolves.toEqual({ downloadUrl: 'bundle.zip' });
131
+ });
127
132
  it("uploads the full bundle only, exactly as before binary patches existed", async () => {
128
133
  const staged = await stageBundleOutput("full-only");
129
134
  const { uploads, releaseHistories } = await runRelease(staged);
130
135
  expect(uploads).toHaveLength(1);
131
136
  expect(uploads[0].filePath).toBe(`${staged.bundleDirectory}/${staged.bundleFileName}`);
137
+ expect(uploads[0].artifact).toEqual({
138
+ type: 'full-bundle',
139
+ targetBinaryVersion: BINARY_VERSION,
140
+ packageHash: staged.bundleFileName,
141
+ });
132
142
  expect(fs.readdirSync(staged.bundleDirectory)).toEqual([staged.bundleFileName]);
133
143
  expect(releaseHistories[0][APP_VERSION]).toEqual({
134
144
  enabled: true,
@@ -426,11 +436,11 @@ describe("release with asset diff bases", () => {
426
436
  }
427
437
  /** Serves the staged base archives, and records which releases were asked for. */
428
438
  function recordingDownloader(bases, downloads) {
429
- return async (downloadUrl) => {
430
- downloads.push(downloadUrl);
431
- const base = bases.find((candidate) => candidate.downloadUrl === downloadUrl);
439
+ return async (archive) => {
440
+ downloads.push(archive.downloadUrl);
441
+ const base = bases.find((candidate) => candidate.downloadUrl === archive.downloadUrl);
432
442
  if (!base) {
433
- throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
443
+ throw new Error(`the downloader was called with an unexpected url: ${archive.downloadUrl}`);
434
444
  }
435
445
  return { downloadedFilePath: base.archivePath };
436
446
  };
@@ -452,6 +462,24 @@ describe("release with asset diff bases", () => {
452
462
  `${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
453
463
  diffFileName,
454
464
  ]);
465
+ expect(uploads.map(({ artifact }) => artifact)).toEqual([
466
+ {
467
+ type: 'full-bundle',
468
+ targetBinaryVersion: BINARY_VERSION,
469
+ packageHash: staged.bundleFileName,
470
+ },
471
+ {
472
+ type: 'binary-patch',
473
+ targetBinaryVersion: BINARY_VERSION,
474
+ packageHash: staged.bundleFileName,
475
+ },
476
+ {
477
+ type: 'asset-diff',
478
+ targetBinaryVersion: BINARY_VERSION,
479
+ packageHash: staged.bundleFileName,
480
+ basePackageHash: base.packageHash,
481
+ },
482
+ ]);
455
483
  expect(downloads).toEqual([base.downloadUrl]);
456
484
  // Every artifact is uploaded before the history points at any of them.
457
485
  expect(uploadCountsWhenHistorySaved).toEqual([3]);
@@ -25,7 +25,7 @@ const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
25
25
  * @param bundleDownloader Consumer-provided downloader for a released archive
26
26
  * @return The bases that downloaded and verified, newest release first.
27
27
  */
28
- export async function resolveAssetDiffBases({ releaseHistory, diffBaseCount, bundleDownloader, platform, identifier, }) {
28
+ export async function resolveAssetDiffBases({ releaseHistory, diffBaseCount, bundleDownloader, platform, identifier, targetBinaryVersion, }) {
29
29
  // The seeded entry for the update inside the app binary has neither a url nor a hash,
30
30
  // and nothing can be diffed against a package that was never published. A key that is
31
31
  // not a version cannot be ordered against the others either, and ordering is what picks
@@ -37,7 +37,12 @@ export async function resolveAssetDiffBases({ releaseHistory, diffBaseCount, bun
37
37
  const bases = [];
38
38
  for (const [version, releaseInfo] of candidates) {
39
39
  try {
40
- const { downloadedFilePath } = await bundleDownloader(releaseInfo.downloadUrl, platform, identifier);
40
+ const { downloadedFilePath } = await bundleDownloader({
41
+ downloadUrl: releaseInfo.downloadUrl,
42
+ targetBinaryVersion,
43
+ releaseVersion: version,
44
+ packageHash: releaseInfo.packageHash,
45
+ }, platform, identifier);
41
46
  if (!(await matchesPackageHash(downloadedFilePath, releaseInfo.packageHash))) {
42
47
  console.warn(`warn: Skipping the asset diff base v${version}: the downloaded archive does not hash to the ` +
43
48
  `package hash recorded for it (${releaseInfo.packageHash}).`);
@@ -13,6 +13,7 @@ import { resolveAssetDiffBases } from "./resolveAssetDiffBases.js";
13
13
  const CONTENTS_DIR_NAME = 'CodePush';
14
14
  /** Where the resolver unpacks a base for verification, so leftovers are recognizable. */
15
15
  const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
16
+ const TARGET_BINARY_VERSION = '1.0.0';
16
17
  let workDir;
17
18
  let bundleDirectory;
18
19
  /** Releases one full archive the way the CLI does, and returns what the history records. */
@@ -49,16 +50,16 @@ function releaseHistoryOf(releases) {
49
50
  /** Stands in for the consumer's downloader: hands back a local archive per download url. */
50
51
  function fakeDownloader(archives) {
51
52
  const calls = [];
52
- const download = async (downloadUrl, platform, identifier) => {
53
- calls.push([downloadUrl, platform, identifier]);
54
- const archive = archives[downloadUrl];
55
- if (archive instanceof Error) {
56
- throw archive;
53
+ const download = async (downloadInfo, platform, identifier) => {
54
+ calls.push([downloadInfo, platform, identifier]);
55
+ const downloadedArchive = archives[downloadInfo.downloadUrl];
56
+ if (downloadedArchive instanceof Error) {
57
+ throw downloadedArchive;
57
58
  }
58
- if (!archive) {
59
- throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
59
+ if (!downloadedArchive) {
60
+ throw new Error(`the downloader was called with an unexpected url: ${downloadInfo.downloadUrl}`);
60
61
  }
61
- return { downloadedFilePath: archive };
62
+ return { downloadedFilePath: downloadedArchive };
62
63
  };
63
64
  return { download, calls };
64
65
  }
@@ -86,14 +87,15 @@ describe("resolveAssetDiffBases", () => {
86
87
  bundleDownloader: download,
87
88
  platform: 'android',
88
89
  identifier: 'my-app',
90
+ targetBinaryVersion: TARGET_BINARY_VERSION,
89
91
  });
90
92
  expect(bases).toEqual([
91
93
  { basePackageHash: newest.packageHash, baseBundleFilePath: newest.archivePath },
92
94
  { basePackageHash: second.packageHash, baseBundleFilePath: second.archivePath },
93
95
  ]);
94
96
  expect(calls).toEqual([
95
- [newest.downloadUrl, 'android', 'my-app'],
96
- [second.downloadUrl, 'android', 'my-app'],
97
+ [{ downloadUrl: newest.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: newest.version, packageHash: newest.packageHash }, 'android', 'my-app'],
98
+ [{ downloadUrl: second.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: second.version, packageHash: second.packageHash }, 'android', 'my-app'],
97
99
  ]);
98
100
  });
99
101
  it("skips history entries that lack a download url or a package hash", async () => {
@@ -109,11 +111,16 @@ describe("resolveAssetDiffBases", () => {
109
111
  diffBaseCount: 1,
110
112
  bundleDownloader: download,
111
113
  platform: 'ios',
114
+ targetBinaryVersion: TARGET_BINARY_VERSION,
112
115
  });
113
116
  expect(bases).toEqual([
114
117
  { basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
115
118
  ]);
116
- expect(calls).toEqual([[released.downloadUrl, 'ios', undefined]]);
119
+ expect(calls).toEqual([[
120
+ { downloadUrl: released.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: released.version, packageHash: released.packageHash },
121
+ 'ios',
122
+ undefined,
123
+ ]]);
117
124
  });
118
125
  it("ignores a history entry whose key is not a version, and resolves the rest", async () => {
119
126
  const [released] = await stageReleases(['1.4.0']);
@@ -131,11 +138,16 @@ describe("resolveAssetDiffBases", () => {
131
138
  diffBaseCount: 2,
132
139
  bundleDownloader: download,
133
140
  platform: 'ios',
141
+ targetBinaryVersion: TARGET_BINARY_VERSION,
134
142
  });
135
143
  expect(bases).toEqual([
136
144
  { basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
137
145
  ]);
138
- expect(calls).toEqual([[released.downloadUrl, 'ios', undefined]]);
146
+ expect(calls).toEqual([[
147
+ { downloadUrl: released.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: released.version, packageHash: released.packageHash },
148
+ 'ios',
149
+ undefined,
150
+ ]]);
139
151
  });
140
152
  it("drops a base whose downloaded bytes do not match its recorded package hash", async () => {
141
153
  const [older, newer] = await stageReleases(['1.2.0', '1.2.1']);
@@ -150,6 +162,7 @@ describe("resolveAssetDiffBases", () => {
150
162
  diffBaseCount: 2,
151
163
  bundleDownloader: download,
152
164
  platform: 'android',
165
+ targetBinaryVersion: TARGET_BINARY_VERSION,
153
166
  });
154
167
  expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
155
168
  expect(warn.mock.calls.join('\n')).toMatch(/1\.2\.1/);
@@ -167,6 +180,7 @@ describe("resolveAssetDiffBases", () => {
167
180
  diffBaseCount: 2,
168
181
  bundleDownloader: download,
169
182
  platform: 'android',
183
+ targetBinaryVersion: TARGET_BINARY_VERSION,
170
184
  });
171
185
  expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
172
186
  expect(warn.mock.calls.join('\n')).toMatch(/1\.3\.1.*the network is down/s);
@@ -4,7 +4,7 @@ import path from "path";
4
4
  import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
5
5
  import { makeCodePushBundle } from "./makeCodePushBundle.js";
6
6
  import { resolveAssetDiffBases } from "./resolveAssetDiffBases.js";
7
- import type { CliConfigInterface, ReleaseHistoryInterface } from "../../typings/react-native-code-push.d.ts";
7
+ import type { BundleDownloadInfo, CliConfigInterface, ReleaseHistoryInterface } from "../../typings/react-native-code-push.d.ts";
8
8
 
9
9
  /**
10
10
  * Exercises base acquisition against real archives and real hashes: a base is only usable
@@ -17,6 +17,7 @@ const CONTENTS_DIR_NAME = 'CodePush';
17
17
 
18
18
  /** Where the resolver unpacks a base for verification, so leftovers are recognizable. */
19
19
  const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
20
+ const TARGET_BINARY_VERSION = '1.0.0';
20
21
 
21
22
  type StagedRelease = {
22
23
  version: string;
@@ -68,22 +69,22 @@ function releaseHistoryOf(releases: StagedRelease[]): ReleaseHistoryInterface {
68
69
 
69
70
  /** Stands in for the consumer's downloader: hands back a local archive per download url. */
70
71
  function fakeDownloader(archives: Record<string, string | Error>) {
71
- const calls: Array<[string, string, string | undefined]> = [];
72
+ const calls: Array<[BundleDownloadInfo, string, string | undefined]> = [];
72
73
  const download: NonNullable<CliConfigInterface['bundleDownloader']> = async (
73
- downloadUrl,
74
+ downloadInfo,
74
75
  platform,
75
76
  identifier,
76
77
  ) => {
77
- calls.push([downloadUrl, platform, identifier]);
78
+ calls.push([downloadInfo, platform, identifier]);
78
79
 
79
- const archive = archives[downloadUrl];
80
- if (archive instanceof Error) {
81
- throw archive;
80
+ const downloadedArchive = archives[downloadInfo.downloadUrl];
81
+ if (downloadedArchive instanceof Error) {
82
+ throw downloadedArchive;
82
83
  }
83
- if (!archive) {
84
- throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
84
+ if (!downloadedArchive) {
85
+ throw new Error(`the downloader was called with an unexpected url: ${downloadInfo.downloadUrl}`);
85
86
  }
86
- return { downloadedFilePath: archive };
87
+ return { downloadedFilePath: downloadedArchive };
87
88
  };
88
89
  return { download, calls };
89
90
  }
@@ -119,6 +120,7 @@ describe("resolveAssetDiffBases", () => {
119
120
  bundleDownloader: download,
120
121
  platform: 'android',
121
122
  identifier: 'my-app',
123
+ targetBinaryVersion: TARGET_BINARY_VERSION,
122
124
  });
123
125
 
124
126
  expect(bases).toEqual([
@@ -126,8 +128,8 @@ describe("resolveAssetDiffBases", () => {
126
128
  { basePackageHash: second.packageHash, baseBundleFilePath: second.archivePath },
127
129
  ]);
128
130
  expect(calls).toEqual([
129
- [newest.downloadUrl, 'android', 'my-app'],
130
- [second.downloadUrl, 'android', 'my-app'],
131
+ [{ downloadUrl: newest.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: newest.version, packageHash: newest.packageHash }, 'android', 'my-app'],
132
+ [{ downloadUrl: second.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: second.version, packageHash: second.packageHash }, 'android', 'my-app'],
131
133
  ]);
132
134
  });
133
135
 
@@ -145,12 +147,17 @@ describe("resolveAssetDiffBases", () => {
145
147
  diffBaseCount: 1,
146
148
  bundleDownloader: download,
147
149
  platform: 'ios',
150
+ targetBinaryVersion: TARGET_BINARY_VERSION,
148
151
  });
149
152
 
150
153
  expect(bases).toEqual([
151
154
  { basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
152
155
  ]);
153
- expect(calls).toEqual([[released.downloadUrl, 'ios', undefined]]);
156
+ expect(calls).toEqual([[
157
+ { downloadUrl: released.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: released.version, packageHash: released.packageHash },
158
+ 'ios',
159
+ undefined,
160
+ ]]);
154
161
  });
155
162
 
156
163
  it("ignores a history entry whose key is not a version, and resolves the rest", async () => {
@@ -170,12 +177,17 @@ describe("resolveAssetDiffBases", () => {
170
177
  diffBaseCount: 2,
171
178
  bundleDownloader: download,
172
179
  platform: 'ios',
180
+ targetBinaryVersion: TARGET_BINARY_VERSION,
173
181
  });
174
182
 
175
183
  expect(bases).toEqual([
176
184
  { basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
177
185
  ]);
178
- expect(calls).toEqual([[released.downloadUrl, 'ios', undefined]]);
186
+ expect(calls).toEqual([[
187
+ { downloadUrl: released.downloadUrl, targetBinaryVersion: TARGET_BINARY_VERSION, releaseVersion: released.version, packageHash: released.packageHash },
188
+ 'ios',
189
+ undefined,
190
+ ]]);
179
191
  });
180
192
 
181
193
  it("drops a base whose downloaded bytes do not match its recorded package hash", async () => {
@@ -192,6 +204,7 @@ describe("resolveAssetDiffBases", () => {
192
204
  diffBaseCount: 2,
193
205
  bundleDownloader: download,
194
206
  platform: 'android',
207
+ targetBinaryVersion: TARGET_BINARY_VERSION,
195
208
  });
196
209
 
197
210
  expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
@@ -212,6 +225,7 @@ describe("resolveAssetDiffBases", () => {
212
225
  diffBaseCount: 2,
213
226
  bundleDownloader: download,
214
227
  platform: 'android',
228
+ targetBinaryVersion: TARGET_BINARY_VERSION,
215
229
  });
216
230
 
217
231
  expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
@@ -40,12 +40,14 @@ export async function resolveAssetDiffBases({
40
40
  bundleDownloader,
41
41
  platform,
42
42
  identifier,
43
+ targetBinaryVersion,
43
44
  }: {
44
45
  releaseHistory: ReleaseHistoryInterface;
45
46
  diffBaseCount: number;
46
47
  bundleDownloader: NonNullable<CliConfigInterface['bundleDownloader']>;
47
48
  platform: 'ios' | 'android';
48
49
  identifier?: string;
50
+ targetBinaryVersion: string;
49
51
  }): Promise<AssetDiffBase[]> {
50
52
  // The seeded entry for the update inside the app binary has neither a url nor a hash,
51
53
  // and nothing can be diffed against a package that was never published. A key that is
@@ -60,7 +62,12 @@ export async function resolveAssetDiffBases({
60
62
  const bases: AssetDiffBase[] = [];
61
63
  for (const [version, releaseInfo] of candidates) {
62
64
  try {
63
- const { downloadedFilePath } = await bundleDownloader(releaseInfo.downloadUrl, platform, identifier);
65
+ const { downloadedFilePath } = await bundleDownloader({
66
+ downloadUrl: releaseInfo.downloadUrl,
67
+ targetBinaryVersion,
68
+ releaseVersion: version,
69
+ packageHash: releaseInfo.packageHash,
70
+ }, platform, identifier);
64
71
 
65
72
  if (!(await matchesPackageHash(downloadedFilePath, releaseInfo.packageHash))) {
66
73
  console.warn(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bravemobile/react-native-code-push",
3
- "version": "13.1.0-beta.3",
3
+ "version": "13.1.0-beta.4",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "src/CodePush.js",
6
6
  "react-native": "src/CodePush.js",
@@ -633,6 +633,31 @@ declare namespace CodePush {
633
633
 
634
634
  export default CodePush;
635
635
 
636
+ /**
637
+ * Identifies the archive passed to `bundleUploader` during a release.
638
+ *
639
+ * `targetBinaryVersion` is the value of the release command's `--binary-version` option.
640
+ * A full bundle's `packageHash` identifies its contents across target binary versions, while
641
+ * binary patch and asset diff archives need the target binary version to distinguish their contents.
642
+ * Asset diff archives also identify the released package they were built against.
643
+ */
644
+ export type BundleUploadArtifact = {
645
+ targetBinaryVersion: string;
646
+ packageHash: string;
647
+ } & (
648
+ | { type: 'full-bundle' }
649
+ | { type: 'binary-patch' }
650
+ | { type: 'asset-diff'; basePackageHash: string }
651
+ );
652
+
653
+ /** Identifies the previously released full archive passed to `bundleDownloader`. */
654
+ export interface BundleDownloadInfo {
655
+ downloadUrl: string;
656
+ targetBinaryVersion: string;
657
+ releaseVersion: ReleaseVersion;
658
+ packageHash: string;
659
+ }
660
+
636
661
  /**
637
662
  * Interface for the config file required for `npx code-push` CLI operation.
638
663
  *
@@ -647,26 +672,16 @@ export interface CliConfigInterface {
647
672
  *
648
673
  * @param source The relative path of the generated bundle file. (e.g. build/bundleOutput/1087bc338fc45a961c...)
649
674
  * @param platform The target platform of the bundle file. This is the string passed when executing the CLI command. ('ios'/'android')
675
+ * @param artifact Information that identifies the full bundle, binary patch, or asset diff archive. The release command always supplies it; it remains optional so existing callers and three-parameter implementations stay compatible.
650
676
  * @return {Promise<{downloadUrl: string}>} An object containing the `downloadUrl` property, which is the URL from which the uploaded bundle file can be downloaded. This URL will be recorded in the release history.
651
677
  */
652
678
  bundleUploader: (
653
679
  source: string,
654
680
  platform: "ios" | "android",
655
681
  identifier?: string,
682
+ artifact?: BundleUploadArtifact,
656
683
  ) => Promise<{downloadUrl: string}>;
657
684
 
658
- /**
659
- * Downloads a previously released update archive so the release command can compute
660
- * asset diffs against it. Receives the `downloadUrl` recorded in the release history and
661
- * must resolve with the local path of the downloaded file.
662
- * Optional: when absent, releases are published without asset diff archives.
663
- */
664
- bundleDownloader?: (
665
- downloadUrl: string,
666
- platform: "ios" | "android",
667
- identifier?: string,
668
- ) => Promise<{ downloadedFilePath: string }>;
669
-
670
685
  /**
671
686
  * Interface that must be implemented to retrieve ReleaseHistory information.
672
687
  *
@@ -702,4 +717,19 @@ export interface CliConfigInterface {
702
717
  platform: "ios" | "android",
703
718
  identifier?: string,
704
719
  ) => Promise<void>;
720
+
721
+ /**
722
+ * Downloads a previously released update archive so the release command can compute
723
+ * asset diffs against it. Receives the `downloadUrl` recorded in the release history and
724
+ * must resolve with the local path of the downloaded file.
725
+ * Optional: when absent, releases are published without asset diff archives.
726
+ *
727
+ * @param archive Information that identifies the released full archive. The release command
728
+ * supplies its download URL, target binary version, release version, and package hash together.
729
+ */
730
+ bundleDownloader?: (
731
+ archive: BundleDownloadInfo,
732
+ platform: "ios" | "android",
733
+ identifier?: string,
734
+ ) => Promise<{ downloadedFilePath: string }>;
705
735
  }