@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
@@ -1,9 +1,7 @@
1
1
  import path from "node:path";
2
- // Module paths, never a barrel. The `@akanjs/devkit` root re-exports all 41 modules, which would drag
3
- // @trapezedev/project, ssh2, ink and the cloud stack in; and `frontendBuild`'s own
4
- // barrel reaches `cssCompiler`/`ssrBaseArtifactBuilder`, which pull tailwindcss + @tailwindcss/node
5
- // (~40MB) into a process that then holds them for the whole dev session. Phase 2 moved css compilation
6
- // into the batch worker, so this process has no use for them — `entryModuleGraph.test.ts` keeps it that way.
2
+ // Module paths, never a barrel: the devkit and `frontendBuild` barrels would hold tailwindcss (~40MB), ssh2, ink
3
+ // and the cloud stack for the whole dev session (`entryModuleGraph.test.ts` enforces it).
4
+ import { ApplicationBuildReporter } from "@akanjs/devkit/applicationBuildReporter";
7
5
  import { CodegenLock } from "@akanjs/devkit/codegenLock";
8
6
  import type { App } from "@akanjs/devkit/commandDecorators";
9
7
  import { AppExecutor, type PageRoot, WorkspaceExecutor } from "@akanjs/devkit/executors";
@@ -41,6 +39,7 @@ interface IncrementalBuilderOptions {
41
39
  }
42
40
 
43
41
  type IncrementalBuilderBootDeps = Pick<IncrementalBuilderOptions, "artifact" | "optimizedFonts" | "discovery">;
42
+ type BatchWork = Pick<BuildBatchRequest, "generation" | "needs" | "changedFiles">;
44
43
 
45
44
  class IncrementalBuilder {
46
45
  #logger = new Logger("IncrementalBuilder");
@@ -84,10 +83,7 @@ class IncrementalBuilder {
84
83
  return `${this.#app.cwdPath}/.akan/artifact`;
85
84
  }
86
85
 
87
- /**
88
- * Build a route and answer it. The reply is part of the work item on purpose: `shutdown` drains the
89
- * work queue before exiting, so folding the flush in here is what makes "drained" mean "answered".
90
- */
86
+ // The reply is part of the work item on purpose: `shutdown` drains the queue, so "drained" must mean "answered".
91
87
  async handleBuildRoute(msg: BuilderReq): Promise<void> {
92
88
  await this.#enqueueWork(`build-route:${msg.routeId}`, async () =>
93
89
  BuilderChannel.send(await this.#handleBuildRoute(msg)),
@@ -123,7 +119,7 @@ class IncrementalBuilder {
123
119
  } as BuildRouteResultPayload,
124
120
  };
125
121
  } catch (err) {
126
- const errMsg = err instanceof Error ? err.message : String(err);
122
+ const errMsg = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
127
123
  this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
128
124
  this.#sendBuildStatus("route", { generation: msg.generation, ok: false, files: msg.seeds, message: errMsg });
129
125
  return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
@@ -160,16 +156,11 @@ class IncrementalBuilder {
160
156
  }
161
157
  }
162
158
 
163
- /** No queued work and no debounced css rebuild, so nothing is lost if the process exits now. */
164
159
  get #idle(): boolean {
165
160
  return this.#inFlight === 0 && this.#cssRebuildTimer === null;
166
161
  }
167
162
 
168
- /**
169
- * Reported only when the builder is idle. The host's only lever against the bundler arenas
170
- * `Bun.build` retains is to recycle this process, and a recycle decided while work is queued would
171
- * either truncate that work or race the shutdown drain — so a busy builder simply says nothing.
172
- */
163
+ // Idle only: the host recycles on these metrics, and a recycle decided mid-work would truncate it or race the drain.
173
164
  #reportMetrics(): void {
