@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.21

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 (289) hide show
  1. package/README.ko.md +2 -2
  2. package/README.md +3 -2
  3. package/abstractDoc.test.ts +6 -15
  4. package/abstractDoc.ts +6 -8
  5. package/agentsIndex.test.ts +4 -7
  6. package/agentsIndex.ts +6 -22
  7. package/akanApp/BackendImportGraph.test.ts +8 -22
  8. package/akanApp/BackendImportGraph.ts +1 -20
  9. package/akanApp/akanApp.host.test.ts +1 -3
  10. package/akanApp/akanApp.host.ts +82 -254
  11. package/akanApp/devHostPolicy.test.ts +10 -34
  12. package/akanApp/devHostPolicy.ts +12 -98
  13. package/akanConfig/akanConfig.test.ts +142 -106
  14. package/akanConfig/akanConfig.ts +147 -191
  15. package/akanConfig/types.ts +4 -3
  16. package/akanContext.ts +110 -160
  17. package/akanMcpContract.ts +28 -73
  18. package/appSelectionMemory.ts +1 -4
  19. package/applicationBuildReporter.ts +37 -5
  20. package/applicationBuildRunner.test.ts +5 -3
  21. package/applicationBuildRunner.ts +54 -61
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +32 -30
  24. package/artifact/implicitRootLayout.ts +58 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +32 -1
  28. package/biomeBase.ts +2 -5
  29. package/cloud/cloudApi.test.ts +202 -0
  30. package/cloud/cloudApi.ts +77 -39
  31. package/cloud/configLock.test.ts +64 -0
  32. package/cloud/configLock.ts +96 -0
  33. package/cloud/constants.ts +26 -5
  34. package/cloud/globalConfig.ts +70 -32
  35. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  36. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  37. package/codeAgent/agent/CodeAgent.ts +551 -0
  38. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  39. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  40. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  41. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  42. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  43. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  44. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  45. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  46. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  47. package/codeAgent/agent/CodeMailbox.ts +148 -0
  48. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  49. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  50. package/codeAgent/agent/SubagentPool.ts +214 -0
  51. package/codeAgent/agent/akanCodeModel.ts +47 -0
  52. package/codeAgent/agent/akanCodePaths.ts +31 -0
  53. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  54. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  55. package/codeAgent/agent/codeAgent.test.ts +178 -0
  56. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  57. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  58. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  59. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  60. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  61. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  62. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  63. package/codeAgent/agent/consoleToStderr.ts +8 -0
  64. package/codeAgent/agent/jsonLines.ts +29 -0
  65. package/codeAgent/agent/stringArg.ts +5 -0
  66. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  67. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  68. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  69. package/codeAgent/feedback/PreviewView.ts +95 -0
  70. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  71. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  72. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  73. package/codeAgent/index.ts +34 -0
  74. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  75. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  76. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  77. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  78. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  79. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  80. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  81. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  82. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  83. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  84. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  85. package/codeAgent/tools/AkanCli.ts +50 -0
  86. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  87. package/codeAgent/tools/AkanEditScope.ts +48 -0
  88. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  89. package/codeAgent/tools/AkanToolPack.ts +92 -0
  90. package/codeAgent/tools/AkanVerifier.ts +48 -0
  91. package/codeAgent/tools/AskToolPack.ts +99 -0
  92. package/codeAgent/tools/MailToolPack.ts +76 -0
  93. package/codeAgent/tools/McpClient.ts +187 -0
  94. package/codeAgent/tools/McpOAuth.ts +233 -0
  95. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  96. package/codeAgent/tools/McpServerConfig.ts +129 -0
  97. package/codeAgent/tools/McpSignIn.ts +158 -0
  98. package/codeAgent/tools/McpTokenStore.ts +81 -0
  99. package/codeAgent/tools/McpToolPack.ts +199 -0
  100. package/codeAgent/tools/SessionToolPack.ts +91 -0
  101. package/codeAgent/tools/WebToolPack.ts +147 -0
  102. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  103. package/codeAgent/tools/askToolPack.test.ts +116 -0
  104. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  105. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  106. package/codegenLock.test.ts +4 -13
  107. package/codegenLock.ts +3 -17
  108. package/commandDecorators/argMeta.ts +1 -5
  109. package/commandDecorators/camelToKebabCase.ts +1 -0
  110. package/commandDecorators/command.ts +46 -102
  111. package/commandDecorators/commandBuilder.ts +30 -61
  112. package/commandDecorators/commandDecorators.test.ts +2 -10
  113. package/commandDecorators/dependencyBuilder.ts +10 -1
  114. package/commandDecorators/helpFormatter.ts +68 -145
  115. package/commandDecorators/index.ts +0 -2
  116. package/commandDecorators/targetMeta.ts +9 -7
  117. package/createTunnel.ts +7 -14
  118. package/dependencyScanner.test.ts +4 -16
  119. package/dependencyScanner.ts +31 -80
  120. package/devkitUtils.test.ts +48 -20
  121. package/executors.linkFallback.test.ts +103 -0
  122. package/executors.test.ts +132 -106
  123. package/executors.ts +195 -496
  124. package/fileEditor.ts +9 -37
  125. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  126. package/fileSys.ts +6 -8
  127. package/formSetterScanner.ts +3 -16
  128. package/frontendBuild/allRoutesBuilder.ts +7 -21
  129. package/frontendBuild/autoImportSync.test.ts +27 -38
  130. package/frontendBuild/autoImportSync.ts +31 -65
  131. package/frontendBuild/buildRouteClient.test.ts +62 -37
  132. package/frontendBuild/bundleDefine.ts +12 -0
  133. package/frontendBuild/clientBuildTypes.ts +5 -36
  134. package/frontendBuild/clientEntriesBundler.ts +9 -40
  135. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  136. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  137. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  138. package/frontendBuild/cssCandidateCache.ts +6 -41
  139. package/frontendBuild/cssCompiler.ts +29 -78
  140. package/frontendBuild/cssImportResolver.ts +6 -19
  141. package/frontendBuild/devChangePlanner.ts +7 -10
  142. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  143. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  144. package/frontendBuild/fontOptimizer.test.ts +6 -14
  145. package/frontendBuild/fontOptimizer.ts +15 -72
  146. package/frontendBuild/fontPruner.test.ts +5 -10
  147. package/frontendBuild/fontPruner.ts +5 -20
  148. package/frontendBuild/frontendBuild.test.ts +46 -69
  149. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  150. package/frontendBuild/hmrWatcher.test.ts +8 -32
  151. package/frontendBuild/hmrWatcher.ts +14 -71
  152. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  153. package/frontendBuild/precompressArtifacts.ts +5 -18
  154. package/frontendBuild/routeClientBuilder.ts +23 -46
  155. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  156. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  157. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  158. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  159. package/frontendBuild/styleContract.ts +1 -8
  160. package/frontendBuild/themeValidator.ts +4 -26
  161. package/frontendBuild/watchRootResolver.test.ts +23 -42
  162. package/frontendBuild/watchRootResolver.ts +5 -27
  163. package/getRelatedCnsts.ts +12 -92
  164. package/incrementalBuilder/buildBatch.proc.ts +29 -58
  165. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  166. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  167. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  168. package/incrementalBuilder/builderChannel.test.ts +24 -49
  169. package/incrementalBuilder/builderChannel.ts +6 -37
  170. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  171. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  172. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  173. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  174. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  175. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  176. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  177. package/index.ts +5 -19
  178. package/integration/devResourceProbe.ts +50 -56
  179. package/integration/devStability.integration.test.ts +47 -194
  180. package/integration/devStabilityHarness.test.ts +13 -28
  181. package/integration/devStabilityHarness.ts +65 -330
  182. package/integration/ssrMemoryProbe.ts +41 -167
  183. package/libSource.test.ts +4 -15
  184. package/libSource.ts +19 -31
  185. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  186. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  187. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  188. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  189. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  190. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  191. package/lint/gritRules.test.ts +8 -24
  192. package/lint/no-deep-internal-import.grit +2 -2
  193. package/lint/no-document-cookie.grit +29 -0
  194. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  195. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  196. package/lint/no-web-storage.grit +30 -0
  197. package/linter.test.ts +1 -2
  198. package/linter.ts +26 -116
  199. package/mobile/index.ts +6 -0
  200. package/mobile/mobileTarget.ts +16 -13
  201. package/mobile/nativeApi.test.ts +30 -0
  202. package/mobile/nativeApi.ts +40 -0
  203. package/mobile/nativeApp.test.ts +85 -0
  204. package/mobile/nativeApp.ts +210 -0
  205. package/mobile/nativeConfig.test.ts +199 -0
  206. package/mobile/nativeConfig.ts +184 -0
  207. package/mobile/nativeWebDir.test.ts +80 -0
  208. package/mobile/nativeWebDir.ts +62 -0
  209. package/mobile/placeholderAppId.test.ts +13 -0
  210. package/mobile/placeholderAppId.ts +16 -0
  211. package/mobile/usageDescriptions.test.ts +27 -0
  212. package/mobile/usageDescriptions.ts +24 -0
  213. package/package.json +10 -10
  214. package/packageExports.test.ts +3 -18
  215. package/packageExportsMap.ts +3 -19
  216. package/piDependencyPins.test.ts +46 -0
  217. package/platformTest/BunTestReport.test.ts +60 -0
  218. package/platformTest/BunTestReport.ts +65 -0
  219. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  220. package/platformTest/IdleWatchedProcess.ts +73 -0
  221. package/platformTest/LinuxDockerTarget.ts +105 -0
  222. package/platformTest/PlatformTestRun.test.ts +64 -0
  223. package/platformTest/PlatformTestRun.ts +141 -0
  224. package/platformTest/PlatformTestTarget.ts +142 -0
  225. package/platformTest/TestSnapshot.test.ts +72 -0
  226. package/platformTest/TestSnapshot.ts +103 -0
  227. package/platformTest/WindowsSshTarget.ts +151 -0
  228. package/prompter.ts +3 -13
  229. package/qualityScanner.test.ts +9 -26
  230. package/qualityScanner.ts +200 -286
  231. package/recipeScanner.test.ts +7 -12
  232. package/recipeScanner.ts +9 -38
  233. package/repoIdentity.ts +2 -9
  234. package/routeSourceValidator.test.ts +0 -1
  235. package/routeSourceValidator.ts +7 -42
  236. package/scanInfo.ts +91 -225
  237. package/selectModel.ts +1 -2
  238. package/semver.ts +3 -5
  239. package/slicePlanner.test.ts +6 -24
  240. package/slicePlanner.ts +11 -32
  241. package/spinner.test.ts +3 -8
  242. package/spinner.ts +4 -16
  243. package/sshTunnel.ts +1 -6
  244. package/ssrScanner.test.ts +1 -6
  245. package/ssrScanner.ts +103 -113
  246. package/stripAnsi.ts +3 -0
  247. package/subspace.test.ts +30 -25
  248. package/subspace.ts +81 -115
  249. package/subspaceConfig.ts +11 -9
  250. package/testHelpers.ts +194 -0
  251. package/transforms/asyncDefaultExportDetector.ts +3 -11
  252. package/transforms/barrelAnalyzer.ts +43 -135
  253. package/transforms/barrelImportsPlugin.ts +38 -175
  254. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  255. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  256. package/transforms/moduleSyntax.ts +32 -0
  257. package/transforms/rscUseClientTransform.ts +35 -20
  258. package/transforms/transforms.test.ts +153 -44
  259. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  260. package/transforms/useClientBundlePlugin.ts +2 -22
  261. package/typeChecker.ts +7 -63
  262. package/types.ts +1 -0
  263. package/uploadRelease.ts +10 -25
  264. package/workflow/artifacts.ts +18 -0
  265. package/workflow/executor.test.ts +1 -12
  266. package/workflow/executor.ts +101 -110
  267. package/workflow/index.ts +0 -1
  268. package/workflow/moduleIndex.test.ts +4 -15
  269. package/workflow/moduleIndex.ts +9 -49
  270. package/workflow/plan.ts +18 -25
  271. package/workflow/render.ts +80 -96
  272. package/workflow/rolloutGate.test.ts +1 -1
  273. package/workflow/source.ts +52 -69
  274. package/workflow/sourceAst.ts +50 -0
  275. package/workspaceLayout.test.ts +13 -3
  276. package/workspaceLayout.ts +20 -23
  277. package/abstractCompactor.ts +0 -112
  278. package/aiEditor.test.ts +0 -60
  279. package/aiEditor.ts +0 -387
  280. package/applicationReleasePackager.ts +0 -209
  281. package/capacitorApp.test.ts +0 -372
  282. package/capacitorApp.ts +0 -1648
  283. package/commandDecorators/commandMeta.ts +0 -7
  284. package/commandDecorators/types.ts +0 -10
  285. package/getDirname.ts +0 -4
  286. package/guideline.ts +0 -19
  287. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  288. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  289. 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,15 +1,11 @@
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;
@@ -20,15 +16,41 @@ export function toClientReferencePath(absPath: string, workspaceRoot: string): s
20
16
  return path.relative(path.resolve(workspaceRoot), path.resolve(absPath)).split(path.sep).join("/");
