@better-update/cli 0.15.4 → 0.17.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/dist/index.mjs +425 -70
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
|
-
import { Console, Context, Data, Deferred, Duration, Effect,
|
|
5
|
+
import { Console, Context, Data, Deferred, Duration, Effect, Layer, Match, Option, ParseResult, Schema } from "effect";
|
|
6
6
|
import { Command, FetchHttpClient, FileSystem, HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity, HttpClient, HttpClientRequest, OpenApi, Path } from "@effect/platform";
|
|
7
7
|
import { NodeContext } from "@effect/platform-node";
|
|
8
8
|
import path, { join } from "node:path";
|
|
@@ -14,10 +14,13 @@ import { createServer } from "node:http";
|
|
|
14
14
|
import { maxBy, uniqBy } from "es-toolkit";
|
|
15
15
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
16
16
|
import forge from "node-forge";
|
|
17
|
-
import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { createReadStream, existsSync, promises, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { spawn as spawn$1 } from "node-pty";
|
|
19
|
+
import chalk from "chalk";
|
|
18
20
|
import os from "node:os";
|
|
19
21
|
import plistMod from "@expo/plist";
|
|
20
22
|
import { ExpoRunFormatter } from "@expo/xcpretty";
|
|
23
|
+
import ignore from "ignore";
|
|
21
24
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
22
25
|
import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
|
|
23
26
|
import qrcode from "qrcode-terminal";
|
|
@@ -28,7 +31,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
28
31
|
|
|
29
32
|
//#endregion
|
|
30
33
|
//#region package.json
|
|
31
|
-
var version = "0.
|
|
34
|
+
var version = "0.17.0";
|
|
32
35
|
|
|
33
36
|
//#endregion
|
|
34
37
|
//#region src/lib/interactive-mode.ts
|
|
@@ -1663,6 +1666,7 @@ var InvalidArgumentError = class extends Data.TaggedError("InvalidArgumentError"
|
|
|
1663
1666
|
var InteractiveProhibitedError = class extends Data.TaggedError("InteractiveProhibitedError") {};
|
|
1664
1667
|
var CredentialsJsonError = class extends Data.TaggedError("CredentialsJsonError") {};
|
|
1665
1668
|
var DirtyRepoError = class extends Data.TaggedError("DirtyRepoError") {};
|
|
1669
|
+
var StagingError = class extends Data.TaggedError("StagingError") {};
|
|
1666
1670
|
|
|
1667
1671
|
//#endregion
|
|
1668
1672
|
//#region src/lib/format-error.ts
|
|
@@ -4388,56 +4392,178 @@ const sha256Namespaced = (contentType, contentSha256Hex) => {
|
|
|
4388
4392
|
return toBase64Url(createHash("sha256").update(input).digest());
|
|
4389
4393
|
};
|
|
4390
4394
|
|
|
4395
|
+
//#endregion
|
|
4396
|
+
//#region src/lib/pty-runner.ts
|
|
4397
|
+
const ptyDimensions = () => {
|
|
4398
|
+
const stdout = process$1.stdout;
|
|
4399
|
+
return {
|
|
4400
|
+
cols: typeof stdout.columns === "number" && stdout.columns > 0 ? stdout.columns : 120,
|
|
4401
|
+
rows: typeof stdout.rows === "number" && stdout.rows > 0 ? stdout.rows : 40
|
|
4402
|
+
};
|
|
4403
|
+
};
|
|
4404
|
+
const mergeEnv$1 = (overrides) => {
|
|
4405
|
+
const merged = {};
|
|
4406
|
+
for (const [key, value] of Object.entries(process$1.env)) if (typeof value === "string") merged[key] = value;
|
|
4407
|
+
for (const [key, value] of Object.entries(overrides)) merged[key] = value;
|
|
4408
|
+
return merged;
|
|
4409
|
+
};
|
|
4410
|
+
const trySpawn = (input) => {
|
|
4411
|
+
const { cols, rows } = ptyDimensions();
|
|
4412
|
+
try {
|
|
4413
|
+
return spawn$1(input.command, [...input.args], {
|
|
4414
|
+
name: "xterm-256color",
|
|
4415
|
+
cols,
|
|
4416
|
+
rows,
|
|
4417
|
+
cwd: input.cwd,
|
|
4418
|
+
env: mergeEnv$1(input.env)
|
|
4419
|
+
});
|
|
4420
|
+
} catch (error) {
|
|
4421
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
4422
|
+
}
|
|
4423
|
+
};
|
|
4424
|
+
/**
|
|
4425
|
+
* Run a command in a pseudo-terminal so the subprocess sees a real TTY
|
|
4426
|
+
* (preserves spinners, progress bars, and ANSI colors emitted by tools like
|
|
4427
|
+
* CocoaPods and `expo prebuild`). Subprocess output is tee'd: forwarded to
|
|
4428
|
+
* `process.stdout` as raw bytes (so colors/positioning are preserved), and
|
|
4429
|
+
* also buffered into lines for the optional `onLine` callback.
|
|
4430
|
+
*
|
|
4431
|
+
* Returns the subprocess exit code. Spawn failures and signal exits surface
|
|
4432
|
+
* as non-zero exit codes (128+signal for Unix-style signal exits).
|
|
4433
|
+
*/
|
|
4434
|
+
const runInPty = (input) => Effect.async((resume) => {
|
|
4435
|
+
const spawned = trySpawn(input);
|
|
4436
|
+
if (spawned instanceof Error) {
|
|
4437
|
+
process$1.stderr.write(`Failed to spawn "${input.command}" in pty: ${spawned.message}\n`);
|
|
4438
|
+
resume(Effect.succeed(1));
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
const proc = spawned;
|
|
4442
|
+
let lineBuf = "";
|
|
4443
|
+
const handleLine = (line) => {
|
|
4444
|
+
if (input.onLine === void 0) return;
|
|
4445
|
+
const annotation = input.onLine(line);
|
|
4446
|
+
if (annotation !== void 0) process$1.stdout.write(`${annotation}\n`);
|
|
4447
|
+
};
|
|
4448
|
+
proc.onData((chunk) => {
|
|
4449
|
+
if (input.silent !== true) process$1.stdout.write(chunk);
|
|
4450
|
+
if (input.onLine === void 0) return;
|
|
4451
|
+
lineBuf += chunk;
|
|
4452
|
+
let nl = lineBuf.indexOf("\n");
|
|
4453
|
+
while (nl !== -1) {
|
|
4454
|
+
const line = lineBuf.slice(0, nl).replace(/\r$/u, "");
|
|
4455
|
+
lineBuf = lineBuf.slice(nl + 1);
|
|
4456
|
+
handleLine(line);
|
|
4457
|
+
nl = lineBuf.indexOf("\n");
|
|
4458
|
+
}
|
|
4459
|
+
});
|
|
4460
|
+
const handleResize = () => {
|
|
4461
|
+
const { cols, rows } = ptyDimensions();
|
|
4462
|
+
try {
|
|
4463
|
+
proc.resize(cols, rows);
|
|
4464
|
+
} catch {}
|
|
4465
|
+
};
|
|
4466
|
+
process$1.stdout.on("resize", handleResize);
|
|
4467
|
+
proc.onExit(({ exitCode, signal }) => {
|
|
4468
|
+
process$1.stdout.off("resize", handleResize);
|
|
4469
|
+
if (lineBuf.length > 0) {
|
|
4470
|
+
handleLine(lineBuf.replace(/\r$/u, ""));
|
|
4471
|
+
lineBuf = "";
|
|
4472
|
+
}
|
|
4473
|
+
const code = signal !== void 0 && signal !== 0 ? 128 + signal : exitCode;
|
|
4474
|
+
resume(Effect.succeed(code));
|
|
4475
|
+
});
|
|
4476
|
+
return Effect.sync(() => {
|
|
4477
|
+
try {
|
|
4478
|
+
proc.kill();
|
|
4479
|
+
} catch {}
|
|
4480
|
+
process$1.stdout.off("resize", handleResize);
|
|
4481
|
+
});
|
|
4482
|
+
});
|
|
4483
|
+
|
|
4484
|
+
//#endregion
|
|
4485
|
+
//#region src/lib/warning-style.ts
|
|
4486
|
+
const ANSI_REGEX_GLOBAL = /[][[()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[\dA-ORZcf-ntqry=><]/gu;
|
|
4487
|
+
const stripAnsi = (input) => input.replaceAll(ANSI_REGEX_GLOBAL, "");
|
|
4488
|
+
const hasAnsi = (input) => {
|
|
4489
|
+
ANSI_REGEX_GLOBAL.lastIndex = 0;
|
|
4490
|
+
return ANSI_REGEX_GLOBAL.test(input);
|
|
4491
|
+
};
|
|
4492
|
+
const WARNING_PATTERNS = [
|
|
4493
|
+
/^\s*warning:/iu,
|
|
4494
|
+
/^\s*\[!\]/u,
|
|
4495
|
+
/^\s*WARNING:/u,
|
|
4496
|
+
/^\s*WARN\b/u,
|
|
4497
|
+
/\bis deprecated\b/iu,
|
|
4498
|
+
/^\s*DEPRECATION\b/iu,
|
|
4499
|
+
/\[MT\]/u,
|
|
4500
|
+
/⚠/u
|
|
4501
|
+
];
|
|
4502
|
+
const isWarningLine = (rawLine) => {
|
|
4503
|
+
const plain = stripAnsi(rawLine);
|
|
4504
|
+
return WARNING_PATTERNS.some((pattern) => pattern.test(plain));
|
|
4505
|
+
};
|
|
4506
|
+
/**
|
|
4507
|
+
* Style a single output line as a warning. If the line already contains ANSI
|
|
4508
|
+
* escapes (the subprocess pre-colored it), only prepend our yellow ⚠ marker
|
|
4509
|
+
* so the original colors survive. Otherwise color the whole line yellow.
|
|
4510
|
+
*/
|
|
4511
|
+
const styleWarningLine = (line) => hasAnsi(line) ? `${chalk.yellow("⚠")} ${line}` : chalk.yellow(`⚠ ${line}`);
|
|
4512
|
+
/**
|
|
4513
|
+
* Emit a CLI-owned warning. Suppressed in JSON mode; in human mode writes a
|
|
4514
|
+
* yellow, ⚠-prefixed line to stderr so it stands out from regular info logs.
|
|
4515
|
+
*/
|
|
4516
|
+
const printWarn = (message) => Effect.gen(function* () {
|
|
4517
|
+
if ((yield* OutputMode).json) return;
|
|
4518
|
+
yield* Console.warn(chalk.yellow(`⚠ warning: ${message}`));
|
|
4519
|
+
});
|
|
4520
|
+
|
|
4391
4521
|
//#endregion
|
|
4392
4522
|
//#region src/commands/build/run-step.ts
|
|
4393
|
-
const
|
|
4394
|
-
step,
|
|
4395
|
-
exitCode: 1,
|
|
4396
|
-
message: `${step} failed to spawn: ${String(cause)}`
|
|
4397
|
-
})), Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(new BuildFailedError({
|
|
4523
|
+
const buildFailed = (step, exitCode, message) => new BuildFailedError({
|
|
4398
4524
|
step,
|
|
4399
|
-
exitCode
|
|
4400
|
-
message
|
|
4401
|
-
})
|
|
4525
|
+
exitCode,
|
|
4526
|
+
message
|
|
4527
|
+
});
|
|
4528
|
+
const annotateWarning = (line) => isWarningLine(line) ? styleWarningLine(line) : void 0;
|
|
4402
4529
|
/**
|
|
4403
|
-
* Run a build step
|
|
4404
|
-
*
|
|
4530
|
+
* Run a build step in a PTY so the subprocess sees a real TTY (spinners,
|
|
4531
|
+
* progress bars, ANSI colors are preserved). Completed lines are inspected
|
|
4532
|
+
* and any detected warning is re-echoed with our yellow ⚠ annotation.
|
|
4533
|
+
*/
|
|
4534
|
+
const runStep = (cmd, step) => runInPty({
|
|
4535
|
+
command: cmd.command,
|
|
4536
|
+
args: cmd.args,
|
|
4537
|
+
cwd: cmd.cwd,
|
|
4538
|
+
env: cmd.env,
|
|
4539
|
+
onLine: annotateWarning
|
|
4540
|
+
}).pipe(Effect.flatMap((code) => code === 0 ? Effect.void : Effect.fail(buildFailed(step, code, `${step} exited with code ${code}`))));
|
|
4541
|
+
/**
|
|
4542
|
+
* Run a build step in a PTY, but feed each completed line through the supplied
|
|
4543
|
+
* xcpretty-style formatter before writing. Warning detection still applies to
|
|
4544
|
+
* the formatter's output so xcodebuild deprecation notices stand out.
|
|
4545
|
+
*
|
|
4546
|
+
* The PTY guarantees xcodebuild sees a real TTY (so it keeps colored output);
|
|
4547
|
+
* the formatter strips noise. On failure the formatter's build summary is
|
|
4548
|
+
* flushed to stderr to help diagnose.
|
|
4405
4549
|
*/
|
|
4406
4550
|
const runStepFormatted = (cmd, step, formatter) => Effect.gen(function* () {
|
|
4407
|
-
const
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
for (const output of formatted) process$1.stdout.write(`${output}\n`);
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
exitCode: 1,
|
|
4420
|
-
message: `${step} stdout stream error: ${String(cause)}`
|
|
4421
|
-
})), Effect.fork);
|
|
4422
|
-
const stderrFiber = yield* proc.stderr.pipe(Stream.decodeText(), Stream.splitLines, Stream.runForEach((line) => Effect.sync(() => process$1.stderr.write(`${line}\n`))), Effect.mapError((cause) => new BuildFailedError({
|
|
4423
|
-
step,
|
|
4424
|
-
exitCode: 1,
|
|
4425
|
-
message: `${step} stderr stream error: ${String(cause)}`
|
|
4426
|
-
})), Effect.fork);
|
|
4427
|
-
yield* Effect.all([Fiber.join(stdoutFiber), Fiber.join(stderrFiber)], { concurrency: 2 }).pipe(Effect.catchAll(() => Effect.void));
|
|
4428
|
-
const code = yield* proc.exitCode.pipe(Effect.mapError((cause) => new BuildFailedError({
|
|
4429
|
-
step,
|
|
4430
|
-
exitCode: 1,
|
|
4431
|
-
message: `${step} exit code error: ${String(cause)}`
|
|
4432
|
-
})));
|
|
4551
|
+
const code = yield* runInPty({
|
|
4552
|
+
command: cmd.command,
|
|
4553
|
+
args: cmd.args,
|
|
4554
|
+
cwd: cmd.cwd,
|
|
4555
|
+
env: cmd.env,
|
|
4556
|
+
silent: true,
|
|
4557
|
+
onLine: (line) => {
|
|
4558
|
+
const formatted = formatter.pipe(line);
|
|
4559
|
+
for (const output of formatted) if (isWarningLine(output)) process$1.stdout.write(`${styleWarningLine(output)}\n`);
|
|
4560
|
+
else process$1.stdout.write(`${output}\n`);
|
|
4561
|
+
}
|
|
4562
|
+
});
|
|
4433
4563
|
if (code !== 0) {
|
|
4434
4564
|
const summary = formatter.getBuildSummary();
|
|
4435
|
-
if (summary) process$1.stderr.write(`${summary}\n`);
|
|
4436
|
-
return yield*
|
|
4437
|
-
step,
|
|
4438
|
-
exitCode: code,
|
|
4439
|
-
message: `${step} exited with code ${code}`
|
|
4440
|
-
});
|
|
4565
|
+
if (summary.length > 0) process$1.stderr.write(`${summary}\n`);
|
|
4566
|
+
return yield* Effect.fail(buildFailed(step, code, `${step} exited with code ${code}`));
|
|
4441
4567
|
}
|
|
4442
4568
|
});
|
|
4443
4569
|
|
|
@@ -4470,7 +4596,18 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
|
4470
4596
|
applicationIdentifier,
|
|
4471
4597
|
tempDir
|
|
4472
4598
|
});
|
|
4473
|
-
yield* runStep(
|
|
4599
|
+
yield* runStep({
|
|
4600
|
+
command: "bunx",
|
|
4601
|
+
args: [
|
|
4602
|
+
"expo",
|
|
4603
|
+
"prebuild",
|
|
4604
|
+
"--platform",
|
|
4605
|
+
"android",
|
|
4606
|
+
"--clean"
|
|
4607
|
+
],
|
|
4608
|
+
cwd: projectRoot,
|
|
4609
|
+
env: commandEnv
|
|
4610
|
+
}, "expo prebuild android");
|
|
4474
4611
|
const fs = yield* FileSystem.FileSystem;
|
|
4475
4612
|
const signingGradlePath = path.join(tempDir, "signing.gradle");
|
|
4476
4613
|
yield* fs.writeFileString(signingGradlePath, renderSigningGradle({
|
|
@@ -4479,8 +4616,16 @@ const runAndroidBuild = (input) => Effect.gen(function* () {
|
|
|
4479
4616
|
keyAlias: credentials.keyAlias,
|
|
4480
4617
|
keyPassword: credentials.keyPassword
|
|
4481
4618
|
}));
|
|
4482
|
-
|
|
4483
|
-
|
|
4619
|
+
yield* runStep({
|
|
4620
|
+
command: "./gradlew",
|
|
4621
|
+
args: [
|
|
4622
|
+
"--init-script",
|
|
4623
|
+
signingGradlePath,
|
|
4624
|
+
`:app:${gradleTaskName(format, flavor, buildType)}`
|
|
4625
|
+
],
|
|
4626
|
+
cwd: androidDir,
|
|
4627
|
+
env: commandEnv
|
|
4628
|
+
}, "gradlew");
|
|
4484
4629
|
const artifactPath = yield* findAndroidArtifact({
|
|
4485
4630
|
projectRoot,
|
|
4486
4631
|
format,
|
|
@@ -5333,8 +5478,8 @@ const validateIosBuild = (params) => Effect.gen(function* () {
|
|
|
5333
5478
|
const validatedBundleIds = new Set(perBundle.map((entry) => entry.bundleId).filter((id) => id !== void 0));
|
|
5334
5479
|
for (const expected of params.expectedTargets) if (!validatedBundleIds.has(expected.bundleId)) warnings.push(`Expected signed target "${expected.bundleId}" was not found in the archive.`);
|
|
5335
5480
|
if (warnings.length > 0) {
|
|
5336
|
-
yield*
|
|
5337
|
-
for (const warning of warnings) yield*
|
|
5481
|
+
yield* printWarn("Post-build validation warnings:");
|
|
5482
|
+
for (const warning of warnings) yield* printWarn(` - ${warning}`);
|
|
5338
5483
|
}
|
|
5339
5484
|
return {
|
|
5340
5485
|
passed: warnings.length === 0,
|
|
@@ -5491,8 +5636,24 @@ const findXcworkspace = (iosDir) => Effect.gen(function* () {
|
|
|
5491
5636
|
return workspace;
|
|
5492
5637
|
});
|
|
5493
5638
|
const prebuildAndPods = (params) => Effect.gen(function* () {
|
|
5494
|
-
yield* runStep(
|
|
5495
|
-
|
|
5639
|
+
yield* runStep({
|
|
5640
|
+
command: "bunx",
|
|
5641
|
+
args: [
|
|
5642
|
+
"expo",
|
|
5643
|
+
"prebuild",
|
|
5644
|
+
"--platform",
|
|
5645
|
+
"ios",
|
|
5646
|
+
"--clean"
|
|
5647
|
+
],
|
|
5648
|
+
cwd: params.projectRoot,
|
|
5649
|
+
env: params.commandEnv
|
|
5650
|
+
}, "expo prebuild ios");
|
|
5651
|
+
yield* runStep({
|
|
5652
|
+
command: "pod",
|
|
5653
|
+
args: ["install"],
|
|
5654
|
+
cwd: params.iosDir,
|
|
5655
|
+
env: params.commandEnv
|
|
5656
|
+
}, "pod install");
|
|
5496
5657
|
});
|
|
5497
5658
|
const findAppDirectory = (root) => Effect.gen(function* () {
|
|
5498
5659
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -5527,13 +5688,46 @@ const runIosSimulatorBuild = (input) => Effect.gen(function* () {
|
|
|
5527
5688
|
const scheme = iosProfile.scheme ?? workspaceFilename.replace(/\.xcworkspace$/u, "");
|
|
5528
5689
|
const configuration = iosProfile.buildConfiguration ?? "Release";
|
|
5529
5690
|
const derivedDataPath = path.join(tempDir, "derived-data");
|
|
5530
|
-
const buildCmd =
|
|
5691
|
+
const buildCmd = {
|
|
5692
|
+
command: "xcodebuild",
|
|
5693
|
+
args: [
|
|
5694
|
+
"-workspace",
|
|
5695
|
+
workspaceFilename,
|
|
5696
|
+
"-scheme",
|
|
5697
|
+
scheme,
|
|
5698
|
+
"-configuration",
|
|
5699
|
+
configuration,
|
|
5700
|
+
"-sdk",
|
|
5701
|
+
"iphonesimulator",
|
|
5702
|
+
"-destination",
|
|
5703
|
+
"generic/platform=iOS Simulator",
|
|
5704
|
+
"-derivedDataPath",
|
|
5705
|
+
derivedDataPath,
|
|
5706
|
+
"build",
|
|
5707
|
+
"CODE_SIGNING_ALLOWED=NO",
|
|
5708
|
+
"CODE_SIGNING_REQUIRED=NO",
|
|
5709
|
+
"CODE_SIGN_IDENTITY="
|
|
5710
|
+
],
|
|
5711
|
+
cwd: iosDir,
|
|
5712
|
+
env: commandEnv
|
|
5713
|
+
};
|
|
5531
5714
|
const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
|
|
5532
5715
|
yield* formatter ? runStepFormatted(buildCmd, "xcodebuild build (simulator)", formatter) : runStep(buildCmd, "xcodebuild build (simulator)");
|
|
5533
5716
|
const appDir = yield* findAppDirectory(path.join(derivedDataPath, "Build", "Products", `${configuration}-iphonesimulator`));
|
|
5534
5717
|
const archiveName = `${path.basename(appDir, ".app")}-simulator.tar.gz`;
|
|
5535
5718
|
const archivePath = path.join(tempDir, archiveName);
|
|
5536
|
-
yield* runStep(
|
|
5719
|
+
yield* runStep({
|
|
5720
|
+
command: "tar",
|
|
5721
|
+
args: [
|
|
5722
|
+
"-czf",
|
|
5723
|
+
archivePath,
|
|
5724
|
+
"-C",
|
|
5725
|
+
path.dirname(appDir),
|
|
5726
|
+
path.basename(appDir)
|
|
5727
|
+
],
|
|
5728
|
+
cwd: projectRoot,
|
|
5729
|
+
env: commandEnv
|
|
5730
|
+
}, "tar simulator .app");
|
|
5537
5731
|
const { sha256, byteSize } = yield* sha256File(archivePath);
|
|
5538
5732
|
return {
|
|
5539
5733
|
artifactPath: archivePath,
|
|
@@ -5636,7 +5830,23 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
5636
5830
|
}))
|
|
5637
5831
|
});
|
|
5638
5832
|
const archivePath = path.join(tempDir, "build.xcarchive");
|
|
5639
|
-
const archiveCmd =
|
|
5833
|
+
const archiveCmd = {
|
|
5834
|
+
command: "xcodebuild",
|
|
5835
|
+
args: [
|
|
5836
|
+
"-workspace",
|
|
5837
|
+
workspaceFilename,
|
|
5838
|
+
"-scheme",
|
|
5839
|
+
scheme,
|
|
5840
|
+
"-configuration",
|
|
5841
|
+
configuration,
|
|
5842
|
+
"-archivePath",
|
|
5843
|
+
archivePath,
|
|
5844
|
+
"-allowProvisioningUpdates",
|
|
5845
|
+
"archive"
|
|
5846
|
+
],
|
|
5847
|
+
cwd: iosDir,
|
|
5848
|
+
env: commandEnv
|
|
5849
|
+
};
|
|
5640
5850
|
const formatter = input.rawOutput ? void 0 : createXcodebuildFormatter(projectRoot);
|
|
5641
5851
|
yield* formatter ? runStepFormatted(archiveCmd, "xcodebuild archive", formatter) : runStep(archiveCmd, "xcodebuild archive");
|
|
5642
5852
|
const exportOptionsPath = path.join(tempDir, "ExportOptions.plist");
|
|
@@ -5656,7 +5866,21 @@ const runIosDeviceBuild = (input) => Effect.gen(function* () {
|
|
|
5656
5866
|
}))
|
|
5657
5867
|
}));
|
|
5658
5868
|
const exportPath = path.join(tempDir, "export");
|
|
5659
|
-
const exportCmd =
|
|
5869
|
+
const exportCmd = {
|
|
5870
|
+
command: "xcodebuild",
|
|
5871
|
+
args: [
|
|
5872
|
+
"-exportArchive",
|
|
5873
|
+
"-archivePath",
|
|
5874
|
+
archivePath,
|
|
5875
|
+
"-exportPath",
|
|
5876
|
+
exportPath,
|
|
5877
|
+
"-exportOptionsPlist",
|
|
5878
|
+
exportOptionsPath,
|
|
5879
|
+
"-allowProvisioningUpdates"
|
|
5880
|
+
],
|
|
5881
|
+
cwd: iosDir,
|
|
5882
|
+
env: commandEnv
|
|
5883
|
+
};
|
|
5660
5884
|
yield* formatter ? runStepFormatted(exportCmd, "xcodebuild exportArchive", formatter) : runStep(exportCmd, "xcodebuild exportArchive");
|
|
5661
5885
|
yield* validateIosBuild({
|
|
5662
5886
|
archivePath,
|
|
@@ -6241,7 +6465,7 @@ const readGradleConfig = (androidDir) => Effect.gen(function* () {
|
|
|
6241
6465
|
const warnOnGradleMismatch = (gradleConfig, expectedPackage) => {
|
|
6242
6466
|
if (!gradleConfig?.applicationId) return Effect.void;
|
|
6243
6467
|
if (gradleConfig.applicationId === expectedPackage) return Effect.void;
|
|
6244
|
-
return
|
|
6468
|
+
return printWarn(`Gradle applicationId "${gradleConfig.applicationId}" differs from app.json package "${expectedPackage}". The Gradle value will be used in the built APK/AAB.`);
|
|
6245
6469
|
};
|
|
6246
6470
|
/**
|
|
6247
6471
|
* Strip Groovy single-line and block comments.
|
|
@@ -6298,6 +6522,131 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
|
|
|
6298
6522
|
})));
|
|
6299
6523
|
});
|
|
6300
6524
|
|
|
6525
|
+
//#endregion
|
|
6526
|
+
//#region src/lib/project-staging.ts
|
|
6527
|
+
const LOCKFILES = [
|
|
6528
|
+
["bun.lock", "bun"],
|
|
6529
|
+
["bun.lockb", "bun"],
|
|
6530
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
6531
|
+
["yarn.lock", "yarn"],
|
|
6532
|
+
["package-lock.json", "npm"]
|
|
6533
|
+
];
|
|
6534
|
+
/**
|
|
6535
|
+
* Paths never copied into staging — covers generated native build outputs and
|
|
6536
|
+
* dependency dirs that must be reinstalled fresh in staging.
|
|
6537
|
+
*/
|
|
6538
|
+
const ALWAYS_IGNORE = [
|
|
6539
|
+
"node_modules",
|
|
6540
|
+
".git",
|
|
6541
|
+
"ios/build",
|
|
6542
|
+
"ios/Pods",
|
|
6543
|
+
"ios/DerivedData",
|
|
6544
|
+
"android/build",
|
|
6545
|
+
"android/app/build",
|
|
6546
|
+
"android/.gradle",
|
|
6547
|
+
"android/.kotlin",
|
|
6548
|
+
".expo",
|
|
6549
|
+
".gradle",
|
|
6550
|
+
".turbo",
|
|
6551
|
+
"dist"
|
|
6552
|
+
];
|
|
6553
|
+
const findLockfile = (fs, dir) => Effect.gen(function* () {
|
|
6554
|
+
for (const [name, pm] of LOCKFILES) if (yield* fs.exists(path.join(dir, name)).pipe(Effect.catchAll(() => Effect.succeed(false)))) return pm;
|
|
6555
|
+
});
|
|
6556
|
+
const walkUpForLockfile = (startCwd, dir) => Effect.gen(function* () {
|
|
6557
|
+
const pm = yield* findLockfile(yield* FileSystem.FileSystem, dir);
|
|
6558
|
+
if (pm !== void 0) return {
|
|
6559
|
+
workspaceRoot: dir,
|
|
6560
|
+
packageManager: pm
|
|
6561
|
+
};
|
|
6562
|
+
const parent = path.dirname(dir);
|
|
6563
|
+
if (parent === dir) return {
|
|
6564
|
+
workspaceRoot: startCwd,
|
|
6565
|
+
packageManager: "bun"
|
|
6566
|
+
};
|
|
6567
|
+
return yield* walkUpForLockfile(startCwd, parent);
|
|
6568
|
+
});
|
|
6569
|
+
/**
|
|
6570
|
+
* Walk up from `cwd` to the first ancestor directory containing a lockfile.
|
|
6571
|
+
* That directory is the install root (monorepo workspace root or the app dir
|
|
6572
|
+
* itself in single-app layouts). Defaults to `cwd` + bun when no lockfile is
|
|
6573
|
+
* found anywhere up to the volume root.
|
|
6574
|
+
*/
|
|
6575
|
+
const detectWorkspaceRoot = (cwd) => walkUpForLockfile(cwd, cwd);
|
|
6576
|
+
/**
|
|
6577
|
+
* Build an `Ignore` matcher for the workspace root. `.easignore` REPLACES
|
|
6578
|
+
* `.gitignore` when present (matches EAS semantics); otherwise `.gitignore`
|
|
6579
|
+
* is layered on top of the always-ignore baseline.
|
|
6580
|
+
*/
|
|
6581
|
+
const buildIgnoreInstance = (workspaceRoot) => Effect.gen(function* () {
|
|
6582
|
+
const fs = yield* FileSystem.FileSystem;
|
|
6583
|
+
const ig = ignore();
|
|
6584
|
+
ig.add([...ALWAYS_IGNORE]);
|
|
6585
|
+
const easignorePath = path.join(workspaceRoot, ".easignore");
|
|
6586
|
+
if (yield* fs.exists(easignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
|
|
6587
|
+
const content = yield* fs.readFileString(easignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
6588
|
+
ig.add(content);
|
|
6589
|
+
return ig;
|
|
6590
|
+
}
|
|
6591
|
+
const gitignorePath = path.join(workspaceRoot, ".gitignore");
|
|
6592
|
+
if (yield* fs.exists(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed(false)))) {
|
|
6593
|
+
const content = yield* fs.readFileString(gitignorePath).pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
6594
|
+
ig.add(content);
|
|
6595
|
+
}
|
|
6596
|
+
return ig;
|
|
6597
|
+
});
|
|
6598
|
+
const copyProjectTree = (params) => Effect.tryPromise({
|
|
6599
|
+
try: async () => {
|
|
6600
|
+
await promises.cp(params.source, params.dest, {
|
|
6601
|
+
recursive: true,
|
|
6602
|
+
dereference: false,
|
|
6603
|
+
filter: (src) => {
|
|
6604
|
+
const rel = path.relative(params.source, src);
|
|
6605
|
+
if (rel === "") return true;
|
|
6606
|
+
const posixRel = rel.split(path.sep).join("/");
|
|
6607
|
+
return !params.ig.ignores(posixRel);
|
|
6608
|
+
}
|
|
6609
|
+
});
|
|
6610
|
+
},
|
|
6611
|
+
catch: (cause) => new StagingError({ message: `Failed to copy project to staging dir: ${formatCause(cause)}` })
|
|
6612
|
+
});
|
|
6613
|
+
const runInstall = (params) => runStep({
|
|
6614
|
+
command: params.packageManager,
|
|
6615
|
+
args: ["install"],
|
|
6616
|
+
cwd: params.stagingRoot,
|
|
6617
|
+
env: params.env
|
|
6618
|
+
}, `${params.packageManager} install`);
|
|
6619
|
+
/**
|
|
6620
|
+
* Copy the user's project (or workspace root, for monorepos) into a fresh
|
|
6621
|
+
* directory inside `tempDir`, then run `<pm> install` there. The build then
|
|
6622
|
+
* runs entirely against the staged copy — the user's working tree stays clean
|
|
6623
|
+
* regardless of what `expo prebuild`, `pod install`, or `gradlew` write.
|
|
6624
|
+
*/
|
|
6625
|
+
const prepareStagingProject = (input) => Effect.gen(function* () {
|
|
6626
|
+
const runtime = yield* CliRuntime;
|
|
6627
|
+
const { workspaceRoot, packageManager } = yield* detectWorkspaceRoot(input.userCwd);
|
|
6628
|
+
const relAppPath = path.relative(workspaceRoot, input.userCwd);
|
|
6629
|
+
const stagingRoot = path.join(input.tempDir, "project");
|
|
6630
|
+
const projectRoot = relAppPath === "" ? stagingRoot : path.join(stagingRoot, relAppPath);
|
|
6631
|
+
yield* Console.log(`Staging build into ${stagingRoot}${relAppPath === "" ? "" : ` (app: ${relAppPath})`}`);
|
|
6632
|
+
yield* copyProjectTree({
|
|
6633
|
+
source: workspaceRoot,
|
|
6634
|
+
dest: stagingRoot,
|
|
6635
|
+
ig: yield* buildIgnoreInstance(workspaceRoot)
|
|
6636
|
+
});
|
|
6637
|
+
yield* runInstall({
|
|
6638
|
+
stagingRoot,
|
|
6639
|
+
packageManager,
|
|
6640
|
+
env: yield* runtime.commandEnvironment(input.envVars)
|
|
6641
|
+
});
|
|
6642
|
+
return {
|
|
6643
|
+
stagingRoot,
|
|
6644
|
+
projectRoot,
|
|
6645
|
+
packageManager,
|
|
6646
|
+
relAppPath
|
|
6647
|
+
};
|
|
6648
|
+
});
|
|
6649
|
+
|
|
6301
6650
|
//#endregion
|
|
6302
6651
|
//#region src/lib/repo-clean.ts
|
|
6303
6652
|
const MAX_FILES_SHOWN = 10;
|
|
@@ -6447,35 +6796,40 @@ const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
|
|
|
6447
6796
|
});
|
|
6448
6797
|
const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
6449
6798
|
const api = yield* apiClient;
|
|
6450
|
-
const
|
|
6799
|
+
const userCwd = yield* (yield* CliRuntime).cwd;
|
|
6451
6800
|
yield* ensureRepoClean({
|
|
6452
|
-
projectRoot,
|
|
6801
|
+
projectRoot: userCwd,
|
|
6453
6802
|
allowDirty: options.allowDirty ?? false,
|
|
6454
6803
|
label: "build"
|
|
6455
6804
|
});
|
|
6456
|
-
const baseConfig = yield* readExpoConfig(
|
|
6805
|
+
const baseConfig = yield* readExpoConfig(userCwd);
|
|
6457
6806
|
const projectId = yield* extractProjectId(baseConfig);
|
|
6458
6807
|
const platform = yield* detectPlatform(options.platform, baseConfig);
|
|
6459
|
-
const profile = yield* readBuildProfile(
|
|
6808
|
+
const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
|
|
6460
6809
|
const envVars = yield* pullEnvVars(api, {
|
|
6461
6810
|
projectId,
|
|
6462
6811
|
environment: profile.environment
|
|
6463
6812
|
});
|
|
6464
6813
|
yield* applyAutoIncrement({
|
|
6465
|
-
projectRoot,
|
|
6814
|
+
projectRoot: userCwd,
|
|
6466
6815
|
platform,
|
|
6467
|
-
config: yield* readExpoConfig(
|
|
6816
|
+
config: yield* readExpoConfig(userCwd, envVars),
|
|
6468
6817
|
...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
|
|
6469
6818
|
...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
|
|
6470
6819
|
});
|
|
6471
|
-
const appMeta = yield* readAppMeta(yield* readExpoConfig(
|
|
6820
|
+
const appMeta = yield* readAppMeta(yield* readExpoConfig(userCwd, envVars), platform);
|
|
6472
6821
|
const runtimeVersion = yield* resolveRuntimeVersion({
|
|
6473
6822
|
raw: appMeta.rawRuntimeVersion,
|
|
6474
6823
|
appVersion: appMeta.appVersion,
|
|
6475
|
-
projectRoot
|
|
6824
|
+
projectRoot: userCwd
|
|
6476
6825
|
});
|
|
6477
|
-
if (options.clearCache) yield* clearBuildCaches(
|
|
6826
|
+
if (options.clearCache) yield* clearBuildCaches(userCwd);
|
|
6478
6827
|
const tempDir = yield* acquireBuildTempDir;
|
|
6828
|
+
const staging = yield* prepareStagingProject({
|
|
6829
|
+
userCwd,
|
|
6830
|
+
tempDir,
|
|
6831
|
+
envVars
|
|
6832
|
+
});
|
|
6479
6833
|
yield* Console.log(`Building ${platform} artifact for profile "${profile.name}" (runtimeVersion=${runtimeVersion})`);
|
|
6480
6834
|
const { build, target, bundleId } = yield* runPlatformBuild({
|
|
6481
6835
|
api,
|
|
@@ -6485,14 +6839,14 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
6485
6839
|
appMeta,
|
|
6486
6840
|
envVars,
|
|
6487
6841
|
projectId,
|
|
6488
|
-
projectRoot,
|
|
6842
|
+
projectRoot: staging.projectRoot,
|
|
6489
6843
|
tempDir
|
|
6490
6844
|
});
|
|
6491
6845
|
yield* Console.log(`Artifact produced: ${build.artifactPath}`);
|
|
6492
6846
|
let exportedArtifactPath = void 0;
|
|
6493
6847
|
if (options.output !== void 0) {
|
|
6494
6848
|
const fs = yield* FileSystem.FileSystem;
|
|
6495
|
-
const outputPath = path.resolve(
|
|
6849
|
+
const outputPath = path.resolve(userCwd, options.output);
|
|
6496
6850
|
const outputDir = path.dirname(outputPath);
|
|
6497
6851
|
yield* fs.makeDirectory(outputDir, { recursive: true }).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to create output directory: ${formatCause(cause)}` })));
|
|
6498
6852
|
yield* fs.copyFile(build.artifactPath, outputPath).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to copy artifact to ${outputPath}: ${formatCause(cause)}` })));
|
|
@@ -6509,7 +6863,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
|
|
|
6509
6863
|
]);
|
|
6510
6864
|
return;
|
|
6511
6865
|
}
|
|
6512
|
-
const rawGitContext = yield* readGitContext(
|
|
6866
|
+
const rawGitContext = yield* readGitContext(userCwd);
|
|
6513
6867
|
const gitContext = {
|
|
6514
6868
|
...rawGitContext.ref === void 0 ? {} : { ref: rawGitContext.ref },
|
|
6515
6869
|
...rawGitContext.commit === void 0 ? {} : { commit: rawGitContext.commit },
|
|
@@ -6653,7 +7007,8 @@ const BUILD_EXIT_EXTRAS = {
|
|
|
6653
7007
|
PresignedUrlExpiredError: 7,
|
|
6654
7008
|
CompleteError: 7,
|
|
6655
7009
|
EnvExportError: 7,
|
|
6656
|
-
DirtyRepoError: 3
|
|
7010
|
+
DirtyRepoError: 3,
|
|
7011
|
+
StagingError: 6
|
|
6657
7012
|
};
|
|
6658
7013
|
const buildCommand = defineCommand({
|
|
6659
7014
|
meta: {
|