@akanjs/devkit 3.0.0-beta.19 → 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 (233) hide show
  1. package/abstractDoc.test.ts +6 -15
  2. package/abstractDoc.ts +1 -4
  3. package/agentsIndex.test.ts +4 -7
  4. package/agentsIndex.ts +6 -22
  5. package/akanApp/BackendImportGraph.test.ts +8 -22
  6. package/akanApp/BackendImportGraph.ts +1 -20
  7. package/akanApp/akanApp.host.test.ts +1 -3
  8. package/akanApp/akanApp.host.ts +64 -250
  9. package/akanApp/devHostPolicy.test.ts +10 -34
  10. package/akanApp/devHostPolicy.ts +12 -98
  11. package/akanConfig/akanConfig.test.ts +30 -39
  12. package/akanConfig/akanConfig.ts +40 -136
  13. package/akanContext.ts +106 -159
  14. package/akanMcpContract.ts +28 -73
  15. package/appSelectionMemory.ts +1 -4
  16. package/applicationBuildReporter.ts +37 -5
  17. package/applicationBuildRunner.ts +25 -59
  18. package/applicationReleasePackager.ts +24 -44
  19. package/applicationTestPreload.ts +3 -12
  20. package/artifact/implicitRootLayout.test.ts +3 -19
  21. package/artifact/implicitRootLayout.ts +34 -91
  22. package/artifact/routeSeedIndex.ts +11 -44
  23. package/biomeBase.ts +2 -5
  24. package/capacitorApp.test.ts +4 -14
  25. package/capacitorApp.ts +86 -235
  26. package/cloud/cloudApi.test.ts +202 -0
  27. package/cloud/cloudApi.ts +33 -57
  28. package/cloud/configLock.test.ts +64 -0
  29. package/cloud/configLock.ts +96 -0
  30. package/cloud/constants.ts +3 -2
  31. package/cloud/globalConfig.ts +64 -31
  32. package/codeAgent/agent/AkanCodeServices.ts +15 -57
  33. package/codeAgent/agent/AkanContextFiles.ts +5 -28
  34. package/codeAgent/agent/CodeAgent.ts +33 -108
  35. package/codeAgent/agent/CodeAgentAsks.ts +7 -30
  36. package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
  37. package/codeAgent/agent/CodeAgentGate.ts +8 -34
  38. package/codeAgent/agent/CodeAgentProxy.ts +4 -13
  39. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
  40. package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
  41. package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
  42. package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
  43. package/codeAgent/agent/CodeAgentUi.ts +2 -7
  44. package/codeAgent/agent/CodeMailbox.ts +7 -34
  45. package/codeAgent/agent/CodeSessionFork.ts +4 -19
  46. package/codeAgent/agent/CodeSessionIndex.ts +5 -31
  47. package/codeAgent/agent/SubagentPool.ts +48 -58
  48. package/codeAgent/agent/akanCodeModel.ts +5 -39
  49. package/codeAgent/agent/akanCodePaths.ts +4 -24
  50. package/codeAgent/agent/akanContextFiles.test.ts +0 -2
  51. package/codeAgent/agent/akanSystemPrompt.ts +1 -9
  52. package/codeAgent/agent/codeAgent.test.ts +178 -0
  53. package/codeAgent/agent/codeAgentContract.test.ts +5 -28
  54. package/codeAgent/agent/codeMailbox.test.ts +0 -4
  55. package/codeAgent/agent/codeSessionFork.test.ts +0 -2
  56. package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
  57. package/codeAgent/agent/consoleToStderr.ts +2 -12
  58. package/codeAgent/agent/jsonLines.ts +29 -0
  59. package/codeAgent/agent/stringArg.ts +5 -0
  60. package/codeAgent/feedback/DevLogFeedback.ts +3 -18
  61. package/codeAgent/feedback/PreviewChrome.ts +5 -29
  62. package/codeAgent/feedback/PreviewFeedback.ts +2 -9
  63. package/codeAgent/feedback/PreviewView.ts +3 -15
  64. package/codeAgent/feedback/TurnFeedback.ts +2 -17
  65. package/codeAgent/feedback/VerifyFeedback.ts +1 -7
  66. package/codeAgent/feedback/previewChrome.test.ts +1 -1
  67. package/codeAgent/index.ts +2 -10
  68. package/codeAgent/skills/akan-code/SKILL.md +12 -4
  69. package/codeAgent/tools/AkanCli.ts +4 -16
  70. package/codeAgent/tools/AkanCodePlugins.ts +21 -43
  71. package/codeAgent/tools/AkanEditScope.ts +2 -12
  72. package/codeAgent/tools/AkanEnvKeys.ts +3 -11
  73. package/codeAgent/tools/AkanToolPack.ts +6 -28
  74. package/codeAgent/tools/AkanVerifier.ts +1 -8
  75. package/codeAgent/tools/AskToolPack.ts +3 -15
  76. package/codeAgent/tools/MailToolPack.ts +0 -9
  77. package/codeAgent/tools/McpClient.ts +3 -24
  78. package/codeAgent/tools/McpOAuth.ts +6 -41
  79. package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
  80. package/codeAgent/tools/McpServerConfig.ts +7 -26
  81. package/codeAgent/tools/McpSignIn.ts +46 -49
  82. package/codeAgent/tools/McpTokenStore.ts +6 -17
  83. package/codeAgent/tools/McpToolPack.ts +107 -35
  84. package/codeAgent/tools/SessionToolPack.ts +1 -11
  85. package/codeAgent/tools/WebToolPack.ts +1 -7
  86. package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
  87. package/codeAgent/tools/askToolPack.test.ts +0 -3
  88. package/codeAgent/tools/mcpOAuth.test.ts +315 -27
  89. package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
  90. package/codegenLock.test.ts +4 -13
  91. package/codegenLock.ts +3 -17
  92. package/commandDecorators/argMeta.ts +1 -5
  93. package/commandDecorators/camelToKebabCase.ts +1 -0
  94. package/commandDecorators/command.ts +44 -102
  95. package/commandDecorators/commandBuilder.ts +30 -61
  96. package/commandDecorators/commandDecorators.test.ts +1 -9
  97. package/commandDecorators/dependencyBuilder.ts +10 -1
  98. package/commandDecorators/helpFormatter.ts +66 -144
  99. package/commandDecorators/index.ts +0 -2
  100. package/commandDecorators/targetMeta.ts +9 -7
  101. package/createTunnel.ts +7 -14
  102. package/dependencyScanner.test.ts +4 -16
  103. package/dependencyScanner.ts +27 -78
  104. package/devkitUtils.test.ts +47 -19
  105. package/executors.test.ts +114 -60
  106. package/executors.ts +165 -484
  107. package/fileEditor.ts +9 -37
  108. package/{getDirname.test.ts → fileSys.test.ts} +1 -1
  109. package/fileSys.ts +6 -8
  110. package/formSetterScanner.ts +3 -16
  111. package/frontendBuild/allRoutesBuilder.ts +7 -21
  112. package/frontendBuild/autoImportSync.test.ts +14 -38
  113. package/frontendBuild/autoImportSync.ts +15 -67
  114. package/frontendBuild/buildRouteClient.test.ts +10 -36
  115. package/frontendBuild/bundleDefine.ts +12 -0
  116. package/frontendBuild/clientBuildTypes.ts +5 -36
  117. package/frontendBuild/clientEntriesBundler.ts +9 -40
  118. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  119. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  120. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  121. package/frontendBuild/cssCandidateCache.ts +6 -41
  122. package/frontendBuild/cssCompiler.ts +29 -78
  123. package/frontendBuild/cssImportResolver.ts +5 -18
  124. package/frontendBuild/devChangePlanner.ts +7 -10
  125. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  126. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  127. package/frontendBuild/fontOptimizer.test.ts +5 -13
  128. package/frontendBuild/fontOptimizer.ts +15 -72
  129. package/frontendBuild/fontPruner.test.ts +5 -10
  130. package/frontendBuild/fontPruner.ts +5 -20
  131. package/frontendBuild/frontendBuild.test.ts +27 -52
  132. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  133. package/frontendBuild/hmrWatcher.test.ts +7 -31
  134. package/frontendBuild/hmrWatcher.ts +14 -71
  135. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  136. package/frontendBuild/precompressArtifacts.ts +5 -18
  137. package/frontendBuild/routeClientBuilder.ts +7 -47
  138. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  139. package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
  140. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  141. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  142. package/frontendBuild/styleContract.ts +1 -8
  143. package/frontendBuild/themeValidator.ts +4 -26
  144. package/frontendBuild/watchRootResolver.test.ts +23 -42
  145. package/frontendBuild/watchRootResolver.ts +5 -27
  146. package/getRelatedCnsts.ts +12 -92
  147. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  148. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  149. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  150. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  151. package/incrementalBuilder/builderChannel.test.ts +24 -49
  152. package/incrementalBuilder/builderChannel.ts +6 -37
  153. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  154. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  155. package/incrementalBuilder/devWatchBatch.test.ts +14 -18
  156. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  157. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  158. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  159. package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
  160. package/index.ts +3 -15
  161. package/integration/devResourceProbe.ts +50 -56
  162. package/integration/devStability.integration.test.ts +47 -194
  163. package/integration/devStabilityHarness.test.ts +9 -29
  164. package/integration/devStabilityHarness.ts +65 -330
  165. package/integration/ssrMemoryProbe.ts +41 -167
  166. package/libSource.test.ts +4 -15
  167. package/libSource.ts +19 -31
  168. package/lint/gritRules.test.ts +6 -23
  169. package/linter.test.ts +1 -2
  170. package/linter.ts +22 -115
  171. package/mobile/mobileTarget.ts +14 -13
  172. package/package.json +2 -2
  173. package/packageExports.test.ts +3 -18
  174. package/packageExportsMap.ts +3 -19
  175. package/piDependencyPins.test.ts +3 -14
  176. package/platformTest/IdleWatchedProcess.test.ts +5 -13
  177. package/platformTest/IdleWatchedProcess.ts +1 -5
  178. package/platformTest/LinuxDockerTarget.ts +20 -35
  179. package/platformTest/PlatformTestRun.test.ts +5 -9
  180. package/platformTest/PlatformTestTarget.ts +24 -2
  181. package/platformTest/TestSnapshot.test.ts +6 -14
  182. package/platformTest/TestSnapshot.ts +1 -5
  183. package/platformTest/WindowsSshTarget.ts +6 -22
  184. package/prompter.ts +3 -6
  185. package/qualityScanner.test.ts +6 -24
  186. package/qualityScanner.ts +196 -284
  187. package/recipeScanner.test.ts +7 -12
  188. package/recipeScanner.ts +9 -38
  189. package/repoIdentity.ts +2 -9
  190. package/routeSourceValidator.ts +7 -30
  191. package/scanInfo.ts +82 -214
  192. package/selectModel.ts +1 -2
  193. package/semver.ts +3 -5
  194. package/slicePlanner.test.ts +6 -24
  195. package/slicePlanner.ts +11 -32
  196. package/spinner.test.ts +3 -8
  197. package/spinner.ts +4 -16
  198. package/sshTunnel.ts +1 -6
  199. package/ssrScanner.test.ts +1 -6
  200. package/ssrScanner.ts +103 -113
  201. package/subspace.test.ts +6 -25
  202. package/subspace.ts +31 -109
  203. package/subspaceConfig.ts +3 -11
  204. package/testHelpers.ts +49 -34
  205. package/transforms/asyncDefaultExportDetector.ts +3 -11
  206. package/transforms/barrelAnalyzer.ts +43 -135
  207. package/transforms/barrelImportsPlugin.ts +38 -175
  208. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  209. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  210. package/transforms/moduleSyntax.ts +32 -0
  211. package/transforms/rscUseClientTransform.ts +35 -20
  212. package/transforms/transforms.test.ts +153 -44
  213. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  214. package/transforms/useClientBundlePlugin.ts +2 -22
  215. package/typeChecker.ts +7 -63
  216. package/uploadRelease.ts +10 -25
  217. package/workflow/artifacts.ts +18 -0
  218. package/workflow/executor.test.ts +1 -12
  219. package/workflow/executor.ts +85 -146
  220. package/workflow/index.ts +0 -1
  221. package/workflow/moduleIndex.test.ts +4 -15
  222. package/workflow/moduleIndex.ts +9 -49
  223. package/workflow/plan.ts +18 -25
  224. package/workflow/render.ts +80 -96
  225. package/workflow/source.ts +33 -56
  226. package/workflow/sourceAst.ts +50 -0
  227. package/workspaceLayout.ts +4 -19
  228. package/commandDecorators/commandMeta.ts +0 -7
  229. package/commandDecorators/types.ts +0 -10
  230. package/getDirname.ts +0 -4
  231. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  232. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  233. package/workflow/primitive.ts +0 -59