21
17
  }
22
18
 
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
- */
19
+ const starReexportSpecifiers = (source: string, filePath: string) =>
20
+ ts
21
+ .createSourceFile(filePath, source, ts.ScriptTarget.Latest)
22
+ .statements.filter(
23
+ (statement): statement is ts.ExportDeclaration & { moduleSpecifier: ts.StringLiteral } =>
24
+ ts.isExportDeclaration(statement) &&
25
+ !statement.exportClause &&
26
+ !statement.isTypeOnly &&
27
+ !!statement.moduleSpecifier &&
28
+ ts.isStringLiteral(statement.moduleSpecifier),
29
+ )
30
+ .map((statement) => statement.moduleSpecifier.text);
31
+
32
+ // `Bun.Transpiler.scan` reports no name through `export *`, so the server could register none of a star's names as
33
+ // client references; they would be undefined in a server component. Refused as Next.js refuses it.
34
+ export const scanUseClientExports = (source: string, filePath: string, workspaceRoot?: string): string[] => {
35
+ const stars =
36
+ USE_CLIENT_RE.test(source) && STAR_REEXPORT_HINT_RE.test(source) ? starReexportSpecifiers(source, filePath) : [];
37
+ if (stars.length) {
38
+ const file = workspaceRoot ? toClientReferencePath(filePath, workspaceRoot) : filePath;
39
+ const listed = stars.slice(0, 3).map((specifier) => `\`export * from ${JSON.stringify(specifier)}\``);
40
+ const written = stars.length > 3 ? `${listed.join(", ")} (and ${stars.length - 3} more)` : listed.join(", ");
41
+ throw new Error(
42
+ `${file} is a "use client" module, so it cannot ${written}: the server sees only the names a client module ` +
43
+ `declares, and a star re-export declares none. Re-export them by name — \`export { … } from ${JSON.stringify(stars[0])}\`.`,
44
+ );
45
+ }
46
+ return new Bun.Transpiler({ loader: loaderFor(filePath) }).scan(source).exports;
47
+ };
48
+
49
+ /** `null` unless `source` is a `"use client"` module; the stub source is valid under loader `"ts"`. */
27
50
  export function transformUseClient(source: string, args: UseClientTransformArgs): string | null {
28
51
  if (!USE_CLIENT_RE.test(source)) return null;
29
52
  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);
