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

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 (265) hide show
  1. package/README.md +1 -0
  2. package/abstractDoc.test.ts +6 -15
  3. package/abstractDoc.ts +6 -8
  4. package/agentsIndex.test.ts +4 -7
  5. package/agentsIndex.ts +6 -22
  6. package/akanApp/BackendImportGraph.test.ts +8 -22
  7. package/akanApp/BackendImportGraph.ts +1 -20
  8. package/akanApp/akanApp.host.test.ts +1 -3
  9. package/akanApp/akanApp.host.ts +82 -254
  10. package/akanApp/devHostPolicy.test.ts +10 -34
  11. package/akanApp/devHostPolicy.ts +12 -98
  12. package/akanConfig/akanConfig.test.ts +85 -69
  13. package/akanConfig/akanConfig.ts +69 -153
  14. package/akanConfig/types.ts +1 -0
  15. package/akanContext.ts +106 -159
  16. package/akanMcpContract.ts +28 -73
  17. package/appSelectionMemory.ts +1 -4
  18. package/applicationBuildReporter.ts +37 -5
  19. package/applicationBuildRunner.test.ts +5 -3
  20. package/applicationBuildRunner.ts +54 -61
  21. package/applicationReleasePackager.ts +24 -44
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +9 -30
  24. package/artifact/implicitRootLayout.ts +34 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +6 -1
  28. package/biomeBase.ts +2 -5
  29. package/capacitorApp.test.ts +32 -16
  30. package/capacitorApp.ts +110 -238
  31. package/cloud/cloudApi.test.ts +202 -0
  32. package/cloud/cloudApi.ts +77 -39
  33. package/cloud/configLock.test.ts +64 -0
  34. package/cloud/configLock.ts +96 -0
  35. package/cloud/constants.ts +26 -5
  36. package/cloud/globalConfig.ts +70 -32
  37. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  38. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  39. package/codeAgent/agent/CodeAgent.ts +551 -0
  40. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  41. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  42. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  43. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  44. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  45. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  46. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  47. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  48. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  49. package/codeAgent/agent/CodeMailbox.ts +148 -0
  50. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  51. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  52. package/codeAgent/agent/SubagentPool.ts +214 -0
  53. package/codeAgent/agent/akanCodeModel.ts +47 -0
  54. package/codeAgent/agent/akanCodePaths.ts +31 -0
  55. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  56. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  57. package/codeAgent/agent/codeAgent.test.ts +178 -0
  58. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  59. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  60. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  61. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  62. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  63. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  64. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  65. package/codeAgent/agent/consoleToStderr.ts +8 -0
  66. package/codeAgent/agent/jsonLines.ts +29 -0
  67. package/codeAgent/agent/stringArg.ts +5 -0
  68. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  69. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  70. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  71. package/codeAgent/feedback/PreviewView.ts +95 -0
  72. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  73. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  74. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  75. package/codeAgent/index.ts +34 -0
  76. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  77. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  78. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  79. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  80. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  81. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  82. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  83. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  84. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  85. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  86. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  87. package/codeAgent/tools/AkanCli.ts +50 -0
  88. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  89. package/codeAgent/tools/AkanEditScope.ts +48 -0
  90. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  91. package/codeAgent/tools/AkanToolPack.ts +92 -0
  92. package/codeAgent/tools/AkanVerifier.ts +48 -0
  93. package/codeAgent/tools/AskToolPack.ts +99 -0
  94. package/codeAgent/tools/MailToolPack.ts +76 -0
  95. package/codeAgent/tools/McpClient.ts +187 -0
  96. package/codeAgent/tools/McpOAuth.ts +233 -0
  97. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  98. package/codeAgent/tools/McpServerConfig.ts +129 -0
  99. package/codeAgent/tools/McpSignIn.ts +158 -0
  100. package/codeAgent/tools/McpTokenStore.ts +81 -0
  101. package/codeAgent/tools/McpToolPack.ts +199 -0
  102. package/codeAgent/tools/SessionToolPack.ts +91 -0
  103. package/codeAgent/tools/WebToolPack.ts +147 -0
  104. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  105. package/codeAgent/tools/askToolPack.test.ts +116 -0
  106. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  107. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  108. package/codegenLock.test.ts +4 -13
  109. package/codegenLock.ts +3 -17
  110. package/commandDecorators/argMeta.ts +1 -5
  111. package/commandDecorators/camelToKebabCase.ts +1 -0
  112. package/commandDecorators/command.ts +46 -102
  113. package/commandDecorators/commandBuilder.ts +30 -61
  114. package/commandDecorators/commandDecorators.test.ts +2 -10
  115. package/commandDecorators/dependencyBuilder.ts +10 -1
  116. package/commandDecorators/helpFormatter.ts +68 -145
  117. package/commandDecorators/index.ts +0 -2
  118. package/commandDecorators/targetMeta.ts +9 -7
  119. package/createTunnel.ts +7 -14
  120. package/dependencyScanner.test.ts +4 -16
  121. package/dependencyScanner.ts +31 -80
  122. package/devkitUtils.test.ts +48 -20
  123. package/executors.linkFallback.test.ts +103 -0
  124. package/executors.test.ts +132 -106
  125. package/executors.ts +194 -484
  126. package/fileEditor.ts +9 -37
  127. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  128. package/fileSys.ts +6 -8
  129. package/formSetterScanner.ts +3 -16
  130. package/frontendBuild/allRoutesBuilder.ts +7 -21
  131. package/frontendBuild/autoImportSync.test.ts +27 -38
  132. package/frontendBuild/autoImportSync.ts +31 -65
  133. package/frontendBuild/buildRouteClient.test.ts +62 -37
  134. package/frontendBuild/bundleDefine.ts +12 -0
  135. package/frontendBuild/clientBuildTypes.ts +5 -36
  136. package/frontendBuild/clientEntriesBundler.ts +9 -40
  137. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  138. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  139. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  140. package/frontendBuild/cssCandidateCache.ts +6 -41
  141. package/frontendBuild/cssCompiler.ts +29 -78
  142. package/frontendBuild/cssImportResolver.ts +6 -19
  143. package/frontendBuild/devChangePlanner.ts +7 -10
  144. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  145. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  146. package/frontendBuild/fontOptimizer.test.ts +6 -14
  147. package/frontendBuild/fontOptimizer.ts +15 -72
  148. package/frontendBuild/fontPruner.test.ts +5 -10
  149. package/frontendBuild/fontPruner.ts +5 -20
  150. package/frontendBuild/frontendBuild.test.ts +46 -69
  151. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  152. package/frontendBuild/hmrWatcher.test.ts +8 -32
  153. package/frontendBuild/hmrWatcher.ts +14 -71
  154. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  155. package/frontendBuild/precompressArtifacts.ts +5 -18
  156. package/frontendBuild/routeClientBuilder.ts +23 -46
  157. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  158. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  159. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  160. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  161. package/frontendBuild/styleContract.ts +1 -8
  162. package/frontendBuild/themeValidator.ts +4 -26
  163. package/frontendBuild/watchRootResolver.test.ts +23 -42
  164. package/frontendBuild/watchRootResolver.ts +5 -27
  165. package/getRelatedCnsts.ts +12 -92
  166. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  167. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  168. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  169. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  170. package/incrementalBuilder/builderChannel.test.ts +24 -49
  171. package/incrementalBuilder/builderChannel.ts +6 -37
  172. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  173. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  174. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  175. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  176. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  177. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  178. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  179. package/index.ts +5 -17
  180. package/integration/devResourceProbe.ts +50 -56
  181. package/integration/devStability.integration.test.ts +47 -194
  182. package/integration/devStabilityHarness.test.ts +13 -28
  183. package/integration/devStabilityHarness.ts +65 -330
  184. package/integration/ssrMemoryProbe.ts +41 -167
  185. package/libSource.test.ts +4 -15
  186. package/libSource.ts +19 -31
  187. package/lint/gritRules.test.ts +8 -24
  188. package/lint/no-deep-internal-import.grit +2 -2
  189. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  190. package/linter.test.ts +1 -2
  191. package/linter.ts +26 -116
  192. package/mobile/mobileTarget.ts +14 -13
  193. package/package.json +10 -5
  194. package/packageExports.test.ts +3 -18
  195. package/packageExportsMap.ts +3 -19
  196. package/piDependencyPins.test.ts +46 -0
  197. package/platformTest/BunTestReport.test.ts +60 -0
  198. package/platformTest/BunTestReport.ts +65 -0
  199. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  200. package/platformTest/IdleWatchedProcess.ts +73 -0
  201. package/platformTest/LinuxDockerTarget.ts +105 -0
  202. package/platformTest/PlatformTestRun.test.ts +64 -0
  203. package/platformTest/PlatformTestRun.ts +141 -0
  204. package/platformTest/PlatformTestTarget.ts +142 -0
  205. package/platformTest/TestSnapshot.test.ts +72 -0
  206. package/platformTest/TestSnapshot.ts +103 -0
  207. package/platformTest/WindowsSshTarget.ts +151 -0
  208. package/prompter.ts +3 -13
  209. package/qualityScanner.test.ts +9 -26
  210. package/qualityScanner.ts +197 -285
  211. package/recipeScanner.test.ts +7 -12
  212. package/recipeScanner.ts +9 -38
  213. package/repoIdentity.ts +2 -9
  214. package/routeSourceValidator.test.ts +0 -1
  215. package/routeSourceValidator.ts +7 -42
  216. package/scanInfo.ts +87 -216
  217. package/selectModel.ts +1 -2
  218. package/semver.ts +3 -5
  219. package/slicePlanner.test.ts +6 -24
  220. package/slicePlanner.ts +11 -32
  221. package/spinner.test.ts +3 -8
  222. package/spinner.ts +4 -16
  223. package/sshTunnel.ts +1 -6
  224. package/ssrScanner.test.ts +1 -6
  225. package/ssrScanner.ts +103 -113
  226. package/stripAnsi.ts +3 -0
  227. package/subspace.test.ts +30 -25
  228. package/subspace.ts +81 -115
  229. package/subspaceConfig.ts +11 -9
  230. package/testHelpers.ts +194 -0
  231. package/transforms/asyncDefaultExportDetector.ts +3 -11
  232. package/transforms/barrelAnalyzer.ts +43 -135
  233. package/transforms/barrelImportsPlugin.ts +38 -175
  234. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  235. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  236. package/transforms/moduleSyntax.ts +32 -0
  237. package/transforms/rscUseClientTransform.ts +35 -20
  238. package/transforms/transforms.test.ts +153 -44
  239. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  240. package/transforms/useClientBundlePlugin.ts +2 -22
  241. package/typeChecker.ts +7 -63
  242. package/types.ts +1 -0
  243. package/uploadRelease.ts +10 -25
  244. package/workflow/artifacts.ts +18 -0
  245. package/workflow/executor.test.ts +1 -12
  246. package/workflow/executor.ts +101 -110
  247. package/workflow/index.ts +0 -1
  248. package/workflow/moduleIndex.test.ts +4 -15
  249. package/workflow/moduleIndex.ts +9 -49
  250. package/workflow/plan.ts +18 -25
  251. package/workflow/render.ts +80 -96
  252. package/workflow/rolloutGate.test.ts +1 -1
  253. package/workflow/source.ts +52 -69
  254. package/workflow/sourceAst.ts +50 -0
  255. package/workspaceLayout.ts +4 -19
  256. package/abstractCompactor.ts +0 -112
  257. package/aiEditor.test.ts +0 -60
  258. package/aiEditor.ts +0 -387
  259. package/commandDecorators/commandMeta.ts +0 -7
  260. package/commandDecorators/types.ts +0 -10
  261. package/getDirname.ts +0 -4
  262. package/guideline.ts +0 -19
  263. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  264. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  265. package/workflow/primitive.ts +0 -59
