@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.20

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 (265) hide show
  1. package/README.md +1 -0
  2. package/abstractDoc.test.ts +6 -15
  3. package/abstractDoc.ts +6 -8
  4. package/agentsIndex.test.ts +4 -7
  5. package/agentsIndex.ts +6 -22
  6. package/akanApp/BackendImportGraph.test.ts +8 -22
  7. package/akanApp/BackendImportGraph.ts +1 -20
  8. package/akanApp/akanApp.host.test.ts +1 -3
  9. package/akanApp/akanApp.host.ts +82 -254
  10. package/akanApp/devHostPolicy.test.ts +10 -34
  11. package/akanApp/devHostPolicy.ts +12 -98
  12. package/akanConfig/akanConfig.test.ts +85 -69
  13. package/akanConfig/akanConfig.ts +69 -153
  14. package/akanConfig/types.ts +1 -0
  15. package/akanContext.ts +106 -159
  16. package/akanMcpContract.ts +28 -73
  17. package/appSelectionMemory.ts +1 -4
  18. package/applicationBuildReporter.ts +37 -5
  19. package/applicationBuildRunner.test.ts +5 -3
  20. package/applicationBuildRunner.ts +54 -61
  21. package/applicationReleasePackager.ts +24 -44
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +9 -30
  24. package/artifact/implicitRootLayout.ts +34 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +6 -1
  28. package/biomeBase.ts +2 -5
  29. package/capacitorApp.test.ts +32 -16
  30. package/capacitorApp.ts +110 -238
  31. package/cloud/cloudApi.test.ts +202 -0
  32. package/cloud/cloudApi.ts +77 -39
  33. package/cloud/configLock.test.ts +64 -0
  34. package/cloud/configLock.ts +96 -0
  35. package/cloud/constants.ts +26 -5
  36. package/cloud/globalConfig.ts +70 -32
  37. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  38. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  39. package/codeAgent/agent/CodeAgent.ts +551 -0
  40. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  41. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  42. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  43. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  44. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  45. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  46. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  47. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  48. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  49. package/codeAgent/agent/CodeMailbox.ts +148 -0
  50. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  51. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  52. package/codeAgent/agent/SubagentPool.ts +214 -0
  53. package/codeAgent/agent/akanCodeModel.ts +47 -0
  54. package/codeAgent/agent/akanCodePaths.ts +31 -0
  55. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  56. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  57. package/codeAgent/agent/codeAgent.test.ts +178 -0
  58. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  59. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  60. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  61. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  62. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  63. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  64. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  65. package/codeAgent/agent/consoleToStderr.ts +8 -0
  66. package/codeAgent/agent/jsonLines.ts +29 -0
  67. package/codeAgent/agent/stringArg.ts +5 -0
  68. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  69. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  70. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  71. package/codeAgent/feedback/PreviewView.ts +95 -0
  72. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  73. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  74. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  75. package/codeAgent/index.ts +34 -0
  76. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  77. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  78. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  79. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  80. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  81. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  82. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  83. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  84. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  85. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  86. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  87. package/codeAgent/tools/AkanCli.ts +50 -0
  88. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  89. package/codeAgent/tools/AkanEditScope.ts +48 -0
  90. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  91. package/codeAgent/tools/AkanToolPack.ts +92 -0
  92. package/codeAgent/tools/AkanVerifier.ts +48 -0
  93. package/codeAgent/tools/AskToolPack.ts +99 -0
  94. package/codeAgent/tools/MailToolPack.ts +76 -0
  95. package/codeAgent/tools/McpClient.ts +187 -0
  96. package/codeAgent/tools/McpOAuth.ts +233 -0
  97. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  98. package/codeAgent/tools/McpServerConfig.ts +129 -0
  99. package/codeAgent/tools/McpSignIn.ts +158 -0
  100. package/codeAgent/tools/McpTokenStore.ts +81 -0
  101. package/codeAgent/tools/McpToolPack.ts +199 -0
  102. package/codeAgent/tools/SessionToolPack.ts +91 -0
  103. package/codeAgent/tools/WebToolPack.ts +147 -0
  104. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  105. package/codeAgent/tools/askToolPack.test.ts +116 -0
  106. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  107. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  108. package/codegenLock.test.ts +4 -13
  109. package/codegenLock.ts +3 -17
  110. package/commandDecorators/argMeta.ts +1 -5
  111. package/commandDecorators/camelToKebabCase.ts +1 -0
  112. package/commandDecorators/command.ts +46 -102
  113. package/commandDecorators/commandBuilder.ts +30 -61
  114. package/commandDecorators/commandDecorators.test.ts +2 -10
  115. package/commandDecorators/dependencyBuilder.ts +10 -1
  116. package/commandDecorators/helpFormatter.ts +68 -145
  117. package/commandDecorators/index.ts +0 -2
  118. package/commandDecorators/targetMeta.ts +9 -7
  119. package/createTunnel.ts +7 -14
  120. package/dependencyScanner.test.ts +4 -16
  121. package/dependencyScanner.ts +31 -80
  122. package/devkitUtils.test.ts +48 -20
  123. package/executors.linkFallback.test.ts +103 -0
  124. package/executors.test.ts +132 -106
  125. package/executors.ts +194 -484
  126. package/fileEditor.ts +9 -37
  127. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  128. package/fileSys.ts +6 -8
  129. package/formSetterScanner.ts +3 -16
  130. package/frontendBuild/allRoutesBuilder.ts +7 -21
  131. package/frontendBuild/autoImportSync.test.ts +27 -38
  132. package/frontendBuild/autoImportSync.ts +31 -65
  133. package/frontendBuild/buildRouteClient.test.ts +62 -37
  134. package/frontendBuild/bundleDefine.ts +12 -0
  135. package/frontendBuild/clientBuildTypes.ts +5 -36
  136. package/frontendBuild/clientEntriesBundler.ts +9 -40
  137. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  138. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  139. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  140. package/frontendBuild/cssCandidateCache.ts +6 -41
  141. package/frontendBuild/cssCompiler.ts +29 -78
  142. package/frontendBuild/cssImportResolver.ts +6 -19
  143. package/frontendBuild/devChangePlanner.ts +7 -10
  144. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  145. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  146. package/frontendBuild/fontOptimizer.test.ts +6 -14
  147. package/frontendBuild/fontOptimizer.ts +15 -72
  148. package/frontendBuild/fontPruner.test.ts +5 -10
  149. package/frontendBuild/fontPruner.ts +5 -20
  150. package/frontendBuild/frontendBuild.test.ts +46 -69
  151. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  152. package/frontendBuild/hmrWatcher.test.ts +8 -32
  153. package/frontendBuild/hmrWatcher.ts +14 -71
  154. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  155. package/frontendBuild/precompressArtifacts.ts +5 -18
  156. package/frontendBuild/routeClientBuilder.ts +23 -46
  157. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  158. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  159. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  160. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  161. package/frontendBuild/styleContract.ts +1 -8
  162. package/frontendBuild/themeValidator.ts +4 -26
  163. package/frontendBuild/watchRootResolver.test.ts +23 -42
  164. package/frontendBuild/watchRootResolver.ts +5 -27
  165. package/getRelatedCnsts.ts +12 -92
  166. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  167. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  168. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  169. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  170. package/incrementalBuilder/builderChannel.test.ts +24 -49
  171. package/incrementalBuilder/builderChannel.ts +6 -37
  172. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  173. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  174. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  175. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  176. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  177. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  178. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  179. package/index.ts +5 -17
  180. package/integration/devResourceProbe.ts +50 -56
  181. package/integration/devStability.integration.test.ts +47 -194
  182. package/integration/devStabilityHarness.test.ts +13 -28
  183. package/integration/devStabilityHarness.ts +65 -330
  184. package/integration/ssrMemoryProbe.ts +41 -167
  185. package/libSource.test.ts +4 -15
  186. package/libSource.ts +19 -31
  187. package/lint/gritRules.test.ts +8 -24
  188. package/lint/no-deep-internal-import.grit +2 -2
  189. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  190. package/linter.test.ts +1 -2
  191. package/linter.ts +26 -116
  192. package/mobile/mobileTarget.ts +14 -13
  193. package/package.json +10 -5
  194. package/packageExports.test.ts +3 -18
  195. package/packageExportsMap.ts +3 -19
  196. package/piDependencyPins.test.ts +46 -0
  197. package/platformTest/BunTestReport.test.ts +60 -0
  198. package/platformTest/BunTestReport.ts +65 -0
  199. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  200. package/platformTest/IdleWatchedProcess.ts +73 -0
  201. package/platformTest/LinuxDockerTarget.ts +105 -0
  202. package/platformTest/PlatformTestRun.test.ts +64 -0
  203. package/platformTest/PlatformTestRun.ts +141 -0
  204. package/platformTest/PlatformTestTarget.ts +142 -0
  205. package/platformTest/TestSnapshot.test.ts +72 -0
  206. package/platformTest/TestSnapshot.ts +103 -0
  207. package/platformTest/WindowsSshTarget.ts +151 -0
  208. package/prompter.ts +3 -13
  209. package/qualityScanner.test.ts +9 -26
  210. package/qualityScanner.ts +197 -285
  211. package/recipeScanner.test.ts +7 -12
  212. package/recipeScanner.ts +9 -38
  213. package/repoIdentity.ts +2 -9
  214. package/routeSourceValidator.test.ts +0 -1
  215. package/routeSourceValidator.ts +7 -42
  216. package/scanInfo.ts +87 -216
  217. package/selectModel.ts +1 -2
  218. package/semver.ts +3 -5
  219. package/slicePlanner.test.ts +6 -24
  220. package/slicePlanner.ts +11 -32
  221. package/spinner.test.ts +3 -8
  222. package/spinner.ts +4 -16
  223. package/sshTunnel.ts +1 -6
  224. package/ssrScanner.test.ts +1 -6
  225. package/ssrScanner.ts +103 -113
  226. package/stripAnsi.ts +3 -0
  227. package/subspace.test.ts +30 -25
  228. package/subspace.ts +81 -115
  229. package/subspaceConfig.ts +11 -9
  230. package/testHelpers.ts +194 -0
  231. package/transforms/asyncDefaultExportDetector.ts +3 -11
  232. package/transforms/barrelAnalyzer.ts +43 -135
  233. package/transforms/barrelImportsPlugin.ts +38 -175
  234. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  235. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  236. package/transforms/moduleSyntax.ts +32 -0
  237. package/transforms/rscUseClientTransform.ts +35 -20
  238. package/transforms/transforms.test.ts +153 -44
  239. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  240. package/transforms/useClientBundlePlugin.ts +2 -22
  241. package/typeChecker.ts +7 -63
  242. package/types.ts +1 -0
  243. package/uploadRelease.ts +10 -25
  244. package/workflow/artifacts.ts +18 -0
  245. package/workflow/executor.test.ts +1 -12
  246. package/workflow/executor.ts +101 -110
  247. package/workflow/index.ts +0 -1
  248. package/workflow/moduleIndex.test.ts +4 -15
  249. package/workflow/moduleIndex.ts +9 -49
  250. package/workflow/plan.ts +18 -25
  251. package/workflow/render.ts +80 -96
  252. package/workflow/rolloutGate.test.ts +1 -1
  253. package/workflow/source.ts +52 -69
  254. package/workflow/sourceAst.ts +50 -0
  255. package/workspaceLayout.ts +4 -19
  256. package/abstractCompactor.ts +0 -112
  257. package/aiEditor.test.ts +0 -60
  258. package/aiEditor.ts +0 -387
  259. package/commandDecorators/commandMeta.ts +0 -7
  260. package/commandDecorators/types.ts +0 -10
  261. package/getDirname.ts +0 -4
  262. package/guideline.ts +0 -19
  263. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  264. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  265. package/workflow/primitive.ts +0 -59
