@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
@@ -1,3 +1,5 @@
1
+ import { realpathSync } from "node:fs";
2
+ import path from "node:path";
1
3
  import { Logger } from "akanjs/common";
2
4
 
3
5
  export interface ApplicationBuildPhaseResult {
@@ -35,12 +37,12 @@ export class ApplicationBuildReporter {
35
37
  Logger.rawLog(`Done in ${ApplicationBuildReporter.formatDuration(result.durationMs)}`);
36
38
  }
37
39
 
38
- static formatError(error: unknown): string {
40
+ static formatError(error: unknown, workspaceRoot = process.cwd()): string {
39
41
  if (error instanceof AggregateError) {
40
42
  const nestedMessages = error.errors
41
43
  .map(
42
44
  (nestedError, index) =>
43
- ApplicationBuildReporter.formatError(nestedError).trim() || `Unknown error ${index + 1}`,
45
+ ApplicationBuildReporter.formatError(nestedError, workspaceRoot).trim() || `Unknown error ${index + 1}`,
44
46
  )
45
47
  .map((message) => message.replace(/^/gm, " "))
46
48
  .join("\n");
@@ -48,13 +50,43 @@ export class ApplicationBuildReporter {
48
50
  return nestedMessages ? `${error.message}\n${nestedMessages}` : error.message;
49
51
  }
50
52
  if (error instanceof Error) {
51
- const causeMessage = error.cause ? `\nCaused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
52
- return `${error.message}${causeMessage}`;
53
+ const causeMessage = error.cause
54
+ ? `\nCaused by: ${ApplicationBuildReporter.formatError(error.cause, workspaceRoot)}`
55
+ : "";
56
+ return `${ApplicationBuildReporter.#withLocation(error.message, error, workspaceRoot)}${causeMessage}`;
53
57
  }
54
- if (typeof error === "object" && error !== null && "message" in error) return String(error.message);
58
+ if (typeof error === "object" && error !== null && "message" in error)
59
+ return ApplicationBuildReporter.#withLocation(String(error.message), error, workspaceRoot);
55
60
  return String(error);
56
61
  }
57
62
 
63
+ static #withLocation(message: string, error: object, workspaceRoot: string): string {
64
+ const { position } = error as { position?: { file: string; line: number; column: number } | null };
65
+ if (!position?.file) return message;
66
+ const file = ApplicationBuildReporter.#relativeToWorkspace(position.file, workspaceRoot);
67
+ const location = position.line > 0 ? `${file}:${position.line}:${position.column}` : file;
68
+ return message.includes(location) ? message : message.replace(/^.*/, (headline) => `${headline} (${location})`);
69
+ }
70
+
71
+ // Bun names the file by its realpath, so a workspace root reached through a symlink is matched by its own too.
72
+ static #relativeToWorkspace(file: string, workspaceRoot: string): string {
73
+ if (!path.isAbsolute(file)) return file;
74
+ for (const root of [workspaceRoot, ApplicationBuildReporter.#realpathOf(workspaceRoot)]) {
75
+ const relative = path.relative(root, file);
76
+ if (!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
77
+ return relative.replaceAll(path.sep, "/");
78
+ }
79
+ return file;
80
+ }
81
+
82
+ static #realpathOf(dir: string): string {
83
+ try {
84
+ return realpathSync(dir);
85
+ } catch {
86
+ return dir;
87
+ }
88
+ }
89
+
58
90
  static formatDuration(ms: number): string {
59
91
  if (ms < 1000) return `${ms}ms`;
60
92
  if (ms < 60_000) return `${Math.round(ms / 100) / 10}s`;
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { rm } from "node:fs/promises";
3
- import { AKAN_BACKEND_MINIFY, AKAN_OPTIONAL_BACKEND_EXTERNALS } from "./applicationBuildRunner";
2
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { AkanAppConfig } from "./akanConfig";
6
+ import { AKAN_BACKEND_MINIFY, AKAN_OPTIONAL_BACKEND_EXTERNALS, ApplicationBuildRunner } from "./applicationBuildRunner";
7
+ import type { App } from "./commandDecorators";
8
+ import { Executor } from "./executors";
4
9
 
5
10
  describe("ApplicationBuildRunner", () => {
6
11
  test("externalizes Akan optional backend dependencies", () => {
@@ -16,8 +21,8 @@ describe("ApplicationBuildRunner", () => {
16
21
  });
17
22
 
18
23
  test("a bundler that mangles identifiers loses the class name a Logger reads", async () => {
19
- const dir = `${process.env.TMPDIR ?? "/tmp"}/akan-minify-${Bun.randomUUIDv7()}`;
20
- const entry = `${dir}/entry.ts`;
24
+ const dir = path.join(os.tmpdir(), `akan-minify-${Bun.randomUUIDv7()}`);
25
+ const entry = path.join(dir, "entry.ts");
21
26
  await Bun.write(
22
27
  entry,
23
28
  `const serve = () => class Service { readonly name = this.constructor.name; };
@@ -26,7 +31,7 @@ console.info(new SampleService().name);
26
31
  `,
27
32
  );
28
33
  const build = async (minify: Bun.BuildConfig["minify"]) => {
29
- const result = await Bun.build({ entrypoints: [entry], target: "bun", minify, outdir: `${dir}/out` });
34
+ const result = await Bun.build({ entrypoints: [entry], target: "bun", minify, outdir: path.join(dir, "out") });
30
35
  expect(result.success).toBe(true);
31
36
  const proc = Bun.spawn([process.execPath, result.outputs[0].path], { stdout: "pipe" });
32
37
  return (await new Response(proc.stdout).text()).trim();
@@ -42,4 +47,35 @@ console.info(new SampleService().name);
42
47
  await rm(dir, { recursive: true, force: true });
43
48
  }
44
49
  });
50
+
51
+ test("an image of a --env build boots the env its server carries, not the root .env's", async () => {
52
+ const root = await mkdtemp(path.join(os.tmpdir(), "akan-build-meta-"));
53
+ const baseDevEnv = {
54
+ repoName: "akanjs",
55
+ serveDomain: "akanjs.com",
56
+ env: "local" as const,
57
+ portOffset: 0,
58
+ workspaceRoot: root,
59
+ };
60
+ const config = new AkanAppConfig(
61
+ { name: "portal" } as never,
62
+ [],
63
+ { name: "repo", version: "1.0.0", description: "repo" },
64
+ {},
65
+ baseDevEnv,
66
+ );
67
+ const app = {
68
+ getConfig: async () => config,
69
+ dist: new Executor("dist", root),
70
+ workspace: { workspaceRoot: root },
71
+ } as unknown as App;
72
+ try {
73
+ await new ApplicationBuildRunner(app, { environment: "debug" }).buildAppMeta();
74
+ expect(await readFile(path.join(root, "Dockerfile"), "utf8")).toContain("ENV AKAN_PUBLIC_ENV=debug\n");
75
+ await new ApplicationBuildRunner(app).buildAppMeta();
76
+ expect(await readFile(path.join(root, "Dockerfile"), "utf8")).toContain("ENV AKAN_PUBLIC_ENV=local\n");
77
+ } finally {
78
+ await rm(root, { recursive: true, force: true });
79
+ }
80
+ });
45
81
  });
@@ -15,6 +15,7 @@ import {
15
15
  SsrBaseArtifactBuilder,
16
16
  } from "./frontendBuild";
17
17
  import { Spinner } from "./spinner";
18
+ import { createServerEnvPlugin } from "./transforms/serverEnvPlugin";
18
19
 
19
20
  export interface TypecheckOptions {
20
21
  clean?: boolean;
@@ -29,6 +30,8 @@ export type BuildProgressReporter = ApplicationBuildProgressReporter;
29
30
  export interface ApplicationBuildRunnerOptions {
30
31
  fast?: boolean;
31
32
  reporter?: BuildProgressReporter;
33
+ /** The backend env the build is for, when it is not the workspace's own (a mobile or desktop build's `--env`). */
34
+ environment?: string;
32
35
  }
33
36
  export interface BuildOptions {
34
37
  spinner?: boolean;
@@ -60,9 +63,8 @@ const SSR_RENDER_EXTERNALS = [
60
63
  "react-server-dom-webpack/client.browser",
61
64
  ] as const;
62
65
 
63
- // Identifier mangling renames every class, and `this.constructor.name` is what names a service's logger, an
64
- // `Exception`, a guard, and every frame of a stack trace — for ~2% of boot on server bytes nothing downloads.
65
- // `minify.keepNames` typechecks and does nothing as of Bun 1.4.2.
66
+ // Mangling renames classes, and `this.constructor.name` names loggers, exceptions and guards; `minify.keepNames`
67
+ // is a no-op as of Bun 1.4.2.
66
68
  export const AKAN_BACKEND_MINIFY = { whitespace: true, syntax: true, identifiers: false } as const;
67
69
 
68
70
  export const AKAN_OPTIONAL_BACKEND_EXTERNALS = [
@@ -77,29 +79,29 @@ export class ApplicationBuildRunner {
77
79
  #app: App;
78
80
  #fast: boolean;
79
81
  #reporter?: BuildProgressReporter;
82
+ #environment?: string;
83
+ #spinner?: boolean;
80
84
  #startedAt = Date.now();
81
85
  #phases: BuildPhaseResult[] = [];
82
86
 
83
- constructor(app: App, { fast = false, reporter }: ApplicationBuildRunnerOptions = {}) {
87
+ constructor(app: App, { fast = false, reporter, environment }: ApplicationBuildRunnerOptions = {}) {
84
88
  this.#app = app;
85
89
  this.#fast = fast;
86
90
  this.#reporter = reporter;
91
+ this.#environment = environment;
87
92
  }
88
93
 
89
94
  async build({ spinner = false }: BuildOptions = {}): Promise<BuildResult> {
90
95
  // serial build is needed because of Bun.build is unstable for parallel build
91
- const phaseOptions = { spinner };
96
+ this.#spinner = spinner;
92
97
  const { web, assets } = await this.#app.getConfig();
93
- await this.#runPhase("prepare", "Preparing output directory", () => this.#app.prepareCommand("build"), undefined, {
94
- spinner,
95
- });
96
- if (!this.#fast) await this.#runPhase("typecheck", "Typechecking", () => this.typecheck(), undefined, phaseOptions);
98
+ await this.#runPhase("prepare", "Preparing output directory", () => this.#app.prepareCommand("build"));
99
+ if (!this.#fast) await this.#runPhase("typecheck", "Typechecking", () => this.typecheck());
97
100
  await this.#runPhase(
98
101
  "backend",
99
102
  "Compiling backend",
100
103
  () => this.#buildBackend(),
101
104
  (result) => `${result.entrypoints} entrypoints, ${result.outputs} outputs`,
102
- phaseOptions,
103
105
  );
104
106
  await this.#runPhase(
105
107
  "ssr",
@@ -111,14 +113,12 @@ export class ApplicationBuildRunner {
111
113
  : web.ssr
112
114
  ? "skipped"
113
115
  : "disabled by akan.config.ts web.ssr",
114
- phaseOptions,
115
116
  );
116
117
  await this.#runPhase(
117
118
  "csr",
118
119
  "Building CSR assets",
119
- async () => (web.csr ? await this.#buildCsr() : null),
120
+ async () => (web.csr ? await new CsrArtifactBuilder(this.#app, "build").build() : null),
120
121
  (result) => result?.outputDir ?? (web.csr ? "skipped" : "disabled by akan.config.ts web.csr"),
121
- phaseOptions,
122
122
  );
123
123
  await this.#runPhase(
124
124
  "assets",
@@ -128,7 +128,6 @@ export class ApplicationBuildRunner {
128
128
  result
129
129
  ? `${result.removed.length} font file(s) dropped, ${ApplicationBuildRunner.formatBytes(result.freedBytes)} freed; ${result.kept.length} kept`
130
130
  : "disabled by akan.config.ts assets.pruneFonts",
131
- phaseOptions,
132
131
  );
133
132
  await this.#runPhase(
134
133
  "compress",
@@ -138,11 +137,8 @@ export class ApplicationBuildRunner {
138
137
  result.files > 0
139
138
  ? `${result.files} files, ${ApplicationBuildRunner.formatBytes(result.inputBytes)} -> gzip ${ApplicationBuildRunner.formatBytes(result.outputBytes)} / br ${ApplicationBuildRunner.formatBytes(result.brotliBytes)}`
140
139
  : "no files",
141
- phaseOptions,
142
140
  );
143
- await this.#runPhase("metadata", "Writing production metadata", () => this.#buildAppMeta(), undefined, {
144
- spinner,
145
- });
141
+ await this.#runPhase("metadata", "Writing production metadata", () => this.buildAppMeta());
146
142
  return {
147
143
  phases: this.#phases,
148
144
  durationMs: Date.now() - this.#startedAt,
@@ -154,7 +150,7 @@ export class ApplicationBuildRunner {
154
150
  async typecheck(options: TypecheckOptions = {}) {
155
151
  const { clean = false, incremental = true } = options;
156
152
  await this.#app.getPageKeys({ refresh: true });
157
- const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig({ incremental });
153
+ const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig(incremental);
158
154
  if (clean) await rm(path.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
159
155
  await this.#checkProjectInChildProcess(tsconfigPath);
160
156
  }
@@ -164,19 +160,17 @@ export class ApplicationBuildRunner {
164
160
  label: string,
165
161
  task: () => Promise<T>,
166
162
  summarize?: (result: T) => string | undefined,
167
- options: BuildPhaseRunOptions = {},
168
163
  ) {
169
164
  this.#reporter?.phaseStart?.({ id, label });
170
165
  const phaseStartedAt = Date.now();
171
- const spinner = options.spinner
166
+ const spinner = this.#spinner
172
167
  ? new Spinner(label, { prefix: `${BUILD_PHASE_EMOJIS[id]} ${id}` }).start()
173
168
  : undefined;
174
169
  try {
175
170
  const result = await task();
176
171
  const phase = { id, label, durationMs: Date.now() - phaseStartedAt, summary: summarize?.(result) };
177
172
  this.#phases.push(phase);
178
- const summary = phase.summary ? `: ${phase.summary}` : "";
179
- spinner?.succeed(`${label}${summary}`);
173
+ spinner?.succeed(`${label}${phase.summary ? `: ${phase.summary}` : ""}`);
180
174
  this.#reporter?.phaseDone?.(phase);
181
175
  return result;
182
176
  } catch (error) {
@@ -186,71 +180,94 @@ export class ApplicationBuildRunner {
186
180
  }
187
181
  }
188
182
 
189
- async #buildAppMeta() {
183
+ async buildAppMeta() {
190
184
  const akanConfig = await this.#app.getConfig();
185
+ const dockerfile = akanConfig.dockerfileFor(this.#environment ?? akanConfig.baseDevEnv.env);
191
186
  await Promise.all([
192
187
  this.#app.dist.writeJson("package.json", akanConfig.getProductionPackageJson()),
193
- this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.dockerfile),
188
+ this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, dockerfile),
189
+ this.#app.dist.writeJson("akan.build.json", {
190
+ buildId: await this.#resolveBuildId(),
191
+ akanVersion: akanConfig.akanVersion,
192
+ builtAt: new Date().toISOString(),
193
+ }),
194
194
  ]);
195
195
  }
196
196
 
197
+ //* Read back at runtime as `buildId` on the ops channel; a deployment's own AKAN_BUILD_ID still wins over it there.
198
+ async #resolveBuildId() {
199
+ const fromEnv = process.env.AKAN_BUILD_ID?.trim();
200
+ if (fromEnv) return fromEnv;
201
+ const root = this.#app.workspace.workspaceRoot;
202
+ const sha = Bun.spawnSync(["git", "rev-parse", "--short=12", "HEAD"], { cwd: root, stderr: "ignore" });
203
+ if (sha.exitCode !== 0) return null;
204
+ const dirty = Bun.spawnSync(["git", "status", "--porcelain", "--untracked-files=no"], {
205
+ cwd: root,
206
+ stderr: "ignore",
207
+ });
208
+ const suffix = dirty.exitCode === 0 && dirty.stdout.toString().trim() ? "-dirty" : "";
209
+ return `${sha.stdout.toString().trim()}${suffix}`;
210
+ }
211
+
197
212
  async #buildBackend() {
198
- const akanConfig = await this.#app.getConfig();
199
- const backendExternals = [
200
- ...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]),
201
- ];
202
- const { web } = akanConfig;
213
+ const { externalLibs, web, baseDevEnv, branches } = await this.#app.getConfig();
203
214
  const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
204
215
  for (const entrypoint of backendEntryPoints) {
205
216
  if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
206
217
  }
207
- const backendResult = await this.#buildOrThrow("backend", {
208
- entrypoints: backendEntryPoints,
218
+ const sharedConfig = {
209
219
  outdir: this.#app.dist.cwdPath,
210
220
  target: "bun",
211
221
  minify: AKAN_BACKEND_MINIFY,
212
222
  naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
223
+ // `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
213
224
  define: { "process.env.NODE_ENV": JSON.stringify("production") },
214
- plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
225
+ } satisfies Omit<Bun.BuildConfig, "entrypoints">;
226
+ const serverEnvPlugin = createServerEnvPlugin({
227
+ envDir: path.join(this.#app.cwdPath, "env"),
228
+ //? The root .env wins over the shell in baseDevEnv, so a `--env` build names its own; a desktop app runs it.
229
+ environment: this.#environment ?? baseDevEnv.env,
230
+ environments: ["local", "testing", ...branches],
215
231
  });
232
+ const backendConfig = {
233
+ ...sharedConfig,
234
+ plugins: [this.#createExternalSpecifiersPlugin(externalLibs), serverEnvPlugin],
235
+ };
236
+ //* Built apart so main.js keeps its own module copies; splitting moves lazy vendor `import()`s out of the boot parse.
237
+ const [mainResult, serverResult] = [
238
+ await this.#buildOrThrow("backend", { ...backendConfig, entrypoints: [backendEntryPoints[0]] }),
239
+ await this.#buildOrThrow("backend", { ...backendConfig, entrypoints: [backendEntryPoints[1]], splitting: true }),
240
+ ];
216
241
  // Nothing spawns the RSC worker without SSR, so an api-only image does not carry it.
217
242
  const rscWorkerResult = web.ssr
218
243
  ? await this.#buildOrThrow("rsc-worker", {
244
+ ...backendConfig,
219
245
  entrypoints: [this.#resolveRscWorkerBuildEntry()],
220
- outdir: this.#app.dist.cwdPath,
221
- target: "bun",
222
- minify: AKAN_BACKEND_MINIFY,
223
- naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
224
246
  conditions: ["react-server"],
225
- // `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
226
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
227
- plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
228
247
  })
229
248
  : null;
230
249
  const consoleRuntimeResult = await this.#buildOrThrow("console-runtime", {
250
+ ...sharedConfig,
231
251
  entrypoints: [this.#resolveConsoleRuntimeBuildEntry()],
232
- outdir: this.#app.dist.cwdPath,
233
- target: "bun",
234
- minify: AKAN_BACKEND_MINIFY,
235
252
  naming: { entry: "console-runtime.[ext]", chunk: "chunk-[hash].[ext]" },
236
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
237
253
  });
238
254
  await this.#writeConsoleShim();
255
+ const results = [mainResult, serverResult, rscWorkerResult, consoleRuntimeResult];
239
256
  return {
240
257
  entrypoints: backendEntryPoints.length + (rscWorkerResult ? 2 : 1),
241
- outputs:
242
- backendResult.outputs.length + (rscWorkerResult?.outputs.length ?? 0) + consoleRuntimeResult.outputs.length + 1,
258
+ outputs: results.reduce((sum, result) => sum + (result?.outputs.length ?? 0), 1),
243
259
  };
244
260
  }
245
261
 
246
262
  async #writeConsoleShim() {
247
263
  await Bun.write(
248
264
  path.join(this.#app.dist.cwdPath, "console.js"),
249
- `import { cnst, db, dict, option, server, sig, srv } from "./server.js";
250
- import { assertAkanConsoleAllowed, startAkanConsole } from "./console-runtime.js";
265
+ `process.env.AKAN_COMMAND_TYPE = "console";
266
+ const { cnst, db, dict, option, server, sig, srv } = await import("./server.js");
267
+ const { assertAkanConsoleAllowed, startAkanConsole } = await import("./console-runtime.js");
251
268
 
252
269
  const run = async () => {
253
- assertAkanConsoleAllowed(server.env);
270
+ assertAkanConsoleAllowed();
254
271
  await server.start({ listen: false, web: false });
255
272
  try {
256
273
  await startAkanConsole(server, { globals: { cnst, db, dict, option, sig, srv } });
@@ -283,10 +300,6 @@ void run().catch((error) => {
283
300
  }
284
301
  }
285
302
 
286
- async #buildCsr() {
287
- return await new CsrArtifactBuilder(this.#app, "build").build();
288
- }
289
-
290
303
  async #buildSsr() {
291
304
  const pageKeys = await this.#app.getPageKeys();
292
305
  if (pageKeys.length === 0) {
@@ -298,7 +311,7 @@ void run().catch((error) => {
298
311
  return { base, allRoutes };
299
312
  }
300
313
 
301
- async #writeTypecheckTsconfig({ incremental = true }: TypecheckOptions = {}) {
314
+ async #writeTypecheckTsconfig(incremental: boolean) {
302
315
  const typecheckDir = path.join(this.#app.cwdPath, ".akan", "typecheck");
303
316
  await mkdir(typecheckDir, { recursive: true });
304
317
  //* TypeScript's `include` globs do not cross a symlink, so synced lib pages need their real path.
@@ -367,11 +380,10 @@ void run().catch((error) => {
367
380
  return result;
368
381
  }
369
382
 
370
- #createExternalSpecifiersPlugin(specifiers: readonly string[]): BunPlugin {
371
- const uniqueSpecifiers = [...new Set(specifiers)];
372
- const escaped = uniqueSpecifiers.map((specifier) => specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
383
+ #createExternalSpecifiersPlugin(externalLibs: readonly string[]): BunPlugin {
384
+ const specifiers = new Set([...externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]);
385
+ const escaped = [...specifiers].map((specifier) => specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
373
386
  const filter = new RegExp(`^(${escaped.join("|")})(?:/.*)?$`);
374
-
375
387
  return {
376
388
  name: "akan-backend-externalize-specifiers",
377
389
  setup(build) {
@@ -380,9 +392,5 @@ void run().catch((error) => {
380
392
  };
381
393
  }
382
394
 
383
- static formatBytes(bytes: number): string {
384
- if (bytes < 1024) return `${bytes}B`;
385
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
386
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
387
- }
395
+ static formatBytes = FontPruner.formatBytes;
388
396
  }
@@ -8,7 +8,7 @@ export interface SignalTestPreloadTarget {
8
8
 
9
9
  export async function resolveSignalTestPreloadPath(target: SignalTestPreloadTarget) {
10
10
  const candidates: string[] = [];
11
- const addResolvedPackageCandidate = (basePath: string) => {
11
+ for (const basePath of [target.cwdPath, process.cwd(), path.dirname(Bun.main), import.meta.dir]) {
12
12
  try {
13
13
  candidates.push(
14
14
  path.join(path.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH),
@@ -16,13 +16,7 @@ export async function resolveSignalTestPreloadPath(target: SignalTestPreloadTarg
16
16
  } catch {
17
17
  // Source workspaces and published installs can resolve Akan packages from different roots.
18
18
  }
19
- };
20
-
21
- addResolvedPackageCandidate(target.cwdPath);
22
- addResolvedPackageCandidate(process.cwd());
23
- addResolvedPackageCandidate(path.dirname(Bun.main));
24
- addResolvedPackageCandidate(import.meta.dir);
25
-
19
+ }
26
20
  candidates.push(
27
21
  path.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH),
28
22
  path.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH),
@@ -33,10 +27,7 @@ export async function resolveSignalTestPreloadPath(target: SignalTestPreloadTarg
33
27
  );
34
28
 
35
29
  const uniqueCandidates = [...new Set(candidates)];
36
- for (const candidate of uniqueCandidates) {
37
- if (await Bun.file(candidate).exists()) return candidate;
38
- }
39
-
30
+ for (const candidate of uniqueCandidates) if (await Bun.file(candidate).exists()) return candidate;
40
31
  throw new Error(
41
32
  `Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.\nProbed paths:\n${uniqueCandidates
42
33
  .map((candidate) => ` - ${candidate}`)
@@ -1,25 +1,9 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
4
2
  import path from "node:path";
3
+ import { tempDirs, writeText as write } from "../testHelpers";
5
4
  import { resolveSsrPageEntries } from "./implicitRootLayout";
6
5
 
7
- const tempRoots: string[] = [];
8
-
9
- const makeTempRoot = async () => {
10
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-implicit-root-layout-"));
11
- tempRoots.push(root);
12
- return root;
13
- };
14
-
15
- const write = async (filePath: string, content: string) => {
16
- await mkdir(path.dirname(filePath), { recursive: true });
17
- await writeFile(filePath, content);
18
- };
19
-
20
- afterEach(async () => {
21
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
22
- });
6
+ const makeTempRoot = tempDirs("akan-implicit-root-layout-");
23
7
 
24
8
  describe("resolveSsrPageEntries", () => {
25
9
  test("inherits root layout static exports for grouped root boundaries", async () => {
@@ -31,11 +15,11 @@ describe("resolveSsrPageEntries", () => {
31
15
  await write(path.join(appRoot, "env", "env.client.ts"), "export const env = {};\n");
32
16
  await write(
33
17
  rootLayoutPath,
34
- 'export const theme = "dark";\nexport const fonts = [{ name: "pretendard" }];\nexport const metadata = { title: "Root" };\n',
18
+ 'export const theme = "dark";\nexport const fonts = [{ name: "pretendard" }];\nexport const head = null;\n',
35
19
  );
36
20
  await write(
37
21
  groupedLayoutPath,
38
- 'export function generateMetadata() { return { title: "Home" }; }\nexport default function Layout({ children }) { return children; }\n',
22
+ "export function generateHead() { return null; }\nexport default function Layout({ children }) { return children; }\n",
39
23
  );
40
24
 
41
25
  const entries = await resolveSsrPageEntries({
@@ -54,15 +38,10 @@ describe("resolveSsrPageEntries", () => {
54
38
  expect(generatedSource).toContain('import * as inheritedModule from "../../../page/_layout.tsx";');
55
39
  expect(generatedSource).toContain("const inheritedLayout = resolveRouteModule(inheritedModule as never");
56
40
  expect(generatedSource).not.toContain("<System.Provider");
57
- expect(generatedSource).toContain("export async function generateMetadata(props: PageProps)");
58
- expect(generatedSource).toContain("if (userLayout.generateMetadata) return userLayout.generateMetadata(props);");
59
- expect(generatedSource).toContain("if (userLayout.metadata !== undefined) return userLayout.metadata;");
60
- expect(generatedSource).toContain(
61
- "if (inheritedLayout.generateMetadata) return inheritedLayout.generateMetadata(props);",
62
- );
63
- expect(generatedSource).toContain("return inheritedLayout.metadata;");
64
- expect(generatedSource).not.toContain("Object.keys(userLayout.metadata)");
65
- expect(generatedSource).not.toContain("Object.keys(inheritedLayout.metadata)");
41
+ expect(generatedSource).toContain("export async function generateHead(props: PageProps)");
42
+ expect(generatedSource).toContain("if (userLayout.generateHead) return userLayout.generateHead(props);");
43
+ expect(generatedSource).toContain("return inheritedLayout.head;");
44
+ expect(generatedSource).not.toContain("generateMetadata");
66
45
  expect(generatedSource).toContain("export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;");
67
46
  expect(generatedSource).toContain("export const Error = userLayout.Error ?? inheritedLayout.Error;");
68
47
  expect(generatedSource).toContain("export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;");
@@ -171,4 +150,27 @@ describe("resolveSsrPageEntries", () => {
171
150
  expect(generatedSource).toContain("? await UserLayout({ params, searchParams, children })");
172
151
  expect(generatedSource).toContain(" return layout;\n}");
173
152
  });
153
+
154
+ test("prunes generated wrappers whose route file moved away", async () => {
155
+ const appRoot = await makeTempRoot();
156
+ const pageRoot = path.join(appRoot, "page");
157
+ await write(path.join(appRoot, "env", "env.client.ts"), "export const env = {};\n");
158
+ await write(path.join(pageRoot, "a", "_overrides.tsx"), "export default {};\n");
159
+ await write(path.join(pageRoot, "b", "_overrides.tsx"), "export default {};\n");
160
+ await write(path.join(pageRoot, "(home)", "_layout.tsx"), "export default ({ children }) => children;\n");
161
+ const resolve = async (pageKeys: string[]) =>
162
+ await resolveSsrPageEntries({ appCwdPath: appRoot, appName: "demo", pageKeys });
163
+ const generatedPaths = async (pageKeys: string[]) =>
164
+ (await resolve(pageKeys))
165
+ .filter((entry) => entry.moduleAbsPath.includes(`${path.sep}.akan${path.sep}`))
166
+ .map((entry) => entry.moduleAbsPath);
167
+
168
+ const before = await generatedPaths(["./(home)/_layout.tsx", "./a/_overrides.tsx", "./a/x.tsx"]);
169
+ const after = await generatedPaths(["./b/_overrides.tsx", "./b/y.tsx"]);
170
+
171
+ expect(before).toHaveLength(2);
172
+ for (const stale of before) expect(await Bun.file(stale).exists()).toBe(false);
173
+ expect(after).toHaveLength(2);
174
+ for (const fresh of after) expect(await Bun.file(fresh).exists()).toBe(true);
175
+ });
174
176
  });