@akanjs/devkit 3.0.0-beta.19 → 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 (233) hide show
  1. package/abstractDoc.test.ts +6 -15
  2. package/abstractDoc.ts +1 -4
  3. package/agentsIndex.test.ts +4 -7
  4. package/agentsIndex.ts +6 -22
  5. package/akanApp/BackendImportGraph.test.ts +8 -22
  6. package/akanApp/BackendImportGraph.ts +1 -20
  7. package/akanApp/akanApp.host.test.ts +1 -3
  8. package/akanApp/akanApp.host.ts +64 -250
  9. package/akanApp/devHostPolicy.test.ts +10 -34
  10. package/akanApp/devHostPolicy.ts +12 -98
  11. package/akanConfig/akanConfig.test.ts +30 -39
  12. package/akanConfig/akanConfig.ts +40 -136
  13. package/akanContext.ts +106 -159
  14. package/akanMcpContract.ts +28 -73
  15. package/appSelectionMemory.ts +1 -4
  16. package/applicationBuildReporter.ts +37 -5
  17. package/applicationBuildRunner.ts +25 -59
  18. package/applicationReleasePackager.ts +24 -44
  19. package/applicationTestPreload.ts +3 -12
  20. package/artifact/implicitRootLayout.test.ts +3 -19
  21. package/artifact/implicitRootLayout.ts +34 -91
  22. package/artifact/routeSeedIndex.ts +11 -44
  23. package/biomeBase.ts +2 -5
  24. package/capacitorApp.test.ts +4 -14
  25. package/capacitorApp.ts +86 -235
  26. package/cloud/cloudApi.test.ts +202 -0
  27. package/cloud/cloudApi.ts +33 -57
  28. package/cloud/configLock.test.ts +64 -0
  29. package/cloud/configLock.ts +96 -0
  30. package/cloud/constants.ts +3 -2
  31. package/cloud/globalConfig.ts +64 -31
  32. package/codeAgent/agent/AkanCodeServices.ts +15 -57
  33. package/codeAgent/agent/AkanContextFiles.ts +5 -28
  34. package/codeAgent/agent/CodeAgent.ts +33 -108
  35. package/codeAgent/agent/CodeAgentAsks.ts +7 -30
  36. package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
  37. package/codeAgent/agent/CodeAgentGate.ts +8 -34
  38. package/codeAgent/agent/CodeAgentProxy.ts +4 -13
  39. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
  40. package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
  41. package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
  42. package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
  43. package/codeAgent/agent/CodeAgentUi.ts +2 -7
  44. package/codeAgent/agent/CodeMailbox.ts +7 -34
  45. package/codeAgent/agent/CodeSessionFork.ts +4 -19
  46. package/codeAgent/agent/CodeSessionIndex.ts +5 -31
  47. package/codeAgent/agent/SubagentPool.ts +48 -58
  48. package/codeAgent/agent/akanCodeModel.ts +5 -39
  49. package/codeAgent/agent/akanCodePaths.ts +4 -24
  50. package/codeAgent/agent/akanContextFiles.test.ts +0 -2
  51. package/codeAgent/agent/akanSystemPrompt.ts +1 -9
  52. package/codeAgent/agent/codeAgent.test.ts +178 -0
  53. package/codeAgent/agent/codeAgentContract.test.ts +5 -28
  54. package/codeAgent/agent/codeMailbox.test.ts +0 -4
  55. package/codeAgent/agent/codeSessionFork.test.ts +0 -2
  56. package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
  57. package/codeAgent/agent/consoleToStderr.ts +2 -12
  58. package/codeAgent/agent/jsonLines.ts +29 -0
  59. package/codeAgent/agent/stringArg.ts +5 -0
  60. package/codeAgent/feedback/DevLogFeedback.ts +3 -18
  61. package/codeAgent/feedback/PreviewChrome.ts +5 -29
  62. package/codeAgent/feedback/PreviewFeedback.ts +2 -9
  63. package/codeAgent/feedback/PreviewView.ts +3 -15
  64. package/codeAgent/feedback/TurnFeedback.ts +2 -17
  65. package/codeAgent/feedback/VerifyFeedback.ts +1 -7
  66. package/codeAgent/feedback/previewChrome.test.ts +1 -1
  67. package/codeAgent/index.ts +2 -10
  68. package/codeAgent/skills/akan-code/SKILL.md +12 -4
  69. package/codeAgent/tools/AkanCli.ts +4 -16
  70. package/codeAgent/tools/AkanCodePlugins.ts +21 -43
  71. package/codeAgent/tools/AkanEditScope.ts +2 -12
  72. package/codeAgent/tools/AkanEnvKeys.ts +3 -11
  73. package/codeAgent/tools/AkanToolPack.ts +6 -28
  74. package/codeAgent/tools/AkanVerifier.ts +1 -8
  75. package/codeAgent/tools/AskToolPack.ts +3 -15
  76. package/codeAgent/tools/MailToolPack.ts +0 -9
  77. package/codeAgent/tools/McpClient.ts +3 -24
  78. package/codeAgent/tools/McpOAuth.ts +6 -41
  79. package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
  80. package/codeAgent/tools/McpServerConfig.ts +7 -26
  81. package/codeAgent/tools/McpSignIn.ts +46 -49
  82. package/codeAgent/tools/McpTokenStore.ts +6 -17
  83. package/codeAgent/tools/McpToolPack.ts +107 -35
  84. package/codeAgent/tools/SessionToolPack.ts +1 -11
  85. package/codeAgent/tools/WebToolPack.ts +1 -7
  86. package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
  87. package/codeAgent/tools/askToolPack.test.ts +0 -3
  88. package/codeAgent/tools/mcpOAuth.test.ts +315 -27
  89. package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
  90. package/codegenLock.test.ts +4 -13
  91. package/codegenLock.ts +3 -17
  92. package/commandDecorators/argMeta.ts +1 -5
  93. package/commandDecorators/camelToKebabCase.ts +1 -0
  94. package/commandDecorators/command.ts +44 -102
  95. package/commandDecorators/commandBuilder.ts +30 -61
  96. package/commandDecorators/commandDecorators.test.ts +1 -9
  97. package/commandDecorators/dependencyBuilder.ts +10 -1
  98. package/commandDecorators/helpFormatter.ts +66 -144
  99. package/commandDecorators/index.ts +0 -2
  100. package/commandDecorators/targetMeta.ts +9 -7
  101. package/createTunnel.ts +7 -14
  102. package/dependencyScanner.test.ts +4 -16
  103. package/dependencyScanner.ts +27 -78
  104. package/devkitUtils.test.ts +47 -19
  105. package/executors.test.ts +114 -60
  106. package/executors.ts +165 -484
  107. package/fileEditor.ts +9 -37
  108. package/{getDirname.test.ts → fileSys.test.ts} +1 -1
  109. package/fileSys.ts +6 -8
  110. package/formSetterScanner.ts +3 -16
  111. package/frontendBuild/allRoutesBuilder.ts +7 -21
  112. package/frontendBuild/autoImportSync.test.ts +14 -38
  113. package/frontendBuild/autoImportSync.ts +15 -67
  114. package/frontendBuild/buildRouteClient.test.ts +10 -36
  115. package/frontendBuild/bundleDefine.ts +12 -0
  116. package/frontendBuild/clientBuildTypes.ts +5 -36
  117. package/frontendBuild/clientEntriesBundler.ts +9 -40
  118. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  119. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  120. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  121. package/frontendBuild/cssCandidateCache.ts +6 -41
  122. package/frontendBuild/cssCompiler.ts +29 -78
  123. package/frontendBuild/cssImportResolver.ts +5 -18
  124. package/frontendBuild/devChangePlanner.ts +7 -10
  125. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  126. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  127. package/frontendBuild/fontOptimizer.test.ts +5 -13
  128. package/frontendBuild/fontOptimizer.ts +15 -72
  129. package/frontendBuild/fontPruner.test.ts +5 -10
  130. package/frontendBuild/fontPruner.ts +5 -20
  131. package/frontendBuild/frontendBuild.test.ts +27 -52
  132. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  133. package/frontendBuild/hmrWatcher.test.ts +7 -31
  134. package/frontendBuild/hmrWatcher.ts +14 -71
  135. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  136. package/frontendBuild/precompressArtifacts.ts +5 -18
  137. package/frontendBuild/routeClientBuilder.ts +7 -47
  138. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  139. package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
  140. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  141. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  142. package/frontendBuild/styleContract.ts +1 -8
  143. package/frontendBuild/themeValidator.ts +4 -26
  144. package/frontendBuild/watchRootResolver.test.ts +23 -42
  145. package/frontendBuild/watchRootResolver.ts +5 -27
  146. package/getRelatedCnsts.ts +12 -92
  147. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  148. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  149. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  150. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  151. package/incrementalBuilder/builderChannel.test.ts +24 -49
  152. package/incrementalBuilder/builderChannel.ts +6 -37
  153. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  154. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  155. package/incrementalBuilder/devWatchBatch.test.ts +14 -18
  156. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  157. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  158. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  159. package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
  160. package/index.ts +3 -15
  161. package/integration/devResourceProbe.ts +50 -56
  162. package/integration/devStability.integration.test.ts +47 -194
  163. package/integration/devStabilityHarness.test.ts +9 -29
  164. package/integration/devStabilityHarness.ts +65 -330
  165. package/integration/ssrMemoryProbe.ts +41 -167
  166. package/libSource.test.ts +4 -15
  167. package/libSource.ts +19 -31
  168. package/lint/gritRules.test.ts +6 -23
  169. package/linter.test.ts +1 -2
  170. package/linter.ts +22 -115
  171. package/mobile/mobileTarget.ts +14 -13
  172. package/package.json +2 -2
  173. package/packageExports.test.ts +3 -18
  174. package/packageExportsMap.ts +3 -19
  175. package/piDependencyPins.test.ts +3 -14
  176. package/platformTest/IdleWatchedProcess.test.ts +5 -13
  177. package/platformTest/IdleWatchedProcess.ts +1 -5
  178. package/platformTest/LinuxDockerTarget.ts +20 -35
  179. package/platformTest/PlatformTestRun.test.ts +5 -9
  180. package/platformTest/PlatformTestTarget.ts +24 -2
  181. package/platformTest/TestSnapshot.test.ts +6 -14
  182. package/platformTest/TestSnapshot.ts +1 -5
  183. package/platformTest/WindowsSshTarget.ts +6 -22
  184. package/prompter.ts +3 -6
  185. package/qualityScanner.test.ts +6 -24
  186. package/qualityScanner.ts +196 -284
  187. package/recipeScanner.test.ts +7 -12
  188. package/recipeScanner.ts +9 -38
  189. package/repoIdentity.ts +2 -9
  190. package/routeSourceValidator.ts +7 -30
  191. package/scanInfo.ts +82 -214
  192. package/selectModel.ts +1 -2
  193. package/semver.ts +3 -5
  194. package/slicePlanner.test.ts +6 -24
  195. package/slicePlanner.ts +11 -32
  196. package/spinner.test.ts +3 -8
  197. package/spinner.ts +4 -16
  198. package/sshTunnel.ts +1 -6
  199. package/ssrScanner.test.ts +1 -6
  200. package/ssrScanner.ts +103 -113
  201. package/subspace.test.ts +6 -25
  202. package/subspace.ts +31 -109
  203. package/subspaceConfig.ts +3 -11
  204. package/testHelpers.ts +49 -34
  205. package/transforms/asyncDefaultExportDetector.ts +3 -11
  206. package/transforms/barrelAnalyzer.ts +43 -135
  207. package/transforms/barrelImportsPlugin.ts +38 -175
  208. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  209. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  210. package/transforms/moduleSyntax.ts +32 -0
  211. package/transforms/rscUseClientTransform.ts +35 -20
  212. package/transforms/transforms.test.ts +153 -44
  213. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  214. package/transforms/useClientBundlePlugin.ts +2 -22
  215. package/typeChecker.ts +7 -63
  216. package/uploadRelease.ts +10 -25
  217. package/workflow/artifacts.ts +18 -0
  218. package/workflow/executor.test.ts +1 -12
  219. package/workflow/executor.ts +85 -146
  220. package/workflow/index.ts +0 -1
  221. package/workflow/moduleIndex.test.ts +4 -15
  222. package/workflow/moduleIndex.ts +9 -49
  223. package/workflow/plan.ts +18 -25
  224. package/workflow/render.ts +80 -96
  225. package/workflow/source.ts +33 -56
  226. package/workflow/sourceAst.ts +50 -0
  227. package/workspaceLayout.ts +4 -19
  228. package/commandDecorators/commandMeta.ts +0 -7
  229. package/commandDecorators/types.ts +0 -10
  230. package/getDirname.ts +0 -4
  231. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  232. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  233. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,12 @@
