@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
@@ -2,17 +2,24 @@ 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 AkanNativeSettings,
21
+ type AkanNativeTarget,
22
+ type AkanNativeUpdatesConfig,
16
23
  type AkanRouteConfig,
17
24
  type AkanWebConfig,
18
25
  type AkanWebOption,
@@ -31,78 +38,14 @@ import {
31
38
 
32
39
  const DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
33
40
  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
- ];
41
+ "lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
42
+ "@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
43
+ "@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
44
+ "@material-ui/icons @tabler/icons-react mui-core react-icons/*",
45
+ ].flatMap((line) => line.split(" "));
60
46
  const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
61
47
  const DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
62
48
  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
49
  const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
107
50
  deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
108
51
  imageSizes: [32, 48, 64, 96, 128, 256, 384],
@@ -130,18 +73,14 @@ const normalizeStringList = (values: string[] | undefined) => {
130
73
  return normalized.length > 0 ? [...new Set(normalized)] : undefined;
131
74
  };
132
75
 
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.
76
+ // A reverse-DNS / Android package segment: lowercase alphanumerics, never empty, never starting with a digit.
135
77
  const sanitizeAppIdSegment = (value: string): string => {
136
78
  const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
137
79
  if (!cleaned) return "app";
138
80
  return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
139
81
  };
140
82
 
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.
83
+ // The repo name is the org segment: a bare `com.<appName>.app` is routinely already claimed on Apple's portal.
145
84
  export const deriveDefaultAppId = (orgName: string, appName: string): string =>
