@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
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { AppExecutor, WorkspaceExecutor } from "../executors";
7
7
  import type { PackageJson } from "../types";
8
8
  import { AkanAppConfig, AkanLibConfig, deriveDefaultAppId } from "./akanConfig";
9
- import type { DeepPartial, LibConfigResult } from "./types";
9
+ import type { LibConfigInput } from "./types";
10
10
 
11
11
  const akanPackageJson = JSON.parse(
12
12
  fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../akanjs/package.json"), "utf8"),
@@ -33,8 +33,24 @@ const baseDevEnv = {
33
33
  workspaceRoot: "/workspace",
34
34
  };
35
35
 
36
+ const loadExtAppConfig = async (tmpPrefix: string, appConfig: string, libConfig: string) => {
37
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), tmpPrefix));
38
+ try {
39
+ fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
40
+ fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
41
+ fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), appConfig);
42
+ fs.writeFileSync(path.join(root, "libs/extlib/akan.config.ts"), libConfig);
43
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
44
+ fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
45
+ const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
46
+ return await AppExecutor.from(workspace, "extapp").getConfig();
47
+ } finally {
48
+ fs.rmSync(root, { recursive: true, force: true });
49
+ }
50
+ };
51
+
36
52
  describe("AkanAppConfig", () => {
37
- test("applies defaults for route domains, i18n, image, mobile, and imports", () => {
53
+ test("applies defaults for route domains, i18n, image, native, and imports", () => {
38
54
  const config = new AkanAppConfig(app, ["shared"], packageJson, {}, baseDevEnv);
39
55
 
40
56
  expect([...config.domains].sort()).toEqual([
@@ -46,7 +62,7 @@ describe("AkanAppConfig", () => {
46
62
  expect(config.i18n.defaultLocale).toBe("en");
47
63
  expect(config.i18n.locales).toContain("en");
48
64
  expect(config.images.formats).toEqual(["image/webp"]);
49
- expect(config.mobile).toMatchObject({
65
+ expect(config.native).toMatchObject({
50
66
  appName: "portal",
51
67
  appId: "com.akanjs.portal",
52
68
  version: "0.0.1",
@@ -86,7 +102,7 @@ describe("AkanAppConfig", () => {
86
102
  },
87
103
  ],
88
104
  i18n: { locales: ["ko", "en"], defaultLocale: "ko" },
89
- mobile: {
105
+ native: {
90
106
  appName: "Portal App",
91
107
  appId: "com.portal.mobile",
92
108
  version: "1.2.3",
@@ -119,8 +135,8 @@ describe("AkanAppConfig", () => {
119
135
  expect(config.i18n.defaultLocale).toBe("ko");
120
136
  expect(config.images.qualities).toEqual([80, 90]);
121
137
  expect(config.images.dangerouslyAllowSVG).toBe(true);
122
- expect(config.mobile.buildNum).toBe(7);
123
- expect(config.mobile.targets.default).toMatchObject({
138
+ expect(config.native.buildNum).toBe(7);
139
+ expect(config.native.targets.default).toMatchObject({
124
140
  name: "default",
125
141
  appName: "Portal App",
126
142
  appId: "com.portal.mobile",
@@ -156,10 +172,79 @@ describe("AkanAppConfig", () => {
156
172
  expect(new AkanAppConfig(app, [], packageJson, { web: true }, baseDevEnv).web).toEqual({ ssr: true, csr: true });
157
173
  });
158
174
 
159
- test("refuses a csr-less build that ships a mobile app", () => {
175
+ test("writes the image env from getProductionEnv, one ENV line per key in its order", () => {
176
+ const config = new AkanAppConfig(
177
+ app,
178
+ [],
179
+ packageJson,
180
+ { routes: [{ basePath: "admin", domains: {} }], web: false, database: { modes: ["single", "cluster"] } },
181
+ baseDevEnv,
182
+ );
183
+
184
+ expect(config.getProductionEnv()).toEqual({
185
+ PORT: "8282",
186
+ NODE_ENV: "production",
187
+ AKAN_PUBLIC_REPO_NAME: "akanjs",
188
+ AKAN_PUBLIC_SERVE_DOMAIN: "akanjs.com",
189
+ AKAN_PUBLIC_APP_NAME: "portal",
190
+ AKAN_PUBLIC_ENV: "debug",
191
+ AKAN_PUBLIC_BASE_PATHS: "admin",
192
+ AKAN_PUBLIC_DEFAULT_LOCALE: "en",
193
+ AKAN_PUBLIC_LOCALES: config.i18n.locales.join(","),
194
+ AKAN_PUBLIC_API_PREFIX: "/api",
195
+ AKAN_PUBLIC_WS_PREFIX: "/ws",
196
+ AKAN_PUBLIC_OPERATION_MODE: "cloud",
197
+ AKAN_DATABASE_MODES: "single,cluster",
198
+ AKAN_LOG_TO_FILE: "0",
199
+ AKAN_SSR: "false",
200
+ AKAN_CSR: "false",
201
+ });
202
+ const envBlock = Object.entries(config.getProductionEnv())
203
+ .map(([key, value]) => `ENV ${key}=${value}`)
204
+ .join("\n");
205
+ expect(config.dockerfile).toContain(`COPY . .\n${envBlock}\nCMD ["bun","main.js"]`);
206
+ });
207
+
208
+ test("writes the Dockerfile instructions it wrote before its env came from getProductionEnv", () => {
209
+ //? 9adfb95c's output for this config; only the blank lines its empty interpolations left are gone since.
210
+ const before = [
211
+ "FROM oven/bun:1-slim",
212
+ "RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends ca-certificates tzdata && rm -rf /var/lib/apt/lists/*",
213
+ "RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime",
214
+ "ARG TARGETARCH",
215
+ "",
216
+ "RUN mkdir -p /workspace",
217
+ "WORKDIR /workspace",
218
+ "COPY ./package.json ./package.json",
219
+ "RUN bun install --production",
220
+ "",
221
+ "COPY . .",
222
+ "ENV PORT=8282",
223
+ "ENV NODE_ENV=production",
224
+ "ENV AKAN_PUBLIC_REPO_NAME=akanjs",
225
+ "ENV AKAN_PUBLIC_SERVE_DOMAIN=akanjs.com",
226
+ "ENV AKAN_PUBLIC_APP_NAME=portal",
227
+ "ENV AKAN_PUBLIC_ENV=debug",
228
+ "",
229
+ "ENV AKAN_PUBLIC_DEFAULT_LOCALE=en",
230
+ "ENV AKAN_PUBLIC_LOCALES=en,ko",
231
+ "ENV AKAN_PUBLIC_API_PREFIX=/api",
232
+ "ENV AKAN_PUBLIC_WS_PREFIX=/ws",
233
+ "ENV AKAN_PUBLIC_OPERATION_MODE=cloud",
234
+ "ENV AKAN_DATABASE_MODES=single",
235
+ "ENV AKAN_LOG_TO_FILE=0",
236
+ "",
237
+ 'CMD ["bun","main.js"]',
238
+ ];
239
+ const lines = (text: string) => text.split("\n").filter((line) => line !== "");
240
+
241
+ expect(lines(new AkanAppConfig(app, [], packageJson, {}, baseDevEnv).dockerfile)).toEqual(lines(before.join("\n")));
242
+ });
243
+
244
+ test("refuses a csr-less build that ships a native app", () => {
160
245
  expect(
161
- () => new AkanAppConfig(app, [], packageJson, { web: { csr: false }, mobile: { appName: "portal" } }, baseDevEnv),
162
- ).toThrow("the Capacitor build ships that bundle");
246
+ () => new AkanAppConfig(app, [], packageJson, { web: { csr: false }, native: { appName: "portal" } }, baseDevEnv),
247
+ ).toThrow("the native apps ship that bundle");
163
248
  });
164
249
 
165
250
  test("installs only ca-certificates and tzdata in the default image", () => {
@@ -249,31 +334,65 @@ describe("AkanAppConfig", () => {
249
334
  ...akanPackageJson.dependencies,
250
335
  ...akanPackageJson.peerDependencies,
251
336
  };
252
- const singleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "single" }, baseDevEnv);
253
- const multipleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "multiple" }, baseDevEnv);
254
- const clusterConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "cluster" }, baseDevEnv);
255
-
256
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("ioredis");
257
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("bullmq");
258
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
259
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("postgres");
260
- expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("protobufjs");
261
-
262
- expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
263
- "@libsql/client": runtimeDependencies["@libsql/client"],
337
+ const configOf = (database: { modes: ("single" | "multiple" | "cluster")[] }) =>
338
+ new AkanAppConfig(app, [], packageJson, { database }, baseDevEnv);
339
+ const dependenciesOf = (config: AkanAppConfig) => config.getProductionPackageJson().dependencies ?? {};
340
+ const singleConfig = configOf({ modes: ["single"] });
341
+ const multipleConfig = configOf({ modes: ["multiple"] });
342
+ const clusterConfig = configOf({ modes: ["cluster"] });
343
+ const edgeAndCloud = configOf({ modes: ["single", "cluster"] });
344
+
345
+ for (const driver of ["ioredis", "bullmq", "postgres", "@libsql/client", "protobufjs"])
346
+ expect(dependenciesOf(singleConfig)).not.toHaveProperty(driver);
347
+ expect(dependenciesOf(multipleConfig)).toMatchObject({
264
348
  bullmq: runtimeDependencies.bullmq,
265
349
  ioredis: runtimeDependencies.ioredis,
266
- protobufjs: runtimeDependencies.protobufjs,
267
350
  });
268
- expect(multipleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("postgres");
269
-
270
- expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
351
+ expect(dependenciesOf(clusterConfig)).toMatchObject({
271
352
  bullmq: runtimeDependencies.bullmq,
272
353
  ioredis: runtimeDependencies.ioredis,
273
354
  postgres: runtimeDependencies.postgres,
274
- protobufjs: runtimeDependencies.protobufjs,
275
355
  });
276
- expect(clusterConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
356
+ // multiple opens the same SQLite as single and Redis replaced protobuf on the wire, so neither ships by default.
357
+ for (const config of [multipleConfig, clusterConfig])
358
+ for (const driver of ["@libsql/client", "protobufjs"]) expect(dependenciesOf(config)).not.toHaveProperty(driver);
359
+ expect(dependenciesOf(multipleConfig)).not.toHaveProperty("postgres");
360
+ // One image for an edge site and a cloud cluster carries both modes' drivers, and says which it carries.
361
+ expect(dependenciesOf(edgeAndCloud)).toMatchObject({ postgres: runtimeDependencies.postgres });
362
+ expect(edgeAndCloud.dockerfile).toContain("ENV AKAN_DATABASE_MODES=single,cluster");
363
+ expect(singleConfig.dockerfile).toContain("ENV AKAN_DATABASE_MODES=single");
364
+ });
365
+
366
+ test("runs single when the app declares no mode", () => {
367
+ expect(new AkanAppConfig(app, [], packageJson, {}, baseDevEnv).database.modes).toEqual(["single"]);
368
+ });
369
+
370
+ test("refuses a mode name that is not one of the three, naming the file", () => {
371
+ expect(
372
+ () => new AkanAppConfig(app, [], packageJson, { database: { modes: ["clsuter" as "cluster"] } }, baseDevEnv),
373
+ ).toThrow("database.modes in apps/portal/akan.config.ts");
374
+ });
375
+
376
+ test("resolves a command's mode within the declared ones", () => {
377
+ const previous = process.env.AKAN_DATABASE_MODE;
378
+ try {
379
+ const config = new AkanAppConfig(
380
+ app,
381
+ [],
382
+ packageJson,
383
+ { database: { modes: ["single", "cluster"] } },
384
+ baseDevEnv,
385
+ );
386
+ delete process.env.AKAN_DATABASE_MODE;
387
+ expect(config.resolveDatabaseMode()).toBe("single");
388
+ process.env.AKAN_DATABASE_MODE = "cluster";
389
+ expect(config.resolveDatabaseMode()).toBe("cluster");
390
+ process.env.AKAN_DATABASE_MODE = "multiple";
391
+ expect(() => config.resolveDatabaseMode()).toThrow('Add "multiple" to database.modes');
392
+ } finally {
393
+ if (previous === undefined) delete process.env.AKAN_DATABASE_MODE;
394
+ else process.env.AKAN_DATABASE_MODE = previous;
395
+ }
277
396
  });
278
397
 
279
398
  test("resolves database mode runtime packages and missing install specs", () => {
@@ -300,69 +419,22 @@ describe("AkanAppConfig", () => {
300
419
  );
301
420
 
302
421
  expect(config.getDatabaseModeRuntimePackages("single")).toEqual([]);
303
- expect(config.getDatabaseModeRuntimePackages("multiple")).toEqual([
304
- "@libsql/client",
305
- "bullmq",
306
- "ioredis",
307
- "protobufjs",
308
- ]);
309
- expect(config.getDatabaseModeRuntimePackages("cluster")).toEqual(["bullmq", "ioredis", "postgres", "protobufjs"]);
310
- expect(config.getMissingDatabaseModeDependencySpecs("multiple")).toEqual([
311
- `@libsql/client@${runtimeDependencies["@libsql/client"]}`,
312
- `protobufjs@${runtimeDependencies.protobufjs}`,
313
- ]);
422
+ expect(config.getDatabaseModeRuntimePackages("multiple")).toEqual(["bullmq", "ioredis"]);
423
+ expect(config.getDatabaseModeRuntimePackages("cluster")).toEqual(["bullmq", "ioredis", "postgres"]);
424
+ expect(config.getMissingDatabaseModeDependencySpecs("multiple")).toEqual([]);
314
425
  expect(config.getMissingDatabaseModeDependencySpecs("cluster")).toEqual([
315
426
  `postgres@${runtimeDependencies.postgres}`,
316
- `protobufjs@${runtimeDependencies.protobufjs}`,
317
- ]);
318
- // The workspace-root install covers the toolchain/runtime plus every app Capacitor plugin
319
- // (deduped — "@capacitor/core" appears in both source lists).
320
- const expectedMobilePackages: string[] = [
321
- "@capacitor/cli",
322
- "@capacitor/core",
323
- "@capacitor/ios",
324
- "@capacitor/android",
325
- "@capacitor/assets",
326
- "@capacitor/app",
327
- "@capacitor/browser",
328
- "@capacitor/camera",
329
- "@capacitor/device",
330
- "@capacitor/geolocation",
331
- "@capacitor/haptics",
332
- "@capacitor/inappbrowser",
333
- "@capacitor/keyboard",
334
- "@capacitor/preferences",
335
- "@capacitor/push-notifications",
336
- "capacitor-plugin-safe-area",
337
- ];
338
- expect(config.getMobileRuntimePackages() as string[]).toEqual(expectedMobilePackages);
339
- expect(config.getMissingMobileDependencySpecs()).toEqual(
340
- expectedMobilePackages.map((lib) => `${lib}@${runtimeDependencies[lib as keyof typeof runtimeDependencies]}`),
341
- );
342
- expect(config.getMobileAppCapacitorPlugins()).toEqual([
343
- "@capacitor/app",
344
- "@capacitor/browser",
345
- "@capacitor/camera",
346
- "@capacitor/core",
347
- "@capacitor/device",
348
- "@capacitor/geolocation",
349
- "@capacitor/haptics",
350
- "@capacitor/inappbrowser",
351
- "@capacitor/keyboard",
352
- "@capacitor/preferences",
353
- "@capacitor/push-notifications",
354
- "capacitor-plugin-safe-area",
355
427
  ]);
356
428
  });
357
429
 
358
- test("normalizes multiple mobile targets and validates base paths", () => {
430
+ test("normalizes the native targets and validates base paths", () => {
359
431
  const config = new AkanAppConfig(
360
432
  app,
361
433
  [],
362
434
  packageJson,
363
435
  {
364
436
  routes: [{ basePath: "admin", domains: {} }],
365
- mobile: {
437
+ native: {
366
438
  appName: "Portal",
367
439
  appId: "com.portal.app",
368
440
  version: "1.0.0",
@@ -375,12 +447,9 @@ describe("AkanAppConfig", () => {
375
447
  appId: "com.portal.admin",
376
448
  buildNum: 8,
377
449
  permissions: ["camera"],
378
- deepLinks: {
379
- schemes: ["portal-admin", "portal-admin"],
380
- domains: ["https://Portal.Admin/"],
381
- ios: { teamId: " TEAMID " },
382
- android: { sha256CertFingerprints: ["AA:BB", "AA:BB"] },
383
- },
450
+ deepLinks: { schemes: ["portal-admin", "portal-admin"], domains: ["https://Portal.Admin/"] },
451
+ ios: { teamId: " TEAMID " },
452
+ android: { sha256CertFingerprints: ["AA:BB", "AA:BB"] },
384
453
  },
385
454
  },
386
455
  },
@@ -388,7 +457,7 @@ describe("AkanAppConfig", () => {
388
457
  baseDevEnv,
389
458
  );
390
459
 
391
- expect(config.mobile.targets.admin).toMatchObject({
460
+ expect(config.native.targets.admin).toMatchObject({
392
461
  name: "admin",
393
462
  basePath: "admin",
394
463
  indexPath: "/admin/home",
@@ -397,38 +466,216 @@ describe("AkanAppConfig", () => {
397
466
  version: "1.0.0",
398
467
  buildNum: 8,
399
468
  permissions: ["camera"],
400
- deepLinks: {
401
- schemes: ["portal-admin"],
402
- domains: ["portal.admin"],
403
- ios: { teamId: "TEAMID" },
404
- android: { sha256CertFingerprints: ["AA:BB"] },
469
+ deepLinks: { schemes: ["portal-admin"], domains: ["portal.admin"] },
470
+ ios: { teamId: "TEAMID" },
471
+ android: { sha256CertFingerprints: ["AA:BB"] },
472
+ });
473
+
474
+ expect(
475
+ () =>
476
+ new AkanAppConfig(app, [], packageJson, { native: { targets: { bad: { basePath: "missing" } } } }, baseDevEnv),
477
+ ).toThrow("unknown basePath");
478
+ });
479
+
480
+ test("an app without basePaths has one target, default, with no basePath and the section's settings", () => {
481
+ const config = new AkanAppConfig(
482
+ app,
483
+ [],
484
+ packageJson,
485
+ { native: { indexPath: "/explore", desktop: { server: true } } },
486
+ baseDevEnv,
487
+ );
488
+
489
+ expect(Object.keys(config.native.targets)).toEqual(["default"]);
490
+ expect(config.native.targets.default.basePath).toBeUndefined();
491
+ expect(config.native.targets.default).toMatchObject({ indexPath: "/explore", desktop: { server: true } });
492
+ });
493
+
494
+ test("a platform section may name its own indexPath, normalized like the section's", () => {
495
+ const config = new AkanAppConfig(
496
+ app,
497
+ [],
498
+ packageJson,
499
+ {
500
+ native: {
501
+ indexPath: "/mobile",
502
+ ios: { indexPath: "cockpit/" },
503
+ desktop: { indexPath: " / ", server: true },
504
+ },
405
505
  },
506
+ baseDevEnv,
507
+ );
508
+
509
+ expect(config.native.targets.default).toMatchObject({
510
+ indexPath: "/mobile",
511
+ ios: { indexPath: "/cockpit" },
512
+ desktop: { indexPath: "/", server: true },
406
513
  });
514
+ expect(config.native.targets.default.android).toBeUndefined();
515
+ });
407
516
 
517
+ test("a target takes the native section with its own fields over it: objects merge, lists and values replace", () => {
518
+ const config = new AkanAppConfig(
519
+ app,
520
+ [],
521
+ packageJson,
522
+ {
523
+ native: {
524
+ fileName: "portal",
525
+ plugins: ["iap"],
526
+ ios: { infoPlist: { ITSAppUsesNonExemptEncryption: false }, files: { "sound.caf": "assets/sound.caf" } },
527
+ android: {
528
+ manifest: ["<queries/>"],
529
+ googleServices: "secrets/google-services.json",
530
+ files: { "res/raw/chime.mp3": "assets/chime.mp3" },
531
+ },
532
+ targets: {
533
+ default: {
534
+ plugins: ["share"],
535
+ ios: { files: { "extra.caf": "assets/extra.caf" } },
536
+ android: { manifest: ["<uses-feature/>"] },
537
+ },
538
+ },
539
+ },
540
+ },
541
+ baseDevEnv,
542
+ );
543
+
544
+ expect(config.native.targets.default).toMatchObject({
545
+ fileName: "portal",
546
+ plugins: ["share"],
547
+ ios: {
548
+ infoPlist: { ITSAppUsesNonExemptEncryption: false },
549
+ files: { "sound.caf": "assets/sound.caf", "extra.caf": "assets/extra.caf" },
550
+ },
551
+ android: {
552
+ manifest: ["<uses-feature/>"],
553
+ googleServices: "secrets/google-services.json",
554
+ files: { "res/raw/chime.mp3": "assets/chime.mp3" },
555
+ },
556
+ });
557
+ });
558
+
559
+ test("gives every target the section's updates, a target overriding a field", () => {
560
+ const updates = { url: "https://releases.example.com/portal", publicKey: "key=" };
561
+ const config = new AkanAppConfig(
562
+ app,
563
+ [],
564
+ packageJson,
565
+ { native: { updates, targets: { default: {}, pilot: { updates: { channel: "pilot" } } } } },
566
+ baseDevEnv,
567
+ );
568
+
569
+ expect(config.native.targets.default.updates).toEqual(updates);
570
+ expect(config.native.targets.pilot.updates).toEqual({ ...updates, channel: "pilot" });
408
571
  expect(
409
572
  () =>
410
573
  new AkanAppConfig(
411
574
  app,
412
575
  [],
413
576
  packageJson,
414
- {
415
- mobile: { targets: { bad: { basePath: "missing" } } },
416
- },
577
+ { native: { targets: { pilot: { updates: { channel: "pilot" } } } } },
417
578
  baseDevEnv,
418
579
  ),
419
- ).toThrow("unknown basePath");
580
+ ).toThrow("native.targets.pilot.updates in apps/portal/akan.config.ts has no url or publicKey");
581
+ expect(
582
+ () => new AkanAppConfig(app, [], packageJson, { native: { updates: { channel: "pilot" } } }, baseDevEnv),
583
+ ).toThrow("native.updates in apps/portal/akan.config.ts has no url or publicKey.");
584
+ });
585
+
586
+ test("merges the desktop settings field by field, the target winning", () => {
587
+ const config = new AkanAppConfig(
588
+ app,
589
+ [],
590
+ packageJson,
591
+ {
592
+ native: {
593
+ desktop: { recovery: "reload", window: { fullscreen: true } },
594
+ targets: { default: { desktop: { window: { skipTaskbar: true } } } },
595
+ },
596
+ },
597
+ baseDevEnv,
598
+ );
599
+
600
+ expect(config.native.targets.default.desktop).toEqual({
601
+ recovery: "reload",
602
+ window: { fullscreen: true, skipTaskbar: true },
603
+ });
604
+ });
605
+
606
+ test("merges the section's push and privacy into each target field by field; an icon object is one value", () => {
607
+ const publicKey = Buffer.alloc(32, 7).toString("base64");
608
+ const config = new AkanAppConfig(
609
+ app,
610
+ [],
611
+ packageJson,
612
+ {
613
+ native: {
614
+ updates: { url: "https://updates.example.com/portal", publicKey },
615
+ icon: { image: "assets/icon.png", backgroundColor: "#000000" },
616
+ android: { push: { color: "#ff5a5f" } },
617
+ ios: { privacy: { tracking: false } },
618
+ targets: {
619
+ default: {},
620
+ beta: {
621
+ updates: { channel: "beta" },
622
+ icon: { image: "assets/beta.png" },
623
+ android: { push: { smallIcon: "assets/noti.png" } },
624
+ },
625
+ },
626
+ },
627
+ },
628
+ baseDevEnv,
629
+ );
630
+
631
+ expect(config.native.targets.default.updates).toEqual({ url: "https://updates.example.com/portal", publicKey });
632
+ expect(config.native.targets.beta).toMatchObject({
633
+ updates: { url: "https://updates.example.com/portal", publicKey, channel: "beta" },
634
+ android: { push: { color: "#ff5a5f", smallIcon: "assets/noti.png" } },
635
+ ios: { privacy: { tracking: false } },
636
+ });
637
+ expect(config.native.targets.beta.icon).toEqual({ image: "assets/beta.png" });
638
+ });
639
+
640
+ test("refuses `mobile` and every setting that moved, naming where it went", () => {
641
+ const make = (config: Record<string, unknown>) => () =>
642
+ new AkanAppConfig(app, [], packageJson, config as never, baseDevEnv);
643
+
644
+ expect(make({ mobile: { appName: "portal" } })).toThrow("declares `mobile`, which is now `native`");
645
+ expect(make({ native: { assets: { icon: "assets/icon.png" } } })).toThrow(
646
+ "native.assets in apps/portal/akan.config.ts has moved: icon and splash sit directly in the native section.",
647
+ );
648
+ expect(make({ native: { targets: { default: { native: { desktop: { server: true } } } } } })).toThrow(
649
+ "native.targets.default.native in apps/portal/akan.config.ts has moved",
650
+ );
651
+ expect(make({ native: { files: { "ios/sound.caf": "assets/sound.caf" } } })).toThrow(
652
+ "native.files in apps/portal/akan.config.ts has moved: ios.files",
653
+ );
654
+ expect(make({ native: { deepLinks: { ios: { teamId: "TEAMID" } } } })).toThrow(
655
+ "native.deepLinks.ios in apps/portal/akan.config.ts has moved: ios.teamId.",
656
+ );
657
+ expect(make({ native: { android: { files: { "App/x.json": "x.json" } } } })).toThrow(
658
+ 'native.android.files["App/x.json"] in apps/portal/akan.config.ts must land at res/<type>/<file> or assets/<path>.',
659
+ );
660
+ expect(make({ native: { targets: { kiosk: { ios: { files: { "../x.caf": "x.caf" } } } } } })).toThrow(
661
+ 'native.targets.kiosk.ios.files["../x.caf"] in apps/portal/akan.config.ts must land at <path in the app bundle>.',
662
+ );
663
+ expect(make({ native: { ios: { scheme: "App" } } })).toThrow(
664
+ "native.ios.scheme in apps/portal/akan.config.ts is not a native setting",
665
+ );
666
+ expect(make({ native: { server: { url: "http://x" } } })).toThrow(
667
+ "native.server in apps/portal/akan.config.ts is not a native setting",
668
+ );
420
669
  });
421
670
 
422
- test("derives a repo-scoped default appId and records explicit-mobile intent", () => {
423
- // No mobile section: appId defaults to the repo-scoped reverse-DNS id, and mobile is not explicit.
424
- const withoutMobile = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
425
- expect(withoutMobile.mobile.appId).toBe("com.akanjs.portal");
426
- expect(withoutMobile.hasMobileConfig).toBe(false);
671
+ test("derives a repo-scoped default appId and records an explicit native section", () => {
672
+ const withoutNative = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
673
+ expect(withoutNative.native.appId).toBe("com.akanjs.portal");
674
+ expect(withoutNative.hasNativeConfig).toBe(false);
427
675
 
428
- // Explicit mobile section without appId still uses the repo-scoped default but marks intent.
429
- const withMobile = new AkanAppConfig(app, [], packageJson, { mobile: { version: "2.0.0" } }, baseDevEnv);
430
- expect(withMobile.mobile.appId).toBe("com.akanjs.portal");
431
- expect(withMobile.hasMobileConfig).toBe(true);
676
+ const withNative = new AkanAppConfig(app, [], packageJson, { native: { version: "2.0.0" } }, baseDevEnv);
677
+ expect(withNative.native.appId).toBe("com.akanjs.portal");
678
+ expect(withNative.hasNativeConfig).toBe(true);
432
679
  });
433
680
  });
434
681
 
@@ -466,25 +713,13 @@ describe("AkanAppConfig lib externalLibs", () => {
466
713
  });
467
714
 
468
715
  test("reads them off every workspace lib config on load", async () => {
469
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libext-"));
470
- try {
471
- fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
472
- fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
473
- fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default { externalLibs: ['shiki'] };\n");
474
- fs.writeFileSync(
475
- path.join(root, "libs/extlib/akan.config.ts"),
476
- "export default { externalLibs: ['puppeteer'] };\n",
477
- );
478
- fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
479
- fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
480
-
481
- const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
482
- const config = await AppExecutor.from(workspace, "extapp").getConfig();
716
+ const config = await loadExtAppConfig(
717
+ "akan-config-libext-",
718
+ "export default { externalLibs: ['shiki'] };\n",
719
+ "export default { externalLibs: ['puppeteer'] };\n",
720
+ );
483
721
 
484
- expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
485
- } finally {
486
- fs.rmSync(root, { recursive: true, force: true });
487
- }
722
+ expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
488
723
  });
489
724
  });
490
725
 
@@ -528,26 +763,77 @@ describe("AkanAppConfig lib docker runs", () => {
528
763
  });
529
764
 
530
765
  test("reads them off every workspace lib config on load", async () => {
531
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libdocker-"));
532
- try {
533
- fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
534
- fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
535
- fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default {};\n");
536
- fs.writeFileSync(
537
- path.join(root, "libs/extlib/akan.config.ts"),
538
- "export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
539
- );
540
- fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
541
- fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
766
+ const config = await loadExtAppConfig(
767
+ "akan-config-libdocker-",
768
+ "export default {};\n",
769
+ "export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
770
+ );
542
771
 
543
- const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
544
- const config = await AppExecutor.from(workspace, "extapp").getConfig();
772
+ expect(config.dockerfile).toContain("RUN echo from-lib");
773
+ expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
774
+ });
775
+ });
545
776
 
546
- expect(config.dockerfile).toContain("RUN echo from-lib");
547
- expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
548
- } finally {
549
- fs.rmSync(root, { recursive: true, force: true });
550
- }
777
+ describe("AkanAppConfig trustedDependencies and bin", () => {
778
+ const sha256 = "b".repeat(64);
779
+ const libBin = { ffmpeg: { "linux-x64": { url: "https://files.test/ffmpeg.tar.xz", sha256, file: "bin/ffmpeg" } } };
780
+
781
+ test("the production package.json trusts the app's and its libs' packages, and nothing when none are named", () => {
782
+ const withTrusted = new AkanAppConfig(
783
+ app,
784
+ [],
785
+ packageJson,
786
+ { trustedDependencies: [" rclnodejs ", "sharp"] },
787
+ baseDevEnv,
788
+ [],
789
+ {
790
+ externalLibs: [],
791
+ trustedDependencies: ["sharp", "@serialport/bindings-cpp"],
792
+ docker: { preRuns: [], postRuns: [] },
793
+ },
794
+ );
795
+ expect(withTrusted.getProductionPackageJson().trustedDependencies).toEqual([
796
+ "rclnodejs",
797
+ "sharp",
798
+ "@serialport/bindings-cpp",
799
+ ]);
800
+ expect(new AkanAppConfig(app, [], packageJson, {}, baseDevEnv).getProductionPackageJson()).not.toHaveProperty(
801
+ "trustedDependencies",
802
+ );
803
+ expect(() => new AkanAppConfig(app, [], packageJson, { trustedDependencies: [""] }, baseDevEnv)).toThrow(
804
+ "apps/portal/akan.config.ts: trustedDependencies lists package names",
805
+ );
806
+ });
807
+
808
+ test("keeps the app's bin apart from each lib's, with paths made absolute where they were declared", () => {
809
+ const config = new AkanAppConfig(
810
+ { name: "portal", cwdPath: "/repo/apps/portal" } as never,
811
+ [],
812
+ packageJson,
813
+ { bin: { ffmpeg: { "darwin-arm64": { path: "tools/ffmpeg" } } } },
814
+ baseDevEnv,
815
+ [],
816
+ { externalLibs: [], docker: { preRuns: [], postRuns: [] }, bin: [{ lib: "media", bin: libBin }] },
817
+ );
818
+ expect(config.bin).toEqual({
819
+ ffmpeg: { "darwin-arm64": { path: path.resolve("/repo/apps/portal/tools/ffmpeg") } },
820
+ });
821
+ expect(config.libBins).toEqual([{ lib: "media", bin: libBin }]);
822
+ expect(
823
+ new AkanLibConfig({ name: "media", cwdPath: "/repo/libs/media" } as never, {
824
+ bin: { ffprobe: { "linux-x64": { path: "../../tools/ffprobe" } } },
825
+ }).bin,
826
+ ).toEqual({ ffprobe: { "linux-x64": { path: path.resolve("/repo/tools/ffprobe") } } });
827
+ });
828
+
829
+ test("reads them off every workspace lib config on load", async () => {
830
+ const config = await loadExtAppConfig(
831
+ "akan-config-libbin-",
832
+ "export default { trustedDependencies: ['sharp'] };\n",
833
+ `export default { trustedDependencies: ['rclnodejs'], bin: ${JSON.stringify(libBin)} };\n`,
834
+ );
835
+ expect(config.trustedDependencies).toEqual(["sharp", "rclnodejs"]);
836
+ expect(config.libBins).toEqual([{ lib: "extlib", bin: libBin }]);
551
837
  });
552
838
  });
553
839
 
@@ -556,7 +842,7 @@ describe("AkanLibConfig", () => {
556
842
  const lib = { name: "shared" } as never;
557
843
  expect(new AkanLibConfig(lib, {}).externalLibs).toEqual([]);
558
844
 
559
- const config: DeepPartial<LibConfigResult> = {
845
+ const config: LibConfigInput = {
560
846
  externalLibs: ["firebase-admin"],
561
847
  };
562
848
  expect(new AkanLibConfig(lib, config).externalLibs).toEqual(["firebase-admin"]);