@akanjs/cli 2.4.0-rc.8 → 2.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.
@@ -778,6 +778,13 @@ var normalizeStringList = (values) => {
778
778
  const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
779
779
  return normalized.length > 0 ? [...new Set(normalized)] : undefined;
780
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)}`;
781
788
  var normalizeDeepLinkDomain = (domain) => {
782
789
  const normalized = domain.trim();
783
790
  if (!normalized)
@@ -818,6 +825,7 @@ class AkanAppConfig {
818
825
  i18n;
819
826
  publicEnv;
820
827
  mobile;
828
+ hasMobileConfig;
821
829
  secrets;
822
830
  baseDevEnv;
823
831
  libs;
@@ -848,6 +856,7 @@ class AkanAppConfig {
848
856
  process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
849
857
  this.publicEnv = config?.publicEnv ?? [];
850
858
  this.secrets = config?.secrets ?? [];
859
+ this.hasMobileConfig = Boolean(config.mobile);
851
860
  this.mobile = this.#resolveMobileConfig(config.mobile);
852
861
  this.docker = this.#makeDockerContent(config?.docker ?? {});
853
862
  }
@@ -858,7 +867,7 @@ class AkanAppConfig {
858
867
  ...rawMobile
859
868
  } = mobile ?? {};
860
869
  const appName = rawMobile.appName ?? this.app.name;
861
- const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
870
+ const appId = rawMobile.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
862
871
  const version = rawMobile.version ?? "0.0.1";
863
872
  const buildNum = rawMobile.buildNum ?? 1;
864
873
  const defaultTargetName = this.#defaultMobileTargetName(rawTargets);
@@ -4324,6 +4333,8 @@ var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
4324
4333
  var BACKEND_STDERR_TAIL_LIMIT = 40;
4325
4334
  var BUILDER_READY_TIMEOUT_MS = 150000;
4326
4335
  var BUILDER_START_MAX_ATTEMPTS = 3;
4336
+ var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
4337
+ var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
4327
4338
  var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
4328
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;
4329
4340
  var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
@@ -4400,6 +4411,43 @@ var mergeBackendRestartReasons = (current, next) => ({
4400
4411
  });
4401
4412
  var shouldReplaceLastGoodMessage = (current, next) => !current || generationValue(next.data.generation) >= generationValue(current.data.generation);
4402
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
+ };
4403
4451
  var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
4404
4452
 
4405
4453
  class BackendImportGraph {
@@ -4522,8 +4570,12 @@ class AkanAppHost {
4522
4570
  #restartTimer = null;
4523
4571
  #backendRecoveryTimer = null;
4524
4572
  #backendRecoveryAttempts = 0;
4573
+ #backendGaveUp = false;
4525
4574
  #backendLifecycleState = "stopped";
4526
4575
  #pendingRestartReason = null;
4576
+ #pendingRecycle = null;
4577
+ #builderRecoveryTimer = null;
4578
+ #builderRecoveryAttempts = 0;
4527
4579
  #backendStartStatus = null;
4528
4580
  #backendBuildStatusGeneration = 0;
4529
4581
  #backendStderrTail = [];
@@ -4561,6 +4613,10 @@ class AkanAppHost {
4561
4613
  clearTimeout(this.#backendRecoveryTimer);
4562
4614
  this.#backendRecoveryTimer = null;
4563
4615
  }
4616
+ if (this.#builderRecoveryTimer) {
4617
+ clearTimeout(this.#builderRecoveryTimer);
4618
+ this.#builderRecoveryTimer = null;
4619
+ }
4564
4620
  await this.#stopBackend();
4565
4621
  this.#stopBuilder();
4566
4622
  return this;
@@ -4576,6 +4632,7 @@ class AkanAppHost {
4576
4632
  }
4577
4633
  #startBackend(startStatus = null) {
4578
4634
  this.#backendStartStatus = startStatus;
4635
+ this.#backendGaveUp = false;
4579
4636
  this.#setBackendLifecycleState("starting");
4580
4637
  this.#backendReady = false;
4581
4638
  this.#backendStderrTail = [];
@@ -4767,7 +4824,8 @@ class AkanAppHost {
4767
4824
  if (this.#backendRecoveryTimer || this.#backend)
4768
4825
  return;
4769
4826
  if (shouldAbandonBackendRecovery(this.#backendRecoveryAttempts)) {
4770
- 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;
4771
4829
  this.#setBackendLifecycleState("stopped", `gave up after ${this.#backendRecoveryAttempts} recovery attempts`);
4772
4830
  this.logger.error(`[backend-recovery] ${message}`);
4773
4831
  if (this.#backendStderrTail.length > 0) {
@@ -4814,6 +4872,7 @@ ${this.#backendStderrTail.join(`
4814
4872
  if (message.type === "build-status") {
4815
4873
  this.#recordBuildStatus(message.data);
4816
4874
  this.#sendOrQueueBuildStatus(message.data);
4875
+ this.#reviveBackendAfterGreenBuild(message.data);
4817
4876
  return;
4818
4877
  }
4819
4878
  if (message.type === "pages-updated")
@@ -4828,19 +4887,25 @@ ${this.#backendStderrTail.join(`
4828
4887
  }
4829
4888
  async#handleInvalidate(message) {
4830
4889
  this.#logDevPlan(message);
4831
- if (shouldRestartDevHostByDevPlan(message)) {
4832
- try {
4833
- await this.#restartDevHost(message);
4834
- } catch (err) {
4835
- 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;
4836
4899
  }
4837
- return;
4838
- }
4839
- if (shouldRestartBuilderByDevPlan(message)) {
4900
+ this.#pendingRecycle = null;
4840
4901
  try {
4841
- await this.#restartDevChildren(message);
4902
+ if (refreshConfig)
4903
+ await this.#restartDevHost(merged);
4904
+ else
4905
+ await this.#restartDevChildren(merged);
4842
4906
  } catch (err) {
4843
- this.#recordDevHostRestartFailure(message, err, "Runtime metadata");
4907
+ this.#recordDevHostRestartFailure(merged, err, refreshConfig ? "Config" : "Runtime metadata");
4908
+ this.#resurrectDevChildren(merged);
4844
4909
  }
4845
4910
  return;
4846
4911
  }
@@ -4850,6 +4915,70 @@ ${this.#backendStderrTail.join(`
4850
4915
  }
4851
4916
  this.#sendToBackend(message);
4852
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
+ }
4853
4982
  async#restartDevChildren(message) {
4854
4983
  const generation = message.devPlan?.generation ?? message.generation;
4855
4984
  this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
@@ -4870,6 +4999,11 @@ ${this.#backendStderrTail.join(`
4870
4999
  clearTimeout(this.#backendRecoveryTimer);
4871
5000
  this.#backendRecoveryTimer = null;
4872
5001
  }
5002
+ if (this.#builderRecoveryTimer) {
5003
+ clearTimeout(this.#builderRecoveryTimer);
5004
+ this.#builderRecoveryTimer = null;
5005
+ }
5006
+ this.#builderRecoveryAttempts = 0;
4873
5007
  this.#pendingRestartReason = null;
4874
5008
  this.#lastGoodFrontend = {};
4875
5009
  this.#buildStatusByPhase.clear();
@@ -4907,7 +5041,7 @@ ${this.#backendStderrTail.join(`
4907
5041
  phase: "scan",
4908
5042
  ok: false,
4909
5043
  files: message.files,
4910
- message: `${kind} change requires restarting \`akan start\` to apply: ${detail}`
5044
+ message: `${kind} change failed to apply; recovering the dev server automatically: ${detail}`
4911
5045
  };
4912
5046
  this.#recordBuildStatus(status);
4913
5047
  this.#sendOrQueueBuildStatus(status);
@@ -14077,6 +14211,7 @@ var resolveMobilePath = (target, pathname) => {
14077
14211
  };
14078
14212
  var targetHtmlFilename = (target) => target.basePath?.replace(/^\/+|\/+$/g, "") ? `${target.basePath.replace(/^\/+|\/+$/g, "")}.html` : "index.html";
14079
14213
  // pkgs/@akanjs/devkit/capacitorApp.ts
14214
+ var SWIFTUICORE_MIN_IOS_MAJOR = 18;
14080
14215
  var iosNativeBlockedEnvKeys = new Set([
14081
14216
  "AR",
14082
14217
  "AS",
@@ -14114,17 +14249,56 @@ async function writeRootCapacitorConfig(appRoot, content) {
14114
14249
  await clearRootCapacitorConfigs(appRoot);
14115
14250
  await Bun.write(path42.join(appRoot, "capacitor.config.json"), content);
14116
14251
  }
14117
- var getLocalIP = () => {
14118
- const interfaces = os.networkInterfaces();
14119
- for (const iface of Object.values(interfaces)) {
14120
- if (!iface)
14121
- continue;
14122
- for (const alias of iface) {
14123
- if (alias.family === "IPv4" && !alias.internal)
14124
- 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 });
14125
14295
  }
14126
14296
  }
14127
- 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 };
14128
14302
  };
14129
14303
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14130
14304
  var asString = (value) => typeof value === "string" ? value : undefined;
@@ -14144,6 +14318,30 @@ var dedupeIosRunTargets = (targets) => {
14144
14318
  }
14145
14319
  return [...byKey.values()];
14146
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));
14147
14345
  function walkRecords(value, visit) {
14148
14346
  if (Array.isArray(value)) {
14149
14347
  for (const item of value)
@@ -14196,21 +14394,38 @@ function parseSimctlDevices(output) {
14196
14394
  try {
14197
14395
  const json = JSON.parse(output);
14198
14396
  const devices = json.devices ?? {};
14199
- return Object.values(devices).flat().filter(isRecord2).flatMap((device) => {
14200
- const id = firstString(device.udid, device.UDID, device.identifier);
14201
- const name = firstString(device.name, device.displayName);
14202
- const isAvailable = device.isAvailable !== false && device.availabilityError === undefined;
14203
- if (!id || !name || !isAvailable)
14204
- return [];
14205
- 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
+ });
14206
14415
  });
14207
14416
  } catch {
14208
14417
  const targets = [];
14418
+ let runtime;
14209
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
+ }
14210
14425
  const match = line.match(/^\s*(.+?)\s+\(([0-9A-Fa-f-]{20,})\)\s+\(([^)]+)\)/);
14211
14426
  if (!match)
14212
14427
  continue;
14213
- 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 });
14214
14429
  }
