@better-update/cli 0.3.0 → 0.4.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.
Files changed (152) hide show
  1. package/dist/index.mjs +5190 -0
  2. package/dist/index.mjs.map +1 -0
  3. package/package.json +12 -9
  4. package/CHANGELOG.md +0 -58
  5. package/oxlint.config.ts +0 -6
  6. package/src/app-layer.ts +0 -29
  7. package/src/application/build-workflow.ts +0 -222
  8. package/src/application/command-exit.ts +0 -13
  9. package/src/application/login.ts +0 -87
  10. package/src/application/update-promote.ts +0 -88
  11. package/src/application/update-publish.ts +0 -402
  12. package/src/application/update-rollback.ts +0 -275
  13. package/src/commands/analytics/adoption.ts +0 -40
  14. package/src/commands/analytics/channels.ts +0 -35
  15. package/src/commands/analytics/helpers.ts +0 -3
  16. package/src/commands/analytics/index.ts +0 -13
  17. package/src/commands/analytics/platforms.ts +0 -39
  18. package/src/commands/analytics/updates.ts +0 -35
  19. package/src/commands/audit-logs/helpers.ts +0 -3
  20. package/src/commands/audit-logs/index.ts +0 -8
  21. package/src/commands/audit-logs/list.ts +0 -66
  22. package/src/commands/branches.ts +0 -70
  23. package/src/commands/build/android.ts +0 -129
  24. package/src/commands/build/index.ts +0 -63
  25. package/src/commands/build/ios.ts +0 -199
  26. package/src/commands/build/reserve-and-upload.test.ts +0 -263
  27. package/src/commands/build/reserve-and-upload.ts +0 -160
  28. package/src/commands/build/run-step.ts +0 -131
  29. package/src/commands/builds/compatibility-matrix.ts +0 -48
  30. package/src/commands/builds/delete.ts +0 -15
  31. package/src/commands/builds/get.ts +0 -34
  32. package/src/commands/builds/helpers.ts +0 -3
  33. package/src/commands/builds/index.ts +0 -20
  34. package/src/commands/builds/install-link.ts +0 -20
  35. package/src/commands/builds/list.ts +0 -38
  36. package/src/commands/channels/create.ts +0 -37
  37. package/src/commands/channels/delete.ts +0 -15
  38. package/src/commands/channels/helpers.ts +0 -18
  39. package/src/commands/channels/index.ts +0 -24
  40. package/src/commands/channels/list.ts +0 -38
  41. package/src/commands/channels/pause.ts +0 -15
  42. package/src/commands/channels/resume.ts +0 -15
  43. package/src/commands/channels/rollout/complete.ts +0 -17
  44. package/src/commands/channels/rollout/create.ts +0 -36
  45. package/src/commands/channels/rollout/index.ts +0 -11
  46. package/src/commands/channels/rollout/revert.ts +0 -17
  47. package/src/commands/channels/rollout/update.ts +0 -23
  48. package/src/commands/channels/update.ts +0 -32
  49. package/src/commands/credentials/delete.ts +0 -24
  50. package/src/commands/credentials/index.ts +0 -10
  51. package/src/commands/credentials/list.ts +0 -33
  52. package/src/commands/credentials/upload.ts +0 -91
  53. package/src/commands/env/delete.ts +0 -35
  54. package/src/commands/env/export.ts +0 -27
  55. package/src/commands/env/get.ts +0 -25
  56. package/src/commands/env/helpers.ts +0 -13
  57. package/src/commands/env/import.ts +0 -31
  58. package/src/commands/env/index.ts +0 -24
  59. package/src/commands/env/list.ts +0 -44
  60. package/src/commands/env/pull.ts +0 -27
  61. package/src/commands/env/set.ts +0 -42
  62. package/src/commands/fingerprint/compare.ts +0 -25
  63. package/src/commands/fingerprint/generate.ts +0 -18
  64. package/src/commands/fingerprint/index.ts +0 -9
  65. package/src/commands/init.ts +0 -35
  66. package/src/commands/login.ts +0 -13
  67. package/src/commands/logout.ts +0 -12
  68. package/src/commands/projects.ts +0 -84
  69. package/src/commands/status.ts +0 -48
  70. package/src/commands/update/delete.ts +0 -15
  71. package/src/commands/update/helpers.ts +0 -22
  72. package/src/commands/update/index.ts +0 -22
  73. package/src/commands/update/list.ts +0 -60
  74. package/src/commands/update/promote.ts +0 -30
  75. package/src/commands/update/publish.ts +0 -94
  76. package/src/commands/update/rollback.ts +0 -42
  77. package/src/commands/update/rollout/complete.ts +0 -17
  78. package/src/commands/update/rollout/index.ts +0 -10
  79. package/src/commands/update/rollout/revert.ts +0 -17
  80. package/src/commands/update/rollout/set.ts +0 -23
  81. package/src/index.ts +0 -53
  82. package/src/lib/android-keystore.test.ts +0 -114
  83. package/src/lib/android-keystore.ts +0 -76
  84. package/src/lib/android-signing-gradle.test.ts +0 -95
  85. package/src/lib/android-signing-gradle.ts +0 -52
  86. package/src/lib/app-json.ts +0 -81
  87. package/src/lib/apple-auth.test.ts +0 -402
  88. package/src/lib/apple-auth.ts +0 -132
  89. package/src/lib/artifact-finder.test.ts +0 -195
  90. package/src/lib/artifact-finder.ts +0 -122
  91. package/src/lib/browser-login.test.ts +0 -88
  92. package/src/lib/browser-login.ts +0 -193
  93. package/src/lib/build-profile.test.ts +0 -290
  94. package/src/lib/build-profile.ts +0 -234
  95. package/src/lib/cli-schemas.ts +0 -39
  96. package/src/lib/command-errors.ts +0 -60
  97. package/src/lib/credentials-downloader.ts +0 -181
  98. package/src/lib/credentials-manager.ts +0 -354
  99. package/src/lib/env-exporter.test.ts +0 -96
  100. package/src/lib/env-exporter.ts +0 -28
  101. package/src/lib/exit-codes.ts +0 -82
  102. package/src/lib/expo-config.ts +0 -130
  103. package/src/lib/expo-export.test.ts +0 -94
  104. package/src/lib/expo-export.ts +0 -281
  105. package/src/lib/fingerprint.ts +0 -67
  106. package/src/lib/format-error.ts +0 -22
  107. package/src/lib/git-context.ts +0 -56
  108. package/src/lib/gradle-config.ts +0 -126
  109. package/src/lib/ios-export-options.test.ts +0 -98
  110. package/src/lib/ios-export-options.ts +0 -62
  111. package/src/lib/ios-keychain.ts +0 -181
  112. package/src/lib/ios-provisioning.test.ts +0 -115
  113. package/src/lib/ios-provisioning.ts +0 -179
  114. package/src/lib/output.ts +0 -32
  115. package/src/lib/pkcs12.ts +0 -73
  116. package/src/lib/plist.ts +0 -39
  117. package/src/lib/post-build-validation.ts +0 -146
  118. package/src/lib/presigned-upload.test.ts +0 -140
  119. package/src/lib/presigned-upload.ts +0 -35
  120. package/src/lib/record.ts +0 -5
  121. package/src/lib/resolve-named-resource.ts +0 -24
  122. package/src/lib/runtime-version.test.ts +0 -119
  123. package/src/lib/runtime-version.ts +0 -62
  124. package/src/lib/sha256.test.ts +0 -108
  125. package/src/lib/sha256.ts +0 -80
  126. package/src/lib/signed-payloads.test.ts +0 -181
  127. package/src/lib/signed-payloads.ts +0 -164
  128. package/src/lib/string-utils.ts +0 -4
  129. package/src/lib/temp-dir.ts +0 -14
  130. package/src/lib/test-utils.ts +0 -13
  131. package/src/lib/update-platforms.test.ts +0 -45
  132. package/src/lib/update-platforms.ts +0 -19
  133. package/src/lib/xcpretty-formatter.ts +0 -21
  134. package/src/services/api-client.ts +0 -42
  135. package/src/services/apple-session-store.ts +0 -100
  136. package/src/services/auth-store.ts +0 -85
  137. package/src/services/cli-runtime.ts +0 -46
  138. package/src/services/config-store.ts +0 -108
  139. package/src/services/presigned-upload.ts +0 -84
  140. package/src/services/update-asset-uploader.ts +0 -72
  141. package/src/types/keychain.d.ts +0 -22
  142. package/tests/e2e/build.test.ts +0 -270
  143. package/tests/e2e/commands.test.ts +0 -694
  144. package/tests/e2e/ota-lifecycle.test.ts +0 -275
  145. package/tests/e2e/publish.test.ts +0 -150
  146. package/tests/helpers/cli-e2e.ts +0 -426
  147. package/tests/helpers/pty-driver.ts +0 -142
  148. package/tests/interactive/harness/provider-prompt.ts +0 -54
  149. package/tests/interactive/login.test.ts +0 -47
  150. package/tests/interactive/provider-select.test.ts +0 -59
  151. package/tsconfig.json +0 -7
  152. package/vitest.config.ts +0 -38
