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

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 +404 -141
  19. package/akanConfig/akanConfig.ts +317 -274
  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 +151 -0
  250. package/mobile/nativeApp.ts +405 -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 +296 -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,193 @@ 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 target takes the native section with its own fields over it: objects merge, lists and values replace", () => {
495
+ const config = new AkanAppConfig(
496
+ app,
497
+ [],
498
+ packageJson,
499
+ {
500
+ native: {
501
+ fileName: "portal",
502
+ plugins: ["iap"],
503
+ ios: { infoPlist: { ITSAppUsesNonExemptEncryption: false }, files: { "sound.caf": "assets/sound.caf" } },
504
+ android: {
505
+ manifest: ["<queries/>"],
506
+ googleServices: "secrets/google-services.json",
507
+ files: { "res/raw/chime.mp3": "assets/chime.mp3" },
508
+ },
509
+ targets: {
510
+ default: {
511
+ plugins: ["share"],
512
+ ios: { files: { "extra.caf": "assets/extra.caf" } },
513
+ android: { manifest: ["<uses-feature/>"] },
514
+ },
515
+ },
516
+ },
517
+ },
518
+ baseDevEnv,
519
+ );
520
+
521
+ expect(config.native.targets.default).toMatchObject({
522
+ fileName: "portal",
523
+ plugins: ["share"],
524
+ ios: {
525
+ infoPlist: { ITSAppUsesNonExemptEncryption: false },
526
+ files: { "sound.caf": "assets/sound.caf", "extra.caf": "assets/extra.caf" },
527
+ },
528
+ android: {
529
+ manifest: ["<uses-feature/>"],
530
+ googleServices: "secrets/google-services.json",
531
+ files: { "res/raw/chime.mp3": "assets/chime.mp3" },
405
532
  },
406
533
  });
534
+ });
535
+
536
+ test("gives every target the section's updates, a target overriding a field", () => {
537
+ const updates = { url: "https://releases.example.com/portal", publicKey: "key=" };
538
+ const config = new AkanAppConfig(
539
+ app,
540
+ [],
541
+ packageJson,
542
+ { native: { updates, targets: { default: {}, pilot: { updates: { channel: "pilot" } } } } },
543
+ baseDevEnv,
544
+ );
407
545
 
546
+ expect(config.native.targets.default.updates).toEqual(updates);
547
+ expect(config.native.targets.pilot.updates).toEqual({ ...updates, channel: "pilot" });
408
548
  expect(
409
549
  () =>
410
550
  new AkanAppConfig(
411
551
  app,
412
552
  [],
413
553
  packageJson,
414
- {
415
- mobile: { targets: { bad: { basePath: "missing" } } },
416
- },
554
+ { native: { targets: { pilot: { updates: { channel: "pilot" } } } } },
417
555
  baseDevEnv,
418
556
  ),
419
- ).toThrow("unknown basePath");
557
+ ).toThrow("native.targets.pilot.updates in apps/portal/akan.config.ts has no url or publicKey");
558
+ expect(
559
+ () => new AkanAppConfig(app, [], packageJson, { native: { updates: { channel: "pilot" } } }, baseDevEnv),
560
+ ).toThrow("native.updates in apps/portal/akan.config.ts has no url or publicKey.");
420
561
  });
