@expo-up/cli 0.1.3-next.2 → 0.1.4

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.
@@ -1,265 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { spawnSync } from "node:child_process";
4
- import { getConfig } from "@expo/config";
5
-
6
- interface GenerateCodesigningOptions {
7
- organization: string;
8
- validityYears: number;
9
- projectRoot?: string;
10
- keyOutputDirectory?: string;
11
- certificateOutputDirectory?: string;
12
- force?: boolean;
13
- expoUpdatesRunner?: (args: string[], cwd: string) => void;
14
- }
15
-
16
- interface ConfigureCodesigningOptions {
17
- projectRoot?: string;
18
- certificateInputDirectory?: string;
19
- keyInputDirectory?: string;
20
- keyId: string;
21
- alg?: string;
22
- }
23
-
24
- interface ExpoAppJson {
25
- expo?: {
26
- updates?: {
27
- codeSigningCertificate?: string;
28
- codeSigningMetadata?: {
29
- keyid?: string;
30
- alg?: string;
31
- };
32
- [key: string]: unknown;
33
- };
34
- [key: string]: unknown;
35
- };
36
- [key: string]: unknown;
37
- }
38
-
39
- const EXPO_CONFIG_FILENAMES = [
40
- "app.json",
41
- "app.config.js",
42
- "app.config.ts",
43
- "app.config.mjs",
44
- "app.config.cjs",
45
- "app.config.json",
46
- ];
47
-
48
- function hasExpoProjectConfig(projectRoot: string): boolean {
49
- return EXPO_CONFIG_FILENAMES.some((filename) =>
50
- fs.existsSync(path.join(projectRoot, filename)),
51
- );
52
- }
53
-
54
- function runExpoUpdates(args: string[], cwd: string): void {
55
- const result = spawnSync(
56
- "npx",
57
- ["expo-updates", "codesigning:generate", ...args],
58
- {
59
- cwd,
60
- encoding: "utf-8",
61
- },
62
- );
63
-
64
- if (result.error) {
65
- throw new Error(`Failed to run expo-updates: ${result.error.message}`);
66
- }
67
-
68
- if (result.status !== 0) {
69
- const details = (result.stderr || result.stdout || "").trim();
70
- throw new Error(
71
- `expo-updates codesigning:generate failed${details ? `: ${details}` : ""}`,
72
- );
73
- }
74
- }
75
-
76
- function ensureGitignoreContains(projectRoot: string, pattern: string): void {
77
- const gitignorePath = path.join(projectRoot, ".gitignore");
78
- if (!fs.existsSync(gitignorePath)) {
79
- fs.writeFileSync(gitignorePath, `${pattern}\n`);
80
- return;
81
- }
82
-
83
- const content = fs.readFileSync(gitignorePath, "utf-8");
84
- if (content.includes(pattern)) return;
85
-
86
- const prefix = content.endsWith("\n") ? "" : "\n";
87
- fs.appendFileSync(
88
- gitignorePath,
89
- `${prefix}# expo-up code signing\n${pattern}\n`,
90
- );
91
- }
92
-
93
- export function resolveProjectRoot(cwd: string = process.cwd()): string {
94
- const absoluteCwd = path.resolve(cwd);
95
- if (hasExpoProjectConfig(absoluteCwd)) return absoluteCwd;
96
-
97
- throw new Error(
98
- "Could not find Expo app config (app.json/app.config.*). Run this command inside your Expo app or pass --project-root.",
99
- );
100
- }
101
-
102
- function readAppJson(projectRoot: string): ExpoAppJson {
103
- if (!hasExpoProjectConfig(projectRoot)) {
104
- throw new Error(
105
- `Expo app config not found in ${projectRoot}. Expected app.json or app.config.*`,
106
- );
107
- }
108
-
109
- const { exp } = getConfig(projectRoot, {
110
- skipSDKVersionRequirement: true,
111
- });
112
- return { expo: exp as unknown as ExpoAppJson["expo"] };
113
- }
114
-
115
- function writeAppJson(projectRoot: string, data: ExpoAppJson): void {
116
- const appJsonPath = path.join(projectRoot, "app.json");
117
- fs.writeFileSync(appJsonPath, JSON.stringify(data, null, 2) + "\n");
118
- }
119
-
120
- function toProjectRelativeFile(
121
- projectRoot: string,
122
- absolutePath: string,
123
- ): string {
124
- const relativePath = path
125
- .relative(projectRoot, absolutePath)
126
- .split(path.sep)
127
- .join("/");
128
- return `./${relativePath}`;
129
- }
130
-
131
- export function generateCodesigning(options: GenerateCodesigningOptions): {
132
- projectRoot: string;
133
- keyOutputDir: string;
134
- certificateOutputDir: string;
135
- privateKeyPath: string;
136
- publicKeyPath: string;
137
- certificatePath: string;
138
- } {
139
- const projectRoot = options.projectRoot
140
- ? path.resolve(options.projectRoot)
141
- : resolveProjectRoot(process.cwd());
142
- const keyOutputDir = path.resolve(
143
- projectRoot,
144
- options.keyOutputDirectory ?? "codesigning-keys",
145
- );
146
- const certificateOutputDir = path.resolve(
147
- projectRoot,
148
- options.certificateOutputDirectory ?? "certs",
149
- );
150
- const run = options.expoUpdatesRunner ?? runExpoUpdates;
151
-
152
- if (!options.organization?.trim()) {
153
- throw new Error("organization is required.");
154
- }
155
-
156
- if (options.validityYears <= 0 || !Number.isFinite(options.validityYears)) {
157
- throw new Error("validityYears must be a positive number.");
158
- }
159
-
160
- if (
161
- !options.force &&
162
- (fs.existsSync(keyOutputDir) || fs.existsSync(certificateOutputDir))
163
- ) {
164
- throw new Error(
165
- "Key/certificate output directory already exists. Use --force to overwrite.",
166
- );
167
- }
168
-
169
- if (options.force) {
170
- if (fs.existsSync(keyOutputDir)) {
171
- fs.rmSync(keyOutputDir, { recursive: true, force: true });
172
- }
173
- if (fs.existsSync(certificateOutputDir)) {
174
- fs.rmSync(certificateOutputDir, { recursive: true, force: true });
175
- }
176
- }
177
-
178
- const privateKeyPath = path.join(keyOutputDir, "private-key.pem");
179
- const publicKeyPath = path.join(keyOutputDir, "public-key.pem");
180
- const certificatePath = path.join(certificateOutputDir, "certificate.pem");
181
- run(
182
- [
183
- "--key-output-directory",
184
- path.relative(projectRoot, keyOutputDir),
185
- "--certificate-output-directory",
186
- path.relative(projectRoot, certificateOutputDir),
187
- "--certificate-validity-duration-years",
188
- `${Math.floor(options.validityYears)}`,
189
- "--certificate-common-name",
190
- options.organization,
191
- ],
192
- projectRoot,
193
- );
194
-
195
- ensureGitignoreContains(projectRoot, "codesigning-keys/");
196
-
197
- return {
198
- projectRoot,
199
- keyOutputDir,
200
- certificateOutputDir,
201
- privateKeyPath,
202
- publicKeyPath,
203
- certificatePath,
204
- };
205
- }
206
-
207
- export function configureCodesigning(options: ConfigureCodesigningOptions): {
208
- projectRoot: string;
209
- certificatePath: string;
210
- keyId: string;
211
- alg: string;
212
- } {
213
- const projectRoot = options.projectRoot
214
- ? path.resolve(options.projectRoot)
215
- : resolveProjectRoot(process.cwd());
216
-
217
- const certificateDir = path.resolve(
218
- projectRoot,
219
- options.certificateInputDirectory ?? "certs",
220
- );
221
- const keyDir = path.resolve(
222
- projectRoot,
223
- options.keyInputDirectory ?? "codesigning-keys",
224
- );
225
- const certificatePath = path.join(certificateDir, "certificate.pem");
226
- const privateKeyPath = path.join(keyDir, "private-key.pem");
227
- const publicKeyPath = path.join(keyDir, "public-key.pem");
228
-
229
- if (!fs.existsSync(certificatePath)) {
230
- throw new Error(`Certificate not found at ${certificatePath}`);
231
- }
232
-
233
- if (!fs.existsSync(privateKeyPath) || !fs.existsSync(publicKeyPath)) {
234
- throw new Error(
235
- `Expected keys at ${keyDir} (private-key.pem and public-key.pem)`,
236
- );
237
- }
238
-
239
- if (!options.keyId?.trim()) {
240
- throw new Error("keyId is required.");
241
- }
242
-
243
- const alg = options.alg ?? "rsa-v1_5-sha256";
244
- const appJson = readAppJson(projectRoot);
245
- if (!appJson.expo) appJson.expo = {};
246
- if (!appJson.expo.updates) appJson.expo.updates = {};
247
-
248
- appJson.expo.updates.codeSigningCertificate = toProjectRelativeFile(
249
- projectRoot,
250
- certificatePath,
251
- );
252
- appJson.expo.updates.codeSigningMetadata = {
253
- keyid: options.keyId,
254
- alg,
255
- };
256
-
257
- writeAppJson(projectRoot, appJson);
258
-
259
- return {
260
- projectRoot,
261
- certificatePath,
262
- keyId: options.keyId,
263
- alg,
264
- };
265
- }
@@ -1,23 +0,0 @@
1
- /// <reference path="../../typescript-config/bun-test-shim.d.ts" />
2
- import { describe, expect, it } from "bun:test";
3
- import { parseDeleteBuildIds } from "./history-utils";
4
-
5
- describe("parseDeleteBuildIds", () => {
6
- it("returns empty array when values are missing", () => {
7
- expect(parseDeleteBuildIds(undefined)).toEqual([]);
8
- });
9
-
10
- it("parses comma and space separated values", () => {
11
- expect(parseDeleteBuildIds(["10,8", "7"])).toEqual([10, 8, 7]);
12
- });
13
-
14
- it("deduplicates and sorts descending", () => {
15
- expect(parseDeleteBuildIds(["7", "10", "7"])).toEqual([10, 7]);
16
- });
17
-
18
- it("throws on invalid value", () => {
19
- expect(() => parseDeleteBuildIds(["latest"])).toThrow(
20
- 'Invalid build id "latest"',
21
- );
22
- });
23
- });
@@ -1,24 +0,0 @@
1
- export function parseDeleteBuildIds(values?: string[]): number[] {
2
- if (!values || values.length === 0) {
3
- return [];
4
- }
5
-
6
- const splitValues = values
7
- .flatMap((value) => value.split(","))
8
- .map((value) => value.trim())
9
- .filter(Boolean);
10
-
11
- if (splitValues.length === 0) {
12
- return [];
13
- }
14
-
15
- const parsed = splitValues.map((value) => {
16
- const num = Number.parseInt(value, 10);
17
- if (!Number.isFinite(num)) {
18
- throw new Error(`Invalid build id "${value}". Use numeric IDs.`);
19
- }
20
- return num;
21
- });
22
-
23
- return Array.from(new Set(parsed)).sort((a, b) => b - a);
24
- }