@akanjs/devkit 3.0.0-beta.2 → 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 (265) hide show
  1. package/README.md +1 -0
  2. package/abstractDoc.test.ts +6 -15
  3. package/abstractDoc.ts +6 -8
  4. package/agentsIndex.test.ts +4 -7
  5. package/agentsIndex.ts +6 -22
  6. package/akanApp/BackendImportGraph.test.ts +8 -22
  7. package/akanApp/BackendImportGraph.ts +1 -20
  8. package/akanApp/akanApp.host.test.ts +1 -3
  9. package/akanApp/akanApp.host.ts +82 -254
  10. package/akanApp/devHostPolicy.test.ts +10 -34
  11. package/akanApp/devHostPolicy.ts +12 -98
  12. package/akanConfig/akanConfig.test.ts +85 -69
  13. package/akanConfig/akanConfig.ts +69 -153
  14. package/akanConfig/types.ts +1 -0
  15. package/akanContext.ts +106 -159
  16. package/akanMcpContract.ts +28 -73
  17. package/appSelectionMemory.ts +1 -4
  18. package/applicationBuildReporter.ts +37 -5
  19. package/applicationBuildRunner.test.ts +5 -3
  20. package/applicationBuildRunner.ts +54 -61
  21. package/applicationReleasePackager.ts +24 -44
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +9 -30
  24. package/artifact/implicitRootLayout.ts +34 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +6 -1
  28. package/biomeBase.ts +2 -5
  29. package/capacitorApp.test.ts +32 -16
  30. package/capacitorApp.ts +110 -238
  31. package/cloud/cloudApi.test.ts +202 -0
  32. package/cloud/cloudApi.ts +77 -39
  33. package/cloud/configLock.test.ts +64 -0
  34. package/cloud/configLock.ts +96 -0
  35. package/cloud/constants.ts +26 -5
  36. package/cloud/globalConfig.ts +70 -32
  37. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  38. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  39. package/codeAgent/agent/CodeAgent.ts +551 -0
  40. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  41. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  42. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  43. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  44. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  45. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  46. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  47. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  48. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  49. package/codeAgent/agent/CodeMailbox.ts +148 -0
  50. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  51. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  52. package/codeAgent/agent/SubagentPool.ts +214 -0
  53. package/codeAgent/agent/akanCodeModel.ts +47 -0
  54. package/codeAgent/agent/akanCodePaths.ts +31 -0
  55. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  56. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  57. package/codeAgent/agent/codeAgent.test.ts +178 -0
  58. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  59. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  60. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  61. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  62. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  63. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  64. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  65. package/codeAgent/agent/consoleToStderr.ts +8 -0
  66. package/codeAgent/agent/jsonLines.ts +29 -0
  67. package/codeAgent/agent/stringArg.ts +5 -0
  68. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  69. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  70. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  71. package/codeAgent/feedback/PreviewView.ts +95 -0
  72. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  73. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  74. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  75. package/codeAgent/index.ts +34 -0
  76. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  77. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  78. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  79. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  80. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  81. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  82. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  83. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  84. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  85. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  86. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  87. package/codeAgent/tools/AkanCli.ts +50 -0
  88. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  89. package/codeAgent/tools/AkanEditScope.ts +48 -0
  90. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  91. package/codeAgent/tools/AkanToolPack.ts +92 -0
  92. package/codeAgent/tools/AkanVerifier.ts +48 -0
  93. package/codeAgent/tools/AskToolPack.ts +99 -0
  94. package/codeAgent/tools/MailToolPack.ts +76 -0
  95. package/codeAgent/tools/McpClient.ts +187 -0
  96. package/codeAgent/tools/McpOAuth.ts +233 -0
  97. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  98. package/codeAgent/tools/McpServerConfig.ts +129 -0
  99. package/codeAgent/tools/McpSignIn.ts +158 -0
  100. package/codeAgent/tools/McpTokenStore.ts +81 -0
  101. package/codeAgent/tools/McpToolPack.ts +199 -0
  102. package/codeAgent/tools/SessionToolPack.ts +91 -0
  103. package/codeAgent/tools/WebToolPack.ts +147 -0
  104. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  105. package/codeAgent/tools/askToolPack.test.ts +116 -0
  106. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  107. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  108. package/codegenLock.test.ts +4 -13
  109. package/codegenLock.ts +3 -17
  110. package/commandDecorators/argMeta.ts +1 -5
  111. package/commandDecorators/camelToKebabCase.ts +1 -0
  112. package/commandDecorators/command.ts +46 -102
  113. package/commandDecorators/commandBuilder.ts +30 -61
  114. package/commandDecorators/commandDecorators.test.ts +2 -10
  115. package/commandDecorators/dependencyBuilder.ts +10 -1
  116. package/commandDecorators/helpFormatter.ts +68 -145
  117. package/commandDecorators/index.ts +0 -2
  118. package/commandDecorators/targetMeta.ts +9 -7
  119. package/createTunnel.ts +7 -14
  120. package/dependencyScanner.test.ts +4 -16
  121. package/dependencyScanner.ts +31 -80
  122. package/devkitUtils.test.ts +48 -20
  123. package/executors.linkFallback.test.ts +103 -0
  124. package/executors.test.ts +132 -106
  125. package/executors.ts +194 -484
  126. package/fileEditor.ts +9 -37
  127. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  128. package/fileSys.ts +6 -8
  129. package/formSetterScanner.ts +3 -16
  130. package/frontendBuild/allRoutesBuilder.ts +7 -21
  131. package/frontendBuild/autoImportSync.test.ts +27 -38
  132. package/frontendBuild/autoImportSync.ts +31 -65
  133. package/frontendBuild/buildRouteClient.test.ts +62 -37
  134. package/frontendBuild/bundleDefine.ts +12 -0
  135. package/frontendBuild/clientBuildTypes.ts +5 -36
  136. package/frontendBuild/clientEntriesBundler.ts +9 -40
  137. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  138. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  139. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  140. package/frontendBuild/cssCandidateCache.ts +6 -41
  141. package/frontendBuild/cssCompiler.ts +29 -78
  142. package/frontendBuild/cssImportResolver.ts +6 -19
  143. package/frontendBuild/devChangePlanner.ts +7 -10
  144. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  145. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  146. package/frontendBuild/fontOptimizer.test.ts +6 -14
  147. package/frontendBuild/fontOptimizer.ts +15 -72
  148. package/frontendBuild/fontPruner.test.ts +5 -10
  149. package/frontendBuild/fontPruner.ts +5 -20
  150. package/frontendBuild/frontendBuild.test.ts +46 -69
  151. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  152. package/frontendBuild/hmrWatcher.test.ts +8 -32
  153. package/frontendBuild/hmrWatcher.ts +14 -71
  154. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  155. package/frontendBuild/precompressArtifacts.ts +5 -18
  156. package/frontendBuild/routeClientBuilder.ts +23 -46
  157. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  158. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  159. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  160. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  161. package/frontendBuild/styleContract.ts +1 -8
  162. package/frontendBuild/themeValidator.ts +4 -26
  163. package/frontendBuild/watchRootResolver.test.ts +23 -42
  164. package/frontendBuild/watchRootResolver.ts +5 -27
  165. package/getRelatedCnsts.ts +12 -92
  166. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  167. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  168. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  169. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  170. package/incrementalBuilder/builderChannel.test.ts +24 -49
  171. package/incrementalBuilder/builderChannel.ts +6 -37
  172. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  173. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  174. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  175. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  176. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  177. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  178. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  179. package/index.ts +5 -17
  180. package/integration/devResourceProbe.ts +50 -56
  181. package/integration/devStability.integration.test.ts +47 -194
  182. package/integration/devStabilityHarness.test.ts +13 -28
  183. package/integration/devStabilityHarness.ts +65 -330
  184. package/integration/ssrMemoryProbe.ts +41 -167
  185. package/libSource.test.ts +4 -15
  186. package/libSource.ts +19 -31
  187. package/lint/gritRules.test.ts +8 -24
  188. package/lint/no-deep-internal-import.grit +2 -2
  189. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  190. package/linter.test.ts +1 -2
  191. package/linter.ts +26 -116
  192. package/mobile/mobileTarget.ts +14 -13
  193. package/package.json +10 -5
  194. package/packageExports.test.ts +3 -18
  195. package/packageExportsMap.ts +3 -19
  196. package/piDependencyPins.test.ts +46 -0
  197. package/platformTest/BunTestReport.test.ts +60 -0
  198. package/platformTest/BunTestReport.ts +65 -0
  199. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  200. package/platformTest/IdleWatchedProcess.ts +73 -0
  201. package/platformTest/LinuxDockerTarget.ts +105 -0
  202. package/platformTest/PlatformTestRun.test.ts +64 -0
  203. package/platformTest/PlatformTestRun.ts +141 -0
  204. package/platformTest/PlatformTestTarget.ts +142 -0
  205. package/platformTest/TestSnapshot.test.ts +72 -0
  206. package/platformTest/TestSnapshot.ts +103 -0
  207. package/platformTest/WindowsSshTarget.ts +151 -0
  208. package/prompter.ts +3 -13
  209. package/qualityScanner.test.ts +9 -26
  210. package/qualityScanner.ts +197 -285
  211. package/recipeScanner.test.ts +7 -12
  212. package/recipeScanner.ts +9 -38
  213. package/repoIdentity.ts +2 -9
  214. package/routeSourceValidator.test.ts +0 -1
  215. package/routeSourceValidator.ts +7 -42
  216. package/scanInfo.ts +87 -216
  217. package/selectModel.ts +1 -2
  218. package/semver.ts +3 -5
  219. package/slicePlanner.test.ts +6 -24
  220. package/slicePlanner.ts +11 -32
  221. package/spinner.test.ts +3 -8
  222. package/spinner.ts +4 -16
  223. package/sshTunnel.ts +1 -6
  224. package/ssrScanner.test.ts +1 -6
  225. package/ssrScanner.ts +103 -113
  226. package/stripAnsi.ts +3 -0
  227. package/subspace.test.ts +30 -25
  228. package/subspace.ts +81 -115
  229. package/subspaceConfig.ts +11 -9
  230. package/testHelpers.ts +194 -0
  231. package/transforms/asyncDefaultExportDetector.ts +3 -11
  232. package/transforms/barrelAnalyzer.ts +43 -135
  233. package/transforms/barrelImportsPlugin.ts +38 -175
  234. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  235. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  236. package/transforms/moduleSyntax.ts +32 -0
  237. package/transforms/rscUseClientTransform.ts +35 -20
  238. package/transforms/transforms.test.ts +153 -44
  239. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  240. package/transforms/useClientBundlePlugin.ts +2 -22
  241. package/typeChecker.ts +7 -63
  242. package/types.ts +1 -0
  243. package/uploadRelease.ts +10 -25
  244. package/workflow/artifacts.ts +18 -0
  245. package/workflow/executor.test.ts +1 -12
  246. package/workflow/executor.ts +101 -110
  247. package/workflow/index.ts +0 -1
  248. package/workflow/moduleIndex.test.ts +4 -15
  249. package/workflow/moduleIndex.ts +9 -49
  250. package/workflow/plan.ts +18 -25
  251. package/workflow/render.ts +80 -96
  252. package/workflow/rolloutGate.test.ts +1 -1
  253. package/workflow/source.ts +52 -69
  254. package/workflow/sourceAst.ts +50 -0
  255. package/workspaceLayout.ts +4 -19
  256. package/abstractCompactor.ts +0 -112
  257. package/aiEditor.test.ts +0 -60
  258. package/aiEditor.ts +0 -387
  259. package/commandDecorators/commandMeta.ts +0 -7
  260. package/commandDecorators/types.ts +0 -10
  261. package/getDirname.ts +0 -4
  262. package/guideline.ts +0 -19
  263. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  264. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  265. 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;
