@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20

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.
Files changed (233) hide show
  1. package/abstractDoc.test.ts +6 -15
  2. package/abstractDoc.ts +1 -4
  3. package/agentsIndex.test.ts +4 -7
  4. package/agentsIndex.ts +6 -22
  5. package/akanApp/BackendImportGraph.test.ts +8 -22
  6. package/akanApp/BackendImportGraph.ts +1 -20
  7. package/akanApp/akanApp.host.test.ts +1 -3
  8. package/akanApp/akanApp.host.ts +64 -250
  9. package/akanApp/devHostPolicy.test.ts +10 -34
  10. package/akanApp/devHostPolicy.ts +12 -98
  11. package/akanConfig/akanConfig.test.ts +30 -39
  12. package/akanConfig/akanConfig.ts +40 -136
  13. package/akanContext.ts +106 -159
  14. package/akanMcpContract.ts +28 -73
  15. package/appSelectionMemory.ts +1 -4
  16. package/applicationBuildReporter.ts +37 -5
  17. package/applicationBuildRunner.ts +25 -59
  18. package/applicationReleasePackager.ts +24 -44
  19. package/applicationTestPreload.ts +3 -12
  20. package/artifact/implicitRootLayout.test.ts +3 -19
  21. package/artifact/implicitRootLayout.ts +34 -91
  22. package/artifact/routeSeedIndex.ts +11 -44
  23. package/biomeBase.ts +2 -5
  24. package/capacitorApp.test.ts +4 -14
  25. package/capacitorApp.ts +86 -235
  26. package/cloud/cloudApi.test.ts +202 -0
  27. package/cloud/cloudApi.ts +33 -57
  28. package/cloud/configLock.test.ts +64 -0
  29. package/cloud/configLock.ts +96 -0
  30. package/cloud/constants.ts +3 -2
  31. package/cloud/globalConfig.ts +64 -31
  32. package/codeAgent/agent/AkanCodeServices.ts +15 -57
  33. package/codeAgent/agent/AkanContextFiles.ts +5 -28
  34. package/codeAgent/agent/CodeAgent.ts +33 -108
  35. package/codeAgent/agent/CodeAgentAsks.ts +7 -30
  36. package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
  37. package/codeAgent/agent/CodeAgentGate.ts +8 -34
  38. package/codeAgent/agent/CodeAgentProxy.ts +4 -13
  39. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
  40. package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
  41. package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
  42. package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
  43. package/codeAgent/agent/CodeAgentUi.ts +2 -7
  44. package/codeAgent/agent/CodeMailbox.ts +7 -34
  45. package/codeAgent/agent/CodeSessionFork.ts +4 -19
  46. package/codeAgent/agent/CodeSessionIndex.ts +5 -31
  47. package/codeAgent/agent/SubagentPool.ts +48 -58
  48. package/codeAgent/agent/akanCodeModel.ts +5 -39
  49. package/codeAgent/agent/akanCodePaths.ts +4 -24
  50. package/codeAgent/agent/akanContextFiles.test.ts +0 -2
  51. package/codeAgent/agent/akanSystemPrompt.ts +1 -9
  52. package/codeAgent/agent/codeAgent.test.ts +178 -0
  53. package/codeAgent/agent/codeAgentContract.test.ts +5 -28
  54. package/codeAgent/agent/codeMailbox.test.ts +0 -4
  55. package/codeAgent/agent/codeSessionFork.test.ts +0 -2
  56. package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
  57. package/codeAgent/agent/consoleToStderr.ts +2 -12
  58. package/codeAgent/agent/jsonLines.ts +29 -0
  59. package/codeAgent/agent/stringArg.ts +5 -0
  60. package/codeAgent/feedback/DevLogFeedback.ts +3 -18
  61. package/codeAgent/feedback/PreviewChrome.ts +5 -29
  62. package/codeAgent/feedback/PreviewFeedback.ts +2 -9
  63. package/codeAgent/feedback/PreviewView.ts +3 -15
  64. package/codeAgent/feedback/TurnFeedback.ts +2 -17
  65. package/codeAgent/feedback/VerifyFeedback.ts +1 -7
  66. package/codeAgent/feedback/previewChrome.test.ts +1 -1
  67. package/codeAgent/index.ts +2 -10
  68. package/codeAgent/skills/akan-code/SKILL.md +12 -4
  69. package/codeAgent/tools/AkanCli.ts +4 -16
  70. package/codeAgent/tools/AkanCodePlugins.ts +21 -43
  71. package/codeAgent/tools/AkanEditScope.ts +2 -12
  72. package/codeAgent/tools/AkanEnvKeys.ts +3 -11
  73. package/codeAgent/tools/AkanToolPack.ts +6 -28
  74. package/codeAgent/tools/AkanVerifier.ts +1 -8
  75. package/codeAgent/tools/AskToolPack.ts +3 -15
  76. package/codeAgent/tools/MailToolPack.ts +0 -9
  77. package/codeAgent/tools/McpClient.ts +3 -24
  78. package/codeAgent/tools/McpOAuth.ts +6 -41
  79. package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
  80. package/codeAgent/tools/McpServerConfig.ts +7 -26
  81. package/codeAgent/tools/McpSignIn.ts +46 -49
  82. package/codeAgent/tools/McpTokenStore.ts +6 -17
  83. package/codeAgent/tools/McpToolPack.ts +107 -35
  84. package/codeAgent/tools/SessionToolPack.ts +1 -11
  85. package/codeAgent/tools/WebToolPack.ts +1 -7
  86. package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
  87. package/codeAgent/tools/askToolPack.test.ts +0 -3
  88. package/codeAgent/tools/mcpOAuth.test.ts +315 -27
  89. package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
  90. package/codegenLock.test.ts +4 -13
  91. package/codegenLock.ts +3 -17
  92. package/commandDecorators/argMeta.ts +1 -5
  93. package/commandDecorators/camelToKebabCase.ts +1 -0
  94. package/commandDecorators/command.ts +44 -102
  95. package/commandDecorators/commandBuilder.ts +30 -61
  96. package/commandDecorators/commandDecorators.test.ts +1 -9
  97. package/commandDecorators/dependencyBuilder.ts +10 -1
  98. package/commandDecorators/helpFormatter.ts +66 -144
  99. package/commandDecorators/index.ts +0 -2
  100. package/commandDecorators/targetMeta.ts +9 -7
  101. package/createTunnel.ts +7 -14
  102. package/dependencyScanner.test.ts +4 -16
  103. package/dependencyScanner.ts +27 -78
  104. package/devkitUtils.test.ts +47 -19
  105. package/executors.test.ts +114 -60
  106. package/executors.ts +165 -484
  107. package/fileEditor.ts +9 -37
  108. package/{getDirname.test.ts → fileSys.test.ts} +1 -1
  109. package/fileSys.ts +6 -8
  110. package/formSetterScanner.ts +3 -16
  111. package/frontendBuild/allRoutesBuilder.ts +7 -21
  112. package/frontendBuild/autoImportSync.test.ts +14 -38
  113. package/frontendBuild/autoImportSync.ts +15 -67
  114. package/frontendBuild/buildRouteClient.test.ts +10 -36
  115. package/frontendBuild/bundleDefine.ts +12 -0
  116. package/frontendBuild/clientBuildTypes.ts +5 -36
  117. package/frontendBuild/clientEntriesBundler.ts +9 -40
  118. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  119. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  120. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  121. package/frontendBuild/cssCandidateCache.ts +6 -41
  122. package/frontendBuild/cssCompiler.ts +29 -78
  123. package/frontendBuild/cssImportResolver.ts +5 -18
  124. package/frontendBuild/devChangePlanner.ts +7 -10
  125. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  126. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  127. package/frontendBuild/fontOptimizer.test.ts +5 -13
  128. package/frontendBuild/fontOptimizer.ts +15 -72
  129. package/frontendBuild/fontPruner.test.ts +5 -10
  130. package/frontendBuild/fontPruner.ts +5 -20
  131. package/frontendBuild/frontendBuild.test.ts +27 -52
  132. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  133. package/frontendBuild/hmrWatcher.test.ts +7 -31
  134. package/frontendBuild/hmrWatcher.ts +14 -71
  135. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  136. package/frontendBuild/precompressArtifacts.ts +5 -18
  137. package/frontendBuild/routeClientBuilder.ts +7 -47
  138. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  139. package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
  140. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  141. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  142. package/frontendBuild/styleContract.ts +1 -8
  143. package/frontendBuild/themeValidator.ts +4 -26
  144. package/frontendBuild/watchRootResolver.test.ts +23 -42
  145. package/frontendBuild/watchRootResolver.ts +5 -27
  146. package/getRelatedCnsts.ts +12 -92
  147. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  148. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  149. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  150. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  151. package/incrementalBuilder/builderChannel.test.ts +24 -49
  152. package/incrementalBuilder/builderChannel.ts +6 -37
  153. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  154. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  155. package/incrementalBuilder/devWatchBatch.test.ts +14 -18
  156. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  157. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  158. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  159. package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
  160. package/index.ts +3 -15
  161. package/integration/devResourceProbe.ts +50 -56
  162. package/integration/devStability.integration.test.ts +47 -194
  163. package/integration/devStabilityHarness.test.ts +9 -29
  164. package/integration/devStabilityHarness.ts +65 -330
  165. package/integration/ssrMemoryProbe.ts +41 -167
  166. package/libSource.test.ts +4 -15
  167. package/libSource.ts +19 -31
  168. package/lint/gritRules.test.ts +6 -23
  169. package/linter.test.ts +1 -2
  170. package/linter.ts +22 -115
  171. package/mobile/mobileTarget.ts +14 -13
  172. package/package.json +2 -2
  173. package/packageExports.test.ts +3 -18
  174. package/packageExportsMap.ts +3 -19
  175. package/piDependencyPins.test.ts +3 -14
  176. package/platformTest/IdleWatchedProcess.test.ts +5 -13
  177. package/platformTest/IdleWatchedProcess.ts +1 -5
  178. package/platformTest/LinuxDockerTarget.ts +20 -35
  179. package/platformTest/PlatformTestRun.test.ts +5 -9
  180. package/platformTest/PlatformTestTarget.ts +24 -2
  181. package/platformTest/TestSnapshot.test.ts +6 -14
  182. package/platformTest/TestSnapshot.ts +1 -5
  183. package/platformTest/WindowsSshTarget.ts +6 -22
  184. package/prompter.ts +3 -6
  185. package/qualityScanner.test.ts +6 -24
  186. package/qualityScanner.ts +196 -284
  187. package/recipeScanner.test.ts +7 -12
  188. package/recipeScanner.ts +9 -38
  189. package/repoIdentity.ts +2 -9
  190. package/routeSourceValidator.ts +7 -30
  191. package/scanInfo.ts +82 -214
  192. package/selectModel.ts +1 -2
  193. package/semver.ts +3 -5
  194. package/slicePlanner.test.ts +6 -24
  195. package/slicePlanner.ts +11 -32
  196. package/spinner.test.ts +3 -8
  197. package/spinner.ts +4 -16
  198. package/sshTunnel.ts +1 -6
  199. package/ssrScanner.test.ts +1 -6
  200. package/ssrScanner.ts +103 -113
  201. package/subspace.test.ts +6 -25
  202. package/subspace.ts +31 -109
  203. package/subspaceConfig.ts +3 -11
  204. package/testHelpers.ts +49 -34
  205. package/transforms/asyncDefaultExportDetector.ts +3 -11
  206. package/transforms/barrelAnalyzer.ts +43 -135
  207. package/transforms/barrelImportsPlugin.ts +38 -175
  208. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  209. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  210. package/transforms/moduleSyntax.ts +32 -0
  211. package/transforms/rscUseClientTransform.ts +35 -20
  212. package/transforms/transforms.test.ts +153 -44
  213. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  214. package/transforms/useClientBundlePlugin.ts +2 -22
  215. package/typeChecker.ts +7 -63
  216. package/uploadRelease.ts +10 -25
  217. package/workflow/artifacts.ts +18 -0
  218. package/workflow/executor.test.ts +1 -12
  219. package/workflow/executor.ts +85 -146
  220. package/workflow/index.ts +0 -1
  221. package/workflow/moduleIndex.test.ts +4 -15
  222. package/workflow/moduleIndex.ts +9 -49
  223. package/workflow/plan.ts +18 -25
  224. package/workflow/render.ts +80 -96
  225. package/workflow/source.ts +33 -56
  226. package/workflow/sourceAst.ts +50 -0
  227. package/workspaceLayout.ts +4 -19
  228. package/commandDecorators/commandMeta.ts +0 -7
  229. package/commandDecorators/types.ts +0 -10
  230. package/getDirname.ts +0 -4
  231. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  232. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  233. package/workflow/primitive.ts +0 -59
