@akanjs/devkit 3.0.0-beta.3 → 3.0.0-beta.31

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 (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +2 -2
  3. package/README.md +3 -2
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +427 -141
  19. package/akanConfig/akanConfig.ts +327 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +21 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +269 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +177 -0
  241. package/mobile/desktopBin.ts +143 -0
  242. package/mobile/desktopServerStage.test.ts +204 -0
  243. package/mobile/desktopServerStage.ts +138 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +177 -0
  250. package/mobile/nativeApp.ts +416 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +414 -0
  254. package/mobile/nativeConfig.ts +293 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +30 -25
  298. package/subspace.ts +81 -115
  299. package/subspaceConfig.ts +11 -9
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,71 @@
1
+ import type { LogLevel } from "akanjs/common";
2
+
3
+ export interface NativeAppLineRead {
4
+ level: LogLevel;
5
+ message: string;
6
+ }
7
+
8
+ //* A line a native dev build prints — the page console its host mirrors, the simulator's console, logcat — read back
9
+ //* at the level the page logged it, so the terminal shows it once, stamped by the app's own logger.
10
+ export class NativeAppLine {
11
+ //? The host's grammar (desktop host.ts, iOS AkanNativeShell.swift): `[page<+><#window> <level>] text`, where `+`
12
+ //? marks a continuation line of a multi-line message and `#n` any window but the first.
13
+ static readonly #page = /^\[page(\+)?(?:#(\d+))? (trace|verbose|debug|log|info|warn|error)\] ?(.*)$/;
14
+ static readonly #logcat = /^([VDIWEFA])\/([^(]+?)\s*\(\s*\d+\):\s?(.*)$/;
15
+ static readonly #named = /^\[([^\]\s]+)\] ([\s\S]*)$/;
16
+ //? A server the desktop app carries (host server.ts): `[server] <its own log line>`, whose level word the akan logger
17
+ //? wrote; a line without one (a crash's stack) is still the app's and stays visible.
18
+ static readonly #server = /^\[server\] /;
19
+ static readonly #serverLevel = /\b(TRACE|VERBOSE|DEBUG|INFO|WARN|ERROR)\b/;
20
+ static readonly #logcatLevels: { [priority: string]: LogLevel } = {
21
+ V: "verbose",
22
+ D: "debug",
23
+ I: "info",
24
+ W: "warn",
25
+ E: "error",
26
+ F: "error",
27
+ A: "error",
28
+ };
29
+
30
+ /** `verbose` keeps the lines no page or host wrote (WebKit, the simulator) at info instead of debug. */
31
+ static read(line: string, { verbose = false }: { verbose?: boolean } = {}): NativeAppLineRead {
32
+ const logcat = NativeAppLine.#logcat.exec(line);
33
+ if (logcat) {
34
+ const [, priority = "I", tag = "", text = ""] = logcat;
35
+ const level = NativeAppLine.#logcatLevels[priority] ?? "info";
36
+ if (tag === "AkanNativeConsole") return NativeAppLine.#pageLine(level, text, { continuation: false });
37
+ if (tag === "AkanNative") return NativeAppLine.#hostLine(`[akan-native] ${text}`, level);
38
+ return { level, message: `[${tag}] ${text}` };
39
+ }
40
+ const page = NativeAppLine.#page.exec(line);
41
+ if (page) {
42
+ const [, continuation, window, level = "info", text = ""] = page;
43
+ return NativeAppLine.#pageLine(level === "log" ? "info" : (level as LogLevel), text, {
44
+ continuation: continuation === "+",
45
+ window,
46
+ });
47
+ }
48
+ if (line.startsWith("[akan-native")) return NativeAppLine.#hostLine(line, "info");
49
+ if (NativeAppLine.#server.test(line)) {
50
+ const word = NativeAppLine.#serverLevel.exec(Bun.stripANSI(line))?.[1];
51
+ return { level: word ? (word.toLowerCase() as LogLevel) : "info", message: line };
52
+ }
53
+ return { level: verbose ? "info" : "debug", message: line };
54
+ }
55
+
56
+ static #pageLine(
57
+ level: LogLevel,
58
+ text: string,
59
+ { continuation, window }: { continuation: boolean; window?: string },
60
+ ): NativeAppLineRead {
61
+ if (continuation) return { level, message: ` ${text}` };
62
+ const named = NativeAppLine.#named.exec(text);
63
+ const scope = `page${window ? `#${window}` : ""}${named ? `:${named[1]}` : ""}`;
64
+ return { level, message: `[${scope}] ${named ? named[2] : text}` };
65
+ }
66
+
67
+ //? The host's own lines are about the host, not the app: where its pages come from is boot detail.
68
+ static #hostLine(line: string, level: LogLevel): NativeAppLineRead {
69
+ return { level: /\bpages from\b/.test(line) ? "debug" : level, message: line };
70
+ }
71
+ }
@@ -0,0 +1,414 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import type { AkanNativeTarget } from "../akanConfig";
5
+ import { tempDirs } from "../testHelpers";
6
+ import { NativeApi } from "./nativeApi";
7
+ import { NativeConfig } from "./nativeConfig";
8
+
9
+ const makeTempRoot = tempDirs("akan-native-config-");
10
+ const repoApp = path.resolve(import.meta.dir, "../../../../apps/minimal");
11
+
12
+ const minimalTarget: AkanNativeTarget = {
13
+ name: "default",
14
+ indexPath: "/explore",
15
+ appName: "minimal",
16
+ appId: "com.minimal.dev.app",
17
+ version: "0.0.1",
18
+ buildNum: 1,
19
+ permissions: ["push"],
20
+ deepLinks: { schemes: ["minimal"], domains: ["example.com"] },
21
+ ios: { teamId: "TEAMID" },
22
+ android: { googleServices: "secrets/google-services.json", sha256CertFingerprints: ["00:11"] },
23
+ };
24
+
25
+ const adminTarget: AkanNativeTarget = {
26
+ name: "admin",
27
+ basePath: "admin",
28
+ appName: "Portal Admin",
29
+ appId: "com.portal.admin",
30
+ fileName: "portal-admin",
31
+ version: "2.1.0",
32
+ buildNum: 42,
33
+ icon: "assets/icon.png",
34
+ splash: "assets/splash.png",
35
+ permissions: ["camera", "location", "contacts", "speech"],
36
+ deepLinks: { domains: ["portal.example"] },
37
+ plugins: ["iap"],
38
+ ios: { infoPlist: { ITSAppUsesNonExemptEncryption: false } },
39
+ android: {
40
+ manifest: ['<queries><package android:name="com.kakao.talk" /></queries>'],
41
+ files: { "res/raw/chime.mp3": "assets/chime.mp3" },
42
+ },
43
+ };
44
+
45
+ const updatesKey = Buffer.alloc(32, 7).toString("base64");
46
+
47
+ describe("NativeConfig.build", () => {
48
+ test("turns the minimal app's push target into the runtime's config, single-instance carrying its link scheme", () => {
49
+ const { config, warnings } = NativeConfig.build({
50
+ appPath: "/repo/apps/minimal",
51
+ target: minimalTarget,
52
+ webDir: "/repo/apps/minimal/.akan/native/default/web",
53
+ contributions: [{ permission: "push", plugins: ["push"] }],
54
+ locales: ["en", "ko"],
55
+ platform: "android",
56
+ });
57
+
58
+ expect(warnings).toEqual([]);
59
+ expect(config).toEqual({
60
+ app: { id: "com.minimal.dev.app", name: "minimal", fileName: "minimal", version: "0.0.1", build: 1 },
61
+ web: { dir: "/repo/apps/minimal/.akan/native/default/web" },
62
+ plugins: [...NativeConfig.basePlugins, "single-instance", "push"],
63
+ capabilities: [
64
+ {
65
+ identifier: "app",
66
+ description: "The plugins minimal ships, each with its default permissions",
67
+ permissions: [...NativeConfig.basePlugins, "single-instance", "push"].map((plugin) => `${plugin}:default`),
68
+ },
69
+ ],
70
+ deepLinks: { schemes: ["minimal"], domains: ["example.com"] },
71
+ android: {
72
+ debugAppIdSuffix: ".debug",
73
+ googleServices: path.resolve("/repo/apps/minimal", "secrets/google-services.json"),
74
+ },
75
+ keyboard: { resize: "none" },
76
+ });
77
+ });
78
+
79
+ test("falls back to the builtin permission plugins, names the app in their texts, and lists basePath links per locale", () => {
80
+ const { config, warnings } = NativeConfig.build({
81
+ appPath: "/repo/apps/portal",
82
+ target: adminTarget,
83
+ webDir: "/repo/apps/portal/.akan/native/admin/web",
84
+ contributions: [],
85
+ locales: ["en", "ko"],
86
+ platform: "android",
87
+ });
88
+
89
+ expect(warnings).toEqual(["Permission 'speech' has no native plugin yet; the app ships without it."]);
90
+ expect(config).toEqual({
91
+ app: { id: "com.portal.admin", name: "Portal Admin", fileName: "portal-admin", version: "2.1.0", build: 42 },
92
+ web: { dir: "/repo/apps/portal/.akan/native/admin/web" },
93
+ plugins: [...NativeConfig.basePlugins, "camera", "geolocation", "contacts", "iap"],
94
+ capabilities: [
95
+ {
96
+ identifier: "app",
97
+ description: "The plugins Portal Admin ships, each with its default permissions",
98
+ permissions: [...NativeConfig.basePlugins, "camera", "geolocation", "contacts", "iap"].map(
99
+ (plugin) => `${plugin}:default`,
100
+ ),
101
+ },
102
+ ],
103
+ usageDescriptions: {
104
+ NSCameraUsageDescription: "Portal Admin requires access to the camera to take photos.",
105
+ NSPhotoLibraryAddUsageDescription: "Portal Admin requires access to the photo library to take photos.",
106
+ NSPhotoLibraryUsageDescription: "Portal Admin requires access to the photo library to take photos.",
107
+ NSLocationAlwaysAndWhenInUseUsageDescription:
108
+ "Portal Admin requires access to the location to get the user's location.",
109
+ NSLocationAlwaysUsageDescription: "Portal Admin requires access to the location to get the user's location.",
110
+ NSLocationWhenInUseUsageDescription: "Portal Admin requires access to the location to get the user's location.",
111
+ NSContactsUsageDescription: "Portal Admin requires access to the contacts to find people you know.",
112
+ },
113
+ deepLinks: { domains: [{ host: "portal.example", pathPrefixes: ["/en/admin", "/ko/admin"] }] },
114
+ native: {
115
+ ios: { infoPlist: { ITSAppUsesNonExemptEncryption: false } },
116
+ android: { manifest: ['<queries><package android:name="com.kakao.talk" /></queries>'] },
117
+ resources: [{ from: path.resolve("/repo/apps/portal", "assets/chime.mp3"), to: "android/res/raw/chime.mp3" }],
118
+ },
119
+ android: { debugAppIdSuffix: ".debug" },
120
+ keyboard: { resize: "none" },
121
+ icon: path.resolve("/repo/apps/portal", "assets/icon.png"),
122
+ splash: { image: path.resolve("/repo/apps/portal", "assets/splash.png") },
123
+ });
124
+ });
125
+
126
+ test("a plugin that claims a permission replaces the builtin one and adds its Android entries", () => {
127
+ const { config } = NativeConfig.build({
128
+ appPath: "/repo/apps/portal",
129
+ target: {
130
+ ...adminTarget,
131
+ permissions: ["camera"],
132
+ plugins: undefined,
133
+ ios: undefined,
134
+ android: undefined,
135
+ icon: undefined,
136
+ splash: undefined,
137
+ },
138
+ webDir: "/web",
139
+ contributions: [
140
+ {
141
+ permission: "camera",
142
+ plugins: ["camera"],
143
+ usageDescriptions: { cameraUsageDescription: "Scan receipts with $(PRODUCT_NAME)." },
144
+ androidPermissions: ["CAMERA"],
145
+ androidFeatures: ["android.hardware.camera"],
146
+ },
147
+ { permission: "push", plugins: ["push"] },
148
+ ],
149
+ locales: ["en"],
150
+ platform: "android",
151
+ });
152
+
153
+ expect(config.plugins).toEqual([...NativeConfig.basePlugins, "camera"]);
154
+ expect(config.usageDescriptions).toEqual({ NSCameraUsageDescription: "Scan receipts with Portal Admin." });
155
+ expect(config.native).toEqual({
156
+ android: {
157
+ manifest: [
158
+ '<uses-permission android:name="android.permission.CAMERA" />',
159
+ '<uses-feature android:name="android.hardware.camera" android:required="false" />',
160
+ ],
161
+ },
162
+ });
163
+ });
164
+
165
+ test("updates bring their plugin and follow the binary's backend unless a channel is named", () => {
166
+ const build = (updates: AkanNativeTarget["updates"], env?: "main" | "develop") =>
167
+ NativeConfig.build({
168
+ appPath: "/repo/apps/portal",
169
+ target: { ...adminTarget, updates },
170
+ webDir: "/web",
171
+ contributions: [],
172
+ locales: ["en"],
173
+ platform: "android",
174
+ env,
175
+ }).config;
176
+ const url = "https://updates.example.com/portal";
177
+ expect(build({ url, publicKey: updatesKey }, "develop").updates).toEqual({
178
+ url,
179
+ publicKey: updatesKey,
180
+ channel: "develop",
181
+ });
182
+ expect(build({ url, publicKey: updatesKey, channel: "beta", readyTimeout: 15_000 }, "main").updates).toEqual({
183
+ url,
184
+ publicKey: updatesKey,
185
+ channel: "beta",
186
+ readyTimeout: 15_000,
187
+ });
188
+ expect(build({ url, publicKey: updatesKey }).updates).toEqual({ url, publicKey: updatesKey });
189
+ expect(build({ url, publicKey: updatesKey }, "main").plugins).toContain("updates");
190
+ expect(build(undefined, "main").plugins).not.toContain("updates");
191
+ });
192
+
193
+ test("push, privacy and the icon and splash objects reach the runtime, paths from the app folder", () => {
194
+ const { config } = NativeConfig.build({
195
+ appPath: "/repo/apps/portal",
196
+ target: {
197
+ ...adminTarget,
198
+ icon: { image: "assets/icon.png", backgroundColor: "#ffffff" },
199
+ splash: { backgroundColor: { light: "#ffffff", dark: "#000000" }, autoHide: false },
200
+ android: { push: { smallIcon: "assets/noti.png", color: "#ff5a5f", channel: { id: "chat", name: "Chat" } } },
201
+ ios: { privacy: { tracking: false, accessedApis: { DiskSpace: ["E174.1"] } } },
202
+ },
203
+ webDir: "/web",
204
+ contributions: [],
205
+ locales: ["en"],
206
+ platform: "android",
207
+ });
208
+ const abs = (relative: string) => path.resolve("/repo/apps/portal", relative);
209
+ expect(config.icon).toEqual({ image: abs("assets/icon.png"), backgroundColor: "#ffffff" });
210
+ expect(config.splash).toEqual({ backgroundColor: { light: "#ffffff", dark: "#000000" }, autoHide: false });
211
+ expect(config.push).toEqual({
212
+ android: { smallIcon: abs("assets/noti.png"), color: "#ff5a5f", channel: { id: "chat", name: "Chat" } },
213
+ });
214
+ expect(config.privacy).toEqual({ tracking: false, accessedApis: { DiskSpace: ["E174.1"] } });
215
+ });
216
+
217
+ test("an app id per platform gives each build its own", () => {
218
+ const idOn = (platform: "ios" | "android") =>
219
+ NativeConfig.build({
220
+ appPath: "/repo/apps/portal",
221
+ target: { ...adminTarget, appId: { ios: "com.puffinplanet.admin", android: "com.portal.admin" } },
222
+ webDir: "/web",
223
+ contributions: [],
224
+ locales: ["en"],
225
+ platform,
226
+ }).config.app.id;
227
+ expect([idOn("ios"), idOn("android")]).toEqual(["com.puffinplanet.admin", "com.portal.admin"]);
228
+ });
229
+
230
+ test("the runtime accepts both configs as they are", async () => {
231
+ const root = await makeTempRoot();
232
+ for (const file of ["secrets/google-services.json", "assets/chime.mp3"])
233
+ await mkdir(path.dirname(path.join(root, file)), { recursive: true }).then(() =>
234
+ writeFile(path.join(root, file), file.endsWith(".json") ? googleServicesJson : "x"),
235
+ );
236
+ for (const image of ["assets/icon.png", "assets/splash.png"])
237
+ await writeFile(path.join(root, image), Buffer.from(onePixelPng, "base64"));
238
+ await mkdir(path.join(root, "web"), { recursive: true });
239
+ await writeFile(path.join(root, "web/index.html"), "<html><head></head><body></body></html>");
240
+ const api = await NativeApi.load(repoApp);
241
+
242
+ const adminWithEverything: AkanNativeTarget = {
243
+ ...adminTarget,
244
+ updates: { url: "https://updates.example.com/portal", publicKey: updatesKey },
245
+ ios: { ...adminTarget.ios, privacy: { tracking: false } },
246
+ android: { ...adminTarget.android, push: { smallIcon: "assets/icon.png", color: "#ff5a5f" } },
247
+ };
248
+ for (const target of [minimalTarget, adminWithEverything]) {
249
+ const { config } = NativeConfig.build({
250
+ appPath: root,
251
+ env: "main",
252
+ target,
253
+ webDir: path.join(root, "web"),
254
+ contributions: [],
255
+ locales: ["en", "ko"],
256
+ platform: "android",
257
+ });
258
+ expect(api.validateConfig(config, { appDir: root })).toEqual([]);
259
+ }
260
+ });
261
+
262
+ test("ships the app's own native plugins by folder and names each by its id in the capability", async () => {
263
+ const root = await makeTempRoot();
264
+ const kiosk = await writeManifest(path.join(root, "native", "kiosk"), "kiosk");
265
+ await writeManifest(path.join(root, "vendor", "led-panel"), "led-panel");
266
+ const { config } = NativeConfig.build({
267
+ appPath: root,
268
+ target: {
269
+ ...minimalTarget,
270
+ permissions: [],
271
+ plugins: ["./vendor/led-panel"],
272
+ deepLinks: undefined,
273
+ android: undefined,
274
+ },
275
+ webDir: path.join(root, "web"),
276
+ contributions: [],
277
+ locales: ["en"],
278
+ platform: "macos",
279
+ nativePlugins: [{ id: "kiosk", dir: kiosk, owner: "apps/board" }],
280
+ });
281
+
282
+ expect(config.plugins).toEqual([...NativeConfig.basePlugins, "./vendor/led-panel", kiosk]);
283
+ expect(config.capabilities?.[0]?.permissions).toEqual(
284
+ [...NativeConfig.basePlugins, "led-panel", "kiosk"].map((id) => `${id}:default`),
285
+ );
286
+ await mkdir(path.join(root, "web"), { recursive: true });
287
+ await writeFile(path.join(root, "web/index.html"), "<html><head></head><body></body></html>");
288
+ const api = await NativeApi.load(repoApp);
289
+ expect(api.validateConfig(config, { appDir: root })).toEqual([]);
290
+ });
291
+
292
+ test("ships a native/ folder once when the target also lists it by path or by id", async () => {
293
+ const root = await makeTempRoot();
294
+ const kiosk = await writeManifest(path.join(root, "native", "kiosk"), "kiosk");
295
+ const { config } = NativeConfig.build({
296
+ appPath: root,
297
+ target: {
298
+ ...minimalTarget,
299
+ permissions: [],
300
+ plugins: ["./native/kiosk/", "kiosk", "haptics"],
301
+ deepLinks: undefined,
302
+ android: undefined,
303
+ },
304
+ webDir: path.join(root, "web"),
305
+ contributions: [],
306
+ locales: ["en"],
307
+ platform: "macos",
308
+ nativePlugins: [{ id: "kiosk", dir: kiosk, owner: "apps/board" }],
309
+ });
310
+
311
+ expect(config.plugins).toEqual([...NativeConfig.basePlugins, kiosk]);
312
+ });
313
+
314
+ test("hands an unattended app's settings on: page recovery, a kiosk window, screen capture and Android autoplay", async () => {
315
+ const root = await makeTempRoot();
316
+ const { config } = NativeConfig.build({
317
+ appPath: root,
318
+ target: {
319
+ ...minimalTarget,
320
+ permissions: [],
321
+ deepLinks: undefined,
322
+ android: { autoplay: true },
323
+ desktop: { recovery: "reload", window: { fullscreen: true, skipTaskbar: true }, screenCapture: "auto" },
324
+ },
325
+ webDir: path.join(root, "web"),
326
+ contributions: [],
327
+ locales: ["en"],
328
+ platform: "macos",
329
+ desktopBin: root,
330
+ });
331
+
332
+ expect(config.desktop).toEqual({
333
+ recovery: "reload",
334
+ window: { fullscreen: true, skipTaskbar: true },
335
+ screenCapture: "auto",
336
+ bin: root,
337
+ });
338
+ expect(config.android).toEqual({ debugAppIdSuffix: ".debug", autoplay: true });
339
+ await mkdir(path.join(root, "web"), { recursive: true });
340
+ await writeFile(path.join(root, "web/index.html"), "<html><head></head><body></body></html>");
341
+ expect((await NativeApi.load(repoApp)).validateConfig(config, { appDir: root })).toEqual([]);
342
+ });
343
+
344
+ test("an app with updates ships the updates plugin and its settings", () => {
345
+ const updates = { url: "https://releases.example.com/board", publicKey: `${"a".repeat(43)}=`, channel: "pilot" };
346
+ const { config } = NativeConfig.build({
347
+ appPath: "/repo/apps/board",
348
+ target: { ...minimalTarget, permissions: [], deepLinks: undefined, android: undefined, updates },
349
+ webDir: "/web",
350
+ contributions: [],
351
+ locales: ["en"],
352
+ platform: "macos",
353
+ });
354
+
355
+ expect(config.plugins).toEqual([...NativeConfig.basePlugins, "updates"]);
356
+ expect(config.updates).toEqual(updates);
357
+ });
358
+
359
+ test("an updates channel left unnamed is the backend env the binary is built for", () => {
360
+ const updates = { url: "https://releases.example.com/board", publicKey: `${"a".repeat(43)}=` };
361
+ const build = (env?: "debug" | "main", channel?: string) =>
362
+ NativeConfig.build({
363
+ appPath: "/repo/apps/board",
364
+ target: {
365
+ ...minimalTarget,
366
+ permissions: [],
367
+ deepLinks: undefined,
368
+ android: undefined,
369
+ updates: { ...updates, ...(channel ? { channel } : {}) },
370
+ },
371
+ webDir: "/web",
372
+ contributions: [],
373
+ locales: ["en"],
374
+ platform: "macos",
375
+ ...(env ? { env } : {}),
376
+ }).config.updates;
377
+
378
+ expect(build("debug")).toEqual({ ...updates, channel: "debug" });
379
+ expect(build("main")).toEqual({ ...updates, channel: "main" });
380
+ expect(build("debug", "pilot")).toEqual({ ...updates, channel: "pilot" });
381
+ expect(build()).toEqual(updates);
382
+ });
383
+
384
+ test("keeps a file name the runtime accepts", () => {
385
+ expect(NativeConfig.fileNameOf("minimal")).toBe("minimal");
386
+ expect(NativeConfig.fileNameOf("my app!")).toBe("my-app");
387
+ expect(NativeConfig.fileNameOf("...")).toBe("app");
388
+ });
389
+ });
390
+
391
+ const writeManifest = async (dir: string, id: string) => {
392
+ await mkdir(dir, { recursive: true });
393
+ await writeFile(
394
+ path.join(dir, "native-plugin.json"),
395
+ JSON.stringify({ id, apiVersion: 1, methods: [], events: [], web: null, desktop: null, ios: null, android: null }),
396
+ );
397
+ return dir;
398
+ };
399
+
400
+ const onePixelPng = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
401
+
402
+ const googleServicesJson = JSON.stringify({
403
+ project_info: { project_number: "1", project_id: "demo", storage_bucket: "demo.appspot.com" },
404
+ client: [
405
+ {
406
+ client_info: { mobilesdk_app_id: "1:1:android:1", android_client_info: { package_name: "com.minimal.dev.app" } },
407
+ api_key: [{ current_key: "demo-key" }],
408
+ },
409
+ {
410
+ client_info: { mobilesdk_app_id: "1:1:android:2", android_client_info: { package_name: "com.portal.admin" } },
411
+ api_key: [{ current_key: "demo-key" }],
412
+ },
413
+ ],
414
+ });