@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.21

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 (289) hide show
  1. package/README.ko.md +2 -2
  2. package/README.md +3 -2
  3. package/abstractDoc.test.ts +6 -15
  4. package/abstractDoc.ts +6 -8
  5. package/agentsIndex.test.ts +4 -7
  6. package/agentsIndex.ts +6 -22
  7. package/akanApp/BackendImportGraph.test.ts +8 -22
  8. package/akanApp/BackendImportGraph.ts +1 -20
  9. package/akanApp/akanApp.host.test.ts +1 -3
  10. package/akanApp/akanApp.host.ts +82 -254
  11. package/akanApp/devHostPolicy.test.ts +10 -34
  12. package/akanApp/devHostPolicy.ts +12 -98
  13. package/akanConfig/akanConfig.test.ts +142 -106
  14. package/akanConfig/akanConfig.ts +147 -191
  15. package/akanConfig/types.ts +4 -3
  16. package/akanContext.ts +110 -160
  17. package/akanMcpContract.ts +28 -73
  18. package/appSelectionMemory.ts +1 -4
  19. package/applicationBuildReporter.ts +37 -5
  20. package/applicationBuildRunner.test.ts +5 -3
  21. package/applicationBuildRunner.ts +54 -61
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +32 -30
  24. package/artifact/implicitRootLayout.ts +58 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +32 -1
  28. package/biomeBase.ts +2 -5
  29. package/cloud/cloudApi.test.ts +202 -0
  30. package/cloud/cloudApi.ts +77 -39
  31. package/cloud/configLock.test.ts +64 -0
  32. package/cloud/configLock.ts +96 -0
  33. package/cloud/constants.ts +26 -5
  34. package/cloud/globalConfig.ts +70 -32
  35. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  36. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  37. package/codeAgent/agent/CodeAgent.ts +551 -0
  38. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  39. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  40. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  41. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  42. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  43. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  44. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  45. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  46. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  47. package/codeAgent/agent/CodeMailbox.ts +148 -0
  48. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  49. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  50. package/codeAgent/agent/SubagentPool.ts +214 -0
  51. package/codeAgent/agent/akanCodeModel.ts +47 -0
  52. package/codeAgent/agent/akanCodePaths.ts +31 -0
  53. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  54. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  55. package/codeAgent/agent/codeAgent.test.ts +178 -0
  56. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  57. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  58. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  59. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  60. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  61. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  62. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  63. package/codeAgent/agent/consoleToStderr.ts +8 -0
  64. package/codeAgent/agent/jsonLines.ts +29 -0
  65. package/codeAgent/agent/stringArg.ts +5 -0
  66. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  67. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  68. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  69. package/codeAgent/feedback/PreviewView.ts +95 -0
  70. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  71. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  72. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  73. package/codeAgent/index.ts +34 -0
  74. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  75. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  76. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  77. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  78. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  79. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  80. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  81. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  82. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  83. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  84. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  85. package/codeAgent/tools/AkanCli.ts +50 -0
  86. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  87. package/codeAgent/tools/AkanEditScope.ts +48 -0
  88. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  89. package/codeAgent/tools/AkanToolPack.ts +92 -0
  90. package/codeAgent/tools/AkanVerifier.ts +48 -0
  91. package/codeAgent/tools/AskToolPack.ts +99 -0
  92. package/codeAgent/tools/MailToolPack.ts +76 -0
  93. package/codeAgent/tools/McpClient.ts +187 -0
  94. package/codeAgent/tools/McpOAuth.ts +233 -0
  95. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  96. package/codeAgent/tools/McpServerConfig.ts +129 -0
  97. package/codeAgent/tools/McpSignIn.ts +158 -0
  98. package/codeAgent/tools/McpTokenStore.ts +81 -0
  99. package/codeAgent/tools/McpToolPack.ts +199 -0
  100. package/codeAgent/tools/SessionToolPack.ts +91 -0
  101. package/codeAgent/tools/WebToolPack.ts +147 -0
  102. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  103. package/codeAgent/tools/askToolPack.test.ts +116 -0
  104. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  105. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  106. package/codegenLock.test.ts +4 -13
  107. package/codegenLock.ts +3 -17
  108. package/commandDecorators/argMeta.ts +1 -5
  109. package/commandDecorators/camelToKebabCase.ts +1 -0
  110. package/commandDecorators/command.ts +46 -102
  111. package/commandDecorators/commandBuilder.ts +30 -61
  112. package/commandDecorators/commandDecorators.test.ts +2 -10
  113. package/commandDecorators/dependencyBuilder.ts +10 -1
  114. package/commandDecorators/helpFormatter.ts +68 -145
  115. package/commandDecorators/index.ts +0 -2
  116. package/commandDecorators/targetMeta.ts +9 -7
  117. package/createTunnel.ts +7 -14
  118. package/dependencyScanner.test.ts +4 -16
  119. package/dependencyScanner.ts +31 -80
  120. package/devkitUtils.test.ts +48 -20
  121. package/executors.linkFallback.test.ts +103 -0
  122. package/executors.test.ts +132 -106
  123. package/executors.ts +195 -496
  124. package/fileEditor.ts +9 -37
  125. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  126. package/fileSys.ts +6 -8
  127. package/formSetterScanner.ts +3 -16
  128. package/frontendBuild/allRoutesBuilder.ts +7 -21
  129. package/frontendBuild/autoImportSync.test.ts +27 -38
  130. package/frontendBuild/autoImportSync.ts +31 -65
  131. package/frontendBuild/buildRouteClient.test.ts +62 -37
  132. package/frontendBuild/bundleDefine.ts +12 -0
  133. package/frontendBuild/clientBuildTypes.ts +5 -36
  134. package/frontendBuild/clientEntriesBundler.ts +9 -40
  135. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  136. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  137. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  138. package/frontendBuild/cssCandidateCache.ts +6 -41
  139. package/frontendBuild/cssCompiler.ts +29 -78
  140. package/frontendBuild/cssImportResolver.ts +6 -19
  141. package/frontendBuild/devChangePlanner.ts +7 -10
  142. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  143. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  144. package/frontendBuild/fontOptimizer.test.ts +6 -14
  145. package/frontendBuild/fontOptimizer.ts +15 -72
  146. package/frontendBuild/fontPruner.test.ts +5 -10
  147. package/frontendBuild/fontPruner.ts +5 -20
  148. package/frontendBuild/frontendBuild.test.ts +46 -69
  149. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  150. package/frontendBuild/hmrWatcher.test.ts +8 -32
  151. package/frontendBuild/hmrWatcher.ts +14 -71
  152. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  153. package/frontendBuild/precompressArtifacts.ts +5 -18
  154. package/frontendBuild/routeClientBuilder.ts +23 -46
  155. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  156. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  157. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  158. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  159. package/frontendBuild/styleContract.ts +1 -8
  160. package/frontendBuild/themeValidator.ts +4 -26
  161. package/frontendBuild/watchRootResolver.test.ts +23 -42
  162. package/frontendBuild/watchRootResolver.ts +5 -27
  163. package/getRelatedCnsts.ts +12 -92
  164. package/incrementalBuilder/buildBatch.proc.ts +29 -58
  165. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  166. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  167. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  168. package/incrementalBuilder/builderChannel.test.ts +24 -49
  169. package/incrementalBuilder/builderChannel.ts +6 -37
  170. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  171. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  172. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  173. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  174. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  175. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  176. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  177. package/index.ts +5 -19
  178. package/integration/devResourceProbe.ts +50 -56
  179. package/integration/devStability.integration.test.ts +47 -194
  180. package/integration/devStabilityHarness.test.ts +13 -28
  181. package/integration/devStabilityHarness.ts +65 -330
  182. package/integration/ssrMemoryProbe.ts +41 -167
  183. package/libSource.test.ts +4 -15
  184. package/libSource.ts +19 -31
  185. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  186. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  187. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  188. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  189. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  190. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  191. package/lint/gritRules.test.ts +8 -24
  192. package/lint/no-deep-internal-import.grit +2 -2
  193. package/lint/no-document-cookie.grit +29 -0
  194. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  195. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  196. package/lint/no-web-storage.grit +30 -0
  197. package/linter.test.ts +1 -2
  198. package/linter.ts +26 -116
  199. package/mobile/index.ts +6 -0
  200. package/mobile/mobileTarget.ts +16 -13
  201. package/mobile/nativeApi.test.ts +30 -0
  202. package/mobile/nativeApi.ts +40 -0
  203. package/mobile/nativeApp.test.ts +85 -0
  204. package/mobile/nativeApp.ts +210 -0
  205. package/mobile/nativeConfig.test.ts +199 -0
  206. package/mobile/nativeConfig.ts +184 -0
  207. package/mobile/nativeWebDir.test.ts +80 -0
  208. package/mobile/nativeWebDir.ts +62 -0
  209. package/mobile/placeholderAppId.test.ts +13 -0
  210. package/mobile/placeholderAppId.ts +16 -0
  211. package/mobile/usageDescriptions.test.ts +27 -0
  212. package/mobile/usageDescriptions.ts +24 -0
  213. package/package.json +10 -10
  214. package/packageExports.test.ts +3 -18
  215. package/packageExportsMap.ts +3 -19
  216. package/piDependencyPins.test.ts +46 -0
  217. package/platformTest/BunTestReport.test.ts +60 -0
  218. package/platformTest/BunTestReport.ts +65 -0
  219. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  220. package/platformTest/IdleWatchedProcess.ts +73 -0
  221. package/platformTest/LinuxDockerTarget.ts +105 -0
  222. package/platformTest/PlatformTestRun.test.ts +64 -0
  223. package/platformTest/PlatformTestRun.ts +141 -0
  224. package/platformTest/PlatformTestTarget.ts +142 -0
  225. package/platformTest/TestSnapshot.test.ts +72 -0
  226. package/platformTest/TestSnapshot.ts +103 -0
  227. package/platformTest/WindowsSshTarget.ts +151 -0
  228. package/prompter.ts +3 -13
  229. package/qualityScanner.test.ts +9 -26
  230. package/qualityScanner.ts +200 -286
  231. package/recipeScanner.test.ts +7 -12
  232. package/recipeScanner.ts +9 -38
  233. package/repoIdentity.ts +2 -9
  234. package/routeSourceValidator.test.ts +0 -1
  235. package/routeSourceValidator.ts +7 -42
  236. package/scanInfo.ts +91 -225
  237. package/selectModel.ts +1 -2
  238. package/semver.ts +3 -5
  239. package/slicePlanner.test.ts +6 -24
  240. package/slicePlanner.ts +11 -32
  241. package/spinner.test.ts +3 -8
  242. package/spinner.ts +4 -16
  243. package/sshTunnel.ts +1 -6
  244. package/ssrScanner.test.ts +1 -6
  245. package/ssrScanner.ts +103 -113
  246. package/stripAnsi.ts +3 -0
  247. package/subspace.test.ts +30 -25
  248. package/subspace.ts +81 -115
  249. package/subspaceConfig.ts +11 -9
  250. package/testHelpers.ts +194 -0
  251. package/transforms/asyncDefaultExportDetector.ts +3 -11
  252. package/transforms/barrelAnalyzer.ts +43 -135
  253. package/transforms/barrelImportsPlugin.ts +38 -175
  254. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  255. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  256. package/transforms/moduleSyntax.ts +32 -0
  257. package/transforms/rscUseClientTransform.ts +35 -20
  258. package/transforms/transforms.test.ts +153 -44
  259. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  260. package/transforms/useClientBundlePlugin.ts +2 -22
  261. package/typeChecker.ts +7 -63
  262. package/types.ts +1 -0
  263. package/uploadRelease.ts +10 -25
  264. package/workflow/artifacts.ts +18 -0
  265. package/workflow/executor.test.ts +1 -12
  266. package/workflow/executor.ts +101 -110
  267. package/workflow/index.ts +0 -1
  268. package/workflow/moduleIndex.test.ts +4 -15
  269. package/workflow/moduleIndex.ts +9 -49
  270. package/workflow/plan.ts +18 -25
  271. package/workflow/render.ts +80 -96
  272. package/workflow/rolloutGate.test.ts +1 -1
  273. package/workflow/source.ts +52 -69
  274. package/workflow/sourceAst.ts +50 -0
  275. package/workspaceLayout.test.ts +13 -3
  276. package/workspaceLayout.ts +20 -23
  277. package/abstractCompactor.ts +0 -112
  278. package/aiEditor.test.ts +0 -60
  279. package/aiEditor.ts +0 -387
  280. package/applicationReleasePackager.ts +0 -209
  281. package/capacitorApp.test.ts +0 -372
  282. package/capacitorApp.ts +0 -1648
  283. package/commandDecorators/commandMeta.ts +0 -7
  284. package/commandDecorators/types.ts +0 -10
  285. package/getDirname.ts +0 -4
  286. package/guideline.ts +0 -19
  287. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  288. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  289. 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,5 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
