@akanjs/devkit 3.0.0-beta.9 → 3.0.1

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 (346) hide show
  1. package/CHANGELOG.md +1819 -0
  2. package/DEV_RUNTIME_KNOBS.md +44 -26
  3. package/README.ko.md +21 -7
  4. package/README.md +17 -8
  5. package/abstractDoc.test.ts +6 -15
  6. package/abstractDoc.ts +6 -8
  7. package/agentsIndex.test.ts +4 -7
  8. package/agentsIndex.ts +6 -22
  9. package/akanApp/BackendImportGraph.test.ts +8 -22
  10. package/akanApp/BackendImportGraph.ts +1 -20
  11. package/akanApp/akanApp.host.test.ts +56 -3
  12. package/akanApp/akanApp.host.ts +218 -278
  13. package/akanApp/devBootLatch.test.ts +39 -0
  14. package/akanApp/devBootLatch.ts +39 -0
  15. package/akanApp/devHostPolicy.test.ts +81 -35
  16. package/akanApp/devHostPolicy.ts +41 -102
  17. package/akanConfig/akanBin.test.ts +62 -0
  18. package/akanConfig/akanBin.ts +78 -0
  19. package/akanConfig/akanConfig.test.ts +439 -141
  20. package/akanConfig/akanConfig.ts +359 -275
  21. package/akanConfig/index.ts +1 -0
  22. package/akanConfig/types.ts +22 -8
  23. package/akanContext.ts +110 -160
  24. package/akanMcpContract.ts +28 -73
  25. package/appSelectionMemory.ts +1 -4
  26. package/applicationBuildReporter.ts +37 -5
  27. package/applicationBuildRunner.test.ts +41 -5
  28. package/applicationBuildRunner.ts +71 -63
  29. package/applicationTestPreload.ts +3 -12
  30. package/artifact/implicitRootLayout.test.ts +34 -31
  31. package/artifact/implicitRootLayout.ts +60 -107
  32. package/artifact/routeSeedIndex.test.ts +5 -18
  33. package/artifact/routeSeedIndex.ts +11 -44
  34. package/biome.base.json +39 -2
  35. package/biomeBase.ts +2 -5
  36. package/cloud/cloudApi.test.ts +202 -0
  37. package/cloud/cloudApi.ts +77 -39
  38. package/cloud/configLock.test.ts +64 -0
  39. package/cloud/configLock.ts +96 -0
  40. package/cloud/constants.ts +26 -5
  41. package/cloud/globalConfig.ts +71 -32
  42. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  43. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  44. package/codeAgent/agent/CodeAgent.ts +551 -0
  45. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  46. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  47. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  48. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  49. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  50. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  51. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  52. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  53. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  54. package/codeAgent/agent/CodeMailbox.ts +148 -0
  55. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  56. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  57. package/codeAgent/agent/SubagentPool.ts +214 -0
  58. package/codeAgent/agent/akanCodeModel.ts +47 -0
  59. package/codeAgent/agent/akanCodePaths.ts +31 -0
  60. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  61. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  62. package/codeAgent/agent/codeAgent.test.ts +178 -0
  63. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  64. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  65. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  66. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  67. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  68. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  69. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  70. package/codeAgent/agent/consoleToStderr.ts +8 -0
  71. package/codeAgent/agent/jsonLines.ts +29 -0
  72. package/codeAgent/agent/stringArg.ts +5 -0
  73. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  74. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  75. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  76. package/codeAgent/feedback/PreviewView.ts +95 -0
  77. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  78. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  79. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  80. package/codeAgent/index.ts +34 -0
  81. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  82. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  83. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  84. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  85. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  86. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  87. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  88. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  89. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  90. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  91. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  92. package/codeAgent/tools/AkanCli.ts +50 -0
  93. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  94. package/codeAgent/tools/AkanEditScope.ts +48 -0
  95. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  96. package/codeAgent/tools/AkanToolPack.ts +92 -0
  97. package/codeAgent/tools/AkanVerifier.ts +48 -0
  98. package/codeAgent/tools/AskToolPack.ts +99 -0
  99. package/codeAgent/tools/MailToolPack.ts +76 -0
  100. package/codeAgent/tools/McpClient.ts +187 -0
  101. package/codeAgent/tools/McpOAuth.ts +233 -0
  102. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  103. package/codeAgent/tools/McpServerConfig.ts +129 -0
  104. package/codeAgent/tools/McpSignIn.ts +158 -0
  105. package/codeAgent/tools/McpTokenStore.ts +81 -0
  106. package/codeAgent/tools/McpToolPack.ts +199 -0
  107. package/codeAgent/tools/SessionToolPack.ts +91 -0
  108. package/codeAgent/tools/WebToolPack.ts +147 -0
  109. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  110. package/codeAgent/tools/askToolPack.test.ts +116 -0
  111. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  112. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  113. package/codegenLock.test.ts +4 -13
  114. package/codegenLock.ts +3 -17
  115. package/commandDecorators/argMeta.ts +1 -5
  116. package/commandDecorators/camelToKebabCase.ts +1 -0
  117. package/commandDecorators/command.ts +46 -102
  118. package/commandDecorators/commandBuilder.ts +30 -61
  119. package/commandDecorators/commandDecorators.test.ts +2 -10
  120. package/commandDecorators/dependencyBuilder.ts +10 -1
  121. package/commandDecorators/helpFormatter.ts +68 -145
  122. package/commandDecorators/index.ts +0 -2
  123. package/commandDecorators/targetMeta.ts +9 -7
  124. package/createTunnel.ts +7 -14
  125. package/csrE2e/csrE2eHarness.fixture.ts +506 -0
  126. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  127. package/csrE2e/csrPageStack.e2e.test.ts +265 -0
  128. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  129. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  130. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  131. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  132. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  133. package/dependencyScanner.test.ts +4 -16
  134. package/dependencyScanner.ts +31 -80
  135. package/devkitUtils.test.ts +48 -20
  136. package/executors.linkFallback.test.ts +103 -0
  137. package/executors.test.ts +176 -106
  138. package/executors.ts +212 -498
  139. package/fileEditor.ts +9 -37
  140. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  141. package/fileSys.ts +22 -9
  142. package/formSetterScanner.ts +3 -16
  143. package/frontendBuild/allRoutesBuilder.ts +7 -21
  144. package/frontendBuild/autoImportSync.test.ts +27 -38
  145. package/frontendBuild/autoImportSync.ts +31 -65
  146. package/frontendBuild/buildRouteClient.test.ts +123 -37
  147. package/frontendBuild/bundleDefine.test.ts +12 -0
  148. package/frontendBuild/bundleDefine.ts +16 -0
  149. package/frontendBuild/clientBuildTypes.ts +5 -36
  150. package/frontendBuild/clientEntriesBundler.ts +9 -40
  151. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  152. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  153. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  154. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  155. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  156. package/frontendBuild/csrDevBundler.test.ts +66 -0
  157. package/frontendBuild/csrDevBundler.ts +95 -0
  158. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  159. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  160. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  161. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  162. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  163. package/frontendBuild/csrDevPatcher.ts +548 -0
  164. package/frontendBuild/csrDevPaths.ts +145 -0
  165. package/frontendBuild/csrDevResolver.test.ts +239 -0
  166. package/frontendBuild/csrDevResolver.ts +363 -0
  167. package/frontendBuild/csrDevTypes.ts +61 -0
  168. package/frontendBuild/csrEntryFiles.ts +24 -0
  169. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  170. package/frontendBuild/cssCandidateCache.ts +6 -41
  171. package/frontendBuild/cssCompiler.ts +31 -82
  172. package/frontendBuild/cssImportResolver.ts +6 -19
  173. package/frontendBuild/devChangePlanner.ts +7 -10
  174. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  175. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  176. package/frontendBuild/devRegistryBundler.ts +192 -0
  177. package/frontendBuild/fontOptimizer.test.ts +6 -14
  178. package/frontendBuild/fontOptimizer.ts +15 -72
  179. package/frontendBuild/fontPruner.test.ts +5 -10
  180. package/frontendBuild/fontPruner.ts +5 -20
  181. package/frontendBuild/frontendBuild.test.ts +57 -70
  182. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  183. package/frontendBuild/hmrWatcher.test.ts +59 -32
  184. package/frontendBuild/hmrWatcher.ts +37 -74
  185. package/frontendBuild/index.ts +4 -0
  186. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  187. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  188. package/frontendBuild/precompressArtifacts.ts +5 -18
  189. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  190. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  191. package/frontendBuild/routeClientBuilder.ts +100 -62
  192. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  193. package/frontendBuild/serverGraphFile.test.ts +77 -0
  194. package/frontendBuild/serverGraphFile.ts +93 -0
  195. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  196. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  197. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  198. package/frontendBuild/ssrDevBundler.ts +85 -0
  199. package/frontendBuild/styleContract.ts +1 -8
  200. package/frontendBuild/themeValidator.ts +4 -26
  201. package/frontendBuild/watchRootResolver.test.ts +23 -42
  202. package/frontendBuild/watchRootResolver.ts +5 -27
  203. package/getRelatedCnsts.ts +12 -92
  204. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  205. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  206. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  207. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  208. package/incrementalBuilder/builderChannel.test.ts +24 -49
  209. package/incrementalBuilder/builderChannel.ts +6 -37
  210. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  211. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  212. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  213. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  214. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  215. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  216. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  217. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  218. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  219. package/index.ts +5 -19
  220. package/integration/devResourceProbe.ts +50 -56
  221. package/integration/devStability.integration.test.ts +111 -202
  222. package/integration/devStabilityHarness.test.ts +13 -28
  223. package/integration/devStabilityHarness.ts +67 -330
  224. package/integration/ssrMemoryProbe.ts +41 -167
  225. package/libSource.test.ts +4 -15
  226. package/libSource.ts +19 -31
  227. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  228. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  229. package/lint/__fixtures__/no-static-in-object-light-model/bad.ts +43 -0
  230. package/lint/__fixtures__/no-static-in-object-light-model/fixture.json +1 -0
  231. package/lint/__fixtures__/no-static-in-object-light-model/good.ts +42 -0
  232. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  233. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  234. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  235. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  236. package/lint/gritRules.test.ts +8 -24
  237. package/lint/no-deep-internal-import.grit +2 -2
  238. package/lint/no-document-cookie.grit +29 -0
  239. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  240. package/lint/no-static-in-object-light-model.grit +27 -0
  241. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  242. package/lint/no-web-storage.grit +30 -0
  243. package/linter.test.ts +1 -2
  244. package/linter.ts +26 -116
  245. package/mobile/desktopBin.test.ts +182 -0
  246. package/mobile/desktopBin.ts +144 -0
  247. package/mobile/desktopServerStage.test.ts +252 -0
  248. package/mobile/desktopServerStage.ts +178 -0
  249. package/mobile/index.ts +9 -0
  250. package/mobile/mobileTarget.test.ts +57 -8
  251. package/mobile/mobileTarget.ts +45 -26
  252. package/mobile/nativeApi.test.ts +30 -0
  253. package/mobile/nativeApi.ts +40 -0
  254. package/mobile/nativeApp.test.ts +184 -0
  255. package/mobile/nativeApp.ts +454 -0
  256. package/mobile/nativeAppLine.test.ts +69 -0
  257. package/mobile/nativeAppLine.ts +71 -0
  258. package/mobile/nativeConfig.test.ts +438 -0
  259. package/mobile/nativeConfig.ts +294 -0
  260. package/mobile/nativePluginFolders.test.ts +81 -0
  261. package/mobile/nativePluginFolders.ts +60 -0
  262. package/mobile/nativeWebDir.test.ts +80 -0
  263. package/mobile/nativeWebDir.ts +66 -0
  264. package/mobile/placeholderAppId.test.ts +13 -0
  265. package/mobile/placeholderAppId.ts +16 -0
  266. package/mobile/usageDescriptions.test.ts +27 -0
  267. package/mobile/usageDescriptions.ts +24 -0
  268. package/package.json +11 -11
  269. package/packageExports.test.ts +3 -18
  270. package/packageExportsMap.ts +3 -19
  271. package/piDependencyPins.test.ts +46 -0
  272. package/platformTest/BunTestReport.test.ts +60 -0
  273. package/platformTest/BunTestReport.ts +65 -0
  274. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  275. package/platformTest/IdleWatchedProcess.ts +73 -0
  276. package/platformTest/LinuxDockerTarget.ts +105 -0
  277. package/platformTest/PlatformTestRun.test.ts +64 -0
  278. package/platformTest/PlatformTestRun.ts +141 -0
  279. package/platformTest/PlatformTestTarget.ts +142 -0
  280. package/platformTest/TestSnapshot.test.ts +72 -0
  281. package/platformTest/TestSnapshot.ts +103 -0
  282. package/platformTest/WindowsSshTarget.ts +151 -0
  283. package/prompter.ts +3 -13
  284. package/qualityScanner.test.ts +9 -26
  285. package/qualityScanner.ts +200 -286
  286. package/recipeScanner.test.ts +7 -12
  287. package/recipeScanner.ts +9 -38
  288. package/repoIdentity.ts +2 -9
  289. package/routeSourceValidator.test.ts +0 -1
  290. package/routeSourceValidator.ts +7 -42
  291. package/scanInfo.ts +91 -225
  292. package/selectModel.ts +1 -2
  293. package/semver.ts +3 -5
  294. package/slicePlanner.test.ts +6 -24
  295. package/slicePlanner.ts +11 -32
  296. package/spinner.test.ts +3 -8
  297. package/spinner.ts +4 -16
  298. package/sshTunnel.ts +1 -6
  299. package/ssrScanner.test.ts +1 -6
  300. package/ssrScanner.ts +103 -113
  301. package/stripAnsi.ts +3 -0
  302. package/subspace.test.ts +19 -26
  303. package/subspace.ts +48 -115
  304. package/subspaceConfig.ts +3 -11
  305. package/testHelpers.ts +194 -0
  306. package/transforms/asyncDefaultExportDetector.ts +3 -11
  307. package/transforms/barrelAnalyzer.ts +43 -135
  308. package/transforms/barrelImportsPlugin.ts +38 -175
  309. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  310. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  311. package/transforms/moduleSyntax.ts +32 -0
  312. package/transforms/rscUseClientTransform.ts +42 -20
  313. package/transforms/serverEnvPlugin.test.ts +103 -0
  314. package/transforms/serverEnvPlugin.ts +41 -0
  315. package/transforms/transforms.test.ts +166 -44
  316. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  317. package/transforms/useClientBundlePlugin.ts +6 -23
  318. package/typeChecker.ts +7 -63
  319. package/types.ts +2 -0
  320. package/uploadRelease.ts +10 -25
  321. package/workflow/artifacts.ts +18 -0
  322. package/workflow/executor.test.ts +1 -12
  323. package/workflow/executor.ts +101 -110
  324. package/workflow/index.ts +0 -1
  325. package/workflow/moduleIndex.test.ts +4 -15
  326. package/workflow/moduleIndex.ts +9 -49
  327. package/workflow/plan.ts +18 -25
  328. package/workflow/render.ts +80 -96
  329. package/workflow/rolloutGate.test.ts +1 -1
  330. package/workflow/source.ts +52 -69
  331. package/workflow/sourceAst.ts +50 -0
  332. package/workspaceLayout.test.ts +18 -3
  333. package/workspaceLayout.ts +22 -23
  334. package/abstractCompactor.ts +0 -112
  335. package/aiEditor.test.ts +0 -60
  336. package/aiEditor.ts +0 -387
  337. package/applicationReleasePackager.ts +0 -209
  338. package/capacitorApp.test.ts +0 -372
  339. package/capacitorApp.ts +0 -1648
  340. package/commandDecorators/commandMeta.ts +0 -7
  341. package/commandDecorators/types.ts +0 -10
  342. package/getDirname.ts +0 -4
  343. package/guideline.ts +0 -19
  344. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  345. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  346. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,178 @@
