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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.ko.md +2 -2
  2. package/README.md +3 -2
  3. package/abstractDoc.test.ts +6 -15
  4. package/abstractDoc.ts +6 -8
  5. package/agentsIndex.test.ts +4 -7
  6. package/agentsIndex.ts +6 -22
  7. package/akanApp/BackendImportGraph.test.ts +8 -22
  8. package/akanApp/BackendImportGraph.ts +1 -20
  9. package/akanApp/akanApp.host.test.ts +1 -3
  10. package/akanApp/akanApp.host.ts +82 -254
  11. package/akanApp/devHostPolicy.test.ts +10 -34
  12. package/akanApp/devHostPolicy.ts +12 -98
  13. package/akanConfig/akanConfig.test.ts +142 -106
  14. package/akanConfig/akanConfig.ts +147 -191
  15. package/akanConfig/types.ts +4 -3
  16. package/akanContext.ts +110 -160
  17. package/akanMcpContract.ts +28 -73
  18. package/appSelectionMemory.ts +1 -4
  19. package/applicationBuildReporter.ts +37 -5
  20. package/applicationBuildRunner.test.ts +5 -3
  21. package/applicationBuildRunner.ts +54 -61
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +32 -30
  24. package/artifact/implicitRootLayout.ts +58 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +32 -1
  28. package/biomeBase.ts +2 -5
  29. package/cloud/cloudApi.test.ts +202 -0
  30. package/cloud/cloudApi.ts +77 -39
  31. package/cloud/configLock.test.ts +64 -0
  32. package/cloud/configLock.ts +96 -0
  33. package/cloud/constants.ts +26 -5
  34. package/cloud/globalConfig.ts +70 -32
  35. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  36. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  37. package/codeAgent/agent/CodeAgent.ts +551 -0
  38. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  39. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  40. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  41. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  42. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  43. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  44. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  45. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  46. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  47. package/codeAgent/agent/CodeMailbox.ts +148 -0
  48. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  49. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  50. package/codeAgent/agent/SubagentPool.ts +214 -0
  51. package/codeAgent/agent/akanCodeModel.ts +47 -0
  52. package/codeAgent/agent/akanCodePaths.ts +31 -0
  53. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  54. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  55. package/codeAgent/agent/codeAgent.test.ts +178 -0
  56. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  57. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  58. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  59. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  60. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  61. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  62. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  63. package/codeAgent/agent/consoleToStderr.ts +8 -0
  64. package/codeAgent/agent/jsonLines.ts +29 -0
  65. package/codeAgent/agent/stringArg.ts +5 -0
  66. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  67. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  68. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  69. package/codeAgent/feedback/PreviewView.ts +95 -0
  70. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  71. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  72. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  73. package/codeAgent/index.ts +34 -0
  74. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  75. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  76. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  77. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  78. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  79. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  80. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  81. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  82. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  83. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  84. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  85. package/codeAgent/tools/AkanCli.ts +50 -0
  86. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  87. package/codeAgent/tools/AkanEditScope.ts +48 -0
  88. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  89. package/codeAgent/tools/AkanToolPack.ts +92 -0
  90. package/codeAgent/tools/AkanVerifier.ts +48 -0
  91. package/codeAgent/tools/AskToolPack.ts +99 -0
  92. package/codeAgent/tools/MailToolPack.ts +76 -0
  93. package/codeAgent/tools/McpClient.ts +187 -0
  94. package/codeAgent/tools/McpOAuth.ts +233 -0
  95. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  96. package/codeAgent/tools/McpServerConfig.ts +129 -0
  97. package/codeAgent/tools/McpSignIn.ts +158 -0
  98. package/codeAgent/tools/McpTokenStore.ts +81 -0
  99. package/codeAgent/tools/McpToolPack.ts +199 -0
  100. package/codeAgent/tools/SessionToolPack.ts +91 -0
  101. package/codeAgent/tools/WebToolPack.ts +147 -0
  102. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  103. package/codeAgent/tools/askToolPack.test.ts +116 -0
  104. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  105. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  106. package/codegenLock.test.ts +4 -13
  107. package/codegenLock.ts +3 -17
  108. package/commandDecorators/argMeta.ts +1 -5
  109. package/commandDecorators/camelToKebabCase.ts +1 -0
  110. package/commandDecorators/command.ts +46 -102
  111. package/commandDecorators/commandBuilder.ts +30 -61
  112. package/commandDecorators/commandDecorators.test.ts +2 -10
  113. package/commandDecorators/dependencyBuilder.ts +10 -1
  114. package/commandDecorators/helpFormatter.ts +68 -145
  115. package/commandDecorators/index.ts +0 -2
  116. package/commandDecorators/targetMeta.ts +9 -7
  117. package/createTunnel.ts +7 -14
  118. package/dependencyScanner.test.ts +4 -16
  119. package/dependencyScanner.ts +31 -80
  120. package/devkitUtils.test.ts +48 -20
  121. package/executors.linkFallback.test.ts +103 -0
  122. package/executors.test.ts +132 -106
  123. package/executors.ts +195 -496
  124. package/fileEditor.ts +9 -37
  125. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  126. package/fileSys.ts +6 -8
  127. package/formSetterScanner.ts +3 -16
  128. package/frontendBuild/allRoutesBuilder.ts +7 -21
  129. package/frontendBuild/autoImportSync.test.ts +27 -38
  130. package/frontendBuild/autoImportSync.ts +31 -65
  131. package/frontendBuild/buildRouteClient.test.ts +62 -37
  132. package/frontendBuild/bundleDefine.ts +12 -0
  133. package/frontendBuild/clientBuildTypes.ts +5 -36
  134. package/frontendBuild/clientEntriesBundler.ts +9 -40
  135. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  136. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  137. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  138. package/frontendBuild/cssCandidateCache.ts +6 -41
  139. package/frontendBuild/cssCompiler.ts +29 -78
  140. package/frontendBuild/cssImportResolver.ts +6 -19
  141. package/frontendBuild/devChangePlanner.ts +7 -10
  142. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  143. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  144. package/frontendBuild/fontOptimizer.test.ts +6 -14
  145. package/frontendBuild/fontOptimizer.ts +15 -72
  146. package/frontendBuild/fontPruner.test.ts +5 -10
  147. package/frontendBuild/fontPruner.ts +5 -20
  148. package/frontendBuild/frontendBuild.test.ts +46 -69
  149. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  150. package/frontendBuild/hmrWatcher.test.ts +8 -32
  151. package/frontendBuild/hmrWatcher.ts +14 -71
  152. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  153. package/frontendBuild/precompressArtifacts.ts +5 -18
  154. package/frontendBuild/routeClientBuilder.ts +23 -46
  155. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  156. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  157. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  158. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  159. package/frontendBuild/styleContract.ts +1 -8
  160. package/frontendBuild/themeValidator.ts +4 -26
  161. package/frontendBuild/watchRootResolver.test.ts +23 -42
  162. package/frontendBuild/watchRootResolver.ts +5 -27
  163. package/getRelatedCnsts.ts +12 -92
  164. package/incrementalBuilder/buildBatch.proc.ts +29 -58
  165. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  166. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  167. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  168. package/incrementalBuilder/builderChannel.test.ts +24 -49
  169. package/incrementalBuilder/builderChannel.ts +6 -37
  170. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  171. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  172. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  173. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  174. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  175. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  176. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  177. package/index.ts +5 -19
  178. package/integration/devResourceProbe.ts +50 -56
  179. package/integration/devStability.integration.test.ts +47 -194
  180. package/integration/devStabilityHarness.test.ts +13 -28
  181. package/integration/devStabilityHarness.ts +65 -330
  182. package/integration/ssrMemoryProbe.ts +41 -167
  183. package/libSource.test.ts +4 -15
  184. package/libSource.ts +19 -31
  185. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  186. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  187. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  188. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  189. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  190. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  191. package/lint/gritRules.test.ts +8 -24
  192. package/lint/no-deep-internal-import.grit +2 -2
  193. package/lint/no-document-cookie.grit +29 -0
  194. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  195. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  196. package/lint/no-web-storage.grit +30 -0
  197. package/linter.test.ts +1 -2
  198. package/linter.ts +26 -116
  199. package/mobile/index.ts +6 -0
  200. package/mobile/mobileTarget.ts +16 -13
  201. package/mobile/nativeApi.test.ts +30 -0
  202. package/mobile/nativeApi.ts +40 -0
  203. package/mobile/nativeApp.test.ts +85 -0
  204. package/mobile/nativeApp.ts +210 -0
  205. package/mobile/nativeConfig.test.ts +199 -0
  206. package/mobile/nativeConfig.ts +184 -0
  207. package/mobile/nativeWebDir.test.ts +80 -0
  208. package/mobile/nativeWebDir.ts +62 -0
  209. package/mobile/placeholderAppId.test.ts +13 -0
  210. package/mobile/placeholderAppId.ts +16 -0
  211. package/mobile/usageDescriptions.test.ts +27 -0
  212. package/mobile/usageDescriptions.ts +24 -0
  213. package/package.json +10 -10
  214. package/packageExports.test.ts +3 -18
  215. package/packageExportsMap.ts +3 -19
  216. package/piDependencyPins.test.ts +46 -0
  217. package/platformTest/BunTestReport.test.ts +60 -0
  218. package/platformTest/BunTestReport.ts +65 -0
  219. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  220. package/platformTest/IdleWatchedProcess.ts +73 -0
  221. package/platformTest/LinuxDockerTarget.ts +105 -0
  222. package/platformTest/PlatformTestRun.test.ts +64 -0
  223. package/platformTest/PlatformTestRun.ts +141 -0
  224. package/platformTest/PlatformTestTarget.ts +142 -0
  225. package/platformTest/TestSnapshot.test.ts +72 -0
  226. package/platformTest/TestSnapshot.ts +103 -0
  227. package/platformTest/WindowsSshTarget.ts +151 -0
  228. package/prompter.ts +3 -13
  229. package/qualityScanner.test.ts +9 -26
  230. package/qualityScanner.ts +200 -286
  231. package/recipeScanner.test.ts +7 -12
  232. package/recipeScanner.ts +9 -38
  233. package/repoIdentity.ts +2 -9
  234. package/routeSourceValidator.test.ts +0 -1
  235. package/routeSourceValidator.ts +7 -42
  236. package/scanInfo.ts +91 -225
  237. package/selectModel.ts +1 -2
  238. package/semver.ts +3 -5
  239. package/slicePlanner.test.ts +6 -24
  240. package/slicePlanner.ts +11 -32
  241. package/spinner.test.ts +3 -8
  242. package/spinner.ts +4 -16
  243. package/sshTunnel.ts +1 -6
  244. package/ssrScanner.test.ts +1 -6
  245. package/ssrScanner.ts +103 -113
  246. package/stripAnsi.ts +3 -0
  247. package/subspace.test.ts +30 -25
  248. package/subspace.ts +81 -115
  249. package/subspaceConfig.ts +11 -9
  250. package/testHelpers.ts +194 -0
  251. package/transforms/asyncDefaultExportDetector.ts +3 -11
  252. package/transforms/barrelAnalyzer.ts +43 -135
  253. package/transforms/barrelImportsPlugin.ts +38 -175
  254. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  255. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  256. package/transforms/moduleSyntax.ts +32 -0
  257. package/transforms/rscUseClientTransform.ts +35 -20
  258. package/transforms/transforms.test.ts +153 -44
  259. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  260. package/transforms/useClientBundlePlugin.ts +2 -22
  261. package/typeChecker.ts +7 -63
  262. package/types.ts +1 -0
  263. package/uploadRelease.ts +10 -25
  264. package/workflow/artifacts.ts +18 -0
  265. package/workflow/executor.test.ts +1 -12
  266. package/workflow/executor.ts +101 -110
  267. package/workflow/index.ts +0 -1
  268. package/workflow/moduleIndex.test.ts +4 -15
  269. package/workflow/moduleIndex.ts +9 -49
  270. package/workflow/plan.ts +18 -25
  271. package/workflow/render.ts +80 -96
  272. package/workflow/rolloutGate.test.ts +1 -1
  273. package/workflow/source.ts +52 -69
  274. package/workflow/sourceAst.ts +50 -0
  275. package/workspaceLayout.test.ts +13 -3
  276. package/workspaceLayout.ts +20 -23
  277. package/abstractCompactor.ts +0 -112
  278. package/aiEditor.test.ts +0 -60
  279. package/aiEditor.ts +0 -387
  280. package/applicationReleasePackager.ts +0 -209
  281. package/capacitorApp.test.ts +0 -372
  282. package/capacitorApp.ts +0 -1648
  283. package/commandDecorators/commandMeta.ts +0 -7
  284. package/commandDecorators/types.ts +0 -10
  285. package/getDirname.ts +0 -4
  286. package/guideline.ts +0 -19
  287. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  288. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  289. package/workflow/primitive.ts +0 -59
