@belte/belte 0.19.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 (326) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +21 -0
  3. package/README.md +559 -0
  4. package/bin/belte.ts +183 -0
  5. package/package.json +110 -0
  6. package/src/App.svelte +31 -0
  7. package/src/appEntry.ts +151 -0
  8. package/src/assets/app.html +14 -0
  9. package/src/belteResolverPlugin.ts +858 -0
  10. package/src/build.ts +147 -0
  11. package/src/buildCli.ts +129 -0
  12. package/src/buildDisconnected.ts +122 -0
  13. package/src/bundleApp.ts +149 -0
  14. package/src/bundleDisconnectedEntry.ts +17 -0
  15. package/src/cliEntry.ts +25 -0
  16. package/src/clientBuildPlugins.ts +41 -0
  17. package/src/clientEntry.ts +7 -0
  18. package/src/compile.ts +64 -0
  19. package/src/controlServerWorker.ts +422 -0
  20. package/src/dedupeSveltePlugin.ts +66 -0
  21. package/src/devEntry.ts +169 -0
  22. package/src/discoveryEntry.ts +81 -0
  23. package/src/lib/browser/applyStreamedResolution.ts +33 -0
  24. package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
  25. package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
  26. package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
  27. package/src/lib/browser/openResolveStream.ts +42 -0
  28. package/src/lib/browser/page.svelte.ts +258 -0
  29. package/src/lib/browser/pageStreamController.ts +17 -0
  30. package/src/lib/browser/refetchPlaceholder.ts +12 -0
  31. package/src/lib/browser/remoteProxy.ts +37 -0
  32. package/src/lib/browser/socketChannel.ts +192 -0
  33. package/src/lib/browser/socketProxy.ts +57 -0
  34. package/src/lib/browser/startClient.ts +153 -0
  35. package/src/lib/browser/subscribe.ts +131 -0
  36. package/src/lib/browser/types/Errors.ts +9 -0
  37. package/src/lib/browser/types/Layouts.ts +7 -0
  38. package/src/lib/browser/types/Pages.ts +7 -0
  39. package/src/lib/browser/types/StreamingDeferred.ts +9 -0
  40. package/src/lib/bundle/BundleMenu.ts +11 -0
  41. package/src/lib/bundle/BundleMenuItem.ts +24 -0
  42. package/src/lib/bundle/BundleWindow.ts +36 -0
  43. package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
  44. package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
  45. package/src/lib/bundle/bindConnectedFlag.ts +29 -0
  46. package/src/lib/bundle/bindRequestNavigate.ts +31 -0
  47. package/src/lib/bundle/buildWebviewLib.ts +111 -0
  48. package/src/lib/bundle/disconnected.css +9 -0
  49. package/src/lib/bundle/disconnected.svelte +386 -0
  50. package/src/lib/bundle/ensureWebviewLib.ts +20 -0
  51. package/src/lib/bundle/exitWithParent.ts +28 -0
  52. package/src/lib/bundle/infoPlist.ts +46 -0
  53. package/src/lib/bundle/installDownloads.ts +24 -0
  54. package/src/lib/bundle/installMacMenu.ts +39 -0
  55. package/src/lib/bundle/listenLocalControlServer.ts +19 -0
  56. package/src/lib/bundle/native/belteMenu.mm +422 -0
  57. package/src/lib/bundle/native/webview.h +4557 -0
  58. package/src/lib/bundle/onMenu.ts +41 -0
  59. package/src/lib/bundle/openWebview.ts +104 -0
  60. package/src/lib/bundle/pngToIcns.ts +47 -0
  61. package/src/lib/bundle/probeBelteServer.ts +34 -0
  62. package/src/lib/bundle/resolveServerBinary.ts +12 -0
  63. package/src/lib/bundle/resolveWebviewLib.ts +53 -0
  64. package/src/lib/bundle/serverBinaryFilename.ts +8 -0
  65. package/src/lib/bundle/signMacApp.ts +35 -0
  66. package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
  67. package/src/lib/bundle/stableLocalPort.ts +19 -0
  68. package/src/lib/bundle/waitForServer.ts +23 -0
  69. package/src/lib/bundle/webviewCachePath.ts +23 -0
  70. package/src/lib/bundle/webviewLibName.ts +11 -0
  71. package/src/lib/cli/connectToServer.ts +23 -0
  72. package/src/lib/cli/createClient.ts +170 -0
  73. package/src/lib/cli/dispatchCommand.ts +71 -0
  74. package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
  75. package/src/lib/cli/parseArgvForRpc.ts +97 -0
  76. package/src/lib/cli/printHelp.ts +119 -0
  77. package/src/lib/cli/printSessionHelp.ts +27 -0
  78. package/src/lib/cli/printSessionStatus.ts +21 -0
  79. package/src/lib/cli/printTrimmed.ts +8 -0
  80. package/src/lib/cli/printValue.ts +10 -0
  81. package/src/lib/cli/resolveCliTarget.ts +48 -0
  82. package/src/lib/cli/runCli.ts +139 -0
  83. package/src/lib/cli/runSession.ts +105 -0
  84. package/src/lib/cli/startLocalInstance.ts +14 -0
  85. package/src/lib/cli/tokenizeLine.ts +51 -0
  86. package/src/lib/cli/types/CliManifest.ts +9 -0
  87. package/src/lib/cli/types/CliManifestEntry.ts +17 -0
  88. package/src/lib/cli/types/CliTarget.ts +13 -0
  89. package/src/lib/mcp/annotationsForMethod.ts +29 -0
  90. package/src/lib/mcp/createMcpResourceServer.ts +101 -0
  91. package/src/lib/mcp/createMcpServer.ts +42 -0
  92. package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
  93. package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
  94. package/src/lib/mcp/mcpSurface.ts +265 -0
  95. package/src/lib/mcp/toolResultFromResponse.ts +66 -0
  96. package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
  97. package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
  98. package/src/lib/mcp/types/McpResourceContents.ts +10 -0
  99. package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
  100. package/src/lib/mcp/types/McpResourceServer.ts +12 -0
  101. package/src/lib/mcp/types/McpServer.ts +9 -0
  102. package/src/lib/mcp/types/McpServerOptions.ts +16 -0
  103. package/src/lib/server/AppModule.ts +33 -0
  104. package/src/lib/server/DELETE.ts +9 -0
  105. package/src/lib/server/GET.ts +9 -0
  106. package/src/lib/server/HEAD.ts +9 -0
  107. package/src/lib/server/PATCH.ts +9 -0
  108. package/src/lib/server/POST.ts +9 -0
  109. package/src/lib/server/PUT.ts +9 -0
  110. package/src/lib/server/agent.ts +76 -0
  111. package/src/lib/server/appDataDir.ts +15 -0
  112. package/src/lib/server/cli/buildEnvContent.ts +19 -0
  113. package/src/lib/server/cli/createTarGz.ts +76 -0
  114. package/src/lib/server/cli/handleCliDownload.ts +153 -0
  115. package/src/lib/server/cli/handleCliInstall.ts +37 -0
  116. package/src/lib/server/cli/installScript.ts +29 -0
  117. package/src/lib/server/cli/maxSourceMtime.ts +26 -0
  118. package/src/lib/server/cookies.ts +29 -0
  119. package/src/lib/server/env.ts +50 -0
  120. package/src/lib/server/error.ts +70 -0
  121. package/src/lib/server/json.ts +28 -0
  122. package/src/lib/server/jsonl.ts +46 -0
  123. package/src/lib/server/prompts/definePrompt.ts +20 -0
  124. package/src/lib/server/prompts/promptRegistry.ts +9 -0
  125. package/src/lib/server/prompts/registerPrompt.ts +6 -0
  126. package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
  127. package/src/lib/server/prompts/types/Prompt.ts +13 -0
  128. package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
  129. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
  130. package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
  131. package/src/lib/server/redirect.ts +42 -0
  132. package/src/lib/server/request.ts +18 -0
  133. package/src/lib/server/rpc/defineVerb.ts +133 -0
  134. package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
  135. package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
  136. package/src/lib/server/rpc/parseArgs.ts +95 -0
  137. package/src/lib/server/rpc/registerVerb.ts +6 -0
  138. package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
  139. package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
  140. package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
  141. package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
  142. package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
  143. package/src/lib/server/rpc/unprocessed.ts +14 -0
  144. package/src/lib/server/rpc/verbRegistry.ts +11 -0
  145. package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
  146. package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
  147. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
  148. package/src/lib/server/runtime/acceptsZstd.ts +8 -0
  149. package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
  150. package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
  151. package/src/lib/server/runtime/containsTraversal.ts +37 -0
  152. package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
  153. package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
  154. package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
  155. package/src/lib/server/runtime/createServer.ts +692 -0
  156. package/src/lib/server/runtime/devReloadResponse.ts +35 -0
  157. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
  158. package/src/lib/server/runtime/envSchemaStore.ts +15 -0
  159. package/src/lib/server/runtime/findOpenPort.ts +35 -0
  160. package/src/lib/server/runtime/getActiveServer.ts +6 -0
  161. package/src/lib/server/runtime/globToPathSet.ts +29 -0
  162. package/src/lib/server/runtime/inProcessServer.ts +20 -0
  163. package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
  164. package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
  165. package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
  166. package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
  167. package/src/lib/server/runtime/mimeForExtension.ts +20 -0
  168. package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
  169. package/src/lib/server/runtime/parsePort.ts +11 -0
  170. package/src/lib/server/runtime/registryManifests.ts +66 -0
  171. package/src/lib/server/runtime/requestContext.ts +5 -0
  172. package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
  173. package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
  174. package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
  175. package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
  176. package/src/lib/server/runtime/serverSlot.ts +13 -0
  177. package/src/lib/server/runtime/setActiveServer.ts +6 -0
  178. package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
  179. package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
  180. package/src/lib/server/runtime/streamFromIterator.ts +86 -0
  181. package/src/lib/server/runtime/streamStash.ts +64 -0
  182. package/src/lib/server/runtime/types/Assets.ts +1 -0
  183. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  184. package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
  185. package/src/lib/server/server.ts +32 -0
  186. package/src/lib/server/socket.ts +31 -0
  187. package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
  188. package/src/lib/server/sockets/defineSocket.ts +167 -0
  189. package/src/lib/server/sockets/lookupSocket.ts +6 -0
  190. package/src/lib/server/sockets/recentHistory.ts +11 -0
  191. package/src/lib/server/sockets/registerSocket.ts +6 -0
  192. package/src/lib/server/sockets/socketOperations.ts +35 -0
  193. package/src/lib/server/sockets/socketRegistry.ts +9 -0
  194. package/src/lib/server/sockets/types/Socket.ts +21 -0
  195. package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
  196. package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
  197. package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
  198. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
  199. package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
  200. package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
  201. package/src/lib/server/sse.ts +53 -0
  202. package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
  203. package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
  204. package/src/lib/shared/HttpError.ts +19 -0
  205. package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
  206. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
  207. package/src/lib/shared/activeCacheStore.ts +20 -0
  208. package/src/lib/shared/appDataDir.ts +34 -0
  209. package/src/lib/shared/belteImportName.ts +44 -0
  210. package/src/lib/shared/browserClientFlags.ts +10 -0
  211. package/src/lib/shared/buildRpcRequest.ts +70 -0
  212. package/src/lib/shared/bundleLayout.ts +36 -0
  213. package/src/lib/shared/bundled.ts +34 -0
  214. package/src/lib/shared/cache.ts +559 -0
  215. package/src/lib/shared/cacheStoreSlot.ts +16 -0
  216. package/src/lib/shared/canonicalJson.ts +63 -0
  217. package/src/lib/shared/carriesBodyArgs.ts +13 -0
  218. package/src/lib/shared/clearLastConnection.ts +7 -0
  219. package/src/lib/shared/commandNameForUrl.ts +17 -0
  220. package/src/lib/shared/createCacheStore.ts +75 -0
  221. package/src/lib/shared/createPushIterator.ts +93 -0
  222. package/src/lib/shared/createRemoteFunction.ts +99 -0
  223. package/src/lib/shared/decodeResponse.ts +47 -0
  224. package/src/lib/shared/detectTarget.ts +27 -0
  225. package/src/lib/shared/exeSuffix.ts +9 -0
  226. package/src/lib/shared/exitOnBuildFailure.ts +17 -0
  227. package/src/lib/shared/extraForwardHeaders.ts +16 -0
  228. package/src/lib/shared/fileStem.ts +9 -0
  229. package/src/lib/shared/findExportCallSite.ts +479 -0
  230. package/src/lib/shared/forwardHeaders.ts +41 -0
  231. package/src/lib/shared/getRemoteMeta.ts +5 -0
  232. package/src/lib/shared/globalCacheStore.ts +15 -0
  233. package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
  234. package/src/lib/shared/importNamesToStrip.ts +13 -0
  235. package/src/lib/shared/invalidateEvent.ts +11 -0
  236. package/src/lib/shared/isCompileTarget.ts +15 -0
  237. package/src/lib/shared/isDebugEnabled.ts +23 -0
  238. package/src/lib/shared/isModuleNotFound.ts +16 -0
  239. package/src/lib/shared/isReadOnlyMethod.ts +14 -0
  240. package/src/lib/shared/isStreamingResponse.ts +11 -0
  241. package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
  242. package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
  243. package/src/lib/shared/jsonlErrorFrame.ts +24 -0
  244. package/src/lib/shared/keyForRemoteCall.ts +29 -0
  245. package/src/lib/shared/lastConnectionPath.ts +7 -0
  246. package/src/lib/shared/loadEnvFile.ts +17 -0
  247. package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
  248. package/src/lib/shared/loadSvelteConfig.ts +18 -0
  249. package/src/lib/shared/log.ts +104 -0
  250. package/src/lib/shared/manifestModule.ts +39 -0
  251. package/src/lib/shared/memoizeByKey.ts +24 -0
  252. package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
  253. package/src/lib/shared/normalizeTarget.ts +10 -0
  254. package/src/lib/shared/pageUrlForFile.ts +14 -0
  255. package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
  256. package/src/lib/shared/parseEnv.ts +30 -0
  257. package/src/lib/shared/parsePromptMarkdown.ts +34 -0
  258. package/src/lib/shared/parseRouteSegments.ts +22 -0
  259. package/src/lib/shared/prepareRpcModule.ts +59 -0
  260. package/src/lib/shared/prepareSocketModule.ts +49 -0
  261. package/src/lib/shared/programNameForPackage.ts +14 -0
  262. package/src/lib/shared/promptNameForFile.ts +10 -0
  263. package/src/lib/shared/queryStringFromArgs.ts +27 -0
  264. package/src/lib/shared/readEnvFile.ts +15 -0
  265. package/src/lib/shared/readLastConnection.ts +18 -0
  266. package/src/lib/shared/readPackageJson.ts +9 -0
  267. package/src/lib/shared/recordRemoteMeta.ts +5 -0
  268. package/src/lib/shared/remoteMetaStore.ts +16 -0
  269. package/src/lib/shared/resolveClientFlags.ts +20 -0
  270. package/src/lib/shared/responseErrorText.ts +9 -0
  271. package/src/lib/shared/rpcUrlForFile.ts +19 -0
  272. package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
  273. package/src/lib/shared/serializeEnv.ts +18 -0
  274. package/src/lib/shared/setCacheStoreResolver.ts +6 -0
  275. package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
  276. package/src/lib/shared/socketNameForFile.ts +11 -0
  277. package/src/lib/shared/sseErrorFrame.ts +29 -0
  278. package/src/lib/shared/streamResponse.ts +169 -0
  279. package/src/lib/shared/stripImport.ts +27 -0
  280. package/src/lib/shared/subscribableFromResponse.ts +51 -0
  281. package/src/lib/shared/toBunRoutePattern.ts +28 -0
  282. package/src/lib/shared/types/CacheEntry.ts +63 -0
  283. package/src/lib/shared/types/CacheInvalidation.ts +9 -0
  284. package/src/lib/shared/types/CacheOptions.ts +33 -0
  285. package/src/lib/shared/types/CacheSnapshot.ts +16 -0
  286. package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
  287. package/src/lib/shared/types/CacheStore.ts +32 -0
  288. package/src/lib/shared/types/ClientFlags.ts +11 -0
  289. package/src/lib/shared/types/CompileTarget.ts +6 -0
  290. package/src/lib/shared/types/HttpVerb.ts +1 -0
  291. package/src/lib/shared/types/LastConnection.ts +9 -0
  292. package/src/lib/shared/types/PromptArgument.ts +12 -0
  293. package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
  294. package/src/lib/shared/types/RemoteFunction.ts +42 -0
  295. package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
  296. package/src/lib/shared/types/StreamedResolution.ts +10 -0
  297. package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
  298. package/src/lib/shared/types/Subscribable.ts +15 -0
  299. package/src/lib/shared/types/SvelteConfig.ts +5 -0
  300. package/src/lib/shared/withJsonSchema.ts +20 -0
  301. package/src/lib/shared/writeLastConnection.ts +13 -0
  302. package/src/lib/shared/writeRoutesDts.ts +67 -0
  303. package/src/lib/test/clearVerbRegistry.ts +11 -0
  304. package/src/lib/test/createTestClient.ts +78 -0
  305. package/src/preload.ts +20 -0
  306. package/src/scaffold.ts +92 -0
  307. package/src/serverBuildPlugins.ts +25 -0
  308. package/src/serverEntry.ts +94 -0
  309. package/src/sveltePlugin.ts +58 -0
  310. package/src/tailwindStylePreprocessor.ts +62 -0
  311. package/template/bunfig.toml +4 -0
  312. package/template/package.json +19 -0
  313. package/template/src/app.ts +23 -0
  314. package/template/src/browser/app.css +21 -0
  315. package/template/src/browser/app.html +24 -0
  316. package/template/src/browser/pages/about/page.svelte +5 -0
  317. package/template/src/browser/pages/layout.svelte +26 -0
  318. package/template/src/browser/pages/page.svelte +20 -0
  319. package/template/src/bundle/icon.png +0 -0
  320. package/template/src/cli/banner.txt +3 -0
  321. package/template/src/cli/footer.txt +1 -0
  322. package/template/src/server/config.ts +17 -0
  323. package/template/src/server/rpc/getHello.ts +35 -0
  324. package/template/svelte.config.js +12 -0
  325. package/template/tsconfig.json +18 -0
  326. package/tsconfig.app.json +16 -0