@@ -489,7 +434,7 @@ export function classifyIosRunFailure(log: string): IosRunFailureClassification
489
434
  kind: "unknown",
490
435
  title: "iOS native run failed.",
491
436
  detail:
492
- "Review the xcodebuild/devicectl output above. You can retry with --noAllowProvisioningUpdates to use the conservative path.",
437
+ "Review the xcodebuild/devicectl output above. You can retry with --no-allow-provisioning-updates to use the conservative path.",
493
438
  };
494
439
  }
495
440
 
@@ -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",
@@ -597,6 +541,29 @@ export function raiseGradleMinSdkVersion(content: string, floor: number = ANDROI
597
541
  return content.replace(/(minSdkVersion\s*=\s*)\d+/, `$1${floor}`);
598
542
  }
599
543
 
544
+ //* Apple's Info.plist keys do not follow the description names (photo → NSPhotoLibrary…), so each one is spelled out.
545
+ const iosUsageDescriptionKeys = {
546
+ cameraUsageDescription: ["NSCameraUsageDescription"],
547
+ photoAddUsageDescription: ["NSPhotoLibraryAddUsageDescription"],
548
+ photoUsageDescription: ["NSPhotoLibraryUsageDescription"],
549
+ contactsUsageDescription: ["NSContactsUsageDescription"],
550
+ //? iOS 11+ reads the AlwaysAndWhenInUse key; the Always key only serves iOS 10 and below.
551
+ locationAlwaysUsageDescription: ["NSLocationAlwaysAndWhenInUseUsageDescription", "NSLocationAlwaysUsageDescription"],
552
+ locationWhenInUseUsageDescription: ["NSLocationWhenInUseUsageDescription"],
553
+ microphoneUsageDescription: ["NSMicrophoneUsageDescription"],
554
+ speechRecognitionUsageDescription: ["NSSpeechRecognitionUsageDescription"],
555
+ } as const;
556
+
557
+ export const toIosInfoPlistUsageDescriptions = (descriptions: { [key: string]: string }) =>
558
+ Object.fromEntries(
559
+ Object.entries(descriptions).flatMap(([key, value]) => {
560
+ const plistKeys: readonly string[] = Object.hasOwn(iosUsageDescriptionKeys, key)
561
+ ? iosUsageDescriptionKeys[key as keyof typeof iosUsageDescriptionKeys]
562
+ : [`NS${capitalize(key)}`];
563
+ return plistKeys.map((plistKey) => [plistKey, value]);
564
+ }),
565
+ );
566
+
600
567
  const mergeAllowNavigation = (configured: unknown, localIp: string | undefined) => {
601
568
  const values = Array.isArray(configured)
602
569
  ? configured.filter((value): value is string => typeof value === "string")
@@ -620,9 +587,7 @@ export function assertJsonSerializable(value: unknown, label = "capacitor.config
620
587
  if (seen.has(objectValue)) throw new Error(`${label} must be JSON serializable. Found circular reference.`);
621
588
  seen.add(objectValue);
622
589
  if (Array.isArray(value)) {
623
- value.forEach((item, index) => {
624
- assertJsonSerializable(item, `${label}[${index}]`, seen);
625
- });
590
+ for (const [index, item] of value.entries()) assertJsonSerializable(item, `${label}[${index}]`, seen);
626
591
  return;
627
592
  }
628
593
  for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
@@ -727,8 +692,6 @@ export class CapacitorApp {
727
692
  readonly iosProjectPath = "ios/App";
728
693
  readonly androidRootPath = "android";
729
694
  readonly androidAssetsPath = "android/app/src/main/assets";
730
- //* Accumulates iOS entitlements contributed by deep links and plugins during a prepare,
731
- //* then flushed to App/App.entitlements once (see #flushIosEntitlements).
732
695
  #iosEntitlements: Record<string, string | string[]> = {};
733
696
  constructor(
734
697
  private readonly app: AppExecutor,
@@ -791,7 +754,6 @@ export class CapacitorApp {
791
754
  await this.#prepareIos({ operation: "release", env, regenerate });
792
755
  await this.#spawnMobile("npx", ["cap", "build", "ios"], { operation: "release", env }, { stdio: "inherit" });
793
756
  this.app.verbose(`build completed iOS.`);
794
- return;
795
757
  }
796
758
  async syncIos() {
797
759
  await this.#spawnMobile("npx", ["cap", "sync", "ios"], { operation: "local", env: "local" });
@@ -816,7 +778,10 @@ export class CapacitorApp {
816
778
  }
817
779
 
818
780
  async #selectIosRunTarget(deviceId?: string) {
819
- const targets = sortIosRunTargets(await this.#loadIosRunTargets());
781
+ const targets = sortIosRunTargets([
782
+ ...(await this.#loadPhysicalIosDevices()),
783
+ ...(await this.#loadIosSimulators()),
784
+ ]);
820
785
  if (deviceId) {
821
786
  const needle = deviceId.toLowerCase();
822
787
  const found =
@@ -858,12 +823,6 @@ export class CapacitorApp {
858
823
  );
859
824
  }
860
825
 
861
- async #loadIosRunTargets() {
862
- const devices = await this.#loadPhysicalIosDevices();
863
- const simulators = await this.#loadIosSimulators();
864
- return [...devices, ...simulators];
865
- }
866
-
867
826
  async #loadPhysicalIosDevices() {
868
827
  try {
869
828
  return parseDevicectlDevices(await this.#spawn("xcrun", ["devicectl", "list", "devices", "--json-output", "-"]));
@@ -906,12 +865,11 @@ export class CapacitorApp {
906
865
  { operation, platform: "ios", iosRunTargetKind: runTarget.kind },
907
866
  mobileEnv,
908
867
  );
909
- await this.#writeRootCapacitorConfig(configContent);
910
- const scheme = this.#iosScheme();
868
+ await writeRootCapacitorConfig(this.app.cwdPath, configContent);
911
869
  const command = buildIosNativeRunCommand({
912
870
  appRoot: this.app.cwdPath,
913
871
  device: runTarget,
914
- scheme,
872
+ scheme: isRecord(this.target.ios) && typeof this.target.ios.scheme === "string" ? this.target.ios.scheme : "App",
915
873
  configuration: operation === "release" ? "Release" : "Debug",
916
874
  });
917
875
  const xcodebuildArgs = noAllowProvisioningUpdates
@@ -923,16 +881,10 @@ export class CapacitorApp {
923
881
  env: mobileEnv,
924
882
  });
925
883
  const devicectlId = runTarget.devicectlId ?? runTarget.id;
926
- await this.#spawn("xcrun", ["devicectl", "device", "install", "app", "--device", devicectlId, command.appPath], {
927
- env: mobileEnv,
928
- });
929
- await this.#spawn(
930
- "xcrun",
931
- ["devicectl", "device", "process", "launch", "--device", devicectlId, this.target.appId],
932
- {
933
- env: mobileEnv,
934
- },
935
- );
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);
936
888
  } catch (error) {
937
889
  throw new Error(
938
890
  formatIosRunFailureMessage({
@@ -943,14 +895,10 @@ export class CapacitorApp {
943
895
  }),
944
896
  );
945
897
  } finally {
946
- await this.#clearRootCapacitorConfigs();
898
+ await clearRootCapacitorConfigs(this.app.cwdPath);
947
899
  }
948
900
  }
949
901
 
950
- #iosScheme() {
951
- return isRecord(this.target.ios) && typeof this.target.ios.scheme === "string" ? this.target.ios.scheme : "App";
952
- }
953
-
954
902
  async #getIosDevelopmentTeam() {
955
903
  const pbxprojPath = path.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
956
904
  if (!(await Bun.file(pbxprojPath).exists())) return undefined;
@@ -973,15 +921,13 @@ export class CapacitorApp {
973
921
  await this.#disableNativeKeyboardResizeInAndroid();
974
922
  await this.project.commit();
975
923
  await this.#generateAssets({ operation, env });
976
- await this.#ensureAndroidAssetsDir();
924
+ await mkdir(path.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
977
925
  await this.#ensureAndroidDebugKeystore();
978
926
  await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
979
927
  await this.#setDeepLinksInAndroid(this.target.deepLinks?.schemes ?? [], this.target.deepLinks?.domains ?? []);
980
928
  }
981
929
 
982
930
  async #updateAndroidBuildTypes() {
983
- //keystore 기본 설정 및 debug, release 설정
984
-
985
931
  const appGradle = await FileEditor.create(path.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
986
932
  const buildTypesBlock = `
987
933
  debug {
@@ -1025,10 +971,7 @@ export class CapacitorApp {
1025
971
  await this.#prepareAndroid({ operation: "release", env, regenerate });
1026
972
  await this.#assertAndroidReleaseSigningConfig();
1027
973
  await this.#updateAndroidBuildTypes();
1028
- //윈도우는 gradlew.bat 사용
1029
- const isWindows = process.platform === "win32";
1030
- const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
1031
-
974
+ const gradleCommand = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
1032
975
  await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
1033
976
  stdio: "inherit",
1034
977
  cwd: path.join(this.app.cwdPath, this.androidRootPath),
@@ -1056,30 +999,18 @@ export class CapacitorApp {
1056
999
  });
1057
1000
  if (changed) await writeFile(manifestPath, manifest);
1058
1001
  }
1059
- async #ensureAndroidAssetsDir() {
1060
- await mkdir(path.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
1061
- }
1062
1002
  async #ensureAndroidDebugKeystore() {
1063
1003
  const keystorePath = path.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
1064
1004
  if (await Bun.file(keystorePath).exists()) return;
1065
1005
 
1006
+ const options =
1007
+ "-storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000";
1066
1008
  await this.#spawn("keytool", [
1067
1009
  "-genkeypair",
1068
1010
  "-v",
1069
1011
  "-keystore",
1070
1012
  keystorePath,
1071
- "-storepass",
1072
- "android",
1073
- "-alias",
1074
- "androiddebugkey",
1075
- "-keypass",
1076
- "android",
1077
- "-keyalg",
1078
- "RSA",
1079
- "-keysize",
1080
- "2048",
1081
- "-validity",
1082
- "10000",
1013
+ ...options.split(" "),
1083
1014
  "-dname",
1084
1015
  "CN=Android Debug,O=Android,C=US",
1085
1016
  ]);
@@ -1094,8 +1025,7 @@ export class CapacitorApp {
1094
1025
  await this.#prepareAndroid({ operation, env, regenerate });
1095
1026
  await this.#assertAndroidAdbReady();
1096
1027
  this.app.logger.info(`Running Android in ${operation} mode on ${env} env`);
1097
- const args = ["cap", "run", "android"];
1098
- await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
1028
+ await this.#spawnMobile("npx", ["cap", "run", "android"], { operation, env }, { stdio: "inherit" });
1099
1029
  }
1100
1030
 
1101
1031
  async #assertAndroidReleaseSigningConfig() {
@@ -1171,8 +1101,7 @@ export class CapacitorApp {
1171
1101
  await Bun.write(path.join(this.targetRoot, "capacitor.config.json"), content);
1172
1102
  return content;
1173
1103
  }
1174
- // An explicit override always wins; an emulator/simulator run reaches the host machine through a
1175
- // 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.
1176
1105
  async #resolveLocalDevHost({
1177
1106
  override,
1178
1107
  platform,
@@ -1196,8 +1125,6 @@ export class CapacitorApp {
1196
1125
  }
1197
1126
  return resolution;
1198
1127
  }
1199
- // Surface the live-reload URL a physical device must reach, and warn when auto-detection landed on
1200
- // a likely-unreachable host so a blank WebView is not mistaken for an app bug.
1201
1128
  #logDevHostResolution(resolution: LocalDevHostResolution, commandEnv: MobileCommandEnv) {
1202
1129
  this.app.log(`Mobile live-reload server: ${this.#localCsrUrl(resolution.host, commandEnv)}`);
1203
1130
  if (resolution.source === "override" || resolution.source === "platform") return;
@@ -1214,14 +1141,12 @@ export class CapacitorApp {
1214
1141
  async #prepareTargetAssets() {
1215
1142
  if (!this.target.assets) return;
1216
1143
  await mkdir(this.targetAssetRoot, { recursive: true });
1217
- if (this.target.assets.icon)
1218
- await cp(path.join(this.app.cwdPath, this.target.assets.icon), path.join(this.targetAssetRoot, "icon.png"), {
1219
- force: true,
1220
- });
1221
- if (this.target.assets.splash)
1222
- await cp(path.join(this.app.cwdPath, this.target.assets.splash), path.join(this.targetAssetRoot, "splash.png"), {
1223
- force: true,
1224
- });
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 });
1225
1150
  }
1226
1151
  async #prepareExternalFiles(platform: "ios" | "android") {
1227
1152
  const files = this.target.files?.[platform];
@@ -1304,20 +1229,11 @@ export class CapacitorApp {
1304
1229
  operation,
1305
1230
  env,
1306
1231
  setIosUsageDescriptions: (descriptions) => this.#setPermissionInIos(descriptions),
1307
- updateIosInfoPlist: async (values) => {
1308
- await Promise.all([
1309
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", values),
1310
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", values),
1311
- ]);
1312
- },
1232
+ updateIosInfoPlist: (values) => this.#updateIosInfoPlist(values),
1313
1233
  addIosEntitlements: (entitlements) => this.#addIosEntitlements(entitlements),
1314
1234
  editIosAppDelegate: (transform) => this.#editIosAppDelegate(transform),
1315
- addAndroidPermissions: (permissions) => {
1316
- this.#setPermissionsInAndroid(permissions);
1317
- },
1318
- addAndroidFeatures: (features) => {
1319
- this.#setFeaturesInAndroid(features);
1320
- },
1235
+ addAndroidPermissions: (permissions) => this.#setPermissionsInAndroid(permissions),
1236
+ addAndroidFeatures: (features) => this.#setFeaturesInAndroid(features),
1321
1237
  };
1322
1238
  }
1323
1239
  async #applyDeepLinks(platform: "ios" | "android", { operation, env }: Pick<RunConfig, "operation" | "env">) {
@@ -1397,12 +1313,6 @@ export class CapacitorApp {
1397
1313
  if (this.target.indexPath) params.set("akanMobileIndexPath", this.target.indexPath);
1398
1314
  return `http://${ip}:${port}/${pathname}?${params}`;
1399
1315
  }
1400
- async #clearRootCapacitorConfigs() {
1401
- await clearRootCapacitorConfigs(this.app.cwdPath);
1402
- }
1403
- async #writeRootCapacitorConfig(content: string) {
1404
- await writeRootCapacitorConfig(this.app.cwdPath, content);
1405
- }
1406
1316
  async #spawn(command: string, args: string[] = [], options: Parameters<AppExecutor["spawn"]>[2] = {}) {
