@akanjs/devkit 3.0.0-beta.9 → 3.0.0

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 (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
package/spinner.ts CHANGED
@@ -2,23 +2,11 @@ import ora, { type Ora } from "ora";
2
2
 
3
3
  export class Spinner {
4
4
  static padding = 12;
5
- /**
6
- * XXX: `discardStdin` must stay off. It makes ora put the terminal into raw mode for as long as the
7
- * spinner runs, and a Bun child spawned in that window snapshots the raw termios and writes it back
8
- * when it exits — long after the spinner restored the terminal. `akan start` spawns the builder and
9
- * the backend under the "Preparing backend..." spinner, so the first builder recycle (a config or
10
- * runtime-metadata change) SIGTERMs that child and silently turns the developer's terminal raw:
11
- * `isig` goes off, Ctrl+C stops producing SIGINT at all, and the dev server looks unkillable.
12
- */
5
+ // XXX: `discardStdin` must stay off: ora raw-modes the terminal while spinning, a Bun child spawned then writes that
6
+ // raw termios back when it exits, and Ctrl+C stops producing SIGINT.
13
7
  static oraOptions = { discardStdin: false } as const;
14
- /**
15
- * ora sizes its clear loop as `ceil(lineWidth / stream.columns)`, so a tty that reports **0** columns
16
- * makes it `Infinity` and `clear()` never returns. An unsized pty does exactly that (`isTTY: true`,
17
- * `columns: 0`) — CI runners, `expect`/`script` harnesses, some detached panes. Measured: 750MB of
18
- * cursor moves and 8.7GB RSS inside a minute, in a loop that no longer reaches the point where SIGINT
19
- * or SIGTERM could be handled, so only SIGKILL ends it. A terminal with no width has nothing to
20
- * animate anyway; fall back to plain lines.
21
- */
8
+ // ora's clear loop is `ceil(lineWidth / columns)`, so an unsized pty (`isTTY` with 0 columns: CI, `script`) never
9
+ // returns from `clear()`; such a terminal gets plain lines.
22
10
  static canAnimate(stream: NodeJS.WriteStream = process.stderr): boolean {
23
11
  return !stream.isTTY || stream.columns > 0;
24
12
  }
package/sshTunnel.ts CHANGED
@@ -20,7 +20,6 @@ interface TunnelSocketData {
20
20
  closed: boolean;
21
21
  }
22
22
 
23
- type BunServer = { stop(): void };
24
23
  type BunSocket = Bun.Socket<TunnelSocketData>;
25
24
 
26
25
  const closeQuietly = (close: () => void) => {
@@ -126,7 +125,7 @@ export const createSshTunnel = async (options: SshTunnelOptions): Promise<SshTun
126
125
  closing = true;
127
126
  for (const socket of sockets) closeSocket(socket, error);
128
127
  closeQuietly(() => client.end());
129
- closeBunServer(server);
128
+ server.stop();
130
129
  };
131
130
 
132
131
  client.once("error", (error) => closeTunnel(error));
@@ -146,7 +145,3 @@ const closeBunSocket = (socket: BunSocket, error?: Error) => {
146
145
  else socket.data.stream?.end();
147
146
  closeQuietly(() => socket.end());
148
147
  };
149
-
150
- const closeBunServer = (server: BunServer) => {
151
- server.stop();
152
- };
@@ -3,12 +3,7 @@ import ts from "typescript";
3
3
  import type { SourceFileInfo } from "./qualityScanner";
4
4
  import { formatSsrBalance, SSR_SERVER_SHARE_TARGET, SsrScanner } from "./ssrScanner";
5
5
 
6
- /**
7
- * `SsrScanner` is a documented gate — `akan quality ssr` prints the server share and AGENTS.md names 50% as
8
- * the floor — so both halves matter: a rule that stops firing lets the share rot, and a rule that
9
- * over-reports is what makes developers stop reading the output. Every rule here gets a positive and the
10
- * negative that must stay quiet.
11
- */
6
+ //? Each rule gets a positive and a negative: one that stops firing lets the share rot, one that over-reports is ignored
12
7
  const fileOf = (file: string, content: string): SourceFileInfo => ({
13
8
  file,
14
9
  absolutePath: `/repo/${file}`,
package/ssrScanner.ts CHANGED
@@ -22,34 +22,32 @@ interface ComponentInfo {
22
22
  vendorTags: boolean;
23
23
  }
24
24
 
25
- // A component with no client-only touch at all never needed the client bundle, so even a small subtree is
26
- // worth moving. A mostly-static component keeps its interaction and hands the static part to the server, so
27
- // it only pays off once the static subtree is large enough to matter.
25
+ // A component with no client-only touch is worth moving even when small; a mostly-static one only once its static
26
+ // subtree is large enough to matter.
28
27
  const STATIC_COMPONENT_MIN_MASS = 4;
29
28
  const MIXED_COMPONENT_MIN_MASS = 10;
30
29
  const MIXED_COMPONENT_MAX_TOUCHES = 2;
31
30
  const MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
32
31
 
32
+ export const fileWarning = (
33
+ rule: string,
34
+ scope: QualityWarning["scope"],
35
+ file: string,
36
+ message: string,
37
+ line?: number,
38
+ ): QualityWarning =>
39
+ line === undefined
40
+ ? { rule, scope, severity: "warning", file, message }
41
+ : { rule, scope, severity: "warning", file, line, message };
42
+
33
43
  export class SsrScanner {
34
- // `usePage` and `getSelf` read request-scoped server context and are legal in server components, so they
35
- // must not count as evidence that a file needs "use client".
44
+ // `usePage` and `getSelf` read request-scoped server context, which server components may do.
36
45
  static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
37
- static #clientGlobals = new Set([
38
- "window",
39
- "document",
40
- "navigator",
41
- "localStorage",
42
- "sessionStorage",
43
- "location",
44
- "history",
45
- "screen",
46
- "matchMedia",
47
- "IntersectionObserver",
48
- "ResizeObserver",
49
- "MutationObserver",
50
- "requestAnimationFrame",
51
- "WebSocket",
52
- ]);
46
+ static #clientGlobals = new Set(
47
+ "window document navigator localStorage sessionStorage location history screen matchMedia IntersectionObserver"
48
+ .split(" ")
49
+ .concat("ResizeObserver MutationObserver requestAnimationFrame WebSocket".split(" ")),
50
+ );
53
51
  // Runtime singletons that only exist in the client bundle; importing either is what forces the directive.
54
52
  static #clientRuntimeImports = new Set(["st", "fetch"]);
55
53
 
@@ -78,27 +76,29 @@ export class SsrScanner {
78
76
  this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 &&
79
77
  !this.#isConventionClientFile(sourceFile.file)
80
78
  ) {
81
- warnings.push({
82
- rule: "akan.ssr.unnecessary-use-client",
83
- scope: "ssr",
84
- severity: "warning",
85
- file: sourceFile.file,
86
- line: 1,
87
- message: `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`,
88
- });
79
+ warnings.push(
80
+ fileWarning(
81
+ "akan.ssr.unnecessary-use-client",
82
+ "ssr",
83
+ sourceFile.file,
84
+ `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`,
85
+ 1,
86
+ ),
87
+ );
89
88
  }
90
89
 
91
90
  for (const component of components) {
92
91
  if (component.vendorTags) continue;
93
92
  if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
94
- warnings.push({
95
- rule: "akan.ssr.client-static-component",
96
- scope: "ssr",
97
- severity: "warning",
98
- file: sourceFile.file,
99
- line: component.line,
100
- message: `Client component "${component.name}" renders ${component.mass} JSX elements with no client-only capability. It is server-renderable markup sitting in the client bundle.`,
101
- });
93
+ warnings.push(
94
+ fileWarning(
95
+ "akan.ssr.client-static-component",
96
+ "ssr",
97
+ sourceFile.file,
98
+ `Client component "${component.name}" renders ${component.mass} JSX elements with no client-only capability. It is server-renderable markup sitting in the client bundle.`,
99
+ component.line,
100
+ ),
101
+ );
102
102
  continue;
103
103
  }
104
104
  if (
@@ -106,14 +106,15 @@ export class SsrScanner {
106
106
  component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES &&
107
107
  component.mass >= MIXED_COMPONENT_MIN_MASS
108
108
  ) {
109
- warnings.push({
110
- rule: "akan.ssr.client-static-markup",
111
- scope: "ssr",
112
- severity: "warning",
113
- file: sourceFile.file,
114
- line: component.line,
115
- message: `Client component "${component.name}" renders ${component.mass} JSX elements around only ${component.touches.length} client-only touch (${[...new Set(component.touches)].join(", ")}). Most of this subtree does not need the client bundle.`,
116
- });
109
+ warnings.push(
110
+ fileWarning(
111
+ "akan.ssr.client-static-markup",
112
+ "ssr",
113
+ sourceFile.file,
114
+ `Client component "${component.name}" renders ${component.mass} JSX elements around only ${component.touches.length} client-only touch (${[...new Set(component.touches)].join(", ")}). Most of this subtree does not need the client bundle.`,
115
+ component.line,
116
+ ),
117
+ );
117
118
  }
118
119
  }
119
120
 
@@ -122,8 +123,7 @@ export class SsrScanner {
122
123
  return warnings;
123
124
  }
124
125
 
125
- // A database module whose rendering happens entirely in Template/Zone/Util has no server-rendered surface at
126
- // all, so every consumer pays for hydration even when it only needs to display the model.
126
+ // A module rendered only from Template/Zone/Util makes every consumer pay for hydration just to display it.
127
127
  #scanModules(sourceFiles: SourceFileInfo[]): QualityWarning[] {
128
128
  const modules = new Map<string, { clientMass: number; serverFiles: number; line: string }>();
129
129
  for (const sourceFile of sourceFiles) {
@@ -136,36 +136,32 @@ export class SsrScanner {
136
136
  }
137
137
  return [...modules]
138
138
  .filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS)
139
- .map(([moduleDir, entry]) => ({
140
- rule: "akan.ssr.module-missing-server-view",
141
- scope: "ssr" as const,
142
- severity: "warning" as const,
143
- file: entry.line,
144
- message: `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`,
145
- }));
139
+ .map(([moduleDir, entry]) =>
140
+ fileWarning(
141
+ "akan.ssr.module-missing-server-view",
142
+ "ssr",
143
+ entry.line,
144
+ `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`,
145
+ ),
146
+ );
146
147
  }
147
148
 
148
- // A load fired from a mount-only effect is data the route already could have fetched: the client renders an
149
- // empty shell, hydrates, then fetches. A reactive effect (non-empty deps) responds to client state instead
150
- // and has no server-side equivalent, so only the empty-dependency form is a finding.
149
+ // Only an empty-dependency effect is a finding: a reactive effect answers client state and has no server equivalent.
151
150
  #getMountLoadWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
152
151
  const warnings: QualityWarning[] = [];
153
- const visit = (node: ts.Node) => {
154
- if (this.#isMountEffect(sourceFile.sourceFile, node)) {
155
- for (const load of this.#getLoadCalls(sourceFile.sourceFile, node)) {
156
- warnings.push({
157
- rule: "akan.ssr.client-mount-load",
158
- scope: "ssr",
159
- severity: "warning",
160
- file: sourceFile.file,
161
- line: this.#getLine(sourceFile.sourceFile, load.node),
162
- message: `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`,
163
- });
164
- }
165
- }
166
- ts.forEachChild(node, visit);
167
- };
168
- ts.forEachChild(sourceFile.sourceFile, visit);
152
+ SsrScanner.#walk(sourceFile.sourceFile, (node) => {
153
+ if (!this.#isMountEffect(sourceFile.sourceFile, node)) return;
154
+ for (const load of this.#getLoadCalls(sourceFile.sourceFile, node))
155
+ warnings.push(
156
+ fileWarning(
157
+ "akan.ssr.client-mount-load",
158
+ "ssr",
159
+ sourceFile.file,
160
+ `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`,
161
+ this.#getLine(sourceFile.sourceFile, load.node),
162
+ ),
163
+ );
164
+ });
169
165
  return warnings;
170
166
  }
