@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,17 +1,16 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, rm, stat, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import type { App } from "../commandDecorators";
5
+ import { tempDirs } from "../testHelpers";
6
6
  import { FontOptimizer } from "./fontOptimizer";
7
7
 
8
8
  const SOURCE_FONT = path.resolve(import.meta.dir, "../../../../libs/shared/public/fonts/Assistant-Regular.woff2");
9
9
 
10
- const tempRoots: string[] = [];
10
+ const tempRoot = tempDirs("akan-devkit-font-");
11
11
 
12
12
  const makeApp = async (layoutSource: string) => {
13
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-font-"));
14
- tempRoots.push(root);
13
+ const root = await tempRoot();
15
14
  const cwdPath = path.join(root, "apps/demo");
16
15
  await mkdir(path.join(cwdPath, "page"), { recursive: true });
17
16
  await mkdir(path.join(cwdPath, "public/fonts"), { recursive: true });
@@ -59,10 +58,6 @@ const chainFontList = (extra = "") => `[\n ${chainFontEntry(extra)},\n ]`;
59
58
 
60
59
  const optimize = (app: App) => new FontOptimizer(app, "start").optimize();
61
60
 
62
- afterEach(async () => {
63
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
64
- });
65
-
66
61
  describe("FontOptimizer cache", () => {
67
62
  test("reuses subset output instead of resubsetting an unchanged font", async () => {
68
63
  const { app } = await makeApp(layoutWith());
@@ -76,7 +71,6 @@ describe("FontOptimizer cache", () => {
76
71
  expect(second.files).toEqual(first.files);
77
72
  expect(second.css).toBe(first.css);
78
73
  expect(second.fonts).toEqual(first.fonts);
79
- // The output was reused, not rewritten — the whole point of the cache.
80
74
  expect((await stat(second.files[0])).mtimeMs).toBe(writtenAt);
81
75
  });
82
76
 
@@ -160,8 +154,6 @@ describe("FontOptimizer discovery", () => {
160
154
  expect(second.css).toContain(".font-brand");
161
155
  });
162
156
 
163
- // A list the build cannot read subsets nothing while the runtime still preloads /_akan/fonts, so the 404s
164
- // have to be announced at build time rather than found in a browser console.
165
157
  test("warns when the chain is handed a font list it cannot read", async () => {
166
158
  const { app, warnings } = await makeApp(chainLayoutWith("brandFonts"));
167
159
 
@@ -170,7 +162,7 @@ describe("FontOptimizer discovery", () => {
170
162
  expect(result.fonts).toEqual([]);
171
163
  expect(result.files).toEqual([]);
172
164
  expect(warnings).toHaveLength(1);
173
- expect(warnings[0]).toContain("page/_layout.tsx");
165
+ expect(warnings[0]).toContain(path.join("page", "_layout.tsx"));
174
166
  });
175
167
 
176
168
  test("warns about an entry it cannot read and keeps the ones it can", async () => {
@@ -21,10 +21,6 @@ export interface OptimizeAppFontsResult {
21
21
  files: string[];
22
22
  }
23
23
 
24
- /**
25
- * Lets a boot that changed nothing about its fonts skip re-subsetting them. `files` is stored relative
26
- * to the artifact root so the `build` and `start` roots keep independent, relocatable caches.
27
- */
28
24
  interface FontOptimizerCache {
29
25
  version: number;
30
26
  key: string;
@@ -43,8 +39,6 @@ export class FontOptimizer {
43
39
  #woff2Ready: Promise<void> | null = null;
44
40
 
45
41
  static #ksX1001Text: string | null = null;
46
- // 2: the key moved to sha256 over a deterministically ordered `auto` text, so v1 entries cannot be
47
- // compared against and their subsets were built from `page`/`ui` only.
48
42
  static readonly #cacheVersion = 2;
49
43
 
50
44
  constructor(app: App, command: FontOptimizerCommand = "start") {
@@ -76,10 +70,7 @@ export class FontOptimizer {
76
70
  return path.join(this.#artifactRoot, "fontCache.json");
77
71
  }
78
72
 
79
- /**
80
- * Null means "do not cache this run": a source we cannot stat is a source whose staleness we cannot
81
- * detect, and the uncached path is also the one that warns about it.
82
- */
73
+ // null skips the cache: an unstat-able source's staleness is undetectable, and the uncached path warns about it.
83
74
  async #buildCacheKey(fonts: ReactFont[]): Promise<string | null> {
84
75
  const sources: unknown[] = [];
85
76
  for (const font of fonts) {
@@ -96,18 +87,13 @@ export class FontOptimizer {
96
87
  if (!stamp) return null;
97
88
  sources.push({ subsetFile: filePath, ...stamp });
98
89
  }
99
- // `auto` derives the subset from app source text, which no font config hash can capture.
100
90
  if (this.#getFontSubsets(font).includes("auto"))
101
91
  sources.push({ autoSubsetText: this.#cacheDigest(await this.#collectAutoSubsetText()) });
102
92
  }
103
93
  return this.#cacheDigest({ version: FontOptimizer.#cacheVersion, fonts, sources });
104
94
  }
105
95
 
106
- /**
107
- * Cache keys get a cryptographic digest, not the 32-bit FNV `#hashFontConfig` computes for filenames:
108
- * a collision there serves a stale subset as if it were current, while a filename only has to be short
109
- * and stable.
110
- */
96
+ // sha256, not #hashFontConfig's 32-bit FNV: a key collision would serve a stale subset as current.
111
97
  #cacheDigest(value: unknown) {
112
98
  return new Bun.CryptoHasher("sha256").update(this.#stableStringify(value)).digest("hex");
113
99
  }
@@ -152,13 +138,11 @@ export class FontOptimizer {
152
138
  const file = Bun.file(filePath);
153
139
  if (!(await file.exists())) return;
154
140
  const source = await file.text();
155
- // A `.fonts()` stage or a `fonts` declaration cannot exist in text that never mentions it, and parsing the
156
- // route files that never declare one is what a cached optimize() otherwise spends its time on.
157
141
  if (!source.includes("fonts")) return;
158
142
  fonts.push(...this.#extractFontsExport(source, filePath));
159
143
  }),
160
144
  );
161
- return this.#dedupeFonts(fonts);
145
+ return [...new Map(fonts.map((font) => [JSON.stringify(font), font] as const)).values()];
162
146
  }
163
147
 
164
148
  async #optimizeFont(font: ReactFont) {
@@ -186,12 +170,6 @@ export class FontOptimizer {
186
170
  if (faceCss.length > 0) this.#cssParts.push(...faceCss, this.#buildRootVariableRule(font));
187
171
  }
188
172
 
189
- /**
190
- * The font list of a route file, from the `.fonts([…])` stage of its default-exported `rootLayout()` chain
191
- * or from the legacy `export const fonts`. Only an inline literal is readable, because the build enumerates
192
- * routes without evaluating them — and a list it cannot read is warned about rather than skipped in silence:
193
- * the runtime still emits `/_akan/fonts` preloads for fonts nothing subset, and every one of them 404s.
194
- */
195
173
  #extractFontsExport(source: string, filePath: string): ReactFont[] {
196
174
  const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
197
175
  const declaration = this.#findFontsDeclaration(sourceFile);
@@ -203,7 +181,7 @@ export class FontOptimizer {
203
181
  this.#app.logger.warn(
204
182
  `[font] ${path.relative(this.#app.cwdPath, filePath)} declares fonts the build cannot read without evaluating the module — write the list inline, or nothing is subset for it and every /_akan/fonts request 404s`,
205
183
  );
206
- return fonts.map((font) => this.#withFontDefaults(font));
184
+ return fonts.map((font) => ({ ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] }));
207
185
  }
208
186
 
209
187
  #isReadableFont(value: unknown): value is ReactFont {
@@ -231,7 +209,7 @@ export class FontOptimizer {
231
209
  return null;
232
210
  }
233
211
 
234
- /** Walks a `rootLayout()` chain from its outermost call inward, so the stage that wins at runtime is the one read. */
212
+ // Outermost call first, so `??=` keeps the stage that wins at runtime.
235
213
  #findChainStageArgument(expression: ts.Expression, stageName: string): ts.Expression | null {
236
214
  let node = this.#unwrapExpression(expression);
237
215
  let argument: ts.Expression | null = null;
@@ -284,16 +262,6 @@ export class FontOptimizer {
284
262
  return null;
285
263
  }
286
264
 
287
- #dedupeFonts(fonts: ReactFont[]) {
288
- const map = new Map<string, ReactFont>();
289
- for (const font of fonts) map.set(JSON.stringify(font), font);
290
- return [...map.values()];
291
- }
292
-
293
- #withFontDefaults(font: ReactFont): ReactFont {
294
- return { ...font, subsets: font.subsets ?? [...DEFAULT_FONT_SUBSETS] };
295
- }
296
-
297
265
  #getFontSubsets(font: ReactFont): ReactFontSubset[] {
298
266
  return font.subsets ?? DEFAULT_FONT_SUBSETS;
299
267
  }
@@ -310,12 +278,8 @@ export class FontOptimizer {
310
278
  return font.optimize !== false;
311
279
  }
312
280
 
313
- #getFontStyles(font: ReactFont): ReactFontStyle[] {
314
- return font.styles?.length ? font.styles : ["normal"];
315
- }
316
-
317
281
  #getFontFaces(font: ReactFont): ReactFontFace[] {
318
- const enabledStyles = new Set(this.#getFontStyles(font));
282
+ const enabledStyles = new Set<ReactFontStyle>(font.styles?.length ? font.styles : ["normal"]);
319
283
  return font.paths
320
284
  .map((fontPath) => {
321
285
  const style = fontPath.style ?? "normal";
@@ -393,8 +357,7 @@ export class FontOptimizer {
393
357
  return null;
394
358
  }
395
359
 
396
- /** `subset-font`, `fonteditor-core` and `fontaine` are imported here rather than at module scope so a
397
- * cache hit — the common case once the cache exists — loads none of them. */
360
+ // The font libraries are imported lazily so a cache hit loads none of them.
398
361
  async #buildFontBuffer(font: ReactFont, sourceBuffer: Buffer, sourcePath: string) {
399
362
  if (font.subset === false) return this.#convertToWoff2(sourceBuffer, sourcePath);
400
363
  const { default: subsetFont } = await import("subset-font");
@@ -403,16 +366,12 @@ export class FontOptimizer {
403
366
 
404
367
  async #convertToWoff2(buffer: Buffer, sourcePath: string) {
405
368
  const { createFont } = await import("fonteditor-core");
406
- await this.#initWoff2();
369
+ this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => undefined);
370
+ await this.#woff2Ready;
407
371
  const font = createFont(buffer, { type: this.#getFontType(sourcePath, buffer) });
408
372
  return font.write({ type: "woff2", toBuffer: true });
409
373
  }
410
374
 
411
- async #initWoff2() {
412
- this.#woff2Ready ??= import("fonteditor-core").then(({ woff2 }) => woff2.init()).then(() => undefined);
413
- return this.#woff2Ready;
414
- }
415
-
416
375
  #getFontType(sourcePath: string, buffer: Buffer) {
417
376
  const signature = buffer.toString("ascii", 0, 4);
418
377
  if (signature === "wOFF") return "woff";
@@ -443,25 +402,15 @@ export class FontOptimizer {
443
402
  }
444
403
 
445
404
  async #getSubsetPresetText(subset: ReactFontSubset) {
446
- if (subset === "latin") return this.#rangeText(0x20, 0x7e);
447
- if (subset === "latin-ext") return `${this.#rangeText(0x20, 0x7e)}${this.#rangeText(0xa0, 0x024f)}`;
405
+ if (subset === "latin") return FontOptimizer.#rangeText(0x20, 0x7e);
406
+ if (subset === "latin-ext")
407
+ return `${FontOptimizer.#rangeText(0x20, 0x7e)}${FontOptimizer.#rangeText(0xa0, 0x024f)}`;
448
408
  if (subset === "ks-x-1001") return FontOptimizer.#getKsX1001Text();
449
409
  if (subset === "auto") return this.#collectAutoSubsetText();
450
410
  return "";
451
411
  }
452
412
 
453
- /**
454
- * Every source that can put a glyph on screen, concatenated in a **stable** order.
455
- *
456
- * The order is load-bearing even though a glyph set is not: `#buildCacheKey` hashes this string, so
457
- * reading the roots concurrently and pushing as each file resolved made the key depend on i/o
458
- * scheduling — measured 8 distinct keys over 8 runs against unchanged sources, which means the cache
459
- * never hit and every build re-subset the fonts.
460
- *
461
- * `lib` is in the roots because that is where user-facing text actually lives: a dictionary's
462
- * `[en, ko]` pairs are the Korean in the app, and a subset built from `page` and `ui` alone renders
463
- * them as tofu — while hashing the same partial text also stopped a new label from invalidating.
464
- */
413
+ // Keep the order stable: #buildCacheKey hashes this text. `lib` counts because dictionaries hold user-visible text.
465
414
  async #collectAutoSubsetText() {
466
415
  //* Synced lib pages hold app-visible text too, and a glob never crosses the symlink that mounts them.
467
416
  const libPageRoots = (await this.#app.getPageRoots()).filter((root) => root.keyPrefix).map((root) => root.dir);
@@ -481,12 +430,6 @@ export class FontOptimizer {
481
430
  return parts.join("");
482
431
  }
483
432
 
484
- #rangeText(start: number, end: number) {
485
- let text = "";
486
- for (let code = start; code <= end; code++) text += String.fromCodePoint(code);
487
- return text;
488
- }
489
-
490
433
  static #getKsX1001Text() {
491
434
  if (FontOptimizer.#ksX1001Text) return FontOptimizer.#ksX1001Text;
492
435
  try {
@@ -500,12 +443,12 @@ export class FontOptimizer {
500
443
  }
501
444
  FontOptimizer.#ksX1001Text = [...chars].join("");
502
445
  } catch {
503
- FontOptimizer.#ksX1001Text = FontOptimizer.#rangeTextStatic(0xac00, 0xd7a3);
446
+ FontOptimizer.#ksX1001Text = FontOptimizer.#rangeText(0xac00, 0xd7a3);
504
447
  }
505
448
  return FontOptimizer.#ksX1001Text;
506
449
  }
507
450
 
508
- static #rangeTextStatic(start: number, end: number) {
451
+ static #rangeText(start: number, end: number) {
509
452
  let text = "";
510
453
  for (let code = start; code <= end; code++) text += String.fromCodePoint(code);
511
454
  return text;
@@ -1,11 +1,11 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import type { App } from "../commandDecorators";
5
+ import { tempDirs } from "../testHelpers";
6
6
  import { FontPruner } from "./fontPruner";
7
7
 
8
- const tempRoots: string[] = [];
8
+ const tempRoot = tempDirs("akan-devkit-font-prune-");
9
9
 
10
10
  interface Tree {
11
11
  layout?: string;
@@ -23,8 +23,7 @@ const write = async (root: string, files: Record<string, string>) => {
23
23
  };
24
24
 
25
25
  const makeApp = async ({ layout = layoutWith(), publicFiles = {}, artifactFiles = {}, csrFiles = {} }: Tree = {}) => {
26
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-font-prune-"));
27
- tempRoots.push(root);
26
+ const root = await tempRoot();
28
27
  const cwdPath = path.join(root, "apps/demo");
29
28
  const distPath = path.join(root, "dist/apps/demo");
30
29
  await mkdir(path.join(cwdPath, "page"), { recursive: true });
@@ -58,10 +57,6 @@ export default function Layout() {
58
57
 
59
58
  const exists = (distPath: string, rel: string) => Bun.file(path.join(distPath, "public", rel)).exists();
60
59
 
61
- afterEach(async () => {
62
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
63
- });
64
-
65
60
  describe("FontPruner", () => {
66
61
  test("drops a subset font's source and reports the bytes freed", async () => {
67
62
  const { app, distPath } = await makeApp({
@@ -18,12 +18,7 @@ export interface FontPruneResult {
18
18
  freedBytes: number;
19
19
  }
20
20
 
21
- /**
22
- * Drops the font sources in a build's `public/` that no built surface reads. A font with `optimize` on is
23
- * served from `/_akan/fonts` after subsetting, so its source is a build input — the image ships it and the
24
- * runtime never opens it. Only `dist` is touched: an app's and a lib's own `public/` keep every file, because
25
- * one lib's fonts are picked over differently by every app that mounts it and by other repos.
26
- */
21
+ /** Deletes the fonts in the build's `dist/…/public` that nothing reads; source `public/` trees are never touched. */
27
22
  export class FontPruner {
28
23
  #app: App;
29
24
  #keepGlobs: string[];
@@ -92,15 +87,8 @@ export class FontPruner {
92
87
  return candidates;
93
88
  }
94
89
 
95
- /**
96
- * Matched by basename rather than by URL, so a reference survives every spelling a referrer may use — an
97
- * absolute or relative `url()`, and the percent-encoded form a font whose filename carries a space needs.
98
- *
99
- * The build's own bundles are read on weaker terms than `public/` and the compiled CSS: every route file's
100
- * `fonts` declaration is inlined into the pages bundle, the client chunks and the CSR shell, so a declared
101
- * source is in all three whether or not anything loads it. A hit there is therefore ignored for a font the
102
- * optimizer already subset — that string is the declaration, not a fetch.
103
- */
90
+ // Basename match (plain and percent-encoded) covers every url() spelling. The bundles inline each route's `fonts`
91
+ // declaration, so a bundle hit counts only for a font the optimizer did not subset.
104
92
  async #findReferrers(
105
93
  candidates: { rel: string; basename: string }[],
106
94
  optimizedSrcs: Set<string>,
@@ -155,11 +143,8 @@ export class FontPruner {
155
143
  for (const dir of dirs) await rmdir(path.join(this.#publicRoot, dir)).catch(() => undefined);
156
144
  }
157
145
 
158
- /**
159
- * The referrer roll-up is `info`, not `verbose`: a generated file that lists every asset in `public/` — a
160
- * service-worker precache manifest is the usual one — is a real reference and keeps every font it names, so
161
- * without this line a build that pruned nothing looks the same as a build with nothing to prune.
162
- */
146
+ // The roll-up is `info`: a precache manifest listing all of public/ keeps every font, which otherwise looks like
147
+ // a build with nothing to prune.
163
148
  #report(result: FontPruneResult) {
164
149
  if (!result.removed.length && !result.kept.length) return;
165
150
  for (const { file, bytes } of result.removed)
@@ -1,9 +1,9 @@
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 { readFile, rm } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
5
  import type { RoutesManifest } from "akanjs/server";
6
+ import { tempDirs, writeText as write } from "../testHelpers";
7
7
  import { CsrArtifactBuilder } from "./csrArtifactBuilder";
8
8
  import { CssCompiler, declaredCustomProperties, isIgnoredNodeModuleSource } from "./cssCompiler";
9
9
  import { CssImportResolver } from "./cssImportResolver";
@@ -13,35 +13,21 @@ import { HmrChangeClassifier } from "./hmrChangeClassifier";
13
13
  import { PagesBundleBuilder } from "./pagesBundleBuilder";
14
14
  import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
15
15
  import { RoutesManifestArtifactSerializer } from "./routesManifestArtifactSerializer";
16
- import { prepareCssAsset } from "./ssrBaseArtifactBuilder";
16
+ import { prepareCssAsset, SsrBaseArtifactBuilder } from "./ssrBaseArtifactBuilder";
17
17
 
18
- const tempRoots: string[] = [];
19
-
20
- const makeTempRoot = async () => {
21
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-frontend-"));
22
- tempRoots.push(root);
23
- return root;
24
- };
25
-
26
- const write = async (filePath: string, content: string) => {
27
- await mkdir(path.dirname(filePath), { recursive: true });
28
- await writeFile(filePath, content);
29
- };
30
-
31
- afterEach(async () => {
32
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
33
- });
18
+ const makeTempRoot = tempDirs("akan-devkit-frontend-");
34
19
 
35
20
  describe("PagesEntrySourceGenerator", () => {
36
21
  const toSpecifier = (absPath: string) => path.resolve(absPath).split(path.sep).join("/");
22
+ const indexAbs = path.resolve("/repo/apps/demo/page/_index.tsx");
23
+ const adminAbs = path.resolve("/repo/apps/demo/page/admin.tsx");
24
+ const entries = [
25
+ { key: "./_index.tsx", moduleAbsPath: indexAbs },
26
+ { key: "./admin.tsx", moduleAbsPath: adminAbs },
27
+ ];
37
28
 
38
29
  test("generates dynamic import source using forward-slash module paths", () => {
39
- const indexAbs = path.resolve("/repo/apps/demo/page/_index.tsx");
40
- const adminAbs = path.resolve("/repo/apps/demo/page/admin.tsx");
41
- const source = PagesEntrySourceGenerator.generate([
42
- { key: "./_index.tsx", moduleAbsPath: indexAbs },
43
- { key: "./admin.tsx", moduleAbsPath: adminAbs },
44
- ]);
30
+ const source = PagesEntrySourceGenerator.generate(entries);
45
31
 
46
32
  expect(source).toBe(
47
33
  [
@@ -55,12 +41,7 @@ describe("PagesEntrySourceGenerator", () => {
55
41
  });
56
42
 
57
43
  test("generates static import source for single-file CSR bundles", async () => {
58
- const indexAbs = path.resolve("/repo/apps/demo/page/_index.tsx");
59
- const adminAbs = path.resolve("/repo/apps/demo/page/admin.tsx");
60
- const source = await PagesEntrySourceGenerator.generateStatic([
61
- { key: "./_index.tsx", moduleAbsPath: indexAbs },
62
- { key: "./admin.tsx", moduleAbsPath: adminAbs },
63
- ]);
44
+ const source = await PagesEntrySourceGenerator.generateStatic(entries);
64
45
 
65
46
  expect(source).toBe(
66
47
  [
@@ -271,6 +252,16 @@ describe("CsrArtifactBuilder", () => {
271
252
  });
272
253
 
273
254
  describe("SsrBaseArtifactBuilder", () => {
255
+ test("a dev artifact serves every web surface, and a build the one akan.config.ts keeps", () => {
256
+ for (const web of [
257
+ { ssr: true, csr: false },
258
+ { ssr: false, csr: false },
259
+ ]) {
260
+ expect(SsrBaseArtifactBuilder.servedWeb("start", web)).toEqual({ ssr: true, csr: true });
261
+ expect(SsrBaseArtifactBuilder.servedWeb("build", web)).toEqual(web);
262
+ }
263
+ });
264
+
274
265
  test("minifies CSS assets only for production builds", async () => {
275
266
  const css = [
276
267
  ".card {",
@@ -333,13 +324,14 @@ describe("RoutesManifestArtifactSerializer", () => {
333
324
  describe("HmrChangeClassifier", () => {
334
325
  test("classifies code, css, config, and ignored files", () => {
335
326
  const classifier = new HmrChangeClassifier();
336
- expect(classifier.classify("/repo/apps/demo/page/_index.tsx")).toBe("code");
337
- expect(classifier.classify("/repo/apps/demo/page/styles.css")).toBe("css");
338
- expect(classifier.classify("/repo/apps/demo/akan.config.ts")).toBe("config");
339
- expect(classifier.classify("/repo/apps/demo/.DS_Store")).toBe("ignore");
340
- expect(classifier.classify(`/repo/apps/demo/node_modules/pkg/index.ts`)).toBe("ignore");
341
- expect(classifier.classify(`/repo/apps/demo/.akan/generated/page.tsx`)).toBe("ignore");
342
- expect(classifier.classify("/repo/apps/demo/public/logo.png")).toBe("ignore");
327
+ const app = path.resolve("/repo/apps/demo");
328
+ expect(classifier.classify(path.join(app, "page/_index.tsx"))).toBe("code");
329
+ expect(classifier.classify(path.join(app, "page/styles.css"))).toBe("css");
330
+ expect(classifier.classify(path.join(app, "akan.config.ts"))).toBe("config");
331
+ expect(classifier.classify(path.join(app, ".DS_Store"))).toBe("ignore");
332
+ expect(classifier.classify(path.join(app, "node_modules/pkg/index.ts"))).toBe("ignore");
333
+ expect(classifier.classify(path.join(app, ".akan/generated/page.tsx"))).toBe("ignore");
334
+ expect(classifier.classify(path.join(app, "public/logo.png"))).toBe("ignore");
343
335
  });
344
336
  });
345
337
 
@@ -394,16 +386,16 @@ describe("DevGeneratedIndexSync", () => {
394
386
 
395
387
  describe("DevChangePlanner", () => {
396
388
  test("classifies server, client, shared, and generated barrel changes", () => {
397
- const root = "/repo";
389
+ const root = path.resolve("/repo");
398
390
  const planner = new DevChangePlanner({ workspaceRoot: root });
399
- const generatedIndex = `${root}/libs/shared/common/index.ts`;
391
+ const generatedIndex = path.join(root, "libs/shared/common/index.ts");
400
392
  const plan = planner.plan({
401
393
  generation: 7,
402
394
  files: [
403
- `${root}/libs/shared/lib/admin/admin.service.ts`,
404
- `${root}/libs/shared/lib/admin/Admin.Template.tsx`,
405
- `${root}/libs/shared/lib/admin/admin.constant.ts`,
406
- `${root}/libs/shared/common/foo.ts`,
395
+ path.join(root, "libs/shared/lib/admin/admin.service.ts"),
396
+ path.join(root, "libs/shared/lib/admin/Admin.Template.tsx"),
397
+ path.join(root, "libs/shared/lib/admin/admin.constant.ts"),
398
+ path.join(root, "libs/shared/common/foo.ts"),
407
399
  ],
408
400
  kinds: ["code"],
409
401
  generatedFiles: [generatedIndex],
@@ -429,21 +421,22 @@ describe("DevChangePlanner", () => {
429
421
  });
430
422
 
431
423
  test("recycles builder for macro-backed dictionary and signal metadata changes", () => {
432
- const root = "/repo";
424
+ const root = path.resolve("/repo");
433
425
  const planner = new DevChangePlanner({ workspaceRoot: root });
426
+ const dictionaryFile = path.join(root, "apps/demo/lib/_demo/demo.dictionary.ts");
434
427
  const dictionaryPlan = planner.plan({
435
428
  generation: 3,
436
- files: [`${root}/apps/demo/lib/_demo/demo.dictionary.ts`],
429
+ files: [dictionaryFile],
437
430
  kinds: ["code"],
438
431
  });
439
432
  const signalPlan = planner.plan({
440
433
  generation: 4,
441
- files: [`${root}/libs/shared/lib/admin/admin.signal.ts`],
434
+ files: [path.join(root, "libs/shared/lib/admin/admin.signal.ts")],
442
435
  kinds: ["code"],
443
436
  });
444
437
 
445
438
  expect(dictionaryPlan.actions).toEqual(["rebuild-client", "restart-backend", "restart-builder"]);
446
- expect(dictionaryPlan.reasonByFile[`${root}/apps/demo/lib/_demo/demo.dictionary.ts`]).toContain("runtime-metadata");
439
+ expect(dictionaryPlan.reasonByFile[dictionaryFile]).toContain("runtime-metadata");
447
440
  expect(signalPlan.actions).toContain("restart-builder");
448
441
  });
449
442
  });
@@ -499,6 +492,14 @@ describe("CssImportResolver", () => {
499
492
  });
500
493
 
501
494
  describe("CssCompiler", () => {
495
+ const cssCompilerFor = (root: string, cwdPath: string, app: object = {}) =>
496
+ new CssCompiler({
497
+ workspace: { workspaceRoot: root },
498
+ cwdPath,
499
+ getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
500
+ ...app,
501
+ } as never);
502
+
502
503
  test("scans installed akanjs sources while ignoring other node_modules", async () => {
503
504
  expect(isIgnoredNodeModuleSource("/repo/node_modules/react/index.js")).toBe(true);
504
505
  expect(isIgnoredNodeModuleSource("/repo/node_modules/akanjs/ui/Button.tsx")).toBe(false);
@@ -512,13 +513,8 @@ describe("CssCompiler", () => {
512
513
  await write(cssPath, `@import ${JSON.stringify(tailwindCssPath)};\n@source "./**/*";\n`);
513
514
  await write(uiSource, 'export const Button = () => <button className="text-fuchsia-500" />;\n');
514
515
 
515
- const compiler = new CssCompiler({
516
- workspace: { workspaceRoot: root },
517
- // The candidate cache lands under `<cwdPath>/.akan/cache`, so point it at this test's temp root
518
- // rather than wherever the suite happens to run from.
519
- cwdPath: root,
520
- getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
521
- } as never);
516
+ // The candidate cache lands under `<cwdPath>/.akan/cache`, so keep it inside this test's temp root.
517
+ const compiler = cssCompilerFor(root, root);
522
518
  const css = await compiler.compileCss([cssPath], []);
523
519
 
524
520
  expect(css).toContain(".text-fuchsia-500");
@@ -540,11 +536,7 @@ describe("CssCompiler", () => {
540
536
  const cssPath = path.join(root, "apps/demo/page/styles.css");
541
537
  await write(cssPath, '@import "../../../libs/shared/ui/tokens.css";\n');
542
538
 
543
- const compiler = new CssCompiler({
544
- workspace: { workspaceRoot: root },
545
- cwdPath: path.join(root, "apps/demo"),
546
- getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
547
- } as never);
539
+ const compiler = cssCompilerFor(root, path.join(root, "apps/demo"));
548
540
 
549
541
  await expect(compiler.compileCss([cssPath], [])).rejects.toThrow(
550
542
  /failed to resolve stylesheet import "\.\.\/\.\.\/\.\.\/libs\/shared\/ui\/tokens\.css"/,
@@ -558,13 +550,10 @@ describe("CssCompiler", () => {
558
550
  await write(path.join(appDir, "page/styles.css"), '@import "../../../libs/shared/ui/brand.css";\n');
559
551
  await write(path.join(root, "libs/shared/ui/brand.css"), ":root { --kakao: #fee500; --naver: #1ec800; }\n");
560
552
 
561
- const compiler = new CssCompiler({
562
- workspace: { workspaceRoot: root },
563
- cwdPath: appDir,
553
+ const compiler = cssCompilerFor(root, appDir, {
564
554
  getPageKeys: async () => ["./_index.tsx"],
565
555
  getConfig: async () => ({ barrelImports: [], basePaths: [] }),
566
- getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
567
- } as never);
556
+ });
568
557
  const cssByBasePath = await compiler.getCssByBasePath();
569
558
 
570
559
  expect(cssByBasePath[""]).toContain("--kakao");
@@ -585,13 +574,11 @@ describe("CssCompiler", () => {
585
574
  await write(path.join(root, "libs/shared/ui/tokens.css"), ":root { --kakao: #fee500; }\n");
586
575
  await write(path.join(root, "libs/unused/ui/tokens.css"), ":root { --unused: #000000; }\n");
587
576
 
588
- const compiler = new CssCompiler({
589
- workspace: { workspaceRoot: root },
590
- cwdPath: appDir,
577
+ const compiler = cssCompilerFor(root, appDir, {
591
578
  getPageKeys: async () => ["./_index.tsx"],
592
579
  getConfig: async () => ({ barrelImports: [] }),
593
580
  getTsConfig: async () => ({ compilerOptions: { paths: { "@libs/*": ["./libs/*"] } } }),
594
- } as never);
581
+ });
595
582
  const { cssPaths } = await compiler.discoverCssAndSources();
596
583
 
597
584
  expect(cssPaths).toEqual([path.join(root, "libs/shared/ui/tokens.css"), path.join(appDir, "page/styles.css")]);
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import type { ChangeKind } 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 CSS_EXTS = new Set([".css"]);
6
6
  const CONFIG_BASENAMES = new Set(["akan.config.ts", "bunfig.toml", "tsconfig.json", "package.json"]);
7
7
 
@@ -19,7 +19,7 @@ export class HmrChangeClassifier {
19
19
  #isUninteresting(abs: string): boolean {
20
20
  const base = path.basename(abs);
21
21
  if (!base) return true;
22
- if (base.startsWith(".")) return true; // .git, .DS_Store, vim swaps, etc.
22
+ if (base.startsWith(".")) return true;
23
23
  if (base.endsWith("~") || base.endsWith(".swp") || base.endsWith(".swx") || base.endsWith(".tmp")) return true;
24
24
  if (abs.includes(`${path.sep}node_modules${path.sep}`)) return true;
25
25
  if (abs.includes(`${path.sep}.akan${path.sep}`)) return true;