@@ -0,0 +1,858 @@
1
+ // node:fs existsSync — Bun plugin onResolve is sync-only; Bun.file().exists() is async
2
+ import { existsSync, statSync } from 'node:fs'
3
+ import type { BunPlugin } from 'bun'
4
+ import { Glob } from 'bun'
5
+ import { belteImportName } from './lib/shared/belteImportName.ts'
6
+ import { fileStem } from './lib/shared/fileStem.ts'
7
+ import { jsonSchemaForPromptArguments } from './lib/shared/jsonSchemaForPromptArguments.ts'
8
+ import { log } from './lib/shared/log.ts'
9
+ import { manifestModule } from './lib/shared/manifestModule.ts'
10
+ import { pageUrlForFile } from './lib/shared/pageUrlForFile.ts'
11
+ import { parsePromptMarkdown } from './lib/shared/parsePromptMarkdown.ts'
12
+ import { prepareRpcModule } from './lib/shared/prepareRpcModule.ts'
13
+ import { prepareSocketModule } from './lib/shared/prepareSocketModule.ts'
14
+ import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
15
+ import { promptNameForFile } from './lib/shared/promptNameForFile.ts'
16
+ import { readPackageJson } from './lib/shared/readPackageJson.ts'
17
+ import { rpcUrlForFile } from './lib/shared/rpcUrlForFile.ts'
18
+ import { socketNameForFile } from './lib/shared/socketNameForFile.ts'
19
+ import { writeRoutesDts } from './lib/shared/writeRoutesDts.ts'
20
+
21
+ /*
22
+ Resolves a bare directory or extensionless path to a concrete file. Mirrors
23
+ Node-style resolution (path.ts, path.js, path/index.ts, path/index.js) so
24
+ project code can use SvelteKit-style aliases like `$shared/foo/utils` that point
25
+ at directories with an index file. The (path → resolved) mapping is
26
+ deterministic per build, so cache it — every module that imports a `$shared`
27
+ alias hits this twice or more, and each call would otherwise do up to nine
28
+ filesystem stats.
29
+ */
30
+ const resolveExtensionCache = new Map<string, string>()
31
+ function resolveExtension(path: string): string {
32
+ const cached = resolveExtensionCache.get(path)
33
+ if (cached !== undefined) {
34
+ return cached
35
+ }
36
+ const resolved = resolveExtensionUncached(path)
37
+ resolveExtensionCache.set(path, resolved)
38
+ return resolved
39
+ }
40
+
41
+ function resolveExtensionUncached(path: string): string {
42
+ if (existsSync(path) && !statSync(path).isDirectory()) {
43
+ return path
44
+ }
45
+ for (const extension of ['.ts', '.js', '.tsx', '.jsx']) {
46
+ if (existsSync(`${path}${extension}`)) {
47
+ return `${path}${extension}`
48
+ }
49
+ }
50
+ for (const extension of ['ts', 'js', 'tsx', 'jsx']) {
51
+ const indexPath = `${path}/index.${extension}`
52
+ if (existsSync(indexPath)) {
53
+ return indexPath
54
+ }
55
+ }
56
+ return path
57
+ }
58
+
59
+ const NS = 'belte-virtual'
60
+
61
+ function escapeRegex(value: string): string {
62
+ return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
63
+ }
64
+
65
+ /* Memoises a zero-arg async producer so repeat calls reuse the first in-flight promise. */
66
+ function once<T>(produce: () => Promise<T>): () => Promise<T> {
67
+ let promise: Promise<T> | undefined
68
+ return () => {
69
+ if (!promise) {
70
+ promise = produce()
71
+ }
72
+ return promise
73
+ }
74
+ }
75
+
76
+ /*
77
+ Bun plugin that wires every virtual import belte produces at build time:
78
+ - `belte:rpc` — { rpcUrl: () => import(rpc-module) } HTTP-verb manifest
79
+ - `belte:sockets` — { socketName: () => import(socket-module) } socket manifest
80
+ - `belte:pages` — { pageUrl: () => import(page.svelte) } manifest
81
+ - `belte:layouts` — { dirPrefix: () => import(layout.svelte) } manifest
82
+ - `belte:prompts` — { promptName: () => import(prompt-module) } manifest
83
+ - `belte:app` — { init?, handle?, handleError? } from src/app.ts
84
+ - `belte:assets` — zstd-compressed chunk bytes embedded for standalone compile
85
+ - `belte:public-assets` — zstd-embedded src/browser/public files
86
+ - `belte:mcp-resources` — zstd-embedded src/mcp/resources files
87
+ - `belte:shell` — app.html content (custom or default)
88
+
89
+ Also rewrites modules under src/server/rpc and src/server/sockets:
90
+ - src/server/rpc/<file>.ts: each HTTP-verb export is bound to a runtime
91
+ implementation — defineVerb on the server, remoteProxy on the client.
92
+ - src/server/sockets/<file>.ts: each `socket(opts)` export is bound to
93
+ defineSocket on the server (with the socket name + opts) or
94
+ socketProxy on the client (name only — opts are server-side).
95
+ */
96
+ export function belteResolverPlugin({
97
+ cwd = process.cwd(),
98
+ embedAssets = false,
99
+ target = 'server',
100
+ }: {
101
+ cwd?: string
102
+ embedAssets?: boolean
103
+ target?: 'server' | 'client'
104
+ } = {}): BunPlugin {
105
+ const serverDir = `${cwd}/src/server`
106
+ const browserDir = `${cwd}/src/browser`
107
+ const sharedDir = `${cwd}/src/shared`
108
+ const mcpDir = `${cwd}/src/mcp`
109
+ const cliDir = `${cwd}/src/cli`
110
+ const rpcDir = `${serverDir}/rpc`
111
+ const socketsDir = `${serverDir}/sockets`
112
+ const pagesDir = `${browserDir}/pages`
113
+ const publicDir = `${browserDir}/public`
114
+ const promptsDir = `${mcpDir}/prompts`
115
+ const resourcesDir = `${mcpDir}/resources`
116
+
117
+ /*
118
+ The bare specifier the project imports belte under (canonical
119
+ `@belte/belte` or a package alias). Resolved once from the project's
120
+ package.json and threaded into every generated module so the codegen's
121
+ imports resolve regardless of which install style the project uses.
122
+ */
123
+ const belteImportNameOnce = once(() => belteImportName(cwd))
124
+ /*
125
+ The whole-tree validation + per-leaf classification only needs to run
126
+ once per build. Memoise the promise so the virtual manifests
127
+ (rpc/sockets/pages/layouts) share a single scan instead of each one
128
+ re-globbing the trees. The shell read is memoised the same way so two
129
+ passes don't re-read app.html from disk.
130
+ */
131
+ const scanPagesOnce = once(() =>
132
+ scanPages(pagesDir).then(async (scan) => {
133
+ await writeRoutesDts({
134
+ cwd,
135
+ pageFiles: scan.pageFiles,
136
+ importName: await belteImportNameOnce(),
137
+ })
138
+ return scan
139
+ }),
140
+ )
141
+ const scanRpcOnce = once(() => scanRpc(rpcDir))
142
+ const scanSocketsOnce = once(() => scanSockets(socketsDir))
143
+ const scanPromptsOnce = once(() => scanPrompts(promptsDir))
144
+ const loadShellOnce = once(() => loadShell(cwd))
145
+
146
+ const rpcFilter = new RegExp(`^${escapeRegex(rpcDir)}/.*\\.ts$`)
147
+ const socketsFilter = new RegExp(`^${escapeRegex(socketsDir)}/.*\\.ts$`)
148
+ const promptsFilter = new RegExp(`^${escapeRegex(promptsDir)}/.*\\.md$`)
149
+
150
+ return {
151
+ name: 'belte-resolver',
152
+ setup(build) {
153
+ build.onResolve(
154
+ {
155
+ filter: /\/_virtual\/(rpc|sockets|prompts|pages|layouts|errors|app|config|mcp-resources|mcp|assets|public-assets|shell|app-info|cli-manifest|cli-name|cli-chrome|bundle-window|bundle-disconnected-component|bundle-disconnected)\.ts$/,
156
+ },
157
+ (args) => {
158
+ const name = fileStem(args.path)
159
+ if (!name) {
160
+ return undefined
161
+ }
162
+ return { path: `belte:${name}`, namespace: NS }
163
+ },
164
+ )
165
+
166
+ /*
167
+ User-facing aliases are the five top-level project directories.
168
+ Sub-paths fall out of them: `$server/rpc/getThing`,
169
+ `$browser/pages/...`, `$mcp/prompts/...`, `$mcp/resources/...`.
170
+ `lib/` is userland — projects declare their own lib aliases.
171
+ */
172
+ const dirAliases: Record<string, string> = {
173
+ $server: serverDir,
174
+ $browser: browserDir,
175
+ $shared: sharedDir,
176
+ $mcp: mcpDir,
177
+ $cli: cliDir,
178
+ }
179
+ for (const [alias, baseDir] of Object.entries(dirAliases)) {
180
+ build.onResolve({ filter: new RegExp(`^\\${alias}(\\/.*)?$`) }, (args) => {
181
+ const subpath = args.path.slice(alias.length)
182
+ return { path: resolveExtension(subpath ? `${baseDir}${subpath}` : baseDir) }
183
+ })
184
+ }
185
+
186
+ /*
187
+ Root-absolute url() references in stylesheets (e.g.
188
+ `url(/fonts/x.woff2)`) point at files served from public/ at the
189
+ site root at runtime, not at anything on disk at build time. Bun's
190
+ CSS bundler otherwise tries to resolve them against the project
191
+ root and fails the whole build. Mark them external so the literal
192
+ `/…` path survives into the emitted CSS, where
193
+ createPublicAssetServer serves it. Scoped to CSS importers: svelte
194
+ <style> blocks compile to injected JS strings and never reach the
195
+ CSS bundler, and belte's own absolute-path JS imports come from
196
+ .ts/virtual importers — neither is a `.css` importer, so both are
197
+ untouched. Relative url()s (`./x.png`) still resolve and bundle
198
+ normally.
199
+ */
200
+ build.onResolve({ filter: /^\// }, (args) => {
201
+ if (args.importer.endsWith('.css')) {
202
+ return { path: args.path, external: true }
203
+ }
204
+ return undefined
205
+ })
206
+
207
+ build.onLoad({ filter: rpcFilter }, async (args) => {
208
+ if (!args.path.startsWith(`${rpcDir}/`)) {
209
+ return undefined
210
+ }
211
+ const relativePath = args.path.slice(rpcDir.length + 1)
212
+ const source = await Bun.file(args.path).text()
213
+ const url = rpcUrlForFile(relativePath)
214
+ const importName = await belteImportNameOnce()
215
+ const prepared = prepareRpcModule(source, importName)
216
+ if (!prepared) {
217
+ throw new Error(
218
+ `[belte] src/server/rpc/${relativePath} has no \`export const <name> = <VERB>(...)\` — every $rpc module must declare exactly one remote function`,
219
+ )
220
+ }
221
+ const expectedName = fileStem(relativePath)
222
+ if (prepared.exportName !== expectedName) {
223
+ throw new Error(
224
+ `[belte] src/server/rpc/${relativePath} exports \`${prepared.exportName}\` but the filename expects \`${expectedName}\` — the export name must match the file's stem`,
225
+ )
226
+ }
227
+ /*
228
+ For the client bundle, replace the entire module source
229
+ with a single proxy stub so the handler body and any
230
+ server-only top-level imports never reach the browser.
231
+ The stub keeps the same export name the source declared,
232
+ so page imports resolve identically on both sides.
233
+ */
234
+ if (target === 'client') {
235
+ const contents = `import { remoteProxy as __belteRemoteProxy__ } from '${importName}/browser/remoteProxy';
236
+ export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prepared.verb)}, ${JSON.stringify(url)});
237
+ `
238
+ return { contents, loader: 'ts' }
239
+ }
240
+ /*
241
+ Server target: strip the user's verb import, then rewrite
242
+ the `<VERB>(` call so the verb (from the identifier) and
243
+ the URL (from the file path) are threaded into the
244
+ runtime constructor — defineVerb. The user's handler body
245
+ stays intact between the parens; any generics on the call
246
+ are dropped (they carry no runtime info). Rewriting is
247
+ tokenizer-driven so `GET` mentions inside strings and
248
+ comments are left alone.
249
+ */
250
+ const banner = `import { defineVerb as __belteDefineVerb__ } from '${importName}/server/rpc/defineVerb';
251
+ `
252
+ return { contents: `${banner}${prepared.rewriteForServer(url)}`, loader: 'ts' }
253
+ })
254
+
255
+ build.onLoad({ filter: socketsFilter }, async (args) => {
256
+ if (!args.path.startsWith(`${socketsDir}/`)) {
257
+ return undefined
258
+ }
259
+ const relativePath = args.path.slice(socketsDir.length + 1)
260
+ const source = await Bun.file(args.path).text()
261
+ const name = socketNameForFile(relativePath)
262
+ const importName = await belteImportNameOnce()
263
+ const prepared = prepareSocketModule(source, importName)
264
+ if (!prepared) {
265
+ throw new Error(
266
+ `[belte] src/server/sockets/${relativePath} has no \`export const <name> = socket(...)\` — every $sockets module must declare exactly one socket`,
267
+ )
268
+ }
269
+ const expectedName = fileStem(relativePath)
270
+ if (prepared.exportName !== expectedName) {
271
+ throw new Error(
272
+ `[belte] src/server/sockets/${relativePath} exports \`${prepared.exportName}\` but the filename expects \`${expectedName}\` — the export name must match the file's stem`,
273
+ )
274
+ }
275
+ if (target === 'client') {
276
+ /*
277
+ Client bundle gets a name-only stub — opts (history,
278
+ clientPublish) are server-side state and don't
279
+ affect the client's wire behaviour.
280
+ */
281
+ const contents = `import { socketProxy as __belteSocketProxy__ } from '${importName}/browser/socketProxy';
282
+ export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name)});
283
+ `
284
+ return { contents, loader: 'ts' }
285
+ }
286
+ const banner = `import { defineSocket as __belteDefineSocket__ } from '${importName}/server/sockets/defineSocket';
287
+ `
288
+ return {
289
+ contents: `${banner}${prepared.rewriteForServer(name)}`,
290
+ loader: 'ts',
291
+ }
292
+ })
293
+
294
+ build.onLoad({ filter: promptsFilter }, async (args) => {
295
+ if (!args.path.startsWith(`${promptsDir}/`)) {
296
+ return undefined
297
+ }
298
+ /*
299
+ Prompts are MCP-only — no client-side counterpart. The
300
+ client bundle never imports a prompts module, but emit an
301
+ empty stub for the client target defensively so a stray
302
+ import can't drag the prompt body into the browser bundle.
303
+ */
304
+ if (target === 'client') {
305
+ return { contents: 'export {}', loader: 'ts' }
306
+ }
307
+ /*
308
+ Server target: a `.md` prompt is data, not code. Parse the
309
+ frontmatter (description + arguments) and body once, then
310
+ generate a module that registers the prompt via definePrompt
311
+ — the body is embedded as a string literal and the render
312
+ closure interpolates `{{name}}` placeholders at call time.
313
+ */
314
+ const relativePath = args.path.slice(promptsDir.length + 1)
315
+ const source = await Bun.file(args.path).text()
316
+ const name = promptNameForFile(relativePath)
317
+ const importName = await belteImportNameOnce()
318
+ const parsed = parsePromptMarkdown(source)
319
+ const jsonSchema = jsonSchemaForPromptArguments(parsed.arguments)
320
+ const optionLines = [
321
+ parsed.description
322
+ ? ` description: ${JSON.stringify(parsed.description)},`
323
+ : undefined,
324
+ jsonSchema ? ` jsonSchema: ${JSON.stringify(jsonSchema)},` : undefined,
325
+ ` render: (args) => __belteRenderPromptTemplate__(__template__, args),`,
326
+ ]
327
+ .filter((line) => line !== undefined)
328
+ .join('\n')
329
+ const contents = `import { definePrompt as __belteDefinePrompt__ } from '${importName}/server/prompts/definePrompt'
330
+ import { renderPromptTemplate as __belteRenderPromptTemplate__ } from '${importName}/server/prompts/renderPromptTemplate'
331
+ const __template__ = ${JSON.stringify(parsed.body)}
332
+ export const prompt = __belteDefinePrompt__(${JSON.stringify(name)}, {
333
+ ${optionLines}
334
+ })
335
+ `
336
+ return { contents, loader: 'ts' }
337
+ })
338
+
339
+ build.onLoad({ filter: /.*/, namespace: NS }, async (args) => {
340
+ if (args.path === 'belte:rpc') {
341
+ return manifestModule({
342
+ files: await scanRpcOnce(),
343
+ keyForFile: rpcUrlForFile,
344
+ importDir: rpcDir,
345
+ exportName: 'rpc',
346
+ })
347
+ }
348
+
349
+ if (args.path === 'belte:sockets') {
350
+ return manifestModule({
351
+ files: await scanSocketsOnce(),
352
+ keyForFile: socketNameForFile,
353
+ importDir: socketsDir,
354
+ exportName: 'sockets',
355
+ })
356
+ }
357
+
358
+ if (args.path === 'belte:prompts') {
359
+ return manifestModule({
360
+ files: await scanPromptsOnce(),
361
+ keyForFile: promptNameForFile,
362
+ importDir: promptsDir,
363
+ exportName: 'prompts',
364
+ label: 'prompt modules',
365
+ })
366
+ }
367
+
368
+ if (args.path === 'belte:pages') {
369
+ const { pageFiles } = await scanPagesOnce()
370
+ return manifestModule({
371
+ files: pageFiles,
372
+ keyForFile: pageUrlForFile,
373
+ importDir: pagesDir,
374
+ exportName: 'pages',
375
+ })
376
+ }
377
+
378
+ if (args.path === 'belte:layouts') {
379
+ const { layoutFiles } = await scanPagesOnce()
380
+ return manifestModule({
381
+ files: layoutFiles,
382
+ keyForFile: pageUrlForFile,
383
+ importDir: pagesDir,
384
+ exportName: 'layouts',
385
+ })
386
+ }
387
+
388
+ if (args.path === 'belte:errors') {
389
+ const { errorFiles } = await scanPagesOnce()
390
+ return manifestModule({
391
+ files: errorFiles,
392
+ keyForFile: pageUrlForFile,
393
+ importDir: pagesDir,
394
+ exportName: 'errors',
395
+ label: 'error pages',
396
+ })
397
+ }
398
+
399
+ if (args.path === 'belte:app') {
400
+ const userApp = `${cwd}/src/app.ts`
401
+ if (await Bun.file(userApp).exists()) {
402
+ log.info('using custom src/app.ts')
403
+ return {
404
+ contents: `export * from ${JSON.stringify(userApp)}`,
405
+ loader: 'js',
406
+ }
407
+ }
408
+ return { contents: 'export {};', loader: 'js' }
409
+ }
410
+
411
+ if (args.path === 'belte:config') {
412
+ /*
413
+ Re-exports src/server/config.ts so serverEntry can eager-import
414
+ it at boot — running its `env(schema)` validation once the env
415
+ layers are merged, before the server starts. Optional: an empty
416
+ stub when absent, so an app with no config builds and boots the
417
+ same (it just reads Bun.env directly).
418
+ */
419
+ const userConfig = `${serverDir}/config.ts`
420
+ if (await Bun.file(userConfig).exists()) {
421
+ log.info('using src/server/config.ts')
422
+ return {
423
+ contents: `export * from ${JSON.stringify(userConfig)}`,
424
+ loader: 'js',
425
+ }
426
+ }
427
+ return { contents: 'export {};', loader: 'js' }
428
+ }
429
+
430
+ if (args.path === 'belte:cli-manifest') {
431
+ /*
432
+ The CLI binary's bake-time manifest. Discovery (a
433
+ one-shot script the bundler runs separately) writes
434
+ `${cwd}/dist/cli-manifest.json` from the populated
435
+ verbRegistry; this virtual splices that JSON in as a
436
+ default-exported object. Empty manifest when the
437
+ discovery file is missing — the binary still works
438
+ but exposes no subcommands until the user runs the
439
+ full `belte cli` flow.
440
+ */
441
+ const manifestPath = `${cwd}/dist/cli-manifest.json`
442
+ if (!existsSync(manifestPath)) {
443
+ return { contents: 'export default {}', loader: 'js' }
444
+ }
445
+ const json = await Bun.file(manifestPath).text()
446
+ return { contents: `export default ${json}`, loader: 'js' }
447
+ }
448
+
449
+ if (args.path === 'belte:cli-name') {
450
+ /*
451
+ Program name shown in `<program> --help`. Reads the
452
+ project's package.json `name` field (scoped names keep
453
+ only the final segment), falling back to `app` when
454
+ missing.
455
+ */
456
+ const pkg = await readPackageJson(cwd)
457
+ const name = programNameForPackage(pkg?.name as string | undefined)
458
+ return { contents: `export default ${JSON.stringify(name)}`, loader: 'js' }
459
+ }
460
+
461
+ if (args.path === 'belte:bundle-window') {
462
+ /*
463
+ Optional bundle window config (title/size/menu) baked into
464
+ the bundled launcher. Re-exports the default from
465
+ src/bundle/window.ts when present; otherwise an empty
466
+ object so the launcher falls back to its defaults.
467
+ */
468
+ const userFile = `${cwd}/src/bundle/window.ts`
469
+ if (existsSync(userFile)) {
470
+ log.info('using custom src/bundle/window.ts')
471
+ return {
472
+ contents: `export { default } from ${JSON.stringify(userFile)}`,
473
+ loader: 'js',
474
+ }
475
+ }
476
+ return { contents: 'export default {}', loader: 'js' }
477
+ }
478
+
479
+ if (args.path === 'belte:bundle-disconnected') {
480
+ /*
481
+ The connect screen HTML baked into the launcher. buildDisconnected
482
+ writes `${cwd}/dist/bundle-disconnected.html`; this virtual splices
483
+ it in as a string export. A minimal inline fallback keeps the
484
+ launcher buildable when the file is missing (the screen still loads,
485
+ just unstyled) — bundleApp always builds it first.
486
+ */
487
+ const htmlPath = `${cwd}/dist/bundle-disconnected.html`
488
+ if (!existsSync(htmlPath)) {
489
+ const fallback =
490
+ '<!doctype html><html><body><div id="app">belte</div></body></html>'
491
+ return {
492
+ contents: `export const disconnectedHtml = ${JSON.stringify(fallback)}`,
493
+ loader: 'js',
494
+ }
495
+ }
496
+ const html = await Bun.file(htmlPath).text()
497
+ return {
498
+ contents: `export const disconnectedHtml = ${JSON.stringify(html)}`,
499
+ loader: 'js',
500
+ }
501
+ }
502
+
503
+ if (args.path === 'belte:bundle-disconnected-component') {
504
+ /*
505
+ The Svelte component the connect-screen build mounts: the project's
506
+ src/bundle/disconnected.svelte override when present, otherwise the
507
+ lib default. Re-exports the default like belte:bundle-window; the
508
+ svelte loader plugin compiles the .svelte target either way.
509
+ */
510
+ const userFile = `${cwd}/src/bundle/disconnected.svelte`
511
+ if (existsSync(userFile)) {
512
+ log.info('using custom src/bundle/disconnected.svelte')
513
+ return {
514
+ contents: `export { default } from ${JSON.stringify(userFile)}`,
515
+ loader: 'js',
516
+ }
517
+ }
518
+ const defaultFile = new URL('./lib/bundle/disconnected.svelte', import.meta.url)
519
+ .pathname
520
+ return {
521
+ contents: `export { default } from ${JSON.stringify(defaultFile)}`,
522
+ loader: 'js',
523
+ }
524
+ }
525
+
526
+ if (args.path === 'belte:cli-chrome') {
527
+ /*
528
+ Optional CLI help chrome baked into the binary: src/cli/
529
+ banner.txt prints atop top-level help, footer.txt prints
530
+ below it. Missing files emit empty strings (no chrome).
531
+ Read as plain text, like belte:shell.
532
+ */
533
+ const bannerFile = `${cliDir}/banner.txt`
534
+ const footerFile = `${cliDir}/footer.txt`
535
+ const banner = (await Bun.file(bannerFile).exists())
536
+ ? await Bun.file(bannerFile).text()
537
+ : ''
538
+ const footer = (await Bun.file(footerFile).exists())
539
+ ? await Bun.file(footerFile).text()
540
+ : ''
541
+ return {
542
+ contents: `export const banner = ${JSON.stringify(banner)}
543
+ export const footer = ${JSON.stringify(footer)}
544
+ `,
545
+ loader: 'js',
546
+ }
547
+ }
548
+
549
+ if (args.path === 'belte:app-info') {
550
+ /*
551
+ Project identity ({ name, version }) read from
552
+ package.json, surfaced in the OpenAPI document's `info`
553
+ block. Falls back to placeholder values when the file
554
+ is missing so the spec still emits.
555
+ */
556
+ const pkg = await readPackageJson(cwd)
557
+ const info = {
558
+ name: (pkg?.name as string | undefined) ?? 'app',
559
+ version: (pkg?.version as string | undefined) ?? '0.0.0',
560
+ }
561
+ return {
562
+ contents: `export const appInfo = ${JSON.stringify(info)}`,
563
+ loader: 'js',
564
+ }
565
+ }
566
+
567
+ if (args.path === 'belte:mcp') {
568
+ /*
569
+ The MCP server is fully framework-generated — tools from
570
+ the verb registry, prompts from src/mcp/prompts, resources
571
+ from src/mcp/resources. createMcpServer is internal; there
572
+ is no user-authored server module.
573
+ */
574
+ const importName = await belteImportNameOnce()
575
+ return {
576
+ contents: `import { createMcpServer } from '${importName}/mcp/createMcpServer'\nexport default createMcpServer()\n`,
577
+ loader: 'js',
578
+ }
579
+ }
580
+
581
+ if (args.path === 'belte:assets') {
582
+ if (!embedAssets) {
583
+ return { contents: 'export const assets = undefined', loader: 'js' }
584
+ }
585
+ const appDir = `${cwd}/dist/_app`
586
+ const files = await Array.fromAsync(
587
+ new Glob('**/*.zst').scan({ cwd: appDir, onlyFiles: true }),
588
+ )
589
+ const contents = await embedZstdDir({
590
+ dir: appDir,
591
+ files,
592
+ keyFor: (file) => `/_app/${file.replace(/\.zst$/, '')}`,
593
+ precompressed: true,
594
+ exportName: 'assets',
595
+ label: 'zstd assets',
596
+ source: 'dist/_app/',
597
+ })
598
+ return { contents, loader: 'js' }
599
+ }
600
+
601
+ if (args.path === 'belte:public-assets') {
602
+ /*
603
+ Embeds every file under public/ (zstd level 22, paid
604
+ once at compile) keyed by its site-root path so the
605
+ standalone binary serves them without a public/ dir on
606
+ disk. Mirrors belte:assets. Empty/undefined when not
607
+ embedding (dev + `belte start` read public/ off disk).
608
+ */
609
+ if (!embedAssets || !existsSync(publicDir)) {
610
+ return {
611
+ contents: 'export const publicAssets = undefined',
612
+ loader: 'js',
613
+ }
614
+ }
615
+ const files = await Array.fromAsync(
616
+ new Glob('**/*').scan({ cwd: publicDir, onlyFiles: true }),
617
+ )
618
+ if (files.length === 0) {
619
+ return {
620
+ contents: 'export const publicAssets = undefined',
621
+ loader: 'js',
622
+ }
623
+ }
624
+ const contents = await embedZstdDir({
625
+ dir: publicDir,
626
+ files,
627
+ keyFor: (file) => `/${file}`,
628
+ precompressed: false,
629
+ exportName: 'publicAssets',
630
+ label: 'public files',
631
+ source: 'public/',
632
+ })
633
+ return { contents, loader: 'js' }
634
+ }
635
+
636
+ if (args.path === 'belte:mcp-resources') {
637
+ /*
638
+ Embeds every file under src/mcp/resources/ (zstd level
639
+ 22) keyed by its path relative to that dir, so the
640
+ standalone binary serves MCP resources without the folder
641
+ on disk. Mirrors belte:public-assets. Undefined when not
642
+ embedding (dev + `belte start` read off disk).
643
+ */
644
+ if (!embedAssets || !existsSync(resourcesDir)) {
645
+ return {
646
+ contents: 'export const mcpResources = undefined',
647
+ loader: 'js',
648
+ }
649
+ }
650
+ const files = await Array.fromAsync(
651
+ new Glob('**/*').scan({ cwd: resourcesDir, onlyFiles: true }),
652
+ )
653
+ if (files.length === 0) {
654
+ return {
655
+ contents: 'export const mcpResources = undefined',
656
+ loader: 'js',
657
+ }
658
+ }
659
+ const contents = await embedZstdDir({
660
+ dir: resourcesDir,
661
+ files,
662
+ keyFor: (file) => file,
663
+ precompressed: false,
664
+ exportName: 'mcpResources',
665
+ label: 'mcp resources',
666
+ source: 'src/mcp/resources/',
667
+ })
668
+ return { contents, loader: 'js' }
669
+ }
670
+
671
+ if (args.path === 'belte:shell') {
672
+ const content = await loadShellOnce()
673
+ return {
674
+ contents: `export const shell = ${JSON.stringify(content)}`,
675
+ loader: 'js',
676
+ }
677
+ }
678
+
679
+ return undefined
680
+ })
681
+ },
682
+ }
683
+ }
684
+
685
+ /*
686
+ Encodes every file in `files` (relative to `dir`) into a base64 zstd map and
687
+ emits `export const <exportName> = { "<key>": _d("<base64>") }`. `keyFor` maps
688
+ a relative path to its lookup key; `precompressed` true means the files are
689
+ already `.zst` on disk (read + base64 as-is), false means compress here at
690
+ level 22. Shared by the belte:assets / belte:public-assets / belte:mcp-resources
691
+ virtuals, which differ only in source dir, key shape, and whether the inputs
692
+ are pre-compressed.
693
+ */
694
+ async function embedZstdDir({
695
+ dir,
696
+ files,
697
+ keyFor,
698
+ precompressed,
699
+ exportName,
700
+ label,
701
+ source,
702
+ }: {
703
+ dir: string
704
+ files: string[]
705
+ keyFor: (file: string) => string
706
+ precompressed: boolean
707
+ exportName: string
708
+ label: string
709
+ source: string
710
+ }): Promise<string> {
711
+ const encoded = await Promise.all(
712
+ files.map(async (file) => {
713
+ const raw = await Bun.file(`${dir}/${file}`).bytes()
714
+ const bytes = precompressed ? raw : await Bun.zstdCompress(raw, { level: 22 })
715
+ return {
716
+ line: ` ${JSON.stringify(keyFor(file))}: _d(${JSON.stringify(bytes.toBase64())}),`,
717
+ bytes: bytes.byteLength,
718
+ }
719
+ }),
720
+ )
721
+ const totalBytes = encoded.reduce((total, entry) => total + entry.bytes, 0)
722
+ const unit = precompressed ? 'KiB' : 'KiB zstd'
723
+ log.info(
724
+ `embedded ${encoded.length} ${label} from ${source} (${(totalBytes / 1024).toFixed(1)} ${unit})`,
725
+ )
726
+ return `const _d = (s) => Uint8Array.fromBase64(s)
727
+ export const ${exportName} = {
728
+ ${encoded.map((entry) => entry.line).join('\n')}
729
+ }
730
+ `
731
+ }
732
+
733
+ type PagesScan = {
734
+ pageFiles: string[]
735
+ layoutFiles: string[]
736
+ errorFiles: string[]
737
+ }
738
+
739
+ /*
740
+ Walks src/browser/pages once and partitions every `.svelte` file into pages,
741
+ layouts, and error pages. Rejects any other file shape — every leaf must live in
742
+ its own folder (or directly under `src/browser/pages/` for the root) and the
743
+ basename must be `page.svelte`, `layout.svelte`, or `error.svelte`. A misnamed
744
+ file (e.g. `about.svelte`) would otherwise be silently ignored; the explicit
745
+ error gives the right hint.
746
+ */
747
+ async function scanPages(pagesDir: string): Promise<PagesScan> {
748
+ if (!existsSync(pagesDir)) {
749
+ return { pageFiles: [], layoutFiles: [], errorFiles: [] }
750
+ }
751
+ const allFiles = await Array.fromAsync(new Glob('**/*.svelte').scan({ cwd: pagesDir }))
752
+ const pageFiles: string[] = []
753
+ const layoutFiles: string[] = []
754
+ const errorFiles: string[] = []
755
+ for (const file of allFiles) {
756
+ const basename = file.split('/').pop() ?? ''
757
+ if (basename === 'page.svelte') {
758
+ pageFiles.push(file)
759
+ continue
760
+ }
761
+ if (basename === 'layout.svelte') {
762
+ layoutFiles.push(file)
763
+ continue
764
+ }
765
+ if (basename === 'error.svelte') {
766
+ errorFiles.push(file)
767
+ continue
768
+ }
769
+ const stem = basename.replace(/\.[^.]+$/, '')
770
+ const parent = file.includes('/') ? `${file.slice(0, file.lastIndexOf('/'))}/` : ''
771
+ throw new Error(
772
+ `[belte] src/browser/pages/${file} is not a recognized page file — every page must live in its own folder as page.svelte, layout.svelte, or error.svelte (try src/browser/pages/${parent}${stem}/page.svelte)`,
773
+ )
774
+ }
775
+ return { pageFiles, layoutFiles, errorFiles }
776
+ }
777
+
778
+ /*
779
+ Walks src/server/rpc once. Every `.ts` file is an HTTP-verb rpc handler. Returns
780
+ an empty list when the directory doesn't exist so a pages-only app
781
+ builds without an `rpc/` folder.
782
+ */
783
+ async function scanRpc(rpcDir: string): Promise<string[]> {
784
+ if (!existsSync(rpcDir)) {
785
+ return []
786
+ }
787
+ return await Array.fromAsync(new Glob('**/*.ts').scan({ cwd: rpcDir }))
788
+ }
789
+
790
+ /*
791
+ Walks src/server/sockets once. Each `.ts` file declares one socket; the
792
+ dispatcher loads modules lazily on first sub/pub frame. Returns an
793
+ empty list when the directory doesn't exist.
794
+ */
795
+ async function scanSockets(socketsDir: string): Promise<string[]> {
796
+ if (!existsSync(socketsDir)) {
797
+ return []
798
+ }
799
+ return await Array.fromAsync(new Glob('**/*.ts').scan({ cwd: socketsDir }))
800
+ }
801
+
802
+ /*
803
+ Walks src/mcp/prompts once. Each `.md` file declares one MCP prompt —
804
+ frontmatter for metadata, body for the template. Returns an empty list
805
+ when the directory doesn't exist so an app without prompts builds the same.
806
+ */
807
+ async function scanPrompts(promptsDir: string): Promise<string[]> {
808
+ if (!existsSync(promptsDir)) {
809
+ return []
810
+ }
811
+ return await Array.fromAsync(new Glob('**/*.md').scan({ cwd: promptsDir }))
812
+ }
813
+
814
+ /*
815
+ Picks `src/browser/app.html` when it exists, otherwise the bundled default
816
+ shell. Reads the file once per build so the resolver's two virtual passes share
817
+ a single disk hit. Rewrites the literal `/_app/client.js` and `/_app/client.css`
818
+ references to the hashed entry filenames emitted by the client build so the
819
+ entry bundles can be served with `immutable` cache headers like the chunks.
820
+ */
821
+ async function loadShell(cwd: string): Promise<string> {
822
+ const userShell = `${cwd}/src/browser/app.html`
823
+ const defaultShell = new URL('./assets/app.html', import.meta.url).pathname
824
+ const filepath = (await Bun.file(userShell).exists()) ? userShell : defaultShell
825
+ if (filepath === userShell) {
826
+ log.info('using custom src/browser/app.html')
827
+ }
828
+ const content = await Bun.file(filepath).text()
829
+ return await rewriteHashedClientEntries(content, cwd)
830
+ }
831
+
832
+ /*
833
+ Scans `dist/_app/` for the hashed client entry filenames produced by
834
+ build.ts (e.g. `client-abc12345.js`, `client-abc12345.css`) and swaps the
835
+ shell's literal `/_app/client.js` and `/_app/client.css` references for
836
+ them. When the directory is missing (someone running the server before a
837
+ build) the shell is returned unchanged so the existing broken-asset
838
+ behaviour is preserved.
839
+ */
840
+ async function rewriteHashedClientEntries(shell: string, cwd: string): Promise<string> {
841
+ const appDir = `${cwd}/dist/_app`
842
+ if (!existsSync(appDir)) {
843
+ return shell
844
+ }
845
+ const entries = await Array.fromAsync(
846
+ new Glob('client-*').scan({ cwd: appDir, onlyFiles: true }),
847
+ )
848
+ const jsEntry = entries.find((file) => /^client-[a-z0-9]+\.js$/i.test(file))
849
+ const cssEntry = entries.find((file) => /^client-[a-z0-9]+\.css$/i.test(file))
850
+ let result = shell
851
+ if (jsEntry) {
852
+ result = result.replace('/_app/client.js', `/_app/${jsEntry}`)
853
+ }
854
+ if (cssEntry) {
855
+ result = result.replace('/_app/client.css', `/_app/${cssEntry}`)
856
+ }
857
+ return result
858
+ }