146
85
  `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
147
86
 
@@ -159,30 +98,27 @@ const normalizeDeepLinkDomain = (domain: string) => {
159
98
  }
160
99
  };
161
100
 
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"];
101
+ const normalizeDeepLinks = (deepLinks: AkanNativeDeepLinks | undefined): AkanNativeDeepLinks | undefined => {
102
+ const schemes = normalizeStringList(deepLinks?.schemes);
103
+ const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
104
+ if (!schemes && !domains) return undefined;
105
+ return { ...(schemes ? { schemes } : {}), ...(domains ? { domains } : {}) };
175
106
  };
176
107
 
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"> & {
108
+ type AppConfigDeclaration = Omit<DeepPartial<AppConfigResult>, "docker" | "web" | "bin" | "native"> & {
179
109
  docker?: DockerOption;
180
110
  web?: AkanWebOption;
111
+ bin?: AkanBinConfig;
112
+ native?: AkanNativeAppConfig;
181
113
  };
182
114
 
183
- /** What the workspace's libs add to an app's build, read off each `libs/<lib>/akan.config.ts`. */
115
+ type LibConfigDeclaration = Omit<DeepPartial<LibConfigResult>, "bin"> & { bin?: AkanBinConfig };
116
+
184
117
  export interface LibContributions {
185
118
  externalLibs: string[];
119
+ trustedDependencies?: string[];
120
+ /** Per lib, since an app carries only the entries of the libs it depends on. */
121
+ bin?: { lib: string; bin: AkanBinConfig }[];
186
122
  docker: LibDockerConfig;
187
123
  /** Keep globs rewritten to the app's own `public/`, where `akan sync` mounts each lib's assets. */
188
124
  keepFonts?: string[];
@@ -190,10 +126,19 @@ export interface LibContributions {
190
126
 
191
127
  const emptyLibContributions = (): LibContributions => ({
192
128
  externalLibs: [],
129
+ trustedDependencies: [],
130
+ bin: [],
193
131
  docker: { preRuns: [], postRuns: [] },
194
132
  keepFonts: [],
195
133
  });
196
134
 
135
+ const normalizePackageNames = (names: unknown, owner: string): string[] => {
136
+ if (names === undefined) return [];
137
+ if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
138
+ throw new Error(`${owner}: trustedDependencies lists package names`);
139
+ return [...new Set(names.map((name: string) => name.trim()))];
140
+ };
141
+
197
142
  const normalizeKeepFonts = (keepFonts: string[] | undefined) => [
198
143
  ...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean)),
199
144
  ];
@@ -211,21 +156,25 @@ export class AkanAppConfig implements AppConfigResult {
211
156
  docker: DockerConfig;
212
157
  /** The Dockerfile `akan build` writes: the declared string verbatim, or one assembled from the parts. */
213
158
  dockerfile: string;
214
- defaultDatabaseMode: DatabaseMode;
159
+ database: AkanDatabaseConfig;
215
160
  web: AkanWebConfig;
216
161
  externalLibs: string[];
162
+ trustedDependencies: string[];
163
+ /** The app's own entries; the libs' are in `libBins`. */
164
+ bin: AkanBinConfig;
165
+ libBins: { lib: string; bin: AkanBinConfig }[];
217
166
  barrelImports: string[];
218
167
  optimizeImports: string[];
219
168
  images: AkanImageConfig;
220
169
  i18n: AkanI18nConfig;
221
170
  api: AkanApiConfig;
222
171
  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;
172
+ native: AkanNativeAppResult;
173
+ /** True only when the app's akan.config.ts declares a `native` section (vs. the synthesized default target). */
174
+ hasNativeConfig: boolean;
226
175
  secrets: string[];
227
176
  assets: AkanAssetsConfig;
228
- /** Raw setting; resolved against the app's lib deps at sync time (see `AppExecutor.syncPages`). */
177
+ /** Raw setting, resolved against the app's lib deps at sync time. */
229
178
  syncPageLibs: string[] | boolean;
230
179
  baseDevEnv: BaseDevEnv;
231
180
  libs: string[];
@@ -250,8 +199,17 @@ export class AkanAppConfig implements AppConfigResult {
250
199
  this.baseDevEnv = baseDevEnv;
251
200
  this.plugins = plugins;
252
201
  this.#applyRoutes(config?.routes);
253
- this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
202
+ this.database = AkanAppConfig.#database(app, config);
254
203
  this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...libContributions.externalLibs])];
204
+ const owner = `apps/${app.name}/akan.config.ts`;
205
+ this.trustedDependencies = [
206
+ ...new Set([
207
+ ...normalizePackageNames(config?.trustedDependencies, owner),
208
+ ...(libContributions.trustedDependencies ?? []),
209
+ ]),
210
+ ];
211
+ this.bin = AkanBin.parse(config?.bin, owner, app.cwdPath);
212
+ this.libBins = libContributions.bin ?? [];
255
213
  this.barrelImports = [
256
214
  ...DEFAULT_BARREL_IMPORTS,
257
215
  ...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
@@ -279,8 +237,12 @@ export class AkanAppConfig implements AppConfigResult {
279
237
  ],
280
238
  };
281
239
  this.syncPageLibs = (config?.syncPageLibs as string[] | boolean | undefined) ?? false;
282
- this.hasMobileConfig = Boolean(config.mobile);
283
- this.mobile = this.#resolveMobileConfig(config.mobile);
240
+ if ((config as { mobile?: unknown }).mobile !== undefined)
241
+ throw new Error(
242
+ `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>.`,
243
+ );
244
+ this.hasNativeConfig = Boolean(config.native);
245
+ this.native = this.#resolveNativeConfig(config.native);
284
246
  this.web = this.#resolveWebConfig(config.web);
285
247
  this.docker = AkanAppConfig.#resolveDocker(config.docker, libContributions.docker);
286
248
  this.dockerfile = this.#makeDockerfile();
@@ -288,101 +250,203 @@ export class AkanAppConfig implements AppConfigResult {
288
250
  #resolveWebConfig(web: AkanWebOption | undefined): AkanWebConfig {
289
251
  const resolved = typeof web === "object" ? { ssr: true, csr: web.csr } : { ssr: web ?? true, csr: web ?? true };
290
252
  // `akan build-ios` / `build-android` copy `dist/apps/<app>/csr/<target>.html` into the native project.
291
- if (!resolved.csr && this.hasMobileConfig)
253
+ if (!resolved.csr && this.hasNativeConfig)
292
254
  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.`,