1
+ import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { AkanAppConfig, NativeEnv } from "../akanConfig";
4
+ import type { App } from "../commandDecorators";
5
+ import type { PackageJson } from "../types";
6
+ import { DesktopBin } from "./desktopBin";
7
+
8
+ /** What a desktop build hands the native runtime as `desktop.server`. */
9
+ export interface DesktopServerBundle {
10
+ dir: string;
11
+ entry: string;
12
+ env: Record<string, string>;
13
+ }
14
+
15
+ //* A desktop app's carried server is the backend `akan build` wrote into dist, installed on its own as the image's
16
+ //* `bun install --production` does. It is staged outside dist: installing there would put host packages into the
17
+ //* image's `COPY . .`.
18
+ export class DesktopServerStage {
19
+ //? An API-only server starts neither the RSC worker nor the akan console and serves no CSR, public/ or SSR artifact,
20
+ //? and `install` wrote its own package.json. The rest of dist is the server's: its chunks, a `.node`, a `.wasm`.
21
+ static readonly uncarried = new Set([
22
+ "Dockerfile",
23
+ "package.json",
24
+ "rscWorker.js",
25
+ "console.js",
26
+ "console-runtime.js",
27
+ "csr",
28
+ "public",
29
+ ".akan",
30
+ ]);
31
+ //? Only the RSC worker loads it, and its peer webpack makes 35 of the 43 MB an install would add.
32
+ static readonly rscRenderer = "react-server-dom-webpack";
33
+
34
+ readonly dir: string;
35
+
36
+ constructor(readonly app: App) {
37
+ this.dir = path.join(app.cwdPath, ".akan", "desktop", "server");
38
+ }
39
+
40
+ static assertCarriable(config: AkanAppConfig) {
41
+ if (!config.database.modes.includes("single"))
42
+ throw new Error(
43
+ `desktop.server in native puts the server in the app, where only database mode single runs (no Redis or Postgres); apps/${config.app.name}/akan.config.ts declares ${config.database.modes.join(", ")}.`,
44
+ );
45
+ }
46
+
47
+ static env(config: AkanAppConfig, environment: NativeEnv): Record<string, string> {
48
+ //? The launcher picks the port, and the log file stays on: a user's computer has no log collector.
49
+ const { PORT: _port, AKAN_LOG_TO_FILE: _fileLog, ...image } = config.getProductionEnv(environment);
50
+ return {
51
+ ...image,
52
+ //? Neither the developer's machine (local, which derives a forgeable JWT secret) nor a deployment (cloud).
53
+ AKAN_PUBLIC_OPERATION_MODE: "edge",
54
+ AKAN_DATABASE_MODE: "single",
55
+ AKAN_DATABASE_MODES: "single",
56
+ //? API only: the shell serves the CSR bundle, and an RSC worker would need a bun on the user's PATH.
57
+ AKAN_SSR: "false",
58
+ AKAN_CSR: "false",
59
+ AKAN_MCP: "false",
60
+ //? Ahead of the launcher's 1.5 s grace before it signals, which sits inside the shell's 2 s quit budget.
61
+ AKAN_SHUTDOWN_TIMEOUT_MS: "1000",
62
+ };
63
+ }
64
+
65
+ //? The image's steps install into a Linux image the desktop app never runs in, so its server has none of it.
66
+ static imageStepsNotice(
67
+ config: Pick<AkanAppConfig, "app" | "docker">,
68
+ carried: string[],
69
+ ): { level: "warn" | "info"; message: string } | null {
70
+ const steps =
71
+ typeof config.docker === "string" ? null : config.docker.preRuns.length + config.docker.postRuns.length;
72
+ if (steps === 0) return null;
73
+ const image =
74
+ steps === null
75
+ ? `apps/${config.app.name}/akan.config.ts writes its own Dockerfile`
76
+ : `The image runs ${steps} docker step${steps === 1 ? "" : "s"} from the app and its libs`;
77
+ return carried.length
78
+ ? {
79
+ level: "info",
80
+ message: `${image}; the desktop app's server runs none of them and carries only bin: ${carried.join(", ")}.`,
81
+ }
82
+ : {
83
+ level: "warn",
84
+ message: `${image}, and a desktop app's server runs none of them: declare in bin the executables its code spawns, or those calls fail on the user's computer.`,
85
+ };
86
+ }
87
+
88
+ //? `omit` (the target's `desktop.server.omit`) wins over externalLibs: the image keeps those packages, the app does not.
89
+ static packageJson(config: AkanAppConfig, built: PackageJson, omit: string[] = []): PackageJson {
90
+ const otherDrivers = new Set(
91
+ config.database.modes
92
+ .filter((mode) => mode !== "single")
93
+ .flatMap((mode) => config.getDatabaseModeRuntimePackages(mode)),
94
+ );
95
+ const dependencies = Object.entries(built.dependencies ?? {}).filter(
96
+ ([name]) =>
97
+ !omit.includes(name) &&
98
+ (config.externalLibs.includes(name) || (name !== DesktopServerStage.rscRenderer && !otherDrivers.has(name))),
99
+ );
100
+ return { ...built, dependencies: Object.fromEntries(dependencies) };
101
+ }
102
+
103
+ /** The packages in `nodeModules` that depend on `name`: what still pulls an omitted package in. */
104
+ static async dependentsOf(nodeModules: string, name: string): Promise<string[]> {
105
+ const dependents = new Set<string>();
106
+ for await (const manifest of new Bun.Glob("**/package.json").scan({ cwd: nodeModules, onlyFiles: true })) {
107
+ const json = (await Bun.file(path.join(nodeModules, manifest))
108
+ .json()
109
+ .catch(() => null)) as PackageJson | null;
110
+ if (!json?.name || json.name === name) continue;
111
+ const declared = { ...json.dependencies, ...json.optionalDependencies, ...json.peerDependencies };
112
+ if (name in declared) dependents.add(json.name);
113
+ }
114
+ return [...dependents].sort();
115
+ }
116
+
117
+ //? `--cpu` installs another CPU's optional packages: an addon's prebuilt binary for the app's CPU, not this computer's.
118
+ static installArgs(arch?: "arm64" | "x64"): string[] {
119
+ return ["install", "--production", "--prefer-offline", ...(arch && arch !== process.arch ? [`--cpu=${arch}`] : [])];
120
+ }
121
+
122
+ //* The packages come from the config alone, so they install before `akan build` runs: a machine that cannot install
123
+ //* them stops at once instead of after the whole build.
124
+ async install(arch?: "arm64" | "x64", omit: string[] = []) {
125
+ const config = await this.app.getConfig();
126
+ DesktopServerStage.assertCarriable(config);
127
+ await rm(this.dir, { recursive: true, force: true });
128
+ await mkdir(this.dir, { recursive: true });
129
+ const built = config.getProductionPackageJson();
130
+ for (const name of omit.filter((name) => !(name in (built.dependencies ?? {}))))
131
+ this.app.logger.warn(`desktop.server.omit names ${name}, which the server does not depend on directly.`);
132
+ await writeFile(
133
+ path.join(this.dir, "package.json"),
134
+ JSON.stringify(DesktopServerStage.packageJson(config, built, omit), null, 2),
135
+ );
136
+ try {
137
+ await this.app.spawn(process.execPath, DesktopServerStage.installArgs(arch), { cwd: this.dir });
138
+ } catch (error) {
139
+ throw new Error(
140
+ `The desktop app's server could not install its packages in ${this.dir}: it needs the npm registry, or a Bun cache that already holds every one of them.\n${error instanceof Error ? error.message : String(error)}`,
141
+ { cause: error },
142
+ );
143
+ }
144
+ //? Launcher links only: a link out of the app bundle breaks its signature, and the server runs none of them.
145
+ await rm(path.join(this.dir, "node_modules", ".bin"), { recursive: true, force: true });
146
+ await this.#assertOmitted(omit);
147
+ }
148
+
149
+ //? Leaving a package out of package.json keeps out only what nothing else needs; another dependency still installs it.
150
+ async #assertOmitted(omit: string[]) {
151
+ const nodeModules = path.join(this.dir, "node_modules");
152
+ const kept: string[] = [];
153
+ for (const name of omit) {
154
+ if (!(await Bun.file(path.join(nodeModules, name, "package.json")).exists())) continue;
155
+ const dependents = await DesktopServerStage.dependentsOf(nodeModules, name);
156
+ kept.push(`${name} (needed by ${dependents.join(", ") || "a package that names it"})`);
157
+ }
158
+ if (kept.length)
159
+ throw new Error(
160
+ `desktop.server.omit leaves out packages another dependency still installs: ${kept.join("; ")}. Omit those too, or keep the package.`,
161
+ );
162
+ }
163
+
164
+ //? Copies the build beside what `install` put in the stage, so it runs after both.
165
+ async prepare(environment: NativeEnv): Promise<DesktopServerBundle> {
166
+ const config = await this.app.getConfig();
167
+ const dist = this.app.dist.cwdPath;
168
+ await mkdir(this.dir, { recursive: true });
169
+ for (const name of await readdir(dist))
170
+ if (!DesktopServerStage.uncarried.has(name))
171
+ await cp(path.join(dist, name), path.join(this.dir, name), { recursive: true });
172
+ const scanInfo = this.app.getScanInfo({ allowEmpty: true }) ?? (await this.app.scan({ write: false }));
173
+ const carried = [...DesktopBin.select(config, scanInfo.getLibs()).keys()];
174
+ const notice = DesktopServerStage.imageStepsNotice(config, carried);
175
+ if (notice) this.app.logger[notice.level](notice.message);
176
+ return { dir: this.dir, entry: "main.js", env: DesktopServerStage.env(config, environment) };
177
+ }
178
+ }
package/mobile/index.ts CHANGED
@@ -1 +1,10 @@
1
+ export * from "./desktopBin";
2
+ export * from "./desktopServerStage";
1
3
  export * from "./mobileTarget";