421
562
 
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);
563
+ test("merges the desktop settings field by field, the target winning", () => {
564
+ const config = new AkanAppConfig(
565
+ app,
566
+ [],
567
+ packageJson,
568
+ {
569
+ native: {
570
+ desktop: { recovery: "reload", window: { fullscreen: true } },
571
+ targets: { default: { desktop: { window: { skipTaskbar: true } } } },
572
+ },
573
+ },
574
+ baseDevEnv,
575
+ );
576
+
577
+ expect(config.native.targets.default.desktop).toEqual({
578
+ recovery: "reload",
579
+ window: { fullscreen: true, skipTaskbar: true },
580
+ });
581
+ });
582
+
583
+ test("merges the section's push and privacy into each target field by field; an icon object is one value", () => {
584
+ const publicKey = Buffer.alloc(32, 7).toString("base64");
585
+ const config = new AkanAppConfig(
586
+ app,
587
+ [],
588
+ packageJson,
589
+ {
590
+ native: {
591
+ updates: { url: "https://updates.example.com/portal", publicKey },
592
+ icon: { image: "assets/icon.png", backgroundColor: "#000000" },
593
+ android: { push: { color: "#ff5a5f" } },
594
+ ios: { privacy: { tracking: false } },
595
+ targets: {
596
+ default: {},
597
+ beta: {
598
+ updates: { channel: "beta" },
599
+ icon: { image: "assets/beta.png" },
600
+ android: { push: { smallIcon: "assets/noti.png" } },
601
+ },
602
+ },
603
+ },
604
+ },
605
+ baseDevEnv,
606
+ );
607
+
608
+ expect(config.native.targets.default.updates).toEqual({ url: "https://updates.example.com/portal", publicKey });
609
+ expect(config.native.targets.beta).toMatchObject({
610
+ updates: { url: "https://updates.example.com/portal", publicKey, channel: "beta" },
611
+ android: { push: { color: "#ff5a5f", smallIcon: "assets/noti.png" } },
612
+ ios: { privacy: { tracking: false } },
613
+ });
614
+ expect(config.native.targets.beta.icon).toEqual({ image: "assets/beta.png" });
615
+ });
616
+
617
+ test("refuses `mobile` and every setting that moved, naming where it went", () => {
618
+ const make = (config: Record<string, unknown>) => () =>
619
+ new AkanAppConfig(app, [], packageJson, config as never, baseDevEnv);
620
+
621
+ expect(make({ mobile: { appName: "portal" } })).toThrow("declares `mobile`, which is now `native`");
622
+ expect(make({ native: { assets: { icon: "assets/icon.png" } } })).toThrow(
623
+ "native.assets in apps/portal/akan.config.ts has moved: icon and splash sit directly in the native section.",
624
+ );
625
+ expect(make({ native: { targets: { default: { native: { desktop: { server: true } } } } } })).toThrow(
626
+ "native.targets.default.native in apps/portal/akan.config.ts has moved",
627
+ );
628
+ expect(make({ native: { files: { "ios/sound.caf": "assets/sound.caf" } } })).toThrow(
629
+ "native.files in apps/portal/akan.config.ts has moved: ios.files",
630
+ );
631
+ expect(make({ native: { deepLinks: { ios: { teamId: "TEAMID" } } } })).toThrow(
632
+ "native.deepLinks.ios in apps/portal/akan.config.ts has moved: ios.teamId.",
633
+ );
634
+ expect(make({ native: { android: { files: { "App/x.json": "x.json" } } } })).toThrow(
635
+ 'native.android.files["App/x.json"] in apps/portal/akan.config.ts must land at res/<type>/<file> or assets/<path>.',
636
+ );
637
+ expect(make({ native: { targets: { kiosk: { ios: { files: { "../x.caf": "x.caf" } } } } } })).toThrow(
638
+ 'native.targets.kiosk.ios.files["../x.caf"] in apps/portal/akan.config.ts must land at <path in the app bundle>.',
639
+ );
640
+ expect(make({ native: { ios: { scheme: "App" } } })).toThrow(
641
+ "native.ios.scheme in apps/portal/akan.config.ts is not a native setting",
642
+ );
643
+ expect(make({ native: { server: { url: "http://x" } } })).toThrow(
644
+ "native.server in apps/portal/akan.config.ts is not a native setting",
645
+ );
646
+ });
647
+
648
+ test("derives a repo-scoped default appId and records an explicit native section", () => {
649
+ const withoutNative = new AkanAppConfig(app, [], packageJson, {}, baseDevEnv);
650
+ expect(withoutNative.native.appId).toBe("com.akanjs.portal");
651
+ expect(withoutNative.hasNativeConfig).toBe(false);
427
652
 
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);
653
+ const withNative = new AkanAppConfig(app, [], packageJson, { native: { version: "2.0.0" } }, baseDevEnv);
654
+ expect(withNative.native.appId).toBe("com.akanjs.portal");
655
+ expect(withNative.hasNativeConfig).toBe(true);
432
656
  });
433
657
  });
434
658
 
@@ -466,25 +690,13 @@ describe("AkanAppConfig lib externalLibs", () => {
466
690
  });
467
691
 
468
692
  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();
693
+ const config = await loadExtAppConfig(
694
+ "akan-config-libext-",
695
+ "export default { externalLibs: ['shiki'] };\n",
696
+ "export default { externalLibs: ['puppeteer'] };\n",
697
+ );
483
698
 
484
- expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
485
- } finally {
486
- fs.rmSync(root, { recursive: true, force: true });
487
- }
699
+ expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
488
700
  });
489
701
  });
490
702
 
@@ -528,26 +740,77 @@ describe("AkanAppConfig lib docker runs", () => {
528
740
  });
