@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
@@ -2,64 +2,19 @@ import path from "node:path";
2
2
  import type { BunPlugin } from "bun";
3
3
  import type { App } from "../commandDecorators";
4
4
 
5
- /**
6
- * Keep framework/runtime singletons external while bundling authored
7
- * workspace sources and ordinary npm dependencies into the pages bundle.
8
- *
9
- * Why a plugin instead of `Bun.build({ external })` or
10
- * `Bun.build({ packages: "external" })`:
11
- * - `packages: "external"` externalizes every bare specifier, including
12
- * `@apps/*` / `@libs/*` — workspace packages that must stay bundled
13
- * so the `"use client"` plugin can rewrite their exports — and ordinary
14
- * npm dependencies like `dayjs` / `tailwind-merge`, which the production runtime
15
- * package.json does not install for the SSR pages artifact.
16
- * - Top-level `external: [...]` applies to macro-time module resolution
17
- * too; any `with { type: "macro" }` import chain that transitively
18
- * resolves `react` during build then fails with `ENOENT "react"`.
19
- *
20
- * An `onResolve` plugin only participates in the output-graph walk, so
21
- * macros evaluate against the real modules in node_modules while the
22
- * emitted bundle keeps bare specifiers only for runtime singletons that
23
- * are installed in the generated production package.json.
24
- *
25
- * Externalization rules (applied in order):
26
- * 1. Relative specifiers (`./`, `../`) are NEVER externalized — they
27
- * always resolve to the current package's source tree and must be
28
- * inlined so transitive `"use client"` stubs work.
29
- * 2. Specifiers listed in `include` (workspace aliases like
30
- * `@apps/*`, `@libs/*`) are NEVER externalized.
31
- * 3. React / RSC runtime packages and build-time tooling packages are
32
- * externalized so the runtime supplies a single shared instance.
33
- * 4. Other bare npm packages are bundled so the Docker runtime does not
34
- * need to install every transitive page dependency separately.
35
- */
36
5
  export interface ExternalizeFrameworkOptions {
37
6
  app: App;
38
- /**
39
- * Prefixes that should be BUNDLED (kept internal). Defaults to
40
- * `@apps/` and `@libs/` which are the workspace aliases used by the
41
- * framework's app template.
42
- */
7
+ /** Prefixes to bundle rather than externalize; defaults to `akanjs/`, `@apps/`, `@libs/`. */
43
8
  include?: string[];
44
- /**
45
- * Extra bare specifiers to force-externalize beyond the allowlist
46
- * defaults. Not commonly needed since the default rule already
47
- * externalizes every non-workspace specifier.
48
- */
9
+ /** Bare specifiers to force-externalize, overriding `include`. */
49
10
  extra?: string[];
50
11
  }
51
12
 
52
- // Workspace package prefixes that must be BUNDLED so their sources go
53
- // through the barrel-imports + `"use client"` transforms. Without this
54
- // the RSC worker would resolve them at runtime — skipping the
55
- // transforms entirely, which re-introduces the react-dom/client
56
- // side-effects we wanted to strip from the server graph.
13
+ // Bundled so they pass the barrel and `"use client"` transforms; resolved at runtime they would drag
14
+ // react-dom/client side effects into the server graph.
57
15
  const DEFAULT_INCLUDE = ["akanjs/", "@apps/", "@libs/"];
58
16
 
59
- // Packages that must stay external even when a broad allowlist would
60
- // otherwise bundle them. These are runtime hosts or shared singleton
61
- // framework packages; ordinary npm dependencies intentionally fall through
62
- // to Bun's resolver and get bundled.
17
+ // Runtime hosts and singleton framework packages stay external; ordinary npm dependencies are bundled.
63
18
  const DEFAULT_EXCLUDE_EXACT = new Set<string>(["akanjs/webkit", "@akanjs/cli", "@akanjs/devkit"]);
64
19
  const DEFAULT_EXCLUDE_PREFIX = ["@akanjs/cli/", "@akanjs/devkit/"];