171
167
 
@@ -179,35 +175,30 @@ export class SsrScanner {
179
175
 
180
176
  #getLoadCalls(sourceFile: ts.SourceFile, node: ts.Node) {
181
177
  const calls: Array<{ callee: string; node: ts.Node }> = [];
182
- const visit = (child: ts.Node) => {
183
- if (ts.isCallExpression(child)) {
184
- const callee = child.expression.getText(sourceFile);
185
- if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
186
- calls.push({ callee, node: child });
187
- }
188
- ts.forEachChild(child, visit);
189
- };
190
- ts.forEachChild(node, visit);
178
+ SsrScanner.#walk(node, (child) => {
179
+ if (!ts.isCallExpression(child)) return;
180
+ const callee = child.expression.getText(sourceFile);
181
+ if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
182
+ calls.push({ callee, node: child });
183
+ });
191
184
  return calls;
192
185
  }
193
186
 
194
187
  #getTemplateStateWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
195
188
  if (!sourceFile.file.endsWith(".Template.tsx")) return [];
196
189
  const warnings: QualityWarning[] = [];
197
- const visit = (node: ts.Node) => {
198
- if (ts.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
199
- warnings.push({
200
- rule: "akan.ssr.template-client-state",
201
- scope: "ssr",
202
- severity: "warning",
203
- file: sourceFile.file,
204
- line: this.#getLine(sourceFile.sourceFile, node),
205
- message: "Template holds form state in useState. Templates are store-driven and carry no local state.",
206
- });
207
- }
208
- ts.forEachChild(node, visit);
209
- };
210
- ts.forEachChild(sourceFile.sourceFile, visit);
190
+ SsrScanner.#walk(sourceFile.sourceFile, (node) => {
191
+ if (ts.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState")
192
+ warnings.push(
193
+ fileWarning(
194
+ "akan.ssr.template-client-state",
195
+ "ssr",
196
+ sourceFile.file,
197
+ "Template holds form state in useState. Templates are store-driven and carry no local state.",
198
+ this.#getLine(sourceFile.sourceFile, node),
199
+ ),
200
+ );
201
+ });
211
202
  return warnings;