53
+ const exports = scanUseClientExports(source, args.path, args.workspaceRoot);
32
54
  if (exports.length === 0) return null;
33
55
 
34
56
  const referencePath = args.workspaceRoot ? toClientReferencePath(args.path, args.workspaceRoot) : args.path;
@@ -50,10 +72,3 @@ export function transformUseClient(source: string, args: UseClientTransformArgs)
50
72
 
51
73
  return lines.join("\n");
52
74
  }
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
- }
@@ -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", () => {
@@ -62,6 +51,65 @@ describe("transformUseClient", () => {
62
51
  });
63
52
  });
64
53
 
54
+ describe('export * in a "use client" module', () => {
55
+ test("is refused with the file, the specifier and the fix, since the server would see none of its names", () => {
56
+ const source = [
57
+ '"use client";',
58
+ 'export * from "../../ui/UserLeave";',
59
+ "export const General = () => null;",
60
+ "",
61
+ ].join("\n");
62
+ expect(() =>
63
+ transformUseClient(source, { path: "/repo/libs/shared/lib/user/User.Template.tsx", workspaceRoot: "/repo" }),
64
+ ).toThrow(
65
+ 'libs/shared/lib/user/User.Template.tsx is a "use client" module, so it cannot `export * from "../../ui/UserLeave"`',
66
+ );
67
+ expect(() => scanUseClientExports('"use client";\nexport * from "./a";\n', "/repo/ui/Barrel.ts")).toThrow(
68
+ "Re-export them by name",
69
+ );
70
+ });
71
+
72
+ test("leaves every other export shape alone, and a module that is not a client boundary", () => {
73
+ const file = "/repo/ui/Barrel.tsx";
74
+ const shapes = [
75
+ '"use client";',
76
+ 'export type * from "./types";',
77
+ 'export * as ns from "./namespace";',
78
+ 'export { LeaveInfo, Voc } from "./leave";',
79
+ "// export * from './commented';",
80
+ "export const text = \"export * from './a string'\";",
81
+ "",
82
+ ].join("\n");
83
+ expect(scanUseClientExports(shapes, file).sort()).toEqual(["LeaveInfo", "Voc", "ns", "text"]);
84
+ expect(scanUseClientExports('export * from "./a";\nexport const X = 1;\n', file)).toEqual(["X"]);
85
+ const stub = transformUseClient(shapes, { path: file, workspaceRoot: "/repo" });
86
+ expect(stub).toContain("export const LeaveInfo = registerClientReference");
87
+ expect(stub).toContain("export const Voc = registerClientReference");
88
+ });
89
+
90
+ test("fails the server bundle, and the build report carries the reason out of the bundler's error", async () => {
91
+ const root = await makeTempRoot();
92
+ await write(path.join(root, "Leave.tsx"), '"use client";\nexport const LeaveInfo = () => null;\n');
93
+ await write(
94
+ path.join(root, "Template.tsx"),
95
+ '"use client";\nexport * from "./Leave";\nexport const General = () => null;\n',
96
+ );
97
+ await write(path.join(root, "page.tsx"), 'import * as Template from "./Template";\nexport const T = Template;\n');
98
+ const failure = await Bun.build({
99
+ entrypoints: [path.join(root, "page.tsx")],
100
+ target: "bun",
101
+ external: ["react-server-dom-webpack/server.node"],
102
+ plugins: [createUseClientBundlePlugin({ workspaceRoot: root })],
103
+ }).then(
104
+ () => null,
105
+ (error: unknown) => error,
106
+ );
107
+ expect(ApplicationBuildReporter.formatError(failure)).toContain(
108
+ 'Template.tsx is a "use client" module, so it cannot `export * from "./Leave"`',
109
+ );
110
+ });
111
+ });
112
+
65
113
  describe("BarrelAnalyzer and rewriteBarrelImports", () => {
66
114
  test("analyzes local, named, aliased, and star re-exports while skipping type and namespace exports", async () => {
67
115
  const root = await makeTempRoot();
@@ -97,14 +145,83 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
97
145
  expect(map?.has("ns")).toBe(false);
98
146
  });
99
147
 
148
+ test("a name a module exports itself wins over the same name its star re-exports bring, as ES decides", async () => {
149
+ const root = await makeTempRoot();
150
+ const pkgDir = path.join(root, "pkg");
151
+ await write(
152
+ path.join(pkgDir, "index.ts"),
153
+ [
154
+ 'export * from "./a";',
155
+ 'export { X } from "./c";',
156
+ 'export * from "./nested";',
157
+ "export const Local = 1;",
158
+ "",
159
+ ].join("\n"),
160
+ );
161
+ await write(path.join(pkgDir, "a.ts"), "export const X = 2;\nexport const Local = 2;\nexport const OnlyA = 2;\n");
162
+ await write(path.join(pkgDir, "c.ts"), "export const X = 1;\n");
163
+ await write(path.join(pkgDir, "nested", "index.ts"), 'export * from "./deep";\nexport { Y } from "./y";\n');
164
+ await write(path.join(pkgDir, "nested", "deep.ts"), "export const Y = 2;\n");
165
+ await write(path.join(pkgDir, "nested", "y.ts"), "export const Y = 1;\n");
166
+ const analyzer = new BarrelAnalyzer({
167
+ resolvePackage: async () => ({ pkgName: "@scope/pkg", entryFile: path.join(pkgDir, "index.ts"), pkgDir }),
168
+ });
169
+
170
+ const runtime = (await import(path.join(pkgDir, "index.ts"))) as Record<string, number>;
171
+ expect([runtime.X, runtime.Local, runtime.Y, runtime.OnlyA]).toEqual([1, 1, 1, 2]);
172
+ const map = await analyzer.analyze("@scope/pkg");
173
+ expect(map?.get("X")).toEqual({ subpath: "@scope/pkg/c", originalName: "X" });
174
+ expect(map?.get("Local")).toEqual({ subpath: "@scope/pkg", originalName: "Local" });
175
+ expect(map?.get("Y")).toEqual({ subpath: "@scope/pkg/nested/y", originalName: "Y" });
176
+ expect(map?.get("OnlyA")).toEqual({ subpath: "@scope/pkg/a", originalName: "OnlyA" });
177
+ });
178
+
179
+ test("two analyses that interleave each keep their own place in their barrel", async () => {
180
+ const root = await makeTempRoot();
181
+ const barrels = { first: ["a1", "a2", "a3"], second: ["b1", "b2"] } as const;
182
+ for (const [pkgName, names] of Object.entries(barrels)) {
183
+ const lines = names.map((name) => `export { ${name} } from "./${name}";`);
184
+ // Parks the second walk past the end of the first barrel, so a shared regex position would end the first early.
185
+ const padding = pkgName === "second" ? [`// ${"-".repeat(200)}`] : [];
186
+ await write(path.join(root, pkgName, "index.ts"), [...padding, ...lines, ""].join("\n"));
187
+ for (const name of names) await write(path.join(root, pkgName, `${name}.ts`), `export const ${name} = 1;\n`);
188
+ }
189
+ const parked = { first: Promise.withResolvers<void>(), second: Promise.withResolvers<void>() };
190
+ const released = { first: Promise.withResolvers<void>(), second: Promise.withResolvers<void>() };
191
+ const analyzer = new BarrelAnalyzer({
192
+ resolvePackage: async (pkgName) => ({
193
+ pkgName,
194
+ entryFile: path.join(root, pkgName, "index.ts"),
195
+ pkgDir: path.join(root, pkgName),
196
+ }),
197
+ resolveRelative: async (fromFile, relSpec) => {
198
+ const pkgName = path.basename(path.dirname(fromFile)) as keyof typeof barrels;
199
+ if (relSpec === `./${barrels[pkgName][0]}`) {
200
+ parked[pkgName].resolve();
201
+ await released[pkgName].promise;
202
+ }
203
+ return path.join(path.dirname(fromFile), `${relSpec.slice(2)}.ts`);
204
+ },
205
+ });
206
+
207
+ const first = analyzer.analyze("first");
208
+ await parked.first.promise;
209
+ const second = analyzer.analyze("second");
210
+ await parked.second.promise;
211
+ released.first.resolve();
212
+ const firstMap = await first;
213
+ released.second.resolve();
214
+ const secondMap = await second;
215
+
216
+ expect(firstMap?.get("a3")).toEqual({ subpath: "first/a3", originalName: "a3" });
217
+ expect(secondMap?.get("b2")).toEqual({ subpath: "second/b2", originalName: "b2" });
218
+ });
219
+
100
220
  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;
221
+ const analyzer = fakeAnalyzer([
222
+ ["A", { subpath: "@scope/pkg/leaf", originalName: "A" }],
223
+ ["Bee", { subpath: "@scope/pkg/leaf", originalName: "B" }],
224
+ ]);
108
225
 
109
226
  const rewritten = await rewriteBarrelImports(
110
227
  'import DefaultExport, { A, Bee as LocalBee, type Shape, Missing } from "@scope/pkg";\nconsole.log(A);',
@@ -126,9 +243,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
126
243
  });
127
244
 
128
245
  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;
246
+ const analyzer = fakeAnalyzer([["st", { subpath: "@apps/demo/lib/st", originalName: "st" }]]);
132
247
 
133
248
  const rewritten = await rewriteBarrelImports(
134
249
  'import { st } from "@apps/demo/client";\nvoid st;\n',
@@ -142,9 +257,7 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
142
257
  });