package/capacitorApp.ts CHANGED
@@ -7,7 +7,7 @@ import { MobileProject } from "@trapezedev/project";
7
7
  import type { AndroidProject } from "@trapezedev/project/dist/android/project";
8
8
  import type { IosProject } from "@trapezedev/project/dist/ios/project";
9
9
  import type { AkanNativeContext, AkanPlugin } from "akanjs";
10
- import { capitalize } from "akanjs/common";
10
+ import { capitalize, isRecord } from "akanjs/common";
11
11
  import type { AkanMobileTargetConfig, MobilePermission } from "./akanConfig";
12
12
  import { type AppExecutor, CommandExecutionError } from "./executors";
13
13
  import { FileEditor } from "./fileEditor";
@@ -63,8 +63,7 @@ export type IosRunFailureKind =
63
63
  | "simulator-runtime"
64
64
  | "unknown";
65
65
 
66
- // Recent iOS SDKs (iOS 18+) split SwiftUI into a SwiftUICore dylib that does not exist on older
67
- // runtimes, so an app built against them dyld-crashes at launch on an iOS <18 simulator/device.
66
+ // iOS 18+ SDKs split SwiftUI into a SwiftUICore dylib older runtimes lack, so such a build dyld-crashes below iOS 18.
68
67
  export const SWIFTUICORE_MIN_IOS_MAJOR = 18;
