@akanjs/devkit 3.0.0-beta.3 → 3.0.0-beta.30

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 +2 -2
  3. package/README.md +3 -2
  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 +404 -141
  19. package/akanConfig/akanConfig.ts +317 -274
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +21 -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 +269 -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 +177 -0
  241. package/mobile/desktopBin.ts +143 -0
  242. package/mobile/desktopServerStage.test.ts +204 -0
  243. package/mobile/desktopServerStage.ts +138 -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 +151 -0
  250. package/mobile/nativeApp.ts +405 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +414 -0
  254. package/mobile/nativeConfig.ts +296 -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 +30 -25
  298. package/subspace.ts +81 -115
  299. package/subspaceConfig.ts +11 -9
  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,363 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { CsrDevPaths } from "./csrDevPaths";
4
+ import type { CsrDevContext } from "./csrDevTypes";
5
+ import { PagesBundleBuilder } from "./pagesBundleBuilder";
6
+
7
+ interface MetafileImport {
8
+ path: string;
9
+ kind: string;
10
+ original?: string;
11
+ external?: boolean;
12
+ }
13
+
14
+ interface PrepassMetafile {
15
+ inputs: Record<string, { imports: MetafileImport[] }>;
16
+ }
17
+
18
+ interface BrowserMap {
19
+ /** What the `browser` object maps to `false`: an empty module in a browser build. */
20
+ offFiles: Set<string>;
21
+ offNames: Set<string>;
22
+ }
23
+
24
+ export interface CsrDevResolverOptions {
25
+ paths: CsrDevPaths;
26
+ context: CsrDevContext;
27
+ entryFiles: string[];
28
+ resolution?: Record<string, Record<string, string>>;
29
+ /** Specifiers per importer id whose recorded resolution came from Bun's runtime resolver, not the browser build. */
30
+ runtimeResolved?: Record<string, string[]>;
31
+ /** When this process began: a package installed after it is one Bun's resolver here has not seen. */
32
+ startedAt?: number;
33
+ }
34
+
35
+ //* Resolution comes from a plain browser build: Bun's runtime resolver ignores the `browser` condition and picks Node
36
+ //* builds (`@firebase/util` resolves to its node-esm entry), which the registry build would then ship.
37
+ export class CsrDevResolver {
38
+ static readonly inline = "inline";
39
+ readonly #paths: CsrDevPaths;
40
+ readonly #context: CsrDevContext;
41
+ readonly #entryFiles: string[];
42
+ readonly #resolution = new Map<string, Map<string, string>>();
43
+ readonly #fallbacks = new Set<string>();
44
+ readonly #runtimeResolved = new Map<string, Set<string>>();
45
+ readonly #browserMaps = new Map<string, BrowserMap | null>();
46
+ /** Files a browser build loaded, so every import they make has its browser resolution recorded. */
47
+ readonly #loaded = new Set<string>();
48
+ readonly #startedAt: number;
49
+ #prepassDone = false;
50
+
51
+ constructor({
52
+ paths,
53
+ context,
54
+ entryFiles,
55
+ resolution = {},
56
+ runtimeResolved = {},
57
+ startedAt = Date.now() - process.uptime() * 1000,
58
+ }: CsrDevResolverOptions) {
59
+ this.#paths = paths;
60
+ this.#startedAt = startedAt;
61
+ this.#context = context;
62
+ this.#entryFiles = entryFiles;
63
+ for (const [importer, specifiers] of Object.entries(runtimeResolved))
64
+ this.#runtimeResolved.set(paths.fileOf(importer), new Set(specifiers));
65
+ for (const [importer, bySpecifier] of Object.entries(resolution))
66
+ this.#resolution.set(
67
+ paths.fileOf(importer),
68
+ new Map(
69
+ Object.entries(bySpecifier).map(([specifier, target]) => [
70
+ specifier,
71
+ CsrDevPaths.isStub(target) ? target : paths.fileOf(target),
72
+ ]),
73
+ ),
74
+ );
75
+ }
76
+
77
+ get prepassDone(): boolean {
78
+ return this.#prepassDone;
79
+ }
80
+
81
+ get fallbacks(): ReadonlySet<string> {
82
+ return this.#fallbacks;
83
+ }
84
+
85
+ async prepass(): Promise<void> {
86
+ const result = await Bun.build({
87
+ entrypoints: [...this.#entryFiles, ...(this.#context.refreshFile ? [this.#context.refreshFile] : [])],
88
+ target: "browser",
89
+ metafile: true,
90
+ external: [...(this.#context.externals ?? [])],
91
+ env: "AKAN_PUBLIC_*",
92
+ define: this.#context.define,
93
+ optimizeImports: this.#context.optimizeImports,
94
+ plugins: [PagesBundleBuilder.createCssStubPlugin()],
95
+ });
96
+ this.#record((result.metafile as unknown as PrepassMetafile).inputs);
97
+ this.#prepassDone = true;
98
+ }
99
+
100
+ //? The browser build drops a side-effect-free barrel's unused re-exports without loading them, yet the registry
101
+ //? compiles every file an import names. Such a file's own imports are resolved by a browser build of its own, one
102
+ //? level deep, not by the runtime resolver's Node conditions (vfile's `#minpath` would reach `node:path`).
103
+ async cover(files: string[]): Promise<void> {
104
+ if (!this.#prepassDone) return;
105
+ const pending = files.filter((file) => CsrDevPaths.isScript(file) && !this.#loaded.has(file));
106
+ if (pending.length === 0) return;
107
+ for (const file of pending) this.#loaded.add(file);
108
+ const entries = new Set(pending);
109
+ const result = await Bun.build({
110
+ entrypoints: pending,
111
+ target: "browser",
112
+ metafile: true,
113
+ throw: false,
114
+ external: [...(this.#context.externals ?? [])],
115
+ env: "AKAN_PUBLIC_*",
116
+ define: this.#context.define,
117
+ plugins: [
118
+ {
119
+ name: "akan-csr-cover",
120
+ //? What an entry imports is resolved, never parsed: a CommonJS stub, since an empty ESM module fails a named
121
+ //? import ("No matching export") and a failed build hands back no metafile at all.
122
+ setup: (build) => {
123
+ build.onLoad({ filter: /.*/ }, (args) =>
124
+ entries.has(CsrDevPaths.realpath(args.path))
125
+ ? undefined
126
+ : { contents: "module.exports = {};", loader: "js" },
127
+ );
128
+ },
129
+ },
130
+ ],
131
+ });
132
+ //? Left unrecorded when the build fails: those imports keep the runtime resolver's answer, as before.
133
+ if (result.metafile) this.#record((result.metafile as unknown as PrepassMetafile).inputs, entries);
134
+ }
135
+
136
+ #record(inputs: PrepassMetafile["inputs"], only?: Set<string>): void {
137
+ for (const [input, { imports }] of Object.entries(inputs)) {
138
+ const importer = CsrDevPaths.realpath(path.resolve(input));
139
+ if (only && !only.has(importer)) continue;
140
+ //? A re-export the build tree-shook keeps no specifier, so its file is resolved again as an entry of its own.
141
+ if (imports.every((record) => record.original)) this.#loaded.add(importer);
142
+ for (const record of imports) {
143
+ if (!record.original) continue;
144
+ if (record.external) {
145
+ if (record.original.startsWith("node:"))
146
+ this.#remember(importer, record.original, `${CsrDevPaths.stubPrefix}${record.original}`);
147
+ continue;
148
+ }
149
+ const target = path.resolve(record.path);
150
+ if (!fs.existsSync(target)) continue;
151
+ this.#remember(importer, record.original, CsrDevPaths.realpath(target));
152
+ this.#runtimeResolved.get(importer)?.delete(record.original);
153
+ }
154
+ }
155
+ }
156
+
157
+ resolve(importer: string, specifier: string, kind: "import" | "require" = "import"): string | null {
158
+ if (this.#isInlineSpecifier(specifier)) return CsrDevResolver.inline;
159
+ const disabled = this.#disabledInBrowser(importer, specifier, kind);
160
+ if (disabled) return disabled;
161
+ const known = this.#resolution.get(importer)?.get(specifier);
162
+ const usable = known && this.#stillAnswers(importer, specifier, known, kind) ? known : null;
163
+ const target = usable ?? this.#resolveUnknown(importer, specifier, kind);
164
+ if (target === null || CsrDevPaths.isStub(target)) return target;
165
+ if (!CsrDevPaths.isScript(target)) return CsrDevResolver.inline;
166
+ if (!usable) this.#remember(importer, specifier, target);
167
+ return target;
168
+ }
169
+
170
+ //? A relative record in app code stands while the disk still resolves it to the same file, as Bun would: a sibling
171
+ //? with a stronger extension (`x.tsx` beside `x.ts`), a file beside the folder it went into, or a record a CommonJS
172
+ //? re-export folded onto another file (`./a` → `x.js`) resolves anew. A package's files, and app code under a
173
+ //? package.json that maps files with a `browser` object, keep the prepass's answer, which a disk lookup would only
174
+ //? approximate.
175
+ #stillAnswers(importer: string, specifier: string, known: string, kind: "import" | "require"): boolean {
176
+ if (CsrDevPaths.isStub(known)) return true;
177
+ if (!fs.existsSync(known)) return false;
178
+ if (!specifier.startsWith(".") || CsrDevPaths.isVendorFile(importer)) return true;
179
+ if (this.#hasBrowserMap(path.dirname(importer))) return true;
180
+ const onDisk = CsrDevPaths.resolveRelative(path.dirname(importer), specifier, kind);
181
+ return onDisk !== null && CsrDevPaths.realpath(onDisk) === known;
182
+ }
183
+
184
+ isRegistryModule(file: string): boolean {
185
+ if (!CsrDevPaths.isScript(file)) return false;
186
+ const packageName = CsrDevResolver.#packageNameOf(file);
187
+ if (packageName === null) return true;
188
+ return !this.#context.optimizeImports.some((pattern) => {
189
+ const base = CsrDevResolver.#patternBase(pattern);
190
+ return base === packageName || base.startsWith(`${packageName}/`);
191
+ });
192
+ }
193
+
194
+ //? `false` in a package's `browser` object is an empty module to a browser build (object-inspect's
195
+ //? `./util.inspect.js`, which requires `util`). Its record carries no specifier, so the field is read here.
196
+ #disabledInBrowser(importer: string, specifier: string, kind: "import" | "require"): string | null {
197
+ const map = this.#browserMapOf(path.dirname(importer));
198
+ if (!map || (map.offFiles.size === 0 && map.offNames.size === 0)) return null;
199
+ if (!specifier.startsWith(".") && !path.isAbsolute(specifier))
200
+ return map.offNames.has(specifier) ? `${CsrDevPaths.emptyStubPrefix}${specifier}` : null;
201
+ const target = CsrDevPaths.resolveRelative(path.dirname(importer), specifier, kind);
202
+ return target && map.offFiles.has(CsrDevPaths.realpath(target))
203
+ ? `${CsrDevPaths.emptyStubPrefix}${this.#paths.idOf(target)}`
204
+ : null;
205
+ }
206
+
207
+ #hasBrowserMap(dir: string): boolean {
208
+ return this.#browserMapOf(dir) !== null;
209
+ }
210
+
211
+ #browserMapOf(dir: string): BrowserMap | null {
212
+ const cached = this.#browserMaps.get(dir);
213
+ if (cached !== undefined) return cached;
214
+ const pkgFile = path.join(dir, "package.json");
215
+ const parent = path.dirname(dir);
216
+ let map: BrowserMap | null = null;
217
+ if (fs.existsSync(pkgFile)) {
218
+ try {
219
+ const browser = (JSON.parse(fs.readFileSync(pkgFile, "utf8")) as { browser?: unknown }).browser;
220
+ if (typeof browser === "object" && browser !== null) {
221
+ const off = Object.entries(browser)
222
+ .filter(([, target]) => target === false)
223
+ .map(([key]) => key);
224
+ const files = off
225
+ .filter((key) => key.startsWith("."))
226
+ .map((key) => CsrDevPaths.resolveRelative(dir, key))
227
+ .filter((file): file is string => !!file);
228
+ map = {
229
+ offFiles: new Set(files.map((file) => CsrDevPaths.realpath(file))),
230
+ offNames: new Set(off.filter((key) => !key.startsWith("."))),
231
+ };
232
+ }
233
+ } catch {
234
+ // An unreadable package.json maps nothing; Bun's build fails on it anyway.
235
+ }
236
+ } else if (parent !== dir) map = this.#browserMapOf(parent);
237
+ this.#browserMaps.set(dir, map);
238
+ return map;
239
+ }
240
+
241
+ /** A module that left the graph takes its records with it. */
242
+ forget(importer: string): void {
243
+ this.#resolution.delete(importer);
244
+ this.#runtimeResolved.delete(importer);
245
+ }
246
+
247
+ serializeRuntimeResolved(): Record<string, string[]> {
248
+ return Object.fromEntries(
249
+ [...this.#runtimeResolved]
250
+ .filter(([, specifiers]) => specifiers.size > 0)
251
+ .map(([importer, specifiers]) => [this.#paths.idOf(importer), [...specifiers].sort()]),
252
+ );
253
+ }
254
+
255
+ // A target moved away is left out, so the graph does not carry it past a builder restart.
256
+ serialize(): Record<string, Record<string, string>> {
257
+ return Object.fromEntries(
258
+ [...this.#resolution].map(([importer, bySpecifier]) => [
259
+ this.#paths.idOf(importer),
260
+ Object.fromEntries(
261
+ [...bySpecifier]
262
+ .filter(([, target]) => CsrDevPaths.isStub(target) || fs.existsSync(target))
263
+ .map(([specifier, target]) => [specifier, CsrDevPaths.isStub(target) ? target : this.#paths.idOf(target)]),
264
+ ),
265
+ ]),
266
+ );
267
+ }
268
+
269
+ //? Bun.resolveSync keeps what it found per directory until a build walks that directory again, so in the resident
270
+ //? builder it may hand back a file moved away since (`Foo.tsx` → `Foo/index.tsx`, `.ts` → `.tsx`). A relative import
271
+ //? is therefore looked up on the disk as it is now, and a bare one it names a missing file for goes to a fresh worker.
272
+ #resolveUnknown(importer: string, specifier: string, kind: "import" | "require"): string | null {
273
+ if (specifier.startsWith("node:")) return `${CsrDevPaths.stubPrefix}${specifier}`;
274
+ const relative = specifier.startsWith(".") || path.isAbsolute(specifier);
275
+ const cannotResolve = () => new Error(`[csr-dev] cannot resolve "${specifier}" from ${this.#paths.idOf(importer)}`);
276
+ if (relative) {
277
+ const onDisk =
278
+ CsrDevPaths.resolveRelative(path.dirname(importer), specifier, kind) ??
279
+ CsrDevResolver.#onDisk(CsrDevPaths.tryResolve(specifier, path.dirname(importer)));
280
+ if (!onDisk) throw cannotResolve();
281
+ return CsrDevPaths.realpath(onDisk);
282
+ }
283
+ //? Ahead of the prepass too: a new file's bare imports (`akanjs/ui`, `@apps/<app>/client`) are ones its package
284
+ //? already resolved, and asking for the prepass would hand every such save to a build worker.
285
+ const sibling = this.#resolvedBySibling(importer, specifier);
286
+ if (sibling) return sibling;
287
+ const resolved = CsrDevPaths.tryResolve(specifier, path.dirname(importer));
288
+ const moved = resolved !== null && path.isAbsolute(resolved) && !fs.existsSync(resolved);
289
+ //? Bun keeps a process's node_modules listings: a package installed while the builder runs (`bun add`) resolves only
290
+ //? in a fresh process, so one installed since this process began goes to a worker. Any other miss, a subpath typo
291
+ //? of a package installed long ago included, fails here as the worker would.
292
+ const installed =
293
+ resolved === null && CsrDevPaths.installedSince(specifier, path.dirname(importer), this.#startedAt);
294
+ if (!this.#prepassDone || moved) {
295
+ //? A specifier no resolver finds in the user's own code is a typo, failed here like a missing relative file: the
296
+ //? resolution build a worker would run for it fails the same way. A package's `browser` field may map one away.
297
+ if (resolved === null && !installed && !CsrDevPaths.isVendorFile(importer)) throw cannotResolve();
298
+ return null;
299
+ }
300
+ if (!resolved) {
301
+ if (installed) return null;
302
+ throw cannotResolve();
303
+ }
304
+ if (!path.isAbsolute(resolved)) return `${CsrDevPaths.stubPrefix}${specifier}`;
305
+ this.#fallbacks.add(specifier);
306
+ this.#runtimeResolved.set(importer, (this.#runtimeResolved.get(importer) ?? new Set<string>()).add(specifier));
307
+ return CsrDevPaths.realpath(resolved);
308
+ }
309
+
310
+ static #onDisk(resolved: string | null): string | null {
311
+ return resolved && path.isAbsolute(resolved) && fs.existsSync(resolved) ? resolved : null;
312
+ }
313
+
314
+ //? The browser build tree-shakes the unused re-exports of a side-effect-free barrel, so their imports have no
315
+ //? recorded resolution; a file of the same package resolves a bare specifier to the same target, conditions included.
316
+ //? Not one the runtime resolver found (it ignores the `browser` condition the prepass exists to honour), not the
317
+ //? importer's own record, and not a target moved away since.
318
+ #resolvedBySibling(importer: string, specifier: string): string | null {
319
+ const scope = CsrDevResolver.#packageScopeOf(importer);
320
+ for (const [other, bySpecifier] of this.#resolution) {
321
+ const target = bySpecifier.get(specifier);
322
+ if (!target || other === importer || this.#runtimeResolved.get(other)?.has(specifier)) continue;
323
+ if (!CsrDevPaths.isStub(target) && !fs.existsSync(target)) continue;
324
+ if (CsrDevResolver.#packageScopeOf(other) === scope) return target;
325
+ }
326
+ return null;
327
+ }
328
+
329
+ #remember(importer: string, specifier: string, target: string): void {
330
+ const byImporter = this.#resolution.get(importer) ?? new Map<string, string>();
331
+ this.#resolution.set(importer, byImporter.set(specifier, target));
332
+ }
333
+
334
+ //? Only the barrel a bare import names is inlined, so its icons tree-shake per importer; what the barrel itself
335
+ //? imports stays a registry module, keeping shared state such as react-icons' IconContext a single instance.
336
+ #isInlineSpecifier(specifier: string): boolean {
337
+ if (specifier.startsWith(".") || path.isAbsolute(specifier) || specifier.startsWith("node:")) return false;
338
+ return this.#context.optimizeImports.some((pattern) => {
339
+ const base = CsrDevResolver.#patternBase(pattern);
340
+ return specifier === base || specifier.startsWith(`${base}/`);
341
+ });
342
+ }
343
+
344
+ static #patternBase(pattern: string): string {
345
+ return pattern.endsWith("/*") ? pattern.slice(0, -2) : pattern;
346
+ }
347
+
348
+ static #packageNameOf(file: string): string | null {
349
+ const marker = `${path.sep}node_modules${path.sep}`;
350
+ const index = file.lastIndexOf(marker);
351
+ if (index < 0) return null;
352
+ const [scope, name] = file.slice(index + marker.length).split(path.sep);
353
+ return (scope?.startsWith("@") ? `${scope}/${name}` : scope) || null;
354
+ }
355
+
356
+ static #packageScopeOf(file: string): string {
357
+ const marker = `${path.sep}node_modules${path.sep}`;
358
+ const index = file.lastIndexOf(marker);
359
+ if (index < 0) return "";
360
+ const [scope, name] = file.slice(index + marker.length).split(path.sep);
361
+ return file.slice(0, index + marker.length) + (scope?.startsWith("@") ? `${scope}${path.sep}${name}` : scope);
362
+ }
363
+ }
@@ -0,0 +1,61 @@
1
+ import type { PageEntry } from "../artifact/implicitRootLayout";
2
+
3
+ export interface CsrDevGraphModule {
4
+ vendor: boolean;
5
+ mtimeMs: number;
6
+ hash: string;
7
+ deps: string[];
8
+ helpers?: string;
9
+ }
10
+
11
+ export interface CsrDevGraph {
12
+ version: 1;
13
+ configKey: string;
14
+ refresh: string;
15
+ entries: Record<string, string>;
16
+ modules: Record<string, CsrDevGraphModule>;
17
+ resolution: Record<string, Record<string, string>>;
18
+ /** Specifiers per importer id `resolution` holds from Bun's runtime resolver rather than the browser build. */
19
+ runtimeResolved?: Record<string, string[]>;
20
+ pending: string[];
21
+ /** A build worker's failed round: the ids it built, and the files it failed in or named, by hash. */
22
+ failed?: { roots: string[]; files: Record<string, string> };
23
+ /** The stamp of the signal and dictionary sources the last whole build inlined; see `RegistryMetadataFingerprint`. */
24
+ metadata?: string;
25
+ }
26
+
27
+ export interface CsrDevContext {
28
+ pageEntries: PageEntry[];
29
+ basePaths: string[];
30
+ htmlBasePaths: string[];
31
+ define: Record<string, string>;
32
+ optimizeImports: string[];
33
+ configKey: string;
34
+ /** Null when the page already loads the React Refresh runtime (an SSR page's import map). */
35
+ refreshFile: string | null;
36
+ /** Bare specifiers the page already loads (an SSR page's import map): required as `vendor:<specifier>`, not compiled. */
37
+ externals?: readonly string[];
38
+ }
39
+
40
+ export interface CsrDevSharedHelpers {
41
+ hash: string;
42
+ definition: string;
43
+ }
44
+
45
+ export interface CsrDevCompiledModule {
46
+ id: string;
47
+ file: string;
48
+ vendor: boolean;
49
+ factory: string;
50
+ helpers: CsrDevSharedHelpers | null;
51
+ sourceMap?: string;
52
+ deps: string[];
53
+ mtimeMs: number;
54
+ hash: string;
55
+ }
56
+
57
+ /** Module factories by id and helper definitions by hash, kept in memory so app.js is written without a pass over the disk. */
58
+ export interface CsrDevCode {
59
+ modules: Map<string, string>;
60
+ helpers: Map<string, string>;
61
+ }
@@ -0,0 +1,24 @@
1
+ import path from "node:path";
2
+ import type { PageEntry } from "../artifact/implicitRootLayout";
3
+
4
+ //* The CSR entry file of each basePath's HTML, shared by the CSR artifact and the dev registry. Apart from both
5
+ //* builders so the resident dev builder names entries without loading the artifact builder's CSS compiler (tailwindcss).
6
+ export class CsrEntryFiles {
7
+ static entryFilename(basePath: string): string {
8
+ return `${basePath || "index"}.csr.tsx`;
9
+ }
10
+
11
+ static pageEntriesForBasePath(pageEntries: PageEntry[], basePath: string, basePaths: string[]): PageEntry[] {
12
+ return pageEntries.filter((entry) => {
13
+ const entryBasePath = CsrEntryFiles.basePathOfPageKey(entry.key, basePaths);
14
+ return entryBasePath === null || entryBasePath === basePath;
15
+ });
16
+ }
17
+
18
+ static basePathOfPageKey(pageKey: string, basePaths: string[]): string | null {
19
+ const normalized = pageKey.split(path.sep).join("/").replace(/^\.\//, "");
20
+ const segments = normalized.split("/");
21
+ const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
22
+ return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
23
+ }
24
+ }
@@ -17,18 +17,12 @@ describe("CssCandidateCache token extraction", () => {
17
17
  expect(candidates).toEqual(expect.arrayContaining(["w-full", "hover:bg-muted", "md:w-1/2", "text-[#fff]"]));
18
18
  });
19
19
 
20
- // A candidate that opens with `[` used to be torn into `_td` + `px-3`, so every descendant-variant
21
- // rule in the framework compiled to nothing and its element rendered unstyled with no diagnostic.
22
20
  test("keeps an arbitrary variant that starts with a bracket", async () => {
23
21
  const candidates = await scan(`const c = "[&_td]:px-3 [&>*]:gap-2 [&_tbody_tr]:border-t";`);
24
22
  expect(candidates).toEqual(expect.arrayContaining(["[&_td]:px-3", "[&>*]:gap-2", "[&_tbody_tr]:border-t"]));
25
23
  expect(candidates).not.toContain("_td");
26
24
  });
27
25
 
28
- // A labelled tuple type looks like an arbitrary property, and compiles to a rule whose declaration is
29
- // TypeScript. `optimize()` cannot parse that, and drops far more than the offending rule on its way
30
- // out — every variant in the build went missing, so nothing hovered, focused or responded to a
31
- // breakpoint. Tailwind writes a space in an arbitrary value as `_`, so whitespace rules the token out.
32
26
  test("ignores a labelled tuple type that only looks like an arbitrary property", async () => {
33
27
  const candidates = await scan(
34
28
  `type A = [key: string, make: () => unknown][];\ntype B = [setQueryArgs: (a: Args) => Args, options?: Policy];`,
@@ -1,18 +1,7 @@
1
1
  import { stat } from "node:fs/promises";
2
2
 
3
- /**
4
- * Every identifier-ish token in a source file is a potential tailwind class. The leading alternation
5
- * matters: an arbitrary variant starts with `[` (`[&_td]:px-3`, `[&>*]:gap-2`), and a pattern anchored
6
- * on a word character tears it into `_td` plus `px-3` instead — both meaningless, so the rule compiles
7
- * to nothing and the element silently renders unstyled.
8
- *
9
- * Brackets must hold no whitespace. Tailwind spells a space in an arbitrary value `_`, so a bracket
10
- * group containing one is never a class — but it is exactly the shape of a labelled tuple type
11
- * (`[key: string, make: () => unknown]`). Scanned as a candidate, that compiles to an arbitrary
12
- * *property* rule carrying TypeScript as its declaration, which the lightningcss pass in `optimize()`
13
- * cannot parse; its error recovery then drops a whole span of the stylesheet — in practice every
14
- * variant rule in the build, leaving hover, focus, responsive and dark styles silently absent.
15
- */
3
+ // The leading `[` alternation keeps arbitrary variants (`[&_td]:px-3`) whole. Brackets hold no whitespace: a labelled
4
+ // tuple type scanned as a class compiles to a rule lightningcss cannot parse, and it then drops every variant rule.
16
5
  const CANDIDATE_RE = /-?(?:[\w@]|\[[^\]\s]+\])[\w:/.-]*(?:\[[^\]\s]+\][\w:/.-]*)*/g;
17
6
 
18
7
  interface CachedFile {
@@ -26,19 +15,9 @@ interface CacheFile {
26
15
  files: Record<string, CachedFile>;
27
16
  }
28
17
 
29
- /**
30
- * Tailwind candidate tokens per source file, cached on disk across builds.
31
- *
32
- * The scan reads the **full text** of every source file on every CSS rebuild — measured at 385-508ms
33
- * per save on `apps/akan`. Phase 2 moved css compilation into a per-generation batch worker, so an
34
- * in-memory cache (what `03-phase3-topology-and-trim.md` §3.4 originally proposed) buys nothing: the
35
- * process that would hold it exits before the next save. Disk is what survives the worker, a builder
36
- * recycle and a dev-host restart alike, which is the same reasoning §3.3 used for the font cache.
37
- *
38
- * Keyed on (mtime, size) per file, so a save re-reads only the files in that batch.
39
- */
18
+ /** Tailwind candidates per source file, on disk: the batch worker that compiles CSS exits before the next save. */
40
19
  export class CssCandidateCache {
41
- /** Bump when the token regex or the entry shape changes, so stale extractions are not reused. */
20
+ // Bump when the token regex or the entry shape changes, so stale extractions are not reused.
42
21
  static readonly #version = 3;
43
22
  readonly #path: string;
44
23
  readonly #entries = new Map<string, CachedFile>();
@@ -58,7 +37,6 @@ export class CssCandidateCache {
58
37
  return this.#rescanned;
59
38
  }
60
39
 
61
- /** A cache that cannot be read or is a version behind simply starts empty — it is only an optimisation. */
62
40
  async load(): Promise<this> {
63
41
  const raw = (await Bun.file(this.#path)
64
42
  .json()
@@ -71,12 +49,7 @@ export class CssCandidateCache {
71
49
  return this;
72
50
  }
73
51
 
74
- /**
75
- * The file's candidate tokens, read from disk only when its (mtime, size) no longer matches.
76
- *
77
- * Read errors propagate, as they did before this cache existed: a source file the compiler cannot
78
- * read is a broken build, not a cache miss to paper over.
79
- */
52
+ // A read error propagates: an unreadable source is a broken build, not a cache miss.
80
53
  async candidatesFor(file: string): Promise<string[]> {
81
54
  const stats = await stat(file).catch(() => null);
82
55
  const cached = this.#entries.get(file);
@@ -94,13 +67,6 @@ export class CssCandidateCache {
94
67
  return candidates;
95
68
  }
96
69
 
97
- /**
98
- * Persist, dropping files the scan no longer reaches so a renamed or deleted module does not keep
99
- * feeding its classes to the compiler forever.
100
- *
101
- * A rebuild that re-read nothing writes nothing: the boot double-build and any CSS rebuild triggered
102
- * by something other than a source edit would otherwise rewrite the whole file for no change.
103
- */
104
70
  async save(present: Set<string>): Promise<void> {
105
71
  for (const file of [...this.#entries.keys()]) {
106
72
  if (present.has(file)) continue;
@@ -111,8 +77,7 @@ export class CssCandidateCache {
111
77
  this.#dirty = false;
112
78
  const files: Record<string, CachedFile> = {};
113
79
  for (const [file, entry] of this.#entries) files[file] = entry;
114
- // A cache that cannot be written is a slow build, not a failed one — a read-only checkout must
115
- // still compile.
80
+ // An unwritable cache is a slow build, not a failed one: a read-only checkout must still compile.
116
81
  await Bun.write(
117
82
  this.#path,
118
83
  JSON.stringify({ version: CssCandidateCache.#version, files } satisfies CacheFile),