@@ -1,13 +1,13 @@
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";
7
8
  import { CssCandidateCache } from "./cssCandidateCache";
8
9
  import { CssImportResolver } from "./cssImportResolver";
9
10
 
10
- const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"] as const;
11
11
  const NON_SOURCE_EXT_RE = /\.(json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
12
12
  const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
13
13
  const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
@@ -17,7 +17,6 @@ interface CssDiscovery {
17
17
  sourcePaths: string[];
18
18
  }
19
19
 
20
- /** One `@import` target and the custom properties it declares, which is what proves it arrived downstream. */
21
20
  export interface ImportedStylesheet {
22
21
  cssPath: string;
23
22
  declaredNames: string[];
@@ -32,32 +31,18 @@ export class CssCompiler {
32
31
  this.#app = app;
33
32
  }
34
33
 
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
- */
34
+ // Beside the sources, not in `dist`: the build and the dev server scan the same files, so they share one cache.
39
35
  get #candidateCachePath() {
40
36
  return path.join(this.#app.cwdPath, ".akan/cache/cssCandidates.json");
41
37
  }
42
38
 
43
39
  #cssText: string | null = null;
44
40
  #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
- */
41
+ // Memoised for one compiler, i.e. one rebuild: nothing outlives it, so there is nothing to invalidate.
52
42
  #fileExistsCache = new Map<string, Promise<boolean>>();
53
43
  #resolvedFileCache = new Map<string, Promise<string | null>>();
54
44
  #resolvedSpecifierCache = new Map<string, Promise<string | null>>();
55
- /** Every stylesheet this compile reached, entry points and `@import` targets alike. */
56
45
  #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
46
  importedStylesheetsByBasePath: Record<string, ImportedStylesheet[]> = {};
62
47
 
63
48
  #fileExists(absPath: string): Promise<boolean> {
@@ -107,41 +92,31 @@ export class CssCompiler {
107
92
  const pageKeys = await this.#app.getPageKeys({ refresh });
108
93
  const basePaths = [...akanConfig.basePaths];
109
94
  const rootPageKeys = pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === null);
95
+ const compileBasePath = async (basePath: string, basePathPageKeys: string[], label = basePath) => {
96
+ if (basePathPageKeys.length === 0) return [basePath, ""] as const;
97
+ const started = Date.now();
98
+ const { cssPaths, sourcePaths } = await this.discoverCssAndSources({ refresh, pageKeys: basePathPageKeys });
99
+ const { css, imported } = await this.#compileWithImports(cssPaths, sourcePaths);
100
+ this.importedStylesheetsByBasePath[basePath] = imported;
101
+ this.#logger.verbose(
102
+ `css base=${label} paths=${cssPaths.length} sources=${sourcePaths.length} in ${Date.now() - started}ms`,
103
+ );
104
+ return [basePath, css] as const;
105
+ };
110
106
  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
- }),
107
+ compileBasePath("", rootPageKeys, "root"),
108
+ ...basePaths.map((basePath) =>
109
+ compileBasePath(
110
+ basePath,
111
+ pageKeys.filter((pageKey) => getPageKeyBasePath(pageKey, basePaths) === basePath),
112
+ ),
113
+ ),
134
114
  ]);