212
203
  }
213
204
 
@@ -261,7 +252,7 @@ export class SsrScanner {
261
252
 
262
253
  #getTouches(sourceFile: ts.SourceFile, node: ts.Node): string[] {
263
254
  const touches: string[] = [];
264
- const visit = (child: ts.Node) => {
255
+ SsrScanner.#walk(node, (child) => {
265
256
  if (ts.isCallExpression(child)) {
266
257
  const callee = child.expression.getText(sourceFile);
267
258
  const bareName = callee.split(".").pop() ?? callee;
@@ -275,41 +266,42 @@ export class SsrScanner {
275
266
  if (root === "st") touches.push("st");
276
267
  else if (SsrScanner.#clientGlobals.has(root)) touches.push(root);
277
268
  }
278
- ts.forEachChild(child, visit);
279
- };
280
- ts.forEachChild(node, visit);
269
+ });
281
270
  return touches;
282
271
  }
283
272
 
284
273
  #getTagNames(sourceFile: ts.SourceFile, node: ts.Node): Set<string> {
285
274
  const tags = new Set<string>();
286
- const visit = (child: ts.Node) => {
275
+ SsrScanner.#walk(node, (child) => {
287
276
  if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child))
288
277
  tags.add(child.tagName.getText(sourceFile).split(".")[0]);
289
- ts.forEachChild(child, visit);
290
- };
291
- ts.forEachChild(node, visit);
278
+ });
292
279
  return tags;
293
280
  }
294
281
 
295
282
  #getMass(node: ts.Node) {
296
283
  let mass = 0;
297
- const visit = (child: ts.Node) => {
284
+ SsrScanner.#walk(node, (child) => {
298
285
  if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child)) mass += 1;
299
- ts.forEachChild(child, visit);
300
- };
301
- ts.forEachChild(node, visit);
286
+ });
302
287
  return mass;
303
288
  }
304
289
 
290
+ static #walk(node: ts.Node, visit: (descendant: ts.Node) => void) {
291
+ const walk = (child: ts.Node) => {
292
+ visit(child);
293
+ ts.forEachChild(child, walk);
294
+ };
295
+ ts.forEachChild(node, walk);
296
+ }
297
+
305
298
  #hasUseClient(sourceFile: ts.SourceFile) {
306
299
  const first = sourceFile.statements[0];
307
300
  if (!first || !ts.isExpressionStatement(first) || !ts.isStringLiteral(first.expression)) return false;
308
301
  return first.expression.text === "use client";
309
302
  }
310
303
 
311
- // A bare specifier is a third-party package: it may be client-only, which is a legitimate reason for the
312
- // directive that no amount of AST reading can rule out.
304
+ // A third-party package may be client-only, a legitimate reason for the directive no AST reading can rule out.
313
305
  #hasVendorImport(sourceFile: ts.SourceFile) {
314
306
  return sourceFile.statements.some(
315
307
  (statement) => ts.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)),
@@ -340,9 +332,7 @@ export class SsrScanner {
340
332
  return false;
341
333
  }
342
334
 
343
- // Zone/Template/Util carry the directive mechanically by file role, and `index_.tsx` is the declared
344
- // "use client" + lazy() boundary. In neither case is the directive a stray — for module UI it means markup
345
- // belongs in a Unit or View instead, which the component rules already cover.
335
+ // Zone/Template/Util carry the directive by file role, and `index_.tsx` is the declared lazy() boundary.
346
336
  #isConventionClientFile(file: string) {
347
337
  if (file.endsWith("/index_.tsx")) return true;
348
338
  return /\.(Zone|Template|Util)\.tsx$/.test(file) && this.#getModuleDir(file) !== null;
package/stripAnsi.ts ADDED
@@ -0,0 +1,3 @@
1
+ const ansi = new RegExp(`${String.fromCharCode(27)}(?:[@-Z\\\\-_]|\\[[\\s\\S]*?[@-~])`, "g");
2
+
3
+ export const stripAnsi = (text: string) => text.replace(ansi, "");
package/subspace.test.ts CHANGED
@@ -1,31 +1,17 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, setDefaultTimeout, test } from "bun:test";
2
+ import { mkdir, readdir, rm } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { Executor, WorkspaceExecutor } from "./executors";
6
5
  import { FileSys } from "./fileSys";
7
6
  import { formatSubspaceDiff, formatSubspacePushResults, formatSubspaceStatuses, Subspace } from "./subspace";
8
7
  import { SubspaceConfig } from "./subspaceConfig";
8
+ import { isolateEnv, tempDirs, writeText as write } from "./testHelpers";
9
9
 
10
- const tempRoots: string[] = [];
11
- const originalEnv = { ...process.env };
10
+ //? Each case drives several real git clones and pushes; on a 4-core Windows VM that alone lands at 2-5s.
11
+ setDefaultTimeout(20_000);
12
12
 
13
- beforeEach(() => {
14
- process.env = { ...originalEnv };
15
- process.env.AKAN_PUBLIC_REPO_NAME = "workspace";
16
- process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
17
- process.env.AKAN_PUBLIC_ENV = "local";
18
- });
19
-
20
- afterEach(async () => {
21
- process.env = { ...originalEnv };
22
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
23
- });
24
-
25
- const write = async (filePath: string, content: string) => {
26
- await mkdir(path.dirname(filePath), { recursive: true });
27
- await writeFile(filePath, content);
28
- };
13
+ isolateEnv({ AKAN_PUBLIC_REPO_NAME: "workspace", AKAN_PUBLIC_SERVE_DOMAIN: "example.com", AKAN_PUBLIC_ENV: "local" });
14
+ const makeTempRoot = tempDirs("akan-subspace-");
29
15
 
30
16
  const git = async (cwd: string, args: string[]) =>
31
17
  await new Executor("fixture", cwd).spawn("git", ["-c", "user.email=t@t", "-c", "user.name=t", ...args]);
@@ -64,10 +50,8 @@ const makeSys = async (root: string, member: "apps" | "libs", name: string, extr
64
50
  await write(path.join(root, member, name, relative), content);
65
51
  };
66
52
 
67
- /** A workspace with two apps, one shared library, and a subspace that serves only the first app. */
68
53
  const makeMirror = async (servedApp: string, privateApp: string, libName: string) => {
69
- const workRoot = await mkdtemp(path.join(os.tmpdir(), "akan-subspace-"));
70
- tempRoots.push(workRoot);
54
+ const workRoot = await makeTempRoot();
71
55
  const wsRoot = path.join(workRoot, "workspace");
72
56
  const bareRoot = path.join(workRoot, "subspace.git");
73
57
  await mkdir(wsRoot, { recursive: true });
@@ -96,7 +80,7 @@ const makeMirror = async (servedApp: string, privateApp: string, libName: string
96
80
  await write(path.join(wsRoot, "benchmarks/bench.ts"), "export {};\n");
97
81
  await write(
98
82
  path.join(wsRoot, "akan.subspace.ts"),
99
- `export default { pushableBranches: ["develop"], exclude: ["benchmarks"], subspaces: [{ name: "acme", repo: "${bareRoot}", apps: ["${servedApp}"] }] };\n`,
83
+ `export default { pushableBranches: ["develop"], exclude: ["benchmarks"], subspaces: [{ name: "acme", repo: ${JSON.stringify(bareRoot)}, apps: ["${servedApp}"] }] };\n`,
100
84
  );
101
85
 
102
86
  await makeSys(wsRoot, "apps", servedApp, {