@akanjs/devkit 3.0.0-beta.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  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 +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -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 +270 -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 +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -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 +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -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 +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  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
@@ -2,17 +2,25 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import type { AkanPlugin } from "akanjs";
5
- import { normalizeRoutePrefix } from "akanjs/base";
5
+ import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
6
6
  import { type AkanI18nConfig, resolveAkanI18nConfig } from "akanjs/common";
7
7
  import type { AkanImageConfig } from "akanjs/server";
8
8
  import type { App, Lib } from "../commandDecorators";
9
9
  import { LibExecutor, WorkspaceExecutor } from "../executors";
10
10
  import type { BaseDevEnv, PackageJson } from "../types";
11
+ import { AkanBin } from "./akanBin";
11
12
  import {
12
13
  type AkanApiConfig,
13
14
  type AkanAssetsConfig,
14
- type AkanMobileConfig,
15
- type AkanMobileTargetConfig,
15
+ type AkanBinConfig,
16
+ type AkanDatabaseConfig,
17
+ type AkanNativeAppConfig,
18
+ type AkanNativeAppResult,
19
+ type AkanNativeDeepLinks,
20
+ type AkanNativeDesktopConfig,
21
+ type AkanNativeSettings,
22
+ type AkanNativeTarget,
23
+ type AkanNativeUpdatesConfig,
16
24
  type AkanRouteConfig,
17
25
  type AkanWebConfig,
18
26
  type AkanWebOption,
@@ -31,78 +39,14 @@ import {
31
39
 
32
40
  const DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
33
41
  const DEFAULT_OPTIMIZE_IMPORTS = [
34
- "lucide-react",
35
- "date-fns",
36
- "lodash-es",
37
- "ramda",
38
- "antd",
39
- "react-bootstrap",
40
- "ahooks",
41
- "@ant-design/icons",
42
- "@headlessui/react",
43
- "@headlessui-float/react",
44
- "@heroicons/react/20/solid",
45
- "@heroicons/react/24/solid",
46
- "@heroicons/react/24/outline",
47
- "@visx/visx",
48
- "@tremor/react",
49
- "rxjs",
50
- "@mui/material",
51
- "@mui/icons-material",
52
- "recharts",
53
- "react-use",
54
- "@material-ui/core",
55
- "@material-ui/icons",
56
- "@tabler/icons-react",
57
- "mui-core",
58
- "react-icons/*",
59
- ];
42
+ "lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
43
+ "@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
44
+ "@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
45
+ "@material-ui/icons @tabler/icons-react mui-core react-icons/*",
46
+ ].flatMap((line) => line.split(" "));
60
47
  const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
61
48
  const DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
62
49
  const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
63
- // The firebase client (push tokens) and the Capacitor toolchain — `@capacitor/cli` (`npx cap`),
64
- // `@capacitor/assets` (`npx @capacitor/assets`) plus the `@capacitor/core`/`ios`/`android` runtime
65
- // and native-platform packages that `npx cap add`/`sync` resolve from the workspace node_modules.
66
- // All are declared only as optional peers, so a fresh workspace never auto-installs them; the mobile
67
- // preflight installs them (together with MOBILE_APP_CAPACITOR_PLUGINS) at the workspace root.
68
- const MOBILE_RUNTIME_PACKAGES = [
69
- "@capacitor/cli",
70
- "@capacitor/core",
71
- "@capacitor/ios",
72
- "@capacitor/android",
73
- "@capacitor/assets",
74
- ] as const;
75
- // Capacitor plugins that must additionally be declared in the *app's* package.json
76
- // (apps/<app>/package.json): `npx cap sync` discovers plugins by scanning the app directory's
77
- // dependencies and registers their native code into the iOS/Android projects; packages present only
78
- // at the workspace root are never registered, so the JS bridge throws
79
- // `Capacitor plugin "Device" is not available.` at runtime. They are installed at the workspace root
80
- // alongside MOBILE_RUNTIME_PACKAGES (pinned via optional peers) and declared in the app with a "*"
81
- // range so bun dedupes them to that hoisted version instead of pinning a second source of truth.
82
- const MOBILE_APP_CAPACITOR_PLUGINS = [
83
- "@capacitor/app",
84
- "@capacitor/browser",
85
- "@capacitor/camera",
86
- "@capacitor/core",
87
- "@capacitor/device",
88
- "@capacitor/geolocation",
89
- "@capacitor/haptics",
90
- "@capacitor/inappbrowser",
91
- "@capacitor/keyboard",
92
- "@capacitor/preferences",
93
- "@capacitor/push-notifications",
94
- "capacitor-plugin-safe-area",
95
- ] as const;
96
- const DATABASE_MODE_RUNTIME_PACKAGES = {
97
- single: [],
98
- multiple: ["@libsql/client", "bullmq", "ioredis", "protobufjs"],
99
- cluster: ["bullmq", "ioredis", "postgres", "protobufjs"],
100
- } satisfies Record<DatabaseMode, readonly string[]>;
101
- const AKAN_RUNTIME_PACKAGES = new Set<string>([
102
- ...SSR_RUNTIME_PACKAGES,
103
- ...MOBILE_RUNTIME_PACKAGES,
104
- ...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
105
- ]);
106
50
  const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
107
51
  deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
108
52
  imageSizes: [32, 48, 64, 96, 128, 256, 384],
@@ -130,18 +74,14 @@ const normalizeStringList = (values: string[] | undefined) => {
130
74
  return normalized.length > 0 ? [...new Set(normalized)] : undefined;
131
75
  };
132
76
 
133
- // Reduce a free-form name to a valid reverse-DNS / Android package segment: lowercase, alphanumerics
134
- // only (hyphens/spaces dropped), never empty, never starting with a digit.
77
+ // A reverse-DNS / Android package segment: lowercase alphanumerics, never empty, never starting with a digit.
135
78
  const sanitizeAppIdSegment = (value: string): string => {
136
79
  const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
137
80
  if (!cleaned) return "app";
138
81
  return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
139
82
  };
140
83
 
141
- // The default mobile bundle id for an app that has not pinned `mobile.appId`. Uses the workspace
142
- // (repo) name as the org segment so the default is far less collision-prone than a bare
143
- // `com.<appName>.app`, which Apple's portal routinely already has claimed. Apps that ship pin an
144
- // explicit appId, so this only affects unconfigured/dev apps.
84
+ // The repo name is the org segment: a bare `com.<appName>.app` is routinely already claimed on Apple's portal.
145
85
  export const deriveDefaultAppId = (orgName: string, appName: string): string =>
146
86
  `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
147
87
 
@@ -159,30 +99,27 @@ const normalizeDeepLinkDomain = (domain: string) => {
159
99
  }
160
100
  };
161
101
 
162
- const normalizeDeepLinks = (deepLinks: DeepPartial<AkanMobileTargetConfig["deepLinks"]> | undefined) => {
163
- if (!deepLinks) return undefined;
164
- const schemes = normalizeStringList(deepLinks.schemes as string[] | undefined);
165
- const domains = normalizeStringList((deepLinks.domains as string[] | undefined)?.map(normalizeDeepLinkDomain));
166
- const teamId = (deepLinks.ios?.teamId as string | undefined)?.trim();
167
- const sha256CertFingerprints = normalizeStringList(deepLinks.android?.sha256CertFingerprints as string[] | undefined);
168
- if (!schemes && !domains && !teamId && !sha256CertFingerprints) return undefined;
169
- return {
170
- ...(schemes ? { schemes } : {}),
171
- ...(domains ? { domains } : {}),
172
- ...(teamId ? { ios: { teamId } } : {}),
173
- ...(sha256CertFingerprints ? { android: { sha256CertFingerprints } } : {}),
174
- } satisfies AkanMobileTargetConfig["deepLinks"];
102
+ const normalizeDeepLinks = (deepLinks: AkanNativeDeepLinks | undefined): AkanNativeDeepLinks | undefined => {
103
+ const schemes = normalizeStringList(deepLinks?.schemes);
104
+ const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
105
+ if (!schemes && !domains) return undefined;
106
+ return { ...(schemes ? { schemes } : {}), ...(domains ? { domains } : {}) };
175
107
  };
176
108
 
177
- /** What `akan.config.ts` may write: the resolved shape made partial, with `docker` and `web` in their unions. */
178
- type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web"> & {
109
+ type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web" | "bin" | "native"> & {
179
110
  docker?: DockerOption;
180
111
  web?: AkanWebOption;
112
+ bin?: AkanBinConfig;
113
+ native?: AkanNativeAppConfig;
181
114
  };
182
115
 
183
- /** What the workspace's libs add to an app's build, read off each `libs/<lib>/akan.config.ts`. */
116
+ type LibConfigDeclaration = Omit<DeepPartial<LibConfigResult>, "bin"> & { bin?: AkanBinConfig };
117
+
184
118
  export interface LibContributions {
185
119
  externalLibs: string[];
120
+ trustedDependencies?: string[];
121
+ /** Per lib, since an app carries only the entries of the libs it depends on. */
122
+ bin?: { lib: string; bin: AkanBinConfig }[];
186
123
  docker: LibDockerConfig;
187
124
  /** Keep globs rewritten to the app's own `public/`, where `akan sync` mounts each lib's assets. */
188
125
  keepFonts?: string[];
@@ -190,10 +127,19 @@ export interface LibContributions {
190
127
 
191
128
  const emptyLibContributions = (): LibContributions => ({
192
129
  externalLibs: [],
130
+ trustedDependencies: [],
131
+ bin: [],
193
132
  docker: { preRuns: [], postRuns: [] },
194
133
  keepFonts: [],
195
134
  });
196
135
 
136
+ const normalizePackageNames = (names: unknown, owner: string): string[] => {
137
+ if (names === undefined) return [];
138
+ if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
139
+ throw new Error(`${owner}: trustedDependencies lists package names`);
140
+ return [...new Set(names.map((name: string) => name.trim()))];
141
+ };
142
+
197
143
  const normalizeKeepFonts = (keepFonts: string[] | undefined) => [
198
144
  ...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean)),
199
145
  ];
@@ -211,21 +157,25 @@ export class AkanAppConfig implements AppConfigResult {
211
157
  docker: DockerConfig;
212
158
  /** The Dockerfile `akan build` writes: the declared string verbatim, or one assembled from the parts. */
213
159
  dockerfile: string;
214
- defaultDatabaseMode: DatabaseMode;
160
+ database: AkanDatabaseConfig;
215
161
  web: AkanWebConfig;
216
162
  externalLibs: string[];
163
+ trustedDependencies: string[];
164
+ /** The app's own entries; the libs' are in `libBins`. */
165
+ bin: AkanBinConfig;
166
+ libBins: { lib: string; bin: AkanBinConfig }[];
217
167
  barrelImports: string[];
218
168
  optimizeImports: string[];
219
169
  images: AkanImageConfig;
220
170
  i18n: AkanI18nConfig;
221
171
  api: AkanApiConfig;
222
172
  publicEnv: string[];
223
- mobile: AkanMobileConfig;
224
- /** True only when the app's akan.config.ts explicitly declares a `mobile` section (vs. the synthesized default). */
225
- hasMobileConfig: boolean;
173
+ native: AkanNativeAppResult;
174
+ /** True only when the app's akan.config.ts declares a `native` section (vs. the synthesized default target). */
175
+ hasNativeConfig: boolean;
226
176
  secrets: string[];
227
177
  assets: AkanAssetsConfig;
228
- /** Raw setting; resolved against the app's lib deps at sync time (see `AppExecutor.syncPages`). */
178
+ /** Raw setting, resolved against the app's lib deps at sync time. */
229
179
  syncPageLibs: string[] | boolean;
230
180
  baseDevEnv: BaseDevEnv;
231
181
  libs: string[];
@@ -250,8 +200,17 @@ export class AkanAppConfig implements AppConfigResult {
250
200
  this.baseDevEnv = baseDevEnv;
251
201
  this.plugins = plugins;
252
202
  this.#applyRoutes(config?.routes);
253
- this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
203
+ this.database = AkanAppConfig.#database(app, config);
254
204
  this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...libContributions.externalLibs])];
205
+ const owner = `apps/${app.name}/akan.config.ts`;
206
+ this.trustedDependencies = [
207
+ ...new Set([
208
+ ...normalizePackageNames(config?.trustedDependencies, owner),
209
+ ...(libContributions.trustedDependencies ?? []),
210
+ ]),
211
+ ];
212
+ this.bin = AkanBin.parse(config?.bin, owner, app.cwdPath);
213
+ this.libBins = libContributions.bin ?? [];
255
214
  this.barrelImports = [
256
215
  ...DEFAULT_BARREL_IMPORTS,
257
216
  ...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
@@ -279,8 +238,12 @@ export class AkanAppConfig implements AppConfigResult {
279
238
  ],
280
239
  };
281
240
  this.syncPageLibs = (config?.syncPageLibs as string[] | boolean | undefined) ?? false;
282
- this.hasMobileConfig = Boolean(config.mobile);
283
- this.mobile = this.#resolveMobileConfig(config.mobile);
241
+ if ((config as { mobile?: unknown }).mobile !== undefined)
242
+ throw new Error(
243
+ `apps/${this.app.name}/akan.config.ts declares \`mobile\`, which is now \`native\`: the platform settings sit in native.ios, native.android and native.desktop, and a target overrides them under native.targets.<name>.`,
244
+ );
245
+ this.hasNativeConfig = Boolean(config.native);
246
+ this.native = this.#resolveNativeConfig(config.native);
284
247
  this.web = this.#resolveWebConfig(config.web);