@@ -1,94 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { FileSystem } from "@effect/platform";
4
- import { Effect, Exit } from "effect";
5
-
6
- import { UpdatePublishError } from "./exit-codes";
7
- import { readExpoExportAssets } from "./expo-export";
8
- import { failureError } from "./test-utils";
9
-
10
- const makeFs = (files: Record<string, string>) =>
11
- FileSystem.layerNoop({
12
- readFileString: (filePath: string) => {
13
- const value = files[filePath];
14
- return value === undefined
15
- ? Effect.die(new Error(`ENOENT: ${filePath}`))
16
- : Effect.succeed(value);
17
- },
18
- });
19
-
20
- describe(readExpoExportAssets, () => {
21
- it("parses launch bundle and regular assets from metadata.json", async () => {
22
- const exportDir = "/tmp/export-ios";
23
- const metadataPath = path.join(exportDir, "metadata.json");
24
- const exit = await Effect.runPromiseExit(
25
- readExpoExportAssets({ exportDir, platform: "ios" }).pipe(
26
- Effect.provide(
27
- makeFs({
28
- [metadataPath]: JSON.stringify({
29
- version: 0,
30
- bundler: "metro",
31
- fileMetadata: {
32
- ios: {
33
- bundle: "_expo/static/js/ios/index-ba7f80c877854ce4d715c0cb029ac497.hbc",
34
- assets: [{ path: "assets/4e3f888fc8475f69fd5fa32f1ad5216a", ext: "png" }],
35
- },
36
- },
37
- }),
38
- }),
39
- ),
40
- ),
41
- );
42
-
43
- expect(Exit.isSuccess(exit)).toBe(true);
44
- if (Exit.isSuccess(exit)) {
45
- expect(exit.value).toStrictEqual([
46
- {
47
- path: path.join(
48
- exportDir,
49
- "_expo/static/js/ios/index-ba7f80c877854ce4d715c0cb029ac497.hbc",
50
- ),
51
- key: "index-ba7f80c877854ce4d715c0cb029ac497.hbc",
52
- fileExt: "hbc",
53
- contentType: "application/javascript",
54
- isLaunch: true,
55
- },
56
- {
57
- path: path.join(exportDir, "assets/4e3f888fc8475f69fd5fa32f1ad5216a"),
58
- key: "4e3f888fc8475f69fd5fa32f1ad5216a",
59
- fileExt: "png",
60
- contentType: "image/png",
61
- isLaunch: false,
62
- },
63
- ]);
64
- }
65
- });
66
-
67
- it("fails when the requested platform is missing from metadata", async () => {
68
- const exportDir = "/tmp/export-android";
69
- const metadataPath = path.join(exportDir, "metadata.json");
70
- const exit = await Effect.runPromiseExit(
71
- readExpoExportAssets({ exportDir, platform: "android" }).pipe(
72
- Effect.provide(
73
- makeFs({
74
- [metadataPath]: JSON.stringify({
75
- version: 0,
76
- bundler: "metro",
77
- fileMetadata: {
78
- ios: {
79
- bundle: "_expo/static/js/ios/index.hbc",
80
- assets: [],
81
- },
82
- },
83
- }),
84
- }),
85
- ),
86
- ),
87
- );
88
-
89
- expect(Exit.isFailure(exit)).toBe(true);
90
- if (Exit.isFailure(exit)) {
91
- expect(failureError(exit)).toBeInstanceOf(UpdatePublishError);
92
- }
93
- });
94
- });
@@ -1,281 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { Command, FileSystem } from "@effect/platform";
4
- import { Effect } from "effect";
5
-
6
- import type { CommandExecutor } from "@effect/platform";
7
-
8
- import { CliRuntime } from "../services/cli-runtime";
9
- import { BuildFailedError, UpdatePublishError } from "./exit-codes";
10
- import { asRecord } from "./record";
11
-
12
- import type { Platform } from "./build-profile";
13
-
14
- export interface ExportedUpdateAssetFile {
15
- readonly path: string;
16
- readonly key: string;
17
- readonly fileExt: string;
18
- readonly contentType: string;
19
- readonly isLaunch: boolean;
20
- }
21
-
22
- interface ReadExpoPublicConfigOptions {
23
- readonly projectRoot: string;
24
- readonly envVars: Record<string, string>;
25
- }
26
-
27
- interface RunExpoExportOptions extends ReadExpoPublicConfigOptions {
28
- readonly exportDir: string;
29
- readonly platform: Platform;
30
- readonly clear: boolean;
31
- }
32
-
33
- interface ReadExpoExportAssetsOptions {
34
- readonly exportDir: string;
35
- readonly platform: Platform;
36
- }
37
-
38
- const asString = (value: unknown): string | undefined =>
39
- typeof value === "string" ? value : undefined;
40
-
41
- const normalizeExtension = (value: string | undefined): string | undefined => {
42
- if (!value) {
43
- return undefined;
44
- }
45
- return value.startsWith(".") ? value.slice(1) : value;
46
- };
47
-
48
- const inferContentType = (fileExt: string, isLaunch: boolean): string => {
49
- const normalized = fileExt.toLowerCase();
50
- if (isLaunch || normalized === "js" || normalized === "hbc" || normalized === "bundle") {
51
- return "application/javascript";
52
- }
53
-
54
- switch (normalized) {
55
- case "png": {
56
- return "image/png";
57
- }
58
- case "jpg":
59
- case "jpeg": {
60
- return "image/jpeg";
61
- }
62
- case "webp": {
63
- return "image/webp";
64
- }
65
- case "gif": {
66
- return "image/gif";
67
- }
68
- case "svg": {
69
- return "image/svg+xml";
70
- }
71
- case "json": {
72
- return "application/json";
73
- }
74
- case "mp4": {
75
- return "video/mp4";
76
- }
77
- case "mp3": {
78
- return "audio/mpeg";
79
- }
80
- case "wav": {
81
- return "audio/wav";
82
- }
83
- case "ttf": {
84
- return "font/ttf";
85
- }
86
- case "otf": {
87
- return "font/otf";
88
- }
89
- case "woff": {
90
- return "font/woff";
91
- }
92
- case "woff2": {
93
- return "font/woff2";
94
- }
95
- default: {
96
- return "application/octet-stream";
97
- }
98
- }
99
- };
100
-
101
- const makeBunxCommand = (...args: readonly string[]): Command.Command =>
102
- Command.make("bunx", ...args);
103
-
104
- const runCommand = (
105
- cmd: Command.Command,
106
- step: string,
107
- ): Effect.Effect<void, BuildFailedError, CommandExecutor.CommandExecutor> =>
108
- Command.exitCode(cmd.pipe(Command.stdout("inherit"), Command.stderr("inherit"))).pipe(
109
- Effect.mapError(
110
- (cause) =>
111
- new BuildFailedError({
112
- step,
113
- exitCode: 1,
114
- message: `${step} failed to spawn: ${String(cause)}`,
115
- }),
116
- ),
117
- Effect.flatMap((code) =>
118
- code === 0
119
- ? Effect.void
120
- : Effect.fail(
121
- new BuildFailedError({
122
- step,
123
- exitCode: code,
124
- message: `${step} exited with code ${code}`,
125
- }),
126
- ),
127
- ),
128
- );
129
-
130
- export const readExpoPublicConfig = ({
131
- projectRoot,
132
- envVars,
133
- }: ReadExpoPublicConfigOptions): Effect.Effect<
134
- Record<string, unknown>,
135
- UpdatePublishError,
136
- CliRuntime | CommandExecutor.CommandExecutor
137
- > =>
138
- Effect.gen(function* () {
139
- const runtime = yield* CliRuntime;
140
- const commandEnv = yield* runtime.commandEnvironment(envVars);
141
- const stdout = yield* Command.string(
142
- makeBunxCommand("expo", "config", "--type", "public", "--json").pipe(
143
- Command.workingDirectory(projectRoot),
144
- Command.env(commandEnv),
145
- ),
146
- ).pipe(
147
- Effect.mapError(
148
- (cause) =>
149
- new UpdatePublishError({
150
- message: `Failed to read Expo public config: ${String(cause)}`,
151
- }),
152
- ),
153
- );
154
-
155
- const parsed = yield* Effect.try({
156
- try: () => JSON.parse(stdout) as unknown,
157
- catch: () =>
158
- new UpdatePublishError({
159
- message: "Expo public config output was not valid JSON.",
160
- }),
161
- });
162
-
163
- const config = asRecord(parsed);
164
- if (!config) {
165
- return yield* new UpdatePublishError({
166
- message: "Expo public config did not decode to a JSON object.",
167
- });
168
- }
169
-
170
- return config;
171
- });
172
-
173
- export const runExpoExport = ({
174
- projectRoot,
175
- exportDir,
176
- platform,
177
- envVars,
178
- clear,
179
- }: RunExpoExportOptions): Effect.Effect<
180
- void,
181
- BuildFailedError,
182
- CliRuntime | CommandExecutor.CommandExecutor
183
- > =>
184
- Effect.gen(function* () {
185
- const runtime = yield* CliRuntime;
186
- const commandEnv = yield* runtime.commandEnvironment(envVars);
187
- const args = [
188
- "expo",
189
- "export",
190
- "--platform",
191
- platform,
192
- "--output-dir",
193
- exportDir,
194
- "--dump-assetmap",
195
- ];
196
- if (clear) {
197
- args.push("--clear");
198
- }
199
-
200
- return yield* runCommand(
201
- makeBunxCommand(...args).pipe(Command.workingDirectory(projectRoot), Command.env(commandEnv)),
202
- `expo export ${platform}`,
203
- );
204
- });
205
-
206
- export const readExpoExportAssets = ({
207
- exportDir,
208
- platform,
209
- }: ReadExpoExportAssetsOptions): Effect.Effect<
210
- readonly ExportedUpdateAssetFile[],
211
- UpdatePublishError,
212
- FileSystem.FileSystem
213
- > =>
214
- Effect.gen(function* () {
215
- const fs = yield* FileSystem.FileSystem;
216
- const metadataPath = path.join(exportDir, "metadata.json");
217
- const metadataText = yield* fs.readFileString(metadataPath).pipe(
218
- Effect.mapError(
219
- () =>
220
- new UpdatePublishError({
221
- message: `Expected Expo export metadata at ${metadataPath}.`,
222
- }),
223
- ),
224
- );
225
-
226
- const metadata = yield* Effect.try({
227
- try: () => JSON.parse(metadataText) as unknown,
228
- catch: () =>
229
- new UpdatePublishError({
230
- message: `Failed to parse ${metadataPath} as JSON.`,
231
- }),
232
- });
233
-
234
- const platformMetadata = asRecord(asRecord(asRecord(metadata)?.["fileMetadata"])?.[platform]);
235
- const bundlePath = asString(platformMetadata?.["bundle"]);
236
- if (!bundlePath) {
237
- return yield* new UpdatePublishError({
238
- message: `Expo export did not contain a bundle path for platform "${platform}".`,
239
- });
240
- }
241
-
242
- const bundleExt = normalizeExtension(path.extname(bundlePath)) ?? "js";
243
- const rawAssets = Array.isArray(platformMetadata?.["assets"]) ? platformMetadata["assets"] : [];
244
-
245
- // eslint-disable-next-line unicorn/no-array-method-this-argument -- Effect.forEach, not Array.prototype.forEach; the second arg is a mapping effect, not a thisArg
246
- const assets = yield* Effect.forEach(rawAssets, (rawAsset, index) =>
247
- Effect.gen(function* () {
248
- const asset = asRecord(rawAsset);
249
- const assetPath = asString(asset?.["path"]);
250
- if (!assetPath) {
251
- return yield* new UpdatePublishError({
252
- message: `Expo export asset #${String(index + 1)} is missing its "path" field.`,
253
- });
254
- }
255
-
256
- const fileExt =
257
- normalizeExtension(asString(asset?.["ext"])) ??
258
- normalizeExtension(path.extname(assetPath)) ??
259
- "bin";
260
-
261
- return {
262
- path: path.join(exportDir, assetPath),
263
- key: path.posix.basename(assetPath),
264
- fileExt,
265
- contentType: inferContentType(fileExt, false),
266
- isLaunch: false,
267
- } as const satisfies ExportedUpdateAssetFile;
268
- }),
269
- );
270
-
271
- return [
272
- {
273
- path: path.join(exportDir, bundlePath),
274
- key: path.posix.basename(bundlePath),
275
- fileExt: bundleExt,
276
- contentType: inferContentType(bundleExt, true),
277
- isLaunch: true,
278
- } as const satisfies ExportedUpdateAssetFile,
279
- ...assets,
280
- ] as const;
281
- });
@@ -1,67 +0,0 @@
1
- import { Command } from "@effect/platform";
2
- import { Data, Effect } from "effect";
3
-
4
- import type { CommandExecutor } from "@effect/platform";
5
-
6
- import { isRecord } from "./record";
7
-
8
- export class FingerprintError extends Data.TaggedError("FingerprintError")<{
9
- readonly message: string;
10
- }> {}
11
-
12
- export interface FingerprintSource {
13
- readonly type: string;
14
- readonly filePath?: string;
15
- readonly reasons: readonly string[];
16
- readonly hash: string | null;
17
- }
18
-
19
- export interface FingerprintResult {
20
- readonly hash: string;
21
- readonly sources: readonly FingerprintSource[];
22
- }
23
-
24
- export const runFingerprintFull = (
25
- projectRoot: string,
26
- ): Effect.Effect<FingerprintResult, FingerprintError, CommandExecutor.CommandExecutor> =>
27
- Effect.gen(function* () {
28
- const cmd = Command.make("bunx", "@expo/fingerprint", projectRoot).pipe(
29
- Command.workingDirectory(projectRoot),
30
- );
31
- const stdout = yield* Command.string(cmd).pipe(
32
- Effect.mapError(
33
- (cause) =>
34
- new FingerprintError({
35
- message: `Failed to run "@expo/fingerprint": ${cause.message}`,
36
- }),
37
- ),
38
- );
39
-
40
- const parsed = yield* Effect.try({
41
- try: (): unknown => JSON.parse(stdout),
42
- catch: () =>
43
- new FingerprintError({
44
- message: "Failed to parse @expo/fingerprint output as JSON.",
45
- }),
46
- });
47
-
48
- if (!isRecord(parsed)) {
49
- return yield* new FingerprintError({
50
- message: "@expo/fingerprint output was not a JSON object.",
51
- });
52
- }
53
-
54
- const { hash } = parsed;
55
- if (typeof hash !== "string" || hash.length === 0) {
56
- return yield* new FingerprintError({
57
- message: '@expo/fingerprint output did not contain a "hash" string field.',
58
- });
59
- }
60
-
61
- const sourcesRaw = parsed["sources"];
62
- const sources: readonly FingerprintSource[] = Array.isArray(sourcesRaw)
63
- ? (sourcesRaw as readonly FingerprintSource[])
64
- : [];
65
-
66
- return { hash, sources };
67
- });
@@ -1,22 +0,0 @@
1
- export const formatCause = (cause: unknown): string => {
2
- if (cause instanceof Error) {
3
- return cause.message;
4
- }
5
-
6
- if (typeof cause === "object" && cause !== null) {
7
- const tagged = cause as { readonly _tag?: unknown; readonly message?: unknown };
8
- const tag = typeof tagged._tag === "string" ? tagged._tag : undefined;
9
- const message = typeof tagged.message === "string" ? tagged.message : undefined;
10
- if (tag && message) {
11
- return `${tag}: ${message}`;
12
- }
13
- if (message) {
14
- return message;
15
- }
16
- if (tag) {
17
- return tag;
18
- }
19
- }
20
-
21
- return String(cause);
22
- };
@@ -1,56 +0,0 @@
1
- import { Command } from "@effect/platform";
2
- import { Effect } from "effect";
3
-
4
- import type { CommandExecutor } from "@effect/platform";
5
-
6
- export interface GitContext {
7
- readonly ref: string | undefined;
8
- readonly commit: string | undefined;
9
- readonly commitMessage: string | undefined;
10
- readonly dirty: boolean;
11
- }
12
-
13
- const runString = (
14
- cmd: Command.Command,
15
- cwd: string,
16
- ): Effect.Effect<string, unknown, CommandExecutor.CommandExecutor> =>
17
- Command.string(Command.workingDirectory(cmd, cwd));
18
-
19
- /**
20
- * Best-effort git context extraction. If git is missing, the directory isn't
21
- * a repo, or any command fails, we silently return undefined fields so the
22
- * build can still proceed. This is intentional — git context is metadata,
23
- * not a requirement.
24
- */
25
- export const readGitContext = (
26
- projectRoot: string,
27
- ): Effect.Effect<GitContext, never, CommandExecutor.CommandExecutor> =>
28
- Effect.gen(function* () {
29
- const [commit, ref, commitMessage, status] = yield* Effect.all(
30
- [
31
- runString(Command.make("git", "rev-parse", "HEAD"), projectRoot).pipe(
32
- Effect.map((output) => output.trim()),
33
- Effect.catchAll(() => Effect.succeed("")),
34
- ),
35
- runString(Command.make("git", "symbolic-ref", "--short", "HEAD"), projectRoot).pipe(
36
- Effect.map((output) => output.trim()),
37
- Effect.catchAll(() => Effect.succeed("")),
38
- ),
39
- runString(Command.make("git", "log", "-1", "--format=%s"), projectRoot).pipe(
40
- Effect.map((output) => output.trim()),
41
- Effect.catchAll(() => Effect.succeed("")),
42
- ),
43
- runString(Command.make("git", "status", "--porcelain"), projectRoot).pipe(
44
- Effect.catchAll(() => Effect.succeed("")),
45
- ),
46
- ],
47
- { concurrency: "unbounded" },
48
- );
49
-
50
- return {
51
- ref: ref.length > 0 ? ref : undefined,
52
- commit: commit.length > 0 ? commit : undefined,
53
- commitMessage: commitMessage.length > 0 ? commitMessage : undefined,
54
- dirty: status.trim().length > 0,
55
- };
56
- });
@@ -1,126 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { FileSystem } from "@effect/platform";
4
- import { Console, Effect } from "effect";
5
-
6
- import { asRecord } from "./record";
7
-
8
- export interface GradleConfig {
9
- readonly applicationId?: string;
10
- readonly versionCode?: number;
11
- readonly versionName?: string;
12
- }
13
-
14
- /**
15
- * Parse Groovy `build.gradle` to extract key Android config values.
16
- * Returns `undefined` if:
17
- * - Only `build.gradle.kts` exists (Kotlin DSL not supported by gradle-to-js)
18
- * - No build.gradle found at all
19
- * - Parse fails
20
- *
21
- * Informational only — never blocks the build.
22
- */
23
- export const readGradleConfig = (
24
- androidDir: string,
25
- ): Effect.Effect<GradleConfig | undefined, never, FileSystem.FileSystem> =>
26
- Effect.gen(function* () {
27
- const fs = yield* FileSystem.FileSystem;
28
- const gradlePath = path.join(androidDir, "app", "build.gradle");
29
- const ktsPath = path.join(androidDir, "app", "build.gradle.kts");
30
-
31
- const hasGroovy = yield* fs.exists(gradlePath).pipe(Effect.orElseSucceed(() => false));
32
- const hasKts = yield* fs.exists(ktsPath).pipe(Effect.orElseSucceed(() => false));
33
-
34
- if (!hasGroovy && hasKts) {
35
- // Kotlin DSL — gradle-to-js cannot parse it
36
- return undefined;
37
- }
38
-
39
- if (!hasGroovy) {
40
- return undefined;
41
- }
42
-
43
- const content = yield* fs
44
- .readFileString(gradlePath)
45
- .pipe(Effect.catchAll(() => Effect.succeed(undefined)));
46
- if (!content) {
47
- return undefined;
48
- }
49
-
50
- return yield* Effect.tryPromise({
51
- try: async () => {
52
- const gradle =
53
- // eslint-disable-next-line typescript/no-unsafe-type-assertion -- CJS require returns `any`; narrow to gradle-to-js declared shape
54
- require("gradle-to-js") as {
55
- parseText: (text: string) => Promise<Record<string, unknown>>;
56
- };
57
- return gradle.parseText(stripGroovyComments(content));
58
- },
59
- catch: () => undefined,
60
- }).pipe(
61
- Effect.map(extractGradleConfig),
62
- Effect.catchAll(() => Effect.succeed(undefined)),
63
- );
64
- });
65
-
66
- /**
67
- * Log a warning if Gradle applicationId differs from app.json package name.
68
- */
69
- export const warnOnGradleMismatch = (
70
- gradleConfig: GradleConfig | undefined,
71
- expectedPackage: string,
72
- ): Effect.Effect<void> => {
73
- if (!gradleConfig?.applicationId) {
74
- return Effect.void;
75
- }
76
- if (gradleConfig.applicationId === expectedPackage) {
77
- return Effect.void;
78
- }
79
- return Console.warn(
80
- `Gradle applicationId "${gradleConfig.applicationId}" differs from app.json package "${expectedPackage}". ` +
81
- `The Gradle value will be used in the built APK/AAB.`,
82
- );
83
- };
84
-
85
- // ── helpers ──────────────────────────────────────────────────────
86
-
87
- /**
88
- * Strip Groovy single-line and block comments.
89
- * gradle-to-js chokes on comments — EAS CLI does this same pre-processing.
90
- */
91
- const stripGroovyComments = (text: string): string =>
92
- text.replaceAll(/\/\/.*$/gmu, "").replaceAll(/\/\*[\s\S]*?\*\//gu, "");
93
-
94
- const parseVersionCode = (raw: unknown): number | undefined => {
95
- if (typeof raw === "number") {
96
- return raw;
97
- }
98
- if (typeof raw === "string") {
99
- return Number.parseInt(raw, 10) || undefined;
100
- }
101
- return undefined;
102
- };
103
-
104
- const extractGradleConfig = (parsed: Record<string, unknown>): GradleConfig => {
105
- const android = asRecord(parsed["android"]);
106
- const defaultConfig = asRecord(android?.["defaultConfig"]);
107
-
108
- const applicationId =
109
- typeof defaultConfig?.["applicationId"] === "string"
110
- ? unquote(defaultConfig["applicationId"])
111
- : undefined;
112
- const versionCode = parseVersionCode(defaultConfig?.["versionCode"]);
113
- const versionName =
114
- typeof defaultConfig?.["versionName"] === "string"
115
- ? unquote(defaultConfig["versionName"])
116
- : undefined;
117
-
118
- return {
119
- ...(applicationId === undefined ? {} : { applicationId }),
120
- ...(versionCode === undefined ? {} : { versionCode }),
121
- ...(versionName === undefined ? {} : { versionName }),
122
- };
123
- };
124
-
125
- const unquote = (input: string): string =>
126
- input.startsWith('"') && input.endsWith('"') ? input.slice(1, -1) : input;