@akanjs/devkit 3.0.0-beta.9 → 3.0.1

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 (346) hide show
  1. package/CHANGELOG.md +1819 -0
  2. package/DEV_RUNTIME_KNOBS.md +44 -26
  3. package/README.ko.md +21 -7
  4. package/README.md +17 -8
  5. package/abstractDoc.test.ts +6 -15
  6. package/abstractDoc.ts +6 -8
  7. package/agentsIndex.test.ts +4 -7
  8. package/agentsIndex.ts +6 -22
  9. package/akanApp/BackendImportGraph.test.ts +8 -22
  10. package/akanApp/BackendImportGraph.ts +1 -20
  11. package/akanApp/akanApp.host.test.ts +56 -3
  12. package/akanApp/akanApp.host.ts +218 -278
  13. package/akanApp/devBootLatch.test.ts +39 -0
  14. package/akanApp/devBootLatch.ts +39 -0
  15. package/akanApp/devHostPolicy.test.ts +81 -35
  16. package/akanApp/devHostPolicy.ts +41 -102
  17. package/akanConfig/akanBin.test.ts +62 -0
  18. package/akanConfig/akanBin.ts +78 -0
  19. package/akanConfig/akanConfig.test.ts +439 -141
  20. package/akanConfig/akanConfig.ts +359 -275
  21. package/akanConfig/index.ts +1 -0
  22. package/akanConfig/types.ts +22 -8
  23. package/akanContext.ts +110 -160
  24. package/akanMcpContract.ts +28 -73
  25. package/appSelectionMemory.ts +1 -4
  26. package/applicationBuildReporter.ts +37 -5
  27. package/applicationBuildRunner.test.ts +41 -5
  28. package/applicationBuildRunner.ts +71 -63
  29. package/applicationTestPreload.ts +3 -12
  30. package/artifact/implicitRootLayout.test.ts +34 -31
  31. package/artifact/implicitRootLayout.ts +60 -107
  32. package/artifact/routeSeedIndex.test.ts +5 -18
  33. package/artifact/routeSeedIndex.ts +11 -44
  34. package/biome.base.json +39 -2
  35. package/biomeBase.ts +2 -5
  36. package/cloud/cloudApi.test.ts +202 -0
  37. package/cloud/cloudApi.ts +77 -39
  38. package/cloud/configLock.test.ts +64 -0
  39. package/cloud/configLock.ts +96 -0
  40. package/cloud/constants.ts +26 -5
  41. package/cloud/globalConfig.ts +71 -32
  42. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  43. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  44. package/codeAgent/agent/CodeAgent.ts +551 -0
  45. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  46. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  47. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  48. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  49. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  50. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  51. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  52. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  53. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  54. package/codeAgent/agent/CodeMailbox.ts +148 -0
  55. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  56. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  57. package/codeAgent/agent/SubagentPool.ts +214 -0
  58. package/codeAgent/agent/akanCodeModel.ts +47 -0
  59. package/codeAgent/agent/akanCodePaths.ts +31 -0
  60. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  61. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  62. package/codeAgent/agent/codeAgent.test.ts +178 -0
  63. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  64. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  65. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  66. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  67. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  68. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  69. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  70. package/codeAgent/agent/consoleToStderr.ts +8 -0
  71. package/codeAgent/agent/jsonLines.ts +29 -0
  72. package/codeAgent/agent/stringArg.ts +5 -0
  73. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  74. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  75. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  76. package/codeAgent/feedback/PreviewView.ts +95 -0
  77. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  78. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  79. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  80. package/codeAgent/index.ts +34 -0
  81. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  82. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  83. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  84. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  85. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  86. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  87. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  88. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  89. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  90. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  91. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  92. package/codeAgent/tools/AkanCli.ts +50 -0
  93. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  94. package/codeAgent/tools/AkanEditScope.ts +48 -0
  95. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  96. package/codeAgent/tools/AkanToolPack.ts +92 -0
  97. package/codeAgent/tools/AkanVerifier.ts +48 -0
  98. package/codeAgent/tools/AskToolPack.ts +99 -0
  99. package/codeAgent/tools/MailToolPack.ts +76 -0
  100. package/codeAgent/tools/McpClient.ts +187 -0
  101. package/codeAgent/tools/McpOAuth.ts +233 -0
  102. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  103. package/codeAgent/tools/McpServerConfig.ts +129 -0
  104. package/codeAgent/tools/McpSignIn.ts +158 -0
  105. package/codeAgent/tools/McpTokenStore.ts +81 -0
  106. package/codeAgent/tools/McpToolPack.ts +199 -0
  107. package/codeAgent/tools/SessionToolPack.ts +91 -0
  108. package/codeAgent/tools/WebToolPack.ts +147 -0
  109. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  110. package/codeAgent/tools/askToolPack.test.ts +116 -0
  111. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  112. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  113. package/codegenLock.test.ts +4 -13
  114. package/codegenLock.ts +3 -17
  115. package/commandDecorators/argMeta.ts +1 -5
  116. package/commandDecorators/camelToKebabCase.ts +1 -0
  117. package/commandDecorators/command.ts +46 -102
  118. package/commandDecorators/commandBuilder.ts +30 -61
  119. package/commandDecorators/commandDecorators.test.ts +2 -10
  120. package/commandDecorators/dependencyBuilder.ts +10 -1
  121. package/commandDecorators/helpFormatter.ts +68 -145
  122. package/commandDecorators/index.ts +0 -2
  123. package/commandDecorators/targetMeta.ts +9 -7
  124. package/createTunnel.ts +7 -14
  125. package/csrE2e/csrE2eHarness.fixture.ts +506 -0
  126. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  127. package/csrE2e/csrPageStack.e2e.test.ts +265 -0
  128. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  129. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  130. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  131. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  132. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  133. package/dependencyScanner.test.ts +4 -16
  134. package/dependencyScanner.ts +31 -80
  135. package/devkitUtils.test.ts +48 -20
  136. package/executors.linkFallback.test.ts +103 -0
  137. package/executors.test.ts +176 -106
  138. package/executors.ts +212 -498
  139. package/fileEditor.ts +9 -37
  140. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  141. package/fileSys.ts +22 -9
  142. package/formSetterScanner.ts +3 -16
  143. package/frontendBuild/allRoutesBuilder.ts +7 -21
  144. package/frontendBuild/autoImportSync.test.ts +27 -38
  145. package/frontendBuild/autoImportSync.ts +31 -65
  146. package/frontendBuild/buildRouteClient.test.ts +123 -37
  147. package/frontendBuild/bundleDefine.test.ts +12 -0
  148. package/frontendBuild/bundleDefine.ts +16 -0
  149. package/frontendBuild/clientBuildTypes.ts +5 -36
  150. package/frontendBuild/clientEntriesBundler.ts +9 -40
  151. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  152. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  153. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  154. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  155. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  156. package/frontendBuild/csrDevBundler.test.ts +66 -0
  157. package/frontendBuild/csrDevBundler.ts +95 -0
  158. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  159. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  160. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  161. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  162. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  163. package/frontendBuild/csrDevPatcher.ts +548 -0
  164. package/frontendBuild/csrDevPaths.ts +145 -0
  165. package/frontendBuild/csrDevResolver.test.ts +239 -0
  166. package/frontendBuild/csrDevResolver.ts +363 -0
  167. package/frontendBuild/csrDevTypes.ts +61 -0
  168. package/frontendBuild/csrEntryFiles.ts +24 -0
  169. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  170. package/frontendBuild/cssCandidateCache.ts +6 -41
  171. package/frontendBuild/cssCompiler.ts +31 -82
  172. package/frontendBuild/cssImportResolver.ts +6 -19
  173. package/frontendBuild/devChangePlanner.ts +7 -10
  174. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  175. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  176. package/frontendBuild/devRegistryBundler.ts +192 -0
  177. package/frontendBuild/fontOptimizer.test.ts +6 -14
  178. package/frontendBuild/fontOptimizer.ts +15 -72
  179. package/frontendBuild/fontPruner.test.ts +5 -10
  180. package/frontendBuild/fontPruner.ts +5 -20
  181. package/frontendBuild/frontendBuild.test.ts +57 -70
  182. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  183. package/frontendBuild/hmrWatcher.test.ts +59 -32
  184. package/frontendBuild/hmrWatcher.ts +37 -74
  185. package/frontendBuild/index.ts +4 -0
  186. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  187. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  188. package/frontendBuild/precompressArtifacts.ts +5 -18
  189. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  190. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  191. package/frontendBuild/routeClientBuilder.ts +100 -62
  192. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  193. package/frontendBuild/serverGraphFile.test.ts +77 -0
  194. package/frontendBuild/serverGraphFile.ts +93 -0
  195. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  196. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  197. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  198. package/frontendBuild/ssrDevBundler.ts +85 -0
  199. package/frontendBuild/styleContract.ts +1 -8
  200. package/frontendBuild/themeValidator.ts +4 -26
  201. package/frontendBuild/watchRootResolver.test.ts +23 -42
  202. package/frontendBuild/watchRootResolver.ts +5 -27
  203. package/getRelatedCnsts.ts +12 -92
  204. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  205. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  206. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  207. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  208. package/incrementalBuilder/builderChannel.test.ts +24 -49
  209. package/incrementalBuilder/builderChannel.ts +6 -37
  210. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  211. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  212. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  213. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  214. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  215. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  216. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  217. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  218. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  219. package/index.ts +5 -19
  220. package/integration/devResourceProbe.ts +50 -56
  221. package/integration/devStability.integration.test.ts +111 -202
  222. package/integration/devStabilityHarness.test.ts +13 -28
  223. package/integration/devStabilityHarness.ts +67 -330
  224. package/integration/ssrMemoryProbe.ts +41 -167
  225. package/libSource.test.ts +4 -15
  226. package/libSource.ts +19 -31
  227. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  228. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  229. package/lint/__fixtures__/no-static-in-object-light-model/bad.ts +43 -0
  230. package/lint/__fixtures__/no-static-in-object-light-model/fixture.json +1 -0
  231. package/lint/__fixtures__/no-static-in-object-light-model/good.ts +42 -0
  232. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  233. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  234. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  235. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  236. package/lint/gritRules.test.ts +8 -24
  237. package/lint/no-deep-internal-import.grit +2 -2
  238. package/lint/no-document-cookie.grit +29 -0
  239. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  240. package/lint/no-static-in-object-light-model.grit +27 -0
  241. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  242. package/lint/no-web-storage.grit +30 -0
  243. package/linter.test.ts +1 -2
  244. package/linter.ts +26 -116
  245. package/mobile/desktopBin.test.ts +182 -0
  246. package/mobile/desktopBin.ts +144 -0
  247. package/mobile/desktopServerStage.test.ts +252 -0
  248. package/mobile/desktopServerStage.ts +178 -0
  249. package/mobile/index.ts +9 -0
  250. package/mobile/mobileTarget.test.ts +57 -8
  251. package/mobile/mobileTarget.ts +45 -26
  252. package/mobile/nativeApi.test.ts +30 -0
  253. package/mobile/nativeApi.ts +40 -0
  254. package/mobile/nativeApp.test.ts +184 -0
  255. package/mobile/nativeApp.ts +454 -0
  256. package/mobile/nativeAppLine.test.ts +69 -0
  257. package/mobile/nativeAppLine.ts +71 -0
  258. package/mobile/nativeConfig.test.ts +438 -0
  259. package/mobile/nativeConfig.ts +294 -0
  260. package/mobile/nativePluginFolders.test.ts +81 -0
  261. package/mobile/nativePluginFolders.ts +60 -0
  262. package/mobile/nativeWebDir.test.ts +80 -0
  263. package/mobile/nativeWebDir.ts +66 -0
  264. package/mobile/placeholderAppId.test.ts +13 -0
  265. package/mobile/placeholderAppId.ts +16 -0
  266. package/mobile/usageDescriptions.test.ts +27 -0
  267. package/mobile/usageDescriptions.ts +24 -0
  268. package/package.json +11 -11
  269. package/packageExports.test.ts +3 -18
  270. package/packageExportsMap.ts +3 -19
  271. package/piDependencyPins.test.ts +46 -0
  272. package/platformTest/BunTestReport.test.ts +60 -0
  273. package/platformTest/BunTestReport.ts +65 -0
  274. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  275. package/platformTest/IdleWatchedProcess.ts +73 -0
  276. package/platformTest/LinuxDockerTarget.ts +105 -0
  277. package/platformTest/PlatformTestRun.test.ts +64 -0
  278. package/platformTest/PlatformTestRun.ts +141 -0
  279. package/platformTest/PlatformTestTarget.ts +142 -0
  280. package/platformTest/TestSnapshot.test.ts +72 -0
  281. package/platformTest/TestSnapshot.ts +103 -0
  282. package/platformTest/WindowsSshTarget.ts +151 -0
  283. package/prompter.ts +3 -13
  284. package/qualityScanner.test.ts +9 -26
  285. package/qualityScanner.ts +200 -286
  286. package/recipeScanner.test.ts +7 -12
  287. package/recipeScanner.ts +9 -38
  288. package/repoIdentity.ts +2 -9
  289. package/routeSourceValidator.test.ts +0 -1
  290. package/routeSourceValidator.ts +7 -42
  291. package/scanInfo.ts +91 -225
  292. package/selectModel.ts +1 -2
  293. package/semver.ts +3 -5
  294. package/slicePlanner.test.ts +6 -24
  295. package/slicePlanner.ts +11 -32
  296. package/spinner.test.ts +3 -8
  297. package/spinner.ts +4 -16
  298. package/sshTunnel.ts +1 -6
  299. package/ssrScanner.test.ts +1 -6
  300. package/ssrScanner.ts +103 -113
  301. package/stripAnsi.ts +3 -0
  302. package/subspace.test.ts +19 -26
  303. package/subspace.ts +48 -115
  304. package/subspaceConfig.ts +3 -11
  305. package/testHelpers.ts +194 -0
  306. package/transforms/asyncDefaultExportDetector.ts +3 -11
  307. package/transforms/barrelAnalyzer.ts +43 -135
  308. package/transforms/barrelImportsPlugin.ts +38 -175
  309. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  310. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  311. package/transforms/moduleSyntax.ts +32 -0
  312. package/transforms/rscUseClientTransform.ts +42 -20
  313. package/transforms/serverEnvPlugin.test.ts +103 -0
  314. package/transforms/serverEnvPlugin.ts +41 -0
  315. package/transforms/transforms.test.ts +166 -44
  316. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  317. package/transforms/useClientBundlePlugin.ts +6 -23
  318. package/typeChecker.ts +7 -63
  319. package/types.ts +2 -0
  320. package/uploadRelease.ts +10 -25
  321. package/workflow/artifacts.ts +18 -0
  322. package/workflow/executor.test.ts +1 -12
  323. package/workflow/executor.ts +101 -110
  324. package/workflow/index.ts +0 -1
  325. package/workflow/moduleIndex.test.ts +4 -15
  326. package/workflow/moduleIndex.ts +9 -49
  327. package/workflow/plan.ts +18 -25
  328. package/workflow/render.ts +80 -96
  329. package/workflow/rolloutGate.test.ts +1 -1
  330. package/workflow/source.ts +52 -69
  331. package/workflow/sourceAst.ts +50 -0
  332. package/workspaceLayout.test.ts +18 -3
  333. package/workspaceLayout.ts +22 -23
  334. package/abstractCompactor.ts +0 -112
  335. package/aiEditor.test.ts +0 -60
  336. package/aiEditor.ts +0 -387
  337. package/applicationReleasePackager.ts +0 -209
  338. package/capacitorApp.test.ts +0 -372
  339. package/capacitorApp.ts +0 -1648
  340. package/commandDecorators/commandMeta.ts +0 -7
  341. package/commandDecorators/types.ts +0 -10
  342. package/getDirname.ts +0 -4
  343. package/guideline.ts +0 -19
  344. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  345. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  346. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,644 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { CsrE2eHarness } from "./csrE2eHarness.fixture";