285
248
  this.docker = AkanAppConfig.#resolveDocker(config.docker, libContributions.docker);
286
249
  this.dockerfile = this.#makeDockerfile();
@@ -288,101 +251,234 @@ export class AkanAppConfig implements AppConfigResult {
288
251
  #resolveWebConfig(web: AkanWebOption | undefined): AkanWebConfig {
289
252
  const resolved = typeof web === "object" ? { ssr: true, csr: web.csr } : { ssr: web ?? true, csr: web ?? true };
290
253
  // `akan build-ios` / `build-android` copy `dist/apps/<app>/csr/<target>.html` into the native project.
291
- if (!resolved.csr && this.hasMobileConfig)
254
+ if (!resolved.csr && this.hasNativeConfig)
292
255
  throw new Error(
293
- `apps/${this.app.name}/akan.config.ts turns the CSR bundle off but declares mobile targets; the Capacitor build ships that bundle. Drop the mobile section or leave CSR on.`,
256
+ `apps/${this.app.name}/akan.config.ts turns the CSR bundle off but declares a native section; the native apps ship that bundle. Drop the native section or leave CSR on.`,
294
257
  );
295
258
  return resolved;
296
259
  }
297
- #resolveMobileConfig(mobile: DeepPartial<AkanMobileConfig> | undefined): AkanMobileConfig {
298
- const {
299
- targets: rawTargets,
300
- indexPath: _indexPath,
301
- ...rawMobile
302
- } = (mobile ?? {}) as DeepPartial<AkanMobileConfig> & { indexPath?: unknown };
303
- const appName = rawMobile.appName ?? this.app.name;
304
- const appId = rawMobile.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
305
- const version = rawMobile.version ?? "0.0.1";
306
- const buildNum = rawMobile.buildNum ?? 1;
307
- const defaultTargetName = this.#defaultMobileTargetName(rawTargets);
308
- const targetEntries = Object.entries(
309
- rawTargets ?? {
310
- [defaultTargetName]: {},
311
- },
312
- );
260
+ #resolveNativeConfig(native: AkanNativeAppConfig | undefined): AkanNativeAppResult {
261
+ const configPath = `apps/${this.app.name}/akan.config.ts`;
262
+ const { targets: rawTargets, ...root } = native ?? {};
263
+ AkanAppConfig.#assertNativeKeys(root, "native", configPath);
264
+ AkanAppConfig.#assertFiles(root, "native", configPath);
265
+ const appName = root.appName ?? this.app.name;
266
+ const appId = root.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
267
+ const version = root.version ?? "0.0.1";
268
+ const buildNum = root.buildNum ?? 1;
269
+ const named = rawTargets !== undefined && Object.keys(rawTargets).length > 0;
270
+ const entries: [string, AkanNativeSettings][] = named
271
+ ? Object.entries(rawTargets)
272
+ : [[this.basePaths.has(this.app.name) ? this.app.name : "default", {}]];
313
273
  const targets = Object.fromEntries(
314
- targetEntries.map(([name, rawTarget]) => {
315
- const target = rawTarget as DeepPartial<AkanMobileTargetConfig>;
316
- const fallbackBasePath = !rawTargets && this.basePaths.has(name) ? name : undefined;
317
- const basePath = (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
318
- const indexPath = normalizeIndexPath(target.indexPath as string | undefined);
319
- const deepLinks = normalizeDeepLinks(target.deepLinks);
320
- if (basePath && !this.basePaths.has(basePath)) {
274
+ entries.map(([name, own]) => {
275
+ const where = named ? `native.targets.${name}` : "native";
276
+ AkanAppConfig.#assertNativeKeys(own ?? {}, where, configPath);
277
+ const merged = AkanAppConfig.#overlay(root, own ?? {});
278
+ const fallbackBasePath = !named && this.basePaths.has(name) ? name : undefined;
279
+ const basePath = (merged.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
280
+ if (basePath && !this.basePaths.has(basePath))
281
+ throw new Error(`Native target '${name}' uses unknown basePath '${basePath}' in ${configPath}`);
282
+ const { updates } = merged;
283
+ if (updates && (!updates.url || !updates.publicKey))
321
284
  throw new Error(
322
- `Mobile target '${name}' uses unknown basePath '${basePath}' in apps/${this.app.name}/akan.config.ts`,
285
+ `${where}.updates in ${configPath} has no url or publicKey${named ? "; give them in native.updates or the target's own" : ""}.`,
323
286
  );
324
- }
325
- const resolved = {
326
- ...rawMobile,
327
- ...target,
287
+ AkanAppConfig.#assertFiles(own ?? {}, where, configPath);
288
+ const {
289
+ basePath: _basePath,
290
+ indexPath: rawIndexPath,
291
+ deepLinks: rawDeepLinks,
292
+ updates: _updates,
293
+ ...rest
294
+ } = merged;
295
+ const indexPath = normalizeIndexPath(rawIndexPath);
296
+ const deepLinks = normalizeDeepLinks(rawDeepLinks);
297
+ const { teamId: rawTeamId, ...iosRest } = AkanAppConfig.#platformIndexPath(merged.ios ?? {});
298
+ const teamId = rawTeamId?.trim();
299
+ const { sha256CertFingerprints: rawFingerprints, ...androidRest } = AkanAppConfig.#platformIndexPath(
300
+ merged.android ?? {},
301
+ );
302
+ const fingerprints = normalizeStringList(rawFingerprints);
303
+ const target: AkanNativeTarget = {
304
+ ...rest,
328
305
  name,
329
- basePath,
330
- indexPath,
331
- deepLinks,
332
- appName: target.appName ?? appName,
333
- appId: target.appId ?? appId,
334
- version: target.version ?? version,
335
- buildNum: target.buildNum ?? buildNum,
336
- plugins: {
337
- ...rawMobile.plugins,
338
- ...target.plugins,
339
- },
340
- android: {
341
- ...rawMobile.android,
342
- ...target.android,
343
- },
344
- ios: {
345
- ...rawMobile.ios,
346
- ...target.ios,
347
- },
348
- } satisfies AkanMobileTargetConfig;
349
- return [name, resolved];
306
+ ...(basePath ? { basePath } : {}),
307
+ ...(indexPath ? { indexPath } : {}),
308
+ ...(deepLinks ? { deepLinks } : {}),
309
+ ...(merged.ios ? { ios: { ...iosRest, ...(teamId ? { teamId } : {}) } } : {}),
310
+ ...(merged.android
311
+ ? { android: { ...androidRest, ...(fingerprints ? { sha256CertFingerprints: fingerprints } : {}) } }
312
+ : {}),
313
+ ...(merged.desktop
314
+ ? {
315
+ desktop: AkanAppConfig.#desktopServer(
316
+ AkanAppConfig.#platformIndexPath(merged.desktop),
317
+ `${where}.desktop.server`,
318
+ configPath,
319
+ ),
320
+ }
321
+ : {}),
322
+ appName: merged.appName ?? this.app.name,
323
+ appId: merged.appId ?? appId,
324
+ version: merged.version ?? version,
325
+ buildNum: merged.buildNum ?? buildNum,
326
+ ...(updates ? { updates: updates as AkanNativeUpdatesConfig } : {}),
327
+ };
328
+ return [name, target];
350
329
  }),
351
330
  );
352
- return {
353
- ...rawMobile,
354
- appName,
355
- appId,
356
- version,
357
- buildNum,
358
- targets,
359
- plugins: rawMobile.plugins,
360
- } as AkanMobileConfig;
361
- }
362
- #defaultMobileTargetName(rawTargets: DeepPartial<AkanMobileConfig>["targets"] | undefined) {
363
- if (rawTargets && Object.keys(rawTargets).length > 0) return Object.keys(rawTargets)[0] as string;
364
- return this.basePaths.has(this.app.name) ? this.app.name : "default";
331
+ return { appName, appId, ...(root.fileName ? { fileName: root.fileName } : {}), version, buildNum, targets };
332
+ }
333
+ //? `true` or `{ omit }`: each omitted name trimmed once, so two targets that omit the same packages stage one server.
334
+ static #desktopServer<T extends { server?: AkanNativeDesktopConfig["server"] }>(
335
+ desktop: T,
336
+ where: string,
337
+ configPath: string,
338
+ ): T {
339
+ const { server } = desktop;
340
+ if (server === undefined || typeof server === "boolean") return desktop;
341
+ if (!AkanAppConfig.#isPlainObject(server))
342
+ throw new Error(`${where} in ${configPath} must be true or { omit: string[] }.`);
343
+ const omit = server.omit;
344
+ if (omit !== undefined && (!Array.isArray(omit) || omit.some((name) => typeof name !== "string" || !name.trim())))
345
+ throw new Error(`${where}.omit in ${configPath} must be a list of package names.`);
346
+ const names = [...new Set((omit ?? []).map((name) => name.trim()))].sort();
347
+ return { ...desktop, server: { omit: names } };
348
+ }
349
+ static #platformIndexPath<T extends { indexPath?: string }>(section: T): T {
350
+ const { indexPath: rawIndexPath, ...rest } = section;
351
+ const indexPath = normalizeIndexPath(rawIndexPath);
352
+ return { ...rest, ...(indexPath ? { indexPath } : {}) } as T;
353
+ }
354
+ /** `own` over `base`: plain objects merge key by key, and any other value, a list included, replaces. */
355
+ static #overlay(base: AkanNativeSettings, own: AkanNativeSettings): AkanNativeSettings {
356
+ const merge = (kept: unknown, value: unknown, top: boolean, key: string): unknown => {
357
+ if (!AkanAppConfig.#isPlainObject(kept) || !AkanAppConfig.#isPlainObject(value)) return value;
358
+ //? An icon or splash given as an object is one value: its image and color go together.
359
+ if (top && (key === "icon" || key === "splash")) return value;
360
+ const out: Record<string, unknown> = { ...kept };
361
+ for (const [inner, next] of Object.entries(value))
362
+ if (next !== undefined) out[inner] = merge(out[inner], next, false, inner);
363
+ return out;
364
+ };
365
+ const out: Record<string, unknown> = { ...base };
366
+ for (const [key, value] of Object.entries(own))
367
+ if (value !== undefined) out[key] = merge(out[key], value, true, key);
368
+ return out as AkanNativeSettings;
369
+ }
370
+ static #isPlainObject(value: unknown): value is Record<string, unknown> {
371
+ return typeof value === "object" && value !== null && !Array.isArray(value);
372
+ }
373
+ static readonly #nativeKeys = {
374
+ native: [
375
+ "basePath",
376
+ "indexPath",
377
+ "appName",
378
+ "appId",
379
+ "fileName",
380
+ "version",
381
+ "buildNum",
382
+ "icon",
383
+ "splash",
384
+ "permissions",
385
+ "plugins",
386
+ "deepLinks",
387
+ "updates",
388
+ "ios",
389
+ "android",
390
+ "desktop",
391
+ ],
392
+ ios: ["indexPath", "teamId", "infoPlist", "entitlements", "privacy", "files"],
393
+ android: [
394
+ "indexPath",
395
+ "sha256CertFingerprints",
396
+ "googleServices",
397
+ "push",
398
+ "autoplay",
399
+ "manifest",
400
+ "application",
401
+ "activity",
402
+ "files",
403
+ ],
404
+ desktop: ["indexPath", "server", "recovery", "window", "screenCapture", "entitlements"],
405
+ desktopServer: ["omit"],
406
+ deepLinks: ["schemes", "domains"],
407
+ } as const;
408
+ //* Settings that moved when `mobile` became `native` are named with where they went, instead of "unknown".
409
+ static readonly #movedKeys: Readonly<Record<string, string>> = {
410
+ native: "the platform sections and plugins sit directly in the native section: ios, android, desktop, plugins",
411
+ assets: "icon and splash sit directly in the native section",
412
+ files:
413
+ "ios.files (keyed by the path in the app bundle) or android.files (keyed res/<type>/<file> or assets/<path>)",
414
+ push: "android.push",
415
+ privacy: "ios.privacy",
416
+ "deepLinks.ios": "ios.teamId",
417
+ "deepLinks.android": "android.sha256CertFingerprints",
418
+ };
419
+ static #assertNativeKeys(section: object, where: string, configPath: string) {
420
+ const check = (value: unknown, known: readonly string[], at: string, kind: string) => {
421
+ if (!AkanAppConfig.#isPlainObject(value)) return;
422
+ for (const key of Object.keys(value)) {
423
+ if (known.includes(key)) continue;
424
+ const moved = AkanAppConfig.#movedKeys[kind === "native" ? key : `${kind}.${key}`];
425
+ throw new Error(
426
+ moved
427
+ ? `${at}.${key} in ${configPath} has moved: ${moved}.`
428
+ : `${at}.${key} in ${configPath} is not a native setting. Known: ${known.join(", ")}.`,
429
+ );
430
+ }
431
+ };
432
+ check(section, AkanAppConfig.#nativeKeys.native, where, "native");
433
+ const settings = section as AkanNativeSettings;
434
+ check(settings.ios, AkanAppConfig.#nativeKeys.ios, `${where}.ios`, "ios");
435
+ check(settings.android, AkanAppConfig.#nativeKeys.android, `${where}.android`, "android");
436
+ check(settings.desktop, AkanAppConfig.#nativeKeys.desktop, `${where}.desktop`, "desktop");
437
+ check(
438
+ settings.desktop?.server,
439
+ AkanAppConfig.#nativeKeys.desktopServer,
440
+ `${where}.desktop.server`,
441
+ "desktopServer",
442
+ );
443
+ check(settings.deepLinks, AkanAppConfig.#nativeKeys.deepLinks, `${where}.deepLinks`, "deepLinks");
444
+ }
445
+ static readonly #androidFilePattern = /^(?:res\/[^/]+\/[^/]+|assets\/.+)$/;
446
+ static #assertFiles(settings: AkanNativeSettings, where: string, configPath: string) {
447
+ const check = (
448
+ files: Record<string, string> | undefined,
449
+ at: string,
450
+ valid: (to: string) => boolean,
451
+ shape: string,
452
+ ) => {
453
+ for (const [to, from] of Object.entries(files ?? {})) {
454
+ if (typeof from !== "string")
455
+ throw new Error(
456
+ `${at}["${to}"] in ${configPath} must name its source file, relative to the app folder, e.g. { "${shape}": "assets/chime.mp3" }.`,
457
+ );
458
+ if (!valid(to)) throw new Error(`${at}["${to}"] in ${configPath} must land at ${shape}.`);
459
+ }
460
+ };
461
+ check(
462
+ settings.ios?.files,
463
+ `${where}.ios.files`,
464
+ (to) => to.length > 0 && !to.startsWith("/") && !to.split("/").includes(".."),
465
+ "<path in the app bundle>",
466
+ );
467
+ check(
468
+ settings.android?.files,
469
+ `${where}.android.files`,
470
+ (to) => AkanAppConfig.#androidFilePattern.test(to),
471
+ "res/<type>/<file> or assets/<path>",
472
+ );
365
473
  }
366
474
  #applyRoutes(routes: AkanRouteConfig[] = []) {
367
475
  for (const route of routes) {
368
- if (route.basePath) {
369
- const basePath = route.basePath.replace(/^\/+|\/+$/g, "");
370
- this.basePaths.add(basePath);
371
- const domains = this.subRoutes.getOrInsert(basePath, new Set());
372
- Object.keys(route.domains).forEach((branch) => void this.branches.add(branch));
373
- Object.values(route.domains)
374
- .flat()
375
- .forEach((domain) => {
376
- if (domain) domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
377
- });
378
- } else {
379
- Object.keys(route.domains).forEach((branch) => void this.branches.add(branch));
380
- Object.values(route.domains)
381
- .flat()
382
- .forEach((domain) => {
383
- if (domain) this.domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
384
- });
385
- }
476
+ const basePath = route.basePath ? route.basePath.replace(/^\/+|\/+$/g, "") : null;
477
+ if (basePath !== null) this.basePaths.add(basePath);
478
+ const domains = basePath !== null ? this.subRoutes.getOrInsert(basePath, new Set()) : this.domains;
479
+ for (const branch of Object.keys(route.domains)) this.branches.add(branch);
480
+ for (const domain of Object.values(route.domains).flat())
481
+ if (domain) domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
386
482
  }
387
483
  const appName = this.app.name.toLowerCase();
388
484
  const serveDomain = this.baseDevEnv.serveDomain.toLowerCase();
@@ -420,21 +516,39 @@ export class AkanAppConfig implements AppConfigResult {
420
516
  command: docker?.command ?? ["bun", "main.js"],
421
517
  };
422
518
  }
423
- #makeDockerfile(): string {
519
+ /** What the built server runs with in its image; a desktop app's carried server starts from the same values. */
520
+ getProductionEnv(environment: string = this.baseDevEnv.env): Record<string, string> {
521
+ return {
522
+ PORT: "8282",
523
+ NODE_ENV: "production",
524
+ AKAN_PUBLIC_REPO_NAME: this.baseDevEnv.repoName,
525
+ AKAN_PUBLIC_SERVE_DOMAIN: this.baseDevEnv.serveDomain,
526
+ AKAN_PUBLIC_APP_NAME: this.app.name,
527
+ AKAN_PUBLIC_ENV: environment,
528
+ ...(this.basePaths.size ? { AKAN_PUBLIC_BASE_PATHS: [...this.basePaths].join(",") } : {}),
529
+ AKAN_PUBLIC_DEFAULT_LOCALE: this.i18n.defaultLocale,
530
+ AKAN_PUBLIC_LOCALES: this.i18n.locales.join(","),
531
+ AKAN_PUBLIC_API_PREFIX: this.api.prefix,
532
+ AKAN_PUBLIC_WS_PREFIX: this.api.websocketPrefix,
533
+ AKAN_PUBLIC_OPERATION_MODE: "cloud",
534
+ AKAN_DATABASE_MODES: this.database.modes.join(","),
535
+ // File logging is off: a container's writable layer is ephemeral and stdout is the collection path.
536
+ AKAN_LOG_TO_FILE: "0",
537
+ // The web env matches what was built (a deployment can only narrow it).
538
+ ...(this.web.ssr ? {} : { AKAN_SSR: "false" }),
539
+ ...(this.web.csr ? {} : { AKAN_CSR: "false" }),
540
+ };
541
+ }
542
+ dockerfileFor(environment: string): string {
543
+ return this.#makeDockerfile(environment);
544
+ }
545
+ #makeDockerfile(environment?: string): string {
424
546
  if (typeof this.docker === "string") return this.docker;
425
547
  const { image, preRuns, postRuns, command } = this.docker;
426
548
  const preRunScripts = this.#getDockerRunScripts(preRuns);
427
549
  const postRunScripts = this.#getDockerRunScripts(postRuns);
428
550
  const imageScript = this.#getDockerImageScript(image, DEFAULT_DOCKER_IMAGE);
429
- // The image default matches what the build actually produced; a deployment narrows it further with its
430
- // own env, and can never widen it past the artifacts that are in the image.
431
- // File logging is off in the image: a container's writable layer is ephemeral and nothing collects a
432
- // file from it, so the rotating files would only fill the node disk (50MB x 100 at `trace`). stdout is
433
- // the collection path; a deployment that wants the files back sets `AKAN_LOG_TO_FILE=1`.
434
- const webEnvLines = [
435
- ...(this.web.ssr ? [] : ["ENV AKAN_SSR=false"]),
436
- ...(this.web.csr ? [] : ["ENV AKAN_CSR=false"]),
437
- ].join("\n");
551
+ const envLines = Object.entries(this.getProductionEnv(environment)).map(([key, value]) => `ENV ${key}=${value}`);
438
552
  return `${imageScript}
439
553
  RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*
440
554
  RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
@@ -446,28 +560,11 @@ COPY ./package.json ./package.json
446
560
  RUN bun install --production
447
561
  ${postRunScripts.join("\n")}
448
562
  COPY . .
449
- ENV PORT=8282
450
- ENV NODE_ENV=production
451
- ENV AKAN_PUBLIC_REPO_NAME=${this.baseDevEnv.repoName}
452
- ENV AKAN_PUBLIC_SERVE_DOMAIN=${this.baseDevEnv.serveDomain}
453
- ENV AKAN_PUBLIC_APP_NAME=${this.app.name}
454
- ENV AKAN_PUBLIC_ENV=${this.baseDevEnv.env}
455
- ${this.basePaths.size ? `ENV AKAN_PUBLIC_BASE_PATHS=${[...this.basePaths].join(",")}` : ""}
456
- ENV AKAN_PUBLIC_DEFAULT_LOCALE=${this.i18n.defaultLocale}
457
- ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
458
- ENV AKAN_PUBLIC_API_PREFIX=${this.api.prefix}
459
- ENV AKAN_PUBLIC_WS_PREFIX=${this.api.websocketPrefix}
460
- ENV AKAN_PUBLIC_OPERATION_MODE=cloud
461
- ENV AKAN_LOG_TO_FILE=0
462
- ${webEnvLines}
563
+ ${envLines.join("\n")}
463
564
  CMD [${command.map((c) => `"${c}"`).join(",")}]`;
464
565
  }
465
566
  static #importGeneration = 0;
466
- /**
467
- * Bun caches dynamic imports by path, so a plain re-import after the user edits the config file
468
- * returns the stale module. `bustImportCache` appends a fresh query string to force re-evaluation
469
- * of the config module itself; modules it imports keep their cached instances.
470
- */
567
+ // Bun caches dynamic imports by path; a fresh query string re-evaluates the config module (its imports stay cached).
471
568
  static async importConfigModule<T = unknown>(
472
569
  cwdPath: string,
473
570
  { bustImportCache = false }: { bustImportCache?: boolean } = {},
@@ -506,6 +603,10 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
506
603
  );
507
604
  return {
508
605
  externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
606
+ trustedDependencies: libConfigs.flatMap((libConfig) => libConfig?.trustedDependencies ?? []),
607
+ bin: libConfigs.flatMap((libConfig) =>
608
+ libConfig && Object.keys(libConfig.bin).length ? [{ lib: libConfig.lib.name, bin: libConfig.bin }] : [],
609
+ ),
509
610
  docker: {
510
611
  preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
511
612
  postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? []),
@@ -519,31 +620,37 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
519
620
  #resolveProductionDependencyVersion(lib: string) {
520
621
  const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
521
622
  if (rootVersion) return rootVersion;
522
- // Fall back to the framework package's own (peer)dependencies so plugin-declared runtime
523
- // packages (e.g. firebase for the push plugin) resolve a version without the framework
524
- // hardcoding a per-feature package list.
623
+ // The framework's own (peer)dependencies resolve plugin runtime packages without a hardcoded list.
525
624
  const akanPackageJson = getAkanPackageJson();
526
- return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
625
+ const version = akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
626
+ if (!version) throw new Error(`Dependency ${lib} not found in package.json`);
627
+ return version;
527
628
  }
528
629
  #getProductionRuntimePackages() {
529
- return [...this.externalLibs, ...SSR_RUNTIME_PACKAGES, ...this.getDatabaseModeRuntimePackages()];
630
+ return [
631
+ ...this.externalLibs,
632
+ ...SSR_RUNTIME_PACKAGES,
633
+ ...this.database.modes.flatMap((mode) => this.getDatabaseModeRuntimePackages(mode)),
634
+ ];
530
635
  }
531
- getDatabaseModeRuntimePackages(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
532
- return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
636
+ getDatabaseModeRuntimePackages(databaseMode: DatabaseMode) {
637
+ return [...DatabaseModes.drivers[databaseMode]];
533
638
  }
534
- getMissingDatabaseModeDependencySpecs(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
639
+ getMissingDatabaseModeDependencySpecs(databaseMode: DatabaseMode) {
535
640
  return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
536
641
  }
537
- getMobileRuntimePackages() {
538
- // The app Capacitor plugins are installed at the workspace root too, so bun can resolve the
539
- // app's "*" declarations to a hoisted, peer-pinned version instead of fetching latest.
540
- return [...new Set([...MOBILE_RUNTIME_PACKAGES, ...MOBILE_APP_CAPACITOR_PLUGINS])];
541
- }
542
- getMissingMobileDependencySpecs() {
543
- return this.#getMissingDependencySpecs(this.getMobileRuntimePackages());
642
+ /** The mode a CLI command runs this app in: the shell's `AKAN_DATABASE_MODE` if the app declares it, else the first. */
643
+ resolveDatabaseMode() {
644
+ return DatabaseModes.resolve({
645
+ requested: process.env.AKAN_DATABASE_MODE,
646
+ declared: this.database.modes.join(","),
647
+ local: true,
648
+ });
544
649
  }
545
- getMobileAppCapacitorPlugins() {
546
- return [...MOBILE_APP_CAPACITOR_PLUGINS];
650
+ static #database(app: App, config: AppConfigDeclaration): AkanDatabaseConfig {
651
+ const declared = config?.database?.modes?.filter((mode): mode is DatabaseMode => !!mode);
652
+ const modes = declared?.length ? declared : ["single"];
653
+ return { modes: DatabaseModes.parseList(modes.join(","), `database.modes in apps/${app.name}/akan.config.ts`) };
547
654
  }
548
655
  #getMissingDependencySpecs(libs: readonly string[]) {
549
656
  const rootDependencies = {
@@ -552,11 +659,10 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
552
659
  };
553
660
  return libs
554
661
  .filter((lib) => !rootDependencies[lib])
555
- .map((lib) => {
556
- const version = this.#resolveProductionDependencyVersion(lib);
557
- if (!version) throw new Error(`Dependency ${lib} not found in package.json`);
558
- return `${lib}@${version}`;
559
- });
662
+ .map((lib) => `${lib}@${this.#resolveProductionDependencyVersion(lib)}`);
663
+ }
664
+ get akanVersion() {
665
+ return getAkanPackageJson().version;
560
666
  }
561
667
  getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
562
668
  return {
@@ -565,12 +671,12 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
565
671
  version: "1.0.0",
566
672
  main: "./main.js",
567
673
  dependencies: Object.fromEntries(
568
- [...new Set(this.#getProductionRuntimePackages())].map((lib) => {
569
- const version = this.#resolveProductionDependencyVersion(lib);
570
- if (!version) throw new Error(`Dependency ${lib} not found in package.json`);
571
- return [lib, version];
572
- }),
674
+ [...new Set(this.#getProductionRuntimePackages())].map((lib) => [
675
+ lib,
676
+ this.#resolveProductionDependencyVersion(lib),
677
+ ]),
573
678
  ),
679
+ ...(this.trustedDependencies.length ? { trustedDependencies: this.trustedDependencies } : {}),
574
680
  ...data,
575
681
  };
576
682
  }
@@ -624,13 +730,18 @@ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfi
624
730
  export class AkanLibConfig implements LibConfigResult {
625
731
  lib: Lib;
626
732
  externalLibs: string[];
733
+ trustedDependencies: string[];
734
+ bin: AkanBinConfig;
627
735
  docker: LibDockerConfig;
628
736
  assets: LibAssetsConfig;
629
737
  /** Live-only: plugins declared in this lib's `akan.config.ts` (never serialized). */
630
738
  plugins: AkanPlugin[];
631
- constructor(lib: Lib, config: DeepPartial<LibConfigResult>, plugins: AkanPlugin[] = []) {
739
+ constructor(lib: Lib, config: LibConfigDeclaration, plugins: AkanPlugin[] = []) {
632
740
  this.lib = lib;
633
741
  this.externalLibs = config?.externalLibs ?? [];
742
+ const owner = `libs/${lib.name}/akan.config.ts`;
743
+ this.trustedDependencies = normalizePackageNames(config?.trustedDependencies, owner);
744
+ this.bin = AkanBin.parse(config?.bin, owner, lib.cwdPath);
634
745
  this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
635
746
  this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts as string[] | undefined) };
636
747
  this.plugins = plugins;
@@ -638,46 +749,19 @@ export class AkanLibConfig implements LibConfigResult {
638
749
  static async from(lib: Lib, { bustImportCache = false }: { bustImportCache?: boolean } = {}) {
639
750
  const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
640
751
  const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
641
- const { plugins, ...config } = (resolved ?? {}) as DeepPartial<LibConfigResult> & { plugins?: AkanPlugin[] };
752
+ const { plugins, ...config } = (resolved ?? {}) as LibConfigDeclaration & { plugins?: AkanPlugin[] };
642
753
  return new AkanLibConfig(lib, config, plugins ?? []);
643
754
  }
644
755
  }
645
756
 
646
- // export const getCapacitorConfig = (configImp: AppConfig, appInfo: AppScanResult, tsconfig: TsConfigJson) => {
647
- // const props: RunnerProps = {
648
- // type: "app",
649
- // name: appInfo.name,
650
- // repoName: appInfo.repoName,
651
- // serveDomain: appInfo.serveDomain,
652
- // env: (process.env.AKAN_PUBLIC_ENV ?? "debug") as "testing" | "local" | "debug" | "develop" | "main",
653
- // libs: appInfo.libDeps,
654
- // tsconfig,
655
- // };
656
- // const config = typeof configImp === "function" ? configImp(props) : configImp;
657
- // const akanConfig = makeAppConfig(config, props);
658
- // return akanConfig;
659
- // };
660
-
661
757
  //! need to refactor
662
- export const increaseBuildNum = async (app: App) => {
663
- const appConfig = await AkanAppConfig.from(app);
758
+ const shiftBuildNum = async (app: App, delta: number) => {
759
+ const { buildNum } = (await AkanAppConfig.from(app)).native;
664
760
  const akanConfigPath = path.join(app.cwdPath, "akan.config.ts");
665
761
  const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
666
- const akanConfigContent = akanConfig.replace(
667
- `buildNum: ${appConfig.mobile.buildNum}`,
668
- `buildNum: ${appConfig.mobile.buildNum + 1}`,
669
- );
670
- //? 개선할 여지가 있는지 확인
671
- fs.writeFileSync(akanConfigPath, akanConfigContent);
762
+ fs.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
672
763
  };
673
764
 
674
- export const decreaseBuildNum = async (app: App) => {
675
- const appConfig = await AkanAppConfig.from(app);
676
- const akanConfigPath = path.join(app.cwdPath, "akan.config.ts");
677
- const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
678
- const akanConfigContent = akanConfig.replace(
679
- `buildNum: ${appConfig.mobile.buildNum}`,
680
- `buildNum: ${appConfig.mobile.buildNum - 1}`,
681
- );
682
- fs.writeFileSync(akanConfigPath, akanConfigContent);
683
- };
765
+ export const increaseBuildNum = async (app: App) => await shiftBuildNum(app, 1);
766
+
767
+ export const decreaseBuildNum = async (app: App) => await shiftBuildNum(app, -1);