69
68
 
70
69
  export interface IosRunFailureClassification {
@@ -73,30 +72,11 @@ export interface IosRunFailureClassification {
73
72
  detail: string;
74
73
  }
75
74
 
76
- const iosNativeBlockedEnvKeys = new Set([
77
- "AR",
78
- "AS",
79
- "CC",
80
- "CFLAGS",
81
- "CONDA_BUILD_SYSROOT",
82
- "CONDA_PREFIX",
83
- "CPP",
84
- "CPPFLAGS",
85
- "CPATH",
86
- "CXX",
87
- "CXXFLAGS",
88
- "LD",
89
- "LDFLAGS",
90
- "LIBRARY_PATH",
91
- "MACOSX_DEPLOYMENT_TARGET",
92
- "NM",
93
- "OBJC",
94
- "OBJCXX",
95
- "PREFIX",
96
- "RANLIB",
97
- "SDKROOT",
98
- "STRIP",
99
- ]);
75
+ const iosNativeBlockedEnvKeys = new Set(
76
+ "AR AS CC CFLAGS CONDA_BUILD_SYSROOT CONDA_PREFIX CPP CPPFLAGS CPATH CXX CXXFLAGS LD LDFLAGS LIBRARY_PATH"
77
+ .split(" ")
78
+ .concat("MACOSX_DEPLOYMENT_TARGET NM OBJC OBJCXX PREFIX RANLIB SDKROOT STRIP".split(" ")),
79
+ );
100
80
 
101
81
  export const rootCapacitorConfigFilenames = [
102
82
  "capacitor.config.ts",
@@ -123,11 +103,6 @@ interface MaterializeCapacitorConfigOptions {
123
103
  }
124
104
  type MobilePlatform = "ios" | "android";
125
105
 
126
- /**
127
- * `AppExecutor.spawn`'s own options plus the two the Capacitor config is written from before the spawn:
128
- * the platform the command targets, and — for iOS — whether it targets a device or a simulator, which
129
- * decide the `capacitor.config.json` that command reads.
130
- */
131
106
  type SpawnMobileOptions = Parameters<AppExecutor["spawn"]>[2] & {
132
107
  platform?: MobilePlatform;
133
108
  iosRunTargetKind?: IosRunTargetKind;
@@ -139,24 +114,8 @@ export interface LocalDevHostResolution {
139
114
  candidates: { name: string; address: string }[];
140
115
  }
141
116
 
142
- // Interface-name prefixes that are almost never the routable LAN NIC a physical device can reach:
143
- // bridges (Thunderbolt/USB), tunnels, AirDrop/awrl, VM/container virtual adapters.
144
- const virtualInterfacePrefixes = [
145
- "bridge",
146
- "utun",
147
- "llw",
148
- "awdl",
149
- "ap",
150
- "vmnet",
151
- "vnic",
152
- "tap",
153
- "tun",
154
- "docker",
155
- "veth",
156
- "vboxnet",
157
- "gif",
158
- "stf",
159
- ];
117
+ // Almost never a LAN NIC a device can reach: bridges, tunnels, AirDrop/awdl, VM and container adapters.
118
+ const virtualInterfacePrefixes = "bridge utun llw awdl ap vmnet vnic tap tun docker veth vboxnet gif stf".split(" ");
160
119
  const physicalInterfacePrefixes = ["en", "eth", "wlan", "wlp", "enp", "eno", "wlo"];
161
120
 
162
121
  const isPrivateLanIpv4 = (address: string): boolean => {
@@ -165,8 +124,7 @@ const isPrivateLanIpv4 = (address: string): boolean => {
165
124
  return Number.isFinite(secondOctet) && secondOctet >= 16 && secondOctet <= 31;
166
125
  };
167
126
 
168
- // Higher = more likely to be the reachable LAN address. Link-local (169.254) is never routable;
169
- // virtual/bridge interfaces are demoted below real NICs; private-LAN ranges get a small boost.
127
+ // Link-local (169.254) is never routable.
170
128
  const scoreDevHostCandidate = (name: string, address: string): number => {
171
129
  const lowerName = name.toLowerCase();
172
130
  let score = 0;
@@ -177,9 +135,7 @@ const scoreDevHostCandidate = (name: string, address: string): number => {
177
135
  return score;
178
136
  };
179
137
 
180
- // Pick the dev-server host a physical device should connect to. An explicit override always wins;
181
- // otherwise rank non-internal IPv4 interfaces so a down/virtual interface (e.g. an inactive
182
- // Thunderbolt bridge enumerated first) never shadows a real LAN NIC. Deterministic tie-break.
138
+ // Ranked, not first-found: an inactive Thunderbolt bridge is often enumerated before the real LAN NIC.
183
139
  export const selectLocalDevHost = (
184
140
  interfaces: NodeJS.Dict<os.NetworkInterfaceInfo[]>,
185
141
  { override }: { override?: string } = {},
@@ -204,9 +160,6 @@ export const selectLocalDevHost = (
204
160
  : { host: "127.0.0.1", source: "loopback", candidates };
205
161
  };
206
162
 
207
- const isRecord = (value: unknown): value is Record<string, unknown> =>
208
- typeof value === "object" && value !== null && !Array.isArray(value);
209
-
210
163
  const asString = (value: unknown) => (typeof value === "string" ? value : undefined);
211
164
 
212
165
  const firstString = (...values: unknown[]) => values.find((value): value is string => typeof value === "string");
@@ -227,8 +180,7 @@ const dedupeIosRunTargets = (targets: IosRunTarget[]) => {
227
180
  return [...byKey.values()];
228
181
  };
229
182
 
230
- // Normalize a simctl runtime — either the JSON key ("com.apple.CoreSimulator.SimRuntime.iOS-18-2")
231
- // or a per-device `runtimeIdentifier` / text header ("iOS 18.2") — into a "iOS 18.2" display string.
183
+ // simctl spells a runtime as a JSON key ("com.apple.CoreSimulator.SimRuntime.iOS-18-2") or as "iOS 18.2".
232
184
  const formatSimctlRuntime = (key?: string): string | undefined => {
233
185
  if (!key) return undefined;
234
186
  if (/^(iOS|watchOS|tvOS|visionOS)\s+[\d.]+$/i.test(key)) return key;
@@ -237,18 +189,11 @@ const formatSimctlRuntime = (key?: string): string | undefined => {
237
189
  return `${match[1]} ${match[2]}${match[3] ? `.${match[3]}` : ""}`;
238
190
  };
239
191
 
240
- // Extract the major OS version from a runtime display string ("iOS 18.2" → 18). Undefined for
241
- // physical devices (no runtime) or unparseable values.
242
192
  export const parseIosRuntimeMajor = (runtime?: string): number | undefined => {
243
- const major = runtime?.match(/(\d+)/)?.[1];
244
- if (major === undefined) return undefined;
245
- const parsed = Number.parseInt(major, 10);
246
- return Number.isNaN(parsed) ? undefined : parsed;
193
+ const major = runtime?.match(/\d+/)?.[0];
194
+ return major === undefined ? undefined : Number.parseInt(major, 10);
247
195
  };
248
196
 
249
- // Rank for default-target ordering: ready targets (booted simulator / connected device) first, then
250
- // physical devices, then newer simulator runtimes. Fixes the old ascending-runtime order that made a
251
- // stale iOS 17.x simulator the default pick.
252
197
  const iosRunTargetRank = (target: IosRunTarget): number => {
253
198
  const state = target.state?.toLowerCase() ?? "";
254
199
  const ready = state.includes("booted") || state.includes("connected") || state.includes("available") ? 1000 : 0;
@@ -515,9 +460,8 @@ export function sanitizeIosNativeRunEnv(env: MobileCommandEnv): MobileCommandEnv
515
460
  return Object.fromEntries(Object.entries(env).filter(([key]) => !iosNativeBlockedEnvKeys.has(key)));
516
461
  }
517
462
 
518
- // Bundle IDs that ship as scaffold placeholders (or use an obviously generic org segment) and are
519
- // almost always already claimed on Apple's developer portal, so device signing fails with
520
- // "cannot be registered to your development team". A unique reverse-DNS id fixes it.
463
+ // Placeholder bundle IDs are already claimed on Apple's portal, so device signing fails with "cannot be registered
464
+ // to your development team".
521
465
  export const PLACEHOLDER_APP_IDS = [
522
466
  "com.myapp.app",
523
467
  "com.myorg.myapp",
@@ -643,9 +587,7 @@ export function assertJsonSerializable(value: unknown, label = "capacitor.config
643
587
  if (seen.has(objectValue)) throw new Error(`${label} must be JSON serializable. Found circular reference.`);
644
588
  seen.add(objectValue);
645
589
  if (Array.isArray(value)) {
646
- value.forEach((item, index) => {
647
- assertJsonSerializable(item, `${label}[${index}]`, seen);
648
- });
590
+ for (const [index, item] of value.entries()) assertJsonSerializable(item, `${label}[${index}]`, seen);
649
591
  return;
650
592
  }
651
593
  for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
@@ -750,8 +692,6 @@ export class CapacitorApp {
750
692
  readonly iosProjectPath = "ios/App";
751
693
  readonly androidRootPath = "android";
752
694
  readonly androidAssetsPath = "android/app/src/main/assets";
753
- //* Accumulates iOS entitlements contributed by deep links and plugins during a prepare,
754
- //* then flushed to App/App.entitlements once (see #flushIosEntitlements).
755
695
  #iosEntitlements: Record<string, string | string[]> = {};
756
696
  constructor(
757
697
  private readonly app: AppExecutor,
@@ -814,7 +754,6 @@ export class CapacitorApp {
814
754
  await this.#prepareIos({ operation: "release", env, regenerate });
815
755
  await this.#spawnMobile("npx", ["cap", "build", "ios"], { operation: "release", env }, { stdio: "inherit" });
816
756
  this.app.verbose(`build completed iOS.`);
817
- return;
818
757
  }
819
758
  async syncIos() {
820
759
  await this.#spawnMobile("npx", ["cap", "sync", "ios"], { operation: "local", env: "local" });
@@ -839,7 +778,10 @@ export class CapacitorApp {
839
778
  }
840
779
 
841
780
  async #selectIosRunTarget(deviceId?: string) {
842
- const targets = sortIosRunTargets(await this.#loadIosRunTargets());
781
+ const targets = sortIosRunTargets([
782
+ ...(await this.#loadPhysicalIosDevices()),
783
+ ...(await this.#loadIosSimulators()),
784
+ ]);
843
785
  if (deviceId) {
844
786
  const needle = deviceId.toLowerCase();
845
787
  const found =
@@ -881,12 +823,6 @@ export class CapacitorApp {
881
823
  );
882
824
  }
883
825
 
884
- async #loadIosRunTargets() {
885
- const devices = await this.#loadPhysicalIosDevices();
886
- const simulators = await this.#loadIosSimulators();
887
- return [...devices, ...simulators];
888
- }
889
-
890
826
  async #loadPhysicalIosDevices() {
891
827
  try {
892
828
  return parseDevicectlDevices(await this.#spawn("xcrun", ["devicectl", "list", "devices", "--json-output", "-"]));
@@ -929,12 +865,11 @@ export class CapacitorApp {
929
865
  { operation, platform: "ios", iosRunTargetKind: runTarget.kind },
930
866
  mobileEnv,
931
867
  );
932
- await this.#writeRootCapacitorConfig(configContent);
933
- const scheme = this.#iosScheme();
868
+ await writeRootCapacitorConfig(this.app.cwdPath, configContent);
934
869
  const command = buildIosNativeRunCommand({
935
870
  appRoot: this.app.cwdPath,
936
871
  device: runTarget,
937
- scheme,
872
+ scheme: isRecord(this.target.ios) && typeof this.target.ios.scheme === "string" ? this.target.ios.scheme : "App",
938
873
  configuration: operation === "release" ? "Release" : "Debug",
939
874
  });
940
875
  const xcodebuildArgs = noAllowProvisioningUpdates
@@ -946,16 +881,10 @@ export class CapacitorApp {
946
881
  env: mobileEnv,
947
882
  });
948
883
  const devicectlId = runTarget.devicectlId ?? runTarget.id;
949
- await this.#spawn("xcrun", ["devicectl", "device", "install", "app", "--device", devicectlId, command.appPath], {
950
- env: mobileEnv,
951
- });
952
- await this.#spawn(
953
- "xcrun",
954
- ["devicectl", "device", "process", "launch", "--device", devicectlId, this.target.appId],
955
- {
956
- env: mobileEnv,
957
- },
958
- );
884
+ const devicectl = (verb: string[], subject: string) =>
885
+ this.#spawn("xcrun", ["devicectl", "device", ...verb, "--device", devicectlId, subject], { env: mobileEnv });
886
+ await devicectl(["install", "app"], command.appPath);
887
+ await devicectl(["process", "launch"], this.target.appId);
959
888
  } catch (error) {
960
889
  throw new Error(
961
890
  formatIosRunFailureMessage({
@@ -966,14 +895,10 @@ export class CapacitorApp {
966
895
  }),
967
896
  );
968
897
  } finally {
969
- await this.#clearRootCapacitorConfigs();
898
+ await clearRootCapacitorConfigs(this.app.cwdPath);
970
899
  }
971
900
  }
972
901
 
973
- #iosScheme() {
974
- return isRecord(this.target.ios) && typeof this.target.ios.scheme === "string" ? this.target.ios.scheme : "App";
975
- }
976
-
977
902
  async #getIosDevelopmentTeam() {
978
903
  const pbxprojPath = path.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
979
904
  if (!(await Bun.file(pbxprojPath).exists())) return undefined;
@@ -996,15 +921,13 @@ export class CapacitorApp {
996
921
  await this.#disableNativeKeyboardResizeInAndroid();
997
922
  await this.project.commit();
998
923
  await this.#generateAssets({ operation, env });
999
- await this.#ensureAndroidAssetsDir();
924
+ await mkdir(path.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
1000
925
  await this.#ensureAndroidDebugKeystore();
1001
926
  await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
1002
927
  await this.#setDeepLinksInAndroid(this.target.deepLinks?.schemes ?? [], this.target.deepLinks?.domains ?? []);
1003
928
  }
1004
929
 
1005
930
  async #updateAndroidBuildTypes() {
1006
- //keystore 기본 설정 및 debug, release 설정
1007
-
1008
931
  const appGradle = await FileEditor.create(path.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
1009
932
  const buildTypesBlock = `
1010
933
  debug {
@@ -1048,10 +971,7 @@ export class CapacitorApp {
1048
971
  await this.#prepareAndroid({ operation: "release", env, regenerate });
1049
972
  await this.#assertAndroidReleaseSigningConfig();
1050
973
  await this.#updateAndroidBuildTypes();
1051
- //윈도우는 gradlew.bat 사용
1052
- const isWindows = process.platform === "win32";
1053
- const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
1054
-
974
+ const gradleCommand = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
1055
975
  await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
1056
976
  stdio: "inherit",
1057
977
  cwd: path.join(this.app.cwdPath, this.androidRootPath),
@@ -1079,30 +999,18 @@ export class CapacitorApp {
1079
999
  });
1080
1000
  if (changed) await writeFile(manifestPath, manifest);
1081
1001
  }
1082
- async #ensureAndroidAssetsDir() {
1083
- await mkdir(path.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
1084
- }
1085
1002
  async #ensureAndroidDebugKeystore() {
1086
1003
  const keystorePath = path.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
1087
1004
  if (await Bun.file(keystorePath).exists()) return;
1088
1005
 
1006
+ const options =
1007
+ "-storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000";
1089
1008
  await this.#spawn("keytool", [
1090
1009
  "-genkeypair",
1091
1010
  "-v",
1092
1011
  "-keystore",
1093
1012
  keystorePath,
1094
- "-storepass",
1095
- "android",
1096
- "-alias",
1097
- "androiddebugkey",
1098
- "-keypass",
1099
- "android",
1100
- "-keyalg",
1101
- "RSA",
1102
- "-keysize",
1103
- "2048",
1104
- "-validity",
1105
- "10000",
1013
+ ...options.split(" "),
1106
1014
  "-dname",
1107
1015
  "CN=Android Debug,O=Android,C=US",
1108
1016
  ]);
@@ -1117,8 +1025,7 @@ export class CapacitorApp {
1117
1025
  await this.#prepareAndroid({ operation, env, regenerate });
1118
1026
  await this.#assertAndroidAdbReady();
1119
1027
  this.app.logger.info(`Running Android in ${operation} mode on ${env} env`);
1120
- const args = ["cap", "run", "android"];
1121
- await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
1028
+ await this.#spawnMobile("npx", ["cap", "run", "android"], { operation, env }, { stdio: "inherit" });
1122
1029
  }
1123
1030
 
1124
1031
  async #assertAndroidReleaseSigningConfig() {
@@ -1194,8 +1101,7 @@ export class CapacitorApp {
1194
1101
  await Bun.write(path.join(this.targetRoot, "capacitor.config.json"), content);
1195
1102
  return content;
1196
1103
  }
1197
- // An explicit override always wins; an emulator/simulator run reaches the host machine through a
1198
- // loopback alias (10.0.2.2 / localhost), so LAN detection only applies to physical-device runs.
1104
+ // Emulators and simulators reach the host through a loopback alias (10.0.2.2 / localhost); only devices need the LAN.
1199
1105
  async #resolveLocalDevHost({
1200
1106
  override,
1201
1107
  platform,
@@ -1219,8 +1125,6 @@ export class CapacitorApp {
1219
1125
  }
1220
1126
  return resolution;
1221
1127
  }
1222
- // Surface the live-reload URL a physical device must reach, and warn when auto-detection landed on
1223
- // a likely-unreachable host so a blank WebView is not mistaken for an app bug.
1224
1128
  #logDevHostResolution(resolution: LocalDevHostResolution, commandEnv: MobileCommandEnv) {
1225
1129
  this.app.log(`Mobile live-reload server: ${this.#localCsrUrl(resolution.host, commandEnv)}`);
1226
1130
  if (resolution.source === "override" || resolution.source === "platform") return;
@@ -1237,14 +1141,12 @@ export class CapacitorApp {
1237
1141
  async #prepareTargetAssets() {
1238
1142
  if (!this.target.assets) return;
1239
1143
  await mkdir(this.targetAssetRoot, { recursive: true });
1240
- if (this.target.assets.icon)
1241
- await cp(path.join(this.app.cwdPath, this.target.assets.icon), path.join(this.targetAssetRoot, "icon.png"), {
1242
- force: true,
1243
- });
1244
- if (this.target.assets.splash)
1245
- await cp(path.join(this.app.cwdPath, this.target.assets.splash), path.join(this.targetAssetRoot, "splash.png"), {
1246
- force: true,
1247
- });
1144
+ for (const [name, source] of [
1145
+ ["icon", this.target.assets.icon],
1146
+ ["splash", this.target.assets.splash],
1147
+ ] as const)
1148
+ if (source)
1149
+ await cp(path.join(this.app.cwdPath, source), path.join(this.targetAssetRoot, `${name}.png`), { force: true });
1248
1150
  }
1249
1151
  async #prepareExternalFiles(platform: "ios" | "android") {
1250
1152
  const files = this.target.files?.[platform];
@@ -1327,20 +1229,11 @@ export class CapacitorApp {
1327
1229
  operation,
1328
1230
  env,
1329
1231
  setIosUsageDescriptions: (descriptions) => this.#setPermissionInIos(descriptions),
1330
- updateIosInfoPlist: async (values) => {
1331
- await Promise.all([
1332
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", values),
1333
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", values),
1334
- ]);
1335
- },
1232
+ updateIosInfoPlist: (values) => this.#updateIosInfoPlist(values),
1336
1233
  addIosEntitlements: (entitlements) => this.#addIosEntitlements(entitlements),
1337
1234
  editIosAppDelegate: (transform) => this.#editIosAppDelegate(transform),
1338
- addAndroidPermissions: (permissions) => {
1339
- this.#setPermissionsInAndroid(permissions);
1340
- },
1341
- addAndroidFeatures: (features) => {
1342
- this.#setFeaturesInAndroid(features);
1343
- },
1235
+ addAndroidPermissions: (permissions) => this.#setPermissionsInAndroid(permissions),
1236
+ addAndroidFeatures: (features) => this.#setFeaturesInAndroid(features),
1344
1237
  };
1345
1238
  }
1346
1239
  async #applyDeepLinks(platform: "ios" | "android", { operation, env }: Pick<RunConfig, "operation" | "env">) {
@@ -1420,12 +1313,6 @@ export class CapacitorApp {
1420
1313
  if (this.target.indexPath) params.set("akanMobileIndexPath", this.target.indexPath);
1421
1314
  return `http://${ip}:${port}/${pathname}?${params}`;
1422
1315
  }
1423
- async #clearRootCapacitorConfigs() {
1424
- await clearRootCapacitorConfigs(this.app.cwdPath);
1425
- }
1426
- async #writeRootCapacitorConfig(content: string) {
1427
- await writeRootCapacitorConfig(this.app.cwdPath, content);
1428
- }
1429
1316
  async #spawn(command: string, args: string[] = [], options: Parameters<AppExecutor["spawn"]>[2] = {}) {
1430
1317
  return await this.app.spawn(command, args, { cwd: this.app.cwdPath, ...options });
1431
1318
  }
@@ -1441,14 +1328,11 @@ export class CapacitorApp {
1441
1328
  { operation, platform: platform ?? this.#inferMobilePlatform(args), iosRunTargetKind },
1442
1329
  mobileEnv,
1443
1330
  );
1444
- await this.#writeRootCapacitorConfig(configContent);
1331
+ await writeRootCapacitorConfig(this.app.cwdPath, configContent);
1445
1332
  try {
1446
- return await this.#spawn(command, args, {
1447
- ...spawnOptions,
1448
- env: mobileEnv,
1449
- });
1333
+ return await this.#spawn(command, args, { ...spawnOptions, env: mobileEnv });
1450
1334
  } finally {
1451
- await this.#clearRootCapacitorConfigs();
1335
+ await clearRootCapacitorConfigs(this.app.cwdPath);
1452
1336
  }
1453
1337
  }
1454
1338
  #inferMobilePlatform(args: string[]): MobilePlatform | undefined {
@@ -1481,7 +1365,6 @@ export class CapacitorApp {
1481
1365
  #addIosEntitlements(entitlements: Record<string, string | string[]>) {
1482
1366
  Object.assign(this.#iosEntitlements, entitlements);
1483
1367
  }
1484
- //* Generic in-place transform of ios/App/App/AppDelegate.swift (no-op if the file is absent).
1485
1368
  //* Feature-specific native wiring (e.g. Firebase) lives in plugins, not the framework.
1486
1369
  async #editIosAppDelegate(transform: (content: string) => string) {
1487
1370
  const appDelegatePath = path.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
@@ -1491,22 +1374,20 @@ export class CapacitorApp {
1491
1374
  const next = transform(content);
1492
1375
  if (next !== content) await editor.setContent(next).save();
1493
1376
  }
1377
+ async #updateIosInfoPlist(values: Record<string, unknown>) {
1378
+ await Promise.all(
1379
+ (["Debug", "Release"] as const).map((build) =>
1380
+ this.project.ios.updateInfoPlist(this.iosTargetName, build, values),
1381
+ ),
1382
+ );
1383
+ }
1494
1384
  async #setPermissionInIos(permissions: { [key: string]: string }) {
1495
- const updateNs = toIosInfoPlistUsageDescriptions(permissions);
1496
- await Promise.all([
1497
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", updateNs),
1498
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs),
1499
- ]);
1385
+ await this.#updateIosInfoPlist(toIosInfoPlistUsageDescriptions(permissions));
1500
1386
  }
1501
1387
  async #setUrlSchemesInIos(schemes: string[]) {
1502
- const urlTypes = schemes.map((scheme) => ({
1503
- CFBundleURLName: this.target.appId,
1504
- CFBundleURLSchemes: [scheme],
1505
- }));
1506
- await Promise.all([
1507
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { CFBundleURLTypes: urlTypes }),
1508
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes }),
1509
- ]);
1388
+ await this.#updateIosInfoPlist({
1389
+ CFBundleURLTypes: schemes.map((scheme) => ({ CFBundleURLName: this.target.appId, CFBundleURLSchemes: [scheme] })),
1390
+ });
1510
1391
  }
