@akanjs/devkit 3.0.0-beta.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
@@ -1,30 +1,18 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, rm } 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";
3
+ import { tempDirs } from "../testHelpers";
5
4
  import { BuilderChannel } from "./builderChannel";
6
5
 
7
- const tempDirs: string[] = [];
8
-
9
- afterEach(async () => {
10
- for (const dir of tempDirs.splice(0)) await rm(dir, { recursive: true, force: true });
11
- });
6
+ const tempDir = tempDirs("builder-channel-");
12
7
 
13
8
  type SendMode = "drain" | "await" | "bare";
14
9
 
15
- /**
16
- * Run a child that sends what the builder sends and then exits the way the recycle drain does, and
17
- * report what the parent actually received.
18
- *
19
- * `payload` picks the message shape, because shape decides whether a bare send survives: `manifest` is a
20
- * map of many short strings (a `build-route-res` delta), `css` is one long string (a base64 stylesheet).
21
- */
10
+ // Shape decides whether a bare send survives: `manifest` is many short strings, `css` one long string.
22
11
  const sendThenExit = async (
23
12
  bytes: number,
24
13
  { mode, payload }: { mode: SendMode; payload: "manifest" | "css" },
25
14
  ): Promise<Array<{ type?: string }>> => {
26
- const dir = await mkdtemp(path.join(os.tmpdir(), "builder-channel-"));
27
- tempDirs.push(dir);
15
+ const dir = await tempDir();
28
16
  const entry = path.join(dir, "child.ts");
29
17
  const modulePath = JSON.stringify(path.join(import.meta.dir, "builderChannel"));
30
18
  const build =
@@ -63,44 +51,42 @@ const sendThenExit = async (
63
51
  return received;
64
52
  };
65
53
 
54
+ const withDeferredFlushes = async (fn: (flushes: Array<() => void>) => Promise<void>) => {
55
+ const send = process.send;
56
+ const flushes: Array<() => void> = [];
57
+ try {
58
+ (process as { send?: unknown }).send = (_msg: unknown, _h: unknown, _o: unknown, cb: () => void) => {
59
+ flushes.push(cb);
60
+ return true;
61
+ };
62
+ await fn(flushes);
63
+ } finally {
64
+ (process as { send?: unknown }).send = send;
65
+ }
66
+ };
67
+
66
68
  describe("BuilderChannel", () => {
67
69
  test("delivers a reply too large for the pipe buffer before the process exits", async () => {
68
70
  expect(await sendThenExit(1_000_000, { mode: "await", payload: "manifest" })).toMatchObject([{ id: 7 }]);
69
- // A manifest delta is usually well past the buffer, but the small case must keep working too.
70
71
  expect(await sendThenExit(0, { mode: "await", payload: "manifest" })).toMatchObject([{ id: 7 }]);
71
72
  });
72
73
 
73
74
  test("a drained event survives the exit even though nobody awaited it", async () => {
74
- // The event path: `css-updated` is relayed with no caller to await it, and the recycle drain exits
75
- // milliseconds later. Only `drain()` stands between the two.
76
75
  expect(await sendThenExit(200_000, { mode: "drain", payload: "css" })).toMatchObject([{ type: "css-updated" }]);
77
76
  });
78
77
 
79
- /**
80
- * darwin only, measured: on Linux a 1MB message sent immediately before `process.exit` **arrives in
81
- * full**, so the loss this control demonstrates does not reproduce there and the assertion would fail
82
- * for the right reason on the wrong platform. `BuilderChannel` itself stays correct and costs nothing
83
- * on Linux — the bug it prevents is one macOS developers hit and a Linux fleet does not.
84
- */
78
+ // darwin only: on Linux a message sent right before `process.exit` arrives in full, so the loss does not reproduce.
85
79
  test.skipIf(process.platform !== "darwin")(
86
80
  "without the flush wait the same messages are lost, which is why this class exists",
87
81
  async () => {
88
82
  // Controls, not requirements: if a future bun flushes ipc writes on exit, these fail and say so.
89
83
  expect(await sendThenExit(1_000_000, { mode: "bare", payload: "manifest" })).toEqual([]);
90
- // 20KB of css, far below the 64KB the manifest shape survives — one long string dies much earlier,
91
- // so no size threshold would have been safe to special-case.
92
84
  expect(await sendThenExit(20_000, { mode: "bare", payload: "css" })).toEqual([]);
93
85
  },
94
86
  );
95
87
 
96
88
  test("drain resolves only once every tracked send has flushed", async () => {
97
- const send = process.send;
98
- const flushes: Array<() => void> = [];
99
- try {
100
- (process as { send?: unknown }).send = (_msg: unknown, _h: unknown, _o: unknown, cb: () => void) => {
101
- flushes.push(cb);
102
- return true;
103
- };
89
+ await withDeferredFlushes(async (flushes) => {
104
90
  BuilderChannel.emit({ type: "builder-ready" });
105
91
  BuilderChannel.emit({ type: "builder-ready" });
106
92
  let drained = false;
@@ -113,31 +99,20 @@ describe("BuilderChannel", () => {
113
99
  expect(drained).toBe(false);
114
100
  flushes[1]?.();
115
101
  expect(await draining).toBe(2);
116
- } finally {
117
- (process as { send?: unknown }).send = send;
118
- }
102
+ });
119
103
  });
120
104
 
121
105
  test("a send started while draining is drained too", async () => {
122
- const send = process.send;
123
- const flushes: Array<() => void> = [];
124
- try {
125
- (process as { send?: unknown }).send = (_msg: unknown, _h: unknown, _o: unknown, cb: () => void) => {
126
- flushes.push(cb);
127
- return true;
128
- };
106
+ await withDeferredFlushes(async (flushes) => {
129
107
  BuilderChannel.emit({ type: "builder-ready" });
130
108
  const draining = BuilderChannel.drain();
131
- // What `#reportMetrics` does from a work item's `finally`, after the drain has begun.
132
109
  BuilderChannel.emit({ type: "builder-metrics", data: { rssBytes: 1, generation: 1, workCount: 1 } });
133
110
  for (let i = 0; i < 4 && flushes.length; i++) {
134
111
  flushes.shift()?.();
135
112
  await Bun.sleep(1);
136
113
  }
137
114
  expect(await draining).toBe(2);
138
- } finally {
139
- (process as { send?: unknown }).send = send;
140
- }
115
+ });
141
116
  });
142
117
 
143
118
  test("resolves instead of hanging when there is no ipc channel", async () => {
@@ -1,42 +1,18 @@
1
1
  import type { BuilderMessage } from "akanjs/server";
2
2
 
3
- /**
4
- * Every message the builder sends its host, and the record of which of those writes have actually left
5
- * the process.
6
- *
7
- * `process.exit` discards an ipc write that has not flushed, and this process exits routinely — the
8
- * recycle that keeps its RSS under the ceiling ends in `process.exit(0)`. Whether a given message
9
- * survives that depends on its *shape*, not only its size. Measured on bun 1.3.14 (darwin,
10
- * `serialization: "advanced"`), send-then-exit-immediately, 20 rounds each:
11
- *
12
- * - one long string: 8KB delivered 20/20, 16KB delivered **0/20**
13
- * - many short strings: 60KB delivered 20/20, 100KB delivered **0/20**
14
- *
15
- * `css-updated` is the first shape (a base64 stylesheet per url) and `build-route-res` the second (a
16
- * manifest delta), so neither sits safely under a threshold worth relying on. Nothing here assumes one:
17
- * every send reports when it flushed, and `drain` is what `shutdown` awaits before exiting.
18
- *
19
- * A natural exit does *not* have this problem — a process that simply returns from `main` flushes 1MB
20
- * 20/20 — which is why the disposable build worker needs none of this. Only an explicit `process.exit`
21
- * cuts a write short.
22
- */
3
+ // `process.exit` drops an ipc write that has not flushed (a natural exit does not), and whether one survives
4
+ // depends on the message's shape as well as its size — so every send reports its flush and `shutdown` drains.
23
5
  export class BuilderChannel {
24
- /**
25
- * Bound so a runtime that ever stops invoking the flush callback costs one late message instead of
26
- * wedging the shutdown drain until the host's kill watchdog fires.
27
- */
6
+ // Bounded so a runtime that never calls the flush callback costs one late message, not a wedged drain.
28
7
  static readonly #flushTimeoutMs = 5_000;
29
- /** Sends that have not flushed yet; `drain` only ever needs to await them all. */
30
8
  static readonly #flushing = new Set<Promise<void>>();
31
9
 
32
- /** Send, and resolve once the write has left this process. */
33
10
  static send(message: BuilderMessage): Promise<void> {
34
11
  const send = process.send;
35
12
  if (!send) return Promise.resolve();
36
13
  const flushed = new Promise<void>((resolve) => {
37
14
  const timer = setTimeout(resolve, BuilderChannel.#flushTimeoutMs);
38
- // A failed send has nothing to retry: the host answers an unanswered request id itself when this
39
- // process exits, and a lost event is superseded by the next generation's.
15
+ // No retry: the host answers an orphaned request id on exit, and a lost event is superseded next generation.
40
16
  send.call(process, message, undefined, undefined, () => {
41
17
  clearTimeout(timer);
42
18
  resolve();
@@ -47,19 +23,12 @@ export class BuilderChannel {
47
23
  return flushed;
48
24
  }
49
25
 
50
- /**
51
- * Send an event no caller awaits. Tracked exactly like `send`, which is the whole point: a relayed
52
- * `css-updated` has nobody to await it, so `drain` is the only thing standing between it and the exit.
53
- */
26
+ // Tracked like `send`: for an event nobody awaits, `drain` is all that stands between it and the exit.
54
27
  static emit(message: BuilderMessage): void {
55
28
  void BuilderChannel.send(message);
56
29
  }
57
30
 
58
- /**
59
- * Resolve once every send handed over so far has flushed, and report how many were still in flight.
60
- * Sends started *while* draining are covered too — `#reportMetrics` can fire from a work item's
61
- * `finally`, after the drain has already begun.
62
- */
31
+ // Loops because `#reportMetrics` can send from a work item's `finally` after the drain has begun.
63
32
  static async drain(): Promise<number> {
64
33
  let flushed = 0;
65
34
  while (BuilderChannel.#flushing.size) {
@@ -10,7 +10,6 @@ const routeReq = (id: number, routeId: string): BuilderReq => ({
10
10
  knownEntries: [],
11
11
  });
12
12
 
13
- /** `routeId` is what tells one generation's answer from the other's; the rest is an empty delta. */
14
13
  const routeRes = (id: number, routeId: string): Extract<BuilderRes, { ok: true }> => ({
15
14
  type: "build-route-res",
16
15
  id,
@@ -23,7 +22,6 @@ describe("BuilderRequestRouter", () => {
23
22
  const router = new BuilderRequestRouter();
24
23
  router.startGeneration();
25
24
 
26
- // Everything but the id travels untouched; the builder never learns it was renumbered.
27
25
  const first = router.issue(routeReq(1, "/home"));
28
26
  const second = router.issue(routeReq(2, "/about"));
29
27
  expect(first.routeId).toBe("/home");
@@ -35,11 +33,6 @@ describe("BuilderRequestRouter", () => {
35
33
  expect(router.inFlightCount).toBe(0);
36
34
  });
37
35
 
38
- /**
39
- * The defect this class exists for. `BuilderRpc` numbers from 1 in every backend process, so without
40
- * renumbering the second generation's `id: 1` request is settled by the first generation's answer —
41
- * a page rendered against another route's client manifest — and its own answer is then dropped.
42
- */
43
36
  test("does not deliver a dead generation's answer to the backend that replaced it", () => {
44
37
  const router = new BuilderRequestRouter();
45
38
  router.startGeneration();
@@ -70,7 +63,6 @@ describe("BuilderRequestRouter", () => {
70
63
  router.startGeneration();
71
64
  const outgoing = router.issue(routeReq(4, "/home"));
72
65
 
73
- // The caller answers the backend itself in this case, so a late builder answer must not answer it again.
74
66
  router.withdraw(outgoing.id);
75
67
  expect(router.inFlightCount).toBe(0);
76
68
  expect(router.settle(routeRes(outgoing.id, "/home"))).toBeNull();
@@ -3,19 +3,8 @@ import type { BuilderCsrReq, BuilderCsrRes, BuilderReq, BuilderRes } from "akanj
3
3
  type BuilderRequest = BuilderReq | BuilderCsrReq;
4
4
  type BuilderResponse = BuilderRes | BuilderCsrRes;
5
5
 
6
- /**
7
- * Correlation ids for builder requests, renumbered by the dev host so they survive a backend restart.
8
- *
9
- * `BuilderRpc` numbers its requests from 1 in each backend *process*, and the host relays them to a
10
- * builder that outlives the backend. So after a restart the two generations collide on id 1: the builder
11
- * answers the request the *previous* backend made, the host relays it, and the new backend settles its own
12
- * request of that number with another route's manifest delta — a page rendered against client modules that
13
- * were never built for it. The answer it was actually waiting for then arrives to an empty pending map and
14
- * is dropped, so the correct build is discarded too.
15
- *
16
- * A host-owned id fixes it without touching the protocol: neither the backend nor the builder learns that
17
- * anything was renumbered, and a response whose generation is gone is discarded instead of misdelivered.
18
- */
6
+ // `BuilderRpc` numbers from 1 per backend process while the builder outlives backends, so ids collide across a
7
+ // restart; host-owned ids keep a dead generation's answer from settling the new backend's request.
19
8
  export class BuilderRequestRouter {
20
9
  #generation = 0;
21
10
  #nextId = 1;
@@ -29,33 +18,25 @@ export class BuilderRequestRouter {
29
18
  return this.#inFlight.size;
30
19
  }
31
20
 
32
- /**
33
- * Begin a new backend generation, abandoning every request the previous one owned.
34
- *
35
- * Nothing is answered on the way out: the process that asked is gone, and its `BuilderRpc` went with it.
36
- */
21
+ // Abandons the previous generation's requests unanswered: the backend that asked is gone.
37
22
  startGeneration(): number {
38
23
  this.#generation += 1;
39
24
  this.#inFlight.clear();
40
25
  return this.#generation;
41
26
  }
42
27
 
43
- /** The request to forward to the builder, carrying this host's id in place of the backend's. */
44
28
  issue<T extends BuilderRequest>(message: T): T {
45
29
  const id = this.#nextId++;
46
30
  this.#inFlight.set(id, { backendId: message.id, generation: this.#generation });
47
31
  return { ...message, id };
48
32
  }
49
33
 
50
- /** Give an id back when the send failed, so the caller answers the backend itself. */
34
+ // For a failed send, after which the caller answers the backend itself.
51
35
  withdraw(id: number): void {
52
36
  this.#inFlight.delete(id);
53
37
  }
54
38
 
55
- /**
56
- * The response to forward to the backend with its own id restored, or `null` when no live backend is
57
- * waiting for it — either it was never issued or the generation that issued it has been replaced.
58
- */
39
+ // null when no live backend is waiting: never issued, or its generation was replaced.
59
40
  settle<T extends BuilderResponse>(message: T): T | null {
60
41
  const request = this.#inFlight.get(message.id);
61
42
  if (!request) return null;
@@ -0,0 +1,94 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { type BatchJob, BuilderWorkQueue } from "./builderWorkQueue";
3
+
4
+ const gate = () => {
5
+ const { promise: opened, resolve: open } = Promise.withResolvers<void>();
6
+ return { open, opened };
7
+ };
8
+
9
+ describe("BuilderWorkQueue", () => {
10
+ test("runs jobs one at a time, in the order they were queued", async () => {
11
+ const order: string[] = [];
12
+ const queue = new BuilderWorkQueue({ runBatch: async () => undefined });
13
+ const first = gate();
14
+ const a = queue.enqueue("a", async () => {
15
+ await first.opened;
16
+ order.push("a");
17
+ });
18
+ const b = queue.enqueue("b", async () => {
19
+ order.push("b");
20
+ return 2;
21
+ });
22
+ expect(queue.size).toBe(2);
23
+ first.open();
24
+ await a;
25
+ expect(await b).toBe(2);
26
+ expect(order).toEqual(["a", "b"]);
27
+ expect(queue.size).toBe(0);
28
+ });
29
+
30
+ test("folds a batch queued behind another batch into it, for the newest generation", async () => {
31
+ const ran: BatchJob[] = [];
32
+ const running = gate();
33
+ const queue = new BuilderWorkQueue({
34
+ runBatch: async (batch) => {
35
+ ran.push(batch);
36
+ if (ran.length === 1) await running.opened;
37
+ },
38
+ });
39
+ const first = queue.enqueueBatch({ generation: 1, needs: ["pages", "css"], changedFiles: ["a.tsx"] });
40
+ const second = queue.enqueueBatch({
41
+ generation: 2,
42
+ needs: ["pages", "css"],
43
+ changedFiles: ["b.tsx"],
44
+ discovery: { files: ["b.tsx"], refresh: false, generation: 2 },
45
+ });
46
+ const third = queue.enqueueBatch({ generation: 3, needs: ["csr", "pages"], changedFiles: ["a.tsx", "c.tsx"] });
47
+ expect(second).toBe(third);
48
+ running.open();
49
+ await Promise.all([first, second, third]);
50
+ expect(ran).toHaveLength(2);
51
+ expect(ran[1]).toEqual({
52
+ generation: 3,
53
+ needs: ["pages", "css", "csr"],
54
+ changedFiles: ["b.tsx", "a.tsx", "c.tsx"],
55
+ discovery: { files: ["b.tsx"], refresh: false, generation: 2 },
56
+ });
57
+ });
58
+
59
+ test("keeps a batch queued after a task behind that task", async () => {
60
+ const order: string[] = [];
61
+ const running = gate();
62
+ const queue = new BuilderWorkQueue({
63
+ runBatch: async (batch) => {
64
+ order.push(`batch:${batch.generation}`);
65
+ if (batch.generation === 1) await running.opened;
66
+ },
67
+ });
68
+ const first = queue.enqueueBatch({ generation: 1, needs: ["pages"], changedFiles: [] });
69
+ const route = queue.enqueue("route", async () => {
70
+ order.push("route");
71
+ });
72
+ const second = queue.enqueueBatch({ generation: 2, needs: ["pages"], changedFiles: [] });
73
+ running.open();
74
+ await Promise.all([first, route, second]);
75
+ expect(order).toEqual(["batch:1", "route", "batch:2"]);
76
+ });
77
+
78
+ test("drains jobs queued while it waits, and survives a job that throws", async () => {
79
+ const queue = new BuilderWorkQueue({ runBatch: async () => undefined });
80
+ const failing = queue.enqueue("fails", async () => {
81
+ throw new Error("boom");
82
+ });
83
+ let late = false;
84
+ void queue.enqueue("queues-more", async () => {
85
+ void queue.enqueue("late", async () => {
86
+ late = true;
87
+ });
88
+ });
89
+ await expect(failing).rejects.toThrow("boom");
90
+ await queue.drain();
91
+ expect(late).toBe(true);
92
+ expect(queue.size).toBe(0);
93
+ });
94
+ });
@@ -0,0 +1,126 @@
1
+ import type { HmrTrace } from "akanjs/server";
2
+ import type { BuildBatchNeed } from "./buildBatchProtocol";
3
+
4
+ export interface DiscoveryJob {
5
+ files: string[];
6
+ refresh: boolean;
7
+ /** The newest batch these files came from: a route build that starts after has taken it in. */
8
+ generation: number;
9
+ }
10
+
11
+ export interface BatchJob {
12
+ generation: number;
13
+ needs: BuildBatchNeed[];
14
+ changedFiles: string[];
15
+ trace?: HmrTrace;
16
+ /** Code files whose client-entry discovery goes stale before this batch builds; `refresh` rebuilds it whole. */
17
+ discovery?: DiscoveryJob;
18
+ }
19
+
20
+ interface QueuedJob {
21
+ label: string;
22
+ batch: BatchJob | null;
23
+ run: () => Promise<void>;
24
+ settle: PromiseWithResolvers<void>;
25
+ }
26
+
27
+ export interface BuilderWorkQueueOptions {
28
+ runBatch: (batch: BatchJob) => Promise<void>;
29
+ onSettled?: (label: string, ms: number) => void;
30
+ }
31
+
32
+ //* The builder's slow lane: build-worker batches, route builds and full CSR builds run here one at a time, so a save's
33
+ //* CSR patch never waits behind them. A batch queued behind another batch folds into it: a burst of saves builds pages
34
+ //* and css once for the newest generation instead of once per save.
35
+ export class BuilderWorkQueue {
36
+ readonly #runBatch: BuilderWorkQueueOptions["runBatch"];
37
+ readonly #onSettled: BuilderWorkQueueOptions["onSettled"];
38
+ readonly #pending: QueuedJob[] = [];
39
+ #running: QueuedJob | null = null;
40
+ #idle: Promise<void> = Promise.resolve();
41
+ #wakeIdle: (() => void) | null = null;
42
+
43
+ constructor({ runBatch, onSettled }: BuilderWorkQueueOptions) {
44
+ this.#runBatch = runBatch;
45
+ this.#onSettled = onSettled;
46
+ }
47
+
48
+ /** Jobs waiting or running. */
49
+ get size(): number {
50
+ return this.#pending.length + (this.#running ? 1 : 0);
51
+ }
52
+
53
+ enqueueBatch(batch: BatchJob): Promise<void> {
54
+ const tail = this.#pending.at(-1);
55
+ if (tail?.batch) {
56
+ tail.batch = BuilderWorkQueue.merge(tail.batch, batch);
57
+ return tail.settle.promise;
58
+ }
59
+ const job = this.#job("batch", batch, async () => undefined);
60
+ job.run = async () => await this.#runBatch(job.batch ?? batch);
61
+ return this.#push(job);
62
+ }
63
+
64
+ enqueue<T>(label: string, run: () => Promise<T>): Promise<T> {
65
+ let result: T | undefined;
66
+ const job = this.#job(label, null, async () => {
67
+ result = await run();
68
+ });
69
+ return this.#push(job).then(() => result as T);
70
+ }
71
+
72
+ /** Resolves once nothing is waiting or running, including jobs queued while it waits. */
73
+ async drain(): Promise<void> {
74
+ while (this.size > 0) await this.#idle;
75
+ }
76
+
77
+ static merge(into: BatchJob, next: BatchJob): BatchJob {
78
+ const discovery =
79
+ into.discovery || next.discovery
80
+ ? {
81
+ files: [...new Set([...(into.discovery?.files ?? []), ...(next.discovery?.files ?? [])])],
82
+ refresh: !!into.discovery?.refresh || !!next.discovery?.refresh,
83
+ generation: Math.max(into.discovery?.generation ?? 0, next.discovery?.generation ?? 0),
84
+ }
85
+ : undefined;
86
+ return {
87
+ generation: Math.max(into.generation, next.generation),
88
+ needs: [...new Set([...into.needs, ...next.needs])],
89
+ changedFiles: [...new Set([...into.changedFiles, ...next.changedFiles])],
90
+ trace: next.trace ?? into.trace,
91
+ ...(discovery ? { discovery } : {}),
92
+ };
93
+ }
94
+
95
+ #job(label: string, batch: BatchJob | null, run: () => Promise<void>): QueuedJob {
96
+ return { label, batch, run, settle: Promise.withResolvers<void>() };
97
+ }
98
+
99
+ #push(job: QueuedJob): Promise<void> {
100
+ if (this.size === 0)
101
+ this.#idle = new Promise<void>((resolve) => {
102
+ this.#wakeIdle = resolve;
103
+ });
104
+ this.#pending.push(job);
105
+ if (!this.#running) void this.#pump();
106
+ return job.settle.promise;
107
+ }
108
+
109
+ async #pump(): Promise<void> {
110
+ for (let job = this.#pending.shift(); job; job = this.#pending.shift()) {
111
+ this.#running = job;
112
+ const started = Date.now();
113
+ try {
114
+ await job.run();
115
+ job.settle.resolve();
116
+ } catch (error) {
117
+ job.settle.reject(error);
118
+ } finally {
119
+ this.#running = null;
120
+ this.#onSettled?.(job.label, Date.now() - started);
121
+ }
122
+ }
123
+ this.#wakeIdle?.();
124
+ this.#wakeIdle = null;
125
+ }
126
+ }
@@ -1,18 +1,22 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { DevChangePlanner } from "../frontendBuild";
2
+ import path from "node:path";
3
+ import { DevChangePlanner, type GeneratedIndexSyncResult } from "../frontendBuild";
3
4
  import { prepareDevWatchBatch } from "./devWatchBatch";
4
5
 
6
+ const prepare = (root: string, generation: number, file: string, indexSync: GeneratedIndexSyncResult) =>
7
+ prepareDevWatchBatch({
8
+ generation,
9
+ batch: { files: [file], kinds: new Set(["code"]) },
10
+ indexSync,
11
+ changePlanner: new DevChangePlanner({ workspaceRoot: root }),
12
+ });
13
+
5
14
  describe("prepareDevWatchBatch", () => {
6
15
  test("includes generated indexes in the same invalidate generation", () => {
7
- const root = "/repo";
8
- const changedFile = `${root}/libs/shared/common/foo.ts`;
9
- const generatedIndex = `${root}/libs/shared/common/index.ts`;
10
- const prepared = prepareDevWatchBatch({
11
- generation: 12,
12
- batch: { files: [changedFile], kinds: new Set(["code"]) },
13
- indexSync: { changedFiles: [generatedIndex], errors: [] },
14
- changePlanner: new DevChangePlanner({ workspaceRoot: root }),
15
- });
16
+ const root = path.resolve("/repo");
17
+ const changedFile = path.join(root, "libs/shared/common/foo.ts");
18
+ const generatedIndex = path.join(root, "libs/shared/common/index.ts");
19
+ const prepared = prepare(root, 12, changedFile, { changedFiles: [generatedIndex], errors: [] });
16
20
 
17
21
  expect(prepared.hasSyncErrors).toBe(false);
18
22
  expect(prepared.files).toEqual([changedFile, generatedIndex]);
@@ -25,15 +29,10 @@ describe("prepareDevWatchBatch", () => {
25
29
  test.each(["common", "srvkit", "ui", "webkit"])(
26
30
  "keeps %s facet add/delete generated index in the same generation",
27
31
  (facet) => {
28
- const root = "/repo";
29
- const changedFile = `${root}/libs/shared/${facet}/tmpExample.ts`;
30
- const generatedIndex = `${root}/libs/shared/${facet}/index.ts`;
31
- const prepared = prepareDevWatchBatch({
32
- generation: 20,
33
- batch: { files: [changedFile], kinds: new Set(["code"]) },
34
- indexSync: { changedFiles: [generatedIndex], errors: [] },
35
- changePlanner: new DevChangePlanner({ workspaceRoot: root }),
36
- });
32
+ const root = path.resolve("/repo");
33
+ const changedFile = path.join(root, "libs/shared", facet, "tmpExample.ts");
34
+ const generatedIndex = path.join(root, "libs/shared", facet, "index.ts");
35
+ const prepared = prepare(root, 20, changedFile, { changedFiles: [generatedIndex], errors: [] });
37
36
 
38
37
  expect(new Set(prepared.files)).toEqual(new Set([changedFile, generatedIndex]));
39
38
  expect(prepared.event.devPlan?.generatedFiles).toEqual([generatedIndex]);
@@ -44,11 +43,9 @@ describe("prepareDevWatchBatch", () => {
44
43
 
45
44
  test("marks failed generated index sync as an error generation", () => {
46
45
  const root = "/repo";
47
- const prepared = prepareDevWatchBatch({
48
- generation: 13,
49
- batch: { files: [`${root}/libs/shared/common/foo.ts`], kinds: new Set(["code"]) },
50
- indexSync: { changedFiles: [], errors: ["sync failed"] },
51
- changePlanner: new DevChangePlanner({ workspaceRoot: root }),
46
+ const prepared = prepare(root, 13, `${root}/libs/shared/common/foo.ts`, {
47
+ changedFiles: [],
48
+ errors: ["sync failed"],
52
49
  });
53
50
 
54
51
  expect(prepared.hasSyncErrors).toBe(true);