@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
@@ -1,61 +1,56 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
3
+ import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
2
4
  import type { App } from "../commandDecorators";
3
5
  import { BarrelAnalyzer } from "../transforms/barrelAnalyzer";
4
6
  import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
5
7
  import type { AkanConfig, ClientEntryDiscovery, ScannedImport } from "./clientBuildTypes";
6
8
 
7
9
  const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
8
- const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
9
10
  const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
10
11
  const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
11
- // File extensions whose imports can't produce React client components and that
12
- // the bundler handles via dedicated loaders. Skipping them avoids resolver
13
- // noise (Bun's scanImports surfaces CSS / asset imports too).
12
+ // Bun's scanImports also surfaces CSS and asset imports, none of which can be a client component.
14
13
  const NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
15
14
  type PackageResolver = Awaited<ReturnType<typeof createTsconfigPackageResolver>>;
16
15
 
17
- /**
18
- * Everything the traversal ever asks of a file, which is why its text is not kept.
19
- *
20
- * `imports` is empty for a client entry: the walk stops there, so they are never scanned.
21
- */
22
16
  interface FileFacts {
23
17
  isClientEntry: boolean;
24
18
  imports: ScannedImport[];
25
19
  }
26
20
 
21
+ interface Lookup<T> {
22
+ epoch: number;
23
+ promise: Promise<T>;
24
+ found?: boolean;
25
+ }
26
+
27
+ interface DiscoveryWalk {
28
+ visiting: Set<string>;
29
+ //? Walked below a file the walk was already inside (an import cycle): their result lacks what that file reaches.
30
+ partial: Set<string>;
31
+ //? Every file's result within this walk, partial ones included: the file the cycle went back to is an ancestor here
32
+ //? and brings the rest itself, and re-walking each path through a dense cycle grows exponentially.
33
+ memo: Map<string, Set<string>>;
34
+ }
35
+
27
36
  const shouldSkipNodeModule = (absPath: string) => NODE_MODULES_RE.test(absPath) && !AKANJS_NODE_MODULE_RE.test(absPath);
28
37
 
29
- /**
30
- * Graph-based `"use client"` discovery, seeded from an explicit file list.
31
- *
32
- * Walks imports — including dynamic `import()` — while flattening barrel
33
- * specifiers through the same `BarrelAnalyzer` the runtime plugin uses, so
34
- * the traversal matches the module graph the bundler will actually see.
35
- */
38
+ /** `"use client"` discovery over the import graph (dynamic imports included), flattening barrels as the bundler does. */
36
39
  export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
37
40
  #akanConfig: Pick<AkanConfig, "barrelImports">;
38
41
  #resolvePackage: PackageResolver;
39
42
  #analyzer: BarrelAnalyzer;
40
43
  #tsTranspiler = new Bun.Transpiler({ loader: "tsx" });
41
- #fileExistsCache = new Map<string, Promise<boolean>>();
42
- /**
43
- * Derived facts per file, never the source text.
44
- *
45
- * This instance lives as long as the builder process does — it is rebuilt only when the akan config
46
- * changes — so caching whole source texts (and a second, barrel-rewritten copy of each) meant the
47
- * watcher held every source file it had ever walked for the whole dev session. Nothing downstream
48
- * wanted the text: the walk needs one boolean and one import list per file, and both were already
49
- * cached separately under the same key.
50
- */
44
+ #fileExistsCache = new Map<string, Lookup<boolean>>();
45
+ // Facts, never source text: this instance lives as long as the builder process, so texts would pin every file walked.
51
46
  #factsCache = new Map<string, Promise<FileFacts | null>>();
52
- #resolvedFileCache = new Map<string, Promise<string | null>>();
53
- #resolvedSpecifierCache = new Map<string, Promise<string | null>>();
47
+ #resolvedFileCache = new Map<string, Lookup<string | null>>();
48
+ #resolvedSpecifierCache = new Map<string, Lookup<string | null>>();
54
49
  #reachableEntriesCache = new Map<string, Set<string>>();
55
- /** Keys of the three caches above whose answer was "not there" — see `#forgetMissing`. */
56
- #missingFiles = new Set<string>();
57
- #unresolvedPaths = new Set<string>();
58
- #unresolvedSpecifiers = new Set<string>();
50
+ #readFiles = new Set<string>();
51
+ //? Bumped by every invalidation: a walk that began before one computed from facts it may have read stale, so it
52
+ //? answers its caller but caches nothing (a registry check walks this instance beside the slow lane's route builds).
53
+ #epoch = 0;
59
54
 