14215
14430
  return targets;
14216
14431
  }
@@ -14245,6 +14460,13 @@ function buildIosNativeRunCommand({
14245
14460
  }
14246
14461
  function classifyIosRunFailure(log) {
14247
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
+ }
14248
14470
  if (lower.includes("unknown argument: '-index-store-path'") || lower.includes("compiler was not recognized")) {
14249
14471
  return {
14250
14472
  kind: "compiler-toolchain",
@@ -14342,6 +14564,21 @@ function formatIosRunFailureMessage(input2) {
14342
14564
  function sanitizeIosNativeRunEnv(env) {
14343
14565
  return Object.fromEntries(Object.entries(env).filter(([key]) => !iosNativeBlockedEnvKeys.has(key)));
14344
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
+ };
14345
14582
  var androidReleaseSigningKeys = [
14346
14583
  "MYAPP_RELEASE_STORE_FILE",
14347
14584
  "MYAPP_RELEASE_STORE_PASSWORD",
@@ -14590,23 +14827,37 @@ class CapacitorApp {
14590
14827
  await this.#runIosPhysicalDevice({ operation, env, runTarget, noAllowProvisioningUpdates });
14591
14828
  }
14592
14829
  async#selectIosRunTarget(deviceId) {
14593
- const targets = await this.#loadIosRunTargets();
14830
+ const targets = sortIosRunTargets(await this.#loadIosRunTargets());
14594
14831
  if (deviceId) {
14595
- const found = targets.find((target) => target.id === deviceId);
14596
- if (!found)
14597
- 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);
14598
14839
  return found;
14599
14840
  }
14600
14841
  if (targets.length === 0) {
14601
14842
  throw new Error("No iOS run targets found. Open Simulator or connect an iPhone, then retry.");
14602
14843
  }
14603
- return await select2({
14844
+ const selected = await select2({
14604
14845
  message: "Select iOS run target",
14605
14846
  choices: targets.map((target) => ({
14606
- name: `[${target.kind}] ${target.name}${target.state ? ` (${target.state})` : ""}`,
14847
+ name: `[${target.kind}] ${target.name}${target.runtime ? ` \u2014 ${target.runtime}` : ""}${target.state ? ` (${target.state})` : ""}`,
14607
14848
  value: target
14608
14849
  }))
14609
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.`);
14610
14861
  }
14611
14862
  async#loadIosRunTargets() {
14612
14863
  const devices = await this.#loadPhysicalIosDevices();
@@ -14853,7 +15104,13 @@ ${error.message}`;
14853
15104
  }
14854
15105
  async#writeCapacitorConfig({ operation }, commandEnv) {
14855
15106
  await mkdir11(this.targetRoot, { recursive: true });
14856
- 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
+ }
14857
15114
  const config = materializeCapacitorConfig(this.target, {
14858
15115
  operation,
14859
15116
  localIp,
@@ -14864,6 +15121,17 @@ ${error.message}`;
14864
15121
  await Bun.write(path42.join(this.targetRoot, "capacitor.config.json"), content);
14865
15122
  return content;
14866
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
+ }
14867
15135
  async#prepareTargetAssets() {
14868
15136
  if (!this.target.assets)
14869
15137
  return;
@@ -17112,10 +17380,14 @@ class IncrementalBuilder {
17112
17380
  this.#cssCompiler = options.cssCompiler;
17113
17381
  this.#optimizedFonts = options.optimizedFonts;
17114
17382
  this.#discovery = options.discovery;
17383
+ this.#generation = options.initialGeneration ?? 0;
17115
17384
  this.#changePlanner = new DevChangePlanner({ workspaceRoot: options.app.workspace.workspaceRoot });
17116
17385
  this.#generatedIndexSync = new DevGeneratedIndexSync({ workspaceRoot: options.app.workspace.workspaceRoot });
17117
17386
  this.#autoImportSync = new AutoImportSync({ workspaceRoot: options.app.workspace.workspaceRoot });
17118
17387
  }
17388
+ get #artifactDir() {
17389
+ return `${this.#app.cwdPath}/.akan/artifact`;
17390
+ }
17119
17391
  async handleBuildRoute(msg) {
17120
17392
  return this.#enqueueWork(`build-route:${msg.routeId}`, async () => this.#handleBuildRoute(msg));
17121
17393
  }
@@ -17304,7 +17576,7 @@ ${cssText}`).toString(36);
17304
17576
  });
17305
17577
  }
17306
17578
  async installWatcher() {
17307
- const [appDir, artifactDir] = [`${this.#app.cwdPath}/page`, `${this.#app.cwdPath}/.akan/artifact`];
17579
+ const [appDir, artifactDir] = [`${this.#app.cwdPath}/page`, this.#artifactDir];
17308
17580
  const roots = await new WatchRootResolver(this.#app).resolve();
17309
17581
  const watcher = new HmrWatcher({
17310
17582
  roots,
@@ -17401,45 +17673,122 @@ ${cssText}`).toString(36);
17401
17673
  }
17402
17674
  }
17403
17675
  async boot() {
17404
- process.on("message", async (msg) => {
17405
- if (!msg || typeof msg !== "object")
17406
- return;
17407
- switch (msg.type) {
17408
- case "build-route": {
17409
- const res = await this.handleBuildRoute(msg);
17410
- process.send?.(res);
17411
- return;
17412
- }
17413
- default:
17414
- return;
17415
- }
17416
- });
17417
- process.on("disconnect", () => {
17418
- this.#logger.warn("host IPC channel closed; exiting builder");
17419
- process.exit(0);
17420
- });
17421
17676
  if (this.#watch)
17422
17677
  await this.installWatcher();
17423
17678
  process.send?.({ type: "builder-ready" });
17424
17679
  this.#logger.verbose(`ready (watch=${this.#watch})`);
17425
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
+ }
17426
17699
  #shouldRebuildCsr() {
17427
17700
  return true;
17428
17701
  }
17429
- 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");
17430
17751
  const { appName, repoName, workspaceRoot } = WorkspaceExecutor.getBaseDevEnv();
17431
17752
  if (!workspaceRoot || !appName)
17432
17753
  throw new Error("AKAN_WORKSPACE_ROOT or AKAN_PUBLIC_APP_NAME is not set");
17433
17754
  const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot, repoName });
17434
17755
  const app = AppExecutor.from(workspace, appName);
17435
17756
  const watch = process.env.AKAN_WATCH !== "0";
17436
- const { artifact: artifact2, cssCompiler: cssCompiler2, optimizedFonts } = await new SsrBaseArtifactBuilder(app).build();
17437
- await new CsrArtifactBuilder(app).build();
17438
- const discovery = await GraphClientEntryDiscovery.create(app);
17439
- 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);
17440
17789
  }
17441
17790
  }
17442
- (await IncrementalBuilder.create()).boot().catch((err) => {
17791
+ IncrementalBuilder.main().catch((err) => {
17443
17792
  console.error(err);
17444
17793
  process.exit(1);
17445
17794
  });