4
+ export * from "./nativeApi";
5
+ export * from "./nativeApp";
6
+ export * from "./nativeConfig";
7
+ export * from "./nativePluginFolders";
8
+ export * from "./nativeWebDir";
9
+ export * from "./placeholderAppId";
10
+ export * from "./usageDescriptions";
@@ -1,7 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import type { AkanMobileTargetConfig } from "../akanConfig";
2
+ import type { AkanNativeTarget } from "../akanConfig";
3
3
  import type { App } from "../commandDecorators";
4
- import { getMobileTargetChoices, resolveMobilePath, resolveMobileTargets, targetHtmlFilename } from "./mobileTarget";
4
+ import {
5
+ appIdsOf,
6
+ getMobileTargetChoices,
7
+ resolveAppId,
8
+ resolveMobilePath,
9
+ resolveMobileTargets,
10
+ targetHtmlFilename,
11
+ } from "./mobileTarget";
5
12
 
6
13
  const target = {
7
14
  name: "akanjs",
@@ -10,25 +17,36 @@ const target = {
10
17
  appId: "com.akanjs.app",
11
18
  version: "1.0.0",
12
19
  buildNum: 1,
13
- } as AkanMobileTargetConfig;
20
+ } as AkanNativeTarget;
14
21
 
15
- describe("mobile target helpers", () => {
22
+ describe("native target helpers", () => {
16
23
  test("maps deep links into target base paths without duplicating prefixes", () => {
17
24
  expect(resolveMobilePath(target, "/order/123")).toBe("/akanjs/order/123");
18
25
  expect(resolveMobilePath(target, "/akanjs/order/123")).toBe("/akanjs/order/123");
19
26
  expect(resolveMobilePath({ ...target, basePath: undefined }, "/order/123")).toBe("/order/123");
20
27
  });
21
28
 
29
+ test("an app id per platform falls back to default, and a platform with neither is refused", () => {
30
+ const appId = { default: "com.yeollege", ios: "com.puffinplanet.yeollege" };
31
+ expect(resolveAppId(appId, "ios")).toBe("com.puffinplanet.yeollege");
32
+ expect(resolveAppId(appId, "android")).toBe("com.yeollege");
33
+ expect(resolveAppId("com.one.app", "macos")).toBe("com.one.app");
34
+ expect(() => resolveAppId({ ios: "com.puffinplanet.yeollege" }, "android")).toThrow(
35
+ "native.appId names no id for android",
36
+ );
37
+ expect(appIdsOf(appId)).toEqual(["com.yeollege", "com.puffinplanet.yeollege"]);
38
+ });
39
+
22
40
  test("selects the self-contained html file for a target", () => {
23
41
  expect(targetHtmlFilename(target)).toBe("akanjs.html");
24
42
  expect(targetHtmlFilename({ ...target, basePath: undefined })).toBe("index.html");
25
43
  });
26
44
 
27
- test("uses the configured mobile target as the only choice when one target is configured", async () => {
45
+ test("uses the configured native target as the only choice when one target is configured", async () => {
28
46
  const app = {
29
47
  getConfig: async () => ({
30
48
  basePaths: new Set(["akanjs", "soft", "office"]),
31
- mobile: { targets: { akanjs: target } },
49
+ native: { targets: { akanjs: target } },
32
50
  }),
33
51
  } as unknown as App;
34
52
 
@@ -36,11 +54,42 @@ describe("mobile target helpers", () => {
36
54
  await expect(resolveMobileTargets(app, undefined)).resolves.toEqual([{ name: "akanjs", config: target }]);
37
55
  });
38
56
 
39
- test("resolves a route base path onto the default mobile target config", async () => {
57
+ test("a target naming no basePath is a template in an app with basePaths, never a page of its own", async () => {
58
+ const template = { ...target, name: "default", basePath: undefined };
59
+ const appWith = (basePaths: string[]) =>
60
+ ({
61
+ name: "angelo",
62
+ getConfig: async () => ({ basePaths: new Set(basePaths), native: { targets: { default: template } } }),
63
+ }) as unknown as App;
64
+
65
+ const two = appWith(["soft", "office"]);
66
+ await expect(getMobileTargetChoices(two)).resolves.toEqual(["soft", "office"]);
67
+ await expect(resolveMobileTargets(two, undefined)).rejects.toThrow(
68
+ "Multiple native targets found for angelo. Pass --target <soft|office|all>.",
69
+ );
70
+ await expect(resolveMobileTargets(two, "office")).resolves.toEqual([
71
+ { name: "office", config: { ...template, name: "office", basePath: "office" } },
72
+ ]);
73
+ await expect(resolveMobileTargets(two, "all")).resolves.toEqual([
74
+ { name: "soft", config: { ...template, name: "soft", basePath: "soft" } },
75
+ { name: "office", config: { ...template, name: "office", basePath: "office" } },
76
+ ]);
77
+
78
+ const one = appWith(["soft"]);
79
+ const soft = [{ name: "soft", config: { ...template, name: "soft", basePath: "soft" } }];
80
+ await expect(resolveMobileTargets(one, undefined)).resolves.toEqual(soft);
81
+ await expect(resolveMobileTargets(one, "all")).resolves.toEqual(soft);
82
+
83
+ await expect(resolveMobileTargets(appWith([]), undefined)).resolves.toEqual([
84
+ { name: "default", config: template },
85
+ ]);
86
+ });
87
+
88
+ test("resolves a route base path onto the default native target config", async () => {
40
89
  const app = {
41
90
  getConfig: async () => ({
42
91
  basePaths: new Set(["akanjs", "soft", "office"]),
43
- mobile: { targets: { akanjs: target } },
92
+ native: { targets: { akanjs: target } },
44
93
  }),
45
94
  } as unknown as App;
46
95
 
@@ -1,45 +1,52 @@
1
- import type { AkanMobileTargetConfig, MobileEnv } from "../akanConfig";
1
+ import type { AkanNativeAppId } from "akanjs";
2
+ import type { AkanAppConfig, AkanNativeTarget, NativeEnv } from "../akanConfig";
2
3
  import type { App } from "../commandDecorators";
3
4
 
4
5
  export type MobilePlatform = "ios" | "android";
6
+ export type DesktopPlatform = "macos" | "windows" | "linux";
7
+ export type NativePlatform = MobilePlatform | DesktopPlatform;
5
8
  export type MobileTargetSelection = string | "all" | undefined;
6
9
 
7
10
  export interface ResolvedMobileTarget {
8
11
  name: string;
9
- config: AkanMobileTargetConfig;
12
+ config: AkanNativeTarget;
10
13
  }
11
14
 
12
- export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly MobileEnv[];
15
+ export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly NativeEnv[];
16
+
17
+ const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, "");
13
18
 
14
19
  export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]> => {
15
20
  const config = await app.getConfig();
16
- return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
21
+ return Object.entries(config.native.targets).map(([name, target]) => ({ name, config: target }));
17
22
  };
18
23
 
24
+ //* An app with basePaths builds a CSR page per basePath and none at its root, so a target naming no basePath opens
25
+ //* nothing on its own there; it is the template each basePath's target is made from.
26
+ const isTemplateOnly = ({ basePaths, native }: Pick<AkanAppConfig, "basePaths" | "native">) =>
27
+ basePaths.size > 0 && Object.values(native.targets).every((target) => !target.basePath);
28
+
19
29
  export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
20
30
  const config = await app.getConfig();
21
- const targetNames = Object.keys(config.mobile.targets);
22
- if (targetNames.length > 0) return targetNames;
23
- const basePaths = [...config.basePaths];
24
- return basePaths;
31
+ const targetNames = Object.keys(config.native.targets);
32
+ if (targetNames.length > 0 && !isTemplateOnly(config)) return targetNames;
33
+ return [...config.basePaths];
25
34
  };
26
35
 
27
36
  const resolveMobileTargetByBasePath = (
28
37
  targets: ResolvedMobileTarget[],
29
38
  basePath: string,
30
39
  ): ResolvedMobileTarget | undefined => {
31
- const normalizedBasePath = basePath.replace(/^\/+|\/+$/g, "");
32
- const byBasePath = targets.find((target) => target.config.basePath?.replace(/^\/+|\/+$/g, "") === normalizedBasePath);
40
+ const normalizedBasePath = trimSlashes(basePath);
41
+ const byBasePath = targets.find(
42
+ ({ config }) => config.basePath !== undefined && trimSlashes(config.basePath) === normalizedBasePath,
43
+ );
33
44
  if (byBasePath) return byBasePath;
34
45
  const [template] = targets;
35
46
  if (!template) return undefined;
36
47
  return {
37
48
  name: normalizedBasePath,
38
- config: {
39
- ...template.config,
40
- name: normalizedBasePath,
41
- basePath: normalizedBasePath,
42
- },
49
+ config: { ...template.config, name: normalizedBasePath, basePath: normalizedBasePath },
43
50
  };
44
51
  };
45
52
 
@@ -49,17 +56,17 @@ export const resolveMobileTargets = async (
49
56
  ): Promise<ResolvedMobileTarget[]> => {
50
57
  const config = await app.getConfig();
51
58
  const targets = await getMobileTargets(app);
52
- if (targets.length === 0) throw new Error(`No mobile targets configured for ${app.name}`);
53
- if (!selection && targets.length === 1) return targets;
59
+ if (targets.length === 0) throw new Error(`No native targets configured for ${app.name}`);
60
+ if (!selection && targets.length === 1 && !isTemplateOnly(config)) return targets;
54
61
  if (!selection) {
55
62
  const choices = await getMobileTargetChoices(app);
56
63
  if (choices.length === 1) return resolveMobileTargets(app, choices[0]);
57
- throw new Error(`Multiple mobile targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
64
+ throw new Error(`Multiple native targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
58
65
  }
59
66
  if (selection === "all") {
60
- if (Object.keys(config.mobile.targets).length > 1) return targets;
67
+ if (Object.keys(config.native.targets).length > 1) return targets;
61
68
  const basePaths = [...config.basePaths];
62
- if (basePaths.length > 1) {
69
+ if (basePaths.length > 1 || isTemplateOnly(config)) {
63
70
  return basePaths.flatMap((basePath) => {
64
71
  const resolved = resolveMobileTargetByBasePath(targets, basePath);
65
72
  return resolved ? [resolved] : [];
@@ -70,18 +77,30 @@ export const resolveMobileTargets = async (
70
77
  const target = targets.find((candidate) => candidate.name === selection);
71
78
  if (target) return [target];
72
79
  const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
73
- if (basePathTarget && config.basePaths.has(selection.replace(/^\/+|\/+$/g, ""))) return [basePathTarget];
80
+ if (basePathTarget && config.basePaths.has(trimSlashes(selection))) return [basePathTarget];
74
81
  const choices = await getMobileTargetChoices(app);
75
- throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
82
+ throw new Error(`Native target '${selection}' was not found. Available: ${choices.join(", ")}`);
83
+ };
84
+
85
+ export const resolveAppId = (appId: AkanNativeAppId, platform: NativePlatform): string => {
86
+ if (typeof appId === "string") return appId;
87
+ const id = appId[platform] ?? appId.default;
88
+ if (!id) throw new Error(`native.appId names no id for ${platform}; give it ${platform} or default.`);
89
+ return id;
76
90
  };
77
91
 
78
- export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: string) => {
79
- const basePath = target.basePath?.replace(/^\/+|\/+$/g, "");
92
+ export const appIdsOf = (appId: AkanNativeAppId): string[] =>
93
+ typeof appId === "string" ? [appId] : Object.values(appId).filter((id): id is string => !!id);
94
+
95
+ export const resolveMobilePath = (target: AkanNativeTarget, pathname: string) => {
96
+ const basePath = trimSlashes(target.basePath ?? "");
80
97
  const normalizedPath = `/${pathname.replace(/^\/+/, "")}`;
81
98
  if (!basePath) return normalizedPath;
82
99
  if (normalizedPath === `/${basePath}` || normalizedPath.startsWith(`/${basePath}/`)) return normalizedPath;
83
100
  return `/${basePath}${normalizedPath === "/" ? "" : normalizedPath}`;
84
101
  };
85
102
 
86
- export const targetHtmlFilename = (target: AkanMobileTargetConfig) =>
87
- target.basePath?.replace(/^\/+|\/+$/g, "") ? `${target.basePath.replace(/^\/+|\/+$/g, "")}.html` : "index.html";
103
+ export const targetHtmlFilename = (target: AkanNativeTarget) => {
104
+ const basePath = trimSlashes(target.basePath ?? "");
105
+ return basePath ? `${basePath}.html` : "index.html";
106
+ };
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdir, realpath, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tempDirs } from "../testHelpers";
5
+ import { NativeApi } from "./nativeApi";
6
+
7
+ const makeTempRoot = tempDirs("akan-native-api-");
8
+ const repoApp = path.resolve(import.meta.dir, "../../../../apps/minimal");
9
+
10
+ describe("NativeApi", () => {
11
+ test("inside this repo, loads the workspace package and checks its major", async () => {
12
+ expect(NativeApi.entryOf(repoApp)).toBe(path.resolve(import.meta.dir, "../../native/packages/cli/src/api.ts"));
13
+ const api = await NativeApi.load(repoApp);
14
+ expect(api.API_VERSION.split(".")[0]).toBe(String(NativeApi.supportedMajor));
15
+ });
16
+
17
+ test("elsewhere, looks for the copy akanjs vendors, and says so when it is missing", async () => {
18
+ const root = await realpath(await makeTempRoot());
19
+ await mkdir(path.join(root, "node_modules/akanjs"), { recursive: true });
20
+ await writeFile(
21
+ path.join(root, "node_modules/akanjs/package.json"),
22
+ JSON.stringify({ name: "akanjs", exports: { "./package.json": "./package.json" } }),
23
+ );
24
+
25
+ expect(NativeApi.entryOf(root)).toBe(
26
+ path.join(root, "node_modules/akanjs/vendor/@akanjs/native/packages/cli/src/api.ts"),
27
+ );
28
+ await expect(NativeApi.load(root)).rejects.toThrow("The installed akanjs has no native build");
29
+ });
30
+ });
@@ -0,0 +1,40 @@
1
+ import path from "node:path";
2
+ import type * as NativeBuildApi from "@akanjs/native/api";
3
+
4
+ export type NativeBuildApiModule = typeof NativeBuildApi;
5
+
6
+ export class NativeApi {
7
+ //* The build API versions itself; a major this devkit was not written against is refused before anything builds.
8
+ static readonly supportedMajor = 0;
9
+
10
+ /** The API of the akanjs the app installed: the workspace package inside this repo, akanjs's vendored copy elsewhere. */
11
+ static entryOf(appDir: string): string {
12
+ const workspaceEntry = NativeApi.#resolve("@akanjs/native/api", appDir);
13
+ if (workspaceEntry) return workspaceEntry;
14
+ const akanjsPackage = NativeApi.#resolve("akanjs/package.json", appDir);
15
+ if (!akanjsPackage) throw new Error(`akanjs is not installed for ${appDir}; the native build ships inside it.`);
16
+ return path.join(path.dirname(akanjsPackage), "vendor", "@akanjs", "native", "packages", "cli", "src", "api.ts");
17
+ }
18
+
19
+ static async load(appDir: string): Promise<NativeBuildApiModule> {
20
+ const entry = NativeApi.entryOf(appDir);
21
+ if (!(await Bun.file(entry).exists()))
22
+ throw new Error(`The installed akanjs has no native build at ${entry}; update akanjs.`);
23
+ const api = (await import(entry)) as NativeBuildApiModule;
24
+ const major = Number(api.API_VERSION.split(".")[0]);
25
+ if (major !== NativeApi.supportedMajor)
26
+ throw new Error(
27
+ `The native build API is ${api.API_VERSION}, and this akan CLI speaks ${NativeApi.supportedMajor}.x; install matching versions of akanjs and @akanjs/cli.`,
28
+ );
29
+ return api;
30
+ }
31
+
32
+ static #resolve(specifier: string, from: string) {
33
+ try {
34
+ return Bun.resolveSync(specifier, from);
35
+ } catch {
36
+ // Not installed where the app can reach it, which the caller answers with the next place to look.
37
+ return null;
38
+ }
39
+ }
40
+ }