@akanjs/cli 2.4.0-rc.7 → 2.4.0-rc.9

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.
@@ -722,7 +722,6 @@ var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
722
722
  var NATIVE_RUNTIME_PACKAGES = ["sharp"];
723
723
  var DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"];
724
724
  var MOBILE_RUNTIME_PACKAGES = [
725
- "firebase",
726
725
  "@capacitor/cli",
727
726
  "@capacitor/core",
728
727
  "@capacitor/ios",
@@ -730,7 +729,6 @@ var MOBILE_RUNTIME_PACKAGES = [
730
729
  "@capacitor/assets"
731
730
  ];
732
731
  var MOBILE_APP_CAPACITOR_PLUGINS = [
733
- "@capacitor-community/fcm",
734
732
  "@capacitor/app",
735
733
  "@capacitor/browser",
736
734
  "@capacitor/camera",
@@ -780,6 +778,13 @@ var normalizeStringList = (values) => {
780
778
  const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
781
779
  return normalized.length > 0 ? [...new Set(normalized)] : undefined;
782
780
  };
781
+ var sanitizeAppIdSegment = (value) => {
782
+ const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
783
+ if (!cleaned)
784
+ return "app";
785
+ return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
786
+ };
787
+ var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
783
788
  var normalizeDeepLinkDomain = (domain) => {
784
789
  const normalized = domain.trim();
785
790
  if (!normalized)
@@ -820,6 +825,7 @@ class AkanAppConfig {
820
825
  i18n;
821
826
  publicEnv;
822
827
  mobile;
828
+ hasMobileConfig;
823
829
  secrets;
824
830
  baseDevEnv;
825
831
  libs;
@@ -850,6 +856,7 @@ class AkanAppConfig {
850
856
  process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
851
857
  this.publicEnv = config?.publicEnv ?? [];
852
858
  this.secrets = config?.secrets ?? [];
859
+ this.hasMobileConfig = Boolean(config.mobile);
853
860
  this.mobile = this.#resolveMobileConfig(config.mobile);
854
861
  this.docker = this.#makeDockerContent(config?.docker ?? {});
855
862
  }
@@ -860,7 +867,7 @@ class AkanAppConfig {
860
867
  ...rawMobile
861
868
  } = mobile ?? {};
862
869
  const appName = rawMobile.appName ?? this.app.name;
863
- const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
870
+ const appId = rawMobile.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
864
871
  const version = rawMobile.version ?? "0.0.1";
865
872
  const buildNum = rawMobile.buildNum ?? 1;
866
873
  const defaultTargetName = this.#defaultMobileTargetName(rawTargets);
@@ -4326,6 +4333,8 @@ var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
4326
4333
  var BACKEND_STDERR_TAIL_LIMIT = 40;
4327
4334
  var BUILDER_READY_TIMEOUT_MS = 150000;
4328
4335
  var BUILDER_START_MAX_ATTEMPTS = 3;
4336
+ var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
4337
+ var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
4329
4338
  var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
4330
4339
  var NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav|html)$/i;
4331
4340
  var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
@@ -4402,6 +4411,43 @@ var mergeBackendRestartReasons = (current, next) => ({
4402
4411
  });
4403
4412
  var shouldReplaceLastGoodMessage = (current, next) => !current || generationValue(next.data.generation) >= generationValue(current.data.generation);
4404
4413
  var shouldQueueBuildStatusReplay = (backendReady, pendingReplayCount) => !backendReady || pendingReplayCount > 0;
4414
+ var hasBuildFailureForGeneration = (statusByPhase, generation) => {
4415
+ if (typeof generation !== "number")
4416
+ return false;
4417
+ for (const status of statusByPhase.values()) {
4418
+ if (!status.ok && status.generation === generation)
4419
+ return true;
4420
+ }
4421
+ return false;
4422
+ };
4423
+ var mergeDevPlans = (current, next) => {
4424
+ if (!current)
4425
+ return next;
4426
+ if (!next)
4427
+ return current;
4428
+ const reasonByFile = { ...current.reasonByFile };
4429
+ for (const [file, reasons] of Object.entries(next.reasonByFile)) {
4430
+ reasonByFile[file] = [...new Set([...reasonByFile[file] ?? [], ...reasons])].sort();
4431
+ }
4432
+ return {
4433
+ generation: Math.max(current.generation, next.generation),
4434
+ files: [...new Set([...current.files, ...next.files])].sort(),
4435
+ generatedFiles: [...new Set([...current.generatedFiles, ...next.generatedFiles])].sort(),
4436
+ roles: [...new Set([...current.roles, ...next.roles])].sort(),
4437
+ actions: [...new Set([...current.actions, ...next.actions])].sort(),
4438
+ reasonByFile
4439
+ };
4440
+ };
4441
+ var mergeInvalidateMessages = (current, next) => {
4442
+ const generation = Math.max(generationValue(current.generation), generationValue(next.generation));
4443
+ return {
4444
+ type: "invalidate",
4445
+ kinds: [...new Set([...current.kinds, ...next.kinds])].sort(),
4446
+ files: [...new Set([...current.files, ...next.files])].sort(),
4447
+ generation: generation >= 0 ? generation : undefined,
4448
+ devPlan: mergeDevPlans(current.devPlan, next.devPlan)
4449
+ };
4450
+ };
4405
4451
  var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
4406
4452
 
4407
4453
  class BackendImportGraph {
@@ -4524,8 +4570,12 @@ class AkanAppHost {
4524
4570
  #restartTimer = null;
4525
4571
  #backendRecoveryTimer = null;
4526
4572
  #backendRecoveryAttempts = 0;
4573
+ #backendGaveUp = false;
4527
4574
  #backendLifecycleState = "stopped";
4528
4575
  #pendingRestartReason = null;
4576
+ #pendingRecycle = null;
4577
+ #builderRecoveryTimer = null;
4578
+ #builderRecoveryAttempts = 0;
4529
4579
  #backendStartStatus = null;
4530
4580
  #backendBuildStatusGeneration = 0;
4531
4581
  #backendStderrTail = [];
@@ -4563,6 +4613,10 @@ class AkanAppHost {
4563
4613
  clearTimeout(this.#backendRecoveryTimer);
4564
4614
  this.#backendRecoveryTimer = null;
4565
4615
  }
4616
+ if (this.#builderRecoveryTimer) {
4617
+ clearTimeout(this.#builderRecoveryTimer);
4618
+ this.#builderRecoveryTimer = null;
4619
+ }
4566
4620
  await this.#stopBackend();
4567
4621
  this.#stopBuilder();
4568
4622
  return this;
@@ -4578,6 +4632,7 @@ class AkanAppHost {
4578
4632
  }
4579
4633
  #startBackend(startStatus = null) {
4580
4634
  this.#backendStartStatus = startStatus;
4635
+ this.#backendGaveUp = false;
4581
4636
  this.#setBackendLifecycleState("starting");
4582
4637
  this.#backendReady = false;
4583
4638
  this.#backendStderrTail = [];
@@ -4769,7 +4824,8 @@ class AkanAppHost {
4769
4824
  if (this.#backendRecoveryTimer || this.#backend)
4770
4825
  return;
4771
4826
  if (shouldAbandonBackendRecovery(this.#backendRecoveryAttempts)) {
4772
- const message = `Backend exited ${this.#backendRecoveryAttempts} times in a row (${reason}); waiting for a server-side edit to retry.`;
4827
+ const message = `Backend exited ${this.#backendRecoveryAttempts} times in a row (${reason}); waiting for an edit or a green build to retry.`;
4828
+ this.#backendGaveUp = true;
4773
4829
  this.#setBackendLifecycleState("stopped", `gave up after ${this.#backendRecoveryAttempts} recovery attempts`);
4774
4830
  this.logger.error(`[backend-recovery] ${message}`);
4775
4831
  if (this.#backendStderrTail.length > 0) {
@@ -4816,6 +4872,7 @@ ${this.#backendStderrTail.join(`
4816
4872
  if (message.type === "build-status") {
4817
4873
  this.#recordBuildStatus(message.data);
4818
4874
  this.#sendOrQueueBuildStatus(message.data);
4875
+ this.#reviveBackendAfterGreenBuild(message.data);
4819
4876
  return;
4820
4877
  }
4821
4878
  if (message.type === "pages-updated")
@@ -4830,19 +4887,25 @@ ${this.#backendStderrTail.join(`
4830
4887
  }
4831
4888
  async#handleInvalidate(message) {
4832
4889
  this.#logDevPlan(message);
4833
- if (shouldRestartDevHostByDevPlan(message)) {
4834
- try {
4835
- await this.#restartDevHost(message);
4836
- } catch (err) {
4837
- this.#recordDevHostRestartFailure(message, err, "Config");
4890
+ const wantsDevHostRestart = shouldRestartDevHostByDevPlan(message);
4891
+ const pending = this.#pendingRecycle;
4892
+ if (wantsDevHostRestart || shouldRestartBuilderByDevPlan(message) || pending && message.kinds.includes("code")) {
4893
+ const refreshConfig = wantsDevHostRestart || (pending?.refreshConfig ?? false);
4894
+ const merged = pending ? mergeInvalidateMessages(pending.message, message) : message;
4895
+ const generation = message.devPlan?.generation ?? message.generation;
4896
+ if (hasBuildFailureForGeneration(this.#buildStatusByPhase, generation)) {
4897
+ this.#deferRecycle(merged, { refreshConfig, generation });
4898
+ return;
4838
4899
  }
4839
- return;
4840
- }
4841
- if (shouldRestartBuilderByDevPlan(message)) {
4900
+ this.#pendingRecycle = null;
4842
4901
  try {
4843
- await this.#restartDevChildren(message);
4902
+ if (refreshConfig)
4903
+ await this.#restartDevHost(merged);
4904
+ else
4905
+ await this.#restartDevChildren(merged);
4844
4906
  } catch (err) {
4845
- this.#recordDevHostRestartFailure(message, err, "Runtime metadata");
4907
+ this.#recordDevHostRestartFailure(merged, err, refreshConfig ? "Config" : "Runtime metadata");
4908
+ this.#resurrectDevChildren(merged);
4846
4909
  }
4847
4910
  return;
4848
4911
  }
@@ -4852,6 +4915,70 @@ ${this.#backendStderrTail.join(`
4852
4915
  }
4853
4916
  this.#sendToBackend(message);
4854
4917
  }
4918
+ #deferRecycle(message, { refreshConfig, generation }) {
4919
+ this.#pendingRecycle = { message, refreshConfig };
4920
+ const kind = refreshConfig ? "Config" : "Runtime metadata";
4921
+ this.logger.warn(`[dev-host] ${kind.toLowerCase()} restart deferred generation=${generation ?? "(unknown)"}; keeping the running dev server until the build error is fixed`);
4922
+ const status = {
4923
+ generation: generation ?? this.#nextBackendBuildStatusGeneration(),
4924
+ phase: "scan",
4925
+ ok: false,
4926
+ files: message.files,
4927
+ message: `${kind} change is on hold while the build is failing; it will apply automatically once the error is fixed.`
4928
+ };
4929
+ this.#recordBuildStatus(status);
4930
+ this.#sendOrQueueBuildStatus(status);
4931
+ }
4932
+ #resurrectDevChildren(message) {
4933
+ const generation = message.devPlan?.generation ?? message.generation;
4934
+ if (!this.#backend)
4935
+ this.#startBackend({ generation, files: message.files });
4936
+ this.#scheduleBuilderRecovery({ generation, files: message.files });
4937
+ }
4938
+ #scheduleBuilderRecovery(reason) {
4939
+ if (this.#builderRecoveryTimer || this.#builder)
4940
+ return;
4941
+ const attempt = this.#builderRecoveryAttempts;
4942
+ const delay = Math.min(BUILDER_RECOVERY_BASE_DELAY_MS * 2 ** attempt, BUILDER_RECOVERY_MAX_DELAY_MS);
4943
+ this.#builderRecoveryAttempts = attempt + 1;
4944
+ this.logger.warn(`[builder-recovery] builder is down; retrying start in ${delay}ms (attempt ${this.#builderRecoveryAttempts})`);
4945
+ this.#builderRecoveryTimer = setTimeout(() => {
4946
+ this.#builderRecoveryTimer = null;
4947
+ if (this.#builder)
4948
+ return;
4949
+ this.#recoverBuilder(reason);
4950
+ }, delay);
4951
+ }
4952
+ async#recoverBuilder(reason) {
4953
+ try {
4954
+ await this.#startBuilder();
4955
+ } catch (err) {
4956
+ this.logger.warn(`[builder-recovery] builder start failed: ${err instanceof Error ? err.message : String(err)}`);
4957
+ this.#scheduleBuilderRecovery(reason);
4958
+ return;
4959
+ }
4960
+ this.#builderRecoveryAttempts = 0;
4961
+ this.logger.info("[builder-recovery] builder recovered");
4962
+ const status = {
4963
+ generation: reason.generation ?? this.#nextBackendBuildStatusGeneration(),
4964
+ phase: "scan",
4965
+ ok: true,
4966
+ files: reason.files,
4967
+ message: "Builder recovered"
4968
+ };
4969
+ this.#recordBuildStatus(status);
4970
+ this.#sendOrQueueBuildStatus(status);
4971
+ if (!this.#backend && !this.#backendRecoveryTimer) {
4972
+ this.#startBackend({ generation: reason.generation, files: reason.files });
4973
+ }
4974
+ }
4975
+ #reviveBackendAfterGreenBuild(status) {
4976
+ if (!status.ok || !this.#backendGaveUp || this.#backend || this.#backendRecoveryTimer)
4977
+ return;
4978
+ this.logger.info(`[backend-recovery] build went green (generation=${status.generation}); retrying backend`);
4979
+ this.#backendRecoveryAttempts = 0;
4980
+ this.#startBackend({ generation: status.generation, files: status.files });
4981
+ }
4855
4982
  async#restartDevChildren(message) {
4856
4983
  const generation = message.devPlan?.generation ?? message.generation;
4857
4984
  this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
@@ -4872,6 +4999,11 @@ ${this.#backendStderrTail.join(`
4872
4999
  clearTimeout(this.#backendRecoveryTimer);
4873
5000
  this.#backendRecoveryTimer = null;
4874
5001
  }
5002
+ if (this.#builderRecoveryTimer) {
5003
+ clearTimeout(this.#builderRecoveryTimer);
5004
+ this.#builderRecoveryTimer = null;
5005
+ }
5006
+ this.#builderRecoveryAttempts = 0;
4875
5007
  this.#pendingRestartReason = null;
4876
5008
  this.#lastGoodFrontend = {};
4877
5009
  this.#buildStatusByPhase.clear();
@@ -4909,7 +5041,7 @@ ${this.#backendStderrTail.join(`
4909
5041
  phase: "scan",
4910
5042
  ok: false,
4911
5043
  files: message.files,
4912
- message: `${kind} change requires restarting \`akan start\` to apply: ${detail}`
5044
+ message: `${kind} change failed to apply; recovering the dev server automatically: ${detail}`
4913
5045
  };
4914
5046
  this.#recordBuildStatus(status);
4915
5047
  this.#sendOrQueueBuildStatus(status);
@@ -14079,6 +14211,7 @@ var resolveMobilePath = (target, pathname) => {
14079
14211
  };
14080
14212
  var targetHtmlFilename = (target) => target.basePath?.replace(/^\/+|\/+$/g, "") ? `${target.basePath.replace(/^\/+|\/+$/g, "")}.html` : "index.html";
14081
14213
  // pkgs/@akanjs/devkit/capacitorApp.ts
14214
+ var SWIFTUICORE_MIN_IOS_MAJOR = 18;
14082
14215
  var iosNativeBlockedEnvKeys = new Set([
14083
14216
  "AR",
14084
14217
  "AS",
@@ -14116,17 +14249,56 @@ async function writeRootCapacitorConfig(appRoot, content) {
14116
14249
  await clearRootCapacitorConfigs(appRoot);
14117
14250
  await Bun.write(path42.join(appRoot, "capacitor.config.json"), content);
14118
14251
  }
14119
- var getLocalIP = () => {
14120
- const interfaces = os.networkInterfaces();
14121
- for (const iface of Object.values(interfaces)) {
14122
- if (!iface)
14123
- continue;
14124
- for (const alias of iface) {
14125
- if (alias.family === "IPv4" && !alias.internal)
14126
- return alias.address;
14252
+ var virtualInterfacePrefixes = [
14253
+ "bridge",
14254
+ "utun",
14255
+ "llw",
14256
+ "awdl",
14257
+ "ap",
14258
+ "vmnet",
14259
+ "vnic",
14260
+ "tap",
14261
+ "tun",
14262
+ "docker",
14263
+ "veth",
14264
+ "vboxnet",
14265
+ "gif",
14266
+ "stf"
14267
+ ];
14268
+ var physicalInterfacePrefixes = ["en", "eth", "wlan", "wlp", "enp", "eno", "wlo"];
14269
+ var isPrivateLanIpv4 = (address) => {
14270
+ if (address.startsWith("10.") || address.startsWith("192.168."))
14271
+ return true;
14272
+ const secondOctet = Number(address.match(/^172\.(\d+)\./)?.[1]);
14273
+ return Number.isFinite(secondOctet) && secondOctet >= 16 && secondOctet <= 31;
14274
+ };
14275
+ var scoreDevHostCandidate = (name, address) => {
14276
+ const lowerName = name.toLowerCase();
14277
+ let score = 0;
14278
+ if (address.startsWith("169.254."))
14279
+ score -= 1000;
14280
+ if (virtualInterfacePrefixes.some((prefix) => lowerName.startsWith(prefix)))
14281
+ score -= 100;
14282
+ else if (physicalInterfacePrefixes.some((prefix) => lowerName.startsWith(prefix)))
14283
+ score += 100;
14284
+ if (isPrivateLanIpv4(address))
14285
+ score += 10;
14286
+ return score;
14287
+ };
14288
+ var selectLocalDevHost = (interfaces, { override } = {}) => {
14289
+ const candidates = [];
14290
+ for (const [name, aliases] of Object.entries(interfaces)) {
14291
+ for (const alias of aliases ?? []) {
14292
+ if (alias.family !== "IPv4" || alias.internal)
14293
+ continue;
14294
+ candidates.push({ name, address: alias.address });
14127
14295
  }
14128
14296
  }
14129
- return "127.0.0.1";
14297
+ const trimmedOverride = override?.trim();
14298
+ if (trimmedOverride)
14299
+ return { host: trimmedOverride, source: "override", candidates };
14300
+ const [best] = [...candidates].sort((a, b) => scoreDevHostCandidate(b.name, b.address) - scoreDevHostCandidate(a.name, a.address) || a.name.localeCompare(b.name) || a.address.localeCompare(b.address));
14301
+ return best ? { host: best.address, source: "detected", candidates } : { host: "127.0.0.1", source: "loopback", candidates };
14130
14302
  };
14131
14303
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14132
14304
  var asString = (value) => typeof value === "string" ? value : undefined;
@@ -14146,6 +14318,30 @@ var dedupeIosRunTargets = (targets) => {
14146
14318
  }
14147
14319
  return [...byKey.values()];
14148
14320
  };
14321
+ var formatSimctlRuntime = (key) => {
14322
+ if (!key)
14323
+ return;
14324
+ if (/^(iOS|watchOS|tvOS|visionOS)\s+[\d.]+$/i.test(key))
14325
+ return key;
14326
+ const match = key.match(/(iOS|watchOS|tvOS|visionOS)-(\d+)(?:-(\d+))?/i);
14327
+ if (!match)
14328
+ return;
14329
+ return `${match[1]} ${match[2]}${match[3] ? `.${match[3]}` : ""}`;
14330
+ };
14331
+ var parseIosRuntimeMajor = (runtime) => {
14332
+ const major = runtime?.match(/(\d+)/)?.[1];
14333
+ if (major === undefined)
14334
+ return;
14335
+ const parsed = Number.parseInt(major, 10);
14336
+ return Number.isNaN(parsed) ? undefined : parsed;
14337
+ };
14338
+ var iosRunTargetRank = (target) => {
14339
+ const state = target.state?.toLowerCase() ?? "";
14340
+ const ready = state.includes("booted") || state.includes("connected") || state.includes("available") ? 1000 : 0;
14341
+ const device = target.kind === "device" ? 500 : 0;
14342
+ return ready + device + (parseIosRuntimeMajor(target.runtime) ?? 0);
14343
+ };
14344
+ var sortIosRunTargets = (targets) => [...targets].sort((a, b) => iosRunTargetRank(b) - iosRunTargetRank(a) || a.name.localeCompare(b.name));
14149
14345
  function walkRecords(value, visit) {
14150
14346
  if (Array.isArray(value)) {
14151
14347
  for (const item of value)
@@ -14198,21 +14394,38 @@ function parseSimctlDevices(output) {
14198
14394
  try {
14199
14395
  const json = JSON.parse(output);
14200
14396
  const devices = json.devices ?? {};
14201
- return Object.values(devices).flat().filter(isRecord2).flatMap((device) => {
14202
- const id = firstString(device.udid, device.UDID, device.identifier);
14203
- const name = firstString(device.name, device.displayName);
14204
- const isAvailable = device.isAvailable !== false && device.availabilityError === undefined;
14205
- if (!id || !name || !isAvailable)
14206
- return [];
14207
- return [{ id, name, kind: "simulator", state: asString(device.state) }];
14397
+ return Object.entries(devices).flatMap(([runtimeKey, list]) => {
14398
+ const runtime = formatSimctlRuntime(runtimeKey);
14399
+ return (Array.isArray(list) ? list : []).filter(isRecord2).flatMap((device) => {
14400
+ const id = firstString(device.udid, device.UDID, device.identifier);
14401
+ const name = firstString(device.name, device.displayName);
14402
+ const isAvailable = device.isAvailable !== false && device.availabilityError === undefined;
14403
+ if (!id || !name || !isAvailable)
14404
+ return [];
14405
+ return [
14406
+ {
14407
+ id,
14408
+ name,
14409
+ kind: "simulator",
14410
+ state: asString(device.state),
14411
+ runtime: runtime ?? formatSimctlRuntime(asString(device.runtimeIdentifier))
14412
+ }
14413
+ ];
14414
+ });
14208
14415
  });
14209
14416
  } catch {
14210
14417
  const targets = [];
14418
+ let runtime;
14211
14419
  for (const line of output.split(/\r?\n/)) {
14420
+ const header = line.match(/^--\s*(.+?)\s*--\s*$/);
14421
+ if (header) {
14422
+ runtime = formatSimctlRuntime(header[1]);
14423
+ continue;
14424
+ }
14212
14425
  const match = line.match(/^\s*(.+?)\s+\(([0-9A-Fa-f-]{20,})\)\s+\(([^)]+)\)/);
14213
14426
  if (!match)
14214
14427
  continue;
14215
- targets.push({ id: match[2], name: match[1].trim(), kind: "simulator", state: match[3] });
14428
+ targets.push({ id: match[2], name: match[1].trim(), kind: "simulator", state: match[3], runtime });
14216
14429
  }
14217
14430
  return targets;
14218
14431
  }
@@ -14247,6 +14460,13 @@ function buildIosNativeRunCommand({
14247
14460
  }
14248
14461
  function classifyIosRunFailure(log) {
14249
14462
  const lower = log.toLowerCase();
14463
+ if (lower.includes("swiftuicore") && (lower.includes("library not loaded") || lower.includes("library missing") || lower.includes("dyld"))) {
14464
+ return {
14465
+ kind: "simulator-runtime",
14466
+ title: "App crashed loading SwiftUICore \u2014 the iOS runtime is too old.",
14467
+ detail: `The build links SwiftUICore, which only exists on iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+. Rerun on an iOS ${SWIFTUICORE_MIN_IOS_MAJOR} or newer simulator/device (pass --device to pick one non-interactively).`
14468
+ };
14469
+ }
14250
14470
  if (lower.includes("unknown argument: '-index-store-path'") || lower.includes("compiler was not recognized")) {
14251
14471
  return {
14252
14472
  kind: "compiler-toolchain",
@@ -14344,6 +14564,21 @@ function formatIosRunFailureMessage(input2) {
14344
14564
  function sanitizeIosNativeRunEnv(env) {
14345
14565
  return Object.fromEntries(Object.entries(env).filter(([key]) => !iosNativeBlockedEnvKeys.has(key)));
14346
14566
  }
14567
+ var PLACEHOLDER_APP_IDS = [
14568
+ "com.myapp.app",
14569
+ "com.myorg.myapp",
14570
+ "com.example.app",
14571
+ "com.example.myapp"
14572
+ ];
14573
+ var placeholderAppIdSegment = /^(example|examples|myorg|myapp|mycompany|myorganization|changeme|todo|sample|test)$/;
14574
+ var isPlaceholderAppId = (appId) => {
14575
+ const normalized = appId?.trim().toLowerCase() ?? "";
14576
+ if (!normalized)
14577
+ return true;
14578
+ if (PLACEHOLDER_APP_IDS.includes(normalized))
14579
+ return true;
14580
+ return normalized.split(".").some((segment) => placeholderAppIdSegment.test(segment));
14581
+ };
14347
14582
  var androidReleaseSigningKeys = [
14348
14583
  "MYAPP_RELEASE_STORE_FILE",
14349
14584
  "MYAPP_RELEASE_STORE_PASSWORD",
@@ -14592,23 +14827,37 @@ class CapacitorApp {
14592
14827
  await this.#runIosPhysicalDevice({ operation, env, runTarget, noAllowProvisioningUpdates });
14593
14828
  }
14594
14829
  async#selectIosRunTarget(deviceId) {
14595
- const targets = await this.#loadIosRunTargets();
14830
+ const targets = sortIosRunTargets(await this.#loadIosRunTargets());
14596
14831
  if (deviceId) {
14597
- const found = targets.find((target) => target.id === deviceId);
14598
- if (!found)
14599
- throw new Error(`iOS run target '${deviceId}' was not found.`);
14832
+ const needle = deviceId.toLowerCase();
14833
+ const found = targets.find((target) => target.id === deviceId) ?? targets.find((target) => target.name.toLowerCase() === needle) ?? targets.find((target) => target.name.toLowerCase().includes(needle) || target.id.toLowerCase().includes(needle) || (target.runtime?.toLowerCase().includes(needle) ?? false));
14834
+ if (!found) {
14835
+ const available = targets.map((t) => `${t.name}${t.runtime ? ` (${t.runtime})` : ""}`).join(", ") || "none";
14836
+ throw new Error(`iOS run target '${deviceId}' was not found. Available: ${available}`);
14837
+ }
14838
+ this.#warnIfLegacySimulatorRuntime(found);
14600
14839
  return found;
14601
14840
  }
14602
14841
  if (targets.length === 0) {
14603
14842
  throw new Error("No iOS run targets found. Open Simulator or connect an iPhone, then retry.");
14604
14843
  }
14605
- return await select2({
14844
+ const selected = await select2({
14606
14845
  message: "Select iOS run target",
14607
14846
  choices: targets.map((target) => ({
14608
- name: `[${target.kind}] ${target.name}${target.state ? ` (${target.state})` : ""}`,
14847
+ name: `[${target.kind}] ${target.name}${target.runtime ? ` \u2014 ${target.runtime}` : ""}${target.state ? ` (${target.state})` : ""}`,
14609
14848
  value: target
14610
14849
  }))
14611
14850
  });
14851
+ this.#warnIfLegacySimulatorRuntime(selected);
14852
+ return selected;
14853
+ }
14854
+ #warnIfLegacySimulatorRuntime(target) {
14855
+ if (target.kind !== "simulator")
14856
+ return;
14857
+ const major = parseIosRuntimeMajor(target.runtime);
14858
+ if (major === undefined || major >= SWIFTUICORE_MIN_IOS_MAJOR)
14859
+ return;
14860
+ this.app.logger.warn(`Selected simulator runs ${target.runtime ?? "an older iOS"}. Recent SDK builds link SwiftUICore and require iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+; if the app crashes at launch with a "Library not loaded: SwiftUICore" dyld error, pick an iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+ simulator instead.`);
14612
14861
  }
14613
14862
  async#loadIosRunTargets() {
14614
14863
  const devices = await this.#loadPhysicalIosDevices();
@@ -14855,7 +15104,13 @@ ${error.message}`;
14855
15104
  }
14856
15105
  async#writeCapacitorConfig({ operation }, commandEnv) {
14857
15106
  await mkdir11(this.targetRoot, { recursive: true });
14858
- const localIp = operation === "local" ? getLocalIP() : undefined;
15107
+ let localIp;
15108
+ if (operation === "local") {
15109
+ const override = commandEnv.AKAN_PUBLIC_CLIENT_HOST ?? process.env.AKAN_PUBLIC_CLIENT_HOST;
15110
+ const resolution = selectLocalDevHost(os.networkInterfaces(), { override });
15111
+ localIp = resolution.host;
15112
+ this.#logDevHostResolution(resolution, commandEnv);
15113
+ }
14859
15114
  const config = materializeCapacitorConfig(this.target, {
14860
15115
  operation,
14861
15116
  localIp,
@@ -14866,6 +15121,17 @@ ${error.message}`;
14866
15121
  await Bun.write(path42.join(this.targetRoot, "capacitor.config.json"), content);
14867
15122
  return content;
14868
15123
  }
15124
+ #logDevHostResolution(resolution, commandEnv) {
15125
+ this.app.log(`Mobile live-reload server: ${this.#localCsrUrl(resolution.host, commandEnv)}`);
15126
+ if (resolution.source === "override")
15127
+ return;
15128
+ const suspicious = resolution.host === "127.0.0.1" || resolution.host.startsWith("169.254.");
15129
+ const alternatives = resolution.candidates.filter((candidate) => candidate.address !== resolution.host);
15130
+ if (!suspicious && alternatives.length === 0)
15131
+ return;
15132
+ const alternativeText = alternatives.length ? ` Other interfaces: ${alternatives.map((candidate) => `${candidate.address} (${candidate.name})`).join(", ")}.` : "";
15133
+ this.app.logger.warn(`A physical device must reach ${resolution.host} on your LAN.${suspicious ? " That address looks non-routable." : ""}${alternativeText} If the device shows a blank screen, pin the right one with AKAN_PUBLIC_CLIENT_HOST=<ip>.`);
15134
+ }
14869
15135
  async#prepareTargetAssets() {
14870
15136
  if (!this.target.assets)
14871
15137
  return;
@@ -17114,10 +17380,14 @@ class IncrementalBuilder {
17114
17380
  this.#cssCompiler = options.cssCompiler;
17115
17381
  this.#optimizedFonts = options.optimizedFonts;
17116
17382
  this.#discovery = options.discovery;
17383
+ this.#generation = options.initialGeneration ?? 0;
17117
17384
  this.#changePlanner = new DevChangePlanner({ workspaceRoot: options.app.workspace.workspaceRoot });
17118
17385
  this.#generatedIndexSync = new DevGeneratedIndexSync({ workspaceRoot: options.app.workspace.workspaceRoot });
17119
17386
  this.#autoImportSync = new AutoImportSync({ workspaceRoot: options.app.workspace.workspaceRoot });
17120
17387
  }
17388
+ get #artifactDir() {
17389
+ return `${this.#app.cwdPath}/.akan/artifact`;
17390
+ }
17121
17391
  async handleBuildRoute(msg) {
17122
17392
  return this.#enqueueWork(`build-route:${msg.routeId}`, async () => this.#handleBuildRoute(msg));
17123
17393
  }
@@ -17306,7 +17576,7 @@ ${cssText}`).toString(36);
17306
17576
  });
17307
17577
  }
17308
17578
  async installWatcher() {
17309
- const [appDir, artifactDir] = [`${this.#app.cwdPath}/page`, `${this.#app.cwdPath}/.akan/artifact`];
17579
+ const [appDir, artifactDir] = [`${this.#app.cwdPath}/page`, this.#artifactDir];
17310
17580
  const roots = await new WatchRootResolver(this.#app).resolve();
17311
17581
  const watcher = new HmrWatcher({
17312
17582
  roots,
@@ -17403,45 +17673,122 @@ ${cssText}`).toString(36);
17403
17673
  }
17404
17674
  }
17405
17675
  async boot() {
17406
- process.on("message", async (msg) => {
17407
- if (!msg || typeof msg !== "object")
17408
- return;
17409
- switch (msg.type) {
17410
- case "build-route": {
17411
- const res = await this.handleBuildRoute(msg);
17412
- process.send?.(res);
17413
- return;
17414
- }
17415
- default:
17416
- return;
17417
- }
17418
- });
17419
- process.on("disconnect", () => {
17420
- this.#logger.warn("host IPC channel closed; exiting builder");
17421
- process.exit(0);
17422
- });
17423
17676
  if (this.#watch)
17424
17677
  await this.installWatcher();
17425
17678
  process.send?.({ type: "builder-ready" });
17426
17679
  this.#logger.verbose(`ready (watch=${this.#watch})`);
17427
17680
  }
17681
+ async announceRecoveredState(changedFiles) {
17682
+ const generation = ++this.#generation;
17683
+ await this.#enqueueWork("boot-recovered", async () => {
17684
+ try {
17685
+ const next = await new PagesBundleBuilder(this.#app).build();
17686
+ process.send?.({
17687
+ type: "pages-updated",
17688
+ data: { bundlePath: next.bundlePath, buildId: next.buildId, generation, changedFiles }
17689
+ });
17690
+ this.#sendBuildStatus("pages", { generation, ok: true, files: changedFiles, message: "Boot build recovered" });
17691
+ } catch (err) {
17692
+ const message = err instanceof Error ? err.message : String(err);
17693
+ this.#logger.error(`recovered pages rebundle failed: ${message}`);
17694
+ this.#sendBuildStatus("pages", { generation, ok: false, files: changedFiles, message });
17695
+ }
17696
+ this.scheduleCssRebuild(this.#artifactDir, { refresh: true, generation, changedFiles });
17697
+ });
17698
+ }
17428
17699
  #shouldRebuildCsr() {
17429
17700
  return true;
17430
17701
  }
17431
- static async create() {
17702
+ static async#buildBootDeps(app) {
17703
+ const { artifact: artifact2, cssCompiler: cssCompiler2, optimizedFonts } = await new SsrBaseArtifactBuilder(app).build();
17704
+ await new CsrArtifactBuilder(app).build();
17705
+ const discovery = await GraphClientEntryDiscovery.create(app);
17706
+ return { artifact: artifact2, cssCompiler: cssCompiler2, optimizedFonts, discovery };
17707
+ }
17708
+ static #recoverBoot(app, bootError, logger) {
17709
+ const firstMessage = bootError instanceof Error ? bootError.message : String(bootError);
17710
+ logger.error(`boot build failed; entering degraded watch mode until the error is fixed: ${firstMessage}`);
17711
+ let generation = 0;
17712
+ const sendFailure = (files, message) => {
17713
+ process.send?.({
17714
+ type: "build-status",
17715
+ data: { generation, phase: "pages", ok: false, files, message: `Boot build failed: ${message}` }
17716
+ });
17717
+ };
17718
+ sendFailure([], firstMessage);
17719
+ process.send?.({ type: "builder-ready" });
17720
+ return new Promise((resolve, reject) => {
17721
+ (async () => {
17722
+ const roots = await new WatchRootResolver(app).resolve();
17723
+ const watcher = new HmrWatcher({
17724
+ roots,
17725
+ logger,
17726
+ onBatch: async (batch) => {
17727
+ generation += 1;
17728
+ const files = [...batch.files].sort();
17729
+ try {
17730
+ if (new Set(batch.kinds).has("config"))
17731
+ await app.getConfig({ refresh: true });
17732
+ const deps = await IncrementalBuilder.#buildBootDeps(app);
17733
+ const builder2 = new IncrementalBuilder({ app, watch: true, initialGeneration: generation, ...deps });
17734
+ watcher.stop();
17735
+ logger.info(`boot build recovered generation=${generation}`);
17736
+ resolve({ builder: builder2, changedFiles: files });
17737
+ } catch (err) {
17738
+ const message = err instanceof Error ? err.message : String(err);
17739
+ logger.error(`boot build retry failed: ${message}`);
17740
+ sendFailure(files, message);
17741
+ }
17742
+ }
17743
+ });
17744
+ watcher.start();
17745
+ logger.warn(`[degraded] watching ${roots.length} roots for a fix`);
17746
+ })().catch(reject);
17747
+ });
17748
+ }
17749
+ static async main() {
17750
+ const logger = new Logger11("IncrementalBuilder");
17432
17751
  const { appName, repoName, workspaceRoot } = WorkspaceExecutor.getBaseDevEnv();
17433
17752
  if (!workspaceRoot || !appName)
17434
17753
  throw new Error("AKAN_WORKSPACE_ROOT or AKAN_PUBLIC_APP_NAME is not set");
17435
17754
  const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot, repoName });
17436
17755
  const app = AppExecutor.from(workspace, appName);
17437
17756
  const watch = process.env.AKAN_WATCH !== "0";
17438
- const { artifact: artifact2, cssCompiler: cssCompiler2, optimizedFonts } = await new SsrBaseArtifactBuilder(app).build();
17439
- await new CsrArtifactBuilder(app).build();
17440
- const discovery = await GraphClientEntryDiscovery.create(app);
17441
- return new IncrementalBuilder({ app, cssCompiler: cssCompiler2, artifact: artifact2, watch, optimizedFonts, discovery });
17757
+ let builder2 = null;
17758
+ process.on("message", (msg) => {
17759
+ if (!msg || typeof msg !== "object" || msg.type !== "build-route")
17760
+ return;
17761
+ if (!builder2) {
17762
+ process.send?.({
17763
+ type: "build-route-res",
17764
+ id: msg.id,
17765
+ ok: false,
17766
+ error: "builder is recovering from a failed boot build; retry after the build error is fixed"
17767
+ });
17768
+ return;
17769
+ }
17770
+ builder2.handleBuildRoute(msg).then((res) => process.send?.(res));
17771
+ });
17772
+ process.on("disconnect", () => {
17773
+ logger.warn("host IPC channel closed; exiting builder");
17774
+ process.exit(0);
17775
+ });
17776
+ let recoveredFiles = null;
17777
+ try {
17778
+ builder2 = new IncrementalBuilder({ app, watch, ...await IncrementalBuilder.#buildBootDeps(app) });
17779
+ } catch (err) {
17780
+ if (!watch)
17781
+ throw err;
17782
+ const recovered = await IncrementalBuilder.#recoverBoot(app, err, logger);
17783
+ builder2 = recovered.builder;
17784
+ recoveredFiles = recovered.changedFiles;
17785
+ }
17786
+ await builder2.boot();
17787
+ if (recoveredFiles)
17788
+ await builder2.announceRecoveredState(recoveredFiles);
17442
17789
  }
17443
17790
  }
17444
- (await IncrementalBuilder.create()).boot().catch((err) => {
17791
+ IncrementalBuilder.main().catch((err) => {
17445
17792
  console.error(err);
17446
17793
  process.exit(1);
17447
17794
  });