255
+ `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
256
  );
295
257
  return resolved;
296
258
  }
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
- );
259
+ #resolveNativeConfig(native: AkanNativeAppConfig | undefined): AkanNativeAppResult {
260
+ const configPath = `apps/${this.app.name}/akan.config.ts`;
261
+ const { targets: rawTargets, ...root } = native ?? {};
262
+ AkanAppConfig.#assertNativeKeys(root, "native", configPath);
263
+ AkanAppConfig.#assertFiles(root, "native", configPath);
264
+ const appName = root.appName ?? this.app.name;
265
+ const appId = root.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
266
+ const version = root.version ?? "0.0.1";
267
+ const buildNum = root.buildNum ?? 1;
268
+ const named = rawTargets !== undefined && Object.keys(rawTargets).length > 0;
269
+ const entries: [string, AkanNativeSettings][] = named
270
+ ? Object.entries(rawTargets)
271
+ : [[this.basePaths.has(this.app.name) ? this.app.name : "default", {}]];
313
272
  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)) {
273
+ entries.map(([name, own]) => {
274
+ const where = named ? `native.targets.${name}` : "native";
275
+ AkanAppConfig.#assertNativeKeys(own ?? {}, where, configPath);
276
+ const merged = AkanAppConfig.#overlay(root, own ?? {});
277
+ const fallbackBasePath = !named && this.basePaths.has(name) ? name : undefined;
278
+ const basePath = (merged.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
279
+ if (basePath && !this.basePaths.has(basePath))
280
+ throw new Error(`Native target '${name}' uses unknown basePath '${basePath}' in ${configPath}`);
281
+ const { updates } = merged;
282
+ if (updates && (!updates.url || !updates.publicKey))
321
283
  throw new Error(
322
- `Mobile target '${name}' uses unknown basePath '${basePath}' in apps/${this.app.name}/akan.config.ts`,
284
+ `${where}.updates in ${configPath} has no url or publicKey${named ? "; give them in native.updates or the target's own" : ""}.`,
323
285
  );
324
- }
325
- const resolved = {
326
- ...rawMobile,
327
- ...target,
286
+ AkanAppConfig.#assertFiles(own ?? {}, where, configPath);
287
+ const {
288
+ basePath: _basePath,
289
+ indexPath: rawIndexPath,
290
+ deepLinks: rawDeepLinks,
291
+ updates: _updates,
292
+ ...rest
293
+ } = merged;
294
+ const indexPath = normalizeIndexPath(rawIndexPath);
295
+ const deepLinks = normalizeDeepLinks(rawDeepLinks);
296
+ const { teamId: rawTeamId, ...iosRest } = AkanAppConfig.#platformIndexPath(merged.ios ?? {});
297
+ const teamId = rawTeamId?.trim();
298
+ const { sha256CertFingerprints: rawFingerprints, ...androidRest } = AkanAppConfig.#platformIndexPath(
299
+ merged.android ?? {},
300
+ );
301
+ const fingerprints = normalizeStringList(rawFingerprints);
302
+ const target: AkanNativeTarget = {
303
+ ...rest,
328
304
  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];
305
+ ...(basePath ? { basePath } : {}),
306
+ ...(indexPath ? { indexPath } : {}),
307
+ ...(deepLinks ? { deepLinks } : {}),
308
+ ...(merged.ios ? { ios: { ...iosRest, ...(teamId ? { teamId } : {}) } } : {}),
309
+ ...(merged.android
310
+ ? { android: { ...androidRest, ...(fingerprints ? { sha256CertFingerprints: fingerprints } : {}) } }
311
+ : {}),
312
+ ...(merged.desktop ? { desktop: AkanAppConfig.#platformIndexPath(merged.desktop) } : {}),
313
+ appName: merged.appName ?? this.app.name,
314
+ appId: merged.appId ?? appId,
315
+ version: merged.version ?? version,
316
+ buildNum: merged.buildNum ?? buildNum,
317
+ ...(updates ? { updates: updates as AkanNativeUpdatesConfig } : {}),
318
+ };
319
+ return [name, target];
350
320
  }),
351
321
  );
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";
322
+ return { appName, appId, ...(root.fileName ? { fileName: root.fileName } : {}), version, buildNum, targets };
323
+ }
324
+ static #platformIndexPath<T extends { indexPath?: string }>(section: T): T {
325
+ const { indexPath: rawIndexPath, ...rest } = section;
326
+ const indexPath = normalizeIndexPath(rawIndexPath);
327
+ return { ...rest, ...(indexPath ? { indexPath } : {}) } as T;
328
+ }
329
+ /** `own` over `base`: plain objects merge key by key, and any other value, a list included, replaces. */
330
+ static #overlay(base: AkanNativeSettings, own: AkanNativeSettings): AkanNativeSettings {
331
+ const merge = (kept: unknown, value: unknown, top: boolean, key: string): unknown => {
332
+ if (!AkanAppConfig.#isPlainObject(kept) || !AkanAppConfig.#isPlainObject(value)) return value;
333
+ //? An icon or splash given as an object is one value: its image and color go together.
334
+ if (top && (key === "icon" || key === "splash")) return value;
335
+ const out: Record<string, unknown> = { ...kept };
336
+ for (const [inner, next] of Object.entries(value))
337
+ if (next !== undefined) out[inner] = merge(out[inner], next, false, inner);
338
+ return out;
339
+ };
340
+ const out: Record<string, unknown> = { ...base };
341
+ for (const [key, value] of Object.entries(own))
342
+ if (value !== undefined) out[key] = merge(out[key], value, true, key);
343
+ return out as AkanNativeSettings;
344
+ }
345
+ static #isPlainObject(value: unknown): value is Record<string, unknown> {
346
+ return typeof value === "object" && value !== null && !Array.isArray(value);
347
+ }
348
+ static readonly #nativeKeys = {
349
+ native: [
350
+ "basePath",
351
+ "indexPath",
352
+ "appName",
353
+ "appId",
354
+ "fileName",
355
+ "version",
356
+ "buildNum",
357
+ "icon",
358
+ "splash",
359
+ "permissions",
360
+ "plugins",
361
+ "deepLinks",
362
+ "updates",
363
+ "ios",
364
+ "android",
365
+ "desktop",
366
+ ],
367
+ ios: ["indexPath", "teamId", "infoPlist", "entitlements", "privacy", "files"],
368
+ android: [
369
+ "indexPath",
370
+ "sha256CertFingerprints",
371
+ "googleServices",
372
+ "push",
373
+ "autoplay",
374
+ "manifest",
375
+ "application",
376
+ "activity",
377
+ "files",
378
+ ],
379
+ desktop: ["indexPath", "server", "recovery", "window", "screenCapture"],
380
+ deepLinks: ["schemes", "domains"],
381
+ } as const;
382
+ //* Settings that moved when `mobile` became `native` are named with where they went, instead of "unknown".
383
+ static readonly #movedKeys: Readonly<Record<string, string>> = {
384
+ native: "the platform sections and plugins sit directly in the native section: ios, android, desktop, plugins",
385
+ assets: "icon and splash sit directly in the native section",
386
+ files:
387
+ "ios.files (keyed by the path in the app bundle) or android.files (keyed res/<type>/<file> or assets/<path>)",
388
+ push: "android.push",
389
+ privacy: "ios.privacy",
390
+ "deepLinks.ios": "ios.teamId",
391
+ "deepLinks.android": "android.sha256CertFingerprints",
392
+ };
393
+ static #assertNativeKeys(section: object, where: string, configPath: string) {
394
+ const check = (value: unknown, known: readonly string[], at: string, kind: string) => {
395
+ if (!AkanAppConfig.#isPlainObject(value)) return;
396
+ for (const key of Object.keys(value)) {
397
+ if (known.includes(key)) continue;
398
+ const moved = AkanAppConfig.#movedKeys[kind === "native" ? key : `${kind}.${key}`];
399
+ throw new Error(
400
+ moved
401
+ ? `${at}.${key} in ${configPath} has moved: ${moved}.`
402
+ : `${at}.${key} in ${configPath} is not a native setting. Known: ${known.join(", ")}.`,
403
+ );
404
+ }
405
+ };
406
+ check(section, AkanAppConfig.#nativeKeys.native, where, "native");
407
+ const settings = section as AkanNativeSettings;
408
+ check(settings.ios, AkanAppConfig.#nativeKeys.ios, `${where}.ios`, "ios");
409
+ check(settings.android, AkanAppConfig.#nativeKeys.android, `${where}.android`, "android");
410
+ check(settings.desktop, AkanAppConfig.#nativeKeys.desktop, `${where}.desktop`, "desktop");
411
+ check(settings.deepLinks, AkanAppConfig.#nativeKeys.deepLinks, `${where}.deepLinks`, "deepLinks");
412
+ }
413
+ static readonly #androidFilePattern = /^(?:res\/[^/]+\/[^/]+|assets\/.+)$/;
414
+ static #assertFiles(settings: AkanNativeSettings, where: string, configPath: string) {
415
+ const check = (
416
+ files: Record<string, string> | undefined,
417
+ at: string,
418
+ valid: (to: string) => boolean,
419
+ shape: string,
420
+ ) => {
421
+ for (const [to, from] of Object.entries(files ?? {})) {
422
+ if (typeof from !== "string")
423
+ throw new Error(
424
+ `${at}["${to}"] in ${configPath} must name its source file, relative to the app folder, e.g. { "${shape}": "assets/chime.mp3" }.`,
425
+ );
426
+ if (!valid(to)) throw new Error(`${at}["${to}"] in ${configPath} must land at ${shape}.`);
427
+ }
428
+ };
429
+ check(
430
+ settings.ios?.files,
431
+ `${where}.ios.files`,
432
+ (to) => to.length > 0 && !to.startsWith("/") && !to.split("/").includes(".."),
433
+ "<path in the app bundle>",
434
+ );
435
+ check(
436
+ settings.android?.files,
437
+ `${where}.android.files`,
438
+ (to) => AkanAppConfig.#androidFilePattern.test(to),
439
+ "res/<type>/<file> or assets/<path>",
440
+ );
365
441
  }
366
442
  #applyRoutes(routes: AkanRouteConfig[] = []) {
367
443
  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
- }
444
+ const basePath = route.basePath ? route.basePath.replace(/^\/+|\/+$/g, "") : null;
445
+ if (basePath !== null) this.basePaths.add(basePath);
446
+ const domains = basePath !== null ? this.subRoutes.getOrInsert(basePath, new Set()) : this.domains;
447
+ for (const branch of Object.keys(route.domains)) this.branches.add(branch);
448
+ for (const domain of Object.values(route.domains).flat())
449
+ if (domain) domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
386
450
  }
387
451
  const appName = this.app.name.toLowerCase();
388
452
  const serveDomain = this.baseDevEnv.serveDomain.toLowerCase();
@@ -420,21 +484,39 @@ export class AkanAppConfig implements AppConfigResult {
420
484
  command: docker?.command ?? ["bun", "main.js"],
421
485
  };
422
486
  }
423
- #makeDockerfile(): string {
487
+ /** What the built server runs with in its image; a desktop app's carried server starts from the same values. */
488
+ getProductionEnv(environment: string = this.baseDevEnv.env): Record<string, string> {
489
+ return {
490
+ PORT: "8282",
491
+ NODE_ENV: "production",
492
+ AKAN_PUBLIC_REPO_NAME: this.baseDevEnv.repoName,
493
+ AKAN_PUBLIC_SERVE_DOMAIN: this.baseDevEnv.serveDomain,
494
+ AKAN_PUBLIC_APP_NAME: this.app.name,
495
+ AKAN_PUBLIC_ENV: environment,
496
+ ...(this.basePaths.size ? { AKAN_PUBLIC_BASE_PATHS: [...this.basePaths].join(",") } : {}),
497
+ AKAN_PUBLIC_DEFAULT_LOCALE: this.i18n.defaultLocale,
498
+ AKAN_PUBLIC_LOCALES: this.i18n.locales.join(","),
499
+ AKAN_PUBLIC_API_PREFIX: this.api.prefix,
500
+ AKAN_PUBLIC_WS_PREFIX: this.api.websocketPrefix,
501
+ AKAN_PUBLIC_OPERATION_MODE: "cloud",
502
+ AKAN_DATABASE_MODES: this.database.modes.join(","),
503
+ // File logging is off: a container's writable layer is ephemeral and stdout is the collection path.
504
+ AKAN_LOG_TO_FILE: "0",
505
+ // The web env matches what was built (a deployment can only narrow it).
506
+ ...(this.web.ssr ? {} : { AKAN_SSR: "false" }),
507
+ ...(this.web.csr ? {} : { AKAN_CSR: "false" }),
508
+ };
509
+ }
510
+ dockerfileFor(environment: string): string {
511
+ return this.#makeDockerfile(environment);
512
+ }
513
+ #makeDockerfile(environment?: string): string {
424
514
  if (typeof this.docker === "string") return this.docker;
425
515
  const { image, preRuns, postRuns, command } = this.docker;
426
516
  const preRunScripts = this.#getDockerRunScripts(preRuns);
427
517
  const postRunScripts = this.#getDockerRunScripts(postRuns);
428
518
  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");
519
+ const envLines = Object.entries(this.getProductionEnv(environment)).map(([key, value]) => `ENV ${key}=${value}`);
438
520
  return `${imageScript}
