@bravemobile/react-native-code-push 13.0.0 → 13.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CodePush.podspec +35 -3
- package/README.md +117 -3
- package/THIRD-PARTY-NOTICES +96 -0
- package/android/app/build.gradle +19 -0
- package/android/app/proguard-rules.pro +5 -0
- package/android/app/src/main/cpp/CMakeLists.txt +38 -0
- package/android/app/src/main/cpp/binarypatch_jni.c +181 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +92 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchResult.java +64 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +1 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +293 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +25 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +20 -6
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +168 -4
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +13 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgressCallback.java +7 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/DownloadProgressEventDispatcher.java +25 -4
- package/android/app/src/main/java/com/microsoft/codepush/react/HDiffPatchNative.java +52 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +393 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +211 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +489 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/DownloadProgressEventDispatcherTest.java +80 -0
- package/android/codepush-export.gradle +115 -0
- package/cli/commands/bundleCommand/bundleCodePush.ts +39 -7
- package/cli/commands/bundleCommand/index.ts +9 -1
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
- package/cli/commands/releaseCommand/index.test.ts +118 -0
- package/cli/commands/releaseCommand/index.ts +20 -2
- package/cli/commands/releaseCommand/release.test.ts +582 -0
- package/cli/commands/releaseCommand/release.ts +232 -15
- package/cli/dist/commands/bundleCommand/bundleCodePush.js +26 -7
- package/cli/dist/commands/bundleCommand/index.js +5 -1
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
- package/cli/dist/commands/releaseCommand/index.js +8 -1
- package/cli/dist/commands/releaseCommand/index.test.js +96 -0
- package/cli/dist/commands/releaseCommand/release.js +165 -17
- package/cli/dist/commands/releaseCommand/release.test.js +367 -0
- package/cli/dist/functions/makeBinaryPatchBundle.js +248 -0
- package/cli/dist/functions/makeBinaryPatchBundle.test.js +321 -0
- package/cli/dist/functions/makeCodePushBundle.js +4 -2
- package/cli/dist/functions/runHermesEmitBinaryCommand.js +48 -1
- package/cli/dist/functions/runHermesEmitBinaryCommand.test.js +100 -0
- package/cli/dist/jest.globalSetup.js +38 -0
- package/cli/dist/utils/binaryPatch.js +86 -0
- package/cli/dist/utils/binaryPatch.test.js +190 -0
- package/cli/functions/makeBinaryPatchBundle.test.ts +410 -0
- package/cli/functions/makeBinaryPatchBundle.ts +367 -0
- package/cli/functions/makeCodePushBundle.ts +4 -2
- package/cli/functions/runHermesEmitBinaryCommand.test.ts +124 -0
- package/cli/functions/runHermesEmitBinaryCommand.ts +57 -1
- package/cli/jest.globalSetup.ts +43 -0
- package/cli/utils/binaryPatch.test.ts +234 -0
- package/cli/utils/binaryPatch.ts +103 -0
- package/cpp/binarypatch/binarypatch_zstd_decompressor.c +187 -0
- package/cpp/binarypatch/binarypatch_zstd_decompressor.h +47 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/checksum_plugin.h +52 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/hpatch_mt/hpatch_mt.h +147 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.c +3315 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch.h +268 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_private.h +297 -0
- package/cpp/binarypatch/vendor/HDiffPatch/libHDiffPatch/HPatch/patch_types.h +434 -0
- package/cpp/binarypatch/vendor/zstd/common/allocations.h +55 -0
- package/cpp/binarypatch/vendor/zstd/common/bits.h +205 -0
- package/cpp/binarypatch/vendor/zstd/common/bitstream.h +454 -0
- package/cpp/binarypatch/vendor/zstd/common/compiler.h +464 -0
- package/cpp/binarypatch/vendor/zstd/common/cpu.h +249 -0
- package/cpp/binarypatch/vendor/zstd/common/debug.c +30 -0
- package/cpp/binarypatch/vendor/zstd/common/debug.h +107 -0
- package/cpp/binarypatch/vendor/zstd/common/entropy_common.c +340 -0
- package/cpp/binarypatch/vendor/zstd/common/error_private.c +64 -0
- package/cpp/binarypatch/vendor/zstd/common/error_private.h +158 -0
- package/cpp/binarypatch/vendor/zstd/common/fse.h +625 -0
- package/cpp/binarypatch/vendor/zstd/common/fse_decompress.c +315 -0
- package/cpp/binarypatch/vendor/zstd/common/huf.h +277 -0
- package/cpp/binarypatch/vendor/zstd/common/mem.h +422 -0
- package/cpp/binarypatch/vendor/zstd/common/pool.c +371 -0
- package/cpp/binarypatch/vendor/zstd/common/pool.h +81 -0
- package/cpp/binarypatch/vendor/zstd/common/portability_macros.h +171 -0
- package/cpp/binarypatch/vendor/zstd/common/threading.c +182 -0
- package/cpp/binarypatch/vendor/zstd/common/threading.h +142 -0
- package/cpp/binarypatch/vendor/zstd/common/xxhash.c +18 -0
- package/cpp/binarypatch/vendor/zstd/common/xxhash.h +7094 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_common.c +48 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_deps.h +123 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_internal.h +324 -0
- package/cpp/binarypatch/vendor/zstd/common/zstd_trace.h +156 -0
- package/cpp/binarypatch/vendor/zstd/decompress/huf_decompress.c +1944 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.c +244 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_ddict.h +44 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress.c +2410 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.c +2209 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_block.h +73 -0
- package/cpp/binarypatch/vendor/zstd/decompress/zstd_decompress_internal.h +240 -0
- package/cpp/binarypatch/vendor/zstd/zstd.h +3206 -0
- package/cpp/binarypatch/vendor/zstd/zstd_errors.h +107 -0
- package/ios/CodePush/CodePush.h +18 -1
- package/ios/CodePush/CodePush.mm +24 -1
- package/ios/CodePush/CodePushBinaryPatch.h +80 -0
- package/ios/CodePush/CodePushBinaryPatch.m +473 -0
- package/ios/CodePush/CodePushDownloadHandler.m +6 -0
- package/ios/CodePush/CodePushPackage.m +183 -2
- package/ios/CodePush/CodePushUpdateUtils.m +49 -1
- package/package.json +10 -3
- package/scripts/binary-patch/build-hdiffpatch.sh +86 -0
- package/scripts/export-embedded-bundle.sh +76 -0
- package/src/CodePush.js +46 -3
- package/src/CodePush.test.js +382 -0
- package/src/package-mixins.js +20 -2
- package/typings/react-native-code-push.d.ts +80 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
5
|
+
import { release } from "./release.js";
|
|
6
|
+
import { makeCodePushBundle } from "../../functions/makeCodePushBundle.js";
|
|
7
|
+
import {
|
|
8
|
+
BINARY_PATCH_ARCHIVE_SUFFIX,
|
|
9
|
+
BINARY_PATCH_BASE_RECORD_NAME,
|
|
10
|
+
BINARY_PATCH_MANIFEST_NAME,
|
|
11
|
+
hashBundleFile,
|
|
12
|
+
writeBinaryPatchBaseRecord,
|
|
13
|
+
type OversizedPatchPolicy,
|
|
14
|
+
} from "../../functions/makeBinaryPatchBundle.js";
|
|
15
|
+
import { applyPatch } from "../../utils/binaryPatch.js";
|
|
16
|
+
import { generatePackageHashFromDirectory } from "../../utils/hash-utils.js";
|
|
17
|
+
import { unzip } from "../../utils/unzip.js";
|
|
18
|
+
import type { ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Covers the release flow around the binary patch option with `--skip-bundle`, which is
|
|
22
|
+
* the one path that reaches every decision - which artifacts exist, in which order they
|
|
23
|
+
* are uploaded, what the history ends up saying - without running the bundler.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Filled in by `bundle`, so `release --skip-bundle` starts from a real archive. */
|
|
27
|
+
const CONTENTS_DIR_NAME = 'CodePush';
|
|
28
|
+
const OUTPUT_DIR_NAME = 'build';
|
|
29
|
+
const BUNDLE_OUTPUT_DIR_NAME = 'bundleOutput';
|
|
30
|
+
|
|
31
|
+
const BINARY_VERSION = '9.9.9';
|
|
32
|
+
const APP_VERSION = '9.9.10';
|
|
33
|
+
|
|
34
|
+
function findRepoRoot(): string {
|
|
35
|
+
let dir = process.cwd();
|
|
36
|
+
for (;;) {
|
|
37
|
+
if (fs.existsSync(path.join(dir, "scripts", "binary-patch", "build-hdiffpatch.sh"))) {
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
const parent = path.dirname(dir);
|
|
41
|
+
if (parent === dir) {
|
|
42
|
+
throw new Error(`cannot locate the repository root from ${process.cwd()}`);
|
|
43
|
+
}
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const repoRoot = findRepoRoot();
|
|
49
|
+
const fixtureDir = path.join(repoRoot, "cli", "fixtures", "binary-patch");
|
|
50
|
+
const baseFixture = path.join(fixtureDir, "base.bundle");
|
|
51
|
+
const targetFixture = path.join(fixtureDir, "target.bundle");
|
|
52
|
+
|
|
53
|
+
let workDir: string;
|
|
54
|
+
|
|
55
|
+
type StagedBundle = {
|
|
56
|
+
outputPath: string;
|
|
57
|
+
bundleDirectory: string;
|
|
58
|
+
bundleFileName: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Leaves an output directory in the state a finished `bundle` run leaves it in: the
|
|
63
|
+
* bundle file, and no update contents - the contents are the bundler's scratch space,
|
|
64
|
+
* and `--skip-bundle` cannot assume they survived.
|
|
65
|
+
*/
|
|
66
|
+
async function stageBundleOutput(
|
|
67
|
+
caseName: string,
|
|
68
|
+
files: Record<string, Buffer | string> = { 'main.jsbundle': fs.readFileSync(targetFixture) },
|
|
69
|
+
): Promise<StagedBundle> {
|
|
70
|
+
const caseDir = fs.mkdtempSync(path.join(workDir, `${caseName}-`));
|
|
71
|
+
const outputPath = path.join(caseDir, OUTPUT_DIR_NAME);
|
|
72
|
+
const contentsPath = path.join(outputPath, CONTENTS_DIR_NAME);
|
|
73
|
+
|
|
74
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
75
|
+
const filePath = path.join(contentsPath, relativePath);
|
|
76
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
77
|
+
fs.writeFileSync(filePath, content);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const bundleDirectory = path.join(outputPath, BUNDLE_OUTPUT_DIR_NAME);
|
|
81
|
+
const { bundleFileName } = await makeCodePushBundle(contentsPath, bundleDirectory);
|
|
82
|
+
fs.rmSync(contentsPath, { recursive: true, force: true });
|
|
83
|
+
|
|
84
|
+
return { outputPath, bundleDirectory, bundleFileName };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class ProcessExitError extends Error {
|
|
88
|
+
constructor(readonly code: number) {
|
|
89
|
+
super(`process.exit(${code})`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Uploads = { filePath: string, downloadUrl: string, logCountBeforeUpload: number }[];
|
|
94
|
+
|
|
95
|
+
function recordingUploader(uploads: Uploads, failOn?: (filePath: string) => boolean) {
|
|
96
|
+
return async (filePath: string) => {
|
|
97
|
+
if (failOn?.(filePath)) {
|
|
98
|
+
throw new Error(`upload rejected: ${path.basename(filePath)}`);
|
|
99
|
+
}
|
|
100
|
+
const downloadUrl = `https://cdn.example.com/${path.basename(filePath)}`;
|
|
101
|
+
uploads.push({ filePath, downloadUrl, logCountBeforeUpload: logs.length });
|
|
102
|
+
return { downloadUrl };
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function historyStore() {
|
|
107
|
+
const saved: ReleaseHistoryInterface[] = [];
|
|
108
|
+
return {
|
|
109
|
+
saved,
|
|
110
|
+
getReleaseHistory: async () => ({}) as ReleaseHistoryInterface,
|
|
111
|
+
setReleaseHistory: async (
|
|
112
|
+
_binaryVersion: string,
|
|
113
|
+
_jsonFilePath: string,
|
|
114
|
+
releaseInfo: ReleaseHistoryInterface,
|
|
115
|
+
) => {
|
|
116
|
+
saved.push(releaseInfo);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type ReleaseOverrides = {
|
|
122
|
+
binaryBundlePath?: string;
|
|
123
|
+
platform?: 'ios' | 'android';
|
|
124
|
+
jsBundleName?: string;
|
|
125
|
+
skipCleanup?: boolean;
|
|
126
|
+
uploadFailsFor?: (filePath: string) => boolean;
|
|
127
|
+
onOversizedPatch?: OversizedPatchPolicy;
|
|
128
|
+
/** Passed in when the case has to read the uploads of a release that threw. */
|
|
129
|
+
uploads?: Uploads;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
async function runRelease(staged: StagedBundle, overrides: ReleaseOverrides = {}) {
|
|
133
|
+
const uploads: Uploads = overrides.uploads ?? [];
|
|
134
|
+
const history = historyStore();
|
|
135
|
+
|
|
136
|
+
await release(
|
|
137
|
+
recordingUploader(uploads, overrides.uploadFailsFor),
|
|
138
|
+
history.getReleaseHistory,
|
|
139
|
+
history.setReleaseHistory,
|
|
140
|
+
BINARY_VERSION,
|
|
141
|
+
APP_VERSION,
|
|
142
|
+
undefined,
|
|
143
|
+
overrides.platform ?? 'ios',
|
|
144
|
+
undefined,
|
|
145
|
+
staged.outputPath,
|
|
146
|
+
'index.ts',
|
|
147
|
+
// Left unset unless the case is about -j, the way commander leaves it.
|
|
148
|
+
overrides.jsBundleName,
|
|
149
|
+
false,
|
|
150
|
+
true,
|
|
151
|
+
undefined,
|
|
152
|
+
true, // skipBundle
|
|
153
|
+
overrides.skipCleanup ?? true,
|
|
154
|
+
staged.bundleDirectory,
|
|
155
|
+
undefined,
|
|
156
|
+
undefined,
|
|
157
|
+
overrides.binaryBundlePath,
|
|
158
|
+
overrides.onOversizedPatch,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return { uploads, releaseHistories: history.saved };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let logs: string[];
|
|
165
|
+
|
|
166
|
+
beforeAll(() => {
|
|
167
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-release-"));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
afterAll(() => {
|
|
171
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
logs = [];
|
|
176
|
+
const collect = (...args: unknown[]) => {
|
|
177
|
+
logs.push(args.map(String).join(' '));
|
|
178
|
+
};
|
|
179
|
+
jest.spyOn(console, 'log').mockImplementation(collect);
|
|
180
|
+
jest.spyOn(console, 'warn').mockImplementation(collect);
|
|
181
|
+
jest.spyOn(console, 'error').mockImplementation(collect);
|
|
182
|
+
jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
|
|
183
|
+
throw new ProcessExitError(code ?? 0);
|
|
184
|
+
}) as never);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
afterEach(() => {
|
|
188
|
+
jest.restoreAllMocks();
|
|
189
|
+
// addToReleaseHistory writes its JSON next to the invocation, and only removes it
|
|
190
|
+
// when the history was stored successfully.
|
|
191
|
+
fs.rmSync(path.resolve(process.cwd(), `${BINARY_VERSION}.json`), { force: true });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("release without --binary-bundle-path", () => {
|
|
195
|
+
it("uploads the full bundle only, exactly as before binary patches existed", async () => {
|
|
196
|
+
const staged = await stageBundleOutput("full-only");
|
|
197
|
+
|
|
198
|
+
const { uploads, releaseHistories } = await runRelease(staged);
|
|
199
|
+
|
|
200
|
+
expect(uploads).toHaveLength(1);
|
|
201
|
+
expect(uploads[0].filePath).toBe(`${staged.bundleDirectory}/${staged.bundleFileName}`);
|
|
202
|
+
expect(fs.readdirSync(staged.bundleDirectory)).toEqual([staged.bundleFileName]);
|
|
203
|
+
expect(releaseHistories[0][APP_VERSION]).toEqual({
|
|
204
|
+
enabled: true,
|
|
205
|
+
mandatory: false,
|
|
206
|
+
downloadUrl: uploads[0].downloadUrl,
|
|
207
|
+
packageHash: staged.bundleFileName,
|
|
208
|
+
});
|
|
209
|
+
// A release without a patch says nothing about one, so a client reading this
|
|
210
|
+
// history behaves exactly as it did before binary patches existed.
|
|
211
|
+
expect(releaseHistories[0][APP_VERSION]).not.toHaveProperty('binaryPatchDownloadUrl');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("release --skip-bundle --binary-bundle-path", () => {
|
|
216
|
+
it("patches the bundle that is already in the output directory and uploads both artifacts", async () => {
|
|
217
|
+
const staged = await stageBundleOutput("skip-bundle");
|
|
218
|
+
|
|
219
|
+
const { uploads, releaseHistories } = await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
220
|
+
|
|
221
|
+
const patchFileName = `${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`;
|
|
222
|
+
// The full archive goes first: the patch is an optimisation on top of it.
|
|
223
|
+
expect(uploads.map(({ filePath }) => path.basename(filePath))).toEqual([staged.bundleFileName, patchFileName]);
|
|
224
|
+
expect(fs.existsSync(path.join(staged.bundleDirectory, patchFileName))).toBe(true);
|
|
225
|
+
|
|
226
|
+
// Both artifacts are described in the same entry: the full bundle every client can
|
|
227
|
+
// download, and the patch a client holding the matching binary can apply instead.
|
|
228
|
+
expect(releaseHistories[0][APP_VERSION].downloadUrl).toBe(uploads[0].downloadUrl);
|
|
229
|
+
expect(releaseHistories[0][APP_VERSION].binaryPatchDownloadUrl).toBe(uploads[1].downloadUrl);
|
|
230
|
+
expect(releaseHistories[0][APP_VERSION].packageHash).toBe(staged.bundleFileName);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("prints the size summary before uploading anything", async () => {
|
|
234
|
+
const staged = await stageBundleOutput("summary");
|
|
235
|
+
|
|
236
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
237
|
+
|
|
238
|
+
const summaryIndex = logs.findIndex((line) => line.startsWith('Binary patch summary (ios)'));
|
|
239
|
+
expect(summaryIndex).toBeGreaterThanOrEqual(0);
|
|
240
|
+
expect(logs[summaryIndex]).toContain('Saved:');
|
|
241
|
+
// Printed while the release can still be stopped, so before the first upload.
|
|
242
|
+
expect(uploads).toHaveLength(2);
|
|
243
|
+
expect(summaryIndex).toBeLessThan(uploads[0].logCountBeforeUpload);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("uses the platform's own bundle name when picking the target to patch", async () => {
|
|
247
|
+
const staged = await stageBundleOutput("android", {
|
|
248
|
+
'index.android.bundle': fs.readFileSync(targetFixture),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await runRelease(staged, { binaryBundlePath: baseFixture, platform: 'android' });
|
|
252
|
+
|
|
253
|
+
expect(logs.some((line) => line.startsWith('Binary patch summary (android)'))).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("patches a bundle that was built with a custom JS bundle name", async () => {
|
|
257
|
+
const staged = await stageBundleOutput("custom-name", {
|
|
258
|
+
'custom.jsbundle': fs.readFileSync(targetFixture),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const { uploads } = await runRelease(staged, {
|
|
262
|
+
binaryBundlePath: baseFixture,
|
|
263
|
+
jsBundleName: 'custom.jsbundle',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(uploads.map(({ filePath }) => path.basename(filePath))).toEqual([
|
|
267
|
+
staged.bundleFileName,
|
|
268
|
+
`${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
const extractRoot = path.join(staged.outputPath, 'extracted');
|
|
272
|
+
fs.mkdirSync(extractRoot, { recursive: true });
|
|
273
|
+
await unzip(path.join(staged.bundleDirectory, `${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`), extractRoot);
|
|
274
|
+
const manifest = JSON.parse(
|
|
275
|
+
fs.readFileSync(path.join(extractRoot, CONTENTS_DIR_NAME, BINARY_PATCH_MANIFEST_NAME), 'utf8'),
|
|
276
|
+
) as { bundlePath: string, patchFile: string };
|
|
277
|
+
|
|
278
|
+
expect(manifest.bundlePath).toBe('custom.jsbundle');
|
|
279
|
+
expect(manifest.patchFile).toBe('custom.jsbundle.patch');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("fails without -j when the released bundle uses a custom JS bundle name", async () => {
|
|
283
|
+
const staged = await stageBundleOutput("custom-name-without-option", {
|
|
284
|
+
'custom.jsbundle': fs.readFileSync(targetFixture),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// The error names -j because passing it is what makes this release work.
|
|
288
|
+
await expect(runRelease(staged, { binaryBundlePath: baseFixture })).rejects.toThrow(
|
|
289
|
+
/main\.jsbundle.*--js-bundle-name/s,
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("fails with an actionable error when the released bundle holds no matching JS bundle", async () => {
|
|
294
|
+
const staged = await stageBundleOutput("no-target", {
|
|
295
|
+
'index.android.bundle': fs.readFileSync(targetFixture),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await expect(runRelease(staged, { binaryBundlePath: baseFixture, platform: 'ios' })).rejects.toThrow(
|
|
299
|
+
/main\.jsbundle.*--js-bundle-name/s,
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("warns when the bundle was compiled against a different base bundle", async () => {
|
|
304
|
+
const staged = await stageBundleOutput("mismatch");
|
|
305
|
+
writeBinaryPatchBaseRecord(staged.outputPath, 'f'.repeat(64));
|
|
306
|
+
|
|
307
|
+
await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
308
|
+
|
|
309
|
+
expect(logs.filter((line) => line.startsWith('warn:')).join('\n')).toMatch(/compiled against base bundle/);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("does not warn when the recorded base bundle is the one being patched against", async () => {
|
|
313
|
+
const staged = await stageBundleOutput("match");
|
|
314
|
+
// The record the `bundle` command writes holds the hash of this same file.
|
|
315
|
+
writeBinaryPatchBaseRecord(staged.outputPath, hashBundleFile(baseFixture));
|
|
316
|
+
|
|
317
|
+
await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
318
|
+
|
|
319
|
+
expect(logs.filter((line) => line.startsWith('warn:'))).toEqual([]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("keeps the release history untouched when the patch archive cannot be uploaded", async () => {
|
|
323
|
+
const staged = await stageBundleOutput("patch-upload-failure");
|
|
324
|
+
|
|
325
|
+
const uploads: Uploads = [];
|
|
326
|
+
const history = historyStore();
|
|
327
|
+
|
|
328
|
+
await expect(
|
|
329
|
+
release(
|
|
330
|
+
recordingUploader(uploads, (filePath) => filePath.endsWith(BINARY_PATCH_ARCHIVE_SUFFIX)),
|
|
331
|
+
history.getReleaseHistory,
|
|
332
|
+
history.setReleaseHistory,
|
|
333
|
+
BINARY_VERSION,
|
|
334
|
+
APP_VERSION,
|
|
335
|
+
undefined,
|
|
336
|
+
'ios',
|
|
337
|
+
undefined,
|
|
338
|
+
staged.outputPath,
|
|
339
|
+
'index.ts',
|
|
340
|
+
'',
|
|
341
|
+
false,
|
|
342
|
+
true,
|
|
343
|
+
undefined,
|
|
344
|
+
true,
|
|
345
|
+
true,
|
|
346
|
+
staged.bundleDirectory,
|
|
347
|
+
undefined,
|
|
348
|
+
undefined,
|
|
349
|
+
baseFixture,
|
|
350
|
+
),
|
|
351
|
+
).rejects.toThrow(ProcessExitError);
|
|
352
|
+
|
|
353
|
+
expect(uploads.map(({ filePath }) => path.basename(filePath))).toEqual([staged.bundleFileName]);
|
|
354
|
+
expect(history.saved).toEqual([]);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("leaves no working directories behind and cleans up the output when asked", async () => {
|
|
358
|
+
const staged = await stageBundleOutput("cleanup");
|
|
359
|
+
|
|
360
|
+
await runRelease(staged, { binaryBundlePath: baseFixture, skipCleanup: true });
|
|
361
|
+
|
|
362
|
+
expect(fs.readdirSync(staged.outputPath)).toEqual([BUNDLE_OUTPUT_DIR_NAME]);
|
|
363
|
+
|
|
364
|
+
await runRelease(staged, { binaryBundlePath: baseFixture, skipCleanup: false });
|
|
365
|
+
|
|
366
|
+
expect(fs.existsSync(staged.outputPath)).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("still finds the bundle to release when a previous run left its patch archive behind", async () => {
|
|
370
|
+
const staged = await stageBundleOutput("leftover-patch");
|
|
371
|
+
fs.writeFileSync(path.join(staged.bundleDirectory, `stale${BINARY_PATCH_ARCHIVE_SUFFIX}`), 'stale');
|
|
372
|
+
|
|
373
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
374
|
+
|
|
375
|
+
expect(path.basename(uploads[0].filePath)).toBe(staged.bundleFileName);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("uploads the patch when it is smaller than the full bundle, without warning about its size", async () => {
|
|
379
|
+
const staged = await stageBundleOutput("worthwhile-patch");
|
|
380
|
+
|
|
381
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
382
|
+
|
|
383
|
+
expect(uploads).toHaveLength(2);
|
|
384
|
+
expect(logs.filter((line) => line.startsWith('warn:'))).toEqual([]);
|
|
385
|
+
expect(logs.join('\n')).not.toContain('Patch skipped:');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("ships a patch archive a client can turn back into the released bundle", async () => {
|
|
389
|
+
const staged = await stageBundleOutput("manifest", {
|
|
390
|
+
'main.jsbundle': fs.readFileSync(targetFixture),
|
|
391
|
+
'assets/logo.png': Buffer.from('logo-bytes'),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
395
|
+
|
|
396
|
+
const extractRoot = path.join(staged.outputPath, 'extracted');
|
|
397
|
+
fs.mkdirSync(extractRoot, { recursive: true });
|
|
398
|
+
await unzip(path.join(staged.bundleDirectory, `${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`), extractRoot);
|
|
399
|
+
|
|
400
|
+
const contents = path.join(extractRoot, CONTENTS_DIR_NAME);
|
|
401
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(contents, BINARY_PATCH_MANIFEST_NAME), 'utf8')) as {
|
|
402
|
+
bundlePath: string,
|
|
403
|
+
patchFile: string,
|
|
404
|
+
};
|
|
405
|
+
expect(manifest.bundlePath).toBe('main.jsbundle');
|
|
406
|
+
|
|
407
|
+
applyPatch(baseFixture, path.join(contents, manifest.patchFile), path.join(contents, manifest.bundlePath));
|
|
408
|
+
fs.rmSync(path.join(contents, manifest.patchFile));
|
|
409
|
+
fs.rmSync(path.join(contents, BINARY_PATCH_MANIFEST_NAME));
|
|
410
|
+
|
|
411
|
+
expect(await generatePackageHashFromDirectory(contents, extractRoot)).toBe(staged.bundleFileName);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The build hooks export the bundle of a native build with a record describing it, so the
|
|
417
|
+
* base bundle a release is handed can be checked against the build it came out of.
|
|
418
|
+
*/
|
|
419
|
+
describe("release --binary-bundle-path with an exported base bundle record", () => {
|
|
420
|
+
/** A base bundle laid out the way a build hook leaves it: the bundle, and its record beside it. */
|
|
421
|
+
function stageExportedBaseBundle(caseName: string, record?: string): string {
|
|
422
|
+
const exportDir = fs.mkdtempSync(path.join(workDir, `${caseName}-export-`));
|
|
423
|
+
const baseBundlePath = path.join(exportDir, 'main.jsbundle');
|
|
424
|
+
fs.copyFileSync(baseFixture, baseBundlePath);
|
|
425
|
+
|
|
426
|
+
if (record !== undefined) {
|
|
427
|
+
fs.writeFileSync(path.join(exportDir, BINARY_PATCH_BASE_RECORD_NAME), record);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return baseBundlePath;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function exportedRecord(overrides: Record<string, unknown> = {}): string {
|
|
434
|
+
return JSON.stringify({
|
|
435
|
+
baseBundleHash: hashBundleFile(baseFixture),
|
|
436
|
+
binaryVersion: BINARY_VERSION,
|
|
437
|
+
buildNumber: '42',
|
|
438
|
+
gitSha: 'a'.repeat(40),
|
|
439
|
+
...overrides,
|
|
440
|
+
}, null, 2);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
it("releases both artifacts when the record describes this bundle and this binary version", async () => {
|
|
444
|
+
const staged = await stageBundleOutput("record-match");
|
|
445
|
+
const baseBundlePath = stageExportedBaseBundle("record-match", exportedRecord());
|
|
446
|
+
|
|
447
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseBundlePath });
|
|
448
|
+
|
|
449
|
+
expect(uploads).toHaveLength(2);
|
|
450
|
+
expect(logs.filter((line) => line.startsWith('warn:'))).toEqual([]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("fails before anything is built when the base bundle is not the file the record describes", async () => {
|
|
454
|
+
const staged = await stageBundleOutput("record-hash-mismatch");
|
|
455
|
+
const baseBundlePath = stageExportedBaseBundle("record-hash-mismatch", exportedRecord({ baseBundleHash: 'f'.repeat(64) }));
|
|
456
|
+
|
|
457
|
+
const uploads: Uploads = [];
|
|
458
|
+
await expect(runRelease(staged, { binaryBundlePath: baseBundlePath, uploads })).rejects.toThrow(
|
|
459
|
+
/does not match the record exported next to it/,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
expect(uploads).toEqual([]);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("fails when the base bundle was exported from a different binary version", async () => {
|
|
466
|
+
const staged = await stageBundleOutput("record-version-mismatch");
|
|
467
|
+
const baseBundlePath = stageExportedBaseBundle("record-version-mismatch", exportedRecord({ binaryVersion: '1.0.0' }));
|
|
468
|
+
|
|
469
|
+
const uploads: Uploads = [];
|
|
470
|
+
await expect(runRelease(staged, { binaryBundlePath: baseBundlePath, uploads })).rejects.toThrow(
|
|
471
|
+
new RegExp(`exported from binary version 1\\.0\\.0, but this release targets ${BINARY_VERSION}`),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(uploads).toEqual([]);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it.each([
|
|
478
|
+
['is not JSON at all', 'not json at all'],
|
|
479
|
+
['holds nothing to read a record out of', 'null'],
|
|
480
|
+
])("warns and releases when the record %s", async (caseName, contents) => {
|
|
481
|
+
const staged = await stageBundleOutput("record-unreadable");
|
|
482
|
+
const baseBundlePath = stageExportedBaseBundle(`record-unreadable-${caseName.replace(/\W+/g, '-')}`, contents);
|
|
483
|
+
|
|
484
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseBundlePath });
|
|
485
|
+
|
|
486
|
+
expect(logs.filter((line) => line.startsWith('warn:')).join('\n')).toMatch(/not a readable binary patch base record/);
|
|
487
|
+
expect(uploads).toHaveLength(2);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("warns and releases when the record leaves out the hash it is checked against", async () => {
|
|
491
|
+
const staged = await stageBundleOutput("record-no-hash");
|
|
492
|
+
const baseBundlePath = stageExportedBaseBundle("record-no-hash", JSON.stringify({ binaryVersion: '1.0.0' }));
|
|
493
|
+
|
|
494
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseBundlePath });
|
|
495
|
+
|
|
496
|
+
expect(logs.filter((line) => line.startsWith('warn:')).join('\n')).toMatch(/not a readable binary patch base record/);
|
|
497
|
+
expect(uploads).toHaveLength(2);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("releases exactly as before when the base bundle has no record beside it", async () => {
|
|
501
|
+
const staged = await stageBundleOutput("record-absent");
|
|
502
|
+
const baseBundlePath = stageExportedBaseBundle("record-absent");
|
|
503
|
+
|
|
504
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseBundlePath });
|
|
505
|
+
|
|
506
|
+
expect(uploads).toHaveLength(2);
|
|
507
|
+
expect(logs.filter((line) => line.startsWith('warn:'))).toEqual([]);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* A patch is only worth publishing when it is smaller than the archive it replaces. The
|
|
513
|
+
* CLI runs unattended, so `--on-oversized-patch` decides what happens when it is not.
|
|
514
|
+
*
|
|
515
|
+
* An update whose bundle is a few bytes long produces one: the patch container and the
|
|
516
|
+
* manifest that describes it together outweigh the whole full archive.
|
|
517
|
+
*/
|
|
518
|
+
describe("release --on-oversized-patch", () => {
|
|
519
|
+
const tinyContents = () => ({ 'main.jsbundle': Buffer.from('tiny') });
|
|
520
|
+
|
|
521
|
+
it("skips the patch by default and releases the full bundle alone", async () => {
|
|
522
|
+
const staged = await stageBundleOutput("oversized-skip", tinyContents());
|
|
523
|
+
|
|
524
|
+
const { uploads, releaseHistories } = await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
525
|
+
|
|
526
|
+
expect(uploads.map(({ filePath }) => path.basename(filePath))).toEqual([staged.bundleFileName]);
|
|
527
|
+
expect(logs.filter((line) => line.startsWith('warn:')).join('\n')).toMatch(/not smaller than the full archive/);
|
|
528
|
+
expect(logs.join('\n')).toContain('Patch skipped:');
|
|
529
|
+
expect(releaseHistories[0][APP_VERSION].downloadUrl).toBe(uploads[0].downloadUrl);
|
|
530
|
+
// Nothing was uploaded to patch from, so the entry must not point at one.
|
|
531
|
+
expect(releaseHistories[0][APP_VERSION]).not.toHaveProperty('binaryPatchDownloadUrl');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("fails before any upload when the policy is fail", async () => {
|
|
535
|
+
const staged = await stageBundleOutput("oversized-fail", tinyContents());
|
|
536
|
+
|
|
537
|
+
const uploads: Uploads = [];
|
|
538
|
+
const history = historyStore();
|
|
539
|
+
|
|
540
|
+
await expect(
|
|
541
|
+
release(
|
|
542
|
+
recordingUploader(uploads),
|
|
543
|
+
history.getReleaseHistory,
|
|
544
|
+
history.setReleaseHistory,
|
|
545
|
+
BINARY_VERSION,
|
|
546
|
+
APP_VERSION,
|
|
547
|
+
undefined,
|
|
548
|
+
'ios',
|
|
549
|
+
undefined,
|
|
550
|
+
staged.outputPath,
|
|
551
|
+
'index.ts',
|
|
552
|
+
undefined,
|
|
553
|
+
false,
|
|
554
|
+
true,
|
|
555
|
+
undefined,
|
|
556
|
+
true,
|
|
557
|
+
true,
|
|
558
|
+
staged.bundleDirectory,
|
|
559
|
+
undefined,
|
|
560
|
+
undefined,
|
|
561
|
+
baseFixture,
|
|
562
|
+
'fail',
|
|
563
|
+
),
|
|
564
|
+
).rejects.toThrow(/not smaller than the full archive/);
|
|
565
|
+
|
|
566
|
+
expect(uploads).toEqual([]);
|
|
567
|
+
expect(history.saved).toEqual([]);
|
|
568
|
+
// The temp directories are still cleaned up on the way out.
|
|
569
|
+
expect(fs.readdirSync(staged.outputPath)).toEqual([BUNDLE_OUTPUT_DIR_NAME]);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("uploads a worthwhile patch even when the policy is fail", async () => {
|
|
573
|
+
const staged = await stageBundleOutput("worthwhile-under-fail");
|
|
574
|
+
|
|
575
|
+
const { uploads } = await runRelease(staged, {
|
|
576
|
+
binaryBundlePath: baseFixture,
|
|
577
|
+
onOversizedPatch: 'fail',
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
expect(uploads).toHaveLength(2);
|
|
581
|
+
});
|
|
582
|
+
});
|