174
165
  if (!this.#idle || this.#shuttingDown) return;
175
166
  BuilderChannel.emit({
@@ -178,34 +169,24 @@ class IncrementalBuilder {
178
169
  });
179
170
  }
180
171
 
181
- /**
182
- * Finish queued work, then exit so the OS reclaims the bundler arenas. The host restarts a
183
- * replacement; build requests that arrive during the drain are refused with the same retry error
184
- * the backend already handles for any other builder restart.
185
- */
172
+ // Exits so the OS reclaims the bundler arenas; the host spawns the replacement.
186
173
  async shutdown(reason: string): Promise<void> {
187
174
  if (this.#shuttingDown) return;
188
175
  this.#shuttingDown = true;
189
176
  const started = Date.now();
190
177
  this.#logger.debug(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
191
- // Stopped before the drain, or a save landing mid-drain would enqueue a batch behind the queue tail
192
- // this method already awaited — and `process.exit(0)` would cut that batch off partway through
193
- // writing its artifacts. The replacement rebuilds every artifact from its boot build anyway, so the
194
- // batch is not lost; a half-written one would be.
178
+ // Before the drain: a save landing mid-drain would queue behind the awaited tail and `process.exit` would cut
179
+ // its artifacts off half-written; the replacement's boot build redoes that work anyway.
195
180
  this.#watcher?.stop();
196
181
  if (this.#cssRebuildTimer) {
197
- // Only reachable if a css batch landed between the idle report and this request: the fresh
198
- // boot build recompiles css from scratch anyway, so dropping the debounce loses nothing.
182
+ // Only if css landed after the idle report; the replacement's boot build recompiles css, so nothing is lost.
199
183
  clearTimeout(this.#cssRebuildTimer);
200
184
  this.#cssRebuildTimer = null;
201
185
  this.#pendingCssRebuild = null;
202
186
  }
203
187
  await this.#workQueue.catch(() => undefined);
204
188
  await this.#cssRebuildQueue.catch(() => undefined);
205
- // Drained queues do not mean the host has the results. The events those work items produced are the
206
- // largest messages this process sends, and `process.exit` discards an ipc write that has not
207
- // flushed — a `css-updated` relayed milliseconds before this line would be dropped with no error
208
- // anywhere, leaving the backend serving the previous bundle. See `BuilderChannel`.
189
+ // Drained queues are not delivered results: `process.exit` drops unflushed ipc writes (a relayed `css-updated`).
209
190
  const flushed = await BuilderChannel.drain();
210
191
  this.#logger.debug(
211
192
  `drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`,
@@ -216,8 +197,7 @@ class IncrementalBuilder {
216
197
  get shuttingDown(): boolean {
217
198
  return this.#shuttingDown;
218
199
  }
219
- //* Watch events name the file's real path, so synced lib pages are matched by `realDir` while their
220
- //* page key still carries the app-relative `(libs)/(<lib>)` prefix.
200
+ //* Watch events carry real paths: synced lib pages match by `realDir`, their keys keep the `(libs)/(<lib>)` prefix.
221
201
  static #matchPageRoot(roots: PageRoot[], abs: string): PageRoot | null {
222
202
  for (const root of roots) {
223
203
  const absRoot = path.resolve(root.realDir);
@@ -245,7 +225,6 @@ class IncrementalBuilder {
245
225
  }
246
226
  return false;
247
227
  }
248
- /** Debounced css-only rebuild; the compile itself runs in a disposable worker like every other build. */
249
228
  scheduleCssRebuild({ generation, changedFiles }: { generation?: number; changedFiles?: string[] }) {
250
229
  this.#pendingCssRebuild = { generation, changedFiles };
251
230
  if (this.#cssRebuildTimer) clearTimeout(this.#cssRebuildTimer);
@@ -280,13 +259,12 @@ class IncrementalBuilder {
280
259
  }, 150);
281
260
  }
282
261
  async installWatcher() {
283
- const artifactDir = this.#artifactDir;
284
262
  const roots = await new WatchRootResolver(this.#app).resolve();
285
263
  const watcher = new HmrWatcher({
286
264
  roots,
287
265
  logger: this.#logger,
288
266
  onBatch: async (batch: ChangeBatch) => {
289
- await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(artifactDir, batch));
267
+ await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(batch));
290
268
  },
291
269
  });
292
270
  await watcher.start();
@@ -294,13 +272,11 @@ class IncrementalBuilder {
294
272
  this.#logger.verbose(`watching ${roots.length} roots`);
295
273
  }
296
274
 
297
- async #handleWatchBatch(artifactDir: string, batch: ChangeBatch) {
275
+ async #handleWatchBatch(batch: ChangeBatch) {
298
276
  const rawKinds = new Set(batch.kinds);
299
277
  if (rawKinds.size === 0) return;
300
278
  const generation = ++this.#generation;
301
- //* Insert framework imports that are used but omitted (e.g. `Int` in *.constant.ts, `fetch` in
302
- //* *.store.ts) before regenerating barrels. Edits land on files already in this batch, so they
303
- //* rebuild in this same generation; the write is idempotent so it does not re-trigger the watcher.
279
+ //* Auto-import edits touch only this batch's files, so they rebuild in this same generation.
304
280
  const [autoImport, indexSync] = await CodegenLock.run(
305
281
  this.#app.workspace.workspaceRoot,
306
282
  `hmr-batch:${this.#app.name}`,
@@ -312,9 +288,7 @@ class IncrementalBuilder {
312
288
  for (const error of autoImport.errors) this.#logger.error(error);
313
289
  if (autoImport.changedFiles.length > 0)
314
290
  this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
315
- //* Both passes above write source files, and this generation's build consumes what they wrote. Hand
316
- //* them to the watcher so its verification scan does not read them back as a user edit and spend a
317
- //* second generation rebuilding identical content.
291
+ //* Absorbed so the watcher's verification scan does not replay these writes as a user edit next generation.
318
292
  await this.#watcher?.absorb([...autoImport.changedFiles, ...indexSync.changedFiles]);
319
293
  const { files, kinds, expandedBatch, devPlan, event, hasSyncErrors } = prepareDevWatchBatch({
320
294
  generation,
@@ -343,8 +317,7 @@ class IncrementalBuilder {
343
317
  }
344
318
  if (indexSync.changedFiles.length > 0) this.#sendBuildStatus("barrel", { generation, ok: true, files });
345
319
 
346
- // Server-only generations (e.g. a .service.ts or srvkit edit) must not rebuild or refresh the
347
- // client: a fresh pages buildId would broadcast rsc-refresh to browsers for no visible change.
320
+ // Server-only generations skip the client: a fresh pages buildId would rsc-refresh browsers for no visible change.
348
321
  const rebuildClient = devPlan.actions.includes("rebuild-client");
349
322
  if (kinds.includes("code") && !rebuildClient) {
350
323
  this.#logger.verbose(`client rebuild skipped; devPlan actions=${devPlan.actions.join(",") || "(none)"}`);
@@ -361,56 +334,36 @@ class IncrementalBuilder {
361
334
 
362
335
  const needs: BuildBatchNeed[] = [];
363
336
  if (kinds.includes("code") && rebuildClient) {
364
- if (this.#shouldRebuildCsr()) needs.push("csr");
337
+ if (this.#csrActive) needs.push("csr");
365
338
  else
366
339
  this.#logger.verbose(
367
340
  `csr-rebundle skipped; request /__csr or ?csr=true (or set AKAN_DEV_CSR_REBUILD=1) to enable per-save CSR rebuilds`,
368
341
  );
369
342
  needs.push("pages");
370
- // Server-only code edits cannot introduce class names the CSS scanner would pick up; only a
371
- // client rebuild or a direct stylesheet edit can change the compiled CSS. Folded into this
372
- // generation's batch rather than debounced separately: the work queue already serializes
373
- // generations, so by the time a second batch runs the 150ms debounce would have fired anyway,
374
- // and a second worker spawn per save costs more than the coalescing ever saved.
343
+ // In this batch rather than debounced: the queue already serializes generations, and a second worker per save
344
+ // costs more than coalescing would save.
375
345
  needs.push("css");
376
346
  }
377
347
 
378
348
  BuilderChannel.emit(event);
379
349
 
380
350
  if (needs.length > 0) await this.#runBatch({ generation, needs, changedFiles: files });
381
- // A css-only batch keeps its debounce: those arrive in bursts while a stylesheet is edited, and
382
- // without a pages build in front of them there is nothing else to space them out.
351
+ // Css-only batches keep the debounce: they arrive in bursts while a stylesheet is edited.
383
352
  else if (kinds.includes("css")) {
384
353
  this.scheduleCssRebuild({ generation, changedFiles: files });
385
354
  this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
386
355
  }
387
356
  }
388
357
 
389
- /**
390
- * Run one generation of build work in a process that exits afterwards. The worker streams the
391
- * messages the backend and the HMR overlay consume, which this relays untouched, so the sequence a
392
- * browser observes is the same one the in-process build produced.
393
- */
394
- async #runBatch({
395
- generation,
396
- needs,
397
- changedFiles,
398
- }: {
399
- generation: number;
400
- needs: BuildBatchNeed[];
401
- changedFiles: string[];
402
- }): Promise<BuildBatchResult> {
358
+ async #runBatch(work: BatchWork): Promise<BuildBatchResult> {
359
+ const { generation, needs, changedFiles } = work;
403
360
  const started = Date.now();
404
- const result = await this.#batchRunner.run(await this.#batchRequest({ generation, needs, changedFiles }), (msg) =>
405
- BuilderChannel.emit(msg),
406
- );
361
+ const result = await this.#batchRunner.run(await this.#batchRequest(work), (msg) => BuilderChannel.emit(msg));
407
362
  if (result.optimizedFonts) this.#optimizedFonts = result.optimizedFonts;
408
363
  if (result.cssAssets) this.#artifact = { ...this.#artifact, cssAssets: result.cssAssets };
409
- // A worker that died before reporting streamed no build-status of its own, so report one per need
410
- // it was given: the generation must go red rather than look like it silently succeeded.
364
+ // A crashed worker streamed no build-status, so each need goes red here instead of looking silently successful.
411
365
  if (result.crashed) {
412
- // `base` is excluded because it is not a `BuildPhase`: a boot build has no phase board to fail, and
413
- // it never travels through here — `#buildBootDeps` runs it and throws into the degraded-boot path.
366
+ // `base` is not a `BuildPhase` and never comes through here: `#buildBootDeps` throws into the degraded boot.
414
367
  for (const need of needs)
415
368
  if (need !== "base")
416
369
  this.#sendBuildStatus(need, { generation, ok: false, files: changedFiles, message: result.errors[need] });
@@ -419,15 +372,7 @@ class IncrementalBuilder {
419
372
  return result;
420
373
  }
421
374
 
422
- async #batchRequest({
423
- generation,
424
- needs,
425
- changedFiles,
426
- }: {
427
- generation: number;
428
- needs: BuildBatchNeed[];
429
- changedFiles: string[];
430
- }): Promise<BuildBatchRequest> {
375
+ async #batchRequest({ generation, needs, changedFiles }: BatchWork): Promise<BuildBatchRequest> {
431
376
  return {
432
377
  appName: this.#app.name,
433
378
  workspaceRoot: this.#app.workspace.workspaceRoot,
@@ -448,10 +393,7 @@ class IncrementalBuilder {
448
393
  this.#logger.verbose(`ready (watch=${this.#watch})`);
449
394
  }
450
395
 
451
- /**
452
- * After a degraded boot recovers, the backend is still serving the last-good bundle; push a
453
- * fresh pages/css state so connected browsers pick up the fixed code without another edit.
454
- */
396
+ // The backend still serves the last-good bundle after a degraded boot; push fresh pages/css without another edit.
455
397
  async announceRecoveredState(changedFiles: string[]): Promise<void> {
456
398
  const generation = ++this.#generation;
457
399
  await this.#enqueueWork("boot-recovered", async () => {
@@ -459,21 +401,12 @@ class IncrementalBuilder {
459
401
  });
460
402
  }
461
403
 
462
- /**
463
- * Re-announce the state this builder just booted with, after the host recycled the previous one.
464
- *
465
- * The backend reads `base-artifact.json` once at boot and never re-reads it, so a pages or css hash
466
- * that moved while the builder was being replaced would otherwise leave it pointing at the previous
467
- * artifact until the next save. Announced from the boot artifact rather than by rebuilding: the
468
- * bundles are already on disk, and spending another ~200MB of bundler arena in a process that was
469
- * just recycled to reclaim memory would defeat the purpose. The host suppresses the announcement
470
- * when the hashes match, which is the common case, so a clean recycle never reloads a browser.
471
- */
404
+ // The backend reads base-artifact.json once, at its boot. Re-announced from disk rather than rebuilt (this process
405
+ // was recycled to free bundler memory); the host drops it when hashes match, so a clean recycle reloads nothing.
472
406
  async announceBootState(): Promise<void> {
473
407
  const generation = ++this.#generation;
474
408
  const reason = "builder-recycle" as const;
475
- // Awaited rather than emitted: this runs during a recycle, so the host may ask this builder to shut
476
- // down at any moment, and "announced boot state" must mean the announcement left the process.
409
+ // Awaited, not emitted: the host may shut this builder down any moment, so "announced" must mean flushed.
477
410
  await BuilderChannel.send({
478
411
  type: "pages-updated",
479
412
  data: {
@@ -500,16 +433,11 @@ class IncrementalBuilder {
500
433
  this.#logger.verbose(`announced boot state after recycle generation=${generation}`);
501
434
  }
502
435
 
503
- /**
504
- * Build the dev CSR artifact because a request asked for it, and keep it in sync from now on. The
505
- * dev server only serves CSR through the opt-in `/__csr` and `?csr=true` routes — mobile local dev
506
- * points a device WebView at the latter — so nothing needs the artifact until one of them is hit.
507
- */
436
+ // On demand: dev serves CSR only via the opt-in `/__csr` and `?csr=true` routes; once built, it rebuilds every save.
508
437
  async handleBuildCsr(msg: BuilderCsrReq): Promise<void> {
509
438
  await this.#enqueueWork("build-csr", async (): Promise<void> => {
510
439
  const started = Date.now();
511
- // Messages are not relayed: an on-demand CSR build is a request/response, and the phase board
512
- // never carried a csr status for it before. The error travels in the response below.
440
+ // Not relayed: an on-demand CSR build is request/response, and its error travels in the response.
513
441
  const result = await this.#batchRunner.run(
514
442
  await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }),
515
443
  );
@@ -525,26 +453,11 @@ class IncrementalBuilder {
525
453
  });
526
454
  }
527
455
 
528
- #shouldRebuildCsr() {
529
- return this.#csrActive;
530
- }
531
-
532
456
  static #csrArmedByEnv() {
533
457
  return process.env.AKAN_DEV_CSR_REBUILD === "1";
534
458
  }
535
459
 
536
- /**
537
- * Build the boot artifact in a process that exits afterwards, and keep only the serializable result.
538
- *
539
- * This runs in a worker for the same reason every other build does, and it was the largest single
540
- * holdout: measured on `apps/akan`, `SsrBaseArtifactBuilder.build()` retains **+1143 MB** that
541
- * `Bun.gc(true)` cannot touch, which is 65 % of the builder's post-boot RSS. Nothing was lost by
542
- * moving it — the builder only ever kept `artifact` and `optimizedFonts`, both plain data, and the
543
- * artifact is written to `base-artifact.json` regardless.
544
- *
545
- * `GraphClientEntryDiscovery.create` stays here because route builds need it live, and it costs
546
- * nothing to keep: measured at **0 ms and 0 MB**, because it builds its graph lazily on first use.
547
- */
460
+ // Discovery stays in-process because route builds need it live; it is free at boot, building its graph lazily.
548
461
  static async #buildBootDeps(app: App, runner: BuildBatchRunner): Promise<IncrementalBuilderBootDeps> {
549
462
  const result = await runner.run({
550
463
  appName: app.name,
@@ -553,15 +466,12 @@ class IncrementalBuilder {
553
466
  generation: 0,
554
467
  needs: ["base"],
555
468
  changedFiles: [],
556
- // Discovered by the worker: at boot the watcher has no validated keys to seed, and the boot build
557
- // globs them itself anyway.
558
469
  pageKeys: null,
559
470
  optimizedFonts: null,
560
471
  cssAssets: null,
561
472
  artifactDir: path.resolve(`${app.cwdPath}/.akan/artifact`),
562
473
  });
563
- // A failed boot build has to throw, not degrade quietly: `main` catches this to enter the degraded
564
- // watch mode that keeps the dev server alive until the error is fixed.
474
+ // Throws rather than degrading quietly: `main` catches it to enter degraded watch mode.
565
475
  if (result.errors.base) throw new Error(result.errors.base);
566
476
  if (!result.artifact || !result.optimizedFonts)
567
477
  throw new Error("boot build reported success without an artifact; the build worker likely died");
@@ -569,13 +479,7 @@ class IncrementalBuilder {
569
479
  return { artifact: result.artifact, optimizedFonts: result.optimizedFonts, discovery };
570
480
  }
571
481
 
572
- /**
573
- * A session that already armed dev CSR keeps it armed across builder restarts through the env flag,
574
- * so the artifact has to be rebuilt for the replacement. Queued after `builder-ready` and run in a
575
- * disposable worker rather than inline: a full minified browser-target build of every page costs
576
- * ~350MB of bundler arena that an inline build would never give back, and nothing serves CSR until a
577
- * `/__csr` or `?csr=true` request arrives anyway.
578
- */
482
+ // The env flag carries an armed CSR session across builder restarts, so the replacement rebuilds it after ready.
579
483
  async rearmCsrFromEnv(): Promise<void> {
580
484
  if (!IncrementalBuilder.#csrArmedByEnv()) return;
581
485
  this.#csrActive = true;
@@ -588,12 +492,8 @@ class IncrementalBuilder {
588
492
  });
589
493
  }
590
494
 
591
- /**
592
- * A compile error in the boot build must not kill the builder: the builder is the dev server's
593
- * file watcher, so exiting here leaves nothing to notice the fix. Report the failure, emit
594
- * builder-ready so the host keeps the backend serving the last-good artifact, then retry the
595
- * boot build on every file change until it succeeds.
596
- */
495
+ // A boot compile error must not kill the builder, the dev server's only file watcher: report it, emit builder-ready
496
+ // so the backend keeps the last-good artifact, and retry the boot build on every change.
597
497
  static #recoverBoot(
598
498
  app: App,
599
499
  bootError: unknown,
@@ -649,8 +549,7 @@ class IncrementalBuilder {
649
549
  const app = AppExecutor.from(workspace, appName);
650
550
  const watch = process.env.AKAN_WATCH !== "0";
651
551
  let builder: IncrementalBuilder | null = null;
652
- // Registered before the boot build so backend requests get an error response (instead of hanging
653
- // the backend) while the builder is still booting or recovering from a failed build.
552
+ // Registered before the boot build so a request during boot or recovery gets an error instead of hanging.
654
553
  const bootingError = "builder is recovering from a failed boot build; retry after the build error is fixed";
655
554
  const recyclingError = "builder is recycling to release bundler memory; retry after it restarts";
656
555
  process.on("message", (msg: BuilderMessage) => {
@@ -663,34 +562,21 @@ class IncrementalBuilder {
663
562
  void builder.shutdown(msg.reason);
664
563
  return;
665
564
  }
666
- if (msg.type === "build-route") {
565
+ if (msg.type !== "build-route" && msg.type !== "build-csr") return;
566
+ if (!builder || builder.shuttingDown) {
667
567
  const error = builder?.shuttingDown ? recyclingError : bootingError;
668
- if (!builder || builder.shuttingDown) {
669
- BuilderChannel.emit({ type: "build-route-res", id: msg.id, ok: false, error });
670
- return;
671
- }
672
- void builder.handleBuildRoute(msg);
568
+ BuilderChannel.emit({ type: `${msg.type}-res`, id: msg.id, ok: false, error });
673
569
  return;
674
570
  }
675
- if (msg.type === "build-csr") {
676
- const error = builder?.shuttingDown ? recyclingError : bootingError;
677
- if (!builder || builder.shuttingDown) {
678
- BuilderChannel.emit({ type: "build-csr-res", id: msg.id, ok: false, error });
679
- return;
680
- }
681
- void builder.handleBuildCsr(msg);
682
- }
571
+ if (msg.type === "build-route") void builder.handleBuildRoute(msg);
572
+ else void builder.handleBuildCsr(msg);
683
573
  });
684
- // The IPC channel closes when the dev host dies (including SIGKILL); exit instead of running
685
- // as an orphaned watcher that keeps rebuilding for nobody. Nothing is drained here on purpose —
686
- // there is no longer anyone on the other end to flush to.
574
+ // Closes when the host dies (even by SIGKILL): nothing left to drain, and an orphan would rebuild for nobody.
687
575
  process.on("disconnect", () => {
688
576
  logger.warn("host IPC channel closed; exiting builder");
689
577
  process.exit(0);
690
578
  });
691
579
  let recoveredFiles: string[] | null = null;
692
- // Owned by `main` rather than the instance: the boot build has to run before an instance exists, and
693
- // a degraded boot re-runs it once per file change until it succeeds.
694
580
  const bootRunner = new BuildBatchRunner({ workspaceRoot, cwd: app.cwdPath });
695
581
  try {
696
582
  builder = new IncrementalBuilder({ app, watch, ...(await IncrementalBuilder.#buildBootDeps(app, bootRunner)) });
@@ -707,7 +593,8 @@ class IncrementalBuilder {
707
593
  }
708
594
  }
709
595
 
710
- void IncrementalBuilder.main().catch((err) => {
596
+ // Awaited, not voided: on Windows Bun exits during a missing file's `Bun.file` read unless an entry await is pending.
597
+ await IncrementalBuilder.main().catch((err) => {
711
598
  console.error(err);
712
599
  process.exit(1);
713
600
  });
package/index.ts CHANGED
@@ -1,17 +1,6 @@
1
- // Types only, deliberately. This barrel re-exports all 40 devkit facets, so a single *value* import
2
- // from it loads every one of them — measured at 417MB / 654ms, versus 38MB / 38ms for the one subpath
3
- // the caller actually needed. That is how `commandDecorators/command.ts` silently put ink, typescript,
4
- // ssh2, @trapezedev/project and the cloud stack into every process that registered a CLI
5
- // command, and it is the single largest regression this package can suffer from a one-line edit.
6
- //
7
- // `export type *` erases at runtime (verified on Bun 1.3.14: the re-exported module is never loaded),
8
- // so `import type { App } from "@akanjs/devkit"` stays free and readable while a value import fails to
9
- // compile with a self-explaining error:
10
- //
11
- // TS1362: 'FileSys' cannot be used as a value because it was exported using 'export type'.
12
- //
13
- // Import values from the owning facet instead: `import { FileSys } from "@akanjs/devkit/fileSys"`.
14
- // Keep this file type-only; do not "fix" a TS1362 by widening an entry back to `export *`.
1
+ // XXX: types only. A value import from this barrel loads every facet (ink, typescript, ssh2, …) into the process;
2
+ // `export type *` erases at runtime. Import values from the owning facet (`@akanjs/devkit/fileSys`) and never "fix" a
3
+ // TS1362 by widening an entry back to `export *`.
15
4
  export type * from "./akanApp";
16
5
  export type * from "./akanConfig";
17
6
  export type * from "./akanContext";
@@ -30,7 +19,6 @@ export type * from "./dependencyScanner";
30
19
  export type * from "./executors";
31
20
  export type * from "./fileSys";
32
21
  export type * from "./frontendBuild";
33
- export type * from "./getDirname";
34
22
  export type * from "./getRelatedCnsts";
35
23
  export type * from "./incrementalBuilder";
36
24
  export type * from "./mobile";