@expo-up/cli 0.1.3 → 0.1.5
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/dist/index.js +60889 -16854
- package/package.json +7 -3
- package/CHANGELOG.md +0 -54
- package/src/auth.ts +0 -135
- package/src/channels.tsx +0 -123
- package/src/cli-utils.test.ts +0 -25
- package/src/cli-utils.ts +0 -19
- package/src/codesigning.test.ts +0 -165
- package/src/codesigning.ts +0 -265
- package/src/history-utils.test.ts +0 -23
- package/src/history-utils.ts +0 -24
- package/src/history.tsx +0 -559
- package/src/index.tsx +0 -368
- package/src/release-utils.test.ts +0 -221
- package/src/release-utils.ts +0 -146
- package/src/release.tsx +0 -511
- package/src/rollback-utils.test.ts +0 -74
- package/src/rollback-utils.ts +0 -53
- package/src/rollback.tsx +0 -267
- package/src/ui.tsx +0 -99
- package/tsconfig.json +0 -12
package/src/index.tsx
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
import { render } from "ink";
|
|
4
|
-
import { program } from "commander";
|
|
5
|
-
import pc from "picocolors";
|
|
6
|
-
import { createInterface } from "node:readline/promises";
|
|
7
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
8
|
-
import {
|
|
9
|
-
login,
|
|
10
|
-
logout,
|
|
11
|
-
getStoredToken,
|
|
12
|
-
writeConfig,
|
|
13
|
-
getStoredChannel,
|
|
14
|
-
getAutoConfig,
|
|
15
|
-
} from "./auth";
|
|
16
|
-
import { Release } from "./release";
|
|
17
|
-
import { Rollback } from "./rollback";
|
|
18
|
-
import { History } from "./history";
|
|
19
|
-
import { ListChannels } from "./channels";
|
|
20
|
-
import { DEFAULT_CHANNEL } from "../../core/src/index";
|
|
21
|
-
import { maskToken, parsePlatform } from "./cli-utils";
|
|
22
|
-
import { configureCodesigning, generateCodesigning } from "./codesigning";
|
|
23
|
-
|
|
24
|
-
function withCommandErrorBoundary<TArgs extends unknown[]>(
|
|
25
|
-
action: (...args: TArgs) => Promise<void> | void,
|
|
26
|
-
): (...args: TArgs) => Promise<void> {
|
|
27
|
-
return async (...args: TArgs) => {
|
|
28
|
-
try {
|
|
29
|
-
await action(...args);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
32
|
-
console.error(pc.red(`Error: ${message}`));
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function askQuestion(question: string): Promise<string> {
|
|
39
|
-
const rl = createInterface({ input, output });
|
|
40
|
-
try {
|
|
41
|
-
return (await rl.question(question)).trim();
|
|
42
|
-
} finally {
|
|
43
|
-
rl.close();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
program
|
|
48
|
-
.name("expo-up")
|
|
49
|
-
.description("Beautiful & Fast Revamp for Expo Workflows")
|
|
50
|
-
.version("0.1.0")
|
|
51
|
-
.option("-d, --debug", "Enable verbose debug logging", false);
|
|
52
|
-
|
|
53
|
-
// --- CHANNEL COMMANDS ---
|
|
54
|
-
program
|
|
55
|
-
.command("set-channel <name>")
|
|
56
|
-
.description("Set your active channel (e.g., main, staging)")
|
|
57
|
-
.action((name: string) => {
|
|
58
|
-
writeConfig({ channel: name });
|
|
59
|
-
console.log(`${pc.green("✔")} Active channel set to: ${pc.cyan(name)}`);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
program
|
|
63
|
-
.command("list-channels")
|
|
64
|
-
.description("List available channels in your storage repository")
|
|
65
|
-
.action(() => {
|
|
66
|
-
const debug = program.opts().debug;
|
|
67
|
-
render(<ListChannels debug={debug} />);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// --- AUTH COMMANDS ---
|
|
71
|
-
program
|
|
72
|
-
.command("login")
|
|
73
|
-
.description("Authenticate with GitHub")
|
|
74
|
-
.action(
|
|
75
|
-
withCommandErrorBoundary(async () => {
|
|
76
|
-
await login();
|
|
77
|
-
process.exit(0);
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
program
|
|
82
|
-
.command("logout")
|
|
83
|
-
.description("Clear local session")
|
|
84
|
-
.action(() => {
|
|
85
|
-
logout();
|
|
86
|
-
process.exit(0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
program
|
|
90
|
-
.command("whoami")
|
|
91
|
-
.description("Check currently logged in session and project info")
|
|
92
|
-
.action(() => {
|
|
93
|
-
const token = getStoredToken();
|
|
94
|
-
const { serverUrl, projectId } = getAutoConfig();
|
|
95
|
-
const channel = getStoredChannel();
|
|
96
|
-
|
|
97
|
-
console.log(
|
|
98
|
-
`${pc.blue("ℹ")} ${pc.bold("Project ID:")} ${projectId || pc.red("Not found in Expo config")}`,
|
|
99
|
-
);
|
|
100
|
-
console.log(
|
|
101
|
-
`${pc.blue("ℹ")} ${pc.bold("Channel:")} ${pc.cyan(channel)}`,
|
|
102
|
-
);
|
|
103
|
-
console.log(
|
|
104
|
-
`${pc.blue("ℹ")} ${pc.bold("Server:")} ${serverUrl || pc.red("Not set")}`,
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
if (token) {
|
|
108
|
-
console.log(
|
|
109
|
-
`${pc.green("✔")} ${pc.bold("Status:")} Logged in (${pc.dim(maskToken(token))})`,
|
|
110
|
-
);
|
|
111
|
-
} else {
|
|
112
|
-
console.log(`${pc.yellow("⚠")} ${pc.bold("Status:")} Not logged in.`);
|
|
113
|
-
}
|
|
114
|
-
process.exit(0);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// --- CORE COMMANDS ---
|
|
118
|
-
program
|
|
119
|
-
.command("release")
|
|
120
|
-
.description("Bundle and upload an update")
|
|
121
|
-
.option("-p, --platform <platform>", "ios, android, or all", "all")
|
|
122
|
-
.option("-c, --channel <channel>", "Override active channel")
|
|
123
|
-
.action((options) => {
|
|
124
|
-
const channel = options.channel || getStoredChannel() || DEFAULT_CHANNEL;
|
|
125
|
-
const platform = parsePlatform(options.platform);
|
|
126
|
-
const debug = program.opts().debug;
|
|
127
|
-
render(<Release channel={channel} platform={platform} debug={debug} />);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
program
|
|
131
|
-
.command("rollback")
|
|
132
|
-
.description("Rollback to a previous build")
|
|
133
|
-
.option("-c, --channel <channel>", "Override active channel")
|
|
134
|
-
.option("-t, --to <build>", "Specific build ID")
|
|
135
|
-
.option("-e, --embedded", "Rollback to native build")
|
|
136
|
-
.action((options) => {
|
|
137
|
-
const channel = options.channel || getStoredChannel() || DEFAULT_CHANNEL;
|
|
138
|
-
const debug = program.opts().debug;
|
|
139
|
-
render(
|
|
140
|
-
<Rollback
|
|
141
|
-
channel={channel}
|
|
142
|
-
to={options.to}
|
|
143
|
-
embedded={options.embedded}
|
|
144
|
-
debug={debug}
|
|
145
|
-
/>,
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
program
|
|
150
|
-
.command("history")
|
|
151
|
-
.description("View build history for a channel")
|
|
152
|
-
.option("-c, --channel <channel>", "Override active channel")
|
|
153
|
-
.option(
|
|
154
|
-
"--delete <buildIds...>",
|
|
155
|
-
"Delete one or more build IDs (CI-friendly, supports comma or space-separated values)",
|
|
156
|
-
)
|
|
157
|
-
.option("--yes", "Skip confirmation prompt for delete actions", false)
|
|
158
|
-
.option(
|
|
159
|
-
"--no-interactive-delete",
|
|
160
|
-
"Disable interactive multi-select delete mode for build history",
|
|
161
|
-
)
|
|
162
|
-
.action((options) => {
|
|
163
|
-
const channel = options.channel || getStoredChannel() || DEFAULT_CHANNEL;
|
|
164
|
-
const debug = program.opts().debug;
|
|
165
|
-
render(
|
|
166
|
-
<History
|
|
167
|
-
channel={channel}
|
|
168
|
-
debug={debug}
|
|
169
|
-
deleteBuildIds={options.delete}
|
|
170
|
-
interactiveDelete={options.interactiveDelete}
|
|
171
|
-
yes={options.yes}
|
|
172
|
-
/>,
|
|
173
|
-
);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
program
|
|
177
|
-
.command("codesigning:generate")
|
|
178
|
-
.description(
|
|
179
|
-
"Generate code signing keys/certificate and configure Expo config",
|
|
180
|
-
)
|
|
181
|
-
.option(
|
|
182
|
-
"-o, --organization <organization>",
|
|
183
|
-
"Organization name for certificate issuer (O)",
|
|
184
|
-
)
|
|
185
|
-
.option(
|
|
186
|
-
"--certificate-validity-duration-years <years>",
|
|
187
|
-
"Certificate validity in years",
|
|
188
|
-
(value) => Number.parseInt(value, 10),
|
|
189
|
-
)
|
|
190
|
-
.option(
|
|
191
|
-
"--key-id <id>",
|
|
192
|
-
"Key ID to write into Expo config (default: main)",
|
|
193
|
-
"main",
|
|
194
|
-
)
|
|
195
|
-
.option(
|
|
196
|
-
"-p, --project-root <path>",
|
|
197
|
-
"Expo app root containing app.json or app.config.* (defaults to auto-detect)",
|
|
198
|
-
)
|
|
199
|
-
.option(
|
|
200
|
-
"--key-output-directory <path>",
|
|
201
|
-
"Directory for private/public key output (default: codesigning-keys)",
|
|
202
|
-
"codesigning-keys",
|
|
203
|
-
)
|
|
204
|
-
.option(
|
|
205
|
-
"--certificate-output-directory <path>",
|
|
206
|
-
"Directory for certificate output (default: certs)",
|
|
207
|
-
"certs",
|
|
208
|
-
)
|
|
209
|
-
.option("--force", "Overwrite existing codesigning-keys directory", false)
|
|
210
|
-
.action(
|
|
211
|
-
withCommandErrorBoundary(async (options: Record<string, unknown>) => {
|
|
212
|
-
const isInteractive = Boolean(
|
|
213
|
-
process.stdin.isTTY && process.stdout.isTTY,
|
|
214
|
-
);
|
|
215
|
-
let organization =
|
|
216
|
-
typeof options.organization === "string" ? options.organization : "";
|
|
217
|
-
let validityYears: number = Number.isFinite(
|
|
218
|
-
options.certificateValidityDurationYears,
|
|
219
|
-
)
|
|
220
|
-
? (options.certificateValidityDurationYears as number)
|
|
221
|
-
: Number.NaN;
|
|
222
|
-
let keyId = typeof options.keyId === "string" ? options.keyId : "main";
|
|
223
|
-
|
|
224
|
-
if (!organization) {
|
|
225
|
-
if (!isInteractive) {
|
|
226
|
-
throw new Error("Missing --organization in non-interactive mode.");
|
|
227
|
-
}
|
|
228
|
-
organization = await askQuestion("Organization name: ");
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (!Number.isFinite(validityYears)) {
|
|
232
|
-
if (!isInteractive) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
"Missing --certificate-validity-duration-years in non-interactive mode.",
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
const value = await askQuestion(
|
|
238
|
-
"Certificate validity in years (e.g. 10): ",
|
|
239
|
-
);
|
|
240
|
-
validityYears = Number.parseInt(value, 10);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (isInteractive) {
|
|
244
|
-
const promptKeyId = await askQuestion("Key ID (default: main): ");
|
|
245
|
-
if (promptKeyId.trim()) keyId = promptKeyId.trim();
|
|
246
|
-
}
|
|
247
|
-
if (!keyId.trim()) keyId = "main";
|
|
248
|
-
|
|
249
|
-
const generated = generateCodesigning({
|
|
250
|
-
organization,
|
|
251
|
-
validityYears,
|
|
252
|
-
projectRoot:
|
|
253
|
-
typeof options.projectRoot === "string"
|
|
254
|
-
? options.projectRoot
|
|
255
|
-
: undefined,
|
|
256
|
-
keyOutputDirectory:
|
|
257
|
-
typeof options.keyOutputDirectory === "string"
|
|
258
|
-
? options.keyOutputDirectory
|
|
259
|
-
: "codesigning-keys",
|
|
260
|
-
certificateOutputDirectory:
|
|
261
|
-
typeof options.certificateOutputDirectory === "string"
|
|
262
|
-
? options.certificateOutputDirectory
|
|
263
|
-
: "certs",
|
|
264
|
-
force: Boolean(options.force),
|
|
265
|
-
});
|
|
266
|
-
const configured = configureCodesigning({
|
|
267
|
-
projectRoot: generated.projectRoot,
|
|
268
|
-
certificateInputDirectory:
|
|
269
|
-
typeof options.certificateOutputDirectory === "string"
|
|
270
|
-
? options.certificateOutputDirectory
|
|
271
|
-
: "certs",
|
|
272
|
-
keyInputDirectory:
|
|
273
|
-
typeof options.keyOutputDirectory === "string"
|
|
274
|
-
? options.keyOutputDirectory
|
|
275
|
-
: "codesigning-keys",
|
|
276
|
-
keyId,
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
console.log(`${pc.green("✔")} Code signing keys generated`);
|
|
280
|
-
console.log(
|
|
281
|
-
`${pc.blue("ℹ")} ${pc.bold("Project:")} ${pc.cyan(generated.projectRoot)}`,
|
|
282
|
-
);
|
|
283
|
-
console.log(
|
|
284
|
-
`${pc.blue("ℹ")} ${pc.bold("Keys:")} ${generated.keyOutputDir}`,
|
|
285
|
-
);
|
|
286
|
-
console.log(
|
|
287
|
-
`${pc.blue("ℹ")} ${pc.bold("Cert:")} ${generated.certificateOutputDir}`,
|
|
288
|
-
);
|
|
289
|
-
console.log(`${pc.green("✔")} Expo config configured`);
|
|
290
|
-
console.log(
|
|
291
|
-
`${pc.blue("ℹ")} ${pc.bold("Key ID:")} ${pc.cyan(configured.keyId)}`,
|
|
292
|
-
);
|
|
293
|
-
console.log("");
|
|
294
|
-
console.log(pc.bold("Server setup (private key):"));
|
|
295
|
-
console.log(
|
|
296
|
-
`1) Set env var with private key PEM content from ${generated.privateKeyPath}`,
|
|
297
|
-
);
|
|
298
|
-
console.log(`2) Set env var for key id: ${configured.keyId}`);
|
|
299
|
-
console.log("3) In your server configureExpoUp(...) set:");
|
|
300
|
-
console.log(
|
|
301
|
-
" certificate: { privateKey: c.env.MY_APP_PRIVATE_KEY, keyId: c.env.MY_APP_KEY_ID }",
|
|
302
|
-
);
|
|
303
|
-
process.exit(0);
|
|
304
|
-
}),
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
program
|
|
308
|
-
.command("codesigning:configure")
|
|
309
|
-
.description(
|
|
310
|
-
"Configure Expo config codeSigning fields from existing cert/key",
|
|
311
|
-
)
|
|
312
|
-
.option(
|
|
313
|
-
"-p, --project-root <path>",
|
|
314
|
-
"Expo app root containing app.json or app.config.* (defaults to auto-detect)",
|
|
315
|
-
)
|
|
316
|
-
.option(
|
|
317
|
-
"--certificate-input-directory <path>",
|
|
318
|
-
"Directory containing certificate.pem (default: certs)",
|
|
319
|
-
"certs",
|
|
320
|
-
)
|
|
321
|
-
.option(
|
|
322
|
-
"--key-input-directory <path>",
|
|
323
|
-
"Directory containing private-key.pem/public-key.pem (default: codesigning-keys)",
|
|
324
|
-
"codesigning-keys",
|
|
325
|
-
)
|
|
326
|
-
.option("--key-id <id>", "Key ID for Expo config codeSigningMetadata")
|
|
327
|
-
.action(
|
|
328
|
-
withCommandErrorBoundary(async (options: Record<string, unknown>) => {
|
|
329
|
-
const isInteractive = Boolean(
|
|
330
|
-
process.stdin.isTTY && process.stdout.isTTY,
|
|
331
|
-
);
|
|
332
|
-
let keyId = typeof options.keyId === "string" ? options.keyId : "";
|
|
333
|
-
if (isInteractive) {
|
|
334
|
-
keyId = await askQuestion("Key ID (default: main): ");
|
|
335
|
-
}
|
|
336
|
-
if (!keyId.trim()) keyId = "main";
|
|
337
|
-
|
|
338
|
-
const result = configureCodesigning({
|
|
339
|
-
projectRoot:
|
|
340
|
-
typeof options.projectRoot === "string"
|
|
341
|
-
? options.projectRoot
|
|
342
|
-
: undefined,
|
|
343
|
-
certificateInputDirectory:
|
|
344
|
-
typeof options.certificateInputDirectory === "string"
|
|
345
|
-
? options.certificateInputDirectory
|
|
346
|
-
: "certs",
|
|
347
|
-
keyInputDirectory:
|
|
348
|
-
typeof options.keyInputDirectory === "string"
|
|
349
|
-
? options.keyInputDirectory
|
|
350
|
-
: "codesigning-keys",
|
|
351
|
-
keyId,
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
console.log(`${pc.green("✔")} Expo config configured for code signing`);
|
|
355
|
-
console.log(
|
|
356
|
-
`${pc.blue("ℹ")} ${pc.bold("Project:")} ${pc.cyan(result.projectRoot)}`,
|
|
357
|
-
);
|
|
358
|
-
console.log(
|
|
359
|
-
`${pc.blue("ℹ")} ${pc.bold("Certificate:")} ${result.certificatePath}`,
|
|
360
|
-
);
|
|
361
|
-
console.log(
|
|
362
|
-
`${pc.blue("ℹ")} ${pc.bold("Key ID:")} ${pc.cyan(result.keyId)}`,
|
|
363
|
-
);
|
|
364
|
-
process.exit(0);
|
|
365
|
-
}),
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
program.parse();
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/// <reference path="../../typescript-config/bun-test-shim.d.ts" />
|
|
2
|
-
import { describe, expect, it } from "bun:test";
|
|
3
|
-
import {
|
|
4
|
-
createMetadataFingerprint,
|
|
5
|
-
createSortedMetadataHash,
|
|
6
|
-
getErrorMessageText,
|
|
7
|
-
getErrorStatus,
|
|
8
|
-
getExpoExportArgs,
|
|
9
|
-
isEmptyRepositoryError,
|
|
10
|
-
parseNumericBuilds,
|
|
11
|
-
stableStringify,
|
|
12
|
-
} from "./release-utils";
|
|
13
|
-
|
|
14
|
-
describe("stableStringify", () => {
|
|
15
|
-
it("normalizes object key order", () => {
|
|
16
|
-
const a = { b: 2, a: 1, nested: { y: 2, x: 1 } };
|
|
17
|
-
const b = { nested: { x: 1, y: 2 }, a: 1, b: 2 };
|
|
18
|
-
|
|
19
|
-
expect(stableStringify(a)).toBe(stableStringify(b));
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("getExpoExportArgs", () => {
|
|
24
|
-
it("returns ios args", () => {
|
|
25
|
-
expect(getExpoExportArgs("ios")).toEqual([
|
|
26
|
-
"expo",
|
|
27
|
-
"export",
|
|
28
|
-
"--platform",
|
|
29
|
-
"ios",
|
|
30
|
-
"--clear",
|
|
31
|
-
]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("returns android args", () => {
|
|
35
|
-
expect(getExpoExportArgs("android")).toEqual([
|
|
36
|
-
"expo",
|
|
37
|
-
"export",
|
|
38
|
-
"--platform",
|
|
39
|
-
"android",
|
|
40
|
-
"--clear",
|
|
41
|
-
]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("returns all-platform args", () => {
|
|
45
|
-
expect(getExpoExportArgs("all")).toEqual([
|
|
46
|
-
"expo",
|
|
47
|
-
"export",
|
|
48
|
-
"--platform",
|
|
49
|
-
"ios",
|
|
50
|
-
"--platform",
|
|
51
|
-
"android",
|
|
52
|
-
"--clear",
|
|
53
|
-
]);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("parseNumericBuilds", () => {
|
|
58
|
-
it("keeps numeric build names sorted descending", () => {
|
|
59
|
-
expect(
|
|
60
|
-
parseNumericBuilds([
|
|
61
|
-
{ name: "4" },
|
|
62
|
-
{ name: "abc" },
|
|
63
|
-
{ name: "12" },
|
|
64
|
-
{ name: "3" },
|
|
65
|
-
]),
|
|
66
|
-
).toEqual([12, 4, 3]);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe("release error helpers", () => {
|
|
71
|
-
it("extracts numeric error status when present", () => {
|
|
72
|
-
expect(getErrorStatus({ status: 404 })).toBe(404);
|
|
73
|
-
expect(getErrorStatus({ status: "409" })).toBe(409);
|
|
74
|
-
expect(getErrorStatus(new Error("boom"))).toBeUndefined();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("extracts message text when present", () => {
|
|
78
|
-
expect(getErrorMessageText({ message: "Git Repository is empty." })).toBe(
|
|
79
|
-
"Git Repository is empty.",
|
|
80
|
-
);
|
|
81
|
-
expect(getErrorMessageText(new Error("hello"))).toBe("hello");
|
|
82
|
-
expect(getErrorMessageText({})).toBe("");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("detects empty repository errors from status or message", () => {
|
|
86
|
-
expect(isEmptyRepositoryError({ status: 409 })).toBe(true);
|
|
87
|
-
expect(
|
|
88
|
-
isEmptyRepositoryError({
|
|
89
|
-
message:
|
|
90
|
-
"Git Repository is empty. - https://docs.github.com/rest/git/refs#get-a-reference",
|
|
91
|
-
}),
|
|
92
|
-
).toBe(true);
|
|
93
|
-
expect(
|
|
94
|
-
isEmptyRepositoryError({
|
|
95
|
-
message: "Repository is empty and has no refs",
|
|
96
|
-
}),
|
|
97
|
-
).toBe(true);
|
|
98
|
-
expect(isEmptyRepositoryError({ status: 404, message: "Not Found" })).toBe(
|
|
99
|
-
false,
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("createMetadataFingerprint", () => {
|
|
105
|
-
it("is stable when non-fileMetadata fields change", () => {
|
|
106
|
-
const first = {
|
|
107
|
-
id: "a",
|
|
108
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
109
|
-
fileMetadata: {
|
|
110
|
-
ios: {
|
|
111
|
-
bundle: "_expo/static/js/ios/entry-a.hbc",
|
|
112
|
-
assets: [{ hash: "h1", path: "assets/1.png", ext: "png" }],
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
const second = {
|
|
117
|
-
id: "b",
|
|
118
|
-
createdAt: "2026-02-01T00:00:00.000Z",
|
|
119
|
-
fileMetadata: {
|
|
120
|
-
ios: {
|
|
121
|
-
bundle: "_expo/static/js/ios/entry-a.hbc",
|
|
122
|
-
assets: [{ hash: "h1", path: "assets/1.png", ext: "png" }],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
expect(createMetadataFingerprint(first)).toBe(
|
|
128
|
-
createMetadataFingerprint(second),
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("changes when bundle or assets change", () => {
|
|
133
|
-
const base = {
|
|
134
|
-
fileMetadata: {
|
|
135
|
-
android: {
|
|
136
|
-
bundle: "_expo/static/js/android/entry-a.hbc",
|
|
137
|
-
assets: [{ hash: "h1", path: "assets/1.png", ext: "png" }],
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
const changedBundle = {
|
|
142
|
-
fileMetadata: {
|
|
143
|
-
android: {
|
|
144
|
-
bundle: "_expo/static/js/android/entry-b.hbc",
|
|
145
|
-
assets: [{ hash: "h1", path: "assets/1.png", ext: "png" }],
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
const changedAsset = {
|
|
150
|
-
fileMetadata: {
|
|
151
|
-
android: {
|
|
152
|
-
bundle: "_expo/static/js/android/entry-a.hbc",
|
|
153
|
-
assets: [{ hash: "h2", path: "assets/1.png", ext: "png" }],
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
expect(createMetadataFingerprint(base)).not.toBe(
|
|
159
|
-
createMetadataFingerprint(changedBundle),
|
|
160
|
-
);
|
|
161
|
-
expect(createMetadataFingerprint(base)).not.toBe(
|
|
162
|
-
createMetadataFingerprint(changedAsset),
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe("createSortedMetadataHash", () => {
|
|
168
|
-
it("is stable for equivalent metadata with different ordering", () => {
|
|
169
|
-
const first = {
|
|
170
|
-
fileMetadata: {
|
|
171
|
-
ios: {
|
|
172
|
-
assets: [
|
|
173
|
-
{ ext: "png", path: "assets/2.png", hash: "h2" },
|
|
174
|
-
{ ext: "png", path: "assets/1.png", hash: "h1" },
|
|
175
|
-
],
|
|
176
|
-
bundle: "_expo/static/js/ios/entry-a.hbc",
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
id: "abc",
|
|
180
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
181
|
-
};
|
|
182
|
-
const second = {
|
|
183
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
184
|
-
id: "abc",
|
|
185
|
-
fileMetadata: {
|
|
186
|
-
ios: {
|
|
187
|
-
bundle: "_expo/static/js/ios/entry-a.hbc",
|
|
188
|
-
assets: [
|
|
189
|
-
{ hash: "h1", path: "assets/1.png", ext: "png" },
|
|
190
|
-
{ hash: "h2", path: "assets/2.png", ext: "png" },
|
|
191
|
-
],
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
expect(createSortedMetadataHash(first)).toBe(
|
|
197
|
-
createSortedMetadataHash(second),
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("changes when metadata content changes", () => {
|
|
202
|
-
const first = {
|
|
203
|
-
fileMetadata: {
|
|
204
|
-
android: {
|
|
205
|
-
bundle: "_expo/static/js/android/entry-a.hbc",
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
const second = {
|
|
210
|
-
fileMetadata: {
|
|
211
|
-
android: {
|
|
212
|
-
bundle: "_expo/static/js/android/entry-b.hbc",
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
expect(createSortedMetadataHash(first)).not.toBe(
|
|
218
|
-
createSortedMetadataHash(second),
|
|
219
|
-
);
|
|
220
|
-
});
|
|
221
|
-
});
|