@akanjs/devkit 3.0.0-beta.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
@@ -1,13 +1,14 @@
1
1
  import path from "node:path";
2
2
  import { Logger } from "akanjs/common";
3
3
  import { compile } from "tailwindcss";
4
+ import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
4
5
  import type { App } from "../commandDecorators";
5
6
  import { BarrelAnalyzer } from "../transforms/barrelAnalyzer";
6
7
  import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
8
+ import { CsrEntryFiles } from "./csrEntryFiles";
7
9
  import { CssCandidateCache } from "./cssCandidateCache";
8
10
  import { CssImportResolver } from "./cssImportResolver";
9
11
 
10
- const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"] as const;
11
12
  const NON_SOURCE_EXT_RE = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
12
13
  const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
13
14
  const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
@@ -17,7 +18,6 @@ interface CssDiscovery {
17
18
  sourcePaths: string[];
18
19
  }
19
20
 
20
- /** One `@import` target and the custom properties it declares, which is what proves it arrived downstream. */
21
21
  export interface ImportedStylesheet {
22
22
  cssPath: string;
23
23
  declaredNames: string[];
@@ -32,32 +32,18 @@ export class CssCompiler {
32
32
  this.#app = app;
33
33
  }
34
34
 
35
- /**
36
- * Beside the sources rather than in `dist`, because the cache is keyed on source mtimes: a build and
37
- * the dev server describe the same files, so they should share it rather than each pay the first scan.
38
- */
35
+ // Beside the sources, not in `dist`: the build and the dev server scan the same files, so they share one cache.
39
36
  get #candidateCachePath() {
40
37
  return path.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
41
38
  }
42
39
 
43
40
  #cssText: string | null = null;
44
41
  #cssTextByBasePath: Record<string, string> | null = null;
45
- /**
46
- * Import resolution memoised for the life of one compiler, which is one rebuild.
47
- *
48
- * The discovery BFS resolves the same specifier from every directory that imports it, and each miss
49
- * costs up to 13 sequential `exists()` calls — the bare path, six extensions, six `index.*`. Nothing
50
- * here outlives the rebuild, so there is no invalidation to get wrong.
51
- */
42
+ // Memoised for one compiler, i.e. one rebuild: nothing outlives it, so there is nothing to invalidate.
52
43
  #fileExistsCache = new Map<string, Promise<boolean>>();
53
44
  #resolvedFileCache = new Map<string, Promise<string | null>>();
54
45
  #resolvedSpecifierCache = new Map<string, Promise<string | null>>();
55
- /** Every stylesheet this compile reached, entry points and `@import` targets alike. */
56
46
  #discoveredCssPaths = new Set<string>();
57
- /**
58
- * `@import` targets per base path, so whoever writes the asset can check the file it actually wrote — the
59
- * compiled text and the written artifact are two different places a declaration can go missing.
60
- */
61
47
  importedStylesheetsByBasePath: Record<string, ImportedStylesheet[]> = {};
62
48
 
63
49
  #fileExists(absPath: string): Promise<boolean> {
@@ -107,41 +93,31 @@ export class CssCompiler {
107
93
  const pageKeys = await this.#app.getPageKeys({ refresh });
108
94
  const basePaths = [...akanConfig.basePaths];
109
95
  const rootPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === null);