1407
1317
  return await this.app.spawn(command, args, { cwd: this.app.cwdPath, ...options });
1408
1318
  }
@@ -1418,14 +1328,11 @@ export class CapacitorApp {
1418
1328
  { operation, platform: platform ?? this.#inferMobilePlatform(args), iosRunTargetKind },
1419
1329
  mobileEnv,
1420
1330
  );
1421
- await this.#writeRootCapacitorConfig(configContent);
1331
+ await writeRootCapacitorConfig(this.app.cwdPath, configContent);
1422
1332
  try {
1423
- return await this.#spawn(command, args, {
1424
- ...spawnOptions,
1425
- env: mobileEnv,
1426
- });
1333
+ return await this.#spawn(command, args, { ...spawnOptions, env: mobileEnv });
1427
1334
  } finally {
1428
- await this.#clearRootCapacitorConfigs();
1335
+ await clearRootCapacitorConfigs(this.app.cwdPath);
1429
1336
  }
1430
1337
  }
1431
1338
  #inferMobilePlatform(args: string[]): MobilePlatform | undefined {
@@ -1458,7 +1365,6 @@ export class CapacitorApp {
1458
1365
  #addIosEntitlements(entitlements: Record<string, string | string[]>) {
1459
1366
  Object.assign(this.#iosEntitlements, entitlements);
1460
1367
  }
1461
- //* Generic in-place transform of ios/App/App/AppDelegate.swift (no-op if the file is absent).
1462
1368
  //* Feature-specific native wiring (e.g. Firebase) lives in plugins, not the framework.
1463
1369
  async #editIosAppDelegate(transform: (content: string) => string) {
1464
1370
  const appDelegatePath = path.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
@@ -1468,24 +1374,20 @@ export class CapacitorApp {
1468
1374
  const next = transform(content);
1469
1375
  if (next !== content) await editor.setContent(next).save();
1470
1376
  }
1471
- async #setPermissionInIos(permissions: { [key: string]: string }) {
1472
- const updateNs = Object.fromEntries(
1473
- Object.entries(permissions).map(([key, value]) => [`NS${capitalize(key)}`, value]),
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
+ ),
1474
1382
  );