@@ -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) {
@@ -215,57 +204,43 @@ export class ApplicationBuildRunner {
215
204
  }
216
205
 
217
206
  async #buildBackend() {
218
- const akanConfig = await this.#app.getConfig();
219
- const backendExternals = [
220
- ...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS, ...AKAN_OPTIONAL_BACKEND_EXTERNALS]),
221
- ];
222
- const { web } = akanConfig;
207
+ const { externalLibs, web } = await this.#app.getConfig();
223
208
  const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
224
209
  for (const entrypoint of backendEntryPoints) {
225
210
  if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
226
211
  }
227
- const backendConfig = {
212
+ const sharedConfig = {
228
213
  outdir: this.#app.dist.cwdPath,
229
214
  target: "bun",
230
215
  minify: AKAN_BACKEND_MINIFY,
231
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.
232
218
  define: { "process.env.NODE_ENV": JSON.stringify("production") },
233
- plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
234
219
  } satisfies Omit<Bun.BuildConfig, "entrypoints">;
235
- //* Built apart so main.js keeps its own module copies; splitting moves lazy vendor `import()`s out of the boot
236
- //* parse (minimal: server.js import 79ms → 24ms, 59MB → 36MB RSS).
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.
237
222
  const [mainResult, serverResult] = [
238
223
  await this.#buildOrThrow("backend", { ...backendConfig, entrypoints: [backendEntryPoints[0]] }),
239
224
  await this.#buildOrThrow("backend", { ...backendConfig, entrypoints: [backendEntryPoints[1]], splitting: true }),
240
225
  ];
241
- const backendResult = { outputs: [...mainResult.outputs, ...serverResult.outputs] };
242
226
  // Nothing spawns the RSC worker without SSR, so an api-only image does not carry it.
243
227
  const rscWorkerResult = web.ssr
244
228
  ? await this.#buildOrThrow("rsc-worker", {
229
+ ...backendConfig,
245
230
  entrypoints: [this.#resolveRscWorkerBuildEntry()],
246
- outdir: this.#app.dist.cwdPath,
247
- target: "bun",
248
- minify: AKAN_BACKEND_MINIFY,
249
- naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
250
231
  conditions: ["react-server"],
251
- // `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
252
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
253
- plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
254
232
  })