96
+ const compileBasePath = async (basePath: string, basePathPageKeys: string[], label = basePath) => {
97
+ if (basePathPageKeys.length === 0) return [basePath, ""] as const;
98
+ const started = Date.now();
99
+ const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
100
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
101
+ this.importedStylesheetsByBasePath[basePath] = imported;
102
+ this.#logger.verbose(
103
+ `css base=${label} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
104
+ );
105
+ return [basePath, css] as const;
106
+ };
110
107
  const cssEntries = await Promise.all([
111
- (async () => {
112
- if (rootPageKeys.length === 0) return ["", ""] as const;
113
- const started = Date.now();
114
- const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: rootPageKeys });
115
- const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
116
- this.importedStylesheetsByBasePath[""] = imported;
117
- this.#logger.verbose(
118
- `css base=root paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
119
- );
120
- return ["", css] as const;
121
- })(),
122
- ...basePaths.map(async (basePath) => {
123
- const basePathPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath);
124
- if (basePathPageKeys.length === 0) return [basePath, ""] as const;
125
- const started = Date.now();
126
- const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
127
- const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
128
- this.importedStylesheetsByBasePath[basePath] = imported;
129
- this.#logger.verbose(
130
- `css base=${basePath} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
131
- );
132
- return [basePath, css] as const;
133
- }),
108
+ compileBasePath("", rootPageKeys, "root"),
109
+ ...basePaths.map((basePath) =>
110
+ compileBasePath(
111
+ basePath,
112
+ pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath),
113
+ ),
114
+ ),
134
115
  ]);
135
116
  this.#cssTextByBasePath = Object.fromEntries(cssEntries);
136
117
  await this.#warnUnreachableStylesheets();
137
118
  return this.#cssTextByBasePath;
138
119
  }
139
120
 
140
- /**
141
- * A stylesheet under `page/` reaches the build only by being imported from a route source. One that nothing
142
- * imports compiles to nothing and reports success, which is indistinguishable from an empty theme — so say it
143
- * out loud once per compile rather than leaving it to be noticed as unstyled elements in the browser.
144
- */
145
121
  async #warnUnreachableStylesheets() {
146
122
  const pageDir = path.join(this.#app.cwdPath, "page");
147
123
  const glob = new Bun.Glob("**/*.css");
@@ -225,11 +201,7 @@ export class CssCompiler {
225
201
  return { cssPaths, sourcePaths: [...sourceFiles] };
226
202
  }
227
203
 
228
- /**
229
- * `libs/<lib>/ui/tokens.css` of every lib the page graph reached, so a lib can own the fixed colours its own
230
- * components need instead of each consuming app re-declaring them. Ordered ahead of the app's stylesheets:
231
- * the app is the last word on any variable both declare.
232
- */
204
+ // Lib tokens go ahead of the app's stylesheets, so the app has the last word on a variable both declare.
233
205
  async #libTokenStylesheets(sourceFiles: Set<string>): Promise<string[]> {
234
206
  const libsRoot = path.join(this.#app.workspace.workspaceRoot, "libs");
235
207
  const libNames = new Set<string>();
@@ -252,10 +224,7 @@ export class CssCompiler {
252
224
  return css;
253
225
  }
254
226
 
255
- /**
256
- * The collector is per compile rather than per compiler instance: `getCssByBasePath` compiles every base path
257
- * concurrently, so instance state would mix one base path's imports into another's check.
258
- */
227
+ // A per-compile collector: getCssByBasePath compiles base paths concurrently, so instance state would mix them.
259
228
  async #compileWithImports(
260
229
  cssPaths: string[],
261
230
  sourcePaths: string[],
@@ -279,7 +248,6 @@ export class CssCompiler {
279
248
 
280
249
  const sourceDirs = new Set<string>();
281
250
  for (const entry of compilers) {
282
- if (!entry) continue;
283
251
  for (const s of entry.compiler.sources as { base: string }[]) sourceDirs.add(s.base);
284
252
  }
285
253
  const scanStarted = Date.now();
@@ -290,7 +258,6 @@ export class CssCompiler {
290
258
  const parts: string[] = [];
291
259
  const imported: ImportedStylesheet[] = [];
292
260
  for (const entry of compilers) {
293
- if (!entry) continue;
294
261
  const part = entry.compiler.build(candidates);
295
262
  parts.push(part);
296
263
  for (const [cssPath, content] of entry.imported) {
@@ -317,28 +284,19 @@ export class CssCompiler {
317
284
  return { path: p, base: path.dirname(p), content };
318
285
  }
319
286
 
320
- /**
321
- * Every specifier is verified here, path-shaped ones included. An `@import` the pipeline cannot resolve is
322
- * a build error and never a no-op: the vocabulary closure means a component whose token declaration failed
323
- * to load renders unstyled, which nothing downstream can distinguish from a design choice.
324
- */
287
+ // An unresolvable @import throws: under vocabulary closure a missing token file just renders unstyled.
325
288
  async #resolveCssImport(id: string, fromBase: string): Promise<string> {
326
289
  if (id.startsWith(".") || id.startsWith("/")) {
327
290
  const filePath = path.resolve(fromBase, id);
328
291
  if (await this.#fileExists(filePath)) return filePath;
329
292
  throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase} (no file at ${filePath})`);