1475
- await Promise.all([
1476
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", updateNs),
1477
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs),
1478
- ]);
1383
+ }
1384
+ async #setPermissionInIos(permissions: { [key: string]: string }) {
1385
+ await this.#updateIosInfoPlist(toIosInfoPlistUsageDescriptions(permissions));
1479
1386
  }
1480
1387
  async #setUrlSchemesInIos(schemes: string[]) {
1481
- const urlTypes = schemes.map((scheme) => ({
1482
- CFBundleURLName: this.target.appId,
1483
- CFBundleURLSchemes: [scheme],
1484
- }));
1485
- await Promise.all([
1486
- this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { CFBundleURLTypes: urlTypes }),
1487
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes }),
1488
- ]);
1388
+ await this.#updateIosInfoPlist({
1389
+ CFBundleURLTypes: schemes.map((scheme) => ({ CFBundleURLName: this.target.appId, CFBundleURLSchemes: [scheme] })),
1390
+ });
1489
1391
  }
1490
1392
  #serializeIosEntitlements(entitlements: Record<string, string | string[]>) {
1491
1393
  const lines: string[] = [];
@@ -1499,8 +1401,6 @@ export class CapacitorApp {
1499
1401
  }
1500
1402
  return lines;
1501
1403
  }
1502
- //* Write the accumulated iOS entitlements (from deep links + plugins) to App/App.entitlements
1503
- //* once per prepare. Skipped entirely when nothing contributed entitlements.
1504
1404
  async #flushIosEntitlements() {