1511
1392
  #serializeIosEntitlements(entitlements: Record<string, string | string[]>) {
1512
1393
  const lines: string[] = [];
@@ -1520,8 +1401,6 @@ export class CapacitorApp {
1520
1401
  }
1521
1402
  return lines;
1522
1403
  }
1523
- //* Write the accumulated iOS entitlements (from deep links + plugins) to App/App.entitlements
1524
- //* once per prepare. Skipped entirely when nothing contributed entitlements.
1525
1404
  async #flushIosEntitlements() {
1526
1405
  if (Object.keys(this.#iosEntitlements).length === 0) return;
1527
1406
  const entitlementsRelPath = "App/App.entitlements";
@@ -1579,73 +1458,51 @@ export class CapacitorApp {
1579
1458
  if (!(await Bun.file(entitlementsPath).exists())) return;
1580
1459
  await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
1581
1460
  }
1582
- async #setUrlSchemesInAndroid(schemes: string[]) {
1461
+ async #setDeepLinksInAndroid(schemes: string[], domains: string[]) {
1583
1462
  const manifestPath = path.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
1584
- let manifest = await readFile(manifestPath, "utf8");
1585
- let changed = false;
1463
+ const original = await readFile(manifestPath, "utf8");
1464
+ const pathPrefix = resolveMobilePath(this.target, "/");
1465
+ let manifest = original;
1586
1466
  for (const scheme of schemes) {
1587
1467
  if (manifest.includes(`android:scheme="${scheme}"`)) continue;
1588
- const filter = [
1589
- " <intent-filter>",
1590
- ' <action android:name="android.intent.action.VIEW" />',
1591
- ' <category android:name="android.intent.category.DEFAULT" />',
1592
- ' <category android:name="android.intent.category.BROWSABLE" />',
1593
- ` <data android:scheme="${scheme}" />`,
1594
- " </intent-filter>",
1595
- ].join("\n");
1596
- manifest = manifest.replace(/(\s*<\/activity>)/, `\n${filter}$1`);
1597
- changed = true;
1468
+ manifest = CapacitorApp.#withIntentFilter(manifest, "<intent-filter>", `<data android:scheme="${scheme}" />`);
1598
1469
  }
1599
- if (changed) await writeFile(manifestPath, manifest);
1600
- }
1601
- async #setDeepLinksInAndroid(schemes: string[], domains: string[]) {
1602
- await this.#setUrlSchemesInAndroid(schemes);
1603
- const manifestPath = path.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
1604
- let manifest = await readFile(manifestPath, "utf8");
1605
- let changed = false;
1606
- const pathPrefix = resolveMobilePath(this.target, "/");
1607
1470
  for (const domain of domains) {
1608
1471
  if (manifest.includes(`android:host="${domain}"`) && manifest.includes('android:scheme="https"')) continue;
1609
- const filter = [
1610
- ' <intent-filter android:autoVerify="true">',
1611
- ' <action android:name="android.intent.action.VIEW" />',
1612
- ' <category android:name="android.intent.category.DEFAULT" />',
1613
- ' <category android:name="android.intent.category.BROWSABLE" />',
1614
- ` <data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" />`,
1615
- " </intent-filter>",
1616
- ].join("\n");
1617
- manifest = manifest.replace(/(\s*<\/activity>)/, `\n${filter}$1`);
1618
- changed = true;
1472
+ manifest = CapacitorApp.#withIntentFilter(
1473
+ manifest,
1474
+ '<intent-filter android:autoVerify="true">',
1475
+ `<data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" />`,
1476
+ );
1619
1477
  }
