@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
@@ -1,29 +1,20 @@
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";
4
2
  import path from "node:path";
5
3
  import { AbstractDoc } from "./abstractDoc";
6
4
  import { AppExecutor, WorkspaceExecutor } from "./executors";
5
+ import { tempDirs, writeText } from "./testHelpers";
7
6
 
8
- const tempRoots: string[] = [];
7
+ const makeTempRoot = tempDirs("akan-abstract-doc-");
9
8
 
10
9
  const makeApp = async (files: Record<string, string>) => {
11
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-abstract-doc-"));
12
- tempRoots.push(root);
10
+ const root = await makeTempRoot();
13
11
  const appName = "abstractDemo";
14
- for (const [filePath, content] of Object.entries(files)) {
15
- const absolutePath = path.join(root, "apps", appName, filePath);
16
- await mkdir(path.dirname(absolutePath), { recursive: true });
17
- await writeFile(absolutePath, content);
18
- }
12
+ for (const [filePath, content] of Object.entries(files))
13
+ await writeText(path.join(root, "apps", appName, filePath), content);
19
14
  const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "repo" });
20
15
  return AppExecutor.from(workspace, appName);
21
16
  };
22
17
 
23
- afterEach(async () => {
24
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
25
- });
26
-
27
18
  describe("AbstractDoc.kindOf", () => {
28
19
  test("reads the module kind off the sys-relative path", () => {
29
20
  expect(AbstractDoc.kindOf("lib/user/user.abstract.md")).toBe("domain");
package/abstractDoc.ts CHANGED
@@ -3,7 +3,6 @@ import type { SysExecutor } from "./executors";
3
3
 
4
4
  export type AbstractKind = "domain" | "service" | "scalar" | "other";
5
5
 
6
- /** One `*.abstract.md` file: the agent-facing summary a module carries next to its source. */
7
6
  export class AbstractDoc {
8
7
  static readonly suffix = ".abstract.md";
9
8
  /** `akan quality` warns above this. */
@@ -17,7 +16,6 @@ export class AbstractDoc {
17
16
  static lineCountOf(content: string) {
18
17
  return content.split(/\r?\n/).length;
19
18
  }
20
- /** Module kind read off the sys-relative path; anything outside a `lib/` module folder is "other". */
21
19
  static kindOf(filePath: string): AbstractKind {
22
20
  const [root, folder, ...rest] = filePath.split("/");
23
21
  if (root !== "lib" || !folder) return "other";
@@ -60,8 +58,7 @@ export class AbstractDoc {
60
58
  get lineCount() {
61
59
  return AbstractDoc.lineCountOf(this.content);
62
60
  }
63
- // An AI editor answers in prose when it decides nothing needs changing, and that prose would silently
64
- // replace the abstract — so a candidate must look like a markdown file and be shorter than what it replaces.
61
+ // An AI editor that decides nothing needs changing answers in prose, which must not replace the abstract.
65
62
  canReplaceWith(candidate: string) {
66
63
  const next = candidate.trim();
67
64
  if (!next.startsWith("#")) return false;
@@ -1,6 +1,4 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
2
  import path from "node:path";
5
3
  import {
6
4
  AGENT_BLOCK_END,
@@ -14,6 +12,7 @@ import {
14
12
  upsertAgentBlock,
15
13
  } from "./agentsIndex";
16
14
  import type { RecipeInfo } from "./recipeScanner";
15
+ import { tempDirs, writeText } from "./testHelpers";
17
16
 
18
17
  const button: RecipeInfo = {
19
18
  name: "buttonRecipe",
@@ -79,12 +78,10 @@ describe("upsertAgentBlock / extractAgentBlock", () => {
79
78
  });
80
79
 
81
80
  describe("collectScopeRecipeSources", () => {
81
+ const makeTempRoot = tempDirs("agents-index-");
82
82
  test("collects own + dependency lib recipes, never the framework's", async () => {
83
- const root = await mkdtemp(path.join(tmpdir(), "agents-index-"));
84
- const write = async (rel: string, content: string) => {
85
- await mkdir(path.dirname(path.join(root, rel)), { recursive: true });
86
- await writeFile(path.join(root, rel), content);
87
- };
83
+ const root = await makeTempRoot();
84
+ const write = async (rel: string, content: string) => await writeText(path.join(root, rel), content);
88
85
  await write("apps/minimal/ui/Recipe/appCard.ts", `export const appCard = recipe(tv({ base: "x" }));`);
89
86
  await write("libs/shared/ui/Recipe/panel.ts", `export const panelRecipe = recipe(tv({ base: "y" }));`);
90
87
  await write("pkgs/akanjs/ui/recipe/buttonRecipe.ts", `export const buttonRecipe = recipe(tv({ base: "z" }));`);
package/agentsIndex.ts CHANGED
@@ -1,31 +1,18 @@
1
1
  import type { RecipeInfo, RecipeSource } from "./recipeScanner";
2
2
 
3
- // recipeScanner 는 상단에서 typescript(~65MB)를 끌어온다. 이 모듈은 executors(CLI 엔트리 그래프)에
4
- // 상주하므로 스캔 스택은 첫 사용 시점에 지연 로드한다 — 정적 import 로 되돌리면 entryModuleGraph 테스트가 깨진다.
3
+ // Lazy: recipeScanner loads `typescript` (~65MB) and this module sits in the CLI entry graph (entryModuleGraph test).
5
4
  let recipeScannerLoad: Promise<typeof import("./recipeScanner")> | null = null;
6
5
  const loadRecipeScanner = () => (recipeScannerLoad ??= import("./recipeScanner"));
7
6
 
8
- /**
9
- * agentsIndex — 스코프별 에이전트 색인의 단일 렌더러.
10
- *
11
- * 색인은 소유 경계로 쪼갠다: 루트 AGENTS.md 는 프레임워크(akanjs/ui) 레시피만 싣고, 각 앱/lib 은
12
- * 자기 스코프에서 import 가능한 레시피(own + 의존 lib)를 자기 AGENTS.md 에 싣는다. 항상 로드되는
13
- * 컨텍스트가 앱 수에 비례해 커지는 것과, import 불가능한 이웃 앱 레시피가 환상을 유발하는 것을 막는다.
14
- *
15
- * 신선도는 두 지점이 보장한다: `SysExecutor.scan(write)` 가 재생성하고(akan sync/build/start 가 전부
16
- * 지나가는 길목), `akan lint` 가 스캔 결과와 커밋된 블록을 비교해 stale 이면 실패시킨다.
17
- */
7
+ //* Split by ownership (root: framework recipes; each app/lib: what it may import), so always-loaded context does not
8
+ //* grow with the app count and no unimportable neighbour recipe is listed.
18
9
 
19
10
  export const AGENT_BLOCK_START = "<!-- akan:agent:start -->";
20
11
  export const AGENT_BLOCK_END = "<!-- akan:agent:end -->";
21
12
 
22
13
  const AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
23
14
 
24
- /**
25
- * The generating release, stamped into the workspace block. Nothing re-runs `akan agent install` on its own, so
26
- * without a stamp a workspace carrying conventions from four releases ago is indistinguishable from a current one —
27
- * `akan doctor` compares this against the installed devkit and says so.
28
- */
15
+ /** Nothing re-runs `akan agent install` on its own, so `akan doctor` compares this stamp against the installed devkit. */
29
16
  export const stampBlockVersion = (block: string, version: string): string =>
30
17
  `${AGENT_VERSION_PREFIX}${version} -->\n\n${block}`;
31
18
 
@@ -36,7 +23,7 @@ export const extractBlockVersion = (content: string): string | null =>
36
23
  /** The running `@akanjs/devkit` version; null when its package.json is unreadable, which must not fail a doctor run. */
37
24
  export const readDevkitVersion = async (): Promise<string | null> => {
38
25
  const { readFile } = await import("node:fs/promises");
39
- const { getDirname } = await import("./getDirname");
26
+ const { getDirname } = await import("./fileSys");
40
27
  try {
41
28
  const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
42
29
  return (JSON.parse(raw) as { version?: string }).version ?? null;
@@ -97,10 +84,7 @@ export const renderRecipeEntries = (recipes: RecipeInfo[]): string => {
97
84
  .join("\n\n");
98
85
  };
99
86
 
100
- /**
101
- * Every recipe source importable from the scope: its own `ui/Recipe/` plus each dependency lib's.
102
- * Framework recipes are excluded on purpose — they live in the root AGENTS.md, valid for every scope.
103
- */
87
+ /** Own `ui/Recipe/` plus each dependency lib's; framework recipes live in the root AGENTS.md instead. */
104
88
  export const collectScopeRecipeSources = async (
105
89
  workspaceRoot: string,
106
90
  scope: AgentsIndexScope,
@@ -1,34 +1,24 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { realpath, rm, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { Logger } from "akanjs/common";
6
5
  import type { App } from "../commandDecorators";
6
+ import { tempDirs, writeText } from "../testHelpers";
7
7
  import { BackendImportGraph } from "./BackendImportGraph";
8
8
  import { filesChangedSince } from "./devHostPolicy";
9
9
 
10
10
  describe("BackendImportGraph", () => {
11
- const tempRoots: string[] = [];
11
+ const makeTempRoot = tempDirs("akan-devkit-graph-");
12
12
 
13
13
  const makeGraph = async (files: Record<string, string>) => {
14
- // Realpath, not the mkdtemp path: `Bun.resolveSync` returns real paths, and on macOS `/var/folders`
15
- // is a symlink, so an unresolved root makes every resolved import look like it escapes the workspace.
16
- const workspaceRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "akan-devkit-graph-")));
17
- tempRoots.push(workspaceRoot);
14
+ // Realpath: `Bun.resolveSync` returns real paths, and macOS `/var/folders` is a symlink.
15
+ const workspaceRoot = await realpath(await makeTempRoot());
18
16
  const cwdPath = path.join(workspaceRoot, "apps/demo");
19
- for (const [rel, source] of Object.entries(files)) {
20
- const filePath = path.join(cwdPath, rel);
21
- await mkdir(path.dirname(filePath), { recursive: true });
22
- await writeFile(filePath, source);
23
- }
17
+ for (const [rel, source] of Object.entries(files)) await writeText(path.join(cwdPath, rel), source);
24
18
  const app = { cwdPath, workspace: { workspaceRoot } } as unknown as App;
25
19
  return { graph: new BackendImportGraph(app, new Logger("test")), cwdPath };
26
20
  };
27
21
 
28
- afterEach(async () => {
29
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
30
- });
31
-
32
22
  test("walks the backend entrypoints' import graph", async () => {
33
23
  const { graph, cwdPath } = await makeGraph({
34
24
  "main.ts": 'import "./server";\n',
@@ -51,7 +41,6 @@ describe("BackendImportGraph", () => {
51
41
  await graph.refresh();
52
42
  expect(graph.has(path.join(cwdPath, "lib/added.ts"))).toBe(false);
53
43
 
54
- // The scan cache is keyed on (mtimeMs, size), so the rewrite must invalidate it.
55
44
  await writeFile(path.join(cwdPath, "server.ts"), 'import "./lib/added";\nexport default 1;\n');
56
45
 
57
46
  await graph.refresh();
@@ -81,8 +70,7 @@ describe("BackendImportGraph", () => {
81
70
  await graph.refresh();
82
71
  const before = await graph.fingerprint();
83
72
 
84
- // The builder is gone here, so no watcher event exists for this save — which is the whole reason
85
- // the stamps are taken. `mtimeMs` has a coarse clock on Linux, so the size has to move too.
73
+ // `mtimeMs` has a coarse clock on Linux, so the size has to move too.
86
74
  await writeFile(path.join(cwdPath, "lib/handler.ts"), "export const handler = () => 'changed';\n");
87
75
 
88
76
  expect(filesChangedSince(before, await graph.fingerprint())).toEqual([path.join(cwdPath, "lib/handler.ts")]);
@@ -96,11 +84,9 @@ describe("BackendImportGraph", () => {
96
84
  });
97
85
  await graph.refresh();
98
86
  const before = await graph.fingerprint();
99
- // A recycle with no edit in it is the common case, and it must not cost a backend restart.
100
87
  expect(filesChangedSince(before, await graph.fingerprint())).toEqual([]);
101
88
 
102
89
  await rm(path.join(cwdPath, "lib/handler.ts"));
103
- // Deleted counts as changed: the backend is still running what used to be there.
104
90
  expect(filesChangedSince(before, await graph.fingerprint())).toEqual([path.join(cwdPath, "lib/handler.ts")]);
105
91
  });
106
92
 
@@ -22,14 +22,7 @@ export class BackendImportGraph {
22
22
  readonly #jsTranspiler = new Bun.Transpiler({ loader: "js" });
23
23
  readonly #jsxTranspiler = new Bun.Transpiler({ loader: "jsx" });
24
24
  #files = new Set<string>();
25
- /**
26
- * `refresh()` runs on every server-side save and on every dev-host recycle, and re-reading plus
27
- * re-parsing files that did not change is the whole cost of it. Keyed on (mtimeMs, size).
28
- *
29
- * Specifiers are cached, not resolved paths: creating a file can change what an *unchanged* importer's
30
- * specifier resolves to, and `Bun.resolveSync` is cheap next to a read plus a transpiler scan. Only
31
- * the scan result is retained — never the source text.
32
- */
25
+ // Specifiers, not resolved paths: a new file can change what an unchanged importer's specifier resolves to.
33
26
  #scanCache = new Map<string, { mtimeMs: number; size: number; specifiers: Bun.Import[] }>();
34
27
  #ready = false;
35
28
  #lastRefreshSucceeded = false;
@@ -51,17 +44,6 @@ export class BackendImportGraph {
51
44
  return this.#files.has(path.resolve(file));
52
45
  }
53
46
 
54
- /**
55
- * Stamp every file the backend runs, so a caller can ask later what moved.
56
- *
57
- * Taken when the builder goes away and compared when its replacement is up, because nothing watches
58
- * the tree in between: the departing builder's watcher left with it, and the replacement's index
59
- * primes from the disk it finds, so an edit that lands in the gap is *baseline* to it and is never
60
- * reported at all. The client half of such an edit is rescued by the replacement's boot build; the
61
- * backend half is a server left running code that no longer exists, with nothing on screen to say so.
62
- *
63
- * One `stat` per graph file, against a gap that costs a whole boot build anyway.
64
- */
65
47
  async fingerprint(): Promise<SourceFingerprints> {
66
48
  const stamps = await Promise.all(
67
49
  [...this.#files].map(async (file) => {
@@ -117,7 +99,6 @@ export class BackendImportGraph {
117
99
  return files;
118
100
  }
119
101
 
120
- /** Null when the file is gone, which is the existence check the walk used to make separately. */
121
102
  async #importsOf(file: string): Promise<Bun.Import[] | null> {
122
103
  const stats = await stat(file).catch(() => null);
123
104
  if (!stats?.isFile()) return null;
@@ -5,13 +5,11 @@ describe("readProcessRssBytes", () => {
5
5
  test("reads this process's own rss", async () => {
6
6
  const rssBytes = await AkanAppHost.readProcessRssBytes(process.pid);
7
7
  if (rssBytes === null) throw new Error("expected to read this process's own rss");
8
- // Loose bounds on purpose: the point is that it read a real number from the OS, not which number.
9
8
  expect(rssBytes).toBeGreaterThan(1024 * 1024);
10
9
  expect(rssBytes).toBeLessThan(64 * 1024 * 1024 * 1024);
11
10
  });
12
11
 
13
- // Null rather than 0, because callers must treat an unreadable pid as "no new information" — a 0
14
- // would read as "settled below the ceiling" and cancel a recycle that should happen.
12
+ // A 0 would read as "settled below the ceiling" and cancel a recycle that should happen.
15
13
  test("returns null for a pid that does not exist", async () => {
16
14
  expect(await AkanAppHost.readProcessRssBytes(2_147_483_646)).toBeNull();
17
15
  });