@bravemobile/react-native-code-push 13.0.0 → 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.
- package/CodePush.podspec +35 -3
- package/README.md +115 -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 +12 -2
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +159 -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/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 +207 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +449 -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 +16 -1
- package/ios/CodePush/CodePushBinaryPatch.h +80 -0
- package/ios/CodePush/CodePushBinaryPatch.m +473 -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 +77 -0
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { bundleCodePush } from "../bundleCommand/bundleCodePush.js";
|
|
3
|
+
import { bundleCodePush, resolveJsBundleName } from "../bundleCommand/bundleCodePush.js";
|
|
4
4
|
import { addToReleaseHistory } from "./addToReleaseHistory.js";
|
|
5
5
|
import type { CliConfigInterface } 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
|
+
import {
|
|
9
|
+
BINARY_PATCH_ARCHIVE_SUFFIX,
|
|
10
|
+
BINARY_PATCH_BASE_RECORD_NAME,
|
|
11
|
+
DEFAULT_OVERSIZED_PATCH_POLICY,
|
|
12
|
+
extractCodePushBundleContents,
|
|
13
|
+
formatBinaryPatchSummary,
|
|
14
|
+
hashBundleFile,
|
|
15
|
+
isPatchArchiveOversized,
|
|
16
|
+
makeBinaryPatchBundle,
|
|
17
|
+
readBinaryPatchBaseRecord,
|
|
18
|
+
type BinaryPatchBaseRecord,
|
|
19
|
+
type BinaryPatchBundle,
|
|
20
|
+
type OversizedPatchPolicy,
|
|
21
|
+
} from "../../functions/makeBinaryPatchBundle.js";
|
|
8
22
|
|
|
9
23
|
export async function release(
|
|
10
24
|
bundleUploader: CliConfigInterface['bundleUploader'],
|
|
@@ -17,7 +31,7 @@ export async function release(
|
|
|
17
31
|
identifier: string | undefined,
|
|
18
32
|
outputPath: string,
|
|
19
33
|
entryFile: string,
|
|
20
|
-
jsBundleName: string,
|
|
34
|
+
jsBundleName: string | undefined,
|
|
21
35
|
mandatory: boolean,
|
|
22
36
|
enable: boolean,
|
|
23
37
|
rollout: number | undefined,
|
|
@@ -26,10 +40,19 @@ export async function release(
|
|
|
26
40
|
bundleDirectory: string,
|
|
27
41
|
outputMetroDir?: string,
|
|
28
42
|
hashCalc?: boolean,
|
|
43
|
+
baseBundlePath?: string,
|
|
44
|
+
onOversizedPatch: OversizedPatchPolicy = DEFAULT_OVERSIZED_PATCH_POLICY,
|
|
29
45
|
): Promise<void> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
if (baseBundlePath) {
|
|
47
|
+
// Checked before the bundler runs, so the wrong base bundle costs a second rather
|
|
48
|
+
// than a full build.
|
|
49
|
+
verifyExportedBaseBundleRecord(baseBundlePath, binaryVersion);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const codePushBundle = skipBundle
|
|
53
|
+
? null
|
|
54
|
+
: await bundleCodePush(framework, platform, outputPath, entryFile, jsBundleName, bundleDirectory, outputMetroDir, baseBundlePath);
|
|
55
|
+
const bundleFileName = codePushBundle?.bundleFileName ?? readBundleFileNameFrom(bundleDirectory);
|
|
33
56
|
const bundleFilePath = `${bundleDirectory}/${bundleFileName}`;
|
|
34
57
|
|
|
35
58
|
const packageHash = await (() => {
|
|
@@ -40,20 +63,34 @@ export async function release(
|
|
|
40
63
|
return bundleFileName;
|
|
41
64
|
})();
|
|
42
65
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
const binaryPatch = baseBundlePath
|
|
67
|
+
? await makeBinaryPatchArtifact({
|
|
68
|
+
baseBundlePath,
|
|
69
|
+
contentsPath: codePushBundle?.contentsPath,
|
|
70
|
+
bundleFilePath,
|
|
71
|
+
jsBundleName: codePushBundle?.jsBundleName ?? resolveJsBundleName(platform, jsBundleName),
|
|
72
|
+
bundleDirectory,
|
|
73
|
+
packageHash,
|
|
74
|
+
outputPath,
|
|
75
|
+
platform,
|
|
76
|
+
onOversizedPatch,
|
|
77
|
+
})
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
// Every artifact is uploaded before the release history is touched, so a failed
|
|
81
|
+
// upload leaves the history describing only updates that can actually be downloaded.
|
|
82
|
+
const downloadUrl = await uploadArtifact(bundleUploader, bundleFilePath, platform, identifier, 'bundle');
|
|
83
|
+
let patchDownloadUrl: string | undefined;
|
|
84
|
+
if (binaryPatch) {
|
|
85
|
+
patchDownloadUrl = await uploadArtifact(bundleUploader, binaryPatch.patchBundleFilePath, platform, identifier, 'binary patch bundle');
|
|
86
|
+
console.log(`log: Binary patch archive uploaded (download url: ${patchDownloadUrl})`);
|
|
87
|
+
}
|
|
52
88
|
|
|
53
89
|
await addToReleaseHistory(
|
|
54
90
|
appVersion,
|
|
55
91
|
binaryVersion,
|
|
56
92
|
downloadUrl,
|
|
93
|
+
patchDownloadUrl,
|
|
57
94
|
packageHash,
|
|
58
95
|
getReleaseHistory,
|
|
59
96
|
setReleaseHistory,
|
|
@@ -74,7 +111,9 @@ function cleanUpOutputs(dir: string) {
|
|
|
74
111
|
}
|
|
75
112
|
|
|
76
113
|
function readBundleFileNameFrom(bundleDirectory: string): string {
|
|
77
|
-
|
|
114
|
+
// A previous release of the same bundle may have left its patch archive here, and
|
|
115
|
+
// that archive is derived from the bundle file rather than a candidate for release.
|
|
116
|
+
const files = fs.readdirSync(bundleDirectory).filter((file) => !file.endsWith(BINARY_PATCH_ARCHIVE_SUFFIX));
|
|
78
117
|
if (files.length !== 1) {
|
|
79
118
|
console.error('The bundlePath must contain only one file.');
|
|
80
119
|
process.exit(1);
|
|
@@ -101,3 +140,181 @@ async function calcHashFromBundleFile(bundleFilePath: string): Promise<string> {
|
|
|
101
140
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
102
141
|
}
|
|
103
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds the binary patch artifact of this release and reports what it saves, before
|
|
146
|
+
* anything is uploaded.
|
|
147
|
+
*
|
|
148
|
+
* A patch that is not smaller than the full archive is not worth publishing, and the
|
|
149
|
+
* CLI cannot ask: it runs unattended. `onOversizedPatch` decides instead - `skip`
|
|
150
|
+
* releases the full bundle alone, `fail` stops the release while nothing has been
|
|
151
|
+
* uploaded yet.
|
|
152
|
+
*
|
|
153
|
+
* @param contentsPath {string | undefined} Update contents of a bundle that was just built. Absent with `--skip-bundle`, where the bundle file being released is unpacked instead, so the patch describes exactly the bytes that go out.
|
|
154
|
+
* @return {Promise<BinaryPatchBundle | null>} The artifact to upload, or `null` when the patch was skipped.
|
|
155
|
+
*/
|
|
156
|
+
async function makeBinaryPatchArtifact({
|
|
157
|
+
baseBundlePath,
|
|
158
|
+
contentsPath,
|
|
159
|
+
bundleFilePath,
|
|
160
|
+
jsBundleName,
|
|
161
|
+
bundleDirectory,
|
|
162
|
+
packageHash,
|
|
163
|
+
outputPath,
|
|
164
|
+
platform,
|
|
165
|
+
onOversizedPatch,
|
|
166
|
+
}: {
|
|
167
|
+
baseBundlePath: string;
|
|
168
|
+
contentsPath: string | undefined;
|
|
169
|
+
bundleFilePath: string;
|
|
170
|
+
jsBundleName: string;
|
|
171
|
+
bundleDirectory: string;
|
|
172
|
+
packageHash: string;
|
|
173
|
+
outputPath: string;
|
|
174
|
+
platform: 'ios' | 'android';
|
|
175
|
+
onOversizedPatch: OversizedPatchPolicy;
|
|
176
|
+
}): Promise<BinaryPatchBundle | null> {
|
|
177
|
+
warnOnBaseBundleMismatch(outputPath, baseBundlePath);
|
|
178
|
+
|
|
179
|
+
let patchContentsPath = contentsPath;
|
|
180
|
+
let extractDir: string | null = null;
|
|
181
|
+
if (patchContentsPath === undefined) {
|
|
182
|
+
const extracted = await extractCodePushBundleContents(bundleFilePath, outputPath);
|
|
183
|
+
patchContentsPath = extracted.contentsPath;
|
|
184
|
+
extractDir = extracted.extractDir;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const binaryPatch = await makeBinaryPatchBundle({
|
|
189
|
+
contentsPath: patchContentsPath,
|
|
190
|
+
baseBundlePath,
|
|
191
|
+
bundleRelativePath: jsBundleName,
|
|
192
|
+
bundleDirectory,
|
|
193
|
+
packageHash,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const fullArchiveSize = fs.statSync(bundleFilePath).size;
|
|
197
|
+
const patchArchiveSize = fs.statSync(binaryPatch.patchBundleFilePath).size;
|
|
198
|
+
const oversized = isPatchArchiveOversized(fullArchiveSize, patchArchiveSize);
|
|
199
|
+
|
|
200
|
+
console.log(formatBinaryPatchSummary({
|
|
201
|
+
platform,
|
|
202
|
+
baseBundleHash: binaryPatch.manifest.baseBundleHash,
|
|
203
|
+
targetBundleHash: binaryPatch.manifest.targetBundleHash,
|
|
204
|
+
fullArchiveSize,
|
|
205
|
+
patchArchiveSize,
|
|
206
|
+
patchSkipped: oversized && onOversizedPatch === 'skip',
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
if (oversized) {
|
|
210
|
+
if (onOversizedPatch === 'fail') {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`The binary patch archive (${patchArchiveSize} bytes) is not smaller than the full archive (${fullArchiveSize} bytes), ` +
|
|
213
|
+
'and --on-oversized-patch is set to "fail". Nothing was uploaded.',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.warn(
|
|
218
|
+
`warn: The binary patch archive (${patchArchiveSize} bytes) is not smaller than the full archive (${fullArchiveSize} bytes). ` +
|
|
219
|
+
'Releasing the full bundle only, without a binary patch.',
|
|
220
|
+
);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return binaryPatch;
|
|
225
|
+
} finally {
|
|
226
|
+
if (extractDir) {
|
|
227
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Cross-checks the base bundle against the record a build hook exported next to it.
|
|
234
|
+
*
|
|
235
|
+
* The base bundle is the one input a release cannot verify on its own: pass the bundle of
|
|
236
|
+
* a different build and everything still succeeds, only for the patch to be unappliable
|
|
237
|
+
* on every device. The record says which bytes and which binary the export came from, so
|
|
238
|
+
* when it is there, the two mistakes that produce a broken release - a base file that was
|
|
239
|
+
* replaced after it was exported, and a base exported from a different binary version -
|
|
240
|
+
* stop the release before anything is built or uploaded.
|
|
241
|
+
*
|
|
242
|
+
* A base bundle produced some other way has no record next to it, and releases exactly as
|
|
243
|
+
* it did before. A record that cannot be read only warns: it is a cross-check, and losing
|
|
244
|
+
* it must never be able to fail a release that is otherwise fine.
|
|
245
|
+
*/
|
|
246
|
+
function verifyExportedBaseBundleRecord(baseBundlePath: string, binaryVersion: string): void {
|
|
247
|
+
const recordPath = path.join(path.dirname(path.resolve(baseBundlePath)), BINARY_PATCH_BASE_RECORD_NAME);
|
|
248
|
+
|
|
249
|
+
let contents: string;
|
|
250
|
+
try {
|
|
251
|
+
contents = fs.readFileSync(recordPath, 'utf8');
|
|
252
|
+
} catch {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
let record: Partial<BinaryPatchBaseRecord> = {};
|
|
257
|
+
try {
|
|
258
|
+
record = (JSON.parse(contents) as Partial<BinaryPatchBaseRecord> | null) ?? {};
|
|
259
|
+
} catch {
|
|
260
|
+
// Left empty, which is reported below as a record that says nothing to check.
|
|
261
|
+
}
|
|
262
|
+
if (typeof record.baseBundleHash !== 'string') {
|
|
263
|
+
console.warn(
|
|
264
|
+
`warn: "${recordPath}" is not a readable binary patch base record, so the base bundle was released without cross-checking it.`,
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const baseBundleHash = hashBundleFile(baseBundlePath);
|
|
270
|
+
if (record.baseBundleHash !== baseBundleHash) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`The base bundle "${baseBundlePath}" does not match the record exported next to it: the record describes ${record.baseBundleHash}, ` +
|
|
273
|
+
`but the file hashes to ${baseBundleHash}. Export the bundle again from the build that produced the binary.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (typeof record.binaryVersion === 'string' && record.binaryVersion !== binaryVersion) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`The base bundle "${baseBundlePath}" was exported from binary version ${record.binaryVersion}, but this release targets ${binaryVersion} ` +
|
|
280
|
+
'(--binary-version). Release against the bundle of the binary being targeted.',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The bundle being released may have been compiled by an earlier `bundle` run against a
|
|
287
|
+
* different base. The patch stays valid - it is always computed against the base given
|
|
288
|
+
* here - but the bytecode alignment that keeps it small is lost, which is worth saying
|
|
289
|
+
* out loud instead of leaving it to be noticed in the size summary.
|
|
290
|
+
*/
|
|
291
|
+
function warnOnBaseBundleMismatch(outputPath: string, baseBundlePath: string): void {
|
|
292
|
+
const record = readBinaryPatchBaseRecord(outputPath);
|
|
293
|
+
if (!record) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const baseBundleHash = hashBundleFile(baseBundlePath);
|
|
298
|
+
if (record.baseBundleHash !== baseBundleHash) {
|
|
299
|
+
console.warn(
|
|
300
|
+
`warn: The bundle was compiled against base bundle ${record.baseBundleHash}, but this release patches against ${baseBundleHash}. ` +
|
|
301
|
+
'The patch is valid but larger than an aligned one.',
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function uploadArtifact(
|
|
307
|
+
bundleUploader: CliConfigInterface['bundleUploader'],
|
|
308
|
+
filePath: string,
|
|
309
|
+
platform: 'ios' | 'android',
|
|
310
|
+
identifier: string | undefined,
|
|
311
|
+
artifactName: string,
|
|
312
|
+
): Promise<string> {
|
|
313
|
+
try {
|
|
314
|
+
const { downloadUrl } = await bundleUploader(filePath, platform, identifier);
|
|
315
|
+
return downloadUrl
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error(`Failed to upload the ${artifactName} file. Exiting the program.\n`, error)
|
|
318
|
+
process.exit(1)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -4,21 +4,30 @@ import { prepareToBundleJS } from "../../functions/prepareToBundleJS.js";
|
|
|
4
4
|
import { runReactNativeBundleCommand } from "../../functions/runReactNativeBundleCommand.js";
|
|
5
5
|
import { runExpoBundleCommand } from "../../functions/runExpoBundleCommand.js";
|
|
6
6
|
import { getReactTempDir } from "../../functions/getReactTempDir.js";
|
|
7
|
-
import { runHermesEmitBinaryCommand } from "../../functions/runHermesEmitBinaryCommand.js";
|
|
7
|
+
import { resolveBaseBytecodeHermesFlags, runHermesEmitBinaryCommand } from "../../functions/runHermesEmitBinaryCommand.js";
|
|
8
8
|
import { makeCodePushBundle } from "../../functions/makeCodePushBundle.js";
|
|
9
|
+
import { hashBundleFile, writeBinaryPatchBaseRecord } from "../../functions/makeBinaryPatchBundle.js";
|
|
9
10
|
import { ROOT_OUTPUT_DIR, ENTRY_FILE } from "../../constant.js";
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* JS bundle file name react-native writes, which is also the name the app looks for
|
|
13
|
+
* inside an update, so it has to be decided the same way everywhere.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveJsBundleName(platform, jsBundleName) {
|
|
16
|
+
const DEFAULT_JS_BUNDLE_NAME = platform === 'ios' ? 'main.jsbundle' : 'index.android.bundle';
|
|
17
|
+
return jsBundleName || DEFAULT_JS_BUNDLE_NAME;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @param baseBundlePath {string} JS bundle from the target binary. When given, the compilation is aligned with it and the base is recorded for a later `release`.
|
|
21
|
+
* @return {Promise<CodePushBundleResult>} CodePush bundle file name (equals to packageHash) and the contents it was made of
|
|
12
22
|
*/
|
|
13
23
|
export async function bundleCodePush(framework, platform = 'ios', outputRootPath = ROOT_OUTPUT_DIR, entryFile = ENTRY_FILE, jsBundleName, // JS bundle file name (not CodePush bundle file)
|
|
14
24
|
bundleDirectory, // CodePush bundle output directory
|
|
15
|
-
outputMetroDir) {
|
|
25
|
+
outputMetroDir, baseBundlePath) {
|
|
16
26
|
if (fs.existsSync(outputRootPath)) {
|
|
17
27
|
fs.rmSync(outputRootPath, { recursive: true });
|
|
18
28
|
}
|
|
19
29
|
const OUTPUT_CONTENT_PATH = `${outputRootPath}/CodePush`;
|
|
20
|
-
const
|
|
21
|
-
const _jsBundleName = jsBundleName || DEFAULT_JS_BUNDLE_NAME; // react-native JS bundle output name
|
|
30
|
+
const _jsBundleName = resolveJsBundleName(platform, jsBundleName); // react-native JS bundle output name
|
|
22
31
|
const SOURCEMAP_OUTPUT = `${outputRootPath}/${_jsBundleName}.map`;
|
|
23
32
|
prepareToBundleJS({ deleteDirs: [outputRootPath, getReactTempDir()], makeDir: OUTPUT_CONTENT_PATH });
|
|
24
33
|
if (framework === 'expo') {
|
|
@@ -29,11 +38,21 @@ outputMetroDir) {
|
|
|
29
38
|
}
|
|
30
39
|
console.log('log: JS bundling complete');
|
|
31
40
|
copyMetroOutputsIfNeeded(outputRootPath, outputMetroDir, OUTPUT_CONTENT_PATH, _jsBundleName, SOURCEMAP_OUTPUT);
|
|
32
|
-
await runHermesEmitBinaryCommand(_jsBundleName, OUTPUT_CONTENT_PATH, SOURCEMAP_OUTPUT);
|
|
41
|
+
await runHermesEmitBinaryCommand(_jsBundleName, OUTPUT_CONTENT_PATH, SOURCEMAP_OUTPUT, baseBundlePath ? resolveBaseBytecodeHermesFlags(baseBundlePath) : []);
|
|
33
42
|
console.log('log: Hermes compilation complete');
|
|
34
43
|
const { bundleFileName: codePushBundleFileName } = await makeCodePushBundle(OUTPUT_CONTENT_PATH, bundleDirectory);
|
|
35
44
|
console.log(`log: CodePush bundle created (file path: ./${bundleDirectory}/${codePushBundleFileName})`);
|
|
36
|
-
|
|
45
|
+
if (baseBundlePath) {
|
|
46
|
+
// Written after the bundle file, and to the output root instead of the update
|
|
47
|
+
// contents, so recording the base cannot change what was just packed or its hash.
|
|
48
|
+
const recordPath = writeBinaryPatchBaseRecord(outputRootPath, hashBundleFile(baseBundlePath));
|
|
49
|
+
console.log(`log: Binary patch base recorded (file path: ${recordPath})`);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
bundleFileName: codePushBundleFileName,
|
|
53
|
+
contentsPath: OUTPUT_CONTENT_PATH,
|
|
54
|
+
jsBundleName: _jsBundleName,
|
|
55
|
+
};
|
|
37
56
|
}
|
|
38
57
|
function copyMetroOutputsIfNeeded(outputRootPath, outputMetroDir, outputContentPath, jsBundleName, sourceMapOutputPath) {
|
|
39
58
|
if (!outputMetroDir) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { program, Option } from "commander";
|
|
2
2
|
import { bundleCodePush } from "./bundleCodePush.js";
|
|
3
|
+
import { resolveBinaryBundlePathOption } from "../../functions/makeBinaryPatchBundle.js";
|
|
3
4
|
import { OUTPUT_BUNDLE_DIR, ROOT_OUTPUT_DIR, ENTRY_FILE } from "../../constant.js";
|
|
4
5
|
program.command('bundle')
|
|
5
6
|
.description('Creates a CodePush bundle file (assumes Hermes is enabled).')
|
|
@@ -10,6 +11,9 @@ program.command('bundle')
|
|
|
10
11
|
.option('-b, --bundle-name <string>', 'bundle file name (default-ios: "main.jsbundle" / default-android: "index.android.bundle")')
|
|
11
12
|
.option('--output-metro-dir <string>', 'name of directory to copy the Metro JS bundle and sourcemap before Hermes compilation')
|
|
12
13
|
.option('--output-bundle-dir <string>', 'name of directory containing the bundle file created by the "bundle" command', OUTPUT_BUNDLE_DIR)
|
|
14
|
+
.option('--binary-bundle-path <string>', 'path to the JS bundle of the target binary. Aligns the Hermes compilation with it and records it as the binary patch base.')
|
|
13
15
|
.action((options) => {
|
|
14
|
-
|
|
16
|
+
// Resolved before bundling so a wrong path fails now rather than after a build.
|
|
17
|
+
const baseBundlePath = resolveBinaryBundlePathOption(options.binaryBundlePath);
|
|
18
|
+
bundleCodePush(options.framework, options.platform, options.outputPath, options.entryFile, options.bundleName, `${options.outputPath}/${options.outputBundleDir}`, options.outputMetroDir, baseBundlePath);
|
|
15
19
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout) {
|
|
3
|
+
export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, binaryPatchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout) {
|
|
4
4
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
5
5
|
const updateInfo = releaseHistory[appVersion];
|
|
6
6
|
if (updateInfo) {
|
|
@@ -14,6 +14,11 @@ export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownl
|
|
|
14
14
|
downloadUrl: bundleDownloadUrl,
|
|
15
15
|
packageHash: packageHash,
|
|
16
16
|
};
|
|
17
|
+
// A release without a binary patch says nothing about one, so that a client reading
|
|
18
|
+
// this history downloads the full bundle exactly as it did before patches existed.
|
|
19
|
+
if (binaryPatchDownloadUrl) {
|
|
20
|
+
newReleaseHistory[appVersion].binaryPatchDownloadUrl = binaryPatchDownloadUrl;
|
|
21
|
+
}
|
|
17
22
|
if (typeof rollout === 'number') {
|
|
18
23
|
newReleaseHistory[appVersion].rollout = rollout;
|
|
19
24
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { program, Option } from "commander";
|
|
2
2
|
import { findAndReadConfigFile } from "../../utils/fsUtils.js";
|
|
3
3
|
import { release } from "./release.js";
|
|
4
|
+
import { DEFAULT_OVERSIZED_PATCH_POLICY, OVERSIZED_PATCH_POLICIES, resolveBinaryBundlePathOption, } from "../../functions/makeBinaryPatchBundle.js";
|
|
4
5
|
import { OUTPUT_BUNDLE_DIR, CONFIG_FILE_NAME, ROOT_OUTPUT_DIR, ENTRY_FILE } from "../../constant.js";
|
|
5
6
|
program.command('release')
|
|
6
7
|
.description('Deploys a new CodePush update for a target binary app.\nAfter creating the CodePush bundle, it uploads the file and updates the ReleaseHistory information.\n`bundleUploader`, `getReleaseHistory`, and `setReleaseHistory` functions should be implemented in the config file.')
|
|
@@ -21,6 +22,10 @@ program.command('release')
|
|
|
21
22
|
.option('--skip-cleanup <bool>', 'skip cleanup process', parseBoolean, false)
|
|
22
23
|
.option('--output-metro-dir <string>', 'name of directory to copy the Metro JS bundle and sourcemap before Hermes compilation')
|
|
23
24
|
.option('--output-bundle-dir <string>', 'name of directory containing the bundle file created by the "bundle" command', OUTPUT_BUNDLE_DIR)
|
|
25
|
+
.option('--binary-bundle-path <string>', 'path to the JS bundle of the target binary. Releases an additional binary patch bundle against it, and aligns the Hermes compilation with it.')
|
|
26
|
+
.addOption(new Option('--on-oversized-patch <policy>', 'what to do when the binary patch bundle is not smaller than the full bundle: "skip" releases the full bundle only, "fail" stops the release before any upload')
|
|
27
|
+
.choices(OVERSIZED_PATCH_POLICIES)
|
|
28
|
+
.default(DEFAULT_OVERSIZED_PATCH_POLICY))
|
|
24
29
|
.action(async (options) => {
|
|
25
30
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
26
31
|
if (typeof options.rollout === 'number' && (options.rollout < 0 || options.rollout > 100)) {
|
|
@@ -31,7 +36,9 @@ program.command('release')
|
|
|
31
36
|
console.error('--hash-calc option can be used only when --skip-bundle is set to true.');
|
|
32
37
|
process.exit(1);
|
|
33
38
|
}
|
|
34
|
-
|
|
39
|
+
// Resolved before bundling so a wrong path fails now rather than after a build.
|
|
40
|
+
const baseBundlePath = resolveBinaryBundlePathOption(options.binaryBundlePath);
|
|
41
|
+
await release(config.bundleUploader, config.getReleaseHistory, config.setReleaseHistory, options.binaryVersion, options.appVersion, options.framework, options.platform, options.identifier, options.outputPath, options.entryFile, options.jsBundleName, options.mandatory, options.enable, options.rollout, options.skipBundle, options.skipCleanup, `${options.outputPath}/${options.outputBundleDir}`, options.outputMetroDir, options.hashCalc, baseBundlePath, options.onOversizedPatch);
|
|
35
42
|
console.log('🚀 Release completed.');
|
|
36
43
|
});
|
|
37
44
|
function parseBoolean(value) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
3
|
+
/**
|
|
4
|
+
* Checks the command definition against the arguments it forwards, which is where an
|
|
5
|
+
* option name can silently drift: commander derives the name it stores from the flag,
|
|
6
|
+
* so reading a differently named property yields `undefined` for every release rather
|
|
7
|
+
* than an error, and every downstream default looks as if it had been chosen.
|
|
8
|
+
*/
|
|
9
|
+
jest.mock("./release.js");
|
|
10
|
+
jest.mock("../../utils/fsUtils.js", () => ({
|
|
11
|
+
findAndReadConfigFile: () => ({
|
|
12
|
+
bundleUploader: async () => ({ downloadUrl: 'https://cdn.example.com/bundle' }),
|
|
13
|
+
getReleaseHistory: async () => ({}),
|
|
14
|
+
setReleaseHistory: async () => undefined,
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
/**
|
|
18
|
+
* `release()` takes positional arguments; these are the positions this suite asserts on.
|
|
19
|
+
*/
|
|
20
|
+
const ARG_INDEX = {
|
|
21
|
+
platform: 6,
|
|
22
|
+
outputPath: 8,
|
|
23
|
+
entryFile: 9,
|
|
24
|
+
jsBundleName: 10,
|
|
25
|
+
skipBundle: 14,
|
|
26
|
+
bundleDirectory: 16,
|
|
27
|
+
baseBundlePath: 19,
|
|
28
|
+
onOversizedPatch: 20,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Parses a `release` invocation against the real command definition. Commander is asked
|
|
32
|
+
* to throw instead of exiting, and to keep its diagnostics to itself, so a rejected
|
|
33
|
+
* option can be asserted on without ending the worker or the output.
|
|
34
|
+
*/
|
|
35
|
+
async function parseReleaseCommand(args) {
|
|
36
|
+
const { program } = await import("commander");
|
|
37
|
+
await import("./index.js");
|
|
38
|
+
const releaseCommand = program.commands.find((command) => command.name() === 'release');
|
|
39
|
+
releaseCommand?.exitOverride();
|
|
40
|
+
releaseCommand?.configureOutput({ writeErr: () => { } });
|
|
41
|
+
await program.parseAsync(['release', ...args], { from: 'user' });
|
|
42
|
+
}
|
|
43
|
+
async function runReleaseCommand(args) {
|
|
44
|
+
const { release } = await import("./release.js");
|
|
45
|
+
await parseReleaseCommand(args);
|
|
46
|
+
const releaseMock = jest.mocked(release);
|
|
47
|
+
expect(releaseMock).toHaveBeenCalledTimes(1);
|
|
48
|
+
return releaseMock.mock.calls[0];
|
|
49
|
+
}
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.resetModules();
|
|
52
|
+
jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
jest.restoreAllMocks();
|
|
56
|
+
});
|
|
57
|
+
/** Relative to the CLI workspace root, which is where the test runner starts. */
|
|
58
|
+
const BASE_BUNDLE_FIXTURE = 'fixtures/binary-patch/base.bundle';
|
|
59
|
+
describe("release command options", () => {
|
|
60
|
+
it("passes the JS bundle name from -j through to the release", async () => {
|
|
61
|
+
const args = await runReleaseCommand([
|
|
62
|
+
'-b', '1.0.0',
|
|
63
|
+
'-v', '1.0.1',
|
|
64
|
+
'-p', 'android',
|
|
65
|
+
'-j', 'custom.jsbundle',
|
|
66
|
+
'--skip-bundle', 'true',
|
|
67
|
+
'--binary-bundle-path', BASE_BUNDLE_FIXTURE,
|
|
68
|
+
]);
|
|
69
|
+
expect(args[ARG_INDEX.jsBundleName]).toBe('custom.jsbundle');
|
|
70
|
+
expect(args[ARG_INDEX.platform]).toBe('android');
|
|
71
|
+
expect(args[ARG_INDEX.skipBundle]).toBe(true);
|
|
72
|
+
expect(args[ARG_INDEX.baseBundlePath]).toBe(path.resolve(BASE_BUNDLE_FIXTURE));
|
|
73
|
+
});
|
|
74
|
+
it("leaves the JS bundle name unset when -j is not given, so the platform default applies", async () => {
|
|
75
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1']);
|
|
76
|
+
expect(args[ARG_INDEX.jsBundleName]).toBeUndefined();
|
|
77
|
+
expect(args[ARG_INDEX.baseBundlePath]).toBeUndefined();
|
|
78
|
+
expect(args[ARG_INDEX.outputPath]).toBe('build');
|
|
79
|
+
expect(args[ARG_INDEX.entryFile]).toBe('index.ts');
|
|
80
|
+
expect(args[ARG_INDEX.bundleDirectory]).toBe('build/bundleOutput');
|
|
81
|
+
});
|
|
82
|
+
it("defaults the oversized patch policy to skipping the patch", async () => {
|
|
83
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1']);
|
|
84
|
+
expect(args[ARG_INDEX.onOversizedPatch]).toBe('skip');
|
|
85
|
+
});
|
|
86
|
+
it("passes the chosen oversized patch policy through to the release", async () => {
|
|
87
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1', '--on-oversized-patch', 'fail']);
|
|
88
|
+
expect(args[ARG_INDEX.onOversizedPatch]).toBe('fail');
|
|
89
|
+
});
|
|
90
|
+
it("rejects an oversized patch policy it does not know", async () => {
|
|
91
|
+
await expect(parseReleaseCommand(['-b', '1.0.0', '-v', '1.0.1', '--on-oversized-patch', 'ask']))
|
|
92
|
+
.rejects.toThrow(/--on-oversized-patch.*'ask'.*skip, fail/s);
|
|
93
|
+
const { release } = await import("./release.js");
|
|
94
|
+
expect(jest.mocked(release)).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
});
|