1620
- if (changed) await writeFile(manifestPath, manifest);
1478
+ if (manifest !== original) await writeFile(manifestPath, manifest);
1479
+ }
1480
+ static #withIntentFilter(manifest: string, open: string, data: string) {
1481
+ const filter = [
1482
+ ` ${open}`,
1483
+ ' <action android:name="android.intent.action.VIEW" />',
1484
+ ' <category android:name="android.intent.category.DEFAULT" />',
1485
+ ' <category android:name="android.intent.category.BROWSABLE" />',
1486
+ ` ${data}`,
1487
+ " </intent-filter>",
1488
+ ].join("\n");
1489
+ return manifest.replace(/(\s*<\/activity>)/, `\n${filter}$1`);
1621
1490
  }
1622
1491
  #setFeaturesInAndroid(features: string[]) {
1623
1492
  for (const feature of features) {
1624
- if (this.#hasFeatureInAndroid(feature)) {
1493
+ if (this.#androidManifestNames("uses-feature").includes(feature)) {
1625
1494
  this.app.logger.info(`${feature} already exists in android`);
1626
- return this;
1495
+ return;
1627
1496
  }
1628
1497
  this.app.logger.info(`Adding ${feature} to android`);
1629
1498
  this.project.android
1630
1499
  .getAndroidManifest()
1631
1500
  .injectFragment("manifest", `<uses-feature android:name="${feature}" />`);
1632
1501
  }
1633
- return this;
1634
1502
  }