439
521
  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
522
  RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
@@ -446,28 +528,11 @@ COPY ./package.json ./package.json
446
528
  RUN bun install --production
447
529
  ${postRunScripts.join("\n")}
448
530
  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}
531
+ ${envLines.join("\n")}
463
532
  CMD [${command.map((c) => `"${c}"`).join(",")}]`;
464
533
  }
465
534
  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
- */
535
+ // Bun caches dynamic imports by path; a fresh query string re-evaluates the config module (its imports stay cached).
471
536
  static async importConfigModule<T = unknown>(
472
537
  cwdPath: string,
473
538
  { bustImportCache = false }: { bustImportCache?: boolean } = {},
@@ -506,6 +571,10 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
506
571
  );
507
572
  return {
508
573
  externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
574
+ trustedDependencies: libConfigs.flatMap((libConfig) => libConfig?.trustedDependencies ?? []),
575
+ bin: libConfigs.flatMap((libConfig) =>
576
+ libConfig && Object.keys(libConfig.bin).length ? [{ lib: libConfig.lib.name, bin: libConfig.bin }] : [],
577
+ ),
509
578
  docker: {
510
579
  preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
511
580
  postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? []),
@@ -519,31 +588,37 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
519
588
  #resolveProductionDependencyVersion(lib: string) {
520
589
  const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
521
590
  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.
591
+ // The framework's own (peer)dependencies resolve plugin runtime packages without a hardcoded list.
525
592
  const akanPackageJson = getAkanPackageJson();
526
- return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
593
+ const version = akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
594
+ if (!version) throw new Error(`Dependency ${lib} not found in package.json`);
595
+ return version;
527
596
  }
528
597
  #getProductionRuntimePackages() {
529
- return [...this.externalLibs, ...SSR_RUNTIME_PACKAGES, ...this.getDatabaseModeRuntimePackages()];
598
+ return [
599
+ ...this.externalLibs,
600
+ ...SSR_RUNTIME_PACKAGES,
601
+ ...this.database.modes.flatMap((mode) => this.getDatabaseModeRuntimePackages(mode)),
602
+ ];
530
603
  }
531
- getDatabaseModeRuntimePackages(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
532
- return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
604
+ getDatabaseModeRuntimePackages(databaseMode: DatabaseMode) {
605
+ return [...DatabaseModes.drivers[databaseMode]];
533
606
  }
534
- getMissingDatabaseModeDependencySpecs(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
607
+ getMissingDatabaseModeDependencySpecs(databaseMode: DatabaseMode) {
535
608
  return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
536
609
  }
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());
610
+ /** The mode a CLI command runs this app in: the shell's `AKAN_DATABASE_MODE` if the app declares it, else the first. */
611
+ resolveDatabaseMode() {
612
+ return DatabaseModes.resolve({
613
+ requested: process.env.AKAN_DATABASE_MODE,
614
+ declared: this.database.modes.join(","),
615
+ local: true,
616
+ });
544
617
  }
545
- getMobileAppCapacitorPlugins() {
546
- return [...MOBILE_APP_CAPACITOR_PLUGINS];
618
+ static #database(app: App, config: AppConfigDeclaration): AkanDatabaseConfig {
619
+ const declared = config?.database?.modes?.filter((mode): mode is DatabaseMode => !!mode);
620
+ const modes = declared?.length ? declared : ["single"];
621
+ return { modes: DatabaseModes.parseList(modes.join(","), `database.modes in apps/${app.name}/akan.config.ts`) };
547
622
  }
548
623
  #getMissingDependencySpecs(libs: readonly string[]) {
549
624
  const rootDependencies = {
@@ -552,11 +627,10 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
552
627
  };
553
628
  return libs
554
629
  .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
- });
630
+ .map((lib) => `${lib}@${this.#resolveProductionDependencyVersion(lib)}`);
631
+ }
632
+ get akanVersion() {
633
+ return getAkanPackageJson().version;
560
634
  }