@@ -12,24 +12,12 @@ export interface WatcherOptions {
12
12
  debounceMs?: number;
13
13
  logger: Logger;
14
14
  onBatch: (batch: ChangeBatch) => void | Promise<void>;
15
- /**
16
- * Delay before re-checking mtimes for changes `fs.watch` never reported. Must clear Bun's ~200ms
17
- * coalescing window, or a write late in the same window as a delivered event stays invisible.
18
- */
15
+ /** Delay (ms, default 250) before the mtime re-check; must outlast Bun's ~200ms fs.watch coalescing window. */
19
16
  verifyDelayMs?: number;
20
17
  }
21
18
 
22
- /**
23
- * Recursive filesystem watcher with debounced batching. We deliberately keep
24
- * classification coarse (`code` / `css` / `config`) so the orchestrator can
25
- * decide whether a full rebuild-and-reload or a narrower action (e.g. CSS
26
- * hot-swap) is sufficient.
27
- *
28
- * Bun's event payloads are treated as a hint, not the answer: its recursive `fs.watch` reports about one
29
- * path per coalescing window and silently discards the rest, so what changed is resolved against a
30
- * `SourceMtimeIndex` instead (see that class for the measurements). Events still drive *when* to look,
31
- * which is what keeps this cheap — Bun does reliably deliver at least one event per window.
32
- */
19
+ // Bun delivers at least one fs.watch event per coalescing window but drops the other paths, so events decide
20
+ // when to look and the SourceMtimeIndex decides what changed.
33
21
  export class HmrWatcher {
34
22
  readonly #roots: string[];
35
23
  readonly #debounceMs: number;
@@ -38,7 +26,6 @@ export class HmrWatcher {
38
26
  readonly #logger: Logger;
39
27
  readonly #watchers: fs.FSWatcher[] = [];
40
28
  readonly #pending = new Map<string, Exclude<ChangeKind, "ignore">>();
41
- /** Paths the watcher's own events named since the last batch — diagnostics only, see `#queue`. */
42
29
  readonly #hinted = new Set<string>();
43
30
  readonly #classifier = new HmrChangeClassifier();
44
31
  readonly #index: SourceMtimeIndex;
@@ -59,19 +46,12 @@ export class HmrWatcher {
59
46
  this.#index = new SourceMtimeIndex({ roots: this.#roots, classifier: this.#classifier });
60
47
  }
61
48
 
62
- /**
63
- * How many changes the mtime scan found that `fs.watch` never reported. Non-zero means this watcher is
64
- * compensating for the Bun defect rather than the defect being absent, which is the number to watch if
65
- * it is ever fixed upstream.
66
- */
49
+ /** Changes the mtime scan found that fs.watch never reported; non-zero means it is compensating for Bun. */
67
50
  get unreportedChanges(): number {
68
51
  return this.#unreportedChanges;
69
52
  }
70
53
 
71
- /**
72
- * Watchers are installed before the baseline is taken, so an edit made during priming is reported by
73
- * the event side even though the baseline already reflects it.
74
- */
54
+ /** Installs the watchers before priming, so an edit made while priming is still reported by the event side. */
75
55
  async start(): Promise<void> {
76
56
  for (const root of this.#roots) {
77
57
  try {
@@ -88,8 +68,7 @@ export class HmrWatcher {
88
68
  }
89
69
  try {
90
70
  await this.#index.prime();
91
- // Before the first batch, because a root that is unreadable at boot blinds the whole session and
92
- // waiting for a save to surface it means waiting for a save that never rebuilds.
71
+ // At boot: edits under an unreadable root never rebuild, so no later save would surface it.
93
72
  this.#reportCoverageGaps();
94
73
  this.#logger.verbose(`[hmr] tracking ${this.#index.trackedFileCount} source files for change verification`);
95
74
  } catch (err) {
@@ -112,23 +91,14 @@ export class HmrWatcher {
112
91
  }
113
92
  }
114
93
 
115
- /**
116
- * Adopt writes the batch handler made itself (regenerated barrels, inserted imports) so the
117
- * verification scan does not spend a second generation rebuilding content this one already consumed.
118
- */
94
+ /** Baseline the batch handler's own writes so the verification scan does not rebuild them a second time. */
119
95
  async absorb(paths: string[]): Promise<void> {
120
96
  if (paths.length === 0) return;
121
97
  await this.#index.absorb(paths);
122
98
  }
123
99
 
124
- /**
125
- * An event says only *that* something happened, never reliably *what*. So with a baseline in hand it is
126
- * used purely to decide when to look, and the mtime scan names the files.
127
- *
128
- * Taking the payload as well would double-report: Bun does deliver a real event for some of the paths a
129
- * scan has already emitted, and adding it back here produced a second batch for the same save — one more
130
- * generation and one more build for no change.
131
- */
100
+ // Once primed, a payload only schedules a scan: Bun also delivers events for paths the scan already emitted,
101
+ // so adding payloads to the batch double-reports a save.
132
102
  #queue(abs: string): void {
133
103
  const kind = this.#classifier.classify(abs);
134
104
  if (!this.#index.primed) {
@@ -138,15 +108,12 @@ export class HmrWatcher {
138
108
  this.#scheduleFlush();
139
109
  return;
140
110
  }
141
- // An ignored path still means a window happened. That is the shape of the original bug — every build
142
- // ends in a burst under `.akan/`, and the burst is what Bun reports instead of the save beside it.
143
- // Rescheduling on each one folds a build's whole burst into a single scan once it goes quiet.
111
+ // An ignored path (a build's .akan/ burst) still marks a coalescing window that may hide a real save.
144
112
  if (kind === "ignore") {
145
113
  this.#scheduleVerify();
146
114
  return;
147
115
  }
148
- // Recorded only so `unreportedChanges` can tell which changes the events did name; it never decides
149
- // what is in a batch.
116
+ // Diagnostics only, for `unreportedChanges`; it never decides what is in a batch.
150
117
  this.#hinted.add(abs);
151
118
  this.#scheduleFlush();
152
119
  }
@@ -186,10 +153,6 @@ export class HmrWatcher {
186
153
  }
187
154
  }
188
155
 
189
- /**
190
- * Fold in everything the mtime index has seen change, since a delivered event names at most one of the
191
- * paths that moved in its window.
192
- */
193
156
  async #mergeDetectedChanges(): Promise<void> {
194
157
  const detected = await this.#index.collectChanges().catch((err) => {
195
158
  this.#logger.error(`[hmr] mtime scan failed: ${(err as Error).message}`);
@@ -205,8 +168,6 @@ export class HmrWatcher {
205
168
  }
206
169
  if (unreported === 0) return;
207
170
  this.#unreportedChanges += unreported;
208
- // Once at info, so it is visible that the watcher is compensating rather than the defect being absent;
209
- // per-batch detail stays at verbose because a save-all trips this on every save.
210
171
  if (!this.#reportedCompensating) {
211
172
  this.#reportedCompensating = true;
212
173
  this.#logger.verbose(
@@ -218,13 +179,6 @@ export class HmrWatcher {
218
179
  );
219
180
  }
220
181
 
221
- /**
222
- * Say so when part of the tree cannot be read, and say so again when it recovers.
223
- *
224
- * A blind spot here means edits under that path are not rebuilt at all, which is indistinguishable from
225
- * the dev server being broken. Keyed on the gap list itself so a persistent failure logs once rather than
226
- * once per save, while a *different* gap appearing still gets its own line.
227
- */
228
182
  #reportCoverageGaps(): void {
229
183
  const gaps = this.#index.coverageGaps;
230
184
  const key = gaps
@@ -247,10 +201,7 @@ export class HmrWatcher {
247
201
  );
248
202
  }
249
203
 
250
- /**
251
- * One scan after the coalescing window closes. A write that lands in the same window as an already
252
- * delivered event produces no further event of its own, so nothing else would ever look for it.
253
- */
204
+ // A write in the same window as a delivered event raises no event of its own, so one scan follows each window.
254
205
  #scheduleVerify(): void {
255
206
  if (this.#stopped || this.#verifyDelayMs <= 0) return;
256
207
  if (this.#verifyTimer) clearTimeout(this.#verifyTimer);
@@ -260,16 +211,8 @@ export class HmrWatcher {
260
211
  }, this.#verifyDelayMs);
261
212
  }
262
213
 
263
- /**
264
- * Terminates rather than looping: a scan that finds nothing schedules nothing, and the build writes its
265
- * artifacts under `.akan/` — which the classifier ignores — while codegen writes are content-guarded,
266
- * so a rebuild does not move a tracked mtime.
267
- *
268
- * The one case that does schedule another is a directory the index could not date reliably (Linux stamps
269
- * directory mtimes on a 1ms clock, see `SourceMtimeIndex`). That still terminates: the next scan is
270
- * `verifyDelayMs` later, by which point the timestamp has settled unless something is writing to that
271
- * directory right now — in which case looking again is the right answer anyway.
272
- */
214
+ // Terminates: build output lands in ignored .akan/ and codegen writes are content-guarded, so a rebuild moves no
215
+ // tracked mtime; only an unsettled directory schedules another scan.
273
216
  async #verify(): Promise<void> {
274
217
  if (this.#stopped || this.#flushing) return;
275
218
  await this.#mergeDetectedChanges();
@@ -5,18 +5,15 @@ import { resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
5
5
  import type { App } from "../commandDecorators";
6
6
  import { createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
7
7
  import { createExternalizeFrameworkPlugin } from "../transforms/externalizeFrameworkPlugin";
8
+ import { loaderFor } from "../transforms/moduleSyntax";
8
9
  import { transformUseClient } from "../transforms/rscUseClientTransform";
9
10
  import { createUseClientBundlePlugin } from "../transforms/useClientBundlePlugin";
11
+ import { bundleDefine } from "./bundleDefine";
10
12
  import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
11
13
 
12
14
  export interface BuildPagesBundleResult {
13
- /** Absolute path to the emitted `pages-[hash].js`. */
14
15
  bundlePath: string;
15
- /**
16
- * Monotonic build identifier. Bun.build emits a fresh filename whenever
17
- * any input changes, but importers still benefit from a `?v=<buildId>`
18
- * query-string cache bust — `buildId` is that value.
19
- */
16
+ /** Cache-bust value for `import(bundlePath?v=<buildId>)`. */
20
17
  buildId: number;
21
18
  splitting: boolean;
22
19
  entryBytes: number;
@@ -27,10 +24,6 @@ export interface BuildPagesBundleResult {
27
24
 
28
25
  const VIRTUAL_PAGES_ENTRY = "akan-pages-entry";
29
26
 
30
- /**
31
- * Build the server-side pages bundle. The RSC worker loads the result with
32
- * `await import(bundlePath?v=buildId)`.
33
- */
34
27
  export class PagesBundleBuilder {
35
28
  #app: App;
36
29
  #command: "build" | "start";
@@ -61,7 +54,7 @@ export class PagesBundleBuilder {
61
54
  chunk: "chunks/[name]-[hash].[ext]",
62
55
  asset: "assets/[name]-[hash].[ext]",
63
56
  },
64
- define: this.#define(),
57
+ define: bundleDefine(this.#app, this.#command, "ssr"),
65
58
  plugins: [
66
59
  PagesBundleBuilder.createPagesEntryPlugin(entrySource),
67
60
  PagesBundleBuilder.createCssStubPlugin(),
@@ -110,17 +103,6 @@ export class PagesBundleBuilder {
110
103
  return process.env.AKAN_SERVER_PAGES_SPLITTING === "1";
111
104
  }
112
105
 
113
- #define(): Record<string, string> {
114
- const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
115
- return {
116
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
117
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
118
- ...Object.fromEntries(
119
- Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
120
- ),
121
- };
122
- }
123
-
124
106
  static createPagesEntryPlugin(source: string): BunPlugin {
125
107
  return {
126
108
  name: "akan-pages-entry",
@@ -176,10 +158,3 @@ export class PagesBundleBuilder {
176
158
  );
177
159
  }
178
160
  }
179
-
180
- function loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
181
- if (absPath.endsWith(".tsx")) return "tsx";
182
- if (absPath.endsWith(".jsx")) return "jsx";
183
- if (absPath.endsWith(".ts")) return "ts";
184
- return "js";
185
- }
@@ -2,17 +2,15 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import zlib from "node:zlib";
4
4
  import type { App } from "../commandDecorators";
5
+ import { FontPruner } from "./fontPruner";
5
6
 
6
7
  const COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
7
8
  const MIN_COMPRESS_BYTES = 1024;
8
- /**
9
- * Quality 11 is roughly 100x slower than gzip, so it is spent only where it pays: there is one CSS asset
10
- * per basePath and it is the largest single file the app ships, worth ~20% over gzip for ~0.2s. Raising the
11
- * several hundred JS chunks from 9 to 11 costs ~12s of build time to save a few hundred KB, so they stay at 9.
12
- */
9
+ // Brotli 11 is ~100x slower than gzip: worth it for the one CSS asset per basePath, not for hundreds of JS chunks.
13
10
  const BROTLI_QUALITY_BY_EXT = { ".css": 11 } as const;
14
11
  const DEFAULT_BROTLI_QUALITY = 9;
15
12
  const GZIP_LEVEL = 9;
13
+ const { formatBytes } = FontPruner;
16
14
 
17
15
  export interface PrecompressArtifactsResult {
18
16
  files: number;
@@ -22,8 +20,7 @@ export interface PrecompressArtifactsResult {
22
20
  }
23
21
 
24
22
  export async function precompressArtifacts(app: App): Promise<PrecompressArtifactsResult> {
25
- //* styles too: WebRouter serves both prefixes through the same sidecar-aware #fileResponse, and the
26
- //* one CSS asset per basePath is the largest uncompressed payload the app ships.
23
+ //* WebRouter serves client/ and styles/ through the same sidecar-aware #fileResponse.
27
24
  const roots = [
28
25
  path.join(app.dist.cwdPath, ".akan/artifact/client"),
29
26
  path.join(app.dist.cwdPath, ".akan/artifact/styles"),
@@ -47,7 +44,7 @@ async function precompressRoot(root: string, result: PrecompressArtifactsResult)
47
44
  for await (const filePath of glob.scan({ cwd: root, absolute: true })) {
48
45
  if (!(await shouldPrecompress(filePath))) continue;
49
46
  const bytes = await Bun.file(filePath).bytes();
50
- const buffer = toArrayBuffer(bytes);
47
+ const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
51
48
  const gz = Bun.gzipSync(buffer, { level: GZIP_LEVEL });
52
49
  const br = brotliCompress(buffer, path.extname(filePath).toLowerCase());
53
50
  await Promise.all([Bun.write(`${filePath}.gz`, gz), Bun.write(`${filePath}.br`, br)]);
@@ -75,13 +72,3 @@ async function shouldPrecompress(filePath: string): Promise<boolean> {
75
72
  if (!(await file.exists())) return false;
76
73
  return file.size >= MIN_COMPRESS_BYTES;
77
74
  }
78
-
79
- function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
80
- return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
81
- }
82
-
83
- function formatBytes(bytes: number): string {
84
- if (bytes < 1024) return `${bytes}B`;
85
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
86
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
87
- }
@@ -3,20 +3,12 @@ import path from "node:path";
3
3
  import type { BaseBuildArtifact, ClientManifest, SsrManifest } from "akanjs/server";
4
4
  import type { App } from "../commandDecorators";
5
5
  import { createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
6
- import { toClientReferencePath } from "../transforms/rscUseClientTransform";
6
+ import { scanUseClientExports, toClientReferencePath } from "../transforms/rscUseClientTransform";
7
7
  import type { ClientBundleTarget, ClientEntryDiscovery } from "./clientBuildTypes";
8
8
  import { ClientEntriesBundler } from "./clientEntriesBundler";
9
9
  import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
10
10
  import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
11
11
 
12
- const SSR_CLIENT_EXTERNALS = [
13
- "react",
14
- "react-dom",
15
- "react-dom/client",
16
- "react/jsx-runtime",
17
- "react/jsx-dev-runtime",
18
- "akanjs/fetch",
19
- ] as const;
20
12
  const SSR_CLIENT_ALIAS_EXTERNALS = [
21
13
  "react",
22
14
  "react-dom",
@@ -24,38 +16,28 @@ const SSR_CLIENT_ALIAS_EXTERNALS = [
24
16
  "react/jsx-runtime",
25
17
  "react/jsx-dev-runtime",
26
18
  ] as const;
19
+ const SSR_CLIENT_EXTERNALS = [...SSR_CLIENT_ALIAS_EXTERNALS, "akanjs/fetch"] as const;
27
20
 
28
21
  export interface BuildRouteClientOptions {
29
22
  app: App;
30
23
  seeds: string[];
24
+ /** Every route's seeds: a build that runs at all bundles the whole app, since two builds each copy shared modules. */
25
+ graphSeeds?: string[];
31
26
  artifact: BaseBuildArtifact;
32
27
  knownEntries?: Set<string>;
33
28
  routeId?: string;
34
29
  command?: "build" | "start";
35
30
  discovery?: ClientEntryDiscovery;
36
- /**
37
- * Client entries resolved by the caller, which skips discovery and bundles exactly this list.
38
- *
39
- * `AllRoutesBuilder` passes every route's entries at once so they share one `Bun.build`. Chunk
40
- * splitting is scoped to a single invocation, so a dependency reachable from entries spread across
41
- * several invocations is emitted once per invocation — mermaid landed in `apps/akan` four times that
42
- * way. Dev keeps one build per route and leaves this unset.
43
- */
31
+ /** Pre-resolved client entries: skips discovery and bundles exactly this list. */
44
32
  entries?: string[];
45
33
  }
46
34
 
47
35
  export interface BuildRouteClientResult {
48
- /** Newly-emitted manifest rows keyed by `${absEntry}#${exportName}`. */
49
36
  manifestDelta: ClientManifest;
50
- /** Newly-emitted ssrManifest rows keyed by entry URL. */
51
37
  ssrManifestDelta: SsrManifest;
52
- /** Absolute paths of the `"use client"` leaves this build added. */
53
38
  newEntries: string[];
54
- /** Absolute paths of all `"use client"` leaves discovered from this route's seeds. */
55
39
  discoveredEntries?: string[];
56
- /** Absolute source files included in the browser bundles for new entries. */
57
40
  clientDeps: string[];
58
- /** Absolute source files included in the browser bundle, grouped by original client entry. */
59
41
  clientDepsByEntry?: Record<string, string[]>;
60
42
  }
61
43
 
@@ -67,6 +49,7 @@ interface BootstrapEntries {
67
49
  export class RouteClientBuilder {
68
50
  #app: App;
69
51
  #seeds: string[];
52
+ #graphSeeds?: string[];
70
53
  #knownEntries: Set<string>;
71
54
  #command: "build" | "start";
72
55
  #discovery?: ClientEntryDiscovery;
@@ -75,6 +58,7 @@ export class RouteClientBuilder {
75
58
  constructor(options: BuildRouteClientOptions) {
76
59
  this.#app = options.app;
77
60
  this.#seeds = options.seeds;
61
+ this.#graphSeeds = options.graphSeeds;
78
62
  this.#knownEntries = options.knownEntries ?? new Set<string>();
79
63
  this.#command = options.command ?? "start";
80
64
  this.#discovery = options.discovery;
@@ -82,17 +66,18 @@ export class RouteClientBuilder {
82
66
  }
83
67
 
84
68
  async build(): Promise<BuildRouteClientResult> {
85
- const discovered =
86
- this.#entries ??
87
- (await (this.#discovery ?? (await GraphClientEntryDiscovery.create(this.#app))).discover(this.#seeds));
88
- const entries = discovered.filter((e) => !this.#knownEntries.has(e));
89
- if (entries.length === 0) return this.#emptyResult(discovered);
69
+ const discovered = this.#entries ?? (await (await this.#getDiscovery()).discover(this.#seeds));
70
+ if (discovered.every((e) => this.#knownEntries.has(e))) return this.#emptyResult(discovered);
71
+ const entries = this.#graphSeeds
72
+ ? [...new Set([...discovered, ...(await (await this.#getDiscovery()).discover(this.#graphSeeds))])].sort()
73
+ : discovered.filter((e) => !this.#knownEntries.has(e));
90
74
 
91
75
  const bootstrapEntries = await this.#createBootstrapEntries(entries);
92
76
  const browserBundle = await this.#buildBrowserBundle(bootstrapEntries);
93
77
  const ssrBundle = await this.#buildSsrBundle(bootstrapEntries);
94
78
 
95
79
  const acceptedEntries = new Set(entries);
80
+ const routeEntries = new Set(discovered);
96
81
  const manifestDelta: ClientManifest = {};
97
82
  const ssrModuleMap: SsrManifest["moduleMap"] = {};
98
83
  const clientDeps = new Set<string>();
@@ -122,19 +107,24 @@ export class RouteClientBuilder {
122
107
  for (const dep of browserBundle.entryDepsByAbsPath.get(buildEntry) ?? []) deps.add(path.resolve(dep));
123
108
  const sortedDeps = [...deps].sort();
124
109
  clientDepsByEntry[originalEntry] = sortedDeps;
125
- for (const dep of sortedDeps) clientDeps.add(dep);
110
+ if (routeEntries.has(originalEntry)) for (const dep of sortedDeps) clientDeps.add(dep);
126
111
  }
127
112
 
128
113
  return {
129
114
  manifestDelta,
130
115
  ssrManifestDelta: { moduleLoading: null, moduleMap: ssrModuleMap },
131
- newEntries: entries,
116
+ newEntries: entries.filter((e) => !this.#knownEntries.has(e)),
132
117
  discoveredEntries: discovered,
133
118
  clientDeps: [...clientDeps].sort(),
134
119
  clientDepsByEntry,
135
120
  };
136
121
  }
137
122
 
123
+ async #getDiscovery(): Promise<ClientEntryDiscovery> {
124
+ this.#discovery ??= await GraphClientEntryDiscovery.create(this.#app);
125
+ return this.#discovery;
126
+ }
127
+
138
128
  #emptyResult(discoveredEntries: string[] = []): BuildRouteClientResult {
139
129
  return {
140
130
  manifestDelta: {},
@@ -206,16 +196,7 @@ export class RouteClientBuilder {
206
196
  }
207
197
 
208
198
  async #scanExportNames(absEntry: string): Promise<string[]> {
209
- const source = await Bun.file(absEntry).text();
210
- const transpiler = new Bun.Transpiler({ loader: this.#loaderFor(absEntry) });
211
- return transpiler.scan(source).exports;
212
- }
213
-
214
- #loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
215
- if (absPath.endsWith(".tsx")) return "tsx";
216
- if (absPath.endsWith(".jsx")) return "jsx";
217
- if (absPath.endsWith(".ts")) return "ts";
218
- return "js";
199
+ return scanUseClientExports(await Bun.file(absEntry).text(), absEntry, this.#app.workspace.workspaceRoot);
219
200
  }
220
201
 
221
202
  static normalizeNamedDefaultFunctionForFastRefresh(source: string): string | null {
@@ -238,12 +219,8 @@ export class RouteClientBuilder {
238
219
  return { [Bun.resolveSync("akanjs/fetch", serverEntry)]: "akanjs/fetch" };
239
220
  }
240
221
 
241
- /**
242
- * `target: "bun"` is load-bearing: these chunks are `await import()`-ed by the SSR renderer, so a dependency
243
- * resolved through its `browser` export condition can touch `document` at module scope and throw mid-render,
244
- * degrading the whole document to client rendering. Bun's `conditions` only adds to the target's defaults —
245
- * `browser` still wins — so the target itself has to say server.
246
- */
222
+ // `target: "bun"` is load-bearing: a `browser` export condition can touch `document` at module scope mid-SSR, and
223
+ // Bun's `conditions` only add to the target's defaults, so the target itself must say server.
247
224
  static resolveSsrClientBundleOptions(command: "build" | "start"): {
248
225
  target: ClientBundleTarget;
249
226
  external: readonly string[];
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
- import type { RoutesManifest, SsrManifestEntry } from "akanjs/server";
1
+ import type { RoutesManifest } from "akanjs/server";
2
+ import { RoutesManifestStore } from "akanjs/server/artifact/routesManifestStore";
3
3
 
4
4
  export type SerializedRoutesManifest = Omit<RoutesManifest, "knownEntries"> &
5
5
  Partial<Pick<RoutesManifest, "knownEntries">>;
@@ -11,7 +11,7 @@ export class RoutesManifestArtifactSerializer {
11
11
 
12
12
  constructor(manifest: RoutesManifest, artifactDir: string, options: { production?: boolean } = {}) {
13
13
  this.#manifest = manifest;
14
- this.#artifactDir = path.resolve(artifactDir);
14
+ this.#artifactDir = artifactDir;
15
15
  this.#production = options.production ?? false;
16
16
  }
17
17
 
@@ -24,39 +24,8 @@ export class RoutesManifestArtifactSerializer {
24
24
  }
25
25
 
26
26
  serialize(): SerializedRoutesManifest {
27
- const serialized: SerializedRoutesManifest = {
28
- ...this.#manifest,
29
- knownEntries: this.#manifest.knownEntries.map((entry) => this.#serializeArtifactPath(entry)),
30
- };
27
+ const serialized: SerializedRoutesManifest = RoutesManifestStore.serialize(this.#manifest, this.#artifactDir);
31
28
  if (this.#production) delete serialized.knownEntries;
32
- return {
33
- ...serialized,
34
- clientManifest: Object.fromEntries(
35
- Object.entries(this.#manifest.clientManifest).map(([key, row]) => [this.#serializeArtifactPath(key), row]),
36
- ),
37
- ssrManifest: {
38
- ...this.#manifest.ssrManifest,
39
- moduleMap: Object.fromEntries(
40
- Object.entries(this.#manifest.ssrManifest.moduleMap).map(([entryUrl, byName]) => [
41
- entryUrl,
42
- Object.fromEntries(
43
- Object.entries(byName).map(([name, entry]) => [
44
- name,
45
- {
46
- ...entry,
47
- id: this.#serializeArtifactPath(entry.id),
48
- chunks: entry.chunks.map((chunk) => this.#serializeArtifactPath(chunk)),
49
- } satisfies SsrManifestEntry,
50
- ]),
51
- ),
52
- ]),
53
- ),
54
- },
55
- };
56
- }
57
-
58
- #serializeArtifactPath(artifactPath: string): string {
59
- if (!path.isAbsolute(artifactPath)) return artifactPath;
60
- return path.relative(this.#artifactDir, artifactPath).split(path.sep).join("/");
29
+ return serialized;
61
30
  }
62
31
  }