1635
- #getFeaturesInAndroid() {
1636
- const androidManifest = this.project.android.getAndroidManifest();
1637
- const element = androidManifest.getDocumentElement();
1638
- if (!element) throw new Error("manifest not found");
1639
- const usesFeature = element.getElementsByTagName("uses-feature");
1640
- return Array.from(usesFeature).map((feature) => feature.getAttribute("android:name"));
1641
- }
1642
- #hasFeatureInAndroid(feature: string) {
1643
- return this.#getFeaturesInAndroid().includes(feature);
1644
- }
1645
-
1646
1503
  #setPermissionsInAndroid(permissions: string[]) {
1647
1504
  for (const permission of permissions) {
1648
- if (this.#hasPermissionInAndroid(permission)) {
1505
+ if (this.#androidManifestNames("uses-permission").includes(`android.permission.${permission}`)) {
1649
1506
  this.app.logger.info(`${permission} already exists in android`);
1650
1507
  continue;
1651
1508
  }
@@ -1654,16 +1511,10 @@ export class CapacitorApp {
1654
1511
  .getAndroidManifest()
1655
1512
  .injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
1656
1513
  }
1657
- return this;
1658
1514
  }
1659
- #getPermissionsInAndroid() {
1660
- const androidManifest = this.project.android.getAndroidManifest();
1661
- const element = androidManifest.getDocumentElement();
1515
+ #androidManifestNames(tagName: "uses-feature" | "uses-permission") {
1516
+ const element = this.project.android.getAndroidManifest().getDocumentElement();
1662
1517
  if (!element) throw new Error("manifest not found");
1663
- const usesPermission = element.getElementsByTagName("uses-permission");
1664
- return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
1665
- }
1666
- #hasPermissionInAndroid(permission: string) {
1667
- return this.#getPermissionsInAndroid().includes(`android.permission.${permission}`);
1518
+ return Array.from(element.getElementsByTagName(tagName)).map((node) => node.getAttribute("android:name"));
1668
1519
  }
1669
1520
  }