135
115
  this.#cssTextByBasePath = Object.fromEntries(cssEntries);
136
116
  await this.#warnUnreachableStylesheets();
137
117
  return this.#cssTextByBasePath;
138
118
  }
139
119
 
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
120
  async #warnUnreachableStylesheets() {
146
121
  const pageDir = path.join(this.#app.cwdPath, "page");
147
122
  const glob = new Bun.Glob("**/*.css");
@@ -225,11 +200,7 @@ export class CssCompiler {
225
200
  return { cssPaths, sourcePaths: [...sourceFiles] };
226
201
  }
227
202
 
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
- */
203
+ // Lib tokens go ahead of the app's stylesheets, so the app has the last word on a variable both declare.
233
204
  async #libTokenStylesheets(sourceFiles: Set<string>): Promise<string[]> {
234
205
  const libsRoot = path.join(this.#app.workspace.workspaceRoot, "libs");
235
206
  const libNames = new Set<string>();
@@ -252,10 +223,7 @@ export class CssCompiler {
252
223
  return css;
253
224
  }
254
225
 
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
- */
226
+ // A per-compile collector: getCssByBasePath compiles base paths concurrently, so instance state would mix them.
259
227
  async #compileWithImports(
260
228
  cssPaths: string[],
261
229
  sourcePaths: string[],
@@ -279,7 +247,6 @@ export class CssCompiler {
279
247
 
280
248
  const sourceDirs = new Set<string>();
281
249
  for (const entry of compilers) {
282
- if (!entry) continue;
283
250
  for (const s of entry.compiler.sources as { base: string }[]) sourceDirs.add(s.base);
284
251
  }
285
252
  const scanStarted = Date.now();
@@ -290,7 +257,6 @@ export class CssCompiler {
290
257
  const parts: string[] = [];
291
258
  const imported: ImportedStylesheet[] = [];
292
259
  for (const entry of compilers) {
293
- if (!entry) continue;
294
260
  const part = entry.compiler.build(candidates);
295
261
  parts.push(part);
296
262
  for (const [cssPath, content] of entry.imported) {
@@ -317,28 +283,19 @@ export class CssCompiler {
317
283
  return { path: p, base: path.dirname(p), content };
318
284
  }
319
285
 
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
- */
286
+ // An unresolvable @import throws: under vocabulary closure a missing token file just renders unstyled.
325
287
  async #resolveCssImport(id: string, fromBase: string): Promise<string> {
326
288
  if (id.startsWith(".") || id.startsWith("/")) {
327
289
  const filePath = path.resolve(fromBase, id);
328
290
  if (await this.#fileExists(filePath)) return filePath;
329
291
  throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase} (no file at ${filePath})`);
330
292
  }
331
- const resolver = await this.#getCssImportResolver();
332
- const resolved = await resolver.resolve(id, fromBase);
293
+ this.#cssImportResolver ??= await CssImportResolver.create(this.#app);
294
+ const resolved = await this.#cssImportResolver.resolve(id, fromBase);
333
295
  if (resolved) return resolved;
334
296
  throw new Error(`[css] failed to resolve stylesheet import "${id}" from ${fromBase}`);
335
297
  }
336
298
 
337
- async #getCssImportResolver() {
338
- if (this.#cssImportResolver) return this.#cssImportResolver;
339
- this.#cssImportResolver = await CssImportResolver.create(this.#app);
340
- return this.#cssImportResolver;
341
- }
342
299
  async #loadModule(id: string, fromBase: string) {
343
300
  const p = require.resolve(id, { paths: [fromBase] });
344
301
  const mod = await import(p);
@@ -349,10 +306,7 @@ export class CssCompiler {
349
306
  fromBase: string,
350
307
  resolvePackage: Awaited<ReturnType<typeof createTsconfigPackageResolver>>,
351
308
  ): 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.
309
+ // Keyed by importer directory even for bare specifiers: the Bun/require fallbacks are directory-dependent.
356
310
  const cacheKey = `${fromBase}\0${id}`;
357
311
  let cached = this.#resolvedSpecifierCache.get(cacheKey);
358
312
  if (cached) return cached;
@@ -423,17 +377,14 @@ function resolveSourceWithRequire(id: string, fromBase: string): string | null {
423
377
  }
424
378
 
425
379
  function isSourceFile(filePath: string) {
426
- return SOURCE_EXTS.includes(path.extname(filePath) as (typeof SOURCE_EXTS)[number]);
380
+ return SOURCE_EXTS.has(path.extname(filePath));
427
381
  }
428
382
 
429
383
  export function isIgnoredNodeModuleSource(filePath: string): boolean {
430
384
  return NODE_MODULES_RE.test(filePath) && !AKANJS_NODE_MODULE_RE.test(filePath);
431
385
  }
432
386
 
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
- */
387
+ /** Skips `@theme` blocks: Tailwind emits those variables only when a utility uses one. */
437
388
  export function declaredCustomProperties(css: string): string[] {
438
389
  const withoutThemeBlocks = css.replace(/@theme[^{]*\{[^}]*\}/g, "");
439
390
  return [...new Set([...withoutThemeBlocks.matchAll(/(?:^|[\s;{])(--[\w-]+)\s*:/g)].map(([, name]) => name))].filter(
@@ -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));
@@ -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 () => {