@bravemobile/react-native-code-push 12.4.1 → 13.1.0-beta.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.
Files changed (111) hide show
  1. package/CodePush.podspec +47 -2
  2. package/README.md +118 -3
  3. package/THIRD-PARTY-NOTICES +96 -0
  4. package/android/app/build.gradle +26 -0
  5. package/android/app/proguard-rules.pro +5 -0
  6. package/android/app/src/main/cpp/CMakeLists.txt +38 -0
  7. package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +46 -11
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
  12. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
  13. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +79 -88
  14. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +159 -4
  15. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
  16. package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
  17. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
  18. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +207 -0
  19. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +449 -0
  20. package/android/codepush-export.gradle +115 -0
  21. package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
  22. package/cli/commands/bundleCommand/index.ts +9 -1
  23. package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
  24. package/cli/commands/releaseCommand/index.test.ts +118 -0
  25. package/cli/commands/releaseCommand/index.ts +20 -2
  26. package/cli/commands/releaseCommand/release.test.ts +582 -0
  27. package/cli/commands/releaseCommand/release.ts +232 -15
  28. package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
  29. package/cli/dist/commands/bundleCommand/index.js +5 -1
  30. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
  31. package/cli/dist/commands/releaseCommand/index.js +8 -1
  32. package/cli/dist/commands/releaseCommand/index.test.js +96 -0
  33. package/cli/dist/commands/releaseCommand/release.js +165 -17
  34. package/cli/dist/commands/releaseCommand/release.test.js +367 -0
  35. package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
  36. package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
  37. package/cli/dist/functions/makeCodePushBundle.js +4 -2
  38. package/cli/dist/functions/runHermesEmitBinaryCommand.js +98 -27
  39. package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
  40. package/cli/dist/jest.globalSetup.js +38 -0
  41. package/cli/dist/utils/binaryPatch.js +86 -0
  42. package/cli/dist/utils/binaryPatch.test.js +190 -0
  43. package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
  44. package/cli/functions/makeBinaryPatchBundle.ts +367 -0
  45. package/cli/functions/makeCodePushBundle.ts +4 -2
  46. package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
  47. package/cli/functions/runHermesEmitBinaryCommand.ts +112 -26
  48. package/cli/jest.globalSetup.ts +43 -0
  49. package/cli/utils/binaryPatch.test.ts +234 -0
  50. package/cli/utils/binaryPatch.ts +103 -0
  51. package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
  52. package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
  53. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
  54. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
  55. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
  56. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
  57. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
  58. package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
  59. package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
  60. package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
  61. package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
  62. package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
  63. package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
  64. package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
  65. package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
  66. package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
  67. package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
  68. package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
  69. package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
  70. package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
  71. package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
  72. package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
  73. package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
  74. package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
  75. package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
  76. package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
  77. package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
  78. package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
  79. package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
  80. package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
  81. package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
  82. package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
  83. package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
  84. package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
  85. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
  86. package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
  87. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
  88. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
  89. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
  90. package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
  91. package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
  92. package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
  93. package/ios/CodePush/CodePush.h +58 -9
  94. package/ios/CodePush/{CodePush.m → CodePush.mm} +77 -63
  95. package/ios/CodePush/CodePushBinaryPatch.h +80 -0
  96. package/ios/CodePush/CodePushBinaryPatch.m +473 -0
  97. package/ios/CodePush/CodePushPackage.m +183 -2
  98. package/ios/CodePush/CodePushUpdateUtils.m +49 -1
  99. package/ios/CodePush.xcodeproj/project.pbxproj +6 -6
  100. package/package.json +28 -4
  101. package/react-native.config.js +3 -1
  102. package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
  103. package/scripts/export-embedded-bundle.sh +76 -0
  104. package/src/CodePush.js +190 -96
  105. package/src/CodePush.test.js +382 -0
  106. package/src/native/NativeCodePush.ts +162 -0
  107. package/src/package-mixins.js +24 -11
  108. package/src/specs/NativeCodePush.ts +43 -0
  109. package/typings/react-native-code-push.d.ts +77 -0
  110. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushDialog.java +0 -102
  111. package/src/AlertAdapter.js +0 -24