60
55
  constructor(akanConfig: Pick<AkanConfig, "barrelImports">, resolvePackage: PackageResolver) {
61
56
  this.#akanConfig = akanConfig;
@@ -70,61 +65,63 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
70
65
  async discover(seeds: string[]): Promise<string[]> {
71
66
  const entries = new Set<string>();
72
67
  for (const seed of seeds) {
73
- for (const entry of await this.#discoverFromFile(seed, new Set())) entries.add(entry);
68
+ const walk: DiscoveryWalk = { visiting: new Set(), partial: new Set(), memo: new Map() };
69
+ for (const entry of await this.#discoverFromFile(seed, walk)) entries.add(entry);
74
70
  }
75
71
  return Array.from(entries).sort();
76
72
  }
77
73
 
78
74
  invalidate(files: string[]): void {
75
+ if (files.length === 0) return;
76
+ let moved = false;
79
77
  for (const file of files) {
80
78
  const absPath = path.resolve(file);
79
+ const exists = fs.existsSync(absPath);
80
+ if (exists !== this.#readFiles.has(absPath)) moved = true;
81
+ if (!exists) this.#readFiles.delete(absPath);
81
82
  this.#factsCache.delete(absPath);
82
83
  this.#fileExistsCache.delete(absPath);
83
- this.#reachableEntriesCache.delete(absPath);
84
84
  }
85
- if (files.length === 0) return;
86
- // Parent files cache the transitive result of their imports, so a changed
87
- // child can affect any reachable-entry cache above it.
85
+ this.#epoch += 1;
86
+ // Reachable-entry results are transitive, so a changed child invalidates every ancestor.
88
87
  this.#reachableEntriesCache.clear();
89
- this.#forgetMissing();
88
+ //? A file gone, or one never read, can leave a found resolution on its old target (`Chart.tsx` to
89
+ //? `Chart/index.tsx`), so a move drops them all; a save of a file already read keeps them (a walk re-resolving
90
+ //? every package cost 3x). Misses lapse with the epoch on their own (#lookup).
91
+ if (!moved) return;
92
+ this.#fileExistsCache.clear();
93
+ this.#resolvedFileCache.clear();
94
+ this.#resolvedSpecifierCache.clear();
90
95
  }
91
96
 
92
- /**
93
- * Drop every "there is no such file" answer, because a batch may be what created it.
94
- *
95
- * These caches are keyed by extension-less path and by `dir\0specifier`, neither of which maps back
96
- * to the path that just appeared, so a negative recorded before a module existed is unreachable any
97
- * other way — and this instance lives as long as the builder process. Positive answers are kept:
98
- * they are keyed by a real path, which arrives in `files` when it changes or goes away.
99
- */
100
- #forgetMissing(): void {
101
- for (const key of this.#missingFiles) this.#fileExistsCache.delete(key);
102
- for (const key of this.#unresolvedPaths) this.#resolvedFileCache.delete(key);
103
- for (const key of this.#unresolvedSpecifiers) this.#resolvedSpecifierCache.delete(key);
104
- this.#missingFiles.clear();
105
- this.#unresolvedPaths.clear();
106
- this.#unresolvedSpecifiers.clear();
97
+ //? A found answer outlives later epochs; a miss, or a lookup still in flight, answers only walks of its own epoch, so
98
+ //? a file created meanwhile is looked up again.
99
+ #lookup<T>(cache: Map<string, Lookup<T>>, key: string, run: () => Promise<T>, isFound: (value: T) => boolean) {
100
+ const cached = cache.get(key);
101
+ if (cached && (cached.found || cached.epoch === this.#epoch)) return cached.promise;
102
+ const entry: Lookup<T> = { epoch: this.#epoch, promise: run() };
103
+ entry.promise.then(
104
+ (value) => {
105
+ entry.found = isFound(value);
106
+ },
107
+ () => {
108
+ entry.found = false;
109
+ },
110
+ );
111
+ cache.set(key, entry);
112
+ return entry.promise;
107
113
  }
108
114
 
109
115
  async #fileExists(p: string): Promise<boolean> {
110
116
  const absPath = path.resolve(p);
111
- let cached = this.#fileExistsCache.get(absPath);
112
- if (!cached) {
113
- cached = Bun.file(absPath)
114
- .exists()
115
- .then((exists) => {
116
- if (!exists) this.#missingFiles.add(absPath);
117
- return exists;
118
- });
119
- this.#fileExistsCache.set(absPath, cached);
120
- }
121
- return cached;
117
+ return await this.#lookup(
118
+ this.#fileExistsCache,
119
+ absPath,
120
+ () => Bun.file(absPath).exists(),
121
+ (exists) => exists,
122
+ );
122
123
  }
123
124
 
124
- /**
125
- * Read a file once and keep only what the traversal asks of it. The text itself is dropped as soon
126
- * as the boolean and the import list are out of it — see `#factsCache`.
127
- */
128
125
  #facts(file: string): Promise<FileFacts | null> {
129
126
  const absPath = path.resolve(file);
130
127
  let cached = this.#factsCache.get(absPath);
@@ -134,7 +131,7 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
134
131
  .text()
135
132
  .catch(() => null);
136
133
  if (content === null) return null;
137
- // A client entry ends the walk, so its imports are never needed.
134
+ this.#readFiles.add(absPath);
138
135
  if (USE_CLIENT_RE.test(content)) return { isClientEntry: true, imports: [] };
139
136
  return { isClientEntry: false, imports: this.#scanImports(await this.#rewrite(content)) };
140
137
  })();
@@ -162,57 +159,71 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
162
159
 
163
160
  async #resolveFileCandidate(absPathNoExt: string): Promise<string | null> {
164
161
  const cacheKey = path.resolve(absPathNoExt);
165
- let cached = this.#resolvedFileCache.get(cacheKey);
166
- if (cached) return cached;
167
- cached = (async () => {
168
- if (await this.#fileExists(cacheKey)) return cacheKey;
169
- for (const ext of SOURCE_EXTS) {
170
- const f = `${cacheKey}${ext}`;
171
- if (await this.#fileExists(f)) return f;
172
- }
173
- for (const ext of SOURCE_EXTS) {
174
- const f = path.join(cacheKey, `index${ext}`);
175
- if (await this.#fileExists(f)) return f;
176
- }
177
- this.#unresolvedPaths.add(cacheKey);
178
- return null;
179
- })();
180
- this.#resolvedFileCache.set(cacheKey, cached);
181
- return cached;
162
+ return await this.#lookup(
163
+ this.#resolvedFileCache,
164
+ cacheKey,
165
+ async () => {
166
+ if (await this.#fileExists(cacheKey)) return cacheKey;
167
+ for (const ext of SOURCE_EXTS) {
168
+ const f = `${cacheKey}${ext}`;
169
+ if (await this.#fileExists(f)) return f;
170
+ }
171
+ for (const ext of SOURCE_EXTS) {
172
+ const f = path.join(cacheKey, `index${ext}`);
173
+ if (await this.#fileExists(f)) return f;
174
+ }
175
+ return null;
176
+ },
177
+ (file) => file !== null,
178
+ );
182
179
  }
183
180
 
184
181
  async #resolveSpecifier(spec: string, importerDir: string): Promise<string | null> {
185
182
  const cacheKey = `${importerDir}\0${spec}`;
186
- let cached = this.#resolvedSpecifierCache.get(cacheKey);
187
- if (cached) return cached;
188
- cached = (async () => {
189
- if (spec.startsWith(".") || spec.startsWith("/")) {
190
- const abs = spec.startsWith("/") ? spec : path.resolve(importerDir, spec);
191
- return this.#resolveFileCandidate(abs);
192
- }
193
- const pkg = await this.#resolvePackage(spec);
194
- if (pkg) return pkg.entryFile;
195
- this.#unresolvedSpecifiers.add(cacheKey);
196
- return null;
197
- })();
198
- this.#resolvedSpecifierCache.set(cacheKey, cached);
199
- return cached;
183
+ return await this.#lookup(
184
+ this.#resolvedSpecifierCache,
185
+ cacheKey,
186
+ async () => {
187
+ if (spec.startsWith(".") || spec.startsWith("/")) {
188
+ const abs = spec.startsWith("/") ? spec : path.resolve(importerDir, spec);
189
+ return await this.#resolveFileCandidate(abs);
190
+ }
191
+ const pkg = await this.#resolvePackage(spec);
192
+ return pkg ? pkg.entryFile : null;
193
+ },
194
+ (file) => file !== null,
195
+ );
200
196
  }
201
197
 
202
- async #discoverFromFile(file: string, visiting: Set<string>): Promise<Set<string>> {
198
+ async #discoverFromFile(file: string, walk: DiscoveryWalk): Promise<Set<string>> {
199
+ const epoch = this.#epoch;
203
200
  const absPath = path.resolve(file);
204
201
  const cached = this.#reachableEntriesCache.get(absPath);
205
202
  if (cached) return new Set(cached);
206
- if (visiting.has(absPath) || shouldSkipNodeModule(absPath)) return new Set();
203
+ const memo = walk.memo.get(absPath);
204
+ if (memo) {
205
+ //? Reused from another path: whatever this path is walking now inherits its gap and stays out of the cache.
206
+ if (walk.partial.has(absPath)) for (const onPath of walk.visiting) walk.partial.add(onPath);
207
+ return new Set(memo);
208
+ }
209
+ if (walk.visiting.has(absPath)) {
210
+ let below = false;
211
+ for (const onPath of walk.visiting) {
212
+ if (below) walk.partial.add(onPath);
213
+ else below = onPath === absPath;
214
+ }
215
+ return new Set();
216
+ }
217
+ if (shouldSkipNodeModule(absPath)) return new Set();
207
218
 
208
- visiting.add(absPath);
219
+ walk.visiting.add(absPath);
209
220
  const entries = new Set<string>();
210
221
  const facts = await this.#facts(absPath);
211
- if (!facts) return this.#finishDiscovery(absPath, visiting, entries);
222
+ if (!facts) return this.#finishDiscovery(absPath, walk, entries, epoch);
212
223
 
213
224
  if (facts.isClientEntry) {
214
225
  entries.add(absPath);
215
- return this.#finishDiscovery(absPath, visiting, entries);
226
+ return this.#finishDiscovery(absPath, walk, entries, epoch);
216
227
  }
217
228
 
218
229
  const importerDir = path.dirname(absPath);
@@ -223,15 +234,16 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
223
234
  const resolved = await this.#resolveSpecifier(spec, importerDir);
224
235
  if (!resolved) continue;
225
236
  if (shouldSkipNodeModule(resolved)) continue;
226
- for (const entry of await this.#discoverFromFile(resolved, visiting)) entries.add(entry);
237
+ for (const entry of await this.#discoverFromFile(resolved, walk)) entries.add(entry);
227
238
  }
228
239
 
229
- return this.#finishDiscovery(absPath, visiting, entries);
240
+ return this.#finishDiscovery(absPath, walk, entries, epoch);
230
241
  }
231
242
 
232
- #finishDiscovery(absPath: string, visiting: Set<string>, entries: Set<string>): Set<string> {
233
- visiting.delete(absPath);
234
- this.#reachableEntriesCache.set(absPath, entries);
243
+ #finishDiscovery(absPath: string, walk: DiscoveryWalk, entries: Set<string>, epoch: number): Set<string> {
244
+ walk.visiting.delete(absPath);
245
+ walk.memo.set(absPath, entries);
246
+ if (epoch === this.#epoch && !walk.partial.has(absPath)) this.#reachableEntriesCache.set(absPath, entries);
235
247
  return new Set(entries);
236
248
  }
237
249
  }
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import type { BaseBuildArtifact } from "akanjs/server";
4
4
  import { type PageEntry, resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
5
5
  import type { App } from "../commandDecorators";
6
- import { getPageKeyBasePath } from "./cssCompiler";
6
+ import { bundleDefine } from "./bundleDefine";
7
+ import { CsrEntryFiles } from "./csrEntryFiles";
7
8
  import { PagesBundleBuilder } from "./pagesBundleBuilder";
8
9
  import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
9
10
 
@@ -61,10 +62,9 @@ export class CsrArtifactBuilder {
61
62
  splitting: false,
62
63
  minify: true,
63
64
  env: "AKAN_PUBLIC_*",
64
- define: this.#define(),
65
+ define: bundleDefine(this.#app, this.#command, "csr"),
65
66
  optimizeImports: akanConfig.optimizeImports,
66
- // The base artifact's compiled sheet is the only stylesheet, as it is for SSR: a raw `.css` reached through
67
- // the route graph is Tailwind source, and every root layout stylesheet in the graph would land in every HTML.
67
+ // A raw `.css` in the route graph is Tailwind source; the base artifact's compiled sheet is the only stylesheet.
68
68
  plugins: [PagesBundleBuilder.createCssStubPlugin()],
69
69
  });
70
70
 
@@ -80,10 +80,7 @@ export class CsrArtifactBuilder {
80
80
 
81
81
  /** The routes one basePath's HTML boots: its own plus every route outside any basePath, matching `bootCsr`. */
82
82
  static pageEntriesForBasePath(pageEntries: PageEntry[], basePath: string, basePaths: string[]): PageEntry[] {
83
- return pageEntries.filter((entry) => {
84
- const entryBasePath = getPageKeyBasePath(entry.key, basePaths);
85
- return entryBasePath === null || entryBasePath === basePath;
86
- });
83
+ return CsrEntryFiles.pageEntriesForBasePath(pageEntries, basePath, basePaths);
87
84
  }
88
85
 
89
86
  static htmlFilename(basePath: string): string {
@@ -91,7 +88,7 @@ export class CsrArtifactBuilder {
91
88
  }
92
89
 
93
90
  static entryFilename(basePath: string): string {
94
- return `${basePath || "index"}.csr.tsx`;
91
+ return CsrEntryFiles.entryFilename(basePath);
95
92
  }
96
93
 
97
94
  static basePathOfHtml(htmlPath: string): string {
@@ -118,17 +115,6 @@ export class CsrArtifactBuilder {
118
115
  return path.join(this.#generatedDir, filename);
119
116
  }
120
117
 
121
- #define(): Record<string, string> {
122
- const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
123
- return {
124
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
125
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
126
- ...Object.fromEntries(
127
- Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
128
- ),
129
- };
130
- }
131
-
132
118
  async #createEntryFile(basePath: string, pageEntries: PageEntry[]): Promise<readonly [string, string]> {
133
119
  return [
134
120
  this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
@@ -168,7 +154,7 @@ void bootCsr(pages);
168
154
 
169
155
  async #inlineCsrArtifacts(cssAssets: Record<string, CssAsset>): Promise<void> {
170
156
  const jsFiles = new Set<string>();
171
- for (const htmlPath of await this.#htmlOutputPaths()) {
157
+ for (const htmlPath of await this.#listOutputFiles((filePath) => filePath.endsWith(".html"))) {
172
158
  const htmlFile = Bun.file(htmlPath);
173
159
  if (!(await htmlFile.exists())) continue;
174
160
  const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
@@ -210,10 +196,6 @@ void bootCsr(pages);
210
196
  return { html: next, jsFiles };
211
197
  }
212
198
 
213
- async #htmlOutputPaths(): Promise<string[]> {
214
- return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
215
- }
216
-
217
199
  async #listOutputFiles(predicate: (filePath: string) => boolean): Promise<string[]> {
218
200
  const glob = new Bun.Glob("**/*");
219
201
  const files: string[] = [];
@@ -223,12 +205,8 @@ void bootCsr(pages);
223
205
  return files.sort();
224
206
  }
225
207
 
226
- /**
227
- * Bun's HTML bundler hoists the module script into `<head>`, so once that script is inline its source is part
228
- * of the text being searched — and a React bundle contains `<body` and `</head>` as strings. Positions are
229
- * taken on a copy with script, style and comment bodies blanked, and the snippet always lands after whatever
230
- * was injected before it: prepending would reverse the cascade order the caller chose.
231
- */
208
+ // Bun hoists the module script into <head> and a React bundle contains "</head>" as text, so positions come from a
209
+ // copy with script/style/comment bodies blanked. Never prepends: that would reverse the caller's cascade order.
232
210
  static injectBeforeHeadEnd(html: string, snippet: string): string {
233
211
  const scannable = CsrArtifactBuilder.blankEmbeddedContent(html);
234
212
  const headEnd = scannable.search(/<\/head\s*>/i);
@@ -0,0 +1,151 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ CSR_DEV_MANIFEST_FILE,
8
+ CSR_DEV_ROUTE_PREFIX,
9
+ type CsrDevManifest,
10
+ csrDevModuleFile,
11
+ } from "akanjs/server/hmr/csrDevManifest";
12
+ import { CsrDevArtifactWriter, type CsrDevArtifactWriterOptions } from "./csrDevArtifactWriter";
13
+ import type { CsrDevCompiledModule, CsrDevGraph } from "./csrDevTypes";
14
+
15
+ const dirs: string[] = [];
16
+ const module: CsrDevCompiledModule = {
17
+ id: "a.ts",
18
+ file: "/a.ts",
19
+ vendor: false,
20
+ factory: "function () {}",
21
+ helpers: null,
22
+ deps: [],
23
+ mtimeMs: 0,
24
+ hash: "",
25
+ };
26
+ const readModule = (dir: string) => readFileSync(path.join(dir, csrDevModuleFile("a.ts", ".js")), "utf8");
27
+ const makeWriter = async (
28
+ options: CsrDevArtifactWriterOptions = { routePrefix: CSR_DEV_ROUTE_PREFIX },
29
+ entries: Record<string, string> = {},
30
+ ) => {
31
+ const dir = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-writer-"));
32
+ dirs.push(dir);
33
+ const writer = new CsrDevArtifactWriter(dir, options);
34
+ const graph: CsrDevGraph = {
35
+ version: 1,
36
+ configKey: "key",
37
+ refresh: "refresh.js",
38
+ entries,
39
+ modules: { "a.ts": { vendor: false, mtimeMs: 0, hash: "", deps: [] } },
40
+ resolution: {},
41
+ pending: [],
42
+ };
43
+ await writer.writeModules([module]);
44
+ const read = () => ({
45
+ manifest: JSON.parse(readFileSync(path.join(dir, CSR_DEV_MANIFEST_FILE), "utf8")) as CsrDevManifest,
46
+ app: readFileSync(path.join(dir, "app.js"), "utf8"),
47
+ });
48
+ return { writer, graph, read, dir };
49
+ };
50
+
51
+ afterAll(async () => {
52
+ await Promise.all(dirs.map((dir) => rm(dir, { recursive: true, force: true })));
53
+ });
54
+
55
+ describe("CsrDevArtifactWriter", () => {
56
+ test("announces a patch before the module files, graph.json and app.js catch up", async () => {
57
+ const { writer, graph, read, dir } = await makeWriter();
58
+ await writer.writeApp(graph, { generation: 4, vendorFile: "v.js" });
59
+ const edited = { ...module, factory: "function () { edited(); }" };
60
+ let atAnnounce: (ReturnType<typeof read> & { graph: boolean; module: string }) | null = null;
61
+ await writer.commitPatch(
62
+ graph,
63
+ { version: 1, generation: 5, appGeneration: 4, vendorFile: "v.js", entries: {} },
64
+ () => {
65
+ atAnnounce = { ...read(), graph: existsSync(path.join(dir, "graph.json")), module: readModule(dir) };
66
+ },
67
+ { modules: [edited] },
68
+ );
69
+ expect(atAnnounce).toMatchObject({ manifest: { generation: 5, appGeneration: 4 }, graph: false });
70
+ expect((atAnnounce as { app: string } | null)?.app).toContain('"generation":4');
71
+ expect((atAnnounce as { module: string } | null)?.module).not.toContain("edited");
72
+ const after = read();
73
+ expect(after.manifest).toMatchObject({ generation: 5, appGeneration: 5 });
74
+ expect(after.app).toContain('"generation":5');
75
+ expect(after.app).toContain("edited()");
76
+ expect(existsSync(path.join(dir, "graph.json"))).toBe(true);
77
+ });
78
+
79
+ test("a module a package's browser field turns off is written empty, and a Node built-in still throws", async () => {
80
+ const { writer, graph, dir } = await makeWriter();
81
+ graph.modules["a.ts"] = { vendor: false, mtimeMs: 0, hash: "", deps: ["stub:empty:fs", "stub:path"] };
82
+ const vendor = readFileSync(path.join(dir, await writer.writeVendor(graph)), "utf8");
83
+ expect(vendor).toContain('__akan.define("stub:empty:fs", function () {});');
84
+ expect(vendor).toContain("[akan-csr] path is a Node built-in the browser does not have");
85
+ });
86
+
87
+ test("rewrites an app.js a crash left behind the manifest's generation", async () => {
88
+ const { writer, graph, read } = await makeWriter();
89
+ await writer.writeApp(graph, { generation: 5, vendorFile: "v.js" });
90
+ const behind: CsrDevManifest = { version: 1, generation: 6, appGeneration: 5, vendorFile: "v.js", entries: {} };
91
+ await writer.writeJson(CSR_DEV_MANIFEST_FILE, behind);
92
+ const healed = await writer.healApp(graph, behind);
93
+ expect(healed.appGeneration).toBe(6);
94
+ expect(read().manifest.appGeneration).toBe(6);
95
+ expect(read().app).toContain('"generation":6');
96
+ });
97
+
98
+ test("leaves an app.js that already holds the manifest's generation alone", async () => {
99
+ const { writer, graph } = await makeWriter();
100
+ const current: CsrDevManifest = { version: 1, generation: 3, vendorFile: "v.js", entries: {} };
101
+ expect(await writer.healApp(graph, current)).toBe(current);
102
+ });
103
+
104
+ test("a library app.js starts its bootstrap module, and a patch is announced under the writer's route", async () => {
105
+ const { writer, graph, dir } = await makeWriter(
106
+ { routePrefix: "/_akan/ssr-dev/", library: true },
107
+ { "": "boot.ts" },
108
+ );
109
+ await writer.writeApp(graph, { generation: 2, vendorFile: "v.js", epoch: 7 });
110
+ expect(readFileSync(path.join(dir, "app.js"), "utf8")).toContain(
111
+ '__akan.startLibrary({"generation":2,"refresh":"refresh.js","bootstrap":"boot.ts","vendorFile":"v.js","epoch":7});',
112
+ );
113
+ expect(await writer.writePatch(3, [module])).toBe("/_akan/ssr-dev/patch-3.js");
114
+ });
115
+
116
+ test("after a whole build, clears what the registry before it left and keeps what the build wrote", async () => {
117
+ const { writer, graph, dir } = await makeWriter();
118
+ const gone = { ...module, id: "gone.ts" };
119
+ await writer.writeModules([gone]);
120
+ await writer.writePatch(7, [module]);
121
+ await Bun.write(path.join(dir, "assets/old-abc.png"), "old");
122
+ await Bun.write(path.join(dir, ".patching"), "123");
123
+ await Bun.write(path.join(dir, "manifest.json.4242.tmp"), "{");
124
+ await Bun.write(path.join(dir, "assets/new-def.png"), "new");
125
+ await writer.pruneAfterFullBuild(graph, "vendor-a.js", {
126
+ generation: 8,
127
+ assets: [path.join(dir, "assets/new-def.png")],
128
+ });
129
+ expect(existsSync(path.join(dir, csrDevModuleFile("a.ts", ".js")))).toBe(true);
130
+ expect(existsSync(path.join(dir, csrDevModuleFile("gone.ts", ".js")))).toBe(false);
131
+ expect(existsSync(path.join(dir, "patch-7.js"))).toBe(false);
132
+ expect(existsSync(path.join(dir, "patch-7.js.layout.json"))).toBe(false);
133
+ expect(existsSync(path.join(dir, "assets/old-abc.png"))).toBe(false);
134
+ expect(existsSync(path.join(dir, "assets/new-def.png"))).toBe(true);
135
+ expect(existsSync(path.join(dir, ".patching"))).toBe(false);
136
+ expect(existsSync(path.join(dir, "manifest.json.4242.tmp"))).toBe(false);
137
+ });
138
+
139
+ test("a reload drops a patch file left under its generation, and a whole build is marked while it writes", async () => {
140
+ const { writer, dir } = await makeWriter();
141
+ await writer.writePatch(6, [module]);
142
+ await writer.forgetPatch(6);
143
+ expect(existsSync(path.join(dir, "patch-6.js"))).toBe(false);
144
+ expect(existsSync(path.join(dir, "patch-6.js.layout.json"))).toBe(false);
145
+ expect(await writer.isBuilding()).toBe(false);
146
+ await writer.markBuilding();
147
+ expect(await writer.isBuilding()).toBe(true);
148
+ await writer.clearBuilding();
149
+ expect(await writer.isBuilding()).toBe(false);
150
+ });
151
+ });