@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,548 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { appGenerationOf, CSR_DEV_MANIFEST_FILE, type CsrDevManifest } from "akanjs/server/hmr/csrDevManifest";
4
+ import type { CsrDevCompileResult } from "./csrDevModuleCompiler";
5
+ import { CsrDevPaths } from "./csrDevPaths";
6
+ import { CsrDevResolver } from "./csrDevResolver";
7
+ import type { CsrDevCode, CsrDevContext, CsrDevGraph } from "./csrDevTypes";
8
+ import type { CsrDevUpdate, CsrDevUpdateOptions, DevRegistryBundler } from "./devRegistryBundler";
9
+
10
+ interface CsrDevPatcherState {
11
+ graph: CsrDevGraph;
12
+ manifest: CsrDevManifest;
13
+ code: CsrDevCode | null;
14
+ }
15
+
16
+ export type CsrDevPatchResult =
17
+ | { kind: "unchanged" }
18
+ | { kind: "update"; update: CsrDevUpdate }
19
+ | { kind: "delegate"; reason: string; generation: number; context: CsrDevContext; first?: boolean };
20
+
21
+ export interface CsrDevPatchOptions extends CsrDevUpdateOptions {
22
+ /**
23
+ * Run the whole-app builds a save can call for in this process: an npm module the graph has not seen, and the
24
+ * resolution build for an import no recorded resolution answers. Without it the save is handed back to a build
25
+ * worker, which exits and returns their memory (a resident builder that ran them sat at 1.3-1.5GB).
26
+ */
27
+ allowWholeAppBuilds?: boolean;
28
+ }
29
+
30
+ //* The incremental path of a dev module registry (CSR, or the client code of SSR pages). The resident builder keeps
31
+ //* one, so the graph, the manifest and every module's factory stay in memory between saves; a build worker makes a
32
+ //* throwaway one that starts from the disk. A save it hands back leaves the graph on disk as it was, so a worker still
33
+ //* sees those files as changed.
34
+ export class CsrDevPatcher {
35
+ readonly #bundler: DevRegistryBundler;
36
+ readonly #resident: boolean;
37
+ #state: CsrDevPatcherState | null = null;
38
+ //? A pending root handed to a worker, by the hash it had: one that fails there too waits for an edit of its own or of
39
+ //? a file the worker failed in, instead of sending every save to a worker.
40
+ readonly #delegated = new Map<string, { hash: string; failed?: Record<string, string>; before: string }>();
41
+
42
+ constructor(bundler: DevRegistryBundler, { resident = false }: { resident?: boolean } = {}) {
43
+ this.#bundler = bundler;
44
+ this.#resident = resident;
45
+ }
46
+
47
+ /** Drops what it holds, so the next update starts from the disk a build worker may have rewritten. */
48
+ forget(): void {
49
+ this.#state = null;
50
+ }
51
+
52
+ /** Roots handed to a worker that died before it wrote their failure: the next round hands them over again. */
53
+ releaseHandedOver(): void {
54
+ for (const [id, handedOver] of this.#delegated) if (!handedOver.failed) this.#delegated.delete(id);
55
+ }
56
+
57
+ async update(
58
+ changedFiles: string[] = [],
59
+ { announce, roots = [], onlyRoots = false, allowWholeAppBuilds = false }: CsrDevPatchOptions = {},
60
+ ): Promise<CsrDevPatchResult> {
61
+ const context = await this.#bundler.context();
62
+ if (!context) return { kind: "unchanged" };
63
+ const resident = this.#state;
64
+ const state = resident ?? (await this.#load());
65
+ if (!state) {
66
+ const manifest = await this.#bundler.writer.readJson<CsrDevManifest>(CSR_DEV_MANIFEST_FILE);
67
+ const reason = !manifest
68
+ ? "first build"
69
+ : (await this.#bundler.writer.isBuilding())
70
+ ? "the last whole build was cut short"
71
+ : "the dev bundle config changed";
72
+ return this.#handBack(reason, (manifest?.generation ?? 0) + 1, context, { first: !manifest });
73
+ }
74
+ const generation = state.manifest.generation + 1;
75
+ if (state.graph.configKey !== context.configKey)
76
+ return this.#handBack("the dev bundle config changed", generation, context);
77
+ const metadataFile = changedFiles.find((file) => this.#bundler.isMetadataFile(file));
78
+ // `lib/useClient.ts` inlines signal and dictionary metadata through a macro at build time.
79
+ if (metadataFile) return this.#handBack(`${path.basename(metadataFile)} changed`, generation, context);
80
+ //? Only a registry read from disk: one held in memory saw every save since, and a metadata save was handed back.
81
+ if (!resident && state.graph.metadata !== (await this.#bundler.metadataFingerprint()))
82
+ return this.#handBack("signal or dictionary metadata changed since the registry was built", generation, context);
83
+ this.#state = this.#resident ? state : null;
84
+ //? Not over a module whose factory file is missing: this patch compiles it, and its commit writes app.js whole.
85
+ const healable =
86
+ appGenerationOf(state.manifest) < state.manifest.generation &&
87
+ this.#withoutFactory(state, (file) => fs.existsSync(file)).length === 0;
88
+ if (healable)
89
+ state.manifest = await this.#bundler.writer.healApp(state.graph, state.manifest, await this.#code(state));
90
+ return await this.#patch(context, state, changedFiles, generation, {
91
+ announce,
92
+ roots,
93
+ onlyRoots,
94
+ allowWholeAppBuilds,
95
+ });
96
+ }
97
+
98
+ async #patch(
99
+ context: CsrDevContext,
100
+ state: CsrDevPatcherState,
101
+ changedFiles: string[],
102
+ generation: number,
103
+ { announce, roots = [], onlyRoots = false, allowWholeAppBuilds }: CsrDevPatchOptions,
104
+ ): Promise<CsrDevPatchResult> {
105
+ const { paths, writer } = this.#bundler;
106
+ const { graph } = state;
107
+ const entries = await this.#bundler.writeEntries(context);
108
+ const entryIds = this.#bundler.entryIds(entries.files);
109
+ const present = CsrDevPatcher.#presence(changedFiles);
110
+ const forgotten = this.#forgetDeletedModules(state, present);
111
+ const changed = onlyRoots ? new Set<string>() : await this.#changedModules(graph, changedFiles, present);
112
+ for (const id of this.#withoutFactory(state, present)) changed.add(id);
113
+ for (const file of entries.changed) changed.add(paths.idOf(file));
114
+ const rootFiles = [...Object.values(entries.files), ...roots.filter((file) => fs.existsSync(file))];
115
+ for (const file of rootFiles) {
116
+ const id = paths.idOf(file);
117
+ if (!graph.modules[id] && !(onlyRoots && (await this.#parked(graph, id)))) changed.add(id);
118
+ }
119
+ const carried = onlyRoots ? [] : await this.#carriedRoots(graph, changed, present);
120
+ const routesMoved =
121
+ this.#bundler.reloadsOnEntryChange &&
122
+ (entries.changed.length > 0 || JSON.stringify(entryIds) !== JSON.stringify(graph.entries));
123
+ if (changed.size === 0) {
124
+ if (carried.length === 0) return { kind: "unchanged" };
125
+ return await this.#retryCarried(context, state, carried, { announce, allowWholeAppBuilds });
126
+ }
127
+
128
+ // A fresh resolver per save: one kept across saves would stay past its prepass, and resolve the next new bare
129
+ // import with Bun's runtime resolver, which ignores the `browser` condition.
130
+ const resolver = new CsrDevResolver({
131
+ paths,
132
+ context,
133
+ entryFiles: rootFiles.map((file) => CsrDevPaths.realpath(file)),
134
+ resolution: graph.resolution,
135
+ runtimeResolved: graph.runtimeResolved,
136
+ });
137
+ const known = new Set(Object.keys(graph.modules).map((id) => paths.fileOf(id)));
138
+ const compile = async (ids: string[]) =>
139
+ await this.#bundler.compiler(context, resolver).compile(
140
+ ids.map((id) => paths.fileOf(id)),
141
+ known,
142
+ { refuseNewVendor: !allowWholeAppBuilds, refusePrepass: !allowWholeAppBuilds },
143
+ );
144
+ let result: CsrDevCompileResult;
145
+ let orphans: string[];
146
+ try {
147
+ ({ result, orphans } = await this.#compileLeavingOrphans(compile, graph, changed));
148
+ for (const module of result.modules) known.add(module.file);
149
+ result = await this.#withTwinImporters(compile, graph, result);
150
+ } catch (error) {
151
+ //? Joined, not replaced: a route's root another round failed stays pending for its own retry.
152
+ graph.pending = [...new Set([...graph.pending, ...changed, ...carried])];
153
+ if (!this.#resident)
154
+ graph.failed = {
155
+ roots: [...changed, ...carried],
156
+ files: await CsrDevPatcher.#hashesOf(paths, CsrDevPatcher.#implicatedFiles(error)),
157
+ };
158
+ await writer.writeJson("graph.json", graph);
159
+ throw error;
160
+ }
161
+ const refusal =
162
+ result.refusedVendors.length > 0
163
+ ? "an npm module joined the graph"
164
+ : result.unresolved.length > 0
165
+ ? `a new import needs the resolution build: ${result.unresolved[0]}`
166
+ : null;
167
+ if (refusal) {
168
+ //? The entry files are already rewritten, so the worker would see them unchanged and patch a moved route table:
169
+ //? a graph whose entries name none makes it reload instead.
170
+ if (routesMoved) await writer.writeJson("graph.json", { ...graph, entries: {} });
171
+ if (onlyRoots)
172
+ for (const id of changed)
173
+ if (!graph.modules[id])
174
+ this.#delegated.set(id, {
175
+ hash: await CsrDevPaths.hashOf(paths.fileOf(id)),
176
+ before: JSON.stringify(graph.failed ?? null),
177
+ });
178
+ return this.#handBack(refusal, generation, context);
179
+ }
180
+ const compiled = result.modules;
181
+ const vendorJoined = compiled.some((module) => module.vendor && !graph.modules[module.id]);
182
+ //? Read before the merge: a new module's file is written after announcing, and one without a file is compiled now.
183
+ const code = await this.#code(state);
184
+ for (const id of [...orphans, ...this.#caseTwins(graph, compiled)]) {
185
+ delete graph.modules[id];
186
+ resolver.forget(paths.fileOf(id));
187
+ code?.modules.delete(id);
188
+ forgotten.push(id);
189
+ }
190
+ graph.pending = graph.pending.filter((id) => !orphans.includes(id));
191
+ this.#bundler.merge(graph, resolver, compiled);
192
+ if (!this.#resident) delete graph.failed;
193
+ for (const module of compiled) this.#delegated.delete(module.id);
194
+ graph.entries = entryIds;
195
+ for (const module of compiled) {
196
+ if (module.vendor || !code) continue;
197
+ code.modules.set(module.id, module.factory);
198
+ if (module.helpers) code.helpers.set(module.helpers.hash, module.helpers.definition);
199
+ }
200
+ const changedIds = compiled.filter((module) => !module.vendor).map((module) => module.id);
201
+ const constantId = changedIds.find((id) => id.endsWith(".constant.ts"));
202
+ // A model class swapped under live store state would mix old and new instances; reload instead.
203
+ const reason = vendorJoined
204
+ ? "an npm module joined the graph"
205
+ : routesMoved
206
+ ? "the route table changed"
207
+ : constantId
208
+ ? `${path.basename(constantId)} changed`
209
+ : undefined;
210
+ //? Only orphans left the graph: nothing a tab holds changed, so no patch goes out.
211
+ if (compiled.length === 0 && !reason) {
212
+ await writer.writeJson("graph.json", graph);
213
+ await writer.forgetModules(forgotten);
214
+ if (carried.length === 0) return { kind: "unchanged" };
215
+ return await this.#retryCarried(context, state, carried, { announce, allowWholeAppBuilds });
216
+ }
217
+ const previous = state.manifest;
218
+ const next: CsrDevManifest = { ...previous, generation, entries: entryIds };
219
+ const update = (patchUrl?: string): CsrDevUpdate => ({
220
+ generation,
221
+ reload: !!reason,
222
+ reason,
223
+ patchUrl,
224
+ changedIds,
225
+ moduleCount: Object.keys(graph.modules).length,
226
+ epoch: previous.epoch,
227
+ });
228
+ // A reload rewrites app.js first: the tabs are about to boot from it.
229
+ let outcome: CsrDevPatchResult;
230
+ if (reason) {
231
+ await writer.forgetPatch(generation);
232
+ await writer.writeModules(compiled);
233
+ const vendorFile = vendorJoined ? await writer.writeVendor(graph) : previous.vendorFile;
234
+ await writer.writeApp(graph, { generation, vendorFile, epoch: previous.epoch }, code);
235
+ state.manifest = { ...next, vendorFile, appGeneration: generation };
236
+ await writer.writeState(graph, state.manifest);
237
+ await writer.prune(vendorFile, generation);
238
+ const reloaded = update();
239
+ announce?.(reloaded);
240
+ outcome = { kind: "update", update: reloaded };
241
+ } else {
242
+ const patched = update(
243
+ await writer.writePatch(
244
+ generation,
245
+ compiled.filter((module) => !module.vendor),
246
+ ),
247
+ );
248
+ await writer.commitPatch(
249
+ graph,
250
+ { ...next, appGeneration: appGenerationOf(previous) },
251
+ () => announce?.(patched),
252
+ { modules: compiled, code },
253
+ );
254
+ state.manifest = { ...next, appGeneration: generation };
255
+ await writer.prune(previous.vendorFile, generation);
256
+ outcome = { kind: "update", update: patched };
257
+ }
258
+ // Only now that graph.json no longer names them: a crash before it left a graph pointing at missing files.
259
+ await writer.forgetModules(forgotten);
260
+ if (carried.length === 0) return outcome;
261
+ const retried = await this.#retryCarried(context, state, carried, { announce, allowWholeAppBuilds });
262
+ return retried.kind === "delegate" ? retried : outcome;
263
+ }
264
+
265
+ //? Compiled apart from the save: a new root that failed to compile, and that no module imports, must not fail every
266
+ //? later save's round with it. Its error still reaches the overlay (this throws after the save's patch went out).
267
+ async #retryCarried(
268
+ context: CsrDevContext,
269
+ state: CsrDevPatcherState,
270
+ carried: string[],
271
+ { announce, allowWholeAppBuilds }: CsrDevPatchOptions,
272
+ ): Promise<CsrDevPatchResult> {
273
+ const { paths, writer } = this.#bundler;
274
+ const retried = await this.#patch(context, state, [], state.manifest.generation + 1, {
275
+ announce,
276
+ roots: carried.map((id) => paths.fileOf(id)),
277
+ onlyRoots: true,
278
+ allowWholeAppBuilds,
279
+ });
280
+ //? Handed to a worker (a new npm import in the fixed root): the graph keeps them pending, so the worker retries
281
+ //? them with whole-app builds allowed instead of never hearing of them.
282
+ if (retried.kind === "delegate") {
283
+ state.graph.pending = [...new Set([...state.graph.pending, ...carried])];
284
+ await writer.writeJson("graph.json", state.graph);
285
+ }
286
+ return retried;
287
+ }
288
+
289
+ //? One Bun.build fails whole: a module that fails to compile and that nothing reaches any more (its last importer
290
+ //? dropped the import in this very save) leaves the graph, where it would fail every later save until fixed itself.
291
+ //? Everything else the roots stopped reaching goes with it, or an importer left behind would bring it back.
292
+ async #compileLeavingOrphans(
293
+ compile: (ids: string[]) => Promise<CsrDevCompileResult>,
294
+ graph: CsrDevGraph,
295
+ changed: Set<string>,
296
+ ): Promise<{ result: CsrDevCompileResult; orphans: string[] }> {
297
+ const { paths } = this.#bundler;
298
+ try {
299
+ return { result: await compile([...changed]), orphans: [] };
300
+ } catch (error) {
301
+ const failing = new Set(CsrDevPatcher.#failingFiles(error).map((file) => paths.idOf(file)));
302
+ const broken = [...changed].filter((id) => failing.has(id) && graph.modules[id]);
303
+ if (broken.length === 0) throw error;
304
+ const rest = [...changed].filter((id) => !broken.includes(id));
305
+ const empty: CsrDevCompileResult = { modules: [], refusedVendors: [], unresolved: [] };
306
+ const result =
307
+ rest.length === 0
308
+ ? empty
309
+ : await compile(rest).catch(() => {
310
+ throw error;
311
+ });
312
+ const modules = { ...graph.modules };
313
+ for (const module of result.modules)
314
+ modules[module.id] = {
315
+ vendor: module.vendor,
316
+ mtimeMs: module.mtimeMs,
317
+ hash: module.hash,
318
+ deps: module.deps.map((dep) => (CsrDevPaths.isStub(dep) ? dep : paths.idOf(dep))),
319
+ };
320
+ const reached = await this.#bundler.reachable({ ...graph, modules });
321
+ if (broken.some((id) => reached.has(id))) throw error;
322
+ const orphans = Object.keys(modules).filter((id) => !reached.has(id) && !modules[id]?.vendor);
323
+ return { result, orphans };
324
+ }
325
+ }
326
+
327
+ //? A case-only rename the patcher never saw (made while CSR was unarmed) compiles under the disk's spelling: the old
328
+ //? id goes, or its stale mtime would compile it again on every save. Only where the disk folds case: on ext4 the two
329
+ //? spellings are two files.
330
+ #caseTwins(graph: CsrDevGraph, compiled: CsrDevCompileResult["modules"]): string[] {
331
+ const { paths } = this.#bundler;
332
+ const idsByFold = new Map<string, string[]>();
333
+ for (const id of Object.keys(graph.modules))
334
+ idsByFold.set(id.toLowerCase(), [...(idsByFold.get(id.toLowerCase()) ?? []), id]);
335
+ return compiled.flatMap((module) =>
336
+ (idsByFold.get(module.id.toLowerCase()) ?? []).filter(
337
+ (id) => id !== module.id && !graph.modules[id]?.vendor && !CsrDevPaths.isNamed(paths.fileOf(id)),
338
+ ),
339
+ );
340
+ }
341
+
342
+ //? Their importers compile in the same round: one still importing the old spelling (valid on a disk that folds case)
343
+ //? would otherwise require a module app.js no longer defines.
344
+ async #withTwinImporters(
345
+ compile: (ids: string[]) => Promise<CsrDevCompileResult>,
346
+ graph: CsrDevGraph,
347
+ result: CsrDevCompileResult,
348
+ ): Promise<CsrDevCompileResult> {
349
+ const twins = new Set(this.#caseTwins(graph, result.modules));
350
+ if (twins.size === 0) return result;
351
+ const compiledIds = new Set(result.modules.map((module) => module.id));
352
+ const importers = Object.entries(graph.modules)
353
+ .filter(([id, module]) => !module.vendor && !compiledIds.has(id) && !twins.has(id))
354
+ .filter(([, module]) => module.deps.some((dep) => twins.has(dep)))
355
+ .map(([id]) => id);
356
+ if (importers.length === 0) return result;
357
+ const more = await compile(importers);
358
+ return {
359
+ modules: [...result.modules, ...more.modules],
360
+ refusedVendors: [...result.refusedVendors, ...more.refusedVendors],
361
+ unresolved: [...result.unresolved, ...more.unresolved],
362
+ };
363
+ }
364
+
365
+ //? Where it failed, and the files its messages name: Bun puts a missing export at the importer and names the module
366
+ //? that lacks it only in the text ("No matching export in \"../ui/C.tsx\""), relative to the working directory.
367
+ static #implicatedFiles(error: unknown): string[] {
368
+ const failing = CsrDevPatcher.#failingFiles(error);
369
+ const messages: unknown[] = error instanceof AggregateError ? error.errors : [];
370
+ const named = messages.flatMap((message) => {
371
+ const text = String((message as { message?: unknown }).message ?? "");
372
+ const bases = [process.cwd(), ...failing.map((file) => path.dirname(file))];
373
+ return [...text.matchAll(/"([^"\n]+)"/g)].flatMap(([, quoted = ""]) => {
374
+ const found = bases
375
+ .map((base) => path.resolve(base, quoted))
376
+ .find((file) => CsrDevPaths.isScript(file) && fs.existsSync(file));
377
+ return found ? [CsrDevPaths.realpath(found)] : [];
378
+ });
379
+ });
380
+ return [...new Set([...failing, ...named])];
381
+ }
382
+
383
+ static #failingFiles(error: unknown): string[] {
384
+ const messages: unknown[] = error instanceof AggregateError ? error.errors : [];
385
+ return messages
386
+ .map((message) => (message as { position?: { file?: unknown } | null }).position?.file)
387
+ .filter((file): file is string => typeof file === "string" && path.isAbsolute(file))
388
+ .map((file) => CsrDevPaths.realpath(file));
389
+ }
390
+
391
+ //? A pending root outside the graph (a route's entry that failed to compile) is retried while a route still reaches
392
+ //? it; one no route names any more (the page stopped importing it) leaves the pending list instead.
393
+ async #carriedRoots(graph: CsrDevGraph, changed: Set<string>, present: (file: string) => boolean): Promise<string[]> {
394
+ const { paths } = this.#bundler;
395
+ const outside = graph.pending.filter((id) => !graph.modules[id] && !changed.has(id) && present(paths.fileOf(id)));
396
+ if (outside.length === 0) return [];
397
+ const wanted = await this.#bundler.wantedRoots(
398
+ outside.map((id) => paths.fileOf(id)),
399
+ graph,
400
+ );
401
+ const kept = outside.filter((id) => wanted.has(paths.fileOf(id)));
402
+ graph.pending = graph.pending.filter((id) => graph.modules[id] || kept.includes(id));
403
+ const carried: string[] = [];
404
+ for (const id of kept) if (!(await this.#parked(graph, id))) carried.push(id);
405
+ return carried;
406
+ }
407
+
408
+ //? The worker's failure is read from the graph it wrote, once, and only a round that built this root. Until that
409
+ //? worker writes, the record on disk is the one from before the hand-over, and the root waits for it.
410
+ async #parked(graph: CsrDevGraph, id: string): Promise<boolean> {
411
+ const { paths } = this.#bundler;
412
+ const handedOver = this.#delegated.get(id);
413
+ if (!handedOver || handedOver.hash !== (await CsrDevPaths.hashOf(paths.fileOf(id)))) return false;
414
+ if (!handedOver.failed) {
415
+ if (JSON.stringify(graph.failed ?? null) === handedOver.before) return true;
416
+ if (graph.failed?.roots.includes(id)) handedOver.failed = graph.failed.files;
417
+ }
418
+ if (!handedOver.failed) return false;
419
+ for (const [failedId, hash] of Object.entries(handedOver.failed))
420
+ if ((await CsrDevPaths.hashOf(paths.fileOf(failedId))) !== hash) return false;
421
+ return true;
422
+ }
423
+
424
+ static async #hashesOf(paths: CsrDevPaths, files: string[]): Promise<Record<string, string>> {
425
+ return Object.fromEntries(
426
+ await Promise.all(files.map(async (file) => [paths.idOf(file), await CsrDevPaths.hashOf(file)] as const)),
427
+ );
428
+ }
429
+
430
+ // Missing factory files are recompiled, not read: a registry whose graph outlived them would fail every read. The
431
+ // resident patcher holds every factory it has read or written, so only a registry just read from disk is stat'ed.
432
+ #withoutFactory({ graph, code }: CsrDevPatcherState, present: (file: string) => boolean): string[] {
433
+ const { paths, writer } = this.#bundler;
434
+ const hasFactory = (id: string) => (code ? code.modules.has(id) : writer.hasModule(id));
435
+ return Object.entries(graph.modules)
436
+ .filter(([id, module]) => !module.vendor && !hasFactory(id) && present(paths.fileOf(id)))
437
+ .map(([id]) => id);
438
+ }
439
+
440
+ //? A case-only rename is the one move `existsSync` misses (`ui/card.tsx` still exists after a rename to
441
+ //? `ui/Card.tsx` on APFS and NTFS), and it reaches the patcher as a save of both names: the realpath of every
442
+ //? module is paid for only then (2.2ms a save on a 385-module registry, against 0.3ms). One it never saw goes with
443
+ //? the compile that meets the new spelling (#caseTwins).
444
+ static #presence(changedFiles: string[]): (file: string) => boolean {
445
+ const caseMoved = changedFiles.some((file) => fs.existsSync(file) && !CsrDevPaths.isNamed(file));
446
+ return caseMoved ? (file) => CsrDevPaths.isNamed(file) : (file) => fs.existsSync(file);
447
+ }
448
+
449
+ #handBack(
450
+ reason: string,
451
+ generation: number,
452
+ context: CsrDevContext,
453
+ { first = false }: { first?: boolean } = {},
454
+ ): CsrDevPatchResult {
455
+ this.#state = null;
456
+ return { kind: "delegate", reason, generation, context, ...(first ? { first } : {}) };
457
+ }
458
+
459
+ async #load(): Promise<CsrDevPatcherState | null> {
460
+ const manifest = await this.#bundler.writer.readJson<CsrDevManifest>(CSR_DEV_MANIFEST_FILE);
461
+ const graph = await this.#bundler.writer.readJson<CsrDevGraph>("graph.json");
462
+ if (!manifest || !graph || graph.version !== 1 || (await this.#bundler.writer.isBuilding())) return null;
463
+ return { graph, manifest, code: null };
464
+ }
465
+
466
+ // Only the resident patcher keeps factories: a worker writes app.js once, reading them from disk after announcing.
467
+ async #code(state: CsrDevPatcherState): Promise<CsrDevCode | undefined> {
468
+ if (!this.#resident) return undefined;
469
+ state.code ??= await this.#bundler.writer.readCode(state.graph);
470
+ return state.code;
471
+ }
472
+
473
+ async #changedModules(
474
+ graph: CsrDevGraph,
475
+ changedFiles: string[],
476
+ present: (file: string) => boolean,
477
+ ): Promise<Set<string>> {
478
+ const { paths } = this.#bundler;
479
+ const changed = new Set<string>(graph.pending.filter((id) => graph.modules[id]));
480
+ for (const file of changedFiles) {
481
+ const id = paths.idOf(file);
482
+ if (graph.modules[id]) changed.add(id);
483
+ }
484
+ for (const id of this.#shadowedImporters(graph, changedFiles)) changed.add(id);
485
+ // Stat, not trust: a save made while CSR was unarmed, or a build that failed, never reached this graph. The hash
486
+ // settles it because route discovery rewrites the generated layout wrappers, touching them on every call.
487
+ for (const [id, module] of Object.entries(graph.modules)) {
488
+ if (module.vendor || changed.has(id)) continue;
489
+ const file = paths.fileOf(id);
490
+ if (CsrDevPaths.mtimeOf(file) === module.mtimeMs) continue;
491
+ if ((await CsrDevPaths.hashOf(file)) !== module.hash) changed.add(id);
492
+ }
493
+ // A dependency the graph lost: deleted and brought back (an undo in the file explorer), it compiles again; moved
494
+ // away (`Foo.tsx` → `Foo/index.tsx`), its importers do, since their own text did not change and app.js would name
495
+ // a module no longer defined.
496
+ //? Not through an importer compiling anyway: its deps in the graph are the ones before this save, and its own
497
+ //? compile follows whatever it still imports, where a dropped import would keep a broken file failing every save.
498
+ for (const [id, module] of Object.entries(graph.modules)) {
499
+ if (changed.has(id)) continue;
500
+ for (const dep of module.deps) {
501
+ if (graph.modules[dep] || CsrDevPaths.isStub(dep)) continue;
502
+ //? Named, not only present: a dep a case rename replaced still exists under its old spelling.
503
+ const file = paths.fileOf(dep);
504
+ if (present(file) && CsrDevPaths.isNamed(file)) changed.add(dep);
505
+ else if (!module.vendor) changed.add(id);
506
+ }
507
+ }
508
+ return changed;
509
+ }
510
+
511
+ //? A new file can take over a relative import another module already resolved (`Foo.tsx` created beside
512
+ //? `Foo/index.tsx`), with no change to that module's own text: it compiles again, and resolves against the disk.
513
+ #shadowedImporters(graph: CsrDevGraph, changedFiles: string[]): string[] {
514
+ const { paths } = this.#bundler;
515
+ const bases = new Set(
516
+ changedFiles
517
+ .map((file) => CsrDevPaths.realpath(file))
518
+ .filter((file) => !graph.modules[paths.idOf(file)] && CsrDevPaths.isScript(file) && fs.existsSync(file))
519
+ .flatMap((file) => {
520
+ const stem = file.slice(0, file.length - path.extname(file).length);
521
+ return path.basename(stem) === "index" ? [stem, path.dirname(stem)] : [stem];
522
+ }),
523
+ );
524
+ if (bases.size === 0) return [];
525
+ return Object.entries(graph.resolution)
526
+ .filter(([importer, bySpecifier]) => {
527
+ if (!graph.modules[importer] || graph.modules[importer]?.vendor) return false;
528
+ const dir = path.dirname(paths.fileOf(importer));
529
+ return Object.keys(bySpecifier).some(
530
+ (specifier) => specifier.startsWith(".") && bases.has(path.resolve(dir, specifier)),
531
+ );
532
+ })
533
+ .map(([importer]) => importer);
534
+ }
535
+
536
+ // A deleted module leaves the graph instead of being rebuilt: rebuilding a missing file fails every save after it.
537
+ // Its importers compile again with the next save (#changedModules): a move resolves anew, a deletion fails with the
538
+ // error that says why. Its files stay until the graph on disk stops naming them.
539
+ #forgetDeletedModules({ graph, code }: CsrDevPatcherState, present: (file: string) => boolean): string[] {
540
+ const { paths } = this.#bundler;
541
+ const deleted = Object.keys(graph.modules).filter((id) => !graph.modules[id]?.vendor && !present(paths.fileOf(id)));
542
+ for (const id of deleted) {
543
+ delete graph.modules[id];
544
+ code?.modules.delete(id);
545
+ }
546
+ return deleted;
547
+ }
548
+ }