255
233
  : null;
256
234
  const consoleRuntimeResult = await this.#buildOrThrow("console-runtime", {
235
+ ...sharedConfig,
257
236
  entrypoints: [this.#resolveConsoleRuntimeBuildEntry()],
258
- outdir: this.#app.dist.cwdPath,
259
- target: "bun",
260
- minify: AKAN_BACKEND_MINIFY,
261
237
  naming: { entry: "console-runtime.[ext]", chunk: "chunk-[hash].[ext]" },
262
- define: { "process.env.NODE_ENV": JSON.stringify("production") },
263
238
  });
264
239
  await this.#writeConsoleShim();
240
+ const results = [mainResult, serverResult, rscWorkerResult, consoleRuntimeResult];
265
241
  return {
266
242
  entrypoints: backendEntryPoints.length + (rscWorkerResult ? 2 : 1),
267
- outputs:
268
- backendResult.outputs.length + (rscWorkerResult?.outputs.length ?? 0) + consoleRuntimeResult.outputs.length + 1,
243
+ outputs: results.reduce((sum, result) => sum + (result?.outputs.length ?? 0), 1),
269
244
  };
270
245
  }
271
246
 
@@ -310,10 +285,6 @@ void run().catch((error) => {
310
285
  }
311
286
  }
312
287
 