@@ -0,0 +1,410 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
6
+ import {
7
+ BINARY_PATCH_ARCHIVE_SUFFIX,
8
+ BINARY_PATCH_BASE_RECORD_NAME,
9
+ BINARY_PATCH_MANIFEST_NAME,
10
+ extractCodePushBundleContents,
11
+ formatBinaryPatchSummary,
12
+ isPatchArchiveOversized,
13
+ makeBinaryPatchBundle,
14
+ readBinaryPatchBaseRecord,
15
+ resolveBaseBundlePath,
16
+ writeBinaryPatchBaseRecord,
17
+ } from "./makeBinaryPatchBundle.js";
18
+ import { makeCodePushBundle } from "./makeCodePushBundle.js";
19
+ import { applyPatch, BINARY_PATCH_ALGORITHM, BINARY_PATCH_FORMAT_VERSION } from "../utils/binaryPatch.js";
20
+ import { generatePackageHashFromDirectory } from "../utils/hash-utils.js";
21
+ import { unzip } from "../utils/unzip.js";
22
+
23
+ /**
24
+ * Exercises the patch artifact against real archives, real hdiffz output and real
25
+ * hashes: the artifact only has value if a client can rebuild the exact contents of
26
+ * the full archive from it, and only real bytes prove that.
27
+ */
28
+
29
+ /** The directory a CodePush archive keeps its contents in, for legacy reasons. */
30
+ const CONTENTS_DIR_NAME = 'CodePush';
31
+
32
+ function findRepoRoot(): string {
33
+ let dir = process.cwd();
34
+ for (;;) {
35
+ if (fs.existsSync(path.join(dir, "scripts", "binary-patch", "build-hdiffpatch.sh"))) {
36
+ return dir;
37
+ }
38
+ const parent = path.dirname(dir);
39
+ if (parent === dir) {
40
+ throw new Error(`cannot locate the repository root from ${process.cwd()}`);
41
+ }
42
+ dir = parent;
43
+ }
44
+ }
45
+
46
+ const repoRoot = findRepoRoot();
47
+ const fixtureDir = path.join(repoRoot, "cli", "fixtures", "binary-patch");
48
+ const baseFixture = path.join(fixtureDir, "base.bundle");
49
+ const targetFixture = path.join(fixtureDir, "target.bundle");
50
+
51
+ let workDir: string;
52
+
53
+ function sha256(filePath: string): string {
54
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
55
+ }
56
+
57
+ /** A unique directory per test so a failed run never leaks state into the next one. */
58
+ function makeCaseDir(name: string): string {
59
+ const caseDir = fs.mkdtempSync(path.join(workDir, `${name}-`));
60
+ return caseDir;
61
+ }
62
+
63
+ /**
64
+ * Writes an update contents directory shaped like the one the bundler produces: the
65
+ * JS bundle next to the assets that ship with it.
66
+ */
67
+ function writeUpdateContents(caseDir: string, files: Record<string, Buffer | string>): string {
68
+ const contentsPath = path.join(caseDir, CONTENTS_DIR_NAME);
69
+ for (const [relativePath, content] of Object.entries(files)) {
70
+ const filePath = path.join(contentsPath, relativePath);
71
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
72
+ fs.writeFileSync(filePath, content);
73
+ }
74
+ return contentsPath;
75
+ }
76
+
77
+ function defaultContents(caseDir: string, bundleName = 'main.jsbundle'): string {
78
+ return writeUpdateContents(caseDir, {
79
+ [bundleName]: fs.readFileSync(targetFixture),
80
+ 'assets/logo.png': Buffer.from('logo-bytes'),
81
+ });
82
+ }
83
+
84
+ async function unzipTo(archivePath: string, destination: string): Promise<string> {
85
+ fs.mkdirSync(destination, { recursive: true });
86
+ await unzip(archivePath, destination);
87
+ return path.join(destination, CONTENTS_DIR_NAME);
88
+ }
89
+
90
+ beforeAll(() => {
91
+ // hdiffz/hpatchz are provisioned for the whole run by the jest global setup.
92
+ workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-patch-bundle-"));
93
+ });
94
+
95
+ afterAll(() => {
96
+ fs.rmSync(workDir, { recursive: true, force: true });
97
+ });
98
+
99
+ afterEach(() => {
100
+ jest.restoreAllMocks();
101
+ });
102
+
103
+ describe("resolveBaseBundlePath", () => {
104
+ it("returns an absolute path for an existing file", () => {
105
+ expect(resolveBaseBundlePath(path.relative(process.cwd(), baseFixture))).toBe(baseFixture);
106
+ });
107
+
108
+ it("fails with a message naming the option when the file does not exist", () => {
109
+ const missing = path.join(makeCaseDir("missing-base"), "no-such.bundle");
110
+
111
+ expect(() => resolveBaseBundlePath(missing)).toThrow(/--binary-bundle-path/);
112
+ expect(() => resolveBaseBundlePath(missing)).toThrow(/does not exist/);
113
+ });
114
+
115
+ it("fails when the path is a directory", () => {
116
+ expect(() => resolveBaseBundlePath(fixtureDir)).toThrow(/is not a file/);
117
+ });
118
+ });
119
+
120
+ describe("makeBinaryPatchBundle", () => {
121
+ it("writes a patch archive next to the full archive, named after the package hash", async () => {
122
+ const caseDir = makeCaseDir("names");
123
+ const contentsPath = defaultContents(caseDir);
124
+ const bundleDirectory = path.join(caseDir, "bundleOutput");
125
+ const { bundleFileName: packageHash } = await makeCodePushBundle(contentsPath, bundleDirectory);
126
+
127
+ const { patchBundleFilePath } = await makeBinaryPatchBundle({
128
+ contentsPath,
129
+ baseBundlePath: baseFixture,
130
+ bundleRelativePath: 'main.jsbundle',
131
+ bundleDirectory,
132
+ packageHash,
133
+ });
134
+
135
+ expect(path.basename(patchBundleFilePath)).toBe(`${packageHash}${BINARY_PATCH_ARCHIVE_SUFFIX}`);
136
+ expect(fs.existsSync(patchBundleFilePath)).toBe(true);
137
+ // The full archive stays exactly as it was uploaded before patches existed.
138
+ expect(fs.readdirSync(bundleDirectory).sort()).toEqual(
139
+ [packageHash, `${packageHash}${BINARY_PATCH_ARCHIVE_SUFFIX}`].sort(),
140
+ );
141
+ });
142
+
143
+ it("pins the manifest to the codec contract and to the target bundle it describes", async () => {
144
+ const caseDir = makeCaseDir("manifest");
145
+ const contentsPath = defaultContents(caseDir);
146
+ const bundleDirectory = path.join(caseDir, "bundleOutput");
147
+
148
+ const { patchBundleFilePath, manifest } = await makeBinaryPatchBundle({
149
+ contentsPath,
150
+ baseBundlePath: baseFixture,
151
+ bundleRelativePath: 'main.jsbundle',
152
+ bundleDirectory,
153
+ packageHash: 'package-hash',
154
+ });
155
+
156
+ expect(manifest).toEqual({
157
+ formatVersion: BINARY_PATCH_FORMAT_VERSION,
158
+ algorithm: BINARY_PATCH_ALGORITHM,
159
+ bundlePath: 'main.jsbundle',
160
+ patchFile: 'main.jsbundle.patch',
161
+ baseBundleHash: sha256(baseFixture),
162
+ targetBundleHash: sha256(targetFixture),
163
+ targetBundleSize: fs.statSync(targetFixture).size,
164
+ });
165
+
166
+ const extracted = await unzipTo(patchBundleFilePath, path.join(caseDir, "extracted"));
167
+ expect(JSON.parse(fs.readFileSync(path.join(extracted, BINARY_PATCH_MANIFEST_NAME), 'utf8'))).toEqual(manifest);
168
+ });
169
+
170
+ it("replaces the target bundle with its patch and keeps every other file", async () => {
171
+ const caseDir = makeCaseDir("layout");
172
+ const contentsPath = defaultContents(caseDir);
173
+ const bundleDirectory = path.join(caseDir, "bundleOutput");
174
+
175
+ const { patchBundleFilePath } = await makeBinaryPatchBundle({
176
+ contentsPath,
177
+ baseBundlePath: baseFixture,
178
+ bundleRelativePath: 'main.jsbundle',
179
+ bundleDirectory,
180
+ packageHash: 'package-hash',
181
+ });
182
+
183
+ const extracted = await unzipTo(patchBundleFilePath, path.join(caseDir, "extracted"));
184
+ expect(fs.existsSync(path.join(extracted, 'main.jsbundle'))).toBe(false);
185
+ expect(fs.existsSync(path.join(extracted, 'main.jsbundle.patch'))).toBe(true);
186
+ expect(fs.readFileSync(path.join(extracted, 'assets/logo.png'), 'utf8')).toBe('logo-bytes');
187
+ });
188
+
189
+ it("restores the exact package hash of the full archive from the patch archive", async () => {
190
+ const caseDir = makeCaseDir("round-trip");
191
+ const contentsPath = defaultContents(caseDir);
192
+ const bundleDirectory = path.join(caseDir, "bundleOutput");
193
+ const { bundleFileName: packageHash } = await makeCodePushBundle(contentsPath, bundleDirectory);
194
+
195
+ const { patchBundleFilePath, manifest } = await makeBinaryPatchBundle({
196
+ contentsPath,
197
+ baseBundlePath: baseFixture,
198
+ bundleRelativePath: 'main.jsbundle',
199
+ bundleDirectory,
200
+ packageHash,
201
+ });
202
+
203
+ // Replay what a client does: apply the patch onto the bundle from the binary,
204
+ // then drop the patch artifacts that are not part of the update contents.
205
+ const extractRoot = path.join(caseDir, "extracted");
206
+ const extracted = await unzipTo(patchBundleFilePath, extractRoot);
207
+ applyPatch(baseFixture, path.join(extracted, manifest.patchFile), path.join(extracted, manifest.bundlePath));
208
+ fs.rmSync(path.join(extracted, manifest.patchFile));
209
+ fs.rmSync(path.join(extracted, BINARY_PATCH_MANIFEST_NAME));
210
+
211
+ expect(sha256(path.join(extracted, manifest.bundlePath))).toBe(sha256(targetFixture));
212
+ expect(await generatePackageHashFromDirectory(extracted, extractRoot)).toBe(packageHash);
213
+ });
214
+
215
+ it("patches only the bundle named by the caller, leaving another platform's bundle untouched", async () => {
216
+ const caseDir = makeCaseDir("android");
217
+ const contentsPath = writeUpdateContents(caseDir, {
218
+ 'index.android.bundle': fs.readFileSync(targetFixture),
219
+ 'main.jsbundle': Buffer.from('ios-bundle-bytes'),
220
+ });
221
+ const bundleDirectory = path.join(caseDir, "bundleOutput");
222
+
223
+ const { patchBundleFilePath, manifest } = await makeBinaryPatchBundle({
224
+ contentsPath,
225
+ baseBundlePath: baseFixture,
226
+ bundleRelativePath: 'index.android.bundle',
227
+ bundleDirectory,
228
+ packageHash: 'package-hash',
229
+ });
230
+
231
+ expect(manifest.bundlePath).toBe('index.android.bundle');
232
+ expect(manifest.patchFile).toBe('index.android.bundle.patch');
233
+
234
+ const extracted = await unzipTo(patchBundleFilePath, path.join(caseDir, "extracted"));
235
+ expect(fs.existsSync(path.join(extracted, 'index.android.bundle'))).toBe(false);
236
+ expect(fs.readFileSync(path.join(extracted, 'main.jsbundle'), 'utf8')).toBe('ios-bundle-bytes');
237
+ });
238
+
239
+ it("warns when the base and the target bundle are the same bytes", async () => {
240
+ const caseDir = makeCaseDir("identical");
241
+ const contentsPath = writeUpdateContents(caseDir, { 'main.jsbundle': fs.readFileSync(baseFixture) });
242
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
243
+
244
+ await makeBinaryPatchBundle({
245
+ contentsPath,
246
+ baseBundlePath: baseFixture,
247
+ bundleRelativePath: 'main.jsbundle',
248
+ bundleDirectory: path.join(caseDir, "bundleOutput"),
249
+ packageHash: 'package-hash',
250
+ });
251
+
252
+ expect(warn.mock.calls.join('\n')).toMatch(/identical/);
253
+ });
254
+
255
+ it("fails with an actionable error when the target bundle is not in the contents", async () => {
256
+ const caseDir = makeCaseDir("no-target");
257
+ const contentsPath = writeUpdateContents(caseDir, { 'index.android.bundle': Buffer.from('android') });
258
+
259
+ await expect(
260
+ makeBinaryPatchBundle({
261
+ contentsPath,
262
+ baseBundlePath: baseFixture,
263
+ bundleRelativePath: 'main.jsbundle',
264
+ bundleDirectory: path.join(caseDir, "bundleOutput"),
265
+ packageHash: 'package-hash',
266
+ }),
267
+ ).rejects.toThrow(/main\.jsbundle.*--js-bundle-name/s);
268
+ });
269
+
270
+ it("leaves no working directory behind, whether it succeeds or fails", async () => {
271
+ const caseDir = makeCaseDir("cleanup");
272
+ const contentsPath = defaultContents(caseDir);
273
+ const bundleDirectory = path.join(caseDir, "bundleOutput");
274
+ const entriesBefore = fs.readdirSync(caseDir).sort();
275
+
276
+ await makeBinaryPatchBundle({
277
+ contentsPath,
278
+ baseBundlePath: baseFixture,
279
+ bundleRelativePath: 'main.jsbundle',
280
+ bundleDirectory,
281
+ packageHash: 'package-hash',
282
+ });
283
+
284
+ expect(fs.readdirSync(caseDir).sort()).toEqual([...entriesBefore, 'bundleOutput'].sort());
285
+
286
+ await expect(
287
+ makeBinaryPatchBundle({
288
+ contentsPath,
289
+ baseBundlePath: path.join(caseDir, 'no-such-base.bundle'),
290
+ bundleRelativePath: 'main.jsbundle',
291
+ bundleDirectory,
292
+ packageHash: 'package-hash',
293
+ }),
294
+ ).rejects.toThrow();
295
+
296
+ expect(fs.readdirSync(caseDir).sort()).toEqual([...entriesBefore, 'bundleOutput'].sort());
297
+ });
298
+ });
299
+
300
+ describe("binary patch base record", () => {
301
+ it("is written outside the update contents, so it cannot change the package hash", async () => {
302
+ const caseDir = makeCaseDir("record");
303
+ const contentsPath = defaultContents(caseDir);
304
+ const hashBefore = await generatePackageHashFromDirectory(contentsPath, caseDir);
305
+
306
+ const recordPath = writeBinaryPatchBaseRecord(caseDir, sha256(baseFixture));
307
+
308
+ expect(path.relative(contentsPath, recordPath).startsWith('..')).toBe(true);
309
+ expect(path.basename(recordPath)).toBe(BINARY_PATCH_BASE_RECORD_NAME);
310
+ expect(await generatePackageHashFromDirectory(contentsPath, caseDir)).toBe(hashBefore);
311
+ expect(readBinaryPatchBaseRecord(caseDir)).toEqual({ baseBundleHash: sha256(baseFixture) });
312
+ });
313
+
314
+ it("reads as absent when there is no record or the record is unreadable", () => {
315
+ const caseDir = makeCaseDir("record-missing");
316
+
317
+ expect(readBinaryPatchBaseRecord(caseDir)).toBeNull();
318
+
319
+ fs.writeFileSync(path.join(caseDir, BINARY_PATCH_BASE_RECORD_NAME), 'not json');
320
+ expect(readBinaryPatchBaseRecord(caseDir)).toBeNull();
321
+ });
322
+ });
323
+
324
+ describe("extractCodePushBundleContents", () => {
325
+ it("unpacks the contents of an already built bundle file so a patch can be built from it", async () => {
326
+ const caseDir = makeCaseDir("extract");
327
+ const contentsPath = defaultContents(caseDir);
328
+ const bundleDirectory = path.join(caseDir, "bundleOutput");
329
+ const { bundleFileName } = await makeCodePushBundle(contentsPath, bundleDirectory);
330
+
331
+ const { extractDir, contentsPath: extractedContents } = await extractCodePushBundleContents(
332
+ path.join(bundleDirectory, bundleFileName),
333
+ caseDir,
334
+ );
335
+
336
+ expect(await generatePackageHashFromDirectory(extractedContents, path.dirname(extractedContents))).toBe(bundleFileName);
337
+ expect(fs.existsSync(path.join(extractedContents, 'main.jsbundle'))).toBe(true);
338
+
339
+ fs.rmSync(extractDir, { recursive: true, force: true });
340
+ });
341
+ });
342
+
343
+ describe("isPatchArchiveOversized", () => {
344
+ it("keeps a patch that is smaller than the full archive", () => {
345
+ expect(isPatchArchiveOversized(100, 99)).toBe(false);
346
+ });
347
+
348
+ it("rejects a patch of exactly the same size, which saves a client nothing", () => {
349
+ expect(isPatchArchiveOversized(100, 100)).toBe(true);
350
+ });
351
+
352
+ it("rejects a patch that is larger than the full archive", () => {
353
+ expect(isPatchArchiveOversized(100, 101)).toBe(true);
354
+ });
355
+ });
356
+
357
+ describe("formatBinaryPatchSummary", () => {
358
+ it("reports both archive sizes and what the patch saves", () => {
359
+ const baseBundleHash = 'a'.repeat(64);
360
+ const targetBundleHash = 'b'.repeat(64);
361
+
362
+ const summary = formatBinaryPatchSummary({
363
+ platform: 'ios',
364
+ baseBundleHash,
365
+ targetBundleHash,
366
+ fullArchiveSize: 19_293_798,
367
+ patchArchiveSize: 3_250_586,
368
+ });
369
+
370
+ expect(summary).toBe(
371
+ [
372
+ 'Binary patch summary (ios)',
373
+ `Base bundle SHA-256: ${baseBundleHash}`,
374
+ `Target bundle SHA-256: ${targetBundleHash}`,
375
+ 'Full archive: 18.4 MB',
376
+ 'Patch archive: 3.1 MB',
377
+ 'Saved: 15.3 MB (83.2%)',
378
+ ].join('\n'),
379
+ );
380
+ });
381
+
382
+ it("reports a negative saving when the patch is larger than the full archive", () => {
383
+ const summary = formatBinaryPatchSummary({
384
+ platform: 'android',
385
+ baseBundleHash: 'a'.repeat(64),
386
+ targetBundleHash: 'b'.repeat(64),
387
+ fullArchiveSize: 1_000,
388
+ patchArchiveSize: 1_500,
389
+ });
390
+
391
+ expect(summary).toContain('Binary patch summary (android)');
392
+ expect(summary).toContain('Saved: -500 B (-50.0%)');
393
+ expect(summary).not.toContain('Patch skipped:');
394
+ });
395
+
396
+ it("states that the patch was skipped, and why, when it is not being released", () => {
397
+ const summary = formatBinaryPatchSummary({
398
+ platform: 'android',
399
+ baseBundleHash: 'a'.repeat(64),
400
+ targetBundleHash: 'b'.repeat(64),
401
+ fullArchiveSize: 1_000,
402
+ patchArchiveSize: 1_500,
403
+ patchSkipped: true,
404
+ });
405
+
406
+ expect(summary.split('\n').at(-1)).toBe(
407
+ 'Patch skipped: not smaller than the full archive; releasing the full bundle only (--on-oversized-patch skip)',
408
+ );
409
+ });
410
+ });