@@ -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
  }
@@ -63,13 +63,7 @@ export class ApplicationReleasePackager {
63
63
  await rm(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
64
64
 
65
65
  const releaseRoot = this.#app.workspace.workspaceRoot;
66
- // Pass a path relative to cwd so tar never sees a Windows drive letter (e.g. "C:\...")
67
- // which GNU tar would interpret as a remote "host:file" spec.
68
- const releaseArchivePath = path
69
- .relative(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`)
70
- .split(path.sep)
71
- .join("/");
72
- await this.#app.workspace.spawn("tar", ["-zcf", releaseArchivePath, "-C", buildRoot, "./"], { cwd: releaseRoot });
66
+ await this.#tar(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`, buildRoot);
73
67
  await this.#writeCsrZipIfPresent();
74
68
  return { platformVersion };
75
69
  }
@@ -112,26 +106,19 @@ export class ApplicationReleasePackager {
112
106
  await this.#writeSourceTsconfig(sourceRoot, libDeps);
113
107
  await Bun.write(`${sourceRoot}/README.md`, readme);
114
108
  const sourceCwd = this.#app.workspace.cwdPath;
115
- // Pass a path relative to cwd so tar never sees a Windows drive letter (e.g. "C:\...")
116
- // which GNU tar would interpret as a remote "host:file" spec.
117
- const sourceArchivePath = path
118
- .relative(sourceCwd, `${sourceCwd}/releases/sources/${this.#app.name}-source.tar.gz`)
119
- .split(path.sep)
120
- .join("/");
121
- await this.#app.workspace.spawn("tar", ["-zcf", sourceArchivePath, "-C", sourceRoot, "./"], { cwd: sourceCwd });
109
+ await this.#tar(sourceCwd, `${sourceCwd}/releases/sources/${this.#app.name}-source.tar.gz`, sourceRoot);
110
+ }
111
+
112
+ async #tar(cwd: string, archivePath: string, sourceDir: string) {
113
+ // Relative to cwd so tar never sees a Windows drive letter ("C:\..."), which GNU tar reads as a remote "host:file".
114
+ const relativeArchivePath = path.relative(cwd, archivePath).split(path.sep).join("/");
115
+ await this.#app.workspace.spawn("tar", ["-zcf", relativeArchivePath, "-C", sourceDir, "./"], { cwd });
122
116
  }