529
741
 
530
742
  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");
743
+ const config = await loadExtAppConfig(
744
+ "akan-config-libdocker-",
745
+ "export default {};\n",
746
+ "export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
747
+ );
748
+
749
+ expect(config.dockerfile).toContain("RUN echo from-lib");
750
+ expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
751
+ });
752
+ });
542
753
 
543
- const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
544
- const config = await AppExecutor.from(workspace, "extapp").getConfig();
754
+ describe("AkanAppConfig trustedDependencies and bin", () => {
755
+ const sha256 = "b".repeat(64);
756
+ const libBin = { ffmpeg: { "linux-x64": { url: "https://files.test/ffmpeg.tar.xz", sha256, file: "bin/ffmpeg" } } };
545
757
 
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
- }
758
+ test("the production package.json trusts the app's and its libs' packages, and nothing when none are named", () => {
759
+ const withTrusted = new AkanAppConfig(
760
+ app,
761
+ [],
762
+ packageJson,
763
+ { trustedDependencies: [" rclnodejs ", "sharp"] },
764
+ baseDevEnv,
765
+ [],
766
+ {
767
+ externalLibs: [],
768
+ trustedDependencies: ["sharp", "@serialport/bindings-cpp"],
769
+ docker: { preRuns: [], postRuns: [] },
770
+ },
771
+ );
772
+ expect(withTrusted.getProductionPackageJson().trustedDependencies).toEqual([
773
+ "rclnodejs",
774
+ "sharp",
775
+ "@serialport/bindings-cpp",
776
+ ]);
777
+ expect(new AkanAppConfig(app, [], packageJson, {}, baseDevEnv).getProductionPackageJson()).not.toHaveProperty(
778
+ "trustedDependencies",
779
+ );
780
+ expect(() => new AkanAppConfig(app, [], packageJson, { trustedDependencies: [""] }, baseDevEnv)).toThrow(
781
+ "apps/portal/akan.config.ts: trustedDependencies lists package names",
782
+ );
783
+ });
784
+
785
+ test("keeps the app's bin apart from each lib's, with paths made absolute where they were declared", () => {
786
+ const config = new AkanAppConfig(
787
+ { name: "portal", cwdPath: "/repo/apps/portal" } as never,
788
+ [],
789
+ packageJson,
790
+ { bin: { ffmpeg: { "darwin-arm64": { path: "tools/ffmpeg" } } } },
791
+ baseDevEnv,
792
+ [],
793
+ { externalLibs: [], docker: { preRuns: [], postRuns: [] }, bin: [{ lib: "media", bin: libBin }] },
794
+ );
795
+ expect(config.bin).toEqual({
796
+ ffmpeg: { "darwin-arm64": { path: path.resolve("/repo/apps/portal/tools/ffmpeg") } },
797
+ });
798
+ expect(config.libBins).toEqual([{ lib: "media", bin: libBin }]);
799
+ expect(
800
+ new AkanLibConfig({ name: "media", cwdPath: "/repo/libs/media" } as never, {
801
+ bin: { ffprobe: { "linux-x64": { path: "../../tools/ffprobe" } } },
802
+ }).bin,
803
+ ).toEqual({ ffprobe: { "linux-x64": { path: path.resolve("/repo/tools/ffprobe") } } });
804
+ });
805
+
806
+ test("reads them off every workspace lib config on load", async () => {
807
+ const config = await loadExtAppConfig(
808
+ "akan-config-libbin-",
809
+ "export default { trustedDependencies: ['sharp'] };\n",
810
+ `export default { trustedDependencies: ['rclnodejs'], bin: ${JSON.stringify(libBin)} };\n`,
811
+ );
812
+ expect(config.trustedDependencies).toEqual(["sharp", "rclnodejs"]);
813
+ expect(config.libBins).toEqual([{ lib: "extlib", bin: libBin }]);
551
814
  });
552
815
  });
553
816
 
@@ -556,7 +819,7 @@ describe("AkanLibConfig", () => {
556
819
  const lib = { name: "shared" } as never;
557
820
  expect(new AkanLibConfig(lib, {}).externalLibs).toEqual([]);
558
821
 
559
- const config: DeepPartial<LibConfigResult> = {
822
+ const config: LibConfigInput = {
560
823
  externalLibs: ["firebase-admin"],
561
824
  };
562
825
  expect(new AkanLibConfig(lib, config).externalLibs).toEqual(["firebase-admin"]);