@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,33 +1,17 @@
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";
3
+ import { tempDirs, writeText as write } from "../testHelpers";
5
4
  import { createTsconfigPackageResolver } from "../transforms/barrelImportsPlugin";
6
5
  import { CLIENT_BUNDLE_NAMING } from "./clientBuildTypes";
7
6
  import { ClientEntriesBundler } from "./clientEntriesBundler";
8
7
  import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
9
8
  import { RouteClientBuilder } from "./routeClientBuilder";
10
9
 
11
- const tempRoots: string[] = [];
12
-
13
- const makeTempRoot = async () => {
14
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-client-entry-"));
15
- tempRoots.push(root);
16
- return root;
17
- };
18
-
19
- const write = async (filePath: string, content: string) => {
20
- await mkdir(path.dirname(filePath), { recursive: true });
21
- await writeFile(filePath, content);
22
- };
23
-
24
- afterEach(async () => {
25
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
26
- });
10
+ const makeTempRoot = tempDirs("akan-client-entry-");
27
11
 
28
12
  describe("route client store bootstrap", () => {
29
13
  test("wraps client entries with app client bootstrap before re-exporting components", () => {
30
- const original = "/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx";
14
+ const original = path.resolve("/repo/pkgs/akanjs/ui/Model/NewWrapper_Client.tsx");
31
15
  const source = RouteClientBuilder.createStoreBootstrapEntrySource({
32
16
  appName: "akan",
33
17
  originalEntry: original,
@@ -147,6 +131,58 @@ describe("route client store bootstrap", () => {
147
131
  );
148
132
  });
149
133
 
134
+ test("bundles every route's entries into one graph so a page never loads a module twice", async () => {
135
+ const root = await makeTempRoot();
136
+ const appDir = path.join(root, "apps/demo");
137
+ const pageA = path.join(appDir, "page/a.tsx");
138
+ const pageB = path.join(appDir, "page/b.tsx");
139
+ const entryA = path.join(appDir, "ui/A.tsx");
140
+ const entryB = path.join(appDir, "ui/B.tsx");
141
+ const shared = path.join(appDir, "common/shared.ts");
142
+ await write(pageA, 'import { A } from "../ui/A";\nexport default A;\n');
143
+ await write(pageB, 'import { B } from "../ui/B";\nexport default B;\n');
144
+ await write(entryA, '"use client";\nimport { mark } from "../common/shared";\nexport const A = () => mark;\n');
145
+ await write(entryB, '"use client";\nimport { mark } from "../common/shared";\nexport const B = () => mark;\n');
146
+ await write(shared, 'export const mark = "shared-module-marker";\n');
147
+ const app = {
148
+ name: "demo",
149
+ cwdPath: appDir,
150
+ dist: { cwdPath: path.join(root, "dist/apps/demo") },
151
+ workspace: { workspaceRoot: root },
152
+ getConfig: async () => ({ barrelImports: [], optimizeImports: [] }),
153
+ getTsConfig: async () => ({ compilerOptions: { paths: {} } }),
154
+ getPublicEnv: () => ({}),
155
+ } as never;
156
+ const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, async () => null);
157
+ const build = (seeds: string[], knownEntries: Set<string>) =>
158
+ new RouteClientBuilder({
159
+ app,
160
+ seeds,
161
+ graphSeeds: [pageA, pageB],
162
+ knownEntries,
163
+ discovery,
164
+ artifact: {} as never,
165
+ browser: "chunks",
166
+ }).build();
167
+
168
+ const first = await build([pageA], new Set());
169
+ const second = await build([pageB], new Set(first.newEntries));
170
+
171
+ expect(first.newEntries).toEqual([entryA, entryB]);
172
+ expect(first.discoveredEntries).toEqual([entryA]);
173
+ expect(first.clientDeps).not.toContain(entryB);
174
+ expect(Object.keys(first.clientDepsByEntry ?? {}).sort()).toEqual([entryA, entryB]);
175
+ expect(second.newEntries).toEqual([]);
176
+ expect(second.discoveredEntries).toEqual([entryB]);
177
+ expect(Object.keys(second.manifestDelta)).toEqual([]);
178
+ const clientOut = path.join(appDir, ".akan/artifact/client");
179
+ const outputs = [...new Bun.Glob("**/*.js").scanSync(clientOut)];
180
+ const carriers = await Promise.all(
181
+ outputs.map(async (file) => (await Bun.file(path.join(clientOut, file)).text()).includes("shared-module-marker")),
182
+ );
183
+ expect(carriers.filter(Boolean)).toHaveLength(1);
184
+ });
185
+
150
186
  test("discovers client entries from installed akanjs package sources", async () => {
151
187
  const root = await makeTempRoot();
152
188
  const seed = path.join(root, "apps/demo/page/_index.tsx");
@@ -159,24 +195,14 @@ describe("route client store bootstrap", () => {
159
195
  await write(uiEntry, 'export { ClientPathWrapper } from "./System/Client";\n');
160
196
  await write(clientEntry, '"use client";\nexport const ClientPathWrapper = () => null;\n');
161
197
 
198
+ const entryFiles = new Map([
199
+ ["akanjs/ui", uiEntry],
200
+ ["akanjs/ui/System/Client.tsx", clientEntry],
201
+ ]);
162
202
  const discovery = new GraphClientEntryDiscovery({ barrelImports: ["akanjs/ui"] }, async (specifier) => {
163
- if (specifier === "akanjs/ui") {
164
- return {
165
- pkgName: "akanjs/ui",
166
- entryFile: uiEntry,
167
- pkgDir: path.dirname(uiEntry),
168
- preserveFilePath: true,
169
- };
170
- }
171
- if (specifier === "akanjs/ui/System/Client.tsx") {
172
- return {
173
- pkgName: "akanjs/ui/System/Client.tsx",
174
- entryFile: clientEntry,
175
- pkgDir: path.dirname(clientEntry),
176
- preserveFilePath: true,
177
- };
178
- }
179
- return null;
203
+ const entryFile = entryFiles.get(specifier);
204
+ if (!entryFile) return null;
205
+ return { pkgName: specifier, entryFile, pkgDir: path.dirname(entryFile), preserveFilePath: true };
180
206
  });
181
207
 
182
208
  expect(await discovery.discover([seed])).toEqual([clientEntry]);
@@ -209,3 +235,63 @@ describe("route client store bootstrap", () => {
209
235
  });
210
236
  });
211
237
  });
238
+
239
+ describe("fast refresh named default function", () => {
240
+ test("hoists a named default function into a declaration plus a trailing default export", () => {
241
+ const source = `export default async function Page<T>(props: T) {\n return null;\n}\n`;
242
+ const next = RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(source, { path: "/app/page.tsx" });
243
+ expect(next).toBe(`async function Page<T>(props: T) {\n return null;\n}\n\nexport default Page;\n`);
244
+ });
245
+
246
+ test("leaves code quoted in a template literal or a comment alone", () => {
247
+ const source = [
248
+ `import { page } from "akanjs/client";`,
249
+ `const snippet = \``,
250
+ `export default function Page() {`,
251
+ ` return <div />;`,
252
+ `}\`;`,
253
+ `/*`,
254
+ `export default function Commented() {}`,
255
+ `*/`,
256
+ `export default page().render(() => <pre>{snippet}</pre>);`,
257
+ "",
258
+ ].join("\n");
259
+ expect(
260
+ RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(source, { path: "/app/page.tsx" }),
261
+ ).toBeNull();
262
+ });
263
+
264
+ test("rewrites only the real declaration when the same name is also quoted", () => {
265
+ const source = [
266
+ "const snippet = `",
267
+ "export default function Page() {}",
268
+ "`;",
269
+ "export default function Page() {",
270
+ " return snippet;",
271
+ "}",
272
+ "",
273
+ ].join("\n");
274
+ const next = RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh(source, { path: "/app/page.tsx" });
275
+ expect(next).toBe(
276
+ [
277
+ "const snippet = `",
278
+ "export default function Page() {}",
279
+ "`;",
280
+ "function Page() {",
281
+ " return snippet;",
282
+ "}",
283
+ "",
284
+ "export default Page;",
285
+ "",
286
+ ].join("\n"),
287
+ );
288
+ });
289
+
290
+ test("leaves a file it cannot parse to the bundler", () => {
291
+ expect(
292
+ RouteClientBuilder.normalizeNamedDefaultFunctionForFastRefresh("export default function Page( {", {
293
+ path: "/app/page.tsx",
294
+ }),
295
+ ).toBeNull();
296
+ });
297
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { App } from "../commandDecorators";
3
+ import { bundleDefine } from "./bundleDefine";
4
+
5
+ describe("bundleDefine", () => {
6
+ test("spells the public env in one order whatever order the process holds it in", () => {
7
+ const appWith = (env: Record<string, string>) => ({ getPublicEnv: () => env }) as unknown as App;
8
+ const builder = bundleDefine(appWith({ AKAN_PUBLIC_Z: "1", AKAN_PUBLIC_A: "2" }), "start", "ssr");
9
+ const worker = bundleDefine(appWith({ AKAN_PUBLIC_A: "2", AKAN_PUBLIC_Z: "1" }), "start", "ssr");
10
+ expect(JSON.stringify(builder)).toBe(JSON.stringify(worker));
11
+ });
12
+ });
@@ -0,0 +1,16 @@
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
+ //? Sorted: Windows hands a child its environment sorted, while keys the config sets at runtime trail the parent's,
9
+ //? and the dev registries hash this object to tell a worker's build from the builder's.
10
+ ...Object.fromEntries(
11
+ Object.entries(app.getPublicEnv())
12
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
13
+ .map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
14
+ ),
15
+ };
16
+ };
@@ -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
 
@@ -0,0 +1,112 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { GraphClientEntryDiscovery } from "./clientEntryDiscovery";
6
+
7
+ const dirs: string[] = [];
8
+
9
+ afterAll(async () => {
10
+ await Promise.all(dirs.map((dir) => rm(dir, { recursive: true, force: true })));
11
+ });
12
+
13
+ describe("GraphClientEntryDiscovery", () => {
14
+ test("a walk an invalidation overtook answers its caller but leaves nothing stale for the next one", async () => {
15
+ for (const yields of [0, 5, 10, 20, 40, 80]) {
16
+ const dir = await mkdtemp(path.join(os.tmpdir(), "akan-discovery-"));
17
+ dirs.push(dir);
18
+ const file = (name: string) => path.join(dir, name);
19
+ await Bun.write(file("S.tsx"), 'import "./A";\n');
20
+ await Bun.write(file("A.tsx"), 'import "./B";\nimport "./D";\n');
21
+ await Bun.write(file("B.tsx"), "export const b = 1;\n");
22
+ await Bun.write(file("C.tsx"), '"use client";\nexport const C = () => null;\n');
23
+ await Bun.write(file("D.tsx"), "export const d = 1;\n");
24
+ const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, (async () => null) as never);
25
+ expect(await discovery.discover([file("S.tsx")])).toEqual([]);
26
+
27
+ await Bun.write(file("B.tsx"), 'import "./C";\nexport const b = 1;\n');
28
+ await Bun.write(file("D.tsx"), "export const d = 2;\n");
29
+ discovery.invalidate([file("D.tsx")]);
30
+ const walk = discovery.discover([file("S.tsx")]);
31
+ for (let tick = 0; tick < yields; tick++) await Promise.resolve();
32
+ discovery.invalidate([file("B.tsx")]);
33
+ await walk;
34
+ expect(await discovery.discover([file("S.tsx")])).toEqual([file("C.tsx")]);
35
+ }
36
+ });
37
+
38
+ const tempDir = async () => {
39
+ const dir = await mkdtemp(path.join(os.tmpdir(), "akan-discovery-"));
40
+ dirs.push(dir);
41
+ return (name: string) => path.join(dir, name);
42
+ };
43
+
44
+ test("an entry moved into a folder, or to another extension, is found once the move is invalidated", async () => {
45
+ const file = await tempDir();
46
+ await Bun.write(file("S.tsx"), 'import "./ui/Chart";\nimport "./ui/Card";\n');
47
+ await Bun.write(file("ui/Chart.tsx"), '"use client";\nexport const Chart = () => null;\n');
48
+ await Bun.write(file("ui/Card.tsx"), '"use client";\nexport const Card = () => null;\n');
49
+ const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, (async () => null) as never);
50
+ expect(await discovery.discover([file("S.tsx")])).toEqual([file("ui/Card.tsx"), file("ui/Chart.tsx")]);
51
+
52
+ await rm(file("ui/Chart.tsx"));
53
+ await Bun.write(file("ui/Chart/index.tsx"), '"use client";\nexport const Chart = () => null;\n');
54
+ await rm(file("ui/Card.tsx"));
55
+ await Bun.write(file("ui/Card.ts"), '"use client";\nexport const Card = () => null;\n');
56
+ discovery.invalidate([file("ui/Chart.tsx"), file("ui/Chart/index.tsx"), file("ui/Card.tsx"), file("ui/Card.ts")]);
57
+ expect(await discovery.discover([file("S.tsx")])).toEqual([file("ui/Card.ts"), file("ui/Chart/index.tsx")]);
58
+ });
59
+
60
+ test("a resolution still in flight when its target appears is not what the next walk reads", async () => {
61
+ const file = await tempDir();
62
+ await Bun.write(file("S.tsx"), 'import "./B";\n');
63
+ await Bun.write(file("B.tsx"), 'import "@demo/C";\nexport const b = 1;\n');
64
+ const gate = Promise.withResolvers<void>();
65
+ const seen = Promise.withResolvers<void>();
66
+ let calls = 0;
67
+ const resolvePackage = async () => {
68
+ calls += 1;
69
+ const exists = await Bun.file(file("C.tsx")).exists();
70
+ if (calls === 1) {
71
+ seen.resolve();
72
+ await gate.promise;
73
+ }
74
+ return exists ? { entryFile: file("C.tsx") } : null;
75
+ };
76
+ const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, resolvePackage as never);
77
+ const walk = discovery.discover([file("S.tsx")]);
78
+ await seen.promise;
79
+ await Bun.write(file("C.tsx"), '"use client";\nexport const C = () => null;\n');
80
+ discovery.invalidate([file("C.tsx")]);
81
+ gate.resolve();
82
+ await walk;
83
+ expect(await discovery.discover([file("S.tsx")])).toEqual([file("C.tsx")]);
84
+ });
85
+
86
+ test("a dense import cycle is walked once per file, not once per path", async () => {
87
+ const file = await tempDir();
88
+ const names = Array.from({ length: 12 }, (_, idx) => `M${idx}.tsx`);
89
+ for (const name of names)
90
+ await Bun.write(file(name), names.map((other) => `import "./${other.replace(".tsx", "")}";\n`).join(""));
91
+ await Bun.write(file("C.tsx"), '"use client";\nexport const C = () => null;\n');
92
+ await Bun.write(
93
+ file("M11.tsx"),
94
+ `${names.map((other) => `import "./${other.replace(".tsx", "")}";\n`).join("")}import "./C";\n`,
95
+ );
96
+ const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, (async () => null) as never);
97
+ const started = performance.now();
98
+ expect(await discovery.discover([file("M0.tsx")])).toEqual([file("C.tsx")]);
99
+ expect(performance.now() - started).toBeLessThan(2_000);
100
+ expect(await discovery.discover([file("M5.tsx")])).toEqual([file("C.tsx")]);
101
+ });
102
+
103
+ test("a file walked inside an import cycle keeps no partial result for a walk that starts at it", async () => {
104
+ const file = await tempDir();
105
+ await Bun.write(file("A.tsx"), 'import "./B";\nimport "./C";\n');
106
+ await Bun.write(file("B.tsx"), 'import "./A";\n');
107
+ await Bun.write(file("C.tsx"), '"use client";\nexport const C = () => null;\n');
108
+ const discovery = new GraphClientEntryDiscovery({ barrelImports: [] }, (async () => null) as never);
109
+ expect(await discovery.discover([file("A.tsx")])).toEqual([file("C.tsx")]);
110
+ expect(await discovery.discover([file("B.tsx")])).toEqual([file("C.tsx")]);
111
+ });
112
+ });