123
117
 
124
118
  async #resetSourceRoot(sourceRoot: string): Promise<void> {
125
- if (await FileSys.dirExists(sourceRoot)) {
126
- const maxRetry = 3;
127
- for (let i = 0; i < maxRetry; i++) {
128
- try {
129
- await rm(sourceRoot, { recursive: true, force: true });
130
- } catch {
131
- //
132
- }
133
- }
134
- }
119
+ if (await FileSys.dirExists(sourceRoot))
120
+ for (let attempt = 0; attempt < 3; attempt++)
121
+ await rm(sourceRoot, { recursive: true, force: true }).catch(() => undefined);
135
122
  await mkdir(sourceRoot, { recursive: true });
136
123
  }
137
124
 
@@ -147,6 +134,17 @@ export class ApplicationReleasePackager {
147
134
  await Bun.write(`${sourceRoot}/tsconfig.json`, JSON.stringify(tsconfig, null, 2));
148
135
  }
149
136
 
137
+ static #envSetup = ` cat <<EOF >> .env
138
+ # ENV For Server => debug | debug.local | develop | develop.local | main | main.local
139
+ SERVER_ENV=debug.local
140
+ # Run Mode For Server => federation | batch | all
141
+ SERVER_MODE=federation
142
+ # ENV For Client => debug | debug.local | develop | develop.local | main | main.local
143
+ AKAN_PUBLIC_CLIENT_ENV=debug.local
144
+ ANALYZE=false
145
+ EOF
146
+ `;
147
+
150
148
  #confidentialReadme(): string {
151
149
  return `# ${this.#app.name}