143
258
 
144
259
  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;
260
+ const analyzer = fakeAnalyzer([["AkanApp", { subpath: "akanjs/server/akanApp", originalName: "AkanApp" }]]);
148
261
 
149
262
  const source = [
150
263
  'import { Code } from "@apps/docs/ui";',
@@ -164,13 +277,10 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
164
277
  });
165
278
 
166
279
  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;
280
+ const analyzer = fakeAnalyzer([
281
+ ["AkanOption", { subpath: "akanjs/server/akanOption", originalName: "AkanOption" }],
282
+ ["Try", { subpath: "akanjs/server/decorators", originalName: "Try" }],
283
+ ]);
174
284
 
175
285
  const rewritten = await rewriteBarrelImports(
176
286
  'import { AkanOption, Try } from "akanjs/server";\nexport const option = new AkanOption();\n',
@@ -188,10 +298,9 @@ describe("BarrelAnalyzer and rewriteBarrelImports", () => {
188
298
  });
189
299
 
190
300
  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;
301
+ const analyzer = fakeAnalyzer([
302
+ ["BottomInset", { subpath: "akanjs/ui/Layout/BottomInset", originalName: "BottomInset" }],
303
+ ]);
195
304
 
196
305
  const rewritten = await rewriteBarrelImports('import { BottomInset } from "akanjs/ui";\n', ["akanjs/ui"], analyzer);
197
306
 
@@ -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,21 +1,8 @@
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
- */
5
+ /** `workspaceRoot` makes reference keys workspace-relative; they must match the client manifest's keys. */
19
6
  export function createUseClientBundlePlugin(options: { workspaceRoot?: string } = {}): BunPlugin {
20
7
  return {
21
8
  name: "akan-use-client-bundle",
@@ -38,10 +25,3 @@ export function createUseClientBundlePlugin(options: { workspaceRoot?: string }
38
25
  },
39
26
  };
40
27
  }
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
- }