3
5
  import { AKAN_BACKEND_MINIFY, AKAN_OPTIONAL_BACKEND_EXTERNALS } from "./applicationBuildRunner";
4
6
 
5
7
  describe("ApplicationBuildRunner", () => {
@@ -16,8 +18,8 @@ describe("ApplicationBuildRunner", () => {
16
18
  });
17
19
 
18
20
  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`;
21
+ const dir = path.join(os.tmpdir(), `akan-minify-${Bun.randomUUIDv7()}`);
22
+ const entry = path.join(dir, "entry.ts");
21
23
  await Bun.write(
22
24
  entry,
23
25
  `const serve = () => class Service { readonly name = this.constructor.name; };
@@ -26,7 +28,7 @@ console.info(new SampleService().name);
26
28
  `,
27
29
  );
28
30
  const build = async (minify: Bun.BuildConfig["minify"]) => {
29
- const result = await Bun.build({ entrypoints: [entry], target: "bun", minify, outdir: `${dir}/out` });
31
+ const result = await Bun.build({ entrypoints: [entry], target: "bun", minify, outdir: path.join(dir, "out") });
30
32
  expect(result.success).toBe(true);
31
33
  const proc = Bun.spawn([process.execPath, result.outputs[0].path], { stdout: "pipe" });
32
34
  return (await new Response(proc.stdout).text()).trim();
@@ -60,9 +60,8 @@ const SSR_RENDER_EXTERNALS = [
60
60
  "react-server-dom-webpack/client.browser",
61
61
  ] as const;
62
62
 
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.
63
+ // Mangling renames classes, and `this.constructor.name` names loggers, exceptions and guards; `minify.keepNames`
64
+ // is a no-op as of Bun 1.4.2.
66
65
  export const AKAN_BACKEND_MINIFY = { whitespace: true, syntax: true, identifiers: false } as const;
67
66
 
68
67
  export const AKAN_OPTIONAL_BACKEND_EXTERNALS = [
@@ -77,6 +76,7 @@ export class ApplicationBuildRunner {
77
76
  #app: App;
78
77
  #fast: boolean;
79
78
  #reporter?: BuildProgressReporter;
79
+ #spinner?: boolean;
80
80
  #startedAt = Date.now();
81
81
  #phases: BuildPhaseResult[] = [];
82
82
 
@@ -88,18 +88,15 @@ export class ApplicationBuildRunner {
88
88
 
89
89
  async build({ spinner = false }: BuildOptions = {}): Promise<BuildResult> {
90
90
  // serial build is needed because of Bun.build is unstable for parallel build
91
- const phaseOptions = { spinner };
91
+ this.#spinner = spinner;
92
92
  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);
93
+ await this.#runPhase("prepare", "Preparing output directory", () => this.#app.prepareCommand("build"));
94
+ if (!this.#fast) await this.#runPhase("typecheck", "Typechecking", () => this.typecheck());
97
95
  await this.#runPhase(
98
96
  "backend",
99
97
  "Compiling backend",
100
98
  () => this.#buildBackend(),
101
99
  (result) => `${result.entrypoints} entrypoints, ${result.outputs} outputs`,
102
- phaseOptions,
103
100
  );
104
101
  await this.#runPhase(
105
102
  "ssr",
@@ -111,14 +108,12 @@ export class ApplicationBuildRunner {
111
108
  : web.ssr
112
109
  ? "skipped"
113
110
  : "disabled by akan.config.ts web.ssr",
114
- phaseOptions,
115
111
  );
116
112
  await this.#runPhase(
117
113
  "csr",
118
114
  "Building CSR assets",
119
- async () => (web.csr ? await this.#buildCsr() : null),
115
+ async () => (web.csr ? await new CsrArtifactBuilder(this.#app, "build").build() : null),
120
116
  (result) => result?.outputDir ?? (web.csr ? "skipped" : "disabled by akan.config.ts web.csr"),
121
- phaseOptions,
122
117
  );
123
118
  await this.#runPhase(
124
119
  "assets",
@@ -128,7 +123,6 @@ export class ApplicationBuildRunner {
128
123
  result
129
124
  ? `${result.removed.length} font file(s) dropped, ${ApplicationBuildRunner.formatBytes(result.freedBytes)} freed; ${result.kept.length} kept`
130
125
  : "disabled by akan.config.ts assets.pruneFonts",
131
- phaseOptions,
132
126
  );
133
127
  await this.#runPhase(
134
128
  "compress",
@@ -138,11 +132,8 @@ export class ApplicationBuildRunner {
138
132
  result.files > 0
139
133
  ? `${result.files} files, ${ApplicationBuildRunner.formatBytes(result.inputBytes)} -> gzip ${ApplicationBuildRunner.formatBytes(result.outputBytes)} / br ${ApplicationBuildRunner.formatBytes(result.brotliBytes)}`
140
134
  : "no files",
141
- phaseOptions,
142
135
  );
143
- await this.#runPhase("metadata", "Writing production metadata", () => this.#buildAppMeta(), undefined, {
144
- spinner,
145
- });
136
+ await this.#runPhase("metadata", "Writing production metadata", () => this.#buildAppMeta());
146
137
  return {
147
138
  phases: this.#phases,
148
139
  durationMs: Date.now() - this.#startedAt,
@@ -154,7 +145,7 @@ export class ApplicationBuildRunner {
154
145
  async typecheck(options: TypecheckOptions = {}) {
155
146
  const { clean = false, incremental = true } = options;
156
147
  await this.#app.getPageKeys({ refresh: true });
157
- const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig({ incremental });
148
+ const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig(incremental);
158
149
  if (clean) await rm(path.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
159
150
  await this.#checkProjectInChildProcess(tsconfigPath);
160
151
  }
@@ -164,19 +155,17 @@ export class ApplicationBuildRunner {
164
155
  label: string,
165
156
  task: () => Promise<T>,
166
157
  summarize?: (result: T) => string | undefined,
167
- options: BuildPhaseRunOptions = {},
168
158
  ) {
169
159
  this.#reporter?.phaseStart?.({ id, label });
170
160
  const phaseStartedAt = Date.now();
171
- const spinner = options.spinner
161
+ const spinner = this.#spinner
172
162
  ? new Spinner(label, { prefix: `${BUILD_PHASE_EMOJIS[id]} ${id}` }).start()
173
163
  : undefined;
174
164
  try {
175
165
  const result = await task();
176
166
  const phase = { id, label, durationMs: Date.now() - phaseStartedAt, summary: summarize?.(result) };
177
167
  this.#phases.push(phase);
178
- const summary = phase.summary ? `: ${phase.summary}` : "";
179
- spinner?.succeed(`${label}${summary}`);
168
+ spinner?.succeed(`${label}${phase.summary ? `: ${phase.summary}` : ""}`);
180
169
  this.#reporter?.phaseDone?.(phase);
181
170
  return result;
182
171
  } catch (error) {
@@ -191,66 +180,79 @@ export class ApplicationBuildRunner {
191
180
  await Promise.all([
192
181
  this.#app.dist.writeJson("package.json", akanConfig.getProductionPackageJson()),
193
182
  this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.dockerfile),
183
+ this.#app.dist.writeJson("akan.build.json", {
184
+ buildId: await this.#resolveBuildId(),
185
+ akanVersion: akanConfig.akanVersion,
186
+ builtAt: new Date().toISOString(),
187
+ }),
194
188
  ]);
195
189
  }
196
190
 
191
+ //* Read back at runtime as `buildId` on the ops channel; a deployment's own AKAN_BUILD_ID still wins over it there.
192
+ async #resolveBuildId() {
193
+ const fromEnv = process.env.AKAN_BUILD_ID?.trim();
194
+ if (fromEnv) return fromEnv;
195
+ const root = this.#app.workspace.workspaceRoot;
196
+ const sha = Bun.spawnSync(["git", "rev-parse", "--short=12", "HEAD"], { cwd: root, stderr: "ignore" });
197
+ if (sha.exitCode !== 0) return null;
198
+ const dirty = Bun.spawnSync(["git", "status", "--porcelain", "--untracked-files=no"], {
199
+ cwd: root,
200
+ stderr: "ignore",
201
+ });
202
+ const suffix = dirty.exitCode === 0 && dirty.stdout.toString().trim() ? "-dirty" : "";
203
+ return `${sha.stdout.toString().trim()}${suffix}`;
204
+ }
205
+
197
206
  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;
207
+ const { externalLibs, web } = await this.#app.getConfig();
203
208
  const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
204
209
  for (const entrypoint of backendEntryPoints) {
205
210
  if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
206
211
  }
207
- const backendResult = await this.#buildOrThrow("backend", {
208
- entrypoints: backendEntryPoints,
212
+ const sharedConfig = {
209
213
  outdir: this.#app.dist.cwdPath,
210
214
  target: "bun",
211
215
  minify: AKAN_BACKEND_MINIFY,
212
216
  naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
217
+ // `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
213
218
  define: { "process.env.NODE_ENV": JSON.stringify("production") },
214
- plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
215
- });
219
+ } satisfies Omit<Bun.BuildConfig, "entrypoints">;
220
+ const backendConfig = { ...sharedConfig, plugins: [this.#createExternalSpecifiersPlugin(externalLibs)] };
221
+ //* Built apart so main.js keeps its own module copies; splitting moves lazy vendor `import()`s out of the boot parse.
222
+ const [mainResult, serverResult] = [
223
+ await this.#buildOrThrow("backend", { ...backendConfig, entrypoints: [backendEntryPoints[0]] }),
224
+ await this.#buildOrThrow("backend", { ...backendConfig, entrypoints: [backendEntryPoints[1]], splitting: true }),
225
+ ];
216
226
  // Nothing spawns the RSC worker without SSR, so an api-only image does not carry it.
217
227
  const rscWorkerResult = web.ssr
218
228
  ? await this.#buildOrThrow("rsc-worker", {
229
+ ...backendConfig,
219
230
  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
231
  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
232
  })