152
150
  본 프로젝트의 소스코드 및 관련자료는 모두 비밀정보로 관리됩니다.
@@ -157,16 +155,7 @@ export class ApplicationReleasePackager {
157
155
  bun install -g bun
158
156
  bun i
159
157
 
160
- cat <<EOF >> .env
161
- # ENV For Server => debug | debug.local | develop | develop.local | main | main.local
162
- SERVER_ENV=debug.local
163
- # Run Mode For Server => federation | batch | all
164
- SERVER_MODE=federation
165
- # ENV For Client => debug | debug.local | develop | develop.local | main | main.local
166
- AKAN_PUBLIC_CLIENT_ENV=debug.local
167
- ANALYZE=false
168
- EOF
169
-
158
+ ${ApplicationReleasePackager.#envSetup}
170
159
  akn start-backend ${this.#app.name}
171
160
  # or akn start-frontend ${this.#app.name}, etc
172
161
  \`\`\`
@@ -185,16 +174,7 @@ export class ApplicationReleasePackager {
185
174
  ## Get Started
186
175
  Run the code below.
187
176
  \`\`\`
188
- cat <<EOF >> .env
189
- # ENV For Server => debug | debug.local | develop | develop.local | main | main.local
190
- SERVER_ENV=debug.local
191
- # Run Mode For Server => federation | batch | all
192
- SERVER_MODE=federation
193
- # ENV For Client => debug | debug.local | develop | develop.local | main | main.local
194
- AKAN_PUBLIC_CLIENT_ENV=debug.local
195
- ANALYZE=false
196
- EOF
197
-
177
+ ${ApplicationReleasePackager.#envSetup}
198
178
  akan start ${this.#app.name}
199
179
  \`\`\`
200
180
 
@@ -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;");