65
20
  const OPTIONAL_BACKEND_EXTERNAL_EXACT = new Set<string>([
@@ -81,19 +36,17 @@ const RUNTIME_EXTERNAL_EXACT = new Set<string>([
81
36
  ]);
82
37
  const RUNTIME_EXTERNAL_PREFIX = ["react-dom/", "react-server-dom-webpack/"];
83
38
 
84
- // File extensions Bun can load when a bare subpath points at an
85
- // extensionless path. Ordered to prefer TS over JS, since workspace
86
- // sources are authored in TypeScript.
39
+ // TS before JS: workspace sources are authored in TypeScript.
87
40
  const CANDIDATE_EXTS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];
88
41
 
42
+ // Rejected `Bun.build` options: `external` also applies at macro time (ENOENT "react"), and `packages: "external"`
43
+ // externalizes workspace and npm packages the runtime does not install. onResolve shapes only the output graph.
89
44
  export async function createExternalizeFrameworkPlugin(options: ExternalizeFrameworkOptions): Promise<BunPlugin> {
90
45
  const tsconfig = await options.app.getTsConfig();
91
46
  const includePrefixes = options.include ?? DEFAULT_INCLUDE;
92
47
  const extraExact = new Set(options.extra ?? []);
93
48
  const workspaceRoot = options.app.workspace.workspaceRoot;
94
49
  const tsconfigPaths = tsconfig.compilerOptions.paths ?? {};
95
- // Pre-compute the tsconfig root-only entries (`akanjs/client` → …)
96
- // so we can resolve their subpaths by concatenating the remainder.
97
50
  const rootEntries = Object.entries(tsconfigPaths)
98
51
  .filter(([k]) => !k.endsWith("/*"))
99
52
  .map(([k, v]) => ({ pkg: k, entryFile: v[0] ?? null }))
@@ -105,7 +58,6 @@ export async function createExternalizeFrameworkPlugin(options: ExternalizeFrame
105
58
 
106
59
  async function resolveWorkspaceSubpath(spec: string): Promise<string | null> {
107
60
  if (!workspaceRoot) return null;
108
- // Wildcard `@apps/*` / `@libs/*` → direct filesystem path.
109
61
  for (const { prefix, replacements } of wildcardEntries) {
110
62
  if (!spec.startsWith(prefix)) continue;
111
63
  const suffix = spec.slice(prefix.length);
@@ -117,12 +69,8 @@ export async function createExternalizeFrameworkPlugin(options: ExternalizeFrame
117
69
  if (hit) return hit;
118
70
  }
119
71
  }
120
- // Root-only tsconfig entries — resolve the subpath against the
121
- // package directory. E.g. `akanjs/client` → `pkgs/akanjs/client/index.ts`
122
- // so `akanjs/client/cookie` → `pkgs/akanjs/client/cookie`.
123
72
  for (const { pkg, entryFile } of rootEntries) {
124
- if (spec !== pkg && !spec.startsWith(`${pkg}/`)) continue;
125
- if (spec === pkg) continue; // the package root itself — let Bun's tsconfig resolver handle it
73
+ if (!spec.startsWith(`${pkg}/`)) continue;
126
74
  const suffix = spec.slice(pkg.length + 1);
127
75
  const pkgDir = path.dirname(path.resolve(workspaceRoot, entryFile));
128
76
  const candidate = path.join(pkgDir, suffix);
@@ -137,13 +85,8 @@ export async function createExternalizeFrameworkPlugin(options: ExternalizeFrame
137
85
  setup(build) {
138
86
  build.onResolve({ filter: /.*/ }, async (args) => {
139
87
  const spec = args.path;
140
- // Relative imports always stay inlined — they anchor to the
141
- // current package's source tree and cannot leak cross-package.
142
- // `.` and `..` (without trailing slash) are valid relative
143
- // specifiers too (equivalent to `./index` / `../index`).
144
88
  if (spec === "." || spec === ".." || spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/"))
145
89
  return undefined;
146
- // Force-external first (overrides the include allowlist).
147
90
  if (extraExact.has(spec) || DEFAULT_EXCLUDE_EXACT.has(spec)) return { path: spec, external: true };
148
91
  for (const prefix of DEFAULT_EXCLUDE_PREFIX) {
149
92
  if (spec.startsWith(prefix)) return { path: spec, external: true };
@@ -153,38 +96,21 @@ export async function createExternalizeFrameworkPlugin(options: ExternalizeFrame
153
96
  for (const prefix of RUNTIME_EXTERNAL_PREFIX) {
154
97
  if (spec.startsWith(prefix)) return { path: spec, external: true };
155
98
  }
156
- // Workspace allowlist — resolve the subpath to an absolute
157
- // on-disk file so Bun.build can read it. Without this, bare
158
- // specifiers like `akanjs/client/cookie` (produced by the
159
- // barrel rewriter) fail Bun's default resolution (only root
160
- // tsconfig-paths entry exists for `akanjs/client`) and get
161
- // silently left as externals.
162
- for (const prefix of includePrefixes) {
163
- if (!spec.startsWith(prefix)) continue;
164
- const resolved = await resolveWorkspaceSubpath(spec);
165
- if (resolved) return { path: resolved };
166
- // Fall through to let Bun attempt its own resolution for the
167
- // package root (`akanjs/client`) — tsconfig `paths` handles
168
- // that case directly.
169
- return undefined;
170
- }
171
- // Everything else (ordinary npm dependencies like dayjs / tailwind-merge /
172
- // immer) is bundled into the pages artifact.
173
- return undefined;
99
+ // Bun resolves only root tsconfig `paths`, so a rewritten `akanjs/client/cookie` would silently stay external.
100
+ if (!includePrefixes.some((prefix) => spec.startsWith(prefix))) return undefined;
101
+ const resolved = await resolveWorkspaceSubpath(spec);
102
+ return resolved ? { path: resolved } : undefined;
174
103
  });
175
104
  },
176
105
  };
177
106
  }
178
107
 
179
108
  async function firstExisting(basePath: string): Promise<string | null> {
180
- // Direct file with its own extension.
181
109
  if (await Bun.file(basePath).exists()) return basePath;
182
- // Try each candidate extension.
183
110
  for (const ext of CANDIDATE_EXTS) {
184
111
  const candidate = `${basePath}${ext}`;
185
112
  if (await Bun.file(candidate).exists()) return candidate;
186
113
  }
187
- // Directory with `index.<ext>` inside.
188
114
  for (const ext of CANDIDATE_EXTS) {
189
115
  const candidate = path.join(basePath, `index${ext}`);
190
116
  if (await Bun.file(candidate).exists()) return candidate;
@@ -0,0 +1,32 @@
1
+ export interface NamedItem {
2
+ imported: string;
3
+ local: string;
4
+ isType: boolean;
5
+ }
6
+
7
+ export const loaderFor = (absPath: string): "ts" | "tsx" | "js" | "jsx" => {
8
+ if (absPath.endsWith(".tsx")) return "tsx";
9
+ if (absPath.endsWith(".jsx")) return "jsx";
10
+ if (absPath.endsWith(".ts")) return "ts";
11
+ return "js";
12
+ };
13
+
14
+ export const parseNamedList = (listBody: string): NamedItem[] => {
15
+ const out: NamedItem[] = [];
16
+ for (const raw of listBody.split(",")) {
17
+ let rest = raw.trim();
18
+ if (!rest) continue;
19
+ let isType = false;
20
+ if (rest.startsWith("type ")) {
21
+ isType = true;
22
+ rest = rest.slice(5).trim();
23
+ }
24
+ const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(rest);
25
+ if (asMatch) {
26
+ out.push({ imported: asMatch[1] ?? "", local: asMatch[2] ?? "", isType });
27
+ continue;
28
+ }
29
+ if (/^\w+$/.test(rest)) out.push({ imported: rest, local: rest, isType });
30
+ }
31
+ return out;
32
+ };
@@ -1,35 +1,64 @@
1
1
  import path from "node:path";
2
+ import ts from "typescript";
3
+ import { loaderFor } from "./moduleSyntax";
2
4
 
3
- // Pure "use client" transform: when `source` starts with the `"use client"`
4
- // directive, replace its exports with `registerClientReference` stubs so the
5
- // RSC renderer can serialize them as client component references instead of
6
- // trying to run them on the server.
7
-
8
- // Matches `"use client"` or `'use client'` at the start of a file,
9
- // optionally after leading whitespace and JS comments.
10
5
  const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
11
6
  const IMPLICIT_ROOT_LAYOUT_RE =
12
7
  /[/\\]\.akan[/\\]generated[/\\](?:implicit-root-layout|root-layouts[/\\].*__root_layout)\.(tsx|ts|jsx|js)$/;
8
+ const STAR_REEXPORT_HINT_RE = /\bexport\s*\*\s*from\b/;
13
9
 
14
10
  export interface UseClientTransformArgs {
15
11
  path: string;
16
12
  workspaceRoot?: string;
13
+ /** Told each `"use client"` module's export names, the only part of it the server graph holds. */
14
+ onClientModule?: (path: string, exports: string[]) => void;
17
15
  }
18
16
 
17
+ export const hasUseClientDirective = (source: string): boolean => USE_CLIENT_RE.test(source);
18
+
19
19
  export function toClientReferencePath(absPath: string, workspaceRoot: string): string {
20
20
  return path.relative(path.resolve(workspaceRoot), path.resolve(absPath)).split(path.sep).join("/");
21
21
  }
22
22
 
23
- /**
24
- * Returns the stubbed module source if `source` is a client module, else null.
25
- * The returned source is TypeScript-compatible (loader "ts" is safe).
26
- */
23
+ const starReexportSpecifiers = (source: string, filePath: string) =>
24
+ ts
25
+ .createSourceFile(filePath, source, ts.ScriptTarget.Latest)
26
+ .statements.filter(
27
+ (statement): statement is ts.ExportDeclaration & { moduleSpecifier: ts.StringLiteral } =>
28
+ ts.isExportDeclaration(statement) &&
29
+ !statement.exportClause &&
30
+ !statement.isTypeOnly &&
31
+ !!statement.moduleSpecifier &&
32
+ ts.isStringLiteral(statement.moduleSpecifier),
33
+ )
34
+ .map((statement) => statement.moduleSpecifier.text);
35
+
36
+ // `Bun.Transpiler.scan` reports no name through `export *`, so the server could register none of a star's names as
37
+ // client references; they would be undefined in a server component. Refused as Next.js refuses it.
38
+ export const scanUseClientExports = (source: string, filePath: string, workspaceRoot?: string): string[] => {
39
+ const stars =
40
+ USE_CLIENT_RE.test(source) && STAR_REEXPORT_HINT_RE.test(source) ? starReexportSpecifiers(source, filePath) : [];
41
+ if (stars.length) {
42
+ const file = workspaceRoot ? toClientReferencePath(filePath, workspaceRoot) : filePath;
43
+ const listed = stars.slice(0, 3).map((specifier) => `\`export * from ${JSON.stringify(specifier)}\``);
44
+ const written = stars.length > 3 ? `${listed.join(", ")} (and ${stars.length - 3} more)` : listed.join(", ");
45
+ throw new Error(
46
+ `${file} is a "use client" module, so it cannot ${written}: the server sees only the names a client module ` +
47
+ `declares, and a star re-export declares none. Re-export them by name — \`export { … } from ${JSON.stringify(stars[0])}\`.`,
48
+ );
49
+ }
50
+ return new Bun.Transpiler({ loader: loaderFor(filePath) }).scan(source).exports;
51
+ };
52
+
53
+ /** `null` unless `source` is a `"use client"` module; the stub source is valid under loader `"ts"`. */
27
54
  export function transformUseClient(source: string, args: UseClientTransformArgs): string | null {
28
55
  if (!USE_CLIENT_RE.test(source)) return null;
29
56
  if (IMPLICIT_ROOT_LAYOUT_RE.test(args.path)) return null;
30
- const transpiler = new Bun.Transpiler({ loader: loaderFor(args.path) });
31
- const { exports } = transpiler.scan(source);
57
+ const exports = scanUseClientExports(source, args.path, args.workspaceRoot);
58
+ //? Not reported without exports: the module then stays in the server bundle as it is, so a save of it changes what
59
+ //? the server runs, which a client module's edit that kept its export names does not.
32
60
  if (exports.length === 0) return null;
61
+ args.onClientModule?.(args.path, exports);
33
62
 
34
63
  const referencePath = args.workspaceRoot ? toClientReferencePath(args.path, args.workspaceRoot) : args.path;
35
64
  const filePathLit = JSON.stringify(referencePath);
@@ -50,10 +79,3 @@ export function transformUseClient(source: string, args: UseClientTransformArgs)
50
79
 
51
80
  return lines.join("\n");
52
81
  }
53
-
54
- function loaderFor(absPath: string): "ts" | "tsx" | "js" | "jsx" {
55
- if (absPath.endsWith(".tsx")) return "tsx";
56
- if (absPath.endsWith(".jsx")) return "jsx";
57
- if (absPath.endsWith(".ts")) return "ts";
58
- return "js";
59
- }
@@ -0,0 +1,103 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import path from "node:path";
3
+ import { tempDirs, writeText as write } from "../testHelpers";
4
+ import { createServerEnvPlugin } from "./serverEnvPlugin";
5
+
6
+ const makeTempRoot = tempDirs("akan-devkit-server-env-");
7
+ const environments = ["local", "testing", "debug", "develop", "main", "qa"];
8
+
9
+ const writeApp = async (root: string) => {
10
+ const envDir = path.join(root, "apps/demo/env");
11
+ await write(
12
+ path.join(envDir, "env.server.ts"),
13
+ [
14
+ ...environments.map((name) => `import { env as ${name} } from "./env.server.${name}";`),
15
+ `const envConfigs = { ${environments.join(", ")} };`,
16
+ 'const currentEnv = process.env.AKAN_PUBLIC_ENV ?? "local";',
17
+ "export const env = envConfigs[currentEnv as keyof typeof envConfigs];",
18
+ "",
19
+ ].join("\n"),
20
+ );
21
+ await write(path.join(envDir, "env.server.type.ts"), 'export const libEnv = { shared: "shared-value" };\n');
22
+ for (const name of environments)
23
+ await write(
24
+ path.join(envDir, `env.server.${name}.ts`),
25
+ [
26
+ 'import { libEnv } from "./env.server.type";',
27
+ `export const env = { ...libEnv, secret: "${name}-secret" };`,
28
+ `export default "${name}-default";`,
29
+ "",
30
+ ].join("\n"),
31
+ );
32
+ const entry = path.join(root, "apps/demo/server.ts");
33
+ await write(entry, 'import { env } from "./env/env.server";\nconsole.info(JSON.stringify({ ...env }));\n');
34
+ return { envDir, entry };
35
+ };
36
+
37
+ const build = async (root: string, environment: string) => {
38
+ const { envDir, entry } = await writeApp(root);
39
+ const result = await Bun.build({
40
+ entrypoints: [entry],
41
+ outdir: path.join(root, "out"),
42
+ target: "bun",
43
+ plugins: [createServerEnvPlugin({ envDir, environment, environments })],
44
+ });
45
+ expect(result.success).toBe(true);
46
+ const output = result.outputs.find((artifact) => artifact.kind === "entry-point");
47
+ if (!output) throw new Error("no entry output");
48
+ return { file: output.path, text: await output.text() };
49
+ };
50
+
51
+ const run = (file: string, environment: string) =>
52
+ Bun.spawnSync([process.execPath, file], { env: { ...process.env, AKAN_PUBLIC_ENV: environment } });
53
+
54
+ describe("createServerEnvPlugin", () => {
55
+ test("bundles only the environment the image runs", async () => {
56
+ const { text } = await build(await makeTempRoot(), "main");
57
+ expect(text).toContain("main-secret");
58
+ expect(text).toContain("shared-value");
59
+ for (const name of environments.filter((name) => name !== "main")) {
60
+ expect(text).not.toContain(`${name}-secret`);
61
+ expect(text).not.toContain(`${name}-default`);
62
+ }
63
+ });
64
+
65
+ test("the built environment boots with its own values", async () => {
66
+ const { file } = await build(await makeTempRoot(), "main");
67
+ const proc = run(file, "main");
68
+ expect(proc.exitCode).toBe(0);
69
+ expect(JSON.parse(proc.stdout.toString())).toEqual({ shared: "shared-value", secret: "main-secret" });
70
+ });
71
+
72
+ test("another environment refuses to boot and names the build to make", async () => {
73
+ const { file } = await build(await makeTempRoot(), "main");
74
+ const proc = run(file, "develop");
75
+ expect(proc.exitCode).not.toBe(0);
76
+ expect(proc.stderr.toString()).toContain(
77
+ "env/env.server.develop.ts is not in this build: `akan build` bundles the server env of AKAN_PUBLIC_ENV=main alone",
78
+ );
79
+ expect(proc.stderr.toString()).toContain(
80
+ "To run develop, build for it: AKAN_PUBLIC_ENV=develop in the workspace's .env, which outranks the shell, or --env develop on a desktop or mobile build.",
81
+ );
82
+ });
83
+
84
+ test("a custom branch is its own environment", async () => {
85
+ const { text } = await build(await makeTempRoot(), "qa");
86
+ expect(text).toContain("qa-secret");
87
+ expect(text).not.toContain("main-secret");
88
+ });
89
+
90
+ test("leaves env files outside the app's env directory alone", async () => {
91
+ const root = await makeTempRoot();
92
+ const other = path.join(root, "libs/shared/env/env.server.testing.ts");
93
+ await write(other, 'export const env = { secret: "lib-testing-secret" };\n');
94
+ const { envDir, entry } = await writeApp(root);
95
+ await write(entry, `import { env } from ${JSON.stringify(other)};\nconsole.info(env.secret);\n`);
96
+ const result = await Bun.build({
97
+ entrypoints: [entry],
98
+ target: "bun",
99
+ plugins: [createServerEnvPlugin({ envDir, environment: "main", environments })],
100
+ });
101
+ expect(await result.outputs[0]?.text()).toContain("lib-testing-secret");
102
+ });
103
+ });
@@ -0,0 +1,41 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { BunPlugin } from "bun";
4
+
5
+ export interface ServerEnvPluginOptions {
6
+ envDir: string;
7
+ /** The environment the image runs: its Dockerfile fixes `AKAN_PUBLIC_ENV` to it. */
8
+ environment: string;
9
+ environments: Iterable<string>;
10
+ }
11
+
12
+ const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+
14
+ //* `env.server.ts` imports every environment's file, so an image would hand out the secrets of each environment it
15
+ //* never runs. Every file but the image's own is swapped for exports that refuse to be read.
16
+ export const createServerEnvPlugin = ({ envDir, environment, environments }: ServerEnvPluginOptions): BunPlugin => {
17
+ const others = [...new Set(environments)].filter((name) => name !== environment);
18
+ //? Bun hands onLoad the real path, which differs from the given one under a symlink (macOS's /var -> /private/var).
19
+ const dirs = new Set([path.resolve(envDir), existsSync(envDir) ? realpathSync.native(envDir) : envDir]);
20
+ const names = others.map(escapeRegExp).join("|");
21
+ const filter = new RegExp(`^(?:${[...dirs].map(escapeRegExp).join("|")})[\\\\/]env\\.server\\.(?:${names})\\.ts$`);
22
+ return {
23
+ name: "akan-server-env",
24
+ setup(build) {
25
+ if (!others.length) return;
26
+ build.onLoad({ filter }, async (args) => {
27
+ const name = path.basename(args.path, ".ts").slice("env.server.".length);
28
+ const { exports } = new Bun.Transpiler({ loader: "ts" }).scan(await Bun.file(args.path).text());
29
+ const message = `env/env.server.${name}.ts is not in this build: \`akan build\` bundles the server env of AKAN_PUBLIC_ENV=${environment} alone, the environment its image runs. To run ${name}, build for it: AKAN_PUBLIC_ENV=${name} in the workspace's .env, which outranks the shell, or --env ${name} on a desktop or mobile build.`;
30
+ return {
31
+ loader: "js",
32
+ contents: [
33
+ `const refuse = () => { throw new Error(${JSON.stringify(message)}); };`,
34
+ "const unbuilt = new Proxy({}, { get: refuse, has: refuse, ownKeys: refuse, getOwnPropertyDescriptor: refuse });",
35
+ `export { ${exports.map((exported) => `unbuilt as ${JSON.stringify(exported)}`).join(", ")} };`,
36
+ ].join("\n"),
37
+ };
38
+ });
39
+ },
40
+ };
41
+ };