@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.
- package/incrementalBuilder.proc.js +410 -61
- package/index.js +360 -43
- package/package.json +2 -2
- package/templates/app/page/_layout.tsx +19 -1
- package/templates/app/public/fonts/Pretendard-Black.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Bold.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-ExtraBold.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-ExtraLight.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Light.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Medium.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Regular.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-SemiBold.woff2 +0 -0
- package/templates/app/public/fonts/Pretendard-Thin.woff2 +0 -0
|
@@ -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 ??
|
|
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
|
|
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
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
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
|
-
|
|
4838
|
-
}
|
|
4839
|
-
if (shouldRestartBuilderByDevPlan(message)) {
|
|
4900
|
+
this.#pendingRecycle = null;
|
|
4840
4901
|
try {
|
|
4841
|
-
|
|
4902
|
+
if (refreshConfig)
|
|
4903
|
+
await this.#restartDevHost(merged);
|
|
4904
|
+
else
|
|
4905
|
+
await this.#restartDevChildren(merged);
|
|
4842
4906
|
} catch (err) {
|
|
4843
|
-
this.#recordDevHostRestartFailure(
|
|
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
|
|
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
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
|
|
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
|
-
|
|
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.
|
|
14200
|
-
const
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
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
|
|
14596
|
-
|
|
14597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`,
|
|
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
|
|
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
|
-
|
|
17437
|
-
|
|
17438
|
-
|
|
17439
|
-
|
|
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
|
-
|
|
17791
|
+
IncrementalBuilder.main().catch((err) => {
|
|
17443
17792
|
console.error(err);
|
|
17444
17793
|
process.exit(1);
|
|
17445
17794
|
});
|