@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
@@ -0,0 +1,572 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import fs 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 { CSR_DEV_MANIFEST_FILE, type CsrDevManifest } from "akanjs/server/hmr/csrDevManifest";
7
+ import { isAkanRuntimeMetadataFile } from "akanjs/server/hmr/runtimeMetadataFile";
8
+ import type { App } from "../commandDecorators";
9
+ import { CsrDevArtifactWriter } from "./csrDevArtifactWriter";
10
+ import { CsrDevPatcher } from "./csrDevPatcher";
11
+ import { CsrDevPaths } from "./csrDevPaths";
12
+ import type { CsrDevContext, CsrDevGraph } from "./csrDevTypes";
13
+ import { DevRegistryBundler } from "./devRegistryBundler";
14
+
15
+ const dirs: string[] = [];
16
+ //? A rename that changes only the case is one only where the disk folds case (APFS, NTFS); ext4 keeps both names.
17
+ const foldsCase = (() => {
18
+ const probe = fs.mkdtempSync(path.join(os.tmpdir(), "akan-case-"));
19
+ try {
20
+ return fs.existsSync(path.join(path.dirname(probe), path.basename(probe).toUpperCase()));
21
+ } finally {
22
+ fs.rmSync(probe, { recursive: true, force: true });
23
+ }
24
+ })();
25
+ const context: CsrDevContext = {
26
+ pageEntries: [],
27
+ basePaths: [],
28
+ htmlBasePaths: [],
29
+ define: {},
30
+ optimizeImports: [],
31
+ configKey: "key",
32
+ refreshFile: null,
33
+ };
34
+
35
+ //? Only what an update touches before it compiles anything: a registry with no modules has nothing to patch.
36
+ const makeRegistry = async (metadata: string | undefined) => {
37
+ const dir = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-patcher-"));
38
+ dirs.push(dir);
39
+ const writer = new CsrDevArtifactWriter(dir, { routePrefix: "/_akan/ssr-dev/" });
40
+ const graph: CsrDevGraph = {
41
+ version: 1,
42
+ configKey: "key",
43
+ refresh: "refresh.js",
44
+ entries: {},
45
+ modules: {},
46
+ resolution: {},
47
+ pending: [],
48
+ ...(metadata ? { metadata } : {}),
49
+ };
50
+ const manifest: CsrDevManifest = { version: 1, generation: 4, appGeneration: 4, vendorFile: "v.js", entries: {} };
51
+ await writer.writeState(graph, manifest);
52
+ const stamp = { current: metadata ?? "" };
53
+ const bundler = {
54
+ context: async () => context,
55
+ writer,
56
+ paths: new CsrDevPaths(dir),
57
+ reloadsOnEntryChange: false,
58
+ isMetadataFile: isAkanRuntimeMetadataFile,
59
+ metadataFingerprint: async () => stamp.current,
60
+ writeEntries: async () => ({ files: {}, changed: [] }),
61
+ entryIds: () => ({}),
62
+ } as unknown as DevRegistryBundler;
63
+ return { bundler, stamp, dir };
64
+ };
65
+
66
+ afterAll(async () => {
67
+ await Promise.all(dirs.map((dir) => rm(dir, { recursive: true, force: true })));
68
+ });
69
+
70
+ describe("CsrDevPatcher", () => {
71
+ test("hands a registry read from disk back to a whole build once a signal save moved its metadata", async () => {
72
+ const { bundler, stamp } = await makeRegistry("before");
73
+ stamp.current = "after";
74
+ const result = await new CsrDevPatcher(bundler).update([]);
75
+ expect(result).toMatchObject({ kind: "delegate", generation: 5 });
76
+ expect(result.kind === "delegate" ? result.reason : "").toContain("metadata");
77
+ });
78
+
79
+ test("patches a registry whose metadata still matches, and one built before the stamp existed is rebuilt", async () => {
80
+ const matching = await makeRegistry("same");
81
+ expect(await new CsrDevPatcher(matching.bundler).update([])).toEqual({ kind: "unchanged" });
82
+ const unstamped = await makeRegistry(undefined);
83
+ unstamped.stamp.current = "now";
84
+ expect((await new CsrDevPatcher(unstamped.bundler).update([])).kind).toBe("delegate");
85
+ });
86
+
87
+ test("a resident patcher checks the stamp once, when it reads the registry", async () => {
88
+ const { bundler, stamp } = await makeRegistry("same");
89
+ const patcher = new CsrDevPatcher(bundler, { resident: true });
90
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
91
+ stamp.current = "moved";
92
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
93
+ patcher.forget();
94
+ expect((await patcher.update([])).kind).toBe("delegate");
95
+ });
96
+
97
+ test("a saved metadata file goes to a whole build even when the stamp matches", async () => {
98
+ const { bundler } = await makeRegistry("same");
99
+ const result = await new CsrDevPatcher(bundler).update(["/repo/apps/demo/lib/user/user.signal.ts"]);
100
+ expect(result).toMatchObject({ kind: "delegate", reason: "user.signal.ts changed" });
101
+ });
102
+
103
+ test("a whole build cut short between its writes goes to another whole build, not to a patch", async () => {
104
+ const { bundler, dir } = await makeRegistry("same");
105
+ await Bun.write(path.join(dir, ".building"), "123");
106
+ expect(await new CsrDevPatcher(bundler, { resident: true }).update([])).toMatchObject({
107
+ kind: "delegate",
108
+ reason: "the last whole build was cut short",
109
+ generation: 5,
110
+ });
111
+ });
112
+
113
+ //? Compiled for real: a move resolves through what is on disk now, which a resident process's resolver cache is not.
114
+ class MovedRegistry extends DevRegistryBundler {
115
+ readonly reloadsOnEntryChange = false;
116
+ readonly #entry: string;
117
+ constructor(root: string, entry: string) {
118
+ const app = {
119
+ workspace: { workspaceRoot: root },
120
+ cwdPath: path.join(root, "apps/demo"),
121
+ verbose: () => undefined,
122
+ };
123
+ super(app as unknown as App, { dirName: "registry", routePrefix: "/_akan/registry/", library: true });
124
+ this.#entry = entry;
125
+ }
126
+ async context() {
127
+ return context;
128
+ }
129
+ async writeEntries() {
130
+ return { files: { "": this.#entry }, changed: [] };
131
+ }
132
+ //? Roots beside the entries, as the SSR registry takes every client entry a route reaches.
133
+ extraRoots: string[] = [];
134
+ protected async rootFiles(entryFiles: string[]) {
135
+ return [...entryFiles, ...this.extraRoots];
136
+ }
137
+ override async metadataFingerprint() {
138
+ return "same";
139
+ }
140
+ wanted: Set<string> | null = null;
141
+ override async wantedRoots(files: string[], graph: CsrDevGraph) {
142
+ if (!this.wanted) return await super.wantedRoots(files, graph);
143
+ const wanted = this.wanted;
144
+ return new Set(files.filter((file) => wanted.has(file)));
145
+ }
146
+ }
147
+
148
+ const movedRegistry = async (files: Record<string, string>, { roots = [] }: { roots?: string[] } = {}) => {
149
+ const root = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-move-"));
150
+ dirs.push(root);
151
+ const file = (relative: string) => path.join(CsrDevPaths.realpath(root), "apps/demo", relative);
152
+ for (const [relative, text] of Object.entries(files)) await Bun.write(file(relative), text);
153
+ const bundler = new MovedRegistry(root, file("entry.ts"));
154
+ bundler.extraRoots = roots.map((relative) => file(relative));
155
+ expect(await bundler.update([])).toMatchObject({ generation: 1, reload: true, first: true });
156
+ const patcher = new CsrDevPatcher(bundler, { resident: true });
157
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
158
+ const app = async () => await Bun.file(path.join(bundler.outDir, "app.js")).text();
159
+ const changedIds = (result: Awaited<ReturnType<CsrDevPatcher["update"]>>) =>
160
+ result.kind === "update" ? result.update.changedIds.sort((a, b) => a.localeCompare(b)) : [];
161
+ return { bundler, patcher, file, app, changedIds };
162
+ };
163
+
164
+ test("a dependency moved into a folder or renamed recompiles its importer, so app.js defines the new file", async () => {
165
+ const root = await mkdtemp(path.join(os.tmpdir(), "akan-csr-dev-move-"));
166
+ dirs.push(root);
167
+ const file = (relative: string) => path.join(root, "apps/demo", relative);
168
+ await Bun.write(file("ui/Foo.tsx"), 'export const foo = () => "foo";\n');
169
+ await Bun.write(file("ui/util.ts"), 'export const util = () => "util";\n');
170
+ await Bun.write(
171
+ file("ui/Page.tsx"),
172
+ 'import { foo } from "./Foo";\nimport { util } from "./util";\nexport const page = () => foo() + util();\n',
173
+ );
174
+ await Bun.write(file("entry.ts"), 'import { page } from "./ui/Page";\nexport const run = page;\n');
175
+ const bundler = new MovedRegistry(root, file("entry.ts"));
176
+ expect(await bundler.update([])).toMatchObject({ generation: 1, reload: true, first: true });
177
+ const patcher = new CsrDevPatcher(bundler, { resident: true });
178
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
179
+
180
+ await rm(file("ui/Foo.tsx"));
181
+ await Bun.write(file("ui/Foo/index.tsx"), 'export const foo = () => "moved";\n');
182
+ await rm(file("ui/util.ts"));
183
+ await Bun.write(file("ui/util.tsx"), 'export const util = () => "renamed";\n');
184
+ const moved = await patcher.update([
185
+ file("ui/Foo.tsx"),
186
+ file("ui/Foo/index.tsx"),
187
+ file("ui/util.ts"),
188
+ file("ui/util.tsx"),
189
+ ]);
190
+
191
+ expect(moved.kind).toBe("update");
192
+ expect(moved.kind === "update" ? moved.update.changedIds.sort((a, b) => a.localeCompare(b)) : []).toEqual([
193
+ "apps/demo/ui/Foo/index.tsx",
194
+ "apps/demo/ui/Page.tsx",
195
+ "apps/demo/ui/util.tsx",
196
+ ]);
197
+ const app = await Bun.file(path.join(bundler.outDir, "app.js")).text();
198
+ expect(app).toContain('__akan.define("apps/demo/ui/Foo/index.tsx"');
199
+ expect(app).toContain('__akan.define("apps/demo/ui/util.tsx"');
200
+ expect(app).not.toContain('__akan.define("apps/demo/ui/Foo.tsx"');
201
+ expect(app).not.toContain('__akan.define("apps/demo/ui/util.ts"');
202
+ });
203
+
204
+ test.skipIf(!foldsCase)(
205
+ "a rename that changes only the case moves the module to its new id, and later saves leave it alone",
206
+ async () => {
207
+ const { patcher, file, app, changedIds } = await movedRegistry({
208
+ "ui/card.tsx": 'export const card = () => "v1";\n',
209
+ "ui/Page.tsx": 'import { card } from "./Card";\nexport const page = () => card();\n',
210
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
211
+ });
212
+ fs.renameSync(file("ui/card.tsx"), file("ui/Card.tsx"));
213
+ await Bun.write(file("ui/Page.tsx"), 'import { card } from "./Card";\nexport const page = () => card() + "!";\n');
214
+ const renamed = await patcher.update([file("ui/card.tsx"), file("ui/Card.tsx"), file("ui/Page.tsx")]);
215
+ expect(changedIds(renamed)).toEqual(["apps/demo/ui/Card.tsx", "apps/demo/ui/Page.tsx"]);
216
+ expect(await app()).toContain('__akan.define("apps/demo/ui/Card.tsx"');
217
+ expect(await app()).not.toContain('__akan.define("apps/demo/ui/card.tsx"');
218
+ expect(await patcher.update([])).toEqual({ kind: "unchanged" });
219
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
220
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
221
+ expect(await app()).toContain('"v2"');
222
+ },
223
+ );
224
+
225
+ test("a new root that fails to compile holds no later save, and leaves pending once no route reaches it", async () => {
226
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
227
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
228
+ "ui/Broken.tsx": 'import { gone } from "./missingHelper";\nexport const broken = () => gone;\n',
229
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
230
+ });
231
+ //? Named by a route, as a route build's check names the entries it asks for.
232
+ bundler.wanted = new Set([file("ui/Broken.tsx")]);
233
+ await expect(patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).rejects.toThrow();
234
+
235
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
236
+ const announced: string[][] = [];
237
+ await expect(
238
+ patcher.update([file("ui/Card.tsx")], { announce: (update) => announced.push(update.changedIds) }),
239
+ ).rejects.toThrow();
240
+ expect(announced).toEqual([["apps/demo/ui/Card.tsx"]]);
241
+
242
+ bundler.wanted = new Set();
243
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v3";\n');
244
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
245
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
246
+ expect(graph.pending).toEqual([]);
247
+ });
248
+
249
+ test("a pending root whose fix needs a whole-app build goes to a worker with it, still pending", async () => {
250
+ //? Written before the first build: Bun's resolver keeps a directory's listing, so a node_modules made later is missed.
251
+ const { bundler, patcher, file } = await movedRegistry({
252
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
253
+ "ui/Broken.tsx": 'import { gone } from "./missingHelper";\nexport const broken = () => gone;\n',
254
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
255
+ "../../node_modules/fake-pkg/package.json": '{"name":"fake-pkg","main":"index.js"}',
256
+ "../../node_modules/fake-pkg/index.js": "export const gone = 1;\n",
257
+ });
258
+ bundler.wanted = new Set([file("ui/Broken.tsx")]);
259
+ await expect(patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).rejects.toThrow();
260
+ await Bun.write(file("ui/Broken.tsx"), 'import { gone } from "fake-pkg";\nexport const broken = () => gone;\n');
261
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
262
+ const result = await patcher.update([file("ui/Card.tsx"), file("ui/Broken.tsx")]);
263
+ expect(result.kind).toBe("delegate");
264
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
265
+ expect(graph.pending).toContain("apps/demo/ui/Broken.tsx");
266
+ });
267
+
268
+ test("a broken file no module imports any more stops failing saves, even brought back after its importer moved on", async () => {
269
+ const { patcher, file, changedIds } = await movedRegistry({
270
+ "ui/X.tsx": "export const x = 1;\n",
271
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
272
+ "ui/Page.tsx":
273
+ 'import { x } from "./X";\nimport { card } from "./Card";\nexport const page = () => x + card();\n',
274
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
275
+ });
276
+ await rm(file("ui/X.tsx"));
277
+ await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
278
+ await Bun.write(file("ui/X.tsx"), "export const x = ;\n");
279
+ await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
280
+
281
+ await Bun.write(file("ui/Page.tsx"), 'import { card } from "./Card";\nexport const page = () => card();\n');
282
+ const announced: string[][] = [];
283
+ await patcher
284
+ .update([file("ui/Page.tsx")], { announce: (update) => announced.push(update.changedIds) })
285
+ .catch(() => undefined);
286
+ expect(announced[0]).toContain("apps/demo/ui/Page.tsx");
287
+
288
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
289
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
290
+ });
291
+
292
+ test("a broken module in the graph whose last importer drops it leaves the graph instead of failing every save", async () => {
293
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
294
+ "ui/X.tsx": "export const x = 1;\n",
295
+ "ui/C.tsx": 'export const C = () => "c1";\n',
296
+ "ui/I.tsx": 'import { x } from "./X";\nexport const I = () => x;\n',
297
+ "entry.ts": 'import { C } from "./ui/C";\nimport { I } from "./ui/I";\nexport const run = [C, I];\n',
298
+ });
299
+ await Bun.write(file("ui/X.tsx"), "export const x = ;\n");
300
+ await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
301
+ await Bun.write(file("ui/I.tsx"), 'import { x } from "./X";\nexport const I = () => x + 1;\n');
302
+ await expect(patcher.update([file("ui/I.tsx")])).rejects.toThrow();
303
+
304
+ await Bun.write(file("ui/I.tsx"), "export const I = () => 0;\n");
305
+ expect(changedIds(await patcher.update([file("ui/I.tsx")]))).toEqual(["apps/demo/ui/I.tsx"]);
306
+ expect(((await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph).pending).toEqual([]);
307
+ await Bun.write(file("ui/C.tsx"), 'export const C = () => "c2";\n');
308
+ expect(changedIds(await patcher.update([file("ui/C.tsx")]))).toEqual(["apps/demo/ui/C.tsx"]);
309
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
310
+ expect(graph.modules["apps/demo/ui/X.tsx"]).toBeUndefined();
311
+ expect(graph.pending).toEqual([]);
312
+ });
313
+
314
+ test("a broken module left behind by a detached importer, or in a cycle, leaves with everything unreachable", async () => {
315
+ for (const shape of ["detached", "cycle"] as const) {
316
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
317
+ "ui/X.tsx":
318
+ shape === "cycle" ? 'import { y } from "./Y";\nexport const x = () => y;\n' : "export const x = 1;\n",
319
+ "ui/Y.tsx": 'import { x } from "./X";\nexport const y = () => x;\n',
320
+ "ui/C.tsx": 'export const C = () => "c1";\n',
321
+ "ui/I.tsx": 'import { y } from "./Y";\nexport const I = () => y;\n',
322
+ "entry.ts": 'import { C } from "./ui/C";\nimport { I } from "./ui/I";\nexport const run = [C, I];\n',
323
+ });
324
+ await Bun.write(file("ui/X.tsx"), "export const x = ;\n");
325
+ await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
326
+ await Bun.write(file("ui/I.tsx"), "export const I = () => 0;\n");
327
+ expect(changedIds(await patcher.update([file("ui/I.tsx")]))).toEqual(["apps/demo/ui/I.tsx"]);
328
+ await Bun.write(file("ui/C.tsx"), 'export const C = () => "c2";\n');
329
+ expect(changedIds(await patcher.update([file("ui/C.tsx")]))).toEqual(["apps/demo/ui/C.tsx"]);
330
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
331
+ expect(Object.keys(graph.modules).sort()).toEqual([
332
+ "apps/demo/entry.ts",
333
+ "apps/demo/ui/C.tsx",
334
+ "apps/demo/ui/I.tsx",
335
+ ]);
336
+ }
337
+ });
338
+
339
+ test("a broken client entry no page names any more leaves the registry, though its last importer was a server file", async () => {
340
+ const { bundler, patcher, file, changedIds } = await movedRegistry(
341
+ {
342
+ "ui/X.tsx": 'export const X = () => "x";\n',
343
+ "ui/Y.tsx": 'export const Y = () => "y1";\n',
344
+ "entry.ts": "export {};\n",
345
+ },
346
+ { roots: ["ui/X.tsx", "ui/Y.tsx"] },
347
+ );
348
+ await Bun.write(file("ui/X.tsx"), "export const X = () => ;\n");
349
+ await expect(patcher.update([file("ui/X.tsx")])).rejects.toThrow();
350
+ bundler.extraRoots = [file("ui/Y.tsx")];
351
+ await Bun.write(file("page/P.tsx"), "export default () => null;\n");
352
+ expect(await patcher.update([file("page/P.tsx")])).toEqual({ kind: "unchanged" });
353
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
354
+ expect(graph.modules["apps/demo/ui/X.tsx"]).toBeUndefined();
355
+ expect(graph.pending).toEqual([]);
356
+ await Bun.write(file("ui/Y.tsx"), 'export const Y = () => "y2";\n');
357
+ expect(changedIds(await patcher.update([file("ui/Y.tsx")]))).toEqual(["apps/demo/ui/Y.tsx"]);
358
+ });
359
+
360
+ test("a require.resolve call weighs extensions as a require does", async () => {
361
+ const { bundler } = await movedRegistry({
362
+ "ui/pair/x.mjs": "export const x = 1;\n",
363
+ "ui/pair/x.js": "exports.x = 2;\n",
364
+ "ui/R.ts": 'export const where = require.resolve("./pair/x");\n',
365
+ "entry.ts": 'import { where } from "./ui/R";\nexport const run = where;\n',
366
+ });
367
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
368
+ expect(graph.resolution["apps/demo/ui/R.ts"]?.["./pair/x"]).toBe("apps/demo/ui/pair/x.js");
369
+ });
370
+
371
+ test("two routes' checks that both fail keep both roots pending", async () => {
372
+ const { bundler, patcher, file } = await movedRegistry({
373
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
374
+ "ui/E1.tsx": 'import { gone } from "./missing1";\nexport const e1 = () => gone;\n',
375
+ "ui/E4.tsx": 'import { gone } from "./missing4";\nexport const e4 = () => gone;\n',
376
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
377
+ });
378
+ bundler.wanted = new Set([file("ui/E1.tsx"), file("ui/E4.tsx")]);
379
+ await expect(patcher.update([], { roots: [file("ui/E1.tsx")], onlyRoots: true })).rejects.toThrow();
380
+ await expect(patcher.update([], { roots: [file("ui/E4.tsx")], onlyRoots: true })).rejects.toThrow();
381
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
382
+ expect(graph.pending.sort()).toEqual(["apps/demo/ui/E1.tsx", "apps/demo/ui/E4.tsx"]);
383
+ });
384
+
385
+ test("a route's check that adds its own root leaves another route's failed root pending", async () => {
386
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
387
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
388
+ "ui/E1.tsx": 'import { gone } from "./missingHelper";\nexport const e1 = () => gone;\n',
389
+ "ui/E3.tsx": "export const e3 = 3;\n",
390
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
391
+ });
392
+ bundler.wanted = new Set([file("ui/E1.tsx"), file("ui/E3.tsx")]);
393
+ await expect(patcher.update([], { roots: [file("ui/E1.tsx")], onlyRoots: true })).rejects.toThrow();
394
+ await patcher.update([], { roots: [file("ui/E3.tsx")], onlyRoots: true });
395
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
396
+ expect(graph.pending).toEqual(["apps/demo/ui/E1.tsx"]);
397
+
398
+ await Bun.write(file("ui/E1.tsx"), "export const e1 = () => 1;\n");
399
+ expect(changedIds(await patcher.update([file("ui/E1.tsx")]))).toEqual(["apps/demo/ui/E1.tsx"]);
400
+ });
401
+
402
+ test("a pending root a worker failed on waits for an edit of its own or of a file its failure named", async () => {
403
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
404
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
405
+ "ui/Other.tsx": 'export const other = () => "o1";\n',
406
+ "ui/Broken.tsx": 'import { gone } from "./missingHelper";\nexport const broken = () => gone;\n',
407
+ "entry.ts":
408
+ 'import { card } from "./ui/Card";\nimport { other } from "./ui/Other";\nexport const run = [card, other];\n',
409
+ "../../node_modules/fake-pkg/package.json": '{"name":"fake-pkg","main":"index.js"}',
410
+ "../../node_modules/fake-pkg/index.js": "export const gone = 1;\n",
411
+ });
412
+ bundler.wanted = new Set([file("ui/Broken.tsx")]);
413
+ await expect(patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).rejects.toThrow();
414
+ const broken =
415
+ 'import { gone } from "fake-pkg";\nimport { nope } from "./Card";\nexport const broken = () => gone + nope;\n';
416
+ await Bun.write(file("ui/Broken.tsx"), broken);
417
+ expect((await patcher.update([file("ui/Broken.tsx")])).kind).toBe("delegate");
418
+ //? The worker the builder runs next fails in Broken, naming Card: the resident patcher reads the graph it wrote.
419
+ await expect(bundler.update([file("ui/Broken.tsx")])).rejects.toThrow();
420
+ patcher.forget();
421
+
422
+ await Bun.write(file("ui/Other.tsx"), 'export const other = () => "o2";\n');
423
+ expect(changedIds(await patcher.update([file("ui/Other.tsx")]))).toEqual(["apps/demo/ui/Other.tsx"]);
424
+ expect(await patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).toEqual({
425
+ kind: "unchanged",
426
+ });
427
+
428
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\nexport const nope = 1;\n');
429
+ expect((await patcher.update([file("ui/Card.tsx")])).kind).toBe("delegate");
430
+ //? Its worker has not run yet: a route's check waits for it rather than handing the root over again.
431
+ expect(await patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).toEqual({
432
+ kind: "unchanged",
433
+ });
434
+ });
435
+
436
+ test("a root handed to a worker that died before writing its failure is handed over again", async () => {
437
+ const { bundler, patcher, file } = await movedRegistry({
438
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
439
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
440
+ "../../node_modules/fake-pkg/package.json": '{"name":"fake-pkg","main":"index.js"}',
441
+ "../../node_modules/fake-pkg/index.js": "export const gone = 1;\n",
442
+ });
443
+ await Bun.write(file("ui/B.tsx"), 'import { gone } from "fake-pkg";\nexport const b = () => gone;\n');
444
+ bundler.wanted = new Set([file("ui/B.tsx")]);
445
+ const check = async () => await patcher.update([], { roots: [file("ui/B.tsx")], onlyRoots: true });
446
+ expect((await check()).kind).toBe("delegate");
447
+ expect(await check()).toEqual({ kind: "unchanged" });
448
+ //? What the builder does when that worker is killed (an OOM) before it writes anything.
449
+ patcher.releaseHandedOver();
450
+ patcher.forget();
451
+ expect((await check()).kind).toBe("delegate");
452
+ });
453
+
454
+ test("a pending root a worker failed on because of another file is tried again once that file is fixed", async () => {
455
+ const { bundler, patcher, file } = await movedRegistry({
456
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
457
+ "ui/C.tsx": 'export const c = () => "c1";\n',
458
+ "entry.ts": 'import { card } from "./ui/Card";\nimport { c } from "./ui/C";\nexport const run = [card, c];\n',
459
+ "../../node_modules/fake-pkg/package.json": '{"name":"fake-pkg","main":"index.js"}',
460
+ "../../node_modules/fake-pkg/index.js": "export const gone = 1;\n",
461
+ });
462
+ await Bun.write(file("ui/B.tsx"), 'import { gone } from "fake-pkg";\nexport const b = () => gone;\n');
463
+ bundler.wanted = new Set([file("ui/B.tsx")]);
464
+ await Bun.write(file("ui/C.tsx"), "export const c = () => ;\n");
465
+ await expect(patcher.update([file("ui/C.tsx")])).rejects.toThrow();
466
+ expect((await patcher.update([], { roots: [file("ui/B.tsx")], onlyRoots: true })).kind).toBe("delegate");
467
+ await expect(bundler.update([], { roots: [file("ui/B.tsx")] })).rejects.toThrow();
468
+ patcher.forget();
469
+
470
+ await Bun.write(file("ui/C.tsx"), 'export const c = () => "c2";\n');
471
+ expect((await patcher.update([file("ui/C.tsx")])).kind).toBe("delegate");
472
+ });
473
+
474
+ test.skipIf(!foldsCase)(
475
+ "an importer still naming the old spelling compiles with the new one, so app.js defines what it requires",
476
+ async () => {
477
+ const { patcher, file, app, changedIds } = await movedRegistry({
478
+ "ui/card.tsx": 'export const card = () => "v1";\n',
479
+ "ui/Page.tsx": 'import { card } from "./card";\nexport const page = () => card();\n',
480
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
481
+ });
482
+ fs.renameSync(file("ui/card.tsx"), file("ui/Card.tsx"));
483
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
484
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual([
485
+ "apps/demo/ui/Card.tsx",
486
+ "apps/demo/ui/Page.tsx",
487
+ ]);
488
+ expect(await app()).toContain('__akan.define("apps/demo/ui/Card.tsx"');
489
+ expect(await app()).not.toContain('__akan.define("apps/demo/ui/card.tsx"');
490
+ await Bun.write(file("ui/Other.tsx"), "export const other = 1;\n");
491
+ expect(await patcher.update([file("ui/Other.tsx")])).toEqual({ kind: "unchanged" });
492
+ },
493
+ );
494
+
495
+ test.skipIf(!foldsCase)(
496
+ "a case-only rename the patcher never saw moves the module once, and later saves leave it alone",
497
+ async () => {
498
+ for (const resident of [false, true]) {
499
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
500
+ "ui/card.tsx": 'export const card = () => "v1";\n',
501
+ "ui/Page.tsx": 'import { card } from "./card";\nexport const page = () => card();\n',
502
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
503
+ });
504
+ fs.renameSync(file("ui/card.tsx"), file("ui/Card.tsx"));
505
+ await Bun.write(file("ui/Page.tsx"), 'import { card } from "./Card";\nexport const page = () => card();\n');
506
+ const later = resident ? patcher : new CsrDevPatcher(bundler, { resident: true });
507
+ expect(changedIds(await later.update([file("ui/Page.tsx")]))).toEqual([
508
+ "apps/demo/ui/Card.tsx",
509
+ "apps/demo/ui/Page.tsx",
510
+ ]);
511
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
512
+ expect(changedIds(await later.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
513
+ await Bun.write(file("ui/Other.tsx"), "export const other = 1;\n");
514
+ expect(await later.update([file("ui/Other.tsx")])).toEqual({ kind: "unchanged" });
515
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
516
+ expect(graph.modules["apps/demo/ui/card.tsx"]).toBeUndefined();
517
+ }
518
+ },
519
+ );
520
+
521
+ test("by default a pending root counts as reached only while an entry or a module imports it", async () => {
522
+ const { bundler, patcher, file, changedIds } = await movedRegistry({
523
+ "ui/Card.tsx": 'export const card = () => "v1";\n',
524
+ "ui/Broken.tsx": "export const broken = ;\n",
525
+ "entry.ts": 'import { card } from "./ui/Card";\nexport const run = card;\n',
526
+ });
527
+ await expect(patcher.update([], { roots: [file("ui/Broken.tsx")], onlyRoots: true })).rejects.toThrow();
528
+ await Bun.write(file("ui/Card.tsx"), 'export const card = () => "v2";\n');
529
+ expect(changedIds(await patcher.update([file("ui/Card.tsx")]))).toEqual(["apps/demo/ui/Card.tsx"]);
530
+ const graph = (await bundler.writer.readJson<CsrDevGraph>("graph.json")) as CsrDevGraph;
531
+ expect(graph.pending).toEqual([]);
532
+ });
533
+
534
+ test("a module whose factory file is missing is compiled again instead of failing every read", async () => {
535
+ const { bundler, patcher, app, changedIds } = await movedRegistry({
536
+ "ui/Page.tsx": 'export const page = () => "page";\n',
537
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
538
+ });
539
+ const manifest = (await bundler.writer.readJson<CsrDevManifest>(CSR_DEV_MANIFEST_FILE)) as CsrDevManifest;
540
+ await bundler.writer.writeJson(CSR_DEV_MANIFEST_FILE, { ...manifest, generation: 2, appGeneration: 1 });
541
+ await rm(path.join(bundler.outDir, "modules"), { recursive: true, force: true });
542
+ patcher.forget();
543
+ const healed = await patcher.update([]);
544
+ expect(changedIds(healed)).toEqual(["apps/demo/entry.ts", "apps/demo/ui/Page.tsx"]);
545
+ expect(await app()).toContain('__akan.define("apps/demo/ui/Page.tsx"');
546
+ });
547
+
548
+ test("a file created beside a folder a relative import resolved to takes that import over", async () => {
549
+ const { patcher, file, app, changedIds } = await movedRegistry({
550
+ "ui/Foo/index.tsx": 'export const foo = () => "folder";\n',
551
+ "ui/Page.tsx": 'import { foo } from "./Foo";\nexport const page = () => foo();\n',
552
+ "entry.ts": 'import { page } from "./ui/Page";\nexport const run = page;\n',
553
+ });
554
+ await Bun.write(file("ui/Foo.tsx"), 'export const foo = () => "file";\n');
555
+ expect(changedIds(await patcher.update([file("ui/Foo.tsx")]))).toEqual([
556
+ "apps/demo/ui/Foo.tsx",
557
+ "apps/demo/ui/Page.tsx",
558
+ ]);
559
+ expect(await app()).toContain('__akan.define("apps/demo/ui/Foo.tsx"');
560
+ });
561
+
562
+ test("a registry with no manifest is a first build, which no tab holds a module of", async () => {
563
+ const { bundler, dir } = await makeRegistry("same");
564
+ await rm(path.join(dir, CSR_DEV_MANIFEST_FILE), { force: true });
565
+ expect(await new CsrDevPatcher(bundler).update([])).toMatchObject({
566
+ kind: "delegate",
567
+ reason: "first build",
568
+ generation: 1,
569
+ first: true,
570
+ });
571
+ });
572
+ });