229
233
  : null;
230
234
  const consoleRuntimeResult = await this.#buildOrThrow("console-runtime", {
235
+ ...sharedConfig,
231
236
  entrypoints: [this.#resolveConsoleRuntimeBuildEntry()],
232
- outdir: this.#app.dist.cwdPath,
233
- target: "bun",
234
- minify: AKAN_BACKEND_MINIFY,
235
237
  naming: { entry: "console-runtime.[ext]", chunk: "chunk-[hash].[ext]" },
236
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
237
238
  });
238
239
  await this.#writeConsoleShim();
240
+ const results = [mainResult, serverResult, rscWorkerResult, consoleRuntimeResult];
239
241
  return {
240
242
  entrypoints: backendEntryPoints.length + (rscWorkerResult ? 2 : 1),
241
- outputs:
242
- backendResult.outputs.length + (rscWorkerResult?.outputs.length ?? 0) + consoleRuntimeResult.outputs.length + 1,
243
+ outputs: results.reduce((sum, result) => sum + (result?.outputs.length ?? 0), 1),
243
244
  };
244
245
  }
245
246
 
246
247
  async #writeConsoleShim() {
247
248
  await Bun.write(
248
249
  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";
250
+ `process.env.AKAN_COMMAND_TYPE = "console";
251
+ const { cnst, db, dict, option, server, sig, srv } = await import("./server.js");
252
+ const { assertAkanConsoleAllowed, startAkanConsole } = await import("./console-runtime.js");
251
253
 
252
254
  const run = async () => {
253
- assertAkanConsoleAllowed(server.env);
255
+ assertAkanConsoleAllowed();
254
256
  await server.start({ listen: false, web: false });
255
257
  try {
256
258
  await startAkanConsole(server, { globals: { cnst, db, dict, option, sig, srv } });
@@ -283,10 +285,6 @@ void run().catch((error) => {
283
285
  }
284
286
  }
285
287
 
286
- async #buildCsr() {
287
- return await new CsrArtifactBuilder(this.#app, "build").build();
288
- }
289
-
290
288
  async #buildSsr() {
291
289
  const pageKeys = await this.#app.getPageKeys();
292
290
  if (pageKeys.length === 0) {
@@ -298,7 +296,7 @@ void run().catch((error) => {
298
296
  return { base, allRoutes };
299
297
  }
300
298
 
301
- async #writeTypecheckTsconfig({ incremental = true }: TypecheckOptions = {}) {
299
+ async #writeTypecheckTsconfig(incremental: boolean) {
302
300
  const typecheckDir = path.join(this.#app.cwdPath, ".akan", "typecheck");
303
301
  await mkdir(typecheckDir, { recursive: true });
304
302
  //* TypeScript's `include` globs do not cross a symlink, so synced lib pages need their real path.
@@ -367,11 +365,10 @@ void run().catch((error) => {
367
365
  return result;
368
366
  }
369
367
 
370
- #createExternalSpecifiersPlugin(specifiers: readonly string[]): BunPlugin {
371
- const uniqueSpecifiers = [...new Set(specifiers)];
372
- const escaped = uniqueSpecifiers.map((specifier) => specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
368
+ #createExternalSpecifiersPlugin(externalLibs: readonly string[]): BunPlugin {
369
+ const specifiers = new Set([...externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]);
370
+ const escaped = [...specifiers].map((specifier) => specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
373
371
  const filter = new RegExp(`^(${escaped.join("|")})(?:/.*)?$`);
374
-
375
372
  return {
376
373
  name: "akan-backend-externalize-specifiers",
377
374
  setup(build) {
@@ -380,9 +377,5 @@ void run().catch((error) => {
380
377
  };
381
378
  }
382
379
 
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
- }
380
+ static formatBytes = FontPruner.formatBytes;
388
381
  }
@@ -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
  });