1
+ import type { App } from "../commandDecorators";
2
+
3
+ export const bundleDefine = (app: App, command: "build" | "start", renderEnv: "ssr" | "csr") => {
4
+ const nodeEnv = command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
5
+ return {
6
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
7
+ "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify(renderEnv),
8
+ ...Object.fromEntries(
9
+ Object.entries(app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
10
+ ),
11
+ };
12
+ };
@@ -10,39 +10,14 @@ export interface BuildClientOptions {
10
10
  outputDir?: string;
11
11
  servePrefix?: string;
12
12
  mode?: "production" | "test";
13
- /** Additional Bun plugins applied to the client bundle build. */
14
13
  plugins?: BunPlugin[];
15
- /**
16
- * Legacy fallback: additional directories to filesystem-scan for
17
- * `"use client"` entry points. Only used when `workspaceRoot` /
18
- * `tsconfigPaths` are not provided (in which case we fall back to the old
19
- * filesystem-scan strategy). Prefer the graph-based discovery.
20
- */
14
+ /** Legacy filesystem-scan roots, used only when `workspaceRoot` / `tsconfigPaths` are absent. */
21
15
  extraClientScanRoots?: string[];
22
- /**
23
- * Environment variables to inline into the browser bundle via `define`.
24
- * Keys are the original `process.env` names. Each key is emitted as
25
- * `process.env.<KEY>` so existing server/client shared code that reads
26
- * `process.env.AKAN_PUBLIC_*` keeps working unchanged.
27
- */
16
+ /** Inlined via `define` as `process.env.<KEY>`. */
28
17
  publicEnv?: Record<string, string>;
29
- /**
30
- * Workspace root used by the graph-based client-entry discovery to resolve
31
- * tsconfig-aliased specifiers (`@libs/shared/ui`, `@apps/<name>/client`,
32
- * ...). When provided together with `tsconfigPaths`, discovery traverses
33
- * the import graph starting from `appDir` instead of doing a filesystem
34
- * scan, so `"use client"` files that no route transitively imports (e.g.
35
- * unused editor components in a shared UI package) stay out of the client
36
- * bundle.
37
- */
18
+ /** With `tsconfigPaths`, discovery walks the import graph from `appDir` instead of scanning the filesystem. */
38
19
  workspaceRoot?: string;
39
- /** Workspace tsconfig `compilerOptions.paths`. See `workspaceRoot`. */
40
20
  tsconfigPaths?: Record<string, string[]>;
41
- /**
42
- * Barrel specifiers whose imports are flattened by `barrelImportsPlugin`.
43
- * Discovery rewrites barrel imports with the same analyzer before scanning,
44
- * so the traversal sees the same leaf files the bundler will see.
45
- */
46
21
  barrelImports?: string[];
47
22
  }
48
23
 
@@ -73,10 +48,7 @@ export interface BundleClientEntriesOptions {
73
48
  entries: string[];
74
49
  plugins?: BunPlugin[];
75
50
  outputSubdir?: string;
76
- /**
77
- * Future Fast Refresh hook. Bun only injects the React Refresh transform;
78
- * the browser runtime/update protocol still needs to be provided by Akan.
79
- */
51
+ /** Bun only injects the React Refresh transform; the browser runtime and update protocol are Akan's. */
80
52
  reactFastRefresh?: boolean;
81
53
  }
82
54
 
@@ -92,13 +64,10 @@ export interface BundleClientEntriesInternalOptions extends BundleClientEntriesO
92
64
  export interface BundleClientEntriesResult {
93
65
  manifest: ClientManifest;
94
66
  ssrManifest: SsrManifest;
95
- /** Absolute path → serve URL for every entry point we produced. */
96
67
  entryUrlsByAbsPath: Map<string, string>;
97
- /** Absolute source entry path → emitted entry file absolute path. */
98
68
  entryOutputAbsByAbsPath: Map<string, string>;
99
- /** Absolute source files that Bun included for each client entry bundle. */
100
69
  entryDepsByAbsPath: Map<string, string[]>;
101
- /** Absolute source entry path → workspace-relative client reference id. */
70
+ /** Workspace-relative client reference id per absolute source entry. */
102
71
  clientReferenceIdByAbsPath: Map<string, string>;
103
72
  }
104
73
 
@@ -2,7 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { SsrManifest, SsrManifestEntry } from "akanjs/server";
4
4
  import type { BunPlugin } from "bun";
5
- import { toClientReferencePath } from "../transforms/rscUseClientTransform";
5
+ import { scanUseClientExports, toClientReferencePath } from "../transforms/rscUseClientTransform";
6
+ import { bundleDefine } from "./bundleDefine";
6
7
  import {
7
8
  type BundleClientEntriesInternalOptions,
8
9
  type BundleClientEntriesResult,
@@ -13,11 +14,6 @@ import {
13
14
  type OpaqueEntryAliases,
14
15
  } from "./clientBuildTypes";
15
16
 
16
- /**
17
- * Low-level primitive shared by the eager base build and the lazy per-route
18
- * builds. Takes a flat entrypoints list, runs `Bun.build`, and extracts a
19
- * `ClientManifest` / `SsrManifest` covering only those entries.
20
- */
21
17
  export class ClientEntriesBundler {
22
18
  #app: BundleClientEntriesInternalOptions["app"];
23
19
  #entries: string[];
@@ -29,7 +25,6 @@ export class ClientEntriesBundler {
29
25
  #target: ClientBundleTarget;
30
26
  #outputSubdir: string;
31
27
  #reactFastRefresh: boolean;
32
- #artifactDir: string;
33
28
  #outdir: string;
34
29
  #servePrefix: string;
35
30
  #manifest: ClientManifest = {};
@@ -52,8 +47,8 @@ export class ClientEntriesBundler {
52
47
  this.#target = options.target ?? "browser";
53
48
  this.#outputSubdir = options.outputSubdir ?? "client";
54
49
  this.#reactFastRefresh = options.reactFastRefresh ?? false;
55
- this.#artifactDir = `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
56
- this.#outdir = `${this.#artifactDir}/${this.#outputSubdir}`;
50
+ const artifactDir = `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
51
+ this.#outdir = `${artifactDir}/${this.#outputSubdir}`;
57
52
  this.#servePrefix = `/_akan/${this.#outputSubdir}`;
58
53
  }
59
54
 
@@ -68,7 +63,7 @@ export class ClientEntriesBundler {
68
63
  format: "esm",
69
64
  naming: CLIENT_BUNDLE_NAMING,
70
65
  metafile: true,
71
- define: this.#getDefine(),
66
+ define: bundleDefine(this.#app, this.#command, "ssr"),
72
67
  minify: this.#command === "build",
73
68
  optimizeImports: akanConfig.optimizeImports,
74
69
  reactFastRefresh: this.#command === "start" && this.#outputSubdir === "client" && this.#reactFastRefresh,
@@ -100,17 +95,6 @@ export class ClientEntriesBundler {
100
95
  };
101
96
  }
102
97
 
103
- #getDefine(): Record<string, string> {
104
- const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
105
- return {
106
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
107
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
108
- ...Object.fromEntries(
109
- Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
110
- ),
111
- };
112
- }
113
-
114
98
  async #createOpaqueEntryAliases(): Promise<OpaqueEntryAliases> {
115
99
  const aliasDir = path.join(this.#app.cwdPath, ".akan", "generated", "client-entry-alias", this.#outputSubdir);
116
100
  fs.mkdirSync(aliasDir, { recursive: true });
@@ -141,22 +125,10 @@ export class ClientEntriesBundler {
141
125
  }
142
126
 
143
127
  async #scanEntryExportNames(absEntry: string): Promise<string[]> {
144
- const source = await Bun.file(absEntry).text();
145
- const transpiler = new Bun.Transpiler({ loader: this.#loaderForEntry(absEntry) });
146
- return transpiler.scan(source).exports;
128
+ return scanUseClientExports(await Bun.file(absEntry).text(), absEntry, this.#app.workspace.workspaceRoot);
147
129
  }
148
130
 
149
- #loaderForEntry(absPath: string): "ts" | "tsx" | "js" | "jsx" {
150
- if (absPath.endsWith(".tsx")) return "tsx";
151
- if (absPath.endsWith(".jsx")) return "jsx";
152
- if (absPath.endsWith(".ts")) return "ts";
153
- return "js";
154
- }
155
-
156
- /**
157
- * Build a BunPlugin that marks a fixed set of bare specifiers as external via `onResolve`,
158
- * as opposed to `Bun.build({ external })`, so macro-time imports still resolve normally.
159
- */
131
+ // `onResolve` rather than `Bun.build({ external })`, so macro-time imports still resolve normally.
160
132
  #createExternalSpecifiersPlugin(): BunPlugin {
161
133
  const set = new Set(this.#external);
162
134
  const subpathSet = new Set(this.#externalSubpaths);
@@ -182,11 +154,8 @@ export class ClientEntriesBundler {
182
154
  exactExternals: Set<string>,
183
155
  subpathExternals: Set<string>,
184
156
  ): boolean {
185
- if (exactExternals.has(specifier)) return true;
186
- if (subpathExternals.has(specifier)) return true;
187
- for (const external of subpathExternals) {
188
- if (specifier.startsWith(`${external}/`)) return true;
189
- }
157
+ if (exactExternals.has(specifier) || subpathExternals.has(specifier)) return true;
158
+ for (const external of subpathExternals) if (specifier.startsWith(`${external}/`)) return true;
190
159
  return false;
191
160
  }
192
161
 
@@ -1,24 +1,17 @@
1
1
  import path from "node:path";
2
+ import { SOURCE_EXTS } from "../akanApp/devHostPolicy";
2
3
  import type { App } from "../commandDecorators";
3
4
  import { BarrelAnalyzer } from "../transforms/barrelAnalyzer";
4
5
  import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
5
6
  import type { AkanConfig, ClientEntryDiscovery, ScannedImport } from "./clientBuildTypes";
6
7
 
7
8
  const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
8
- const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
9
9
  const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
10
10
  const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
11
- // File extensions whose imports can't produce React client components and that
12
- // the bundler handles via dedicated loaders. Skipping them avoids resolver
13
- // noise (Bun's scanImports surfaces CSS / asset imports too).
11
+ // Bun's scanImports also surfaces CSS and asset imports, none of which can be a client component.
14
12
  const NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
15
13
  type PackageResolver = Awaited<ReturnType<typeof createTsconfigPackageResolver>>;
16
14
 
17
- /**
18
- * Everything the traversal ever asks of a file, which is why its text is not kept.
19
- *
20
- * `imports` is empty for a client entry: the walk stops there, so they are never scanned.
21
- */
22
15
  interface FileFacts {
23
16
  isClientEntry: boolean;
24
17
  imports: ScannedImport[];
@@ -26,33 +19,18 @@ interface FileFacts {
26
19
 
27
20
  const shouldSkipNodeModule = (absPath: string) => NODE_MODULES_RE.test(absPath) && !AKANJS_NODE_MODULE_RE.test(absPath);
28
21
 
29
- /**
30
- * Graph-based `"use client"` discovery, seeded from an explicit file list.
31
- *
32
- * Walks imports — including dynamic `import()` — while flattening barrel
33
- * specifiers through the same `BarrelAnalyzer` the runtime plugin uses, so
34
- * the traversal matches the module graph the bundler will actually see.
35
- */
22
+ /** `"use client"` discovery over the import graph (dynamic imports included), flattening barrels as the bundler does. */
36
23
  export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
37
24
  #akanConfig: Pick<AkanConfig, "barrelImports">;
38
25
  #resolvePackage: PackageResolver;
39
26
  #analyzer: BarrelAnalyzer;
40
27
  #tsTranspiler = new Bun.Transpiler({ loader: "tsx" });
41
28
  #fileExistsCache = new Map<string, Promise<boolean>>();
42
- /**
43
- * Derived facts per file, never the source text.
44
- *
45
- * This instance lives as long as the builder process does — it is rebuilt only when the akan config
46
- * changes — so caching whole source texts (and a second, barrel-rewritten copy of each) meant the
47
- * watcher held every source file it had ever walked for the whole dev session. Nothing downstream
48
- * wanted the text: the walk needs one boolean and one import list per file, and both were already
49
- * cached separately under the same key.
50
- */
29
+ // Facts, never source text: this instance lives as long as the builder process, so texts would pin every file walked.
51
30
  #factsCache = new Map<string, Promise<FileFacts | null>>();
52
31
  #resolvedFileCache = new Map<string, Promise<string | null>>();
53
32
  #resolvedSpecifierCache = new Map<string, Promise<string | null>>();
54
33
  #reachableEntriesCache = new Map<string, Set<string>>();
55
- /** Keys of the three caches above whose answer was "not there" — see `#forgetMissing`. */
56
34
  #missingFiles = new Set<string>();
57
35
  #unresolvedPaths = new Set<string>();
58
36
  #unresolvedSpecifiers = new Set<string>();
@@ -83,20 +61,12 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
83
61
  this.#reachableEntriesCache.delete(absPath);
84
62
  }
85
63
  if (files.length === 0) return;
86
- // Parent files cache the transitive result of their imports, so a changed
87
- // child can affect any reachable-entry cache above it.
64
+ // Reachable-entry results are transitive, so a changed child invalidates every ancestor.
88
65
  this.#reachableEntriesCache.clear();
89
66
  this.#forgetMissing();
90
67
  }
91
68
 
92
- /**
93
- * Drop every "there is no such file" answer, because a batch may be what created it.
94
- *
95
- * These caches are keyed by extension-less path and by `dir\0specifier`, neither of which maps back
96
- * to the path that just appeared, so a negative recorded before a module existed is unreachable any
97
- * other way — and this instance lives as long as the builder process. Positive answers are kept:
98
- * they are keyed by a real path, which arrives in `files` when it changes or goes away.
99
- */
69
+ // Negatives are keyed by extension-less path or `dir\0specifier`, which a new file's path cannot reach, so all go.
100
70
  #forgetMissing(): void {
101
71
  for (const key of this.#missingFiles) this.#fileExistsCache.delete(key);
102
72
  for (const key of this.#unresolvedPaths) this.#resolvedFileCache.delete(key);
@@ -121,10 +91,6 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
121
91
  return cached;
122
92
  }
123
93
 
124
- /**
125
- * Read a file once and keep only what the traversal asks of it. The text itself is dropped as soon
126
- * as the boolean and the import list are out of it — see `#factsCache`.
127
- */
128
94
  #facts(file: string): Promise<FileFacts | null> {
129
95
  const absPath = path.resolve(file);
130
96
  let cached = this.#factsCache.get(absPath);
@@ -134,7 +100,6 @@ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
134
100
  .text()
135
101
  .catch(() => null);
136
102
  if (content === null) return null;
137
- // A client entry ends the walk, so its imports are never needed.
138
103
  if (USE_CLIENT_RE.test(content)) return { isClientEntry: true, imports: [] };
139
104
  return { isClientEntry: false, imports: this.#scanImports(await this.#rewrite(content)) };
140
105
  })();
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import type { BaseBuildArtifact } from "akanjs/server";
4
4
  import { type PageEntry, resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
5
5
  import type { App } from "../commandDecorators";
6
+ import { bundleDefine } from "./bundleDefine";
6
7
  import { getPageKeyBasePath } from "./cssCompiler";
7
8
  import { PagesBundleBuilder } from "./pagesBundleBuilder";
8
9
  import { PagesEntrySourceGenerator } from "./pagesEntrySourceGenerator";
@@ -61,10 +62,9 @@ export class CsrArtifactBuilder {
61
62
  splitting: false,
62
63
  minify: true,
63
64
  env: "AKAN_PUBLIC_*",
64
- define: this.#define(),
65
+ define: bundleDefine(this.#app, this.#command, "csr"),
65
66
  optimizeImports: akanConfig.optimizeImports,
66
- // The base artifact's compiled sheet is the only stylesheet, as it is for SSR: a raw `.css` reached through
67
- // the route graph is Tailwind source, and every root layout stylesheet in the graph would land in every HTML.
67
+ // A raw `.css` in the route graph is Tailwind source; the base artifact's compiled sheet is the only stylesheet.
68
68
  plugins: [PagesBundleBuilder.createCssStubPlugin()],
69
69
  });
70
70
 
@@ -118,17 +118,6 @@ export class CsrArtifactBuilder {
118
118
  return path.join(this.#generatedDir, filename);
119
119
  }
120
120
 
121
- #define(): Record<string, string> {
122
- const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
123
- return {
124
- "process.env.NODE_ENV": JSON.stringify(nodeEnv),
125
- "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("csr"),
126
- ...Object.fromEntries(
127
- Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
128
- ),
129
- };
130
- }
131
-
132
121
  async #createEntryFile(basePath: string, pageEntries: PageEntry[]): Promise<readonly [string, string]> {
133
122
  return [
134
123
  this.#generatedPath(CsrArtifactBuilder.entryFilename(basePath)),
@@ -168,7 +157,7 @@ void bootCsr(pages);
168
157
 
169
158
  async #inlineCsrArtifacts(cssAssets: Record<string, CssAsset>): Promise<void> {
170
159
  const jsFiles = new Set<string>();
171
- for (const htmlPath of await this.#htmlOutputPaths()) {
160
+ for (const htmlPath of await this.#listOutputFiles((filePath) => filePath.endsWith(".html"))) {
172
161
  const htmlFile = Bun.file(htmlPath);
173
162
  if (!(await htmlFile.exists())) continue;
174
163
  const basePath = CsrArtifactBuilder.basePathOfHtml(htmlPath);
@@ -210,10 +199,6 @@ void bootCsr(pages);
210
199
  return { html: next, jsFiles };
211
200
  }
212
201
 
213
- async #htmlOutputPaths(): Promise<string[]> {
214
- return await this.#listOutputFiles((filePath) => filePath.endsWith(".html"));
215
- }
216
-
217
202
  async #listOutputFiles(predicate: (filePath: string) => boolean): Promise<string[]> {
218
203
  const glob = new Bun.Glob("**/*");
219
204
  const files: string[] = [];
@@ -223,12 +208,8 @@ void bootCsr(pages);
223
208
  return files.sort();
224
209
  }
225
210
 
226
- /**
227
- * Bun's HTML bundler hoists the module script into `<head>`, so once that script is inline its source is part
228
- * of the text being searched — and a React bundle contains `<body` and `</head>` as strings. Positions are
229
- * taken on a copy with script, style and comment bodies blanked, and the snippet always lands after whatever
230
- * was injected before it: prepending would reverse the cascade order the caller chose.
231
- */
211
+ // Bun hoists the module script into <head> and a React bundle contains "</head>" as text, so positions come from a
212
+ // copy with script/style/comment bodies blanked. Never prepends: that would reverse the caller's cascade order.
232
213
  static injectBeforeHeadEnd(html: string, snippet: string): string {
233
214
  const scannable = CsrArtifactBuilder.blankEmbeddedContent(html);
234
215
  const headEnd = scannable.search(/<\/head\s*>/i);
@@ -17,18 +17,12 @@ describe("CssCandidateCache token extraction", () => {
17
17
  expect(candidates).toEqual(expect.arrayContaining(["w-full", "hover:bg-muted", "md:w-1/2", "text-[#fff]"]));
18
18
  });
19
19
 
20
- // A candidate that opens with `[` used to be torn into `_td` + `px-3`, so every descendant-variant
21
- // rule in the framework compiled to nothing and its element rendered unstyled with no diagnostic.
22
20
  test("keeps an arbitrary variant that starts with a bracket", async () => {
23
21
  const candidates = await scan(`const c = "[&_td]:px-3 [&>*]:gap-2 [&_tbody_tr]:border-t";`);
24
22
  expect(candidates).toEqual(expect.arrayContaining(["[&_td]:px-3", "[&>*]:gap-2", "[&_tbody_tr]:border-t"]));
25
23
  expect(candidates).not.toContain("_td");
26
24
  });
27
25
 
28
- // A labelled tuple type looks like an arbitrary property, and compiles to a rule whose declaration is
29
- // TypeScript. `optimize()` cannot parse that, and drops far more than the offending rule on its way
30
- // out — every variant in the build went missing, so nothing hovered, focused or responded to a
31
- // breakpoint. Tailwind writes a space in an arbitrary value as `_`, so whitespace rules the token out.
32
26
  test("ignores a labelled tuple type that only looks like an arbitrary property", async () => {
33
27
  const candidates = await scan(
34
28
  `type A = [key: string, make: () => unknown][];\ntype B = [setQueryArgs: (a: Args) => Args, options?: Policy];`,
@@ -1,18 +1,7 @@
1
1
  import { stat } from "node:fs/promises";
2
2
 
3
- /**
4
- * Every identifier-ish token in a source file is a potential tailwind class. The leading alternation
5
- * matters: an arbitrary variant starts with `[` (`[&_td]:px-3`, `[&>*]:gap-2`), and a pattern anchored
6
- * on a word character tears it into `_td` plus `px-3` instead — both meaningless, so the rule compiles
7
- * to nothing and the element silently renders unstyled.
8
- *
9
- * Brackets must hold no whitespace. Tailwind spells a space in an arbitrary value `_`, so a bracket
10
- * group containing one is never a class — but it is exactly the shape of a labelled tuple type
11
- * (`[key: string, make: () => unknown]`). Scanned as a candidate, that compiles to an arbitrary
12
- * *property* rule carrying TypeScript as its declaration, which the lightningcss pass in `optimize()`
13
- * cannot parse; its error recovery then drops a whole span of the stylesheet — in practice every
14
- * variant rule in the build, leaving hover, focus, responsive and dark styles silently absent.
15
- */
3
+ // The leading `[` alternation keeps arbitrary variants (`[&_td]:px-3`) whole. Brackets hold no whitespace: a labelled
4
+ // tuple type scanned as a class compiles to a rule lightningcss cannot parse, and it then drops every variant rule.
16
5
  const CANDIDATE_RE = /-?(?:[\w@]|\[[^\]\s]+\])[\w:/.-]*(?:\[[^\]\s]+\][\w:/.-]*)*/g;
17
6
 
18
7
  interface CachedFile {
@@ -26,19 +15,9 @@ interface CacheFile {
26
15
  files: Record<string, CachedFile>;
27
16
  }
28
17
 
29
- /**
30
- * Tailwind candidate tokens per source file, cached on disk across builds.
31
- *
32
- * The scan reads the **full text** of every source file on every CSS rebuild — measured at 385-508ms
33
- * per save on `apps/akan`. Phase 2 moved css compilation into a per-generation batch worker, so an
34
- * in-memory cache (what `03-phase3-topology-and-trim.md` §3.4 originally proposed) buys nothing: the
35
- * process that would hold it exits before the next save. Disk is what survives the worker, a builder
36
- * recycle and a dev-host restart alike, which is the same reasoning §3.3 used for the font cache.
37
- *
38
- * Keyed on (mtime, size) per file, so a save re-reads only the files in that batch.
39
- */
18
+ /** Tailwind candidates per source file, on disk: the batch worker that compiles CSS exits before the next save. */
40
19
  export class CssCandidateCache {
41
- /** Bump when the token regex or the entry shape changes, so stale extractions are not reused. */
20
+ // Bump when the token regex or the entry shape changes, so stale extractions are not reused.
42
21
  static readonly #version = 3;
43
22
  readonly #path: string;
44
23
  readonly #entries = new Map<string, CachedFile>();
@@ -58,7 +37,6 @@ export class CssCandidateCache {
58
37
  return this.#rescanned;
59
38
  }
60
39
 
61
- /** A cache that cannot be read or is a version behind simply starts empty — it is only an optimisation. */
62
40
  async load(): Promise<this> {
63
41
  const raw = (await Bun.file(this.#path)
64
42
  .json()
@@ -71,12 +49,7 @@ export class CssCandidateCache {
71
49
  return this;
72
50
  }
73
51
 
74
- /**
75
- * The file's candidate tokens, read from disk only when its (mtime, size) no longer matches.
76
- *
77
- * Read errors propagate, as they did before this cache existed: a source file the compiler cannot
78
- * read is a broken build, not a cache miss to paper over.
79
- */
52
+ // A read error propagates: an unreadable source is a broken build, not a cache miss.
80
53
  async candidatesFor(file: string): Promise<string[]> {
81
54
  const stats = await stat(file).catch(() => null);
82
55
  const cached = this.#entries.get(file);
@@ -94,13 +67,6 @@ export class CssCandidateCache {
94
67
  return candidates;
95
68
  }
96
69
 
97
- /**
98
- * Persist, dropping files the scan no longer reaches so a renamed or deleted module does not keep
99
- * feeding its classes to the compiler forever.
100
- *
101
- * A rebuild that re-read nothing writes nothing: the boot double-build and any CSS rebuild triggered
102
- * by something other than a source edit would otherwise rewrite the whole file for no change.
103
- */
104
70
  async save(present: Set<string>): Promise<void> {
105
71
  for (const file of [...this.#entries.keys()]) {
106
72
  if (present.has(file)) continue;
@@ -111,8 +77,7 @@ export class CssCandidateCache {
111
77
  this.#dirty = false;
112
78
  const files: Record<string, CachedFile> = {};
113
79
  for (const [file, entry] of this.#entries) files[file] = entry;
114
- // A cache that cannot be written is a slow build, not a failed one — a read-only checkout must
115
- // still compile.
80
+ // An unwritable cache is a slow build, not a failed one: a read-only checkout must still compile.
116
81
  await Bun.write(
117
82
  this.#path,
118
83
  JSON.stringify({ version: CssCandidateCache.#version, files } satisfies CacheFile),