1505
1405
  if (Object.keys(this.#iosEntitlements).length === 0) return;
1506
1406
  const entitlementsRelPath = "App/App.entitlements";
@@ -1558,73 +1458,51 @@ export class CapacitorApp {
1558
1458
  if (!(await Bun.file(entitlementsPath).exists())) return;
1559
1459
  await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
1560
1460
  }
1561
- async #setUrlSchemesInAndroid(schemes: string[]) {
1461
+ async #setDeepLinksInAndroid(schemes: string[], domains: string[]) {
1562
1462
  const manifestPath = path.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
1563
- let manifest = await readFile(manifestPath, "utf8");
1564
- let changed = false;
1463
+ const original = await readFile(manifestPath, "utf8");
1464
+ const pathPrefix = resolveMobilePath(this.target, "/");
1465
+ let manifest = original;
1565
1466
  for (const scheme of schemes) {
1566
1467
  if (manifest.includes(`android:scheme="${scheme}"`)) continue;
1567
- const filter = [
1568
- " <intent-filter>",
1569
- ' <action android:name="android.intent.action.VIEW" />',
1570
- ' <category android:name="android.intent.category.DEFAULT" />',
1571
- ' <category android:name="android.intent.category.BROWSABLE" />',
1572
- ` <data android:scheme="${scheme}" />`,
1573
- " </intent-filter>",
1574
- ].join("\n");
1575
- manifest = manifest.replace(/(\s*<\/activity>)/, `\n${filter}$1`);
1576
- changed = true;
1468
+ manifest = CapacitorApp.#withIntentFilter(manifest, "<intent-filter>", `<data android:scheme="${scheme}" />`);
1577
1469
  }
1578
- if (changed) await writeFile(manifestPath, manifest);
1579
- }
1580
- async #setDeepLinksInAndroid(schemes: string[], domains: string[]) {
1581
- await this.#setUrlSchemesInAndroid(schemes);
1582
- const manifestPath = path.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
1583
- let manifest = await readFile(manifestPath, "utf8");
1584
- let changed = false;
1585
- const pathPrefix = resolveMobilePath(this.target, "/");
1586
1470
  for (const domain of domains) {
1587
1471
  if (manifest.includes(`android:host="${domain}"`) && manifest.includes('android:scheme="https"')) continue;
1588
- const filter = [
1589
- ' <intent-filter android:autoVerify="true">',
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="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" />`,
1594
- " </intent-filter>",
1595
- ].join("\n");
1596
- manifest = manifest.replace(/(\s*<\/activity>)/, `\n${filter}$1`);
1597
- 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
+ );
1598
1477
  }
1599
- 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`);
1600
1490
  }
1601
1491
  #setFeaturesInAndroid(features: string[]) {
1602
1492
  for (const feature of features) {
1603
- if (this.#hasFeatureInAndroid(feature)) {
1493
+ if (this.#androidManifestNames("uses-feature").includes(feature)) {
1604
1494
  this.app.logger.info(`${feature} already exists in android`);
1605
- return this;
1495
+ return;
1606
1496
  }
1607
1497
  this.app.logger.info(`Adding ${feature} to android`);
1608
1498
  this.project.android
1609
1499
  .getAndroidManifest()
1610
1500
  .injectFragment("manifest", `<uses-feature android:name="${feature}" />`);
1611
1501
  }
1612
- return this;
1613
- }
1614
- #getFeaturesInAndroid() {
1615
- const androidManifest = this.project.android.getAndroidManifest();
1616
- const element = androidManifest.getDocumentElement();
1617
- if (!element) throw new Error("manifest not found");
1618
- const usesFeature = element.getElementsByTagName("uses-feature");
1619
- return Array.from(usesFeature).map((feature) => feature.getAttribute("android:name"));
1620
- }
1621
- #hasFeatureInAndroid(feature: string) {
1622
- return this.#getFeaturesInAndroid().includes(feature);
1623
1502
  }