561
635
  getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
562
636
  return {
@@ -565,12 +639,12 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
565
639
  version: "1.0.0",
566
640
  main: "./main.js",
567
641
  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
- }),
642
+ [...new Set(this.#getProductionRuntimePackages())].map((lib) => [
643
+ lib,
644
+ this.#resolveProductionDependencyVersion(lib),
645
+ ]),
573
646
  ),
647
+ ...(this.trustedDependencies.length ? { trustedDependencies: this.trustedDependencies } : {}),
574
648
  ...data,
575
649
  };
576
650
  }
@@ -624,13 +698,18 @@ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfi
624
698
  export class AkanLibConfig implements LibConfigResult {
625
699
  lib: Lib;
626
700
  externalLibs: string[];
701
+ trustedDependencies: string[];
702
+ bin: AkanBinConfig;
627
703
  docker: LibDockerConfig;
628
704
  assets: LibAssetsConfig;
629
705
  /** Live-only: plugins declared in this lib's `akan.config.ts` (never serialized). */
630
706
  plugins: AkanPlugin[];
631
- constructor(lib: Lib, config: DeepPartial<LibConfigResult>, plugins: AkanPlugin[] = []) {
707
+ constructor(lib: Lib, config: LibConfigDeclaration, plugins: AkanPlugin[] = []) {
632
708
  this.lib = lib;
633
709
  this.externalLibs = config?.externalLibs ?? [];
710
+ const owner = `libs/${lib.name}/akan.config.ts`;
711
+ this.trustedDependencies = normalizePackageNames(config?.trustedDependencies, owner);
712
+ this.bin = AkanBin.parse(config?.bin, owner, lib.cwdPath);
634
713
  this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
635
714
  this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts as string[] | undefined) };
636
715
  this.plugins = plugins;
@@ -638,46 +717,19 @@ export class AkanLibConfig implements LibConfigResult {
638
717
  static async from(lib: Lib, { bustImportCache = false }: { bustImportCache?: boolean } = {}) {
639
718
  const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
640
719
  const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
641
- const { plugins, ...config } = (resolved ?? {}) as DeepPartial<LibConfigResult> & { plugins?: AkanPlugin[] };
720
+ const { plugins, ...config } = (resolved ?? {}) as LibConfigDeclaration & { plugins?: AkanPlugin[] };
642
721
  return new AkanLibConfig(lib, config, plugins ?? []);
643
722
  }
644
723
  }
645
724
 
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
725
  //! need to refactor
662
- export const increaseBuildNum = async (app: App) => {
663
- const appConfig = await AkanAppConfig.from(app);
726
+ const shiftBuildNum = async (app: App, delta: number) => {
727
+ const { buildNum } = (await AkanAppConfig.from(app)).native;
664
728
  const akanConfigPath = path.join(app.cwdPath, "akan.config.ts");
665
729
  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);
730
+ fs.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
672
731
  };
673
732
 
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
- };
733
+ export const increaseBuildNum = async (app: App) => await shiftBuildNum(app, 1);
734
+
735
+ export const decreaseBuildNum = async (app: App) => await shiftBuildNum(app, -1);