@better-update/cli 0.16.0 → 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 CHANGED
@@ -14,12 +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
18
  import { spawn as spawn$1 } from "node-pty";
19
19
  import chalk from "chalk";
20
20
  import os from "node:os";
21
21
  import plistMod from "@expo/plist";
22
22
  import { ExpoRunFormatter } from "@expo/xcpretty";
23
+ import ignore from "ignore";
23
24
  import { Buffer as Buffer$1 } from "node:buffer";
24
25
  import { getFormattedSerialNumber, getX509Certificate, parsePKCS12 } from "@expo/pkcs12";
25
26
  import qrcode from "qrcode-terminal";
@@ -30,7 +31,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
30
31
 
31
32
  //#endregion
32
33
  //#region package.json
33
- var version = "0.16.0";
34
+ var version = "0.17.0";
34
35
 
35
36
  //#endregion
36
37
  //#region src/lib/interactive-mode.ts
@@ -1665,6 +1666,7 @@ var InvalidArgumentError = class extends Data.TaggedError("InvalidArgumentError"
1665
1666
  var InteractiveProhibitedError = class extends Data.TaggedError("InteractiveProhibitedError") {};
1666
1667
  var CredentialsJsonError = class extends Data.TaggedError("CredentialsJsonError") {};
1667
1668
  var DirtyRepoError = class extends Data.TaggedError("DirtyRepoError") {};
1669
+ var StagingError = class extends Data.TaggedError("StagingError") {};
1668
1670
 
1669
1671
  //#endregion
1670
1672
  //#region src/lib/format-error.ts
@@ -6520,6 +6522,131 @@ const detectPlatform = (explicit, config) => Effect.gen(function* () {
6520
6522
  })));
6521
6523
  });
6522
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
+
6523
6650
  //#endregion
6524
6651
  //#region src/lib/repo-clean.ts
6525
6652
  const MAX_FILES_SHOWN = 10;
@@ -6669,35 +6796,40 @@ const resolveProfileName = (projectRoot, requested) => Effect.gen(function* () {
6669
6796
  });
6670
6797
  const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6671
6798
  const api = yield* apiClient;
6672
- const projectRoot = yield* (yield* CliRuntime).cwd;
6799
+ const userCwd = yield* (yield* CliRuntime).cwd;
6673
6800
  yield* ensureRepoClean({
6674
- projectRoot,
6801
+ projectRoot: userCwd,
6675
6802
  allowDirty: options.allowDirty ?? false,
6676
6803
  label: "build"
6677
6804
  });
6678
- const baseConfig = yield* readExpoConfig(projectRoot);
6805
+ const baseConfig = yield* readExpoConfig(userCwd);
6679
6806
  const projectId = yield* extractProjectId(baseConfig);
6680
6807
  const platform = yield* detectPlatform(options.platform, baseConfig);
6681
- const profile = yield* readBuildProfile(projectRoot, yield* resolveProfileName(projectRoot, options.profileName));
6808
+ const profile = yield* readBuildProfile(userCwd, yield* resolveProfileName(userCwd, options.profileName));
6682
6809
  const envVars = yield* pullEnvVars(api, {
6683
6810
  projectId,
6684
6811
  environment: profile.environment
6685
6812
  });
6686
6813
  yield* applyAutoIncrement({
6687
- projectRoot,
6814
+ projectRoot: userCwd,
6688
6815
  platform,
6689
- config: yield* readExpoConfig(projectRoot, envVars),
6816
+ config: yield* readExpoConfig(userCwd, envVars),
6690
6817
  ...platform === "ios" && profile.ios?.autoIncrement !== void 0 ? { iosMode: profile.ios.autoIncrement } : {},
6691
6818
  ...platform === "android" && profile.android?.autoIncrement !== void 0 ? { androidMode: profile.android.autoIncrement } : {}
6692
6819
  });
6693
- const appMeta = yield* readAppMeta(yield* readExpoConfig(projectRoot, envVars), platform);
6820
+ const appMeta = yield* readAppMeta(yield* readExpoConfig(userCwd, envVars), platform);
6694
6821
  const runtimeVersion = yield* resolveRuntimeVersion({
6695
6822
  raw: appMeta.rawRuntimeVersion,
6696
6823
  appVersion: appMeta.appVersion,
6697
- projectRoot
6824
+ projectRoot: userCwd
6698
6825
  });
6699
- if (options.clearCache) yield* clearBuildCaches(projectRoot);
6826
+ if (options.clearCache) yield* clearBuildCaches(userCwd);
6700
6827
  const tempDir = yield* acquireBuildTempDir;
6828
+ const staging = yield* prepareStagingProject({
6829
+ userCwd,
6830
+ tempDir,
6831
+ envVars
6832
+ });
6701
6833
  yield* Console.log(`Building ${platform} artifact for profile "${profile.name}" (runtimeVersion=${runtimeVersion})`);
6702
6834
  const { build, target, bundleId } = yield* runPlatformBuild({
6703
6835
  api,
@@ -6707,14 +6839,14 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6707
6839
  appMeta,
6708
6840
  envVars,
6709
6841
  projectId,
6710
- projectRoot,
6842
+ projectRoot: staging.projectRoot,
6711
6843
  tempDir
6712
6844
  });
6713
6845
  yield* Console.log(`Artifact produced: ${build.artifactPath}`);
6714
6846
  let exportedArtifactPath = void 0;
6715
6847
  if (options.output !== void 0) {
6716
6848
  const fs = yield* FileSystem.FileSystem;
6717
- const outputPath = path.resolve(projectRoot, options.output);
6849
+ const outputPath = path.resolve(userCwd, options.output);
6718
6850
  const outputDir = path.dirname(outputPath);
6719
6851
  yield* fs.makeDirectory(outputDir, { recursive: true }).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to create output directory: ${formatCause(cause)}` })));
6720
6852
  yield* fs.copyFile(build.artifactPath, outputPath).pipe(Effect.mapError((cause) => new BuildProfileError({ message: `Failed to copy artifact to ${outputPath}: ${formatCause(cause)}` })));
@@ -6731,7 +6863,7 @@ const runBuildWorkflow = (options) => Effect.scoped(Effect.gen(function* () {
6731
6863
  ]);
6732
6864
  return;
6733
6865
  }
6734
- const rawGitContext = yield* readGitContext(projectRoot);
6866
+ const rawGitContext = yield* readGitContext(userCwd);
6735
6867
  const gitContext = {
6736
6868
  ...rawGitContext.ref === void 0 ? {} : { ref: rawGitContext.ref },
6737
6869
  ...rawGitContext.commit === void 0 ? {} : { commit: rawGitContext.commit },
@@ -6875,7 +7007,8 @@ const BUILD_EXIT_EXTRAS = {
6875
7007
  PresignedUrlExpiredError: 7,
6876
7008
  CompleteError: 7,
6877
7009
  EnvExportError: 7,
6878
- DirtyRepoError: 3
7010
+ DirtyRepoError: 3,
7011
+ StagingError: 6
6879
7012
  };
6880
7013
  const buildCommand = defineCommand({
6881
7014
  meta: {