1624
-
1625
1503
  #setPermissionsInAndroid(permissions: string[]) {
1626
1504
  for (const permission of permissions) {
1627
- if (this.#hasPermissionInAndroid(permission)) {
1505
+ if (this.#androidManifestNames("uses-permission").includes(`android.permission.${permission}`)) {
1628
1506
  this.app.logger.info(`${permission} already exists in android`);
1629
1507
  continue;
1630
1508
  }
@@ -1633,16 +1511,10 @@ export class CapacitorApp {
1633
1511
  .getAndroidManifest()
1634
1512
  .injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
1635
1513
  }
1636
- return this;
1637
1514
  }
1638
- #getPermissionsInAndroid() {
1639
- const androidManifest = this.project.android.getAndroidManifest();
1640
- const element = androidManifest.getDocumentElement();
1515
+ #androidManifestNames(tagName: "uses-feature" | "uses-permission") {
1516
+ const element = this.project.android.getAndroidManifest().getDocumentElement();
1641
1517
  if (!element) throw new Error("manifest not found");
1642
- const usesPermission = element.getElementsByTagName("uses-permission");
1643
- return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
1644
- }
1645
- #hasPermissionInAndroid(permission: string) {
1646
- return this.#getPermissionsInAndroid().includes(`android.permission.${permission}`);
1518
+ return Array.from(element.getElementsByTagName(tagName)).map((node) => node.getAttribute("android:name"));
1647
1519
  }
1648
1520
  }