@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,27 +1,16 @@
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
- import { BarrelAnalyzer } from "./barrelAnalyzer";
3
+ import { ApplicationBuildReporter } from "../applicationBuildReporter";
4
+ import { tempDirs, writeText as write } from "../testHelpers";
5
+ import { BarrelAnalyzer, type BarrelExportTarget } from "./barrelAnalyzer";
6
6
  import { rewriteBarrelImports } from "./barrelImportsPlugin";
7
- import { toClientReferencePath, transformUseClient } from "./rscUseClientTransform";
7
+ import { scanUseClientExports, toClientReferencePath, transformUseClient } from "./rscUseClientTransform";
8
+ import { createUseClientBundlePlugin } from "./useClientBundlePlugin";
8
9
 
9
- const tempRoots: string[] = [];
10
+ const makeTempRoot = tempDirs("akan-devkit-transform-");
10
11
 
11
- const makeTempRoot = async () => {
12
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-transform-"));
13
- tempRoots.push(root);
14
- return root;
15
- };
16
-
17
- const write = async (filePath: string, content: string) => {
18
- await mkdir(path.dirname(filePath), { recursive: true });
19
- await writeFile(filePath, content);
20
- };
21
-
22
- afterEach(async () => {
23
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
24
- });
12
+ const fakeAnalyzer = (entries: [string, BarrelExportTarget][]) =>
13
+ ({ analyze: async () => new Map(entries) }) as unknown as BarrelAnalyzer;
25
14
 