330
293
  }
331
- const resolver = await this.#getCssImportResolver();
332
- const resolved = await resolver.resolve(id, fromBase);
294
+ this.#cssImportResolver ??= await CssImportResolver.create(this.#app);
295
+ const resolved = await this.#cssImportResolver.resolve(id, fromBase);
333
296
  if (resolved) return resolved;
334
297
  throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
335
298
  }
336
299
 
337
- async #getCssImportResolver() {
338
- if (this.#cssImportResolver) return this.#cssImportResolver;
339
- this.#cssImportResolver = await CssImportResolver.create(this.#app);
340
- return this.#cssImportResolver;
341
- }
342
300
  async #loadModule(id: string, fromBase: string) {
343
301
  const p = require.resolve(id, { paths: [fromBase] });
344
302
  const mod = await import(p);
@@ -349,10 +307,7 @@ export class CssCompiler {
349
307
  fromBase: string,
350
308
  resolvePackage: Awaited<ReturnType<typeof createTsconfigPackageResolver>>,
351
309
  ): Promise<string | null> {
352
- // Keyed by importer directory even for bare specifiers: the tsconfig resolver is
353
- // directory-independent, but the `Bun.resolveSync` / `require.resolve` fallbacks below are not.
354
- // The expensive part — the `exists()` probes — is deduplicated by absolute path instead, which is
355
- // shared across every importer.
310
+ // Keyed by importer directory even for bare specifiers: the Bun/require fallbacks are directory-dependent.
356
311
  const cacheKey = `${fromBase}\0${id}`;
357
312
  let cached = this.#resolvedSpecifierCache.get(cacheKey);
358
313
  if (cached) return cached;
@@ -423,17 +378,14 @@ function resolveSourceWithRequire(id: string, fromBase: string): string | null {
423
378
  }
424
379
 
425
380
  function isSourceFile(filePath: string) {
426
- return SOURCE_EXTS.includes(path.extname(filePath) as (typeof SOURCE_EXTS)[number]);
381
+ return SOURCE_EXTS.has(path.extname(filePath));
427
382
  }
428
383
 
429
384
  export function isIgnoredNodeModuleSource(filePath: string): boolean {
430
385
  return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
431
386
  }
432
387
 
433
- /**
434
- * `@theme` blocks are stripped first: those variables are emitted only when a utility uses one, so their
435
- * absence from a build says nothing about whether the stylesheet arrived.
436
- */
388
+ /** Skips `@theme` blocks: Tailwind emits those variables only when a utility uses one. */
437
389
  export function declaredCustomProperties(css: string): string[] {
438
390
  const withoutThemeBlocks = css.replace(/@theme[^{]*\{[^}]*\}/g, "");
439
391
  return [...new Set([...withoutThemeBlocks.matchAll(/(?:^|[\s;{])(--[\w-]+)\s*:/g)].map(([, name]) => name))].filter(
@@ -442,8 +394,5 @@ export function declaredCustomProperties(css: string): string[] {
442
394
  }
443
395
 
444
396
  export function getPageKeyBasePath(pageKey: string, basePaths: string[]): string | null {
445
- const normalized = pageKey.split(path.sep).join("/").replace(/^\.\//, "");
446
- const segments = normalized.split("/");
447
- const firstPublicSegment = segments.find((segment) => segment !== "[lang]" && !/^\(.+\)$/.test(segment));
448
- return firstPublicSegment && basePaths.includes(firstPublicSegment) ? firstPublicSegment : null;
397
+ return CsrEntryFiles.basePathOfPageKey(pageKey, basePaths);
449
398
  }
@@ -25,8 +25,8 @@ export class CssImportResolver {
25
25
  async resolve(id: string, fromBase: string): Promise<string | null> {
26
26
  for (const resolve of [
27
27
  () => this.#resolveWithTsconfig(id),
28
- () => this.#resolveWithBun(id, fromBase),
29
- () => this.#resolveWithRequire(id, fromBase),
28
+ () => this.#resolveCssWith(fromBase, (base) => Bun.resolveSync(id, base)),
29
+ () => this.#resolveCssWith(fromBase, (base) => require.resolve(id, { paths: [base] })),
30
30
  () => this.#resolvePackageStyle(id, fromBase),
31
31
  ]) {
32
32
  const resolved = await resolve();
@@ -35,22 +35,10 @@ export class CssImportResolver {
35
35
  return null;
36
36
  }
37
37
 
38
- #resolveWithBun(id: string, fromBase: string): string | null {
38
+ #resolveCssWith(fromBase: string, resolveFrom: (base: string) => string): string | null {
39
39
  for (const base of this.#resolutionBases(fromBase)) {
40
40
  try {
41
- const resolved = Bun.resolveSync(id, base);
42
- if (CssImportResolver.isCssFile(resolved)) return resolved;
43
- } catch {
44
- // Try the next known package resolution root.
45
- }
46
- }
47
- return null;
48
- }
49
-
50
- #resolveWithRequire(id: string, fromBase: string): string | null {
51
- for (const base of this.#resolutionBases(fromBase)) {
52
- try {
53
- const resolved = require.resolve(id, { paths: [base] });
41
+ const resolved = resolveFrom(base);
54
42
  if (CssImportResolver.isCssFile(resolved)) return resolved;
55
43
  } catch {
56
44
  // Try the next known package resolution root.
@@ -111,8 +99,7 @@ export class CssImportResolver {
111
99
  ? exportValue
112
100
  : exportValue?.style || exportValue?.import || exportValue?.default;
113
101
  if (exportedEntry) return await this.#firstExisting(path.resolve(pkgDir, exportedEntry));
114
- //* A subpath names a file inside the package, so it resolves literally. Falling back to the package's own
115
- //* style entry here would load a different stylesheet than the author asked for and report success.
102
+ //* A subpath resolves literally: the package's own style entry would silently load a different stylesheet.
116
103
  if (subpath !== ".") return await this.#firstExisting(path.resolve(pkgDir, subpath));
117
104
  return await this.#firstExisting(path.resolve(pkgDir, pkg.exports?.["."]?.style || pkg.style || "index.css"));
118
105
  } catch {
@@ -135,7 +122,7 @@ export class CssImportResolver {
135
122
 
136
123
  async #firstExisting(basePath: string): Promise<string | null> {
137
124
  for (const suffix of CSS_IMPORT_EXTS) {
138
- const candidate = `${basePath}${suffix}`;
125
+ const candidate = path.normalize(`${basePath}${suffix}`);
139
126
  if (await Bun.file(candidate).exists()) return candidate;
140
127
  }
141
128
  return null;
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import type { ChangeKind, DevChangeAction, DevChangePlan, DevChangeRole } from "akanjs/server";
3
+ import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
3
4
 
4
- const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
5
5
  const CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
6
6
  const BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
7
7
  const CLIENT_SUFFIXES = [".Template.tsx", ".Unit.tsx", ".Util.tsx", ".View.tsx", ".Zone.tsx", ".store.ts"];
@@ -93,15 +93,15 @@ export class DevChangePlanner {
93
93
  roles.add("barrel");
94
94
  reasons.add(isGenerated ? "generated-index" : "barrel-facet-child");
95
95
  }
96
- if (isSource && this.#isServerBiased(abs, parts)) {
96
+ if (isSource && this.#isServerBiased(base, parts)) {
97
97
  roles.add("server");
98
98
  reasons.add("server-path");
99
99
  }
100
- if (isSource && this.#isClientBiased(abs, parts)) {
100
+ if (isSource && this.#isClientBiased(base, parts)) {
101
101
  roles.add("client");
102
102
  reasons.add("client-path");
103
103
  }
104
- if (isSource && this.#isSharedBiased(abs, parts)) {
104
+ if (isSource && this.#isSharedBiased(base, parts)) {
105
105
  roles.add("shared");
106
106
  reasons.add("shared-path");
107
107
  }
@@ -123,8 +123,7 @@ export class DevChangePlanner {
123
123
  return roles;
124
124
  }
125
125
 
126
- #isServerBiased(abs: string, parts: string[]): boolean {
127
- const base = path.basename(abs);
126
+ #isServerBiased(base: string, parts: string[]): boolean {
128
127
  return (
129
128
  parts.includes("srvkit") ||
130
129
  SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) ||
@@ -133,8 +132,7 @@ export class DevChangePlanner {
133
132
  );
134
133
  }
135
134
 
136
- #isClientBiased(abs: string, parts: string[]): boolean {
137
- const base = path.basename(abs);
135
+ #isClientBiased(base: string, parts: string[]): boolean {
138
136
  return (
139
137
  parts.includes("ui") ||
140
138
  parts.includes("webkit") ||
@@ -143,8 +141,7 @@ export class DevChangePlanner {
143
141
  );
144
142
  }
145
143
 
146
- #isSharedBiased(abs: string, parts: string[]): boolean {
147
- const base = path.basename(abs);
144
+ #isSharedBiased(base: string, parts: string[]): boolean {
148
145
  return (
149
146
  parts.includes("common") ||
150
147
  SHARED_SUFFIXES.some((suffix) => base.endsWith(suffix)) ||
@@ -1,16 +1,10 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
4
+ import { tempDirs } from "../testHelpers";
5
5
  import { DevGeneratedIndexSync } from "./devGeneratedIndexSync";
6
6
 
7
- const tempRoots: string[] = [];
8
-
9
- const makeTempRoot = async () => {
10
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-generated-index-"));
11
- tempRoots.push(root);
12
- return root;
13
- };
7
+ const makeTempRoot = tempDirs("akan-generated-index-");
14
8
 
15
9
  const seedFacet = async (root: string, facet: string, { files, dirs }: { files: string[]; dirs: string[] }) => {
16
10
  const dir = path.join(root, "libs", "util", facet);
@@ -28,23 +22,19 @@ const barrelFor = async (root: string, facet: string, seed: { files: string[]; d
28
22
  return readFile(path.join(dir, "index.ts"), "utf8");
29
23
  };
30
24
 
31
- afterEach(async () => {
32
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
33
- });
34
-
35
25
  describe("DevGeneratedIndexSync facet barrels", () => {
36
26
  test("camelCase facets export only clean camelCase names", async () => {
37
27
  const content = await barrelFor(await makeTempRoot(), "srvkit", {
38
28
  files: [
39
29
  "aes.ts",
40
30
  "cloudflareApi.ts",
41
- "cloudflareApi.helper.ts", // dotted → skipped
42
- "pushNotificationServer.type.ts", // dotted → skipped
43
- "PushNotificationServer.ts", // PascalCase in camel facet → skipped
44
- "my_snake.ts", // snake_case → skipped
45
- "kebab-case.ts", // kebab-case → skipped
31
+ "cloudflareApi.helper.ts",
32
+ "pushNotificationServer.type.ts",
33
+ "PushNotificationServer.ts",
34
+ "my_snake.ts",
35
+ "kebab-case.ts",
46
36
  ],
47
- dirs: ["storageApi", "BadDir"], // PascalCase dir → skipped
37
+ dirs: ["storageApi", "BadDir"],
48
38
  trigger: "aes.ts",
49
39
  });
50
40
  expect(content).toBe(`export * from "./aes";\nexport * from "./cloudflareApi";\nexport * from "./storageApi";\n`);
@@ -52,13 +42,8 @@ describe("DevGeneratedIndexSync facet barrels", () => {
52
42
 
53
43
  test("ui facet exports only clean PascalCase names", async () => {
54
44
  const content = await barrelFor(await makeTempRoot(), "ui", {
55
- files: [
56
- "Globe.tsx",
57
- "AkanLogo.tsx",
58
- "Globe_Dynamic.tsx", // underscore → skipped
59
- "lowerStart.tsx", // camelCase in ui facet → skipped
60
- ],
61
- dirs: ["Code", "badDir"], // camelCase dir → skipped
45
+ files: ["Globe.tsx", "AkanLogo.tsx", "Globe_Dynamic.tsx", "lowerStart.tsx"],
46
+ dirs: ["Code", "badDir"],
62
47
  trigger: "Globe.tsx",
63
48
  });
64
49
  expect(content).toBe(`export * from "./AkanLogo";\nexport * from "./Code";\nexport * from "./Globe";\n`);
@@ -1,12 +1,11 @@
1
1
  import { mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { capitalize } from "akanjs/common";
3
4
  import { FileSys } from "../fileSys";
4
5
 
5
6
  const BARREL_FACETS = new Set(["common", "srvkit", "ui", "webkit", "plugin"]);
6
7
  const FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
7
8
  const FACET_EXCLUDED_FILE_RE = /(^index\.tsx?$|\.d\.ts$|\.(test|spec)\.(ts|tsx)$|\.css$|\.scss$|\.sass$)/;
8
- // `ui` exports PascalCase names only; `common`/`srvkit`/`webkit` export camelCase names only. Names with
9
- // dots, underscores, or hyphens (e.g. `foo.helper`, `Globe_Dynamic`, `kebab-case`) match neither and are skipped.
10
9
  const FACET_PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
11
10
  const FACET_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
12
11
  const MODULE_UI_TYPES = ["Template", "Unit", "Util", "View", "Zone"] as const;
@@ -159,6 +158,4 @@ const exists = async (file: string) =>
159
158
  .then(() => true)
160
159
  .catch(() => false);
161
160
 
162
- const capitalize = (value: string) => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
163
-
164
161
  const formatError = (err: unknown) => (err instanceof Error ? err.message : String(err));
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isAkanRuntimeMetadataFile } from "akanjs/server/hmr/runtimeMetadataFile";
4
+ import type { App } from "../commandDecorators";
5
+ import { CsrDevArtifactWriter } from "./csrDevArtifactWriter";
6
+ import { CsrDevModuleCompiler } from "./csrDevModuleCompiler";
7
+ import { CsrDevPatcher } from "./csrDevPatcher";
8
+ import { CsrDevPaths } from "./csrDevPaths";
9
+ import { CsrDevResolver } from "./csrDevResolver";
10
+ import type { CsrDevCompiledModule, CsrDevContext, CsrDevGraph } from "./csrDevTypes";
11
+ import { RegistryMetadataFingerprint } from "./registryMetadataFingerprint";
12
+ import { WatchRootResolver } from "./watchRootResolver";
13
+
14
+ export interface CsrDevUpdate {
15
+ generation: number;
16
+ reload: boolean;
17
+ reason?: string;
18
+ patchUrl?: string;
19
+ changedIds: string[];
20
+ moduleCount: number;
21
+ epoch?: number;
22
+ /** The registry did not exist before this build, so no tab holds a module of it. */
23
+ first?: boolean;
24
+ }
25
+
26
+ export interface CsrDevUpdateOptions {
27
+ /** Called once an update exists: for a patch before app.js is rewritten, for a reload after everything is on disk. */
28
+ announce?: (update: CsrDevUpdate) => void;
29
+ /** Files the registry must hold once this update is done; the ones it lacks are compiled into it. */
30
+ roots?: string[];
31
+ /**
32
+ * Compile only the roots the registry lacks: a route build asks for its entries, and a file saved meanwhile is the
33
+ * save's own patch to make, with the hold only that patch knows.
34
+ */
35
+ onlyRoots?: boolean;
36
+ }
37
+
38
+ export interface DevRegistryTarget {
39
+ dirName: string;
40
+ routePrefix: string;
41
+ library: boolean;
42
+ }
43
+
44
+ //* A dev module registry under `.akan/artifact/<dirName>`: this owns the context, the entries and the full build;
45
+ //* `CsrDevPatcher` owns the incremental path, which the resident builder keeps in memory between saves.
46
+ export abstract class DevRegistryBundler {
47
+ readonly outDir: string;
48
+ readonly routePrefix: string;
49
+ readonly paths: CsrDevPaths;
50
+ readonly writer: CsrDevArtifactWriter;
51
+ protected readonly app: App;
52
+ protected readonly entryDir: string;
53
+ /** A CSR entry holds the route table, so moving one reloads the tab; an SSR root is one more module to hold. */
54
+ abstract readonly reloadsOnEntryChange: boolean;
55
+
56
+ constructor(app: App, { dirName, routePrefix, library }: DevRegistryTarget) {
57
+ this.app = app;
58
+ this.routePrefix = routePrefix;
59
+ this.paths = new CsrDevPaths(app.workspace.workspaceRoot);
60
+ this.outDir = path.join(app.cwdPath, ".akan/artifact", dirName);
61
+ this.entryDir = path.join(app.cwdPath, ".akan/generated", dirName);
62
+ this.writer = new CsrDevArtifactWriter(this.outDir, { routePrefix, library });
63
+ }
64
+
65
+ /** Null when there is nothing to build. */
66
+ abstract context(): Promise<CsrDevContext | null>;
67
+
68
+ abstract writeEntries(context: CsrDevContext): Promise<{ files: Record<string, string>; changed: string[] }>;
69
+
70
+ /** Every file a full build compiles from, the entries included. */
71
+ protected abstract rootFiles(entryFiles: string[]): Promise<string[]>;
72
+
73
+ // A build worker: a throwaway patcher that starts from the disk, and builds everything itself when it cannot patch.
74
+ async update(changedFiles: string[] = [], options: CsrDevUpdateOptions = {}): Promise<CsrDevUpdate | null> {
75
+ const result = await new CsrDevPatcher(this).update(changedFiles, { ...options, allowWholeAppBuilds: true });
76
+ if (result.kind === "unchanged") return null;
77
+ if (result.kind === "update") return result.update;
78
+ const built = await this.fullBuild(result.context, result.generation, result.reason);
79
+ const update = result.first ? { ...built, first: true } : built;
80
+ options.announce?.(update);
81
+ return update;
82
+ }
83
+
84
+ /** Of these files, the ones the registry still reaches from its roots through imports: those it keeps building. */
85
+ async wantedRoots(files: string[], graph: CsrDevGraph): Promise<Set<string>> {
86
+ const reached = await this.reachable(graph);
87
+ return new Set(files.filter((file) => reached.has(this.paths.idOf(file))));
88
+ }
89
+
90
+ /** The ids its roots reach through imports, the refresh runtime included. */
91
+ async reachable(graph: CsrDevGraph): Promise<Set<string>> {
92
+ const roots = await this.rootFiles(Object.values(graph.entries).map((id) => this.paths.fileOf(id)));
93
+ const reached = new Set<string>();
94
+ const stack = [graph.refresh, ...roots.map((file) => this.paths.idOf(file))];
95
+ for (let id = stack.pop(); id !== undefined; id = stack.pop()) {
96
+ if (reached.has(id)) continue;
97
+ reached.add(id);
98
+ stack.push(...(graph.modules[id]?.deps ?? []));
99
+ }
100
+ return reached;
101
+ }
102
+
103
+ async holds(files: string[]): Promise<boolean> {
104
+ const graph = await this.writer.readJson<CsrDevGraph>("graph.json");
105
+ return graph !== null && files.every((file) => graph.modules[this.paths.idOf(file)] !== undefined);
106
+ }
107
+
108
+ /** A save of one goes to a whole build: `lib/useClient.ts` inlines what they declare through macros. */
109
+ isMetadataFile(file: string): boolean {
110
+ return isAkanRuntimeMetadataFile(file);
111
+ }
112
+
113
+ async metadataFingerprint(): Promise<string> {
114
+ const libsContainer = path.resolve(this.app.workspace.workspaceRoot, "libs");
115
+ const roots = await new WatchRootResolver(this.app).resolve();
116
+ // The whole container comes back when the lib dependencies are unknown (before the first sync).
117
+ const libRoots = roots.includes(libsContainer)
118
+ ? fs.readdirSync(libsContainer).map((name) => path.join(libsContainer, name))
119
+ : roots.filter((root) => path.dirname(root) === libsContainer);
120
+ return await RegistryMetadataFingerprint.of([path.resolve(this.app.cwdPath), ...libRoots], (file) =>
121
+ this.isMetadataFile(file),
122
+ );
123
+ }
124
+
125
+ //? Compiled before anything is written: a build that fails (a module the user broke) leaves the registry the tabs
126
+ //? and the next patch read as it was, where clearing the directory first left no registry at all.
127
+ async fullBuild(context: CsrDevContext, generation: number, reason: string): Promise<CsrDevUpdate> {
128
+ const metadata = await this.metadataFingerprint();
129
+ const entries = await this.writeEntries(context);
130
+ const roots = await this.rootFiles(Object.values(entries.files));
131
+ const resolver = new CsrDevResolver({ paths: this.paths, context, entryFiles: roots });
132
+ await resolver.prepass();
133
+ const compiler = this.compiler(context, resolver);
134
+ const { modules: compiled } = await compiler.compile(
135
+ [...roots, ...(context.refreshFile ? [context.refreshFile] : [])],
136
+ new Set(),
137
+ );
138
+ const graph: CsrDevGraph = {
139
+ version: 1,
140
+ configKey: context.configKey,
141
+ refresh: context.refreshFile
142
+ ? this.paths.idOf(context.refreshFile)
143
+ : `${CsrDevPaths.vendorPrefix}react-refresh/runtime`,
144
+ entries: this.entryIds(entries.files),
145
+ modules: {},
146
+ resolution: {},
147
+ pending: [],
148
+ metadata,
149
+ };
150
+ this.merge(graph, resolver, compiled);
151
+ await this.writer.markBuilding();
152
+ await this.writer.writeModules(compiled);
153
+ const vendorFile = await this.writer.writeVendor(graph);
154
+ const epoch = Date.now();
155
+ await this.writer.writeApp(graph, { generation, vendorFile, epoch });
156
+ await this.writer.writeState(graph, { version: 1, generation, vendorFile, entries: graph.entries, epoch });
157
+ await this.writer.pruneAfterFullBuild(graph, vendorFile, { generation, assets: compiler.assets });
158
+ await this.writer.clearBuilding();
159
+ return { generation, reload: true, reason, changedIds: [], moduleCount: Object.keys(graph.modules).length, epoch };
160
+ }
161
+
162
+ compiler(context: CsrDevContext, resolver: CsrDevResolver): CsrDevModuleCompiler {
163
+ return new CsrDevModuleCompiler({
164
+ app: this.app,
165
+ paths: this.paths,
166
+ resolver,
167
+ context,
168
+ outDir: this.outDir,
169
+ routePrefix: this.routePrefix,
170
+ });
171
+ }
172
+
173
+ merge(graph: CsrDevGraph, resolver: CsrDevResolver, compiled: CsrDevCompiledModule[]): void {
174
+ for (const module of compiled)
175
+ graph.modules[module.id] = {
176
+ vendor: module.vendor,
177
+ mtimeMs: module.mtimeMs,
178
+ hash: module.hash,
179
+ deps: module.deps.map((dep) => (CsrDevPaths.isStub(dep) ? dep : this.paths.idOf(dep))),
180
+ ...(module.helpers ? { helpers: module.helpers.hash } : {}),
181
+ };
182
+ //? Only what this round compiled leaves pending: a root another route's check failed on stays for its retry.
183
+ const compiledIds = new Set(compiled.map((module) => module.id));
184
+ graph.pending = graph.pending.filter((id) => !compiledIds.has(id));
185
+ graph.resolution = resolver.serialize();
186
+ graph.runtimeResolved = resolver.serializeRuntimeResolved();
187
+ }
188
+
189
+ entryIds(files: Record<string, string>): Record<string, string> {
190
+ return Object.fromEntries(Object.entries(files).map(([key, file]) => [key, this.paths.idOf(file)]));
191
+ }
192
+ }