313
- async #buildCsr() {
314
- return await new CsrArtifactBuilder(this.#app, "build").build();
315
- }
316
-
317
288
  async #buildSsr() {
318
289
  const pageKeys = await this.#app.getPageKeys();
319
290
  if (pageKeys.length === 0) {
@@ -325,7 +296,7 @@ void run().catch((error) => {
325
296
  return { base, allRoutes };
326
297
  }
327
298
 
328
- async #writeTypecheckTsconfig({ incremental = true }: TypecheckOptions = {}) {
299
+ async #writeTypecheckTsconfig(incremental: boolean) {
329
300
  const typecheckDir = path.join(this.#app.cwdPath, ".akan", "typecheck");
330
301
  await mkdir(typecheckDir, { recursive: true });
331
302
  //* TypeScript's `include` globs do not cross a symlink, so synced lib pages need their real path.
@@ -394,11 +365,10 @@ void run().catch((error) => {
394
365
  return result;
395
366
  }
396
367
 
397
- #createExternalSpecifiersPlugin(specifiers: readonly string[]): BunPlugin {
398
- const uniqueSpecifiers = [...new Set(specifiers)];
399
- 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, "\\$&"));
400
371
  const filter = new RegExp(`^(${escaped.join("|")})(?:/.*)?$`);
401
-
402
372
  return {
403
373
  name: "akan-backend-externalize-specifiers",
404
374
  setup(build) {
@@ -407,9 +377,5 @@ void run().catch((error) => {
407
377
  };
408
378
  }
409
379
 
410
- static formatBytes(bytes: number): string {
411
- if (bytes < 1024) return `${bytes}B`;
412
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
413
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
414
- }
380
+ static formatBytes = FontPruner.formatBytes;
415
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 () => {
@@ -1,4 +1,3 @@
1
- import { mkdir } from "node:fs/promises";
2
1
  import path from "node:path";
3
2
  import type { App } from "../commandDecorators";
4
3
  import { AsyncDefaultExportDetector } from "../transforms/asyncDefaultExportDetector";
@@ -11,23 +10,17 @@ export interface PageEntry {
11
10
 
12
11
  const LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
13
12
 
14
- async function appHasStModule(appCwdPath: string): Promise<boolean> {
15
- return Bun.file(path.join(appCwdPath, "lib", "st.ts")).exists();
16
- }
17
-
18
13
  const IMPLICIT_LAYOUT_DIR = path.join(".akan", "generated", "root-layouts");
19
14
  const IMPLICIT_DICT_DIR = path.join(".akan", "generated", "dict");
20
15
  const IMPLICIT_OVERRIDES_DIR = path.join(".akan", "generated", "overrides");
21
16
  const OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
22
17
 
23
- /**
24
- * A `_overrides.tsx` manifest is a plain, server-safe module (`export default override({ Modal: BrandModal })`)
25
- * with no `"use client"` directive. The `UiOverrideProvider` that consumes it is a client component, so the
26
- * build emits a `"use client"` wrapper layout that reads the manifest's default map and mounts the provider
27
- * around the subtree. As a normal `"use client"` module the wrapper participates in client-entry discovery and
28
- * the RSC client manifest, so the server (as a client reference) and the client (as the real component) both
29
- * resolve it — and the author never writes `"use client"`.
30
- */
18
+ const importSpecifier = (fromAbsPath: string, toAbsPath: string) => {
19
+ const rel = path.relative(path.dirname(fromAbsPath), toAbsPath).split(path.sep).join("/");
20
+ return rel.startsWith(".") ? rel : `./${rel}`;
21
+ };
22
+
23
+ // UiOverrideProvider is a client component, so `_overrides.tsx` mounts through a generated "use client" layout.
31
24
  async function writeGeneratedOverridesLayoutFile(opts: {
32
25
  appCwdPath: string;
33
26
  key: string;
@@ -35,12 +28,10 @@ async function writeGeneratedOverridesLayoutFile(opts: {
35
28
  }): Promise<string> {
36
29
  const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
37
30
  const absPath = path.join(path.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
38
- const userRel = path.relative(path.dirname(absPath), opts.userAbsPath).split(path.sep).join("/");
39
- const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
40
31
  const source = `"use client";
41
32
  import { UiOverrideProvider } from "akanjs/ui";
42
33
  import { createElement, type ReactNode } from "react";
43
- import value from ${JSON.stringify(userSpecifier)};
34
+ import value from ${JSON.stringify(importSpecifier(absPath, opts.userAbsPath))};
44
35
 
45
36
  export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
46
37
  return createElement(UiOverrideProvider, { value }, children);
@@ -73,10 +64,6 @@ function implicitRootLayoutAbsPath(appCwdPath: string, segments: string[]): stri
73
64
  return path.join(path.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
74
65
  }
75
66
 
76
- function implicitDictionaryMacroAbsPath(appCwdPath: string): string {
77
- return path.join(path.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
78
- }
79
-
80
67
  export function isRootBoundarySegments(segments: string[], basePaths: Iterable<string>): boolean {
81
68
  const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
82
69
  if (firstVisibleIndex === -1) return segments.length <= 1;
@@ -99,10 +86,7 @@ function findRootBoundaries(pageKeys: string[], appCwdPath: string, basePaths: I
99
86
  segments,
100
87
  });
101
88
  }
102
- const hasExplicitRootBoundary = [...boundaries.values()].some((boundary) => boundary.segments.length === 0);
103
- if (!hasExplicitRootBoundary && boundaries.size === 0) {
104
- boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
105
- }
89
+ if (boundaries.size === 0) boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
106
90
  return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
107
91
  }
108
92
 
@@ -123,11 +107,6 @@ function findExplicitRootLayoutAbsPath(pageKeys: string[], appCwdPath: string):
123
107
  return rootLayoutKey ? path.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
124
108
  }
125
109
 
126
- function routePrefixForSegments(segments: string[]): string | null {
127
- const visible = segments.filter((segment) => !/^\(.+\)$/.test(segment));
128
- return visible[0] ?? null;
129
- }
130
-
131
110
  async function assertEnvClientConvention(appCwdPath: string, appName: string) {
132
111
  const envPath = path.join(appCwdPath, "env", "env.client.ts");
133
112
  if (!(await Bun.file(envPath).exists())) {
@@ -138,8 +117,7 @@ async function assertEnvClientConvention(appCwdPath: string, appName: string) {
138
117
  }
139
118
 
140
119
  async function writeGeneratedDictionaryMacroFile(appCwdPath: string, appName: string): Promise<string> {
141
- const absPath = implicitDictionaryMacroAbsPath(appCwdPath);
142
- await mkdir(path.dirname(absPath), { recursive: true });
120
+ const absPath = path.join(path.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
143
121
  await Bun.write(
144
122
  absPath,
145
123
  `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
@@ -163,35 +141,20 @@ async function writeGeneratedRootLayoutFile(opts: {
163
141
  ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName)
164
142
  : null;
165
143
  const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
166
- await mkdir(path.dirname(absPath), { recursive: true });
167
- const dictMacroRel = dictMacroAbsPath
168
- ? path.relative(path.dirname(absPath), dictMacroAbsPath).split(path.sep).join("/")
169
- : null;
170
- const dictMacroSpecifier = dictMacroRel ? (dictMacroRel.startsWith(".") ? dictMacroRel : `./${dictMacroRel}`) : null;
171
- const sourceRel = opts.boundary.sourceAbsPath
172
- ? path.relative(path.dirname(absPath), opts.boundary.sourceAbsPath).split(path.sep).join("/")
173
- : null;
174
- const sourceSpecifier = sourceRel ? (sourceRel.startsWith(".") ? sourceRel : `./${sourceRel}`) : null;
144
+ const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
145
+ const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
175
146
  const inheritedSourceAbsPath =
176
147
  opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
177
- const inheritedSourceRel = inheritedSourceAbsPath
178
- ? path.relative(path.dirname(absPath), inheritedSourceAbsPath).split(path.sep).join("/")
179
- : null;
180
- const inheritedSourceSpecifier = inheritedSourceRel
181
- ? inheritedSourceRel.startsWith(".")
182
- ? inheritedSourceRel
183
- : `./${inheritedSourceRel}`
184
- : null;
148
+ const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
185
149
  const clientImport = opts.includeStInit
186
150
  ? `import { st } from "@apps/${opts.appName}/client";\nvoid st;\n`
187
151
  : `import "@apps/${opts.appName}/client";\n`;
188
- // Both user modules go through `resolveRouteModule`: a `rootLayout()` chain and the legacy named exports read
189
- // the same afterwards, and this file never has to know which shape the app wrote.
152
+ //? resolveRouteModule reads a rootLayout() chain and the legacy named exports alike.
190
153
  const inheritedLabel = inheritedSourceAbsPath ? path.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
191
154
  const inheritedImport = inheritedSourceSpecifier
192
155
  ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};\nconst inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;\n`
193
156
  : "const inheritedLayout: LayoutModule = {};\n";
194
- const prefix = routePrefixForSegments(opts.boundary.segments);
157
+ const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
195
158
  const userLabel = opts.boundary.sourceAbsPath ? path.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
196
159
  const userImport = sourceSpecifier
197
160
  ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};\nconst userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;\nconst UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;\n`
@@ -200,9 +163,8 @@ async function writeGeneratedRootLayoutFile(opts: {
200
163
  ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath)
201
164
  : false;
202
165
  const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
203
- // React has no async client component, so the CSR bundle calls an async layout and awaits its node the way
204
- // `RenderLayer` does for pages. The RSC render keeps the element: awaiting there would hold the shell behind
205
- // the layout's own awaits instead of streaming it as its own Flight chunk.
166
+ // React has no async client component, so CSR awaits an async layout's node; RSC keeps the element, since
167
+ // awaiting there would hold the shell behind the layout's own awaits instead of streaming it.
206
168
  const layoutSignature = isAsyncUserLayout
207
169
  ? "export default async function GeneratedLayout"
208
170
  : "export default function GeneratedLayout";
@@ -215,6 +177,19 @@ async function writeGeneratedRootLayoutFile(opts: {
215
177
  : "";
216
178
  const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
217
179
  const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
180
+ const layoutExports = `export async function generateHead(props: PageProps) {
181
+ if (userLayout.generateHead) return userLayout.generateHead(props);
182
+ if (userLayout.head !== undefined) return userLayout.head;
183
+ if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
184
+ return inheritedLayout.head;
185
+ }
186
+
187
+ export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
188
+ export const Error = userLayout.Error ?? inheritedLayout.Error;
189
+ export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
190
+
191
+ ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
192
+ ${layoutBinding} return `;
218
193
  const source = opts.includeSystemProvider
219
194
  ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
220
195
  import { loadFonts, resolveRouteModule } from "akanjs/client";
@@ -229,19 +204,7 @@ if (defaultFonts.length > 1) throw new Error("[route-convention] only one defaul
229
204
  const defaultFont = defaultFonts[0];
230
205
  const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
231
206
 
232
- export async function generateHead(props: PageProps) {
233
- if (userLayout.generateHead) return userLayout.generateHead(props);
234
- if (userLayout.head !== undefined) return userLayout.head;
235
- if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
236
- return inheritedLayout.head;
237
- }
238
-
239
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
240
- export const Error = userLayout.Error ?? inheritedLayout.Error;
241
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
242
-
243
- ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
244
- ${layoutBinding} return (
207
+ ${layoutExports}(
245
208
  <System.Provider
246
209
  of={GeneratedLayout as never}
247
210
  appName=${JSON.stringify(opts.appName)}
@@ -265,29 +228,13 @@ ${layoutBinding} return (
265
228
  import { resolveRouteModule } from "akanjs/client";
266
229
  import type { ReactNode } from "react";
267
230
  ${inheritedImport}${userImport}
268
- export async function generateHead(props: PageProps) {
269
- if (userLayout.generateHead) return userLayout.generateHead(props);
270
- if (userLayout.head !== undefined) return userLayout.head;
271
- if (inheritedLayout.generateHead) return inheritedLayout.generateHead(props);
272
- return inheritedLayout.head;
273
- }
274
-
275
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
276
- export const Error = userLayout.Error ?? inheritedLayout.Error;
277
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
278
-
279
- ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
280
- ${layoutBinding} return ${layoutReturn};
231
+ ${layoutExports}${layoutReturn};
281
232
  }
282
233
  `;
283
234
  await Bun.write(absPath, source);
284
235
  return absPath;
285
236
  }
286
237
 
287
- /**
288
- * When no root `page/_layout.*` exists on disk, merge a generated implicit root layout
289
- * (with generated client runtime registration and optional `void st` when `lib/st.ts` exists).
290
- */
291
238
  export async function resolveSsrPageEntries(opts: {
292
239
  appCwdPath: string;
293
240
  appName: string;
@@ -295,7 +242,7 @@ export async function resolveSsrPageEntries(opts: {
295
242
  basePaths?: Iterable<string>;
296
243
  }): Promise<PageEntry[]> {
297
244
  const absPageDir = path.resolve(opts.appCwdPath, "page");
298
- const hasSt = await appHasStModule(opts.appCwdPath);
245
+ const hasSt = await Bun.file(path.join(opts.appCwdPath, "lib", "st.ts")).exists();
299
246
  const basePaths = opts.basePaths ?? [];
300
247
  const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
301
248
  const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
@@ -310,9 +257,7 @@ export async function resolveSsrPageEntries(opts: {
310
257
  .filter((key) => !rootLayoutKeys.has(key))
311
258
  .map(async (key) => {
312
259
  const userAbsPath = path.resolve(absPageDir, key);
313
- // `_overrides.tsx` is served through a generated `"use client"` wrapper layout that mounts the provider,
314
- // so the author writes no directive; the raw manifest becomes a build seed so its slot components (and
315
- // the manifest itself) enter the client graph / RSC client manifest.
260
+ //? The raw manifest is a seed, so it and its slot components enter the client graph and RSC client manifest.
316
261
  if (OVERRIDES_KEY_RE.test(key)) {
317
262
  const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
318
263
  appCwdPath: opts.appCwdPath,
@@ -338,9 +283,7 @@ export async function resolveSsrPageEntries(opts: {
338
283
  seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))],
339
284
  })),
340
285
  );
341
- const entries = [...base, ...generated];
342
- entries.sort((a, b) => a.key.localeCompare(b.key));
343
- return entries;
286
+ return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
344
287
  }
345
288
 
346
289
  export async function resolveSsrPageEntriesForApp(app: App, pageKeys: string[]): Promise<PageEntry[]> {