26
15
  describe("transformUseClient", () => {
27
16
  test("returns null for non-client modules and generated root layouts", () => {
@@ -38,6 +27,19 @@ describe("transformUseClient", () => {
38
27
  ).toBeNull();
39
28
  });
40
29
 
30
+ test("a client module with no exports stays server code and is not reported as a client module", () => {
31
+ const reported: string[] = [];
32
+ const onClientModule = (file: string) => reported.push(file);
33
+ expect(
34
+ transformUseClient('"use client";\nimport "./side-effect";\n', { path: "/repo/ui/Fx.tsx", onClientModule }),
35
+ ).toBeNull();
36
+ expect(reported).toEqual([]);
37
+ expect(
38
+ transformUseClient('"use client"; export const value = 1;', { path: "/repo/ui/V.tsx", onClientModule }),
39
+ ).not.toBeNull();
40
+ expect(reported).toEqual(["/repo/ui/V.tsx"]);
41
+ });
42
+
41
43
  test("stubs named and default exports as RSC client references", () => {
42
44
  const source = [
43
45
  "// comment before directive",
@@ -62,6 +64,65 @@ describe("transformUseClient", () => {
62
64
  });
63
65
  });
64
66
 
67
+ describe('export * in a "use client" module', () => {
68
+ test("is refused with the file, the specifier and the fix, since the server would see none of its names", () => {
69
+ const source = [
70
+ '"use client";',
71
+ 'export * from "../../ui/UserLeave";',
72
+ "export const General = () => null;",
73
+ "",
74
+ ].join("\n");
75
+ expect(() =>
76
+ transformUseClient(source, { path: "/repo/libs/shared/lib/user/User.Template.tsx", workspaceRoot: "/repo" }),
77
+ ).toThrow(
78
+ 'libs/shared/lib/user/User.Template.tsx is a "use client" module, so it cannot `export * from "../../ui/UserLeave"`',
79
+ );
80
+ expect(() => scanUseClientExports('"use client";\nexport * from "./a";\n', "/repo/ui/Barrel.ts")).toThrow(
81
+ "Re-export them by name",
82
+ );
83
+ });
84
+
85
+ test("leaves every other export shape alone, and a module that is not a client boundary", () => {
86
+ const file = "/repo/ui/Barrel.tsx";
87
+ const shapes = [
88
+ '"use client";',
89
+ 'export type * from "./types";',
90
+ 'export * as ns from "./namespace";',
91
+ 'export { LeaveInfo, Voc } from "./leave";',
92
+ "// export * from './commented';",
93
+ "export const text = \"export * from './a string'\";",
94
+ "",
95
+ ].join("\n");
96
+ expect(scanUseClientExports(shapes, file).sort()).toEqual(["LeaveInfo", "Voc", "ns", "text"]);
97
+ expect(scanUseClientExports('export * from "./a";\nexport const X = 1;\n', file)).toEqual(["X"]);
98
+ const stub = transformUseClient(shapes, { path: file, workspaceRoot: "/repo" });
99
+ expect(stub).toContain("export const LeaveInfo = registerClientReference");
100
+ expect(stub).toContain("export const Voc = registerClientReference");
101
+ });
102
+
103
+ test("fails the server bundle, and the build report carries the reason out of the bundler's error", async () => {
104
+ const root = await makeTempRoot();
105
+ await write(path.join(root, "Leave.tsx"), '"use client";\nexport const LeaveInfo = () => null;\n');
106
+ await write(
107
+ path.join(root, "Template.tsx"),
108
+ '"use client";\nexport * from "./Leave";\nexport const General = () => null;\n',
109
+ );
110
+ await write(path.join(root, "page.tsx"), 'import * as Template from "./Template";\nexport const T = Template;\n');
111
+ const failure = await Bun.build({
112
+ entrypoints: [path.join(root, "page.tsx")],
113
+ target: "bun",
114
+ external: ["react-server-dom-webpack/server.node"],
115
+ plugins: [createUseClientBundlePlugin({ workspaceRoot: root })],
116
+ }).then(
117
+ () => null,
118
+ (error: unknown) => error,
119
+ );
120
+ expect(ApplicationBuildReporter.formatError(failure)).toContain(
121
+ 'Template.tsx is a "use client" module, so it cannot `export * from "./Leave"`',
122
+ );
123
+ });
124
+ });
125
+
65
126
  describe("BarrelAnalyzer and rewriteBarrelImports", () => {
66
127
  test("analyzes local, named, aliased, and star re-exports while skipping type and namespace exports", async () => {
67
128
  const root = await makeTempRoot();
@@ -97,14 +158,83 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
97
158
  expect(map?.has("ns")).toBe(false);
98
159
  });
99
160
 
161
+ test("a name a module exports itself wins over the same name its star re-exports bring, as ES decides", async () => {
162
+ const root = await makeTempRoot();
163
+ const pkgDir = path.join(root, "pkg");
164
+ await write(
165
+ path.join(pkgDir, "index.ts"),
166
+ [
167
+ 'export * from "./a";',
168
+ 'export { X } from "./c";',
169
+ 'export * from "./nested";',
170
+ "export const Local = 1;",
171
+ "",
172
+ ].join("\n"),
173
+ );
174
+ await write(path.join(pkgDir, "a.ts"), "export const X = 2;\nexport const Local = 2;\nexport const OnlyA = 2;\n");
175
+ await write(path.join(pkgDir, "c.ts"), "export const X = 1;\n");
176
+ await write(path.join(pkgDir, "nested", "index.ts"), 'export * from "./deep";\nexport { Y } from "./y";\n');
177
+ await write(path.join(pkgDir, "nested", "deep.ts"), "export const Y = 2;\n");
178
+ await write(path.join(pkgDir, "nested", "y.ts"), "export const Y = 1;\n");
179
+ const analyzer = new BarrelAnalyzer({
180
+ resolvePackage: async () => ({ pkgName: "@scope/pkg", entryFile: path.join(pkgDir, "index.ts"), pkgDir }),
181
+ });
182
+
183
+ const runtime = (await import(path.join(pkgDir, "index.ts"))) as Record<string, number>;
184
+ expect([runtime.X, runtime.Local, runtime.Y, runtime.OnlyA]).toEqual([1, 1, 1, 2]);
185
+ const map = await analyzer.analyze("@scope/pkg");
186
+ expect(map?.get("X")).toEqual({ subpath: "@scope/pkg/c", originalName: "X" });
187
+ expect(map?.get("Local")).toEqual({ subpath: "@scope/pkg", originalName: "Local" });
188
+ expect(map?.get("Y")).toEqual({ subpath: "@scope/pkg/nested/y", originalName: "Y" });
189
+ expect(map?.get("OnlyA")).toEqual({ subpath: "@scope/pkg/a", originalName: "OnlyA" });
190
+ });
191
+
192
+ test("two analyses that interleave each keep their own place in their barrel", async () => {
193
+ const root = await makeTempRoot();
194
+ const barrels = { first: ["a1", "a2", "a3"], second: ["b1", "b2"] } as const;
195
+ for (const [pkgName, names] of Object.entries(barrels)) {
196
+ const lines = names.map((name) => `export { ${name} } from "./${name}";`);
197
+ // Parks the second walk past the end of the first barrel, so a shared regex position would end the first early.
198
+ const padding = pkgName === "second" ? [`// ${"-".repeat(200)}`] : [];
199
+ await write(path.join(root, pkgName, "index.ts"), [...padding, ...lines, ""].join("\n"));
200
+ for (const name of names) await write(path.join(root, pkgName, `${name}.ts`), `export const ${name} = 1;\n`);
201
+ }
202
+ const parked = { first: Promise.withResolvers<void>(), second: Promise.withResolvers<void>() };
203
+ const released = { first: Promise.withResolvers<void>(), second: Promise.withResolvers<void>() };
204
+ const analyzer = new BarrelAnalyzer({
205
+ resolvePackage: async (pkgName) => ({
206
+ pkgName,
207
+ entryFile: path.join(root, pkgName, "index.ts"),
208
+ pkgDir: path.join(root, pkgName),
209
+ }),
210
+ resolveRelative: async (fromFile, relSpec) => {
211
+ const pkgName = path.basename(path.dirname(fromFile)) as keyof typeof barrels;
212
+ if (relSpec === `./${barrels[pkgName][0]}`) {
213
+ parked[pkgName].resolve();
214
+ await released[pkgName].promise;
215
+ }
216
+ return path.join(path.dirname(fromFile), `${relSpec.slice(2)}.ts`);
217
+ },
218
+ });
219
+
220
+ const first = analyzer.analyze("first");
221
+ await parked.first.promise;
222
+ const second = analyzer.analyze("second");
223
+ await parked.second.promise;
224
+ released.first.resolve();
225
+ const firstMap = await first;
226
+ released.second.resolve();
227
+ const secondMap = await second;
228
+
229
+ expect(firstMap?.get("a3")).toEqual({ subpath: "first/a3", originalName: "a3" });
230
+ expect(secondMap?.get("b2")).toEqual({ subpath: "second/b2", originalName: "b2" });
231
+ });
232
+
100
233
  test("rewrites flattenable named imports and preserves default, type, and unknown imports", async () => {
101
- const analyzer = {
102
- analyze: async () =>
103
- new Map([
104
- ["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
105
- ["Bee", { subpath: "@scope/pkg/leaf", originalName: "B" }],
106
- ]),
107
- } as unknown as BarrelAnalyzer;
234
+ const analyzer = fakeAnalyzer([
235
+ ["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
236
+ ["Bee", { subpath: "@scope/pkg/leaf", originalName: "B" }],
237
+ ]);
108
238
 
109
239
  const rewritten = await rewriteBarrelImports(
110
240
  'import DefaultExport, { A, Bee as LocalBee, type Shape, Missing } from "@scope/pkg";\nconsole.log(A);',
@@ -126,9 +256,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
126
256
  });
127
257
 
128
258
  test("preserves generated client barrel side effects when flattening app client imports", async () => {
129
- const analyzer = {
130
- analyze: async () => new Map([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]),
131
- } as unknown as BarrelAnalyzer;
259
+ const analyzer = fakeAnalyzer([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]);
132
260
 
133
261
  const rewritten = await rewriteBarrelImports(
134
262
  'import { st } from "@apps/demo/client";\nvoid st;\n',
@@ -142,9 +270,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
142
270
  });
143
271
 
144
272
  test("does not rewrite import-looking code inside template literals", async () => {
145
- const analyzer = {
146
- analyze: async () => new Map([["AkanApp", { subpath: "akanjs/server/akanApp", originalName: "AkanApp" }]]),
147
- } as unknown as BarrelAnalyzer;
273
+ const analyzer = fakeAnalyzer([["AkanApp", { subpath: "akanjs/server/akanApp", originalName: "AkanApp" }]]);
148
274
 
149
275
  const source = [
150
276
  'import { Code } from "@apps/docs/ui";',
@@ -164,13 +290,10 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
164
290
  });
165
291
 
166
292
  test("rewrites akanjs/server value imports to leaf subpaths", async () => {
167
- const analyzer = {
168
- analyze: async () =>
169
- new Map([
170
- ["AkanOption", { subpath: "akanjs/server/akanOption", originalName: "AkanOption" }],
171
- ["Try", { subpath: "akanjs/server/decorators", originalName: "Try" }],
172
- ]),
173
- } as unknown as BarrelAnalyzer;
293
+ const analyzer = fakeAnalyzer([
294
+ ["AkanOption", { subpath: "akanjs/server/akanOption", originalName: "AkanOption" }],
295
+ ["Try", { subpath: "akanjs/server/decorators", originalName: "Try" }],
296
+ ]);
174
297
 
175
298
  const rewritten = await rewriteBarrelImports(
176
299
  'import { AkanOption, Try } from "akanjs/server";\nexport const option = new AkanOption();\n',
@@ -188,10 +311,9 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
188
311
  });
189
312
 
190
313
  test("rewrites single-package Akan facet barrels to leaf subpaths", async () => {
191
- const analyzer = {
192
- analyze: async () =>
193
- new Map([["BottomInset", { subpath: "akanjs/ui/Layout/BottomInset", originalName: "BottomInset" }]]),
194
- } as unknown as BarrelAnalyzer;
314
+ const analyzer = fakeAnalyzer([
315
+ ["BottomInset", { subpath: "akanjs/ui/Layout/BottomInset", originalName: "BottomInset" }],
316
+ ]);
195
317
 
196
318
  const rewritten = await rewriteBarrelImports('import { BottomInset } from "akanjs/ui";\n', ["akanjs/ui"], analyzer);
197
319
 
@@ -1,16 +1,10 @@
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";
2
+ import { mkdir, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
4
+ import { tempDirs } from "../testHelpers";
5
5
  import { createTsconfigPackageResolver } from "./barrelImportsPlugin";
6
6
 
7
- const tempRoots: string[] = [];
8
-
9
- const makeTempRoot = async () => {
10
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-resolver-"));
11
- tempRoots.push(root);
12
- return root;
13
- };
7
+ const makeTempRoot = tempDirs("akan-devkit-resolver-");
14
8
 
15
9
  const write = async (root: string, relPath: string, content = "export const value = 1;\n") => {
16
10
  const filePath = path.join(root, relPath);
@@ -25,10 +19,6 @@ const resolverFor = async (root: string, paths: Record<string, string[]>) =>
25
19
  getTsConfig: async () => ({ compilerOptions: { paths } }),
26
20
  } as never);
27
21
 
28
- afterEach(async () => {
29
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
30
- });
31
-
32
22
  describe("createTsconfigPackageResolver — exact tsconfig mapping", () => {
33
23
  test("resolves a package barrel and keeps the specifier", async () => {
34
24
  const root = await makeTempRoot();
@@ -43,9 +33,6 @@ describe("createTsconfigPackageResolver — exact tsconfig mapping", () => {
43
33
  });
44
34
 
45
35
  test("collapses a facet specifier to its parent package", async () => {
46
- // `@libs/util/server` maps to the sibling file `libs/util/server.ts`, not to a package directory, so a
47
- // leaf rewritten under the raw specifier would become `@libs/util/server/lib/sig` — a path that does
48
- // not exist. The parent specifier is what the `@libs/*` wildcard can actually resolve.
49
36
  const root = await makeTempRoot();
50
37
  const entryFile = await write(root, "libs/util/server.ts");
51
38
  const resolve = await resolverFor(root, { "@libs/util/server": ["./libs/util/server.ts"] });
@@ -134,8 +121,6 @@ describe("createTsconfigPackageResolver — wildcard tsconfig mapping", () => {
134
121
  });
135
122
 
136
123
  test("stops at the matched prefix instead of falling through to a shorter one", async () => {
137
- // Falling through would resolve `@libs/util/ui` against `./libs/*`, silently loading an unrelated
138
- // directory that happens to sit at the same subpath under a different root.
139
124
  const root = await makeTempRoot();
140
125
  await write(root, "libs/util/ui/index.ts");
141
126
  const resolve = await resolverFor(root, {
@@ -183,9 +168,7 @@ describe("createTsconfigPackageResolver — node_modules fallback", () => {
183
168
  });
184
169
 
185
170
  test("resolves a scoped package subpath", async () => {
186
- // A scoped name is two path segments, so the package boundary cannot be found by splitting on the
187
- // first slash. Kept vendor-neutral on purpose: `packageExports.test.ts` reads every
188
- // `"@akanjs/devkit/<x>"` literal under this package as a subpath the monorepo imports.
171
+ // Vendor-neutral on purpose: `packageExports.test.ts` reads every `"@akanjs/devkit/<x>"` literal here as an import.
189
172
  const root = await makeTempRoot();
190
173
  await writePackageJson(root, "@vendor/kit", { exports: { "./lint": "./lint/index.ts" } });
191
174
  const entryFile = await write(root, "node_modules/@vendor/kit/lint/index.ts");
@@ -195,8 +178,7 @@ describe("createTsconfigPackageResolver — node_modules fallback", () => {
195
178
  });
196
179
 
197
180
  test("falls back to module/main when exports resolution declines the specifier", async () => {
198
- // `main: "index.js"` has no leading `./`, which the exports resolver rejects — the last fallback
199
- // resolves it against the package directory, and it is the one path that does not preserve the file.
181
+ // `main` without a leading `./` is declined by the exports resolver, so only the last fallback resolves it.
200
182
  const root = await makeTempRoot();
201
183
  await writePackageJson(root, "plainpkg", { main: "index.js" });
202
184
  const entryFile = await write(root, "node_modules/plainpkg/index.js");
@@ -1,22 +1,11 @@
1
1
  import type { BunPlugin } from "bun";
2
+ import { loaderFor } from "./moduleSyntax";
2
3
  import { transformUseClient } from "./rscUseClientTransform";
3
4
 
4
- /**
5
- * BunPlugin that stubs `"use client"` modules so the server bundle sees
6
- * `registerClientReference(proxy, absPath, name)` stubs instead of the
7
- * original client code.
8
- *
9
- * The bundle produced by `PagesBundleBuilder` is loaded in the RSC worker
10
- * with `--conditions react-server`; client components are never supposed
11
- * to execute there. The resulting stub references keep the client
12
- * component manifest lookups working at render time while ensuring the
13
- * server bundle never drags the real client source graph in.
14
- *
15
- * Important: the second argument to `registerClientReference` must match the
16
- * key the client manifest uses. When `workspaceRoot` is provided, both sides
17
- * use workspace-relative keys so production artifacts are portable.
18
- */
19
- export function createUseClientBundlePlugin(options: { workspaceRoot?: string } = {}): BunPlugin {
5
+ /** `workspaceRoot` makes reference keys workspace-relative; they must match the client manifest's keys. */
6
+ export function createUseClientBundlePlugin(
7
+ options: { workspaceRoot?: string; onClientModule?: (path: string, exports: string[]) => void } = {},
8
+ ): BunPlugin {
20
9
  return {
21
10
  name: "akan-use-client-bundle",
22
11
  setup(build) {
@@ -31,6 +20,7 @@ export function createUseClientBundlePlugin(options: { workspaceRoot?: string }
31
20
  const stubbed = transformUseClient(source, {
32
21
  path: args.path,
33
22
  workspaceRoot: options.workspaceRoot,
23
+ onClientModule: options.onClientModule,
34
24
  });
35
25
  if (stubbed === null) return undefined;
36
26
  return { contents: stubbed, loader: loaderFor(args.path) };
@@ -38,10 +28,3 @@ export function createUseClientBundlePlugin(options: { workspaceRoot?: string }
38
28
  },
39
29
  };
40
30
  }
41
-
42
- function loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
43
- if (absPath.endsWith(".tsx")) return "tsx";
44
- if (absPath.endsWith(".jsx")) return "jsx";
45
- if (absPath.endsWith(".ts")) return "ts";
46
- return "js";
47
- }
package/typeChecker.ts CHANGED
@@ -38,31 +38,15 @@ export class TypeChecker {
38
38
  }
39
39
  this.config = parsedConfig;
40
40
  }
41
- /**
42
- * Find tsconfig.json by walking up the directory tree
43
- */
44
41
  #findConfigFile(searchPath: string): string | undefined {
45
42
  return ts.findConfigFile(searchPath, (fileName) => ts.sys.fileExists(fileName), "tsconfig.json");
46
43
  }
47
44
 
48
- /**
49
- * Type-check a single TypeScript file
50
- * @param filePath - Path to the TypeScript file to check
51
- * @returns Array of diagnostic messages
52
- */
53
- check(filePath: string): {
54
- diagnostics: ts.Diagnostic[];
55
- errors: ts.Diagnostic[];
56
- warnings: ts.Diagnostic[];
57
- fileDiagnostics: ts.Diagnostic[];
58
- fileErrors: ts.Diagnostic[];
59
- fileWarnings: ts.Diagnostic[];
60
- } {
45
+ check(filePath: string) {
61
46
  const program = ts.createProgram([filePath], this.config.options);
62
47
  const diagnostics = [
63
48
  ...program.getSemanticDiagnostics(),
64
49
  ...program.getSyntacticDiagnostics(),
65
- // Only check declaration diagnostics when declaration emit is enabled
66
50
  ...(this.config.options.declaration ? program.getDeclarationDiagnostics() : []),
67
51
  ];
68
52
  const errors = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
@@ -73,11 +57,6 @@ export class TypeChecker {
73
57
  return { diagnostics, errors, warnings, fileDiagnostics, fileErrors, fileWarnings };
74
58
  }
75
59
 
76
- /**
77
- * Format diagnostics for console output
78
- * @param diagnostics - Array of TypeScript diagnostics
79
- * @returns Formatted string
80
- */
81
60
  formatDiagnostics(diagnostics: ts.Diagnostic[]): string {
82
61
  if (diagnostics.length === 0) return chalk.bold("✅ No type errors found");
83
62
 
@@ -86,48 +65,24 @@ export class TypeChecker {
86
65
  let warningCount = 0;
87
66
  let suggestionCount = 0;
88
67
 
89
- // Group diagnostics by file
90
68
  const diagnosticsByFile = new Map<string, ts.Diagnostic[]>();
91
69
  diagnostics.forEach((diagnostic) => {
92
70
  if (diagnostic.category === ts.DiagnosticCategory.Error) errorCount++;
93
71
  else if (diagnostic.category === ts.DiagnosticCategory.Warning) warningCount++;
94
72
  else if (diagnostic.category === ts.DiagnosticCategory.Suggestion) suggestionCount++;
95
-
96
- if (diagnostic.file) {
97
- const fileName = diagnostic.file.fileName;
98
- if (!diagnosticsByFile.has(fileName)) diagnosticsByFile.set(fileName, []);
99
- const fileDiagnostics = diagnosticsByFile.get(fileName);
100
- if (fileDiagnostics) fileDiagnostics.push(diagnostic);
101
- } else {
102
- if (!diagnosticsByFile.has("")) diagnosticsByFile.set("", []);
103
- const fileDiagnostics = diagnosticsByFile.get("");
104
- if (fileDiagnostics) fileDiagnostics.push(diagnostic);
105
- }
73
+ diagnosticsByFile.getOrInsert(diagnostic.file?.fileName ?? "", []).push(diagnostic);
106
74
  });
107
75
 
108
- // Format diagnostics by file
109
76
  diagnosticsByFile.forEach((fileDiagnostics, fileName) => {
110
77
  if (fileName) output.push(`\n${chalk.cyan(fileName)}`);
111
78
 
112
79
  fileDiagnostics.forEach((diagnostic) => {
113
- const categoryText =
114
- diagnostic.category === ts.DiagnosticCategory.Error
115
- ? "error"
116
- : diagnostic.category === ts.DiagnosticCategory.Warning
117
- ? "warning"
118
- : "suggestion";
119
- const categoryColor =
120
- diagnostic.category === ts.DiagnosticCategory.Error
121
- ? chalk.red
122
- : diagnostic.category === ts.DiagnosticCategory.Warning
123
- ? chalk.yellow
124
- : chalk.blue;
125
- const icon =
80
+ const [categoryText, categoryColor, icon] =
126
81
  diagnostic.category === ts.DiagnosticCategory.Error
127
- ? "❌"
82
+ ? (["error", chalk.red, "❌"] as const)
128
83
  : diagnostic.category === ts.DiagnosticCategory.Warning
129
- ? "⚠️"
130
- : "💡";
84
+ ? (["warning", chalk.yellow, "⚠️"] as const)
85
+ : (["suggestion", chalk.blue, "💡"] as const);
131
86
  const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
132
87
  const tsCode = chalk.dim(`(TS${diagnostic.code})`);
133
88
 
@@ -137,7 +92,6 @@ export class TypeChecker {
137
92
  output.push(`\n ${icon} ${categoryColor(categoryText)}: ${message} ${tsCode}`);
138
93
  output.push(` ${chalk.gray("at")} ${fileName}:${chalk.bold(`${line + 1}:${character + 1}`)}`);
139
94
 
140
- // Show source line with underline
141
95
  const sourceLines = diagnostic.file.text.split("\n");
142
96
  if (line < sourceLines.length) {
143
97
  const sourceLine = sourceLines[line];
@@ -145,7 +99,6 @@ export class TypeChecker {
145
99
 
146
100
  output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
147
101
 
148
- // Create underline with squiggly for TypeScript
149
102
  const underlinePrefix = " ".repeat(character);
150
103
  const length = diagnostic.length ?? 1;
151
104
  const underline = "~".repeat(Math.max(1, length));
@@ -166,11 +119,6 @@ export class TypeChecker {
166
119
  return `\n${summary.join(", ")} found${output.join("\n")}`;
167
120
  }
168
121
 
169
- /**
170
- * Get detailed diagnostic information with code snippet
171
- * @param filePath - Path to the TypeScript file to check
172
- * @returns Object containing diagnostics and detailed information
173
- */
174
122
  getDetailedDiagnostics(filePath: string): {
175
123
  diagnostics: ts.Diagnostic[];
176
124
  details: { line: number; column: number; message: string; code: number; codeSnippet?: string }[];
@@ -199,11 +147,7 @@ export class TypeChecker {
199
147
  return { diagnostics, details };
200
148
  }
201
149
 
202
- /**
203
- * Check if a file has type errors
204
- * @param filePath - Path to the TypeScript file to check
205
- * @returns true if there are no type errors, false otherwise
206
- */
150
+ /** Also false when the check itself throws. */
207
151
  hasNoTypeErrors(filePath: string): boolean {
208
152
  try {
209
153
  const { diagnostics } = this.check(filePath);
package/types.ts CHANGED
@@ -10,6 +10,8 @@ export interface PackageJson {
10
10
  peerDependencies?: Record<string, string>;
11
11
  peerDependenciesMeta?: Record<string, { optional?: boolean }>;
12
12
  optionalDependencies?: Record<string, string>;
13
+ trustedDependencies?: string[];
14
+ overrides?: Record<string, string>;
13
15
  engines?: Record<string, string>;
14
16
  exports?: Record<string, string | Record<string, string>>;
15
17
  bun?: {
package/uploadRelease.ts CHANGED
@@ -2,10 +2,8 @@ import { Logger, RestClient } from "akanjs/common";
2
2
 
3
3
  import { Spinner } from "./spinner";
4
4
 
5
- const spinning = (message: string) => {
6
- const spinner = new Spinner(message, { prefix: message, enableSpin: true }).start();
7
- return spinner;
8
- };
5
+ const spinning = (message: string) => new Spinner(message, { prefix: message, enableSpin: true }).start();
6
+
9
7
  export const uploadRelease = async (
10
8
  appName: string,
11
9
  {
@@ -27,33 +25,20 @@ export const uploadRelease = async (
27
25
  const logger = new Logger("uploadRelease");
28
26
  const basePath = local ? "http://localhost:8282/backend" : "https://cloud.akanjs.com/backend";
29
27
  const httpClient = new RestClient(basePath);
30
- const buildPath = `${workspaceRoot}/releases/builds/${appName}-release.tar.gz`;
31
- const appBuildPath = `${workspaceRoot}/releases/builds/${appName}-appBuild.zip`;
32
- const sourcePath = `${workspaceRoot}/releases/sources/${appName}-source.tar.gz`;
33
-
34
28
  const readingFilesSpinner = spinning("Reading files...");
35
29
  try {
36
- const buildFile = Bun.file(buildPath);
37
- const sourceFile = Bun.file(sourcePath);
38
- const appBuildFile = Bun.file(appBuildPath);
39
- const buildStat = { mtime: new Date(buildFile.lastModified), size: buildFile.size };
40
- const sourceStat = { mtime: new Date(sourceFile.lastModified), size: sourceFile.size };
41
- const appBuildStat = { mtime: new Date(appBuildFile.lastModified), size: appBuildFile.size };
30
+ const uploads = [
31
+ [`builds/${appName}-release.tar.gz`, `${appName}-release.tar.gz`],
32
+ [`sources/${appName}-source.tar.gz`, `${appName}-source.tar.gz`],
33
+ [`builds/${appName}-appBuild.zip`, `${appName}-appBuild.zip`],
34
+ ].map(([relativePath, name]) => ({ file: Bun.file(`${workspaceRoot}/releases/${relativePath}`), name }));
35
+ const metas = uploads.map(({ file }) => ({ lastModifiedAt: new Date(file.lastModified), size: file.size }));
42
36
  readingFilesSpinner.succeed("Reading files... done");
43
37
 
44
38
  const preparingFormSpinner = spinning("Preparing form data...");
45
39
  const formData = new FormData();
46
- formData.append("files", buildFile, `${appName}-release.tar.gz`);
47
- formData.append("files", sourceFile, `${appName}-source.tar.gz`);
48
- formData.append("files", appBuildFile, `${appName}-appBuild.zip`);
49
- formData.append(
50
- "metas",
51
- JSON.stringify([
52
- { lastModifiedAt: buildStat.mtime, size: buildStat.size },
53
- { lastModifiedAt: sourceStat.mtime, size: sourceStat.size },
54
- { lastModifiedAt: appBuildStat.mtime, size: appBuildStat.size },
55
- ]),
56
- );
40
+ for (const { file, name } of uploads) formData.append("files", file, name);
41
+ formData.append("metas", JSON.stringify(metas));
57
42
  formData.append("type", "release");
58
43
  preparingFormSpinner.succeed("Preparing form data... done");
59
44
 
@@ -1,3 +1,4 @@
1
+ import { capitalize } from "akanjs/common";
1
2
  import type { Workspace } from "../commandDecorators";
2
3
  import type {
3
4
  GeneratedSyncState,
@@ -146,6 +147,23 @@ export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
146
147
  kind: validation.kind,
147
148
  })) satisfies WorkflowApplyCommand[];
148
149
 
150
+ const planInputString = (plan: WorkflowPlan, key: string) => {
151
+ const value = plan.inputs[key];
152
+ return typeof value === "string" ? value : "";
153
+ };
154
+
155
+ export const workflowPathsForPlan = (plan: WorkflowPlan) => {
156
+ const app = planInputString(plan, "app");
157
+ const module = planInputString(plan, "module");
158
+ const moduleClass = module ? capitalize(module) : "<Module>";
159
+ return plan.predictedChanges.map((change) =>
160
+ change.target
161
+ .replace(/^\*\//, app ? `apps/${app}/` : "")
162
+ .replaceAll("<module>", module || "<module>")
163
+ .replaceAll("<Module>", moduleClass),
164
+ );
165
+ };
166
+
149
167
  export const workflowRunsDir = ".akan/workflows/runs";
150
168
 
151
169
  export const createWorkflowRunId = (prefix = "run") =>
@@ -9,11 +9,6 @@ import type {
9
9
  WorkflowStepResult,
10
10
  } from "./types";
11
11
 
12
- /**
13
- * `WorkflowExecutor` takes its registry by constructor injection and its plan as data, so `apply` runs with
14
- * no filesystem and no workspace — the `workspace` argument is what gates every post-apply check. These
15
- * cover the ordering rules that `cli/workflow/workflow.test.ts` can only reach through a real scaffold.
16
- */
17
12
  const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
18
13
  id,
19
14
  tool,
@@ -21,8 +16,7 @@ const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
21
16
  description: id,
22
17
  });
23
18
 
24
- // Every field spelled out rather than cast through `unknown`: `apply` reads `validation` before the first
25
- // step runs, so a partial literal fails at runtime instead of at the type level.
19
+ // Not a cast partial: `apply` reads `validation` before the first step, so a missing field fails only at runtime.
26
20
  const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []): WorkflowPlan => ({
27
21
  schemaVersion: 1,
28
22
  workflow: "add-field",
@@ -37,10 +31,6 @@ const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []):
37
31
  requiresApproval: true,
38
32
  });
39
33
 
40
- /**
41
- * A registry that records **which key** each call resolved through, not just that a step ran — the
42
- * resolution order is the thing under test, and every candidate key would otherwise record the same step id.
43
- */
44
34
  const changed = (path: string): PrimitiveChangedFile => ({ path, action: "modify", reason: "test" });
45
35
 
46
36
  const recordingRegistry = (results: Record<string, WorkflowStepResult | undefined> = {}) => {
@@ -79,7 +69,6 @@ describe("WorkflowExecutor.apply", () => {
79
69
 
80
70
  test("resolves a runner by workflow-scoped key first, then tool, then bare step id", async () => {
81
71
  const { calls, registry, on } = recordingRegistry();
82
- // All three keys are registered for the same step, so only the winner records a call.
83
72
  on(workflowStepKey("add-field", "scoped"));
84
73
  on("tool:scoped");
85
74
  on("scoped");