5
+
6
+ const REGISTRY = "/e2e/registry";
7
+ const workspaceRoot = path.resolve(import.meta.dir, "../../../..");
8
+ const uiFile = (name: string) => path.join(workspaceRoot, "apps/minimal/ui", name);
9
+ const probeFile = uiFile("RegistryProbe.tsx");
10
+ const labelFile = uiFile("RegistryLabel.tsx");
11
+ const lazyFile = uiFile("RegistryLazy_Dynamic.tsx");
12
+ const serverPartFile = uiFile("RegistryServerPart.tsx");
13
+ const pageFile = path.join(workspaceRoot, "apps/minimal/page/(home)/e2e/registry.tsx");
14
+ const storeFile = path.join(workspaceRoot, "apps/minimal/lib/_minimal/minimal.store.ts");
15
+ const valueFile = path.join(workspaceRoot, "apps/minimal/common/registryValue.constant.ts");
16
+ //? Read only by client code (the probe), so breaking it fails the registry's builds and no server bundle.
17
+ const contextFile = uiFile("registryContext.ts");
18
+ const registryManifest = path.join(workspaceRoot, "apps/minimal/.akan/artifact/ssr-dev/manifest.json");
19
+ const configFile = path.join(workspaceRoot, "apps/minimal/akan.config.ts");
20
+ const sharedFile = path.join(workspaceRoot, "apps/minimal/common/registrySharedText.ts");
21
+ const signalFile = path.join(workspaceRoot, "apps/minimal/lib/_minimal/minimal.signal.ts");
22
+ const serviceFile = path.join(workspaceRoot, "apps/minimal/lib/_minimal/minimal.service.ts");
23
+ const bootLogPrefix = "akan-e2e-boots:";
24
+ const workerEntry = path.join(workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts");
25
+ const port = Number(process.env.AKAN_CSR_E2E_SSR_REGISTRY_PORT ?? 8494);
26
+
27
+ //? Build workers of the dev server this suite started (its process group, so another session's leftovers in the same
28
+ //? worktree do not count) that outlived the builder that spawned them (reparented to init), sampled until stopped: one
29
+ //? left running would keep writing a registry the replacement builder is rebuilding. Seen twice, so a worker caught in
30
+ //? the moment between its builder's exit and its own is not counted.
31
+ const sampleOrphanWorkers = (group: number | null) => {
32
+ const seen = new Map<string, number>();
33
+ let sampling = true;
34
+ const loop = (async () => {
35
+ while (sampling) {
36
+ const listing = Bun.spawnSync(["ps", "-axo", "pid=,ppid=,pgid=,command="]).stdout.toString();
37
+ for (const line of listing.split("\n")) {
38
+ const [pid, ppid, pgid] = line.trim().split(/\s+/);
39
+ if (ppid !== "1" || !pid || (group !== null && pgid !== String(group))) continue;
40
+ if (line.includes(workerEntry)) seen.set(pid, (seen.get(pid) ?? 0) + 1);
41
+ }
42
+ await Bun.sleep(200);
43
+ }
44
+ })();
45
+ return async () => {
46
+ sampling = false;
47
+ await loop;
48
+ return [...seen].filter(([, count]) => count > 1).map(([pid]) => pid);
49
+ };
50
+ };
51
+
52
+ interface RegistryWindow {
53
+ __akan?: { generation: number; inspect(): { started: boolean; failed: boolean } };
54
+ __akanE2eBroken?: boolean;
55
+ __akanRegistryProbe?: { effects: number; contextId: string };
56
+ __AKAN_CSR_LAST_UPDATE__?: { generation: number; executed: string[] };
57
+ __AKAN_HMR_TRACES__?: { kind: string; generation: number; trace: { broadcastAt?: number } | null }[];
58
+ }
59
+
60
+ describe.skipIf(!CsrE2eHarness.enabled)("SSR dev registry (minimal)", () => {
61
+ let ssr: CsrE2eHarness;
62
+
63
+ beforeAll(async () => {
64
+ ssr = await CsrE2eHarness.start({ app: "minimal", port, env: { AKAN_DEV_SSR_ARM_DELAY_MS: "3000" } });
65
+ }, 240_000);
66
+
67
+ afterAll(async () => {
68
+ // biome-ignore lint/suspicious/noUnnecessaryConditions: unassigned when beforeAll threw before the harness started
69
+ await ssr?.close();
70
+ }, 60_000);
71
+
72
+ const open = async () => {
73
+ await ssr.open(REGISTRY, { csr: false });
74
+ await ssr.waitFor(
75
+ () =>
76
+ document.querySelector('[data-e2e="lazy"]') !== null &&
77
+ ((window as unknown as RegistryWindow).__akan?.generation ?? 0) > 0,
78
+ { timeout: 20_000 },
79
+ );
80
+ };
81
+ const snapshot = async () =>
82
+ await ssr.evaluate(() => {
83
+ const w = window as unknown as RegistryWindow;
84
+ return {
85
+ count: document.querySelector('[data-e2e="count"]')?.textContent ?? null,
86
+ effects: w.__akanRegistryProbe?.effects ?? 0,
87
+ contextId: w.__akanRegistryProbe?.contextId ?? "",
88
+ executed: w.__AKAN_CSR_LAST_UPDATE__?.executed ?? [],
89
+ };
90
+ });
91
+ const textIs = async (selector: string, expected: string) =>
92
+ await ssr.waitFor((target: string, text: string) => document.querySelector(target)?.textContent === text, {
93
+ args: [selector, expected],
94
+ timeout: 20_000,
95
+ });
96
+ const probeMarked = (marked: boolean) =>
97
+ ssr.waitFor(
98
+ (expected: boolean) =>
99
+ (document.querySelector('[data-e2e="registry-probe"]')?.getAttribute("data-e2e-hot") === "1") === expected,
100
+ { args: [marked], timeout: 20_000 },
101
+ );
102
+ const markProbe = (source: string, value = "1") =>
103
+ source.replace('data-e2e="registry-probe">', `data-e2e="registry-probe" data-e2e-hot="${value}">`);
104
+ const probeHot = (value: string) =>
105
+ ssr.waitFor(
106
+ (expected: string) =>
107
+ document.querySelector('[data-e2e="registry-probe"]')?.getAttribute("data-e2e-hot") === expected,
108
+ { args: [value], timeout: 20_000 },
109
+ );
110
+ const fetchPage = async (pathname = REGISTRY) =>
111
+ await fetch(new URL(`/en${pathname}`, ssr.origin)).then((res) => res.text());
112
+ const waitForServerLog = async (mark: { out: number; err: number }, pattern: RegExp, timeout = 90_000) => {
113
+ const deadline = Date.now() + timeout;
114
+ while (!pattern.test(await ssr.serverLogSince(mark))) {
115
+ if (Date.now() > deadline) throw new Error(`[ssr-registry-e2e] the dev server never logged ${pattern}`);
116
+ await Bun.sleep(250);
117
+ }
118
+ };
119
+
120
+ test("the page hydrates from the registry, with the bootstrap run ahead of its modules", async () => {
121
+ await open();
122
+ expect(
123
+ await ssr.evaluate(() => String((window as { __webpack_require__?: unknown }).__webpack_require__)),
124
+ ).toContain("idPrefix");
125
+ await ssr.evaluate(() => document.querySelector<HTMLButtonElement>('[data-e2e="bump"]')?.click());
126
+ await textIs('[data-e2e="count"]', "1");
127
+ }, 60_000);
128
+
129
+ test("a component edit patches in place, keeping state and every other module instance", async () => {
130
+ await open();
131
+ await ssr.evaluate(() => {
132
+ const bump = document.querySelector<HTMLButtonElement>('[data-e2e="bump"]');
133
+ bump?.click();
134
+ bump?.click();
135
+ });
136
+ await textIs('[data-e2e="count"]', "2");
137
+ const before = await snapshot();
138
+ await ssr.editSource(probeFile, markProbe, async () => {
139
+ await probeMarked(true);
140
+ const after = await snapshot();
141
+ expect(after.count).toBe("2");
142
+ expect(after.contextId).toBe(before.contextId);
143
+ expect(after.executed).toEqual(["apps/minimal/ui/RegistryProbe.tsx"]);
144
+ });
145
+ await probeMarked(false);
146
+ expect(await ssr.reloaded()).toBe(false);
147
+ }, 90_000);
148
+
149
+ test("undoing an edit shows the original again", async () => {
150
+ await open();
151
+ const original = await Bun.file(probeFile).text();
152
+ await Bun.write(probeFile, markProbe(original));
153
+ try {
154
+ await probeMarked(true);
155
+ } finally {
156
+ await Bun.write(probeFile, original);
157
+ }
158
+ await probeMarked(false);
159
+ expect(await ssr.reloaded()).toBe(false);
160
+ }, 90_000);
161
+
162
+ test("a lazy() target patches in place, and re-runs no effect of a module it does not own", async () => {
163
+ await open();
164
+ const { effects } = await snapshot();
165
+ await ssr.editSource(
166
+ lazyFile,
167
+ (source) => source.replace("lazy-0", "lazy-1"),
168
+ async () => {
169
+ await textIs('[data-e2e="lazy"]', "lazy-1");
170
+ expect((await snapshot()).effects).toBe(effects);
171
+ },
172
+ );
173
+ await textIs('[data-e2e="lazy"]', "lazy-0");
174
+ expect(await ssr.reloaded()).toBe(false);
175
+ }, 90_000);
176
+
177
+ test("a component the server renders too updates both copies, its patch going out with the RSC refresh", async () => {
178
+ await open();
179
+ await ssr.evaluate(() => document.querySelector<HTMLButtonElement>('[data-e2e="bump"]')?.click());
180
+ await textIs('[data-e2e="count"]', "1");
181
+ await ssr.editSource(
182
+ labelFile,
183
+ (source) => source.replace("label-0", "label-1"),
184
+ async () => {
185
+ await textIs('[data-e2e="label-client"]', "label-1");
186
+ await textIs('[data-e2e="label-server"]', "label-1");
187
+ const traces = await ssr.evaluate(() => (window as unknown as RegistryWindow).__AKAN_HMR_TRACES__ ?? []);
188
+ const patch = traces.filter((entry) => entry.kind === "ssr").at(-1);
189
+ const refresh = traces.filter((entry) => entry.kind === "rsc-refresh").at(-1);
190
+ expect(patch?.trace?.broadcastAt).toBeDefined();
191
+ expect(Math.abs((patch?.trace?.broadcastAt ?? 0) - (refresh?.trace?.broadcastAt ?? 0))).toBeLessThan(100);
192
+ },
193
+ );
194
+ await textIs('[data-e2e="label-server"]', "label-0");
195
+ await textIs('[data-e2e="label-client"]', "label-0");
196
+ expect(await ssr.evaluate(() => document.querySelector('[data-e2e="count"]')?.textContent)).toBe("1");
197
+ expect(await ssr.reloaded()).toBe(false);
198
+ }, 90_000);
199
+
200
+ test("a client entry that also exports a non-component patches in place, keeping state", async () => {
201
+ await open();
202
+ await ssr.evaluate(() => document.querySelector<HTMLButtonElement>('[data-e2e="bump"]')?.click());
203
+ await textIs('[data-e2e="count"]', "1");
204
+ await ssr.editSource(
205
+ probeFile,
206
+ (source) => `${markProbe(source)}\nexport const registryProbeVersion = 1;\n`,
207
+ async () => {
208
+ await probeMarked(true);
209
+ expect((await snapshot()).count).toBe("1");
210
+ },
211
+ );
212
+ const restoredAt = Date.now();
213
+ await probeMarked(false);
214
+ //? Dropping an export changes what the server holds, so an RSC refresh follows; leaving mid-fetch hard-navigates.
215
+ await ssr.waitFor(
216
+ (since: number) =>
217
+ (
218
+ (window as unknown as { __AKAN_HMR_TRACES__?: { kind: string; receivedAt: number }[] }).__AKAN_HMR_TRACES__ ??
219
+ []
220
+ ).some((entry) => entry.kind === "rsc-refresh" && entry.receivedAt >= since),
221
+ { args: [restoredAt], timeout: 20_000 },
222
+ );
223
+ expect(await ssr.reloaded()).toBe(false);
224
+ }, 90_000);
225
+
226
+ test("a page loaded right after a save renders the saved client code, so hydration matches", async () => {
227
+ await open();
228
+ await ssr.editSource(probeFile, markProbe, async () => {
229
+ //? The patch reaches the tab after the backend took the save's invalidation, which dropped the stale entries.
230
+ await probeMarked(true);
231
+ const html = await fetchPage();
232
+ expect(html).toContain('data-e2e-hot="1"');
233
+ await ssr.open(REGISTRY, { csr: false });
234
+ await probeMarked(true);
235
+ });
236
+ await probeMarked(false);
237
+ }, 90_000);
238
+
239
+ test("a save landing while the page's route builds is in the page that build answers", async () => {
240
+ await open();
241
+ const original = await Bun.file(probeFile).text();
242
+ try {
243
+ //? A client entry's save drops the route and refreshes no tab, so the request below builds the route again.
244
+ await Bun.write(probeFile, markProbe(original, "1"));
245
+ await probeHot("1");
246
+ const mark = await ssr.serverLogMark();
247
+ const html = fetchPage();
248
+ await Bun.sleep(40);
249
+ await Bun.write(probeFile, markProbe(original, "2"));
250
+ expect(await html).toContain('data-e2e-hot="2"');
251
+ expect(await ssr.serverLogSince(mark)).toMatch(/stale build ignored routeId=\/:lang\/e2e\/registry/);
252
+ } finally {
253
+ await Bun.write(probeFile, original);
254
+ }
255
+ await probeMarked(false);
256
+ }, 90_000);
257
+
258
+ test("saves to another page's component while a route builds leave that build standing", async () => {
259
+ await open();
260
+ const original = await Bun.file(probeFile).text();
261
+ const mark = await ssr.serverLogMark();
262
+ let answered = false;
263
+ const html = fetchPage("/lab/buttons").finally(() => {
264
+ answered = true;
265
+ });
266
+ try {
267
+ for (let save = 1; !answered && save <= 20; save += 1) {
268
+ await Bun.write(probeFile, markProbe(original, String(save)));
269
+ await Bun.sleep(300);
270
+ }
271
+ expect(await html).not.toContain("React Client Manifest");
272
+ } finally {
273
+ await Bun.write(probeFile, original);
274
+ }
275
+ const log = await ssr.serverLogSince(mark);
276
+ expect(log.match(/build done routeId=\/:lang\/lab\/buttons/g) ?? []).toHaveLength(1);
277
+ expect(log).not.toMatch(/stale build ignored routeId=\/:lang\/lab\/buttons/);
278
+ await probeMarked(false);
279
+ }, 90_000);
280
+
281
+ test("a page opened after several patches hydrates from the registry that holds them", async () => {
282
+ await open();
283
+ for (let round = 0; round < 3; round += 1) {
284
+ await ssr.editSource(probeFile, markProbe, async () => {
285
+ await probeMarked(true);
286
+ });
287
+ await probeMarked(false);
288
+ }
289
+ await open();
290
+ await ssr.evaluate(() => document.querySelector<HTMLButtonElement>('[data-e2e="bump"]')?.click());
291
+ await textIs('[data-e2e="count"]', "1");
292
+ }, 120_000);
293
+
294
+ test("a server component whose missing import is created afterwards shows it without a reload", async () => {
295
+ await open();
296
+ const extraFile = uiFile("registryExtra.ts");
297
+ try {
298
+ await ssr.editSource(
299
+ serverPartFile,
300
+ (source) =>
301
+ `import { registryExtraText } from "./registryExtra";\n${source.replace("part-0", "{registryExtraText}")}`,
302
+ async () => {
303
+ await ssr.waitFor(() => document.querySelector(".__akan_hmr_overlay[data-status=error]") !== null, {
304
+ timeout: 20_000,
305
+ });
306
+ await Bun.write(extraFile, 'export const registryExtraText = "extra-1";\n');
307
+ await textIs('[data-e2e="server-part"]', "extra-1");
308
+ },
309
+ );
310
+ } finally {
311
+ await rm(extraFile, { force: true });
312
+ }
313
+ await textIs('[data-e2e="server-part"]', "part-0");
314
+ expect(await ssr.reloaded()).toBe(false);
315
+ }, 120_000);
316
+
317
+ test("a client component added in the session renders through the registry once the server names it", async () => {
318
+ await open();
319
+ const newFile = uiFile("RegistryNew.tsx");
320
+ try {
321
+ await Bun.write(
322
+ newFile,
323
+ [
324
+ '"use client";',
325
+ 'import { useState } from "react";',
326
+ "",
327
+ "interface RegistryNewProps {",
328
+ " className?: string;",
329
+ "}",
330
+ "export const RegistryNew = ({ className }: RegistryNewProps) => {",
331
+ ' const [value] = useState("new-0");',
332
+ " return (",
333
+ ' <output className={className} data-e2e="registry-new">',
334
+ " {value}",
335
+ " </output>",
336
+ " );",
337
+ "};",
338
+ "",
339
+ ].join("\n"),
340
+ );
341
+ await ssr.editSource(
342
+ pageFile,
343
+ (source) =>
344
+ source
345
+ .replace("RegistryServerPart }", "RegistryServerPart, RegistryNew }")
346
+ .replace("<RegistryLazy />", "<RegistryLazy />\n <RegistryNew />"),
347
+ async () => {
348
+ await textIs('[data-e2e="registry-new"]', "new-0");
349
+ },
350
+ );
351
+ } finally {
352
+ await rm(newFile, { force: true });
353
+ }
354
+ expect(await ssr.reloaded()).toBe(false);
355
+ }, 120_000);
356
+
357
+ const pageRecovered = async () =>
358
+ await ssr.waitFor(
359
+ () => {
360
+ const state = (window as unknown as RegistryWindow).__akan?.inspect();
361
+ return !!state?.started && !state.failed && document.querySelector('[data-e2e="lazy"]') !== null;
362
+ },
363
+ { timeout: 30_000 },
364
+ );
365
+
366
+ test("a store error only the browser hits leaves the page to reload onto the save that fixes it", async () => {
367
+ await open();
368
+ await ssr.editSource(
369
+ storeFile,
370
+ (source) => `${source}\nif (typeof window !== "undefined") throw new Error("e2e store failure");\n`,
371
+ async () => {
372
+ //? The patch re-runs the store and reloads; the reloaded page's registry fails to start on it.
373
+ await ssr.waitFor(() => (window as unknown as RegistryWindow).__akan?.inspect().failed === true, {
374
+ timeout: 30_000,
375
+ });
376
+ await ssr.evaluate(() => {
377
+ (window as unknown as RegistryWindow).__akanE2eBroken = true;
378
+ });
379
+ },
380
+ );
381
+ await pageRecovered();
382
+ expect(await ssr.evaluate(() => (window as unknown as RegistryWindow).__akanE2eBroken ?? false)).toBe(false);
383
+ await ssr.evaluate(() => document.querySelector<HTMLButtonElement>('[data-e2e="bump"]')?.click());
384
+ await textIs('[data-e2e="count"]', "1");
385
+ }, 120_000);
386
+
387
+ test("a server error page reloads once the save that fixes it lands", async () => {
388
+ await open();
389
+ await ssr.editSource(
390
+ storeFile,
391
+ (source) => `${source}\nthrow new Error("e2e store failure");\n`,
392
+ async () => {
393
+ await ssr.waitFor(() => document.title.startsWith("500"), { timeout: 20_000 });
394
+ },
395
+ );
396
+ await pageRecovered();
397
+ }, 120_000);
398
+
399
+ test("a constant both copies render reloads the tab once, onto the server output it changed", async () => {
400
+ await open();
401
+ const bootLog = async () =>
402
+ await ssr.evaluate(
403
+ (prefix: string) => (window.name.startsWith(prefix) ? JSON.parse(window.name.slice(prefix.length)) : null),
404
+ bootLogPrefix,
405
+ );
406
+ await ssr.evaluate((prefix: string) => {
407
+ window.name = `${prefix}${JSON.stringify({ boots: 0, values: [] })}`;
408
+ }, bootLogPrefix);
409
+ try {
410
+ await ssr.editSource(
411
+ valueFile,
412
+ (source) => source.replace("value-0", "value-1"),
413
+ async () => {
414
+ for (const where of ["server", "client"])
415
+ await ssr.waitFor(
416
+ (target: string) =>
417
+ document.querySelector(`[data-e2e="label-${target}"]`)?.getAttribute("data-e2e-value") === "value-1",
418
+ { args: [where], timeout: 30_000 },
419
+ );
420
+ expect(await fetchPage()).toContain('data-e2e-value="value-1"');
421
+ //? A reload sent before the save's pages build would boot on value-0 and need the refresh after it.
422
+ await Bun.sleep(3_000);
423
+ expect(await bootLog()).toEqual({ boots: 1, values: ["value-1"] });
424
+ },
425
+ );
426
+ } finally {
427
+ await ssr.evaluate(() => {
428
+ window.name = "";
429
+ });
430
+ }
431
+ for (const where of ["server", "client"])
432
+ await ssr.waitFor(
433
+ (target: string) =>
434
+ document.querySelector(`[data-e2e="label-${target}"]`)?.getAttribute("data-e2e-value") === "value-0",
435
+ { args: [where], timeout: 30_000 },
436
+ );
437
+ await pageRecovered();
438
+ }, 150_000);
439
+
440
+ test("a common/ helper both copies render patches the client and refreshes the server across a backend restart", async () => {
441
+ await open();
442
+ await ssr.evaluate(() => document.querySelector<HTMLButtonElement>('[data-e2e="bump"]')?.click());
443
+ await textIs('[data-e2e="count"]', "1");
444
+ await ssr.editSource(
445
+ sharedFile,
446
+ (source) => source.replace("shared-0", "shared-1"),
447
+ async () => {
448
+ await textIs('[data-e2e="shared-client"]', "shared-1");
449
+ await textIs('[data-e2e="shared-server"]', "shared-1");
450
+ },
451
+ );
452
+ await textIs('[data-e2e="shared-client"]', "shared-0");
453
+ await textIs('[data-e2e="shared-server"]', "shared-0");
454
+ expect(await ssr.evaluate(() => document.querySelector('[data-e2e="count"]')?.textContent)).toBe("1");
455
+ expect(await ssr.reloaded()).toBe(false);
456
+ }, 150_000);
457
+
458
+ const fetchHas = async (endpoint: string) =>
459
+ await ssr
460
+ .evaluate((name: string) => {
461
+ const runtime = (globalThis as unknown as Record<symbol, { runtime?: { fetch?: Record<string, unknown> } }>)[
462
+ Symbol.for("akanjs.client.runtime")
463
+ ];
464
+ return typeof runtime?.runtime?.fetch?.[name] === "function";
465
+ }, endpoint)
466
+ .catch(() => false);
467
+ //? A new tab, reopened until one passes: after a metadata save's restart the dev server serves the registry before
468
+ //? until its builder rebuilds it, and a restarting backend refuses connections for a moment.
469
+ const reopenUntil = async (check: () => Promise<boolean>, timeout = 120_000) => {
470
+ const deadline = Date.now() + timeout;
471
+ while (!(await ssr.open(REGISTRY, { csr: false }).then(check, () => false))) {
472
+ if (Date.now() > deadline) throw new Error("[ssr-registry-e2e] no page opened within the wait passed the check");
473
+ await Bun.sleep(1_000);
474
+ }
475
+ };
476
+
477
+ const addEndpoint = (source: string) =>
478
+ source.replace(
479
+ " benchPing: query(",
480
+ ' e2eRegistryPing: query(String, { guards: [Public], mcp: false }).exec(() => "pong"),\n benchPing: query(',
481
+ );
482
+
483
+ test("a signal save restarts the builder, and the open tab and a new one both get the new endpoint", async () => {
484
+ await open();
485
+ const orphans = sampleOrphanWorkers(ssr.serverGroup);
486
+ //? The component save first keeps the slow lane busy, so the restart lands while a batch is in flight.
487
+ await ssr.editSource(probeFile, markProbe, async () => {
488
+ await Bun.sleep(200);
489
+ await ssr.editSource(signalFile, addEndpoint, async () => {
490
+ //? Not reopened: the rebuilt registry's reload, or hello's epoch once the backend is back, moves it on.
491
+ await ssr.waitFor(
492
+ () =>
493
+ typeof (globalThis as unknown as Record<symbol, { runtime?: { fetch?: Record<string, unknown> } }>)[
494
+ Symbol.for("akanjs.client.runtime")
495
+ ]?.runtime?.fetch?.e2eRegistryPing === "function",
496
+ { timeout: 120_000 },
497
+ );
498
+ await reopenUntil(async () => await fetchHas("e2eRegistryPing"));
499
+ });
500
+ });
501
+ await reopenUntil(async () => !(await fetchHas("e2eRegistryPing")) && (await fetchHas("benchPing")));
502
+ expect(await orphans()).toEqual([]);
503
+ }, 300_000);
504
+
505
+ test("a whole build that fails on a broken client module keeps the registry, and the fix brings the endpoint", async () => {
506
+ await open();
507
+ const { epoch } = (await Bun.file(registryManifest).json()) as { epoch: number };
508
+ await ssr.editSource(
509
+ contextFile,
510
+ (source) => `${source}\nexport const registryBroken = ;\n`,
511
+ async () => {
512
+ await ssr.waitFor(() => document.querySelector(".__akan_hmr_overlay[data-status=error]") !== null, {
513
+ timeout: 20_000,
514
+ });
515
+ const mark = await ssr.serverLogMark();
516
+ await ssr.editSource(signalFile, addEndpoint, async () => {
517
+ //? The replacement builder's whole build fails on the broken module, and says so before anything is written.
518
+ await waitForServerLog(mark, /ssr-registry boot build failed/);
519
+ expect(((await Bun.file(registryManifest).json()) as { epoch: number }).epoch).toBe(epoch);
520
+ await Bun.write(
521
+ contextFile,
522
+ (await Bun.file(contextFile).text()).replace("\nexport const registryBroken = ;\n", ""),
523
+ );
524
+ await reopenUntil(async () => await fetchHas("e2eRegistryPing"));
525
+ });
526
+ },
527
+ );
528
+ await reopenUntil(async () => !(await fetchHas("e2eRegistryPing")) && (await fetchHas("benchPing")));
529
+ }, 300_000);
530
+
531
+ test("a server-only save after server and client edits keeps rendering the server edit", async () => {
532
+ await open();
533
+ await ssr.editSource(
534
+ serverPartFile,
535
+ (source) => source.replace("part-0", "part-view"),
536
+ async () => {
537
+ await textIs('[data-e2e="server-part"]', "part-view");
538
+ await ssr.editSource(probeFile, markProbe, async () => {
539
+ await probeMarked(true);
540
+ const mark = await ssr.serverLogMark();
541
+ //? A service restarts the backend alone: its RSC worker boots the boot build's bundle, and only the build
542
+ //? replayed to it (the probe's, which changed nothing the server renders) says which bundle is current.
543
+ await ssr.editSource(
544
+ serviceFile,
545
+ (source) => `${source}\n`,
546
+ async () => {
547
+ await waitForServerLog(mark, /\[backend-reload\] restarting backend[\s\S]*backend ready pid=\d+/);
548
+ const deadline = Date.now() + 20_000;
549
+ while (!(await fetchPage().catch(() => "")).includes("part-view")) {
550
+ if (Date.now() > deadline) throw new Error("[ssr-registry-e2e] the restarted backend lost the edit");
551
+ await Bun.sleep(250);
552
+ }
553
+ await textIs('[data-e2e="server-part"]', "part-view");
554
+ },
555
+ );
556
+ });
557
+ },
558
+ );
559
+ await textIs('[data-e2e="server-part"]', "part-0");
560
+ await probeMarked(false);
561
+ }, 180_000);
562
+
563
+ test("a client module moved into a folder keeps a new tab hydrating, and so does moving it back", async () => {
564
+ await open();
565
+ const original = await Bun.file(labelFile).text();
566
+ const movedFile = uiFile("RegistryLabel/index.tsx");
567
+ //? A tab that stopped hydrating shows the label but never counts: a new document is tried until one does. Within
568
+ //? the save's own patch and pages build, not whenever a later build happens to heal the registry.
569
+ const hydratesWith = async (label: string) =>
570
+ await reopenUntil(async () => {
571
+ const hydrated = async () => {
572
+ await textIs('[data-e2e="label-client"]', label);
573
+ await ssr.evaluate(() => document.querySelector<HTMLButtonElement>('[data-e2e="bump"]')?.click());
574
+ await textIs('[data-e2e="count"]', "1");
575
+ };
576
+ return await hydrated().then(
577
+ () => true,
578
+ () => false,
579
+ );
580
+ }, 15_000);
581
+ try {
582
+ await rm(labelFile);
583
+ await Bun.write(
584
+ movedFile,
585
+ original.replace("label-0", "label-moved").replace('from "../common/', 'from "../../common/'),
586
+ );
587
+ await hydratesWith("label-moved");
588
+ } finally {
589
+ await rm(path.dirname(movedFile), { recursive: true, force: true });
590
+ await Bun.write(labelFile, original);
591
+ }
592
+ await hydratesWith("label-0");
593
+ }, 180_000);
594
+
595
+ test("a tab that reconnects to a restarted dev host before its registry exists reloads onto the new one", async () => {
596
+ await open();
597
+ //? A config save restarts the dev host, which clears `.akan`: the next registry starts over from generation 1,
598
+ //? and the boot build is held back (AKAN_DEV_SSR_ARM_DELAY_MS) so the tab's hello comes before it.
599
+ await ssr.editSource(
600
+ configFile,
601
+ (source) => `${source}\n`,
602
+ async () => {
603
+ const deadline = Date.now() + 90_000;
604
+ while (!(await ssr.reloaded())) {
605
+ if (Date.now() > deadline) throw new Error("[ssr-registry-e2e] the tab never reloaded after the restart");
606
+ await Bun.sleep(250);
607
+ }
608
+ await ssr.waitFor(
609
+ () => {
610
+ const w = window as unknown as RegistryWindow & { __AKAN_RSC_REFRESH__?: unknown };
611
+ const state = w.__akan?.inspect();
612
+ return typeof w.__AKAN_RSC_REFRESH__ === "function" && !!state?.started && !state.failed;
613
+ },
614
+ { timeout: 90_000 },
615
+ );
616
+ await ssr.editSource(probeFile, markProbe, async () => {
617
+ await probeMarked(true);
618
+ });
619
+ },
620
+ );
621
+ await reopenUntil(async () => await fetchHas("benchPing"));
622
+ }, 300_000);
623
+
624
+ test("a build error shows the overlay, and the fix patches the page without a reload", async () => {
625
+ await open();
626
+ await ssr.editSource(
627
+ probeFile,
628
+ (source) => source.replace("return (", "return (<"),
629
+ async () => {
630
+ await ssr.waitFor(() => document.querySelector(".__akan_hmr_overlay[data-status=error]") !== null, {
631
+ timeout: 20_000,
632
+ });
633
+ },
634
+ );
635
+ await ssr.waitFor(() => document.querySelector(".__akan_hmr_overlay[data-status=error]") === null, {
636
+ timeout: 20_000,
637
+ });
638
+ await ssr.editSource(probeFile, markProbe, async () => {
639
+ await probeMarked(true);
640
+ });
641
+ await probeMarked(false);
642
+ expect(await ssr.reloaded()).toBe(false);
643
+ }, 120_000);
644
+ });
@@ -1,22 +1,14 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
4
2
  import path from "node:path";
5
3
  import { TypeScriptDependencyScanner } from "./dependencyScanner";
4
+ import { tempDirs, writeText } from "./testHelpers";
6
5
  import type { PackageJson } from "./types";
7
6
 
8
- const tempRoots: string[] = [];
9
-
10
- const makeTempRoot = async () => {
11
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-depscan-"));
12
- tempRoots.push(root);
13
- return root;
14
- };
7
+ const makeTempRoot = tempDirs("akan-devkit-depscan-");
15
8
 
16
9
  const write = async (root: string, relPath: string, content: string) => {
17
10
  const filePath = path.join(root, relPath);
18
- await mkdir(path.dirname(filePath), { recursive: true });
19
- await writeFile(filePath, content);
11
+ await writeText(filePath, content);
20
12
  return filePath;
21
13
  };
22
14
 
@@ -27,10 +19,6 @@ const scannerFor = async (root: string, dependencies: Record<string, string> = {
27
19
  rootPackageJson: { dependencies } as unknown as PackageJson,
28
20
  });
29
21
 
30
- afterEach(async () => {
31
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
32
- });
33
-
34
22
  describe("TypeScriptDependencyScanner — getPackageBuildDependencies", () => {
35
23
  test("collects imports from ordinary source files", async () => {
36
24
  const root = await makeTempRoot();