@bravemobile/react-native-code-push 13.1.0-beta.2 → 13.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +22 -8
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +5 -2
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +37 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +189 -2
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
- package/cli/commands/releaseCommand/index.test.ts +32 -0
- package/cli/commands/releaseCommand/index.ts +16 -1
- package/cli/commands/releaseCommand/release.test.ts +212 -5
- package/cli/commands/releaseCommand/release.ts +128 -6
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
- package/cli/dist/commands/releaseCommand/index.js +12 -2
- package/cli/dist/commands/releaseCommand/index.test.js +25 -0
- package/cli/dist/commands/releaseCommand/release.js +90 -7
- package/cli/dist/commands/releaseCommand/release.test.js +175 -5
- package/cli/dist/functions/makeAssetDiffBundle.js +114 -0
- package/cli/dist/functions/makeAssetDiffBundle.test.js +215 -0
- package/cli/dist/functions/resolveAssetDiffBases.js +71 -0
- package/cli/dist/functions/resolveAssetDiffBases.test.js +174 -0
- package/cli/dist/utils/zip.js +12 -2
- package/cli/functions/makeAssetDiffBundle.test.ts +283 -0
- package/cli/functions/makeAssetDiffBundle.ts +166 -0
- package/cli/functions/resolveAssetDiffBases.test.ts +220 -0
- package/cli/functions/resolveAssetDiffBases.ts +101 -0
- package/cli/package.json +2 -0
- package/cli/utils/zip.ts +14 -3
- package/ios/CodePush/CodePushBinaryPatch.m +21 -8
- package/ios/CodePush/CodePushPackage.m +29 -2
- package/package.json +1 -1
- package/src/CodePush.js +7 -1
- package/src/CodePush.test.js +75 -5
- package/typings/react-native-code-push.d.ts +19 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
|
|
6
|
+
import { ASSET_DIFF_ARCHIVE_INFIX, ASSET_DIFF_MANIFEST_NAME, assetDiffArchiveName, makeAssetDiffBundle, } from "./makeAssetDiffBundle.js";
|
|
7
|
+
import { BINARY_PATCH_MANIFEST_NAME, makeBinaryPatchBundle } from "./makeBinaryPatchBundle.js";
|
|
8
|
+
import { makeCodePushBundle } from "./makeCodePushBundle.js";
|
|
9
|
+
import { applyPatch } from "../utils/binaryPatch.js";
|
|
10
|
+
import { generatePackageHashFromDirectory } from "../utils/hash-utils.js";
|
|
11
|
+
import { walk } from "../utils/promisfied-fs.js";
|
|
12
|
+
import { unzip } from "../utils/unzip.js";
|
|
13
|
+
/**
|
|
14
|
+
* Exercises the diff artifact against real archives, real hdiffz output and real hashes.
|
|
15
|
+
* A diff is only worth shipping if a client that owns the base package can rebuild the
|
|
16
|
+
* exact contents of the new full archive from it, and only real bytes prove that.
|
|
17
|
+
*/
|
|
18
|
+
/** The directory a CodePush archive keeps its contents in, for legacy reasons. */
|
|
19
|
+
const CONTENTS_DIR_NAME = 'CodePush';
|
|
20
|
+
const BUNDLE_NAME = 'index.android.bundle';
|
|
21
|
+
function findRepoRoot() {
|
|
22
|
+
let dir = process.cwd();
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (fs.existsSync(path.join(dir, "scripts", "binary-patch", "build-hdiffpatch.sh"))) {
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
const parent = path.dirname(dir);
|
|
28
|
+
if (parent === dir) {
|
|
29
|
+
throw new Error(`cannot locate the repository root from ${process.cwd()}`);
|
|
30
|
+
}
|
|
31
|
+
dir = parent;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const repoRoot = findRepoRoot();
|
|
35
|
+
const fixtureDir = path.join(repoRoot, "cli", "fixtures", "binary-patch");
|
|
36
|
+
const baseFixture = path.join(fixtureDir, "base.bundle");
|
|
37
|
+
const targetFixture = path.join(fixtureDir, "target.bundle");
|
|
38
|
+
/**
|
|
39
|
+
* The asset both releases hold. Sized like a real asset rather than a few bytes, because
|
|
40
|
+
* the builder only ships a diff that is smaller than the patch archive: dropping a
|
|
41
|
+
* four-byte file would save less than the deletion manifest costs, and the builder would
|
|
42
|
+
* rightly report the diff as not worth shipping.
|
|
43
|
+
*/
|
|
44
|
+
const unchangedAsset = crypto.randomBytes(4096);
|
|
45
|
+
let workDir;
|
|
46
|
+
/** A unique directory per test so a failed run never leaks state into the next one. */
|
|
47
|
+
function makeCaseDir(name) {
|
|
48
|
+
return fs.mkdtempSync(path.join(workDir, `${name}-`));
|
|
49
|
+
}
|
|
50
|
+
/** Writes an update contents directory shaped like the one the bundler produces. */
|
|
51
|
+
function writeUpdateContents(parentDir, files) {
|
|
52
|
+
const contentsPath = path.join(parentDir, CONTENTS_DIR_NAME);
|
|
53
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
54
|
+
const filePath = path.join(contentsPath, relativePath);
|
|
55
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
56
|
+
fs.writeFileSync(filePath, content);
|
|
57
|
+
}
|
|
58
|
+
return contentsPath;
|
|
59
|
+
}
|
|
60
|
+
function baseContentFiles() {
|
|
61
|
+
return {
|
|
62
|
+
[BUNDLE_NAME]: fs.readFileSync(baseFixture),
|
|
63
|
+
'assets/keep.png': unchangedAsset,
|
|
64
|
+
'assets/change.png': 'OLD',
|
|
65
|
+
'assets/gone.png': 'GONE',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function updateContentFiles() {
|
|
69
|
+
return {
|
|
70
|
+
[BUNDLE_NAME]: fs.readFileSync(targetFixture),
|
|
71
|
+
'assets/keep.png': unchangedAsset,
|
|
72
|
+
'assets/change.png': 'NEW',
|
|
73
|
+
'assets/new.png': 'ADDED',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Releases a base package and then the update, exactly the way the CLI does: a full
|
|
78
|
+
* archive for each, plus the patch archive the diff is derived from.
|
|
79
|
+
*/
|
|
80
|
+
async function stageRelease(name, baseFiles) {
|
|
81
|
+
const caseDir = makeCaseDir(name);
|
|
82
|
+
const bundleDirectory = path.join(caseDir, 'bundleOutput');
|
|
83
|
+
const baseContents = writeUpdateContents(path.join(caseDir, 'base'), baseFiles);
|
|
84
|
+
const { bundleFileName: basePackageHash } = await makeCodePushBundle(baseContents, bundleDirectory);
|
|
85
|
+
const updateContents = writeUpdateContents(path.join(caseDir, 'update'), updateContentFiles());
|
|
86
|
+
const { bundleFileName: packageHash } = await makeCodePushBundle(updateContents, bundleDirectory);
|
|
87
|
+
const { patchBundleFilePath } = await makeBinaryPatchBundle({
|
|
88
|
+
contentsPath: updateContents,
|
|
89
|
+
baseBundlePath: baseFixture,
|
|
90
|
+
bundleRelativePath: BUNDLE_NAME,
|
|
91
|
+
bundleDirectory,
|
|
92
|
+
packageHash,
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
caseDir,
|
|
96
|
+
bundleDirectory,
|
|
97
|
+
packageHash,
|
|
98
|
+
basePackageHash,
|
|
99
|
+
baseBundleFilePath: path.join(bundleDirectory, basePackageHash),
|
|
100
|
+
patchBundleFilePath,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function extractTo(archivePath, destination) {
|
|
104
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
105
|
+
await unzip(archivePath, destination);
|
|
106
|
+
return destination;
|
|
107
|
+
}
|
|
108
|
+
/** Every file in an archive, as the archive names it. */
|
|
109
|
+
async function listArchiveFiles(archivePath, destination) {
|
|
110
|
+
await extractTo(archivePath, destination);
|
|
111
|
+
const files = await walk(destination);
|
|
112
|
+
return files.map((filePath) => path.relative(destination, filePath).split(path.sep).join('/')).sort();
|
|
113
|
+
}
|
|
114
|
+
beforeAll(() => {
|
|
115
|
+
// hdiffz/hpatchz are provisioned for the whole run by the jest global setup.
|
|
116
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-asset-diff-"));
|
|
117
|
+
});
|
|
118
|
+
afterAll(() => {
|
|
119
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
120
|
+
});
|
|
121
|
+
describe("makeAssetDiffBundle", () => {
|
|
122
|
+
it("ships only the files the base does not already hold", async () => {
|
|
123
|
+
const release = await stageRelease("ships", baseContentFiles());
|
|
124
|
+
const diff = await makeAssetDiffBundle({
|
|
125
|
+
patchBundleFilePath: release.patchBundleFilePath,
|
|
126
|
+
baseBundleFilePath: release.baseBundleFilePath,
|
|
127
|
+
bundleDirectory: release.bundleDirectory,
|
|
128
|
+
packageHash: release.packageHash,
|
|
129
|
+
basePackageHash: release.basePackageHash,
|
|
130
|
+
});
|
|
131
|
+
expect(diff).not.toBeNull();
|
|
132
|
+
expect(path.basename(diff.diffBundleFilePath)).toBe(`${release.packageHash}${ASSET_DIFF_ARCHIVE_INFIX}${release.basePackageHash}.zip`);
|
|
133
|
+
expect(await listArchiveFiles(diff.diffBundleFilePath, path.join(release.caseDir, "extracted"))).toEqual([
|
|
134
|
+
ASSET_DIFF_MANIFEST_NAME,
|
|
135
|
+
`${CONTENTS_DIR_NAME}/${BINARY_PATCH_MANIFEST_NAME}`,
|
|
136
|
+
`${CONTENTS_DIR_NAME}/${BUNDLE_NAME}.patch`,
|
|
137
|
+
`${CONTENTS_DIR_NAME}/assets/change.png`,
|
|
138
|
+
`${CONTENTS_DIR_NAME}/assets/new.png`,
|
|
139
|
+
].sort());
|
|
140
|
+
});
|
|
141
|
+
it("lists files the base holds but the update dropped, with the contents prefix", async () => {
|
|
142
|
+
const release = await stageRelease("deletions", baseContentFiles());
|
|
143
|
+
const diff = await makeAssetDiffBundle({
|
|
144
|
+
patchBundleFilePath: release.patchBundleFilePath,
|
|
145
|
+
baseBundleFilePath: release.baseBundleFilePath,
|
|
146
|
+
bundleDirectory: release.bundleDirectory,
|
|
147
|
+
packageHash: release.packageHash,
|
|
148
|
+
basePackageHash: release.basePackageHash,
|
|
149
|
+
});
|
|
150
|
+
const extracted = await extractTo(diff.diffBundleFilePath, path.join(release.caseDir, "extracted"));
|
|
151
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(extracted, ASSET_DIFF_MANIFEST_NAME), 'utf8'));
|
|
152
|
+
expect(manifest.deletedFiles).toEqual([`${CONTENTS_DIR_NAME}/assets/gone.png`]);
|
|
153
|
+
});
|
|
154
|
+
it("reproduces the full package hash when merged the way the client merges", async () => {
|
|
155
|
+
const release = await stageRelease("round-trip", baseContentFiles());
|
|
156
|
+
const diff = await makeAssetDiffBundle({
|
|
157
|
+
patchBundleFilePath: release.patchBundleFilePath,
|
|
158
|
+
baseBundleFilePath: release.baseBundleFilePath,
|
|
159
|
+
bundleDirectory: release.bundleDirectory,
|
|
160
|
+
packageHash: release.packageHash,
|
|
161
|
+
basePackageHash: release.basePackageHash,
|
|
162
|
+
});
|
|
163
|
+
// The installed base package, which is what a client merges the diff onto.
|
|
164
|
+
const packageDir = await extractTo(release.baseBundleFilePath, path.join(release.caseDir, "package"));
|
|
165
|
+
const diffDir = await extractTo(diff.diffBundleFilePath, path.join(release.caseDir, "downloaded"));
|
|
166
|
+
const diffContents = path.join(diffDir, CONTENTS_DIR_NAME);
|
|
167
|
+
// Rebuild the bundle from its patch, then drop the two patch-only files.
|
|
168
|
+
const patchManifest = JSON.parse(fs.readFileSync(path.join(diffContents, BINARY_PATCH_MANIFEST_NAME), 'utf8'));
|
|
169
|
+
applyPatch(baseFixture, path.join(diffContents, patchManifest.patchFile), path.join(diffContents, patchManifest.bundlePath));
|
|
170
|
+
fs.rmSync(path.join(diffContents, patchManifest.patchFile));
|
|
171
|
+
fs.rmSync(path.join(diffContents, BINARY_PATCH_MANIFEST_NAME));
|
|
172
|
+
// Apply the deletions to the base package, then drop the deletion manifest.
|
|
173
|
+
const diffManifestPath = path.join(diffDir, ASSET_DIFF_MANIFEST_NAME);
|
|
174
|
+
const { deletedFiles } = JSON.parse(fs.readFileSync(diffManifestPath, 'utf8'));
|
|
175
|
+
for (const deletedFile of deletedFiles) {
|
|
176
|
+
fs.rmSync(path.join(packageDir, deletedFile));
|
|
177
|
+
}
|
|
178
|
+
fs.rmSync(diffManifestPath);
|
|
179
|
+
// Overlay what the diff shipped onto the base package.
|
|
180
|
+
fs.cpSync(diffDir, packageDir, { recursive: true });
|
|
181
|
+
expect(await generatePackageHashFromDirectory(path.join(packageDir, CONTENTS_DIR_NAME), packageDir)).toBe(release.packageHash);
|
|
182
|
+
});
|
|
183
|
+
it("skips the diff when it would not be smaller than the patch archive", async () => {
|
|
184
|
+
// A base that shares no bytes with the update leaves nothing to prune, so the diff
|
|
185
|
+
// is the patch archive plus a deletion manifest.
|
|
186
|
+
const release = await stageRelease("oversized", {
|
|
187
|
+
[BUNDLE_NAME]: fs.readFileSync(baseFixture),
|
|
188
|
+
'assets/keep.png': crypto.randomBytes(4096),
|
|
189
|
+
'assets/change.png': 'OLD',
|
|
190
|
+
});
|
|
191
|
+
const archivesBefore = fs.readdirSync(release.bundleDirectory).sort();
|
|
192
|
+
const diff = await makeAssetDiffBundle({
|
|
193
|
+
patchBundleFilePath: release.patchBundleFilePath,
|
|
194
|
+
baseBundleFilePath: release.baseBundleFilePath,
|
|
195
|
+
bundleDirectory: release.bundleDirectory,
|
|
196
|
+
packageHash: release.packageHash,
|
|
197
|
+
basePackageHash: release.basePackageHash,
|
|
198
|
+
});
|
|
199
|
+
expect(diff).toBeNull();
|
|
200
|
+
expect(fs.existsSync(path.join(release.bundleDirectory, assetDiffArchiveName(release.packageHash, release.basePackageHash)))).toBe(false);
|
|
201
|
+
expect(fs.readdirSync(release.bundleDirectory).sort()).toEqual(archivesBefore);
|
|
202
|
+
});
|
|
203
|
+
it("leaves no working directory behind", async () => {
|
|
204
|
+
const release = await stageRelease("cleanup", baseContentFiles());
|
|
205
|
+
const entriesBefore = fs.readdirSync(release.caseDir).sort();
|
|
206
|
+
await makeAssetDiffBundle({
|
|
207
|
+
patchBundleFilePath: release.patchBundleFilePath,
|
|
208
|
+
baseBundleFilePath: release.baseBundleFilePath,
|
|
209
|
+
bundleDirectory: release.bundleDirectory,
|
|
210
|
+
packageHash: release.packageHash,
|
|
211
|
+
basePackageHash: release.basePackageHash,
|
|
212
|
+
});
|
|
213
|
+
expect(fs.readdirSync(release.caseDir).sort()).toEqual(entriesBefore);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Picks the previously released packages a new release builds asset diffs against, and
|
|
3
|
+
* hands back a verified local copy of each.
|
|
4
|
+
*
|
|
5
|
+
* A diff is only installable by a client whose package is byte for byte the base it was
|
|
6
|
+
* built against, so the base has to be the exact archive the release history describes.
|
|
7
|
+
* Every download is therefore unpacked and hashed before it is accepted: a base that is
|
|
8
|
+
* not what the history recorded would produce a diff that sends every client holding
|
|
9
|
+
* that version into the full-download fallback.
|
|
10
|
+
*
|
|
11
|
+
* Bases are the most recent releases, because that is where the installed clients are.
|
|
12
|
+
* One base failing is never a reason to fail the release - the release still publishes
|
|
13
|
+
* its full and patch archives, and simply serves fewer diffs.
|
|
14
|
+
*/
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import os from "os";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import semver from "semver";
|
|
19
|
+
import { generatePackageHashFromDirectory } from "../utils/hash-utils.js";
|
|
20
|
+
import { extractCodePushBundleContents } from "./makeBinaryPatchBundle.js";
|
|
21
|
+
const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
|
|
22
|
+
/**
|
|
23
|
+
* @param releaseHistory {ReleaseHistoryInterface} History of the binary version being released to
|
|
24
|
+
* @param diffBaseCount {number} How many of the most recent releases to build diffs against
|
|
25
|
+
* @param bundleDownloader Consumer-provided downloader for a released archive
|
|
26
|
+
* @return The bases that downloaded and verified, newest release first.
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveAssetDiffBases({ releaseHistory, diffBaseCount, bundleDownloader, platform, identifier, }) {
|
|
29
|
+
// The seeded entry for the update inside the app binary has neither a url nor a hash,
|
|
30
|
+
// and nothing can be diffed against a package that was never published. A key that is
|
|
31
|
+
// not a version cannot be ordered against the others either, and ordering is what picks
|
|
32
|
+
// the recent releases, so it is not eligible.
|
|
33
|
+
const candidates = Object.entries(releaseHistory)
|
|
34
|
+
.filter(([version, releaseInfo]) => Boolean(semver.valid(version)) && Boolean(releaseInfo.downloadUrl) && Boolean(releaseInfo.packageHash))
|
|
35
|
+
.sort(([left], [right]) => semver.rcompare(left, right))
|
|
36
|
+
.slice(0, diffBaseCount);
|
|
37
|
+
const bases = [];
|
|
38
|
+
for (const [version, releaseInfo] of candidates) {
|
|
39
|
+
try {
|
|
40
|
+
const { downloadedFilePath } = await bundleDownloader(releaseInfo.downloadUrl, platform, identifier);
|
|
41
|
+
if (!(await matchesPackageHash(downloadedFilePath, releaseInfo.packageHash))) {
|
|
42
|
+
console.warn(`warn: Skipping the asset diff base v${version}: the downloaded archive does not hash to the ` +
|
|
43
|
+
`package hash recorded for it (${releaseInfo.packageHash}).`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
bases.push({ basePackageHash: releaseInfo.packageHash, baseBundleFilePath: downloadedFilePath });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.warn(`warn: Skipping the asset diff base v${version}: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return bases;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether an archive holds the contents the release history recorded for it.
|
|
56
|
+
*
|
|
57
|
+
* Unpacked into a working directory of its own: the extraction reuses one directory name,
|
|
58
|
+
* so bases sharing a parent would have the second unpack wipe the first.
|
|
59
|
+
*/
|
|
60
|
+
async function matchesPackageHash(bundleFilePath, packageHash) {
|
|
61
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), TEMP_DIR_PREFIX));
|
|
62
|
+
try {
|
|
63
|
+
const { contentsPath } = await extractCodePushBundleContents(bundleFilePath, tempRoot);
|
|
64
|
+
// Hashed relative to the contents parent, the way the archive itself was hashed.
|
|
65
|
+
const actualHash = await generatePackageHashFromDirectory(contentsPath, path.join(contentsPath, '..'));
|
|
66
|
+
return actualHash === packageHash;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
|
|
5
|
+
import { makeCodePushBundle } from "./makeCodePushBundle.js";
|
|
6
|
+
import { resolveAssetDiffBases } from "./resolveAssetDiffBases.js";
|
|
7
|
+
/**
|
|
8
|
+
* Exercises base acquisition against real archives and real hashes: a base is only usable
|
|
9
|
+
* if the bytes that came back are the ones the release history recorded, and only real
|
|
10
|
+
* archives prove that the verification accepts a good download and rejects a wrong one.
|
|
11
|
+
*/
|
|
12
|
+
/** The directory a CodePush archive keeps its contents in, for legacy reasons. */
|
|
13
|
+
const CONTENTS_DIR_NAME = 'CodePush';
|
|
14
|
+
/** Where the resolver unpacks a base for verification, so leftovers are recognizable. */
|
|
15
|
+
const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
|
|
16
|
+
let workDir;
|
|
17
|
+
let bundleDirectory;
|
|
18
|
+
/** Releases one full archive the way the CLI does, and returns what the history records. */
|
|
19
|
+
async function stageRelease(version) {
|
|
20
|
+
const contentsPath = path.join(workDir, version, CONTENTS_DIR_NAME);
|
|
21
|
+
fs.mkdirSync(contentsPath, { recursive: true });
|
|
22
|
+
fs.writeFileSync(path.join(contentsPath, 'index.android.bundle'), `bundle bytes of ${version}`);
|
|
23
|
+
const { bundleFileName } = await makeCodePushBundle(contentsPath, bundleDirectory);
|
|
24
|
+
return {
|
|
25
|
+
version,
|
|
26
|
+
downloadUrl: `https://downloads.test/${version}.zip`,
|
|
27
|
+
packageHash: bundleFileName,
|
|
28
|
+
archivePath: path.join(bundleDirectory, bundleFileName),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function stageReleases(versions) {
|
|
32
|
+
const releases = [];
|
|
33
|
+
for (const version of versions) {
|
|
34
|
+
releases.push(await stageRelease(version));
|
|
35
|
+
}
|
|
36
|
+
return releases;
|
|
37
|
+
}
|
|
38
|
+
function releaseHistoryOf(releases) {
|
|
39
|
+
return Object.fromEntries(releases.map((release) => [
|
|
40
|
+
release.version,
|
|
41
|
+
{
|
|
42
|
+
enabled: true,
|
|
43
|
+
mandatory: false,
|
|
44
|
+
downloadUrl: release.downloadUrl,
|
|
45
|
+
packageHash: release.packageHash,
|
|
46
|
+
},
|
|
47
|
+
]));
|
|
48
|
+
}
|
|
49
|
+
/** Stands in for the consumer's downloader: hands back a local archive per download url. */
|
|
50
|
+
function fakeDownloader(archives) {
|
|
51
|
+
const calls = [];
|
|
52
|
+
const download = async (downloadUrl, platform, identifier) => {
|
|
53
|
+
calls.push([downloadUrl, platform, identifier]);
|
|
54
|
+
const archive = archives[downloadUrl];
|
|
55
|
+
if (archive instanceof Error) {
|
|
56
|
+
throw archive;
|
|
57
|
+
}
|
|
58
|
+
if (!archive) {
|
|
59
|
+
throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
|
|
60
|
+
}
|
|
61
|
+
return { downloadedFilePath: archive };
|
|
62
|
+
};
|
|
63
|
+
return { download, calls };
|
|
64
|
+
}
|
|
65
|
+
function leftoverTempDirs() {
|
|
66
|
+
return fs.readdirSync(os.tmpdir()).filter((entry) => entry.startsWith(TEMP_DIR_PREFIX));
|
|
67
|
+
}
|
|
68
|
+
beforeAll(() => {
|
|
69
|
+
workDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-base-resolution-"));
|
|
70
|
+
bundleDirectory = path.join(workDir, 'bundleOutput');
|
|
71
|
+
});
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
jest.restoreAllMocks();
|
|
74
|
+
});
|
|
75
|
+
afterAll(() => {
|
|
76
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
77
|
+
});
|
|
78
|
+
describe("resolveAssetDiffBases", () => {
|
|
79
|
+
it("takes the newest releases first, up to the requested count", async () => {
|
|
80
|
+
const releases = await stageReleases(['1.0.0', '1.0.1', '1.0.2', '1.0.9', '1.0.10']);
|
|
81
|
+
const [newest, second] = [releases[4], releases[3]];
|
|
82
|
+
const { download, calls } = fakeDownloader(Object.fromEntries(releases.map((release) => [release.downloadUrl, release.archivePath])));
|
|
83
|
+
const bases = await resolveAssetDiffBases({
|
|
84
|
+
releaseHistory: releaseHistoryOf(releases),
|
|
85
|
+
diffBaseCount: 2,
|
|
86
|
+
bundleDownloader: download,
|
|
87
|
+
platform: 'android',
|
|
88
|
+
identifier: 'my-app',
|
|
89
|
+
});
|
|
90
|
+
expect(bases).toEqual([
|
|
91
|
+
{ basePackageHash: newest.packageHash, baseBundleFilePath: newest.archivePath },
|
|
92
|
+
{ basePackageHash: second.packageHash, baseBundleFilePath: second.archivePath },
|
|
93
|
+
]);
|
|
94
|
+
expect(calls).toEqual([
|
|
95
|
+
[newest.downloadUrl, 'android', 'my-app'],
|
|
96
|
+
[second.downloadUrl, 'android', 'my-app'],
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
it("skips history entries that lack a download url or a package hash", async () => {
|
|
100
|
+
const [released] = await stageReleases(['1.1.0']);
|
|
101
|
+
const { download, calls } = fakeDownloader({ [released.downloadUrl]: released.archivePath });
|
|
102
|
+
const bases = await resolveAssetDiffBases({
|
|
103
|
+
releaseHistory: {
|
|
104
|
+
...releaseHistoryOf([released]),
|
|
105
|
+
// What `create-history` seeds for the update inside the app binary.
|
|
106
|
+
'1.1.1': { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" },
|
|
107
|
+
'1.1.2': { enabled: true, mandatory: false, downloadUrl: released.downloadUrl, packageHash: "" },
|
|
108
|
+
},
|
|
109
|
+
diffBaseCount: 1,
|
|
110
|
+
bundleDownloader: download,
|
|
111
|
+
platform: 'ios',
|
|
112
|
+
});
|
|
113
|
+
expect(bases).toEqual([
|
|
114
|
+
{ basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
|
|
115
|
+
]);
|
|
116
|
+
expect(calls).toEqual([[released.downloadUrl, 'ios', undefined]]);
|
|
117
|
+
});
|
|
118
|
+
it("ignores a history entry whose key is not a version, and resolves the rest", async () => {
|
|
119
|
+
const [released] = await stageReleases(['1.4.0']);
|
|
120
|
+
const { download, calls } = fakeDownloader({ [released.downloadUrl]: released.archivePath });
|
|
121
|
+
const bases = await resolveAssetDiffBases({
|
|
122
|
+
releaseHistory: {
|
|
123
|
+
...releaseHistoryOf([released]),
|
|
124
|
+
'not-a-version': {
|
|
125
|
+
enabled: true,
|
|
126
|
+
mandatory: false,
|
|
127
|
+
downloadUrl: released.downloadUrl,
|
|
128
|
+
packageHash: released.packageHash,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
diffBaseCount: 2,
|
|
132
|
+
bundleDownloader: download,
|
|
133
|
+
platform: 'ios',
|
|
134
|
+
});
|
|
135
|
+
expect(bases).toEqual([
|
|
136
|
+
{ basePackageHash: released.packageHash, baseBundleFilePath: released.archivePath },
|
|
137
|
+
]);
|
|
138
|
+
expect(calls).toEqual([[released.downloadUrl, 'ios', undefined]]);
|
|
139
|
+
});
|
|
140
|
+
it("drops a base whose downloaded bytes do not match its recorded package hash", async () => {
|
|
141
|
+
const [older, newer] = await stageReleases(['1.2.0', '1.2.1']);
|
|
142
|
+
// The newest base comes back as some other release's archive.
|
|
143
|
+
const { download } = fakeDownloader({
|
|
144
|
+
[newer.downloadUrl]: older.archivePath,
|
|
145
|
+
[older.downloadUrl]: older.archivePath,
|
|
146
|
+
});
|
|
147
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
148
|
+
const bases = await resolveAssetDiffBases({
|
|
149
|
+
releaseHistory: releaseHistoryOf([older, newer]),
|
|
150
|
+
diffBaseCount: 2,
|
|
151
|
+
bundleDownloader: download,
|
|
152
|
+
platform: 'android',
|
|
153
|
+
});
|
|
154
|
+
expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
|
|
155
|
+
expect(warn.mock.calls.join('\n')).toMatch(/1\.2\.1/);
|
|
156
|
+
expect(leftoverTempDirs()).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
it("drops a base whose download fails instead of failing the release", async () => {
|
|
159
|
+
const [older, newer] = await stageReleases(['1.3.0', '1.3.1']);
|
|
160
|
+
const { download } = fakeDownloader({
|
|
161
|
+
[newer.downloadUrl]: new Error('the network is down'),
|
|
162
|
+
[older.downloadUrl]: older.archivePath,
|
|
163
|
+
});
|
|
164
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
165
|
+
const bases = await resolveAssetDiffBases({
|
|
166
|
+
releaseHistory: releaseHistoryOf([older, newer]),
|
|
167
|
+
diffBaseCount: 2,
|
|
168
|
+
bundleDownloader: download,
|
|
169
|
+
platform: 'android',
|
|
170
|
+
});
|
|
171
|
+
expect(bases).toEqual([{ basePackageHash: older.packageHash, baseBundleFilePath: older.archivePath }]);
|
|
172
|
+
expect(warn.mock.calls.join('\n')).toMatch(/1\.3\.1.*the network is down/s);
|
|
173
|
+
});
|
|
174
|
+
});
|
package/cli/dist/utils/zip.js
CHANGED
|
@@ -7,6 +7,18 @@ import yazl from "yazl";
|
|
|
7
7
|
import { generateRandomFilename, normalizePath, isDirectory } from "./file-utils.js";
|
|
8
8
|
import { walk } from "./promisfied-fs.js";
|
|
9
9
|
export function zip(updateContentsPath) {
|
|
10
|
+
// For legacy reasons, put the root directory in the zip
|
|
11
|
+
return zipRelativeTo(updateContentsPath, path.join(updateContentsPath, '..'));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Zips the contents of `rootPath` with entries relative to the root itself, so a file
|
|
15
|
+
* placed directly under the root lands at the archive root rather than inside a wrapper
|
|
16
|
+
* directory.
|
|
17
|
+
*/
|
|
18
|
+
export function zipDirectoryContents(rootPath) {
|
|
19
|
+
return zipRelativeTo(rootPath, rootPath);
|
|
20
|
+
}
|
|
21
|
+
function zipRelativeTo(updateContentsPath, baseDirectoryPath) {
|
|
10
22
|
// eslint-disable-next-line no-async-promise-executor
|
|
11
23
|
return new Promise(async (resolve, reject) => {
|
|
12
24
|
const releaseFiles = [];
|
|
@@ -24,8 +36,6 @@ export function zip(updateContentsPath) {
|
|
|
24
36
|
}
|
|
25
37
|
reject(error);
|
|
26
38
|
}
|
|
27
|
-
const directoryPath = updateContentsPath;
|
|
28
|
-
const baseDirectoryPath = path.join(directoryPath, '..'); // For legacy reasons, put the root directory in the zip
|
|
29
39
|
const files = await walk(updateContentsPath);
|
|
30
40
|
files.forEach((filePath) => {
|
|
31
41
|
const relativePath = path.relative(baseDirectoryPath, filePath);
|