@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,35 @@
1
+ import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
2
+
3
+ // Keepalive comment cadence — keeps the idle SSE connection from being dropped.
4
+ const KEEPALIVE_INTERVAL_MS = 15000
5
+
6
+ /*
7
+ The dev live-reload channel (`/__belte/dev`, dev only). An SSE stream that
8
+ carries no events of its own — the browser-side client (DEV_RELOAD_CLIENT_SCRIPT)
9
+ reloads when this connection drops and reconnects, which only happens when the
10
+ dev orchestrator restarts the server after a rebuild. The opening `retry: 250`
11
+ shortens EventSource's reconnect backoff; a periodic comment keeps the idle
12
+ connection alive. The interval is cleared when the consumer disconnects.
13
+ */
14
+ export function devReloadResponse(): Response {
15
+ let keepalive: ReturnType<typeof setInterval>
16
+ const body = new ReadableStream<Uint8Array>({
17
+ start(controller) {
18
+ controller.enqueue(new TextEncoder().encode('retry: 250\n\n'))
19
+ keepalive = setInterval(() => {
20
+ controller.enqueue(new TextEncoder().encode(': keepalive\n\n'))
21
+ }, KEEPALIVE_INTERVAL_MS)
22
+ },
23
+ cancel() {
24
+ clearInterval(keepalive)
25
+ },
26
+ })
27
+ return new Response(body, {
28
+ headers: {
29
+ 'Content-Type': 'text/event-stream; charset=utf-8',
30
+ 'Cache-Control': NO_STORE,
31
+ 'X-Content-Type-Options': 'nosniff',
32
+ Connection: 'keep-alive',
33
+ },
34
+ })
35
+ }
@@ -0,0 +1,27 @@
1
+ import type { Server } from 'bun'
2
+ import { isStreamingResponse } from '../../shared/isStreamingResponse.ts'
3
+
4
+ /*
5
+ Opts a long-lived streaming response (SSE / JSONL) out of Bun's per-connection
6
+ idle timeout. Such a stream can stay quiet for longer than the 10s default
7
+ between frames, which Bun would otherwise read as an idle connection and close
8
+ mid-stream. `server.timeout(req, 0)` clears the timeout for just this in-flight
9
+ request, leaving the global default in place for ordinary traffic. Streaming is
10
+ detected by Content-Type (the shared signal the CLI/MCP drain paths use) rather
11
+ than `body instanceof ReadableStream`, since every bodied Response exposes one.
12
+
13
+ A streamed SSR page (`text/html`) is deliberately not opted out: it inherits the
14
+ configured `idleTimeout` as a bounded cap on how long it can hold the connection,
15
+ and the client re-fetches any placeholder left unresolved by a cut stream (see
16
+ flushUnresolvedPlaceholders). Non-stream responses pass through.
17
+ */
18
+ export function disableIdleTimeoutForStream(
19
+ server: Server<unknown>,
20
+ req: Request,
21
+ response: Response,
22
+ ): Response {
23
+ if (isStreamingResponse(response)) {
24
+ server.timeout(req, 0)
25
+ }
26
+ return response
27
+ }
@@ -0,0 +1,15 @@
1
+ import type { StandardSchemaV1 } from '../../shared/types/StandardSchemaV1.ts'
2
+
3
+ /*
4
+ Holds the schema handed to env() so the bundle launcher can project the
5
+ first-run setup form from it (jsonSchemaForSchema) without re-running boot
6
+ validation. `skipValidation` lets the launcher import src/server/config.ts
7
+ purely to register the schema: env() records it and returns early instead of
8
+ validating Bun.env — which the launcher has no business doing, that's the
9
+ embedded server's job at its own boot. In-process module state, so the server
10
+ child the launcher spawns gets a fresh store and validates normally.
11
+ */
12
+ export const envSchemaStore: {
13
+ schema: StandardSchemaV1 | undefined
14
+ skipValidation: boolean
15
+ } = { schema: undefined, skipValidation: false }
@@ -0,0 +1,35 @@
1
+ // Ports probed upward from `start` before giving up and letting the kernel assign one.
2
+ const SCAN_RANGE = 100
3
+
4
+ /*
5
+ Returns the first bindable TCP port at or above `start`, probing upward.
6
+ Used when no PORT is configured so the server lands on a predictable
7
+ 3000+ port (3000, then 3001, …) instead of a random kernel-assigned one —
8
+ running a second app just steps to the next free port. Each probe binds a
9
+ throwaway server and stops it; like any release-then-rebind there's a tiny
10
+ race before the real listener takes the port, negligible for a local boot.
11
+ After SCAN_RANGE occupied ports it gives up scanning and lets the kernel
12
+ assign any free port (bind to 0).
13
+ */
14
+ export function findOpenPort(start: number): number {
15
+ for (let port = start; port < start + SCAN_RANGE; port++) {
16
+ try {
17
+ return bindAndRelease(port)
18
+ } catch {
19
+ // port in use — try the next one up
20
+ }
21
+ }
22
+ // every candidate was taken; bind to 0 so the kernel picks a free port
23
+ return bindAndRelease(0)
24
+ }
25
+
26
+ /*
27
+ Binds a throwaway server to `port` (0 = let the kernel assign one), reads the
28
+ actual bound port, and releases it. Throws if the port is already in use.
29
+ */
30
+ function bindAndRelease(port: number): number {
31
+ const probe = Bun.serve({ port, fetch: () => new Response() })
32
+ const bound = probe.port as number
33
+ probe.stop(true)
34
+ return bound
35
+ }
@@ -0,0 +1,6 @@
1
+ import type { Server } from 'bun'
2
+ import { serverSlot } from './serverSlot.ts'
3
+
4
+ export function getActiveServer(): Server<unknown> | undefined {
5
+ return serverSlot.active
6
+ }
@@ -0,0 +1,29 @@
1
+ import { Glob } from 'bun'
2
+
3
+ /*
4
+ Scans `cwd` for files matching `pattern` and returns their request paths as
5
+ a Set, mapping each relative file path to a root-relative URL via `keyFor`.
6
+ Used to snapshot the on-disk asset trees (the `public/` files, the `_app`
7
+ precompressed `.zst` siblings) once at boot so the request path is a Set
8
+ lookup instead of a filesystem stat.
9
+
10
+ A missing directory makes scan throw ENOENT — swallowed to an empty Set so
11
+ the caller just falls through. This scan-and-catch is also the reliable
12
+ directory existence test: `Bun.file(dir).exists()` returns false for a
13
+ directory, so guarding the scan with it silently yields an empty Set.
14
+ */
15
+ export async function globToPathSet(
16
+ cwd: string,
17
+ pattern: string,
18
+ keyFor: (file: string) => string,
19
+ options?: { dot?: boolean },
20
+ ): Promise<Set<string>> {
21
+ try {
22
+ const files = await Array.fromAsync(
23
+ new Glob(pattern).scan({ cwd, dot: options?.dot ?? false }),
24
+ )
25
+ return new Set(files.map(keyFor))
26
+ } catch {
27
+ return new Set()
28
+ }
29
+ }
@@ -0,0 +1,20 @@
1
+ import type { Server } from 'bun'
2
+
3
+ /*
4
+ The Server `server()` hands back under in-process dispatch (CLI / MCP / test
5
+ client), where no Bun.serve has booted so there is no live connection to act on.
6
+ Each member is the honest no-op for a request that never rode a socket: timeout
7
+ and upgrade do nothing, publish reaches no subscribers (0 bytes sent),
8
+ subscriberCount is 0, requestIP has no peer (null). Handlers using these idioms
9
+ run unchanged in-process instead of throwing; createServer's live Server takes
10
+ precedence whenever one is booted. Connection-scoped only — config-shaped
11
+ members (port, url, hostname …) are intentionally absent: there is no server to
12
+ describe, and a stubbed value would mislead more than their plain absence.
13
+ */
14
+ export const inProcessServer = {
15
+ timeout() {},
16
+ upgrade: () => false,
17
+ publish: () => 0,
18
+ subscriberCount: () => 0,
19
+ requestIP: () => null,
20
+ } as unknown as Server<unknown>
@@ -0,0 +1,25 @@
1
+ import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
2
+ import { isDebugEnabled } from '../../shared/isDebugEnabled.ts'
3
+
4
+ /*
5
+ The framework's default 500 response. Shared by the per-request scope's catch
6
+ (runWithRequestScope) and Bun.serve's global error() fallback so the two can't
7
+ drift. Only reached when the app supplies no `handleError` hook.
8
+
9
+ Secure by default: a bare `Internal Server Error` so paths, library versions,
10
+ and message contents never leak to clients in production. The full stack is
11
+ shown only under `DEBUG=belte` (the same dev signal that turns on request
12
+ logging); the cause is logged server-side regardless of the flag.
13
+ */
14
+ export function internalErrorResponse(error: unknown): Response {
15
+ const body = isDebugEnabled('belte')
16
+ ? `<pre>${String((error as Error)?.stack ?? error)}</pre>`
17
+ : 'Internal Server Error'
18
+ return new Response(body, {
19
+ status: 500,
20
+ headers: {
21
+ 'Content-Type': 'text/html; charset=utf-8',
22
+ 'Cache-Control': NO_STORE,
23
+ },
24
+ })
25
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ True when a WebSocket upgrade carries a browser Origin that doesn't match the
3
+ request's own host — the cross-site WebSocket hijacking (CSWSH) shape. A
4
+ mismatched Origin means another site is trying to open the socket in a visitor's
5
+ authenticated browser. Native clients (CLI, MCP) send no Origin, so an absent
6
+ header is allowed; only a present-and-mismatched (or unparseable) Origin is
7
+ rejected. An unparseable Origin is treated as cross-origin (fail closed).
8
+ */
9
+ export function isCrossOriginUpgrade(request: Request, requestUrl: URL): boolean {
10
+ const origin = request.headers.get('origin')
11
+ if (!origin) {
12
+ return false
13
+ }
14
+ try {
15
+ return new URL(origin).host !== requestUrl.host
16
+ } catch {
17
+ return true
18
+ }
19
+ }
@@ -0,0 +1,36 @@
1
+ import type { Server } from 'bun'
2
+
3
+ // Ports tried upward from `start` before giving up and letting the kernel assign one.
4
+ const SCAN_RANGE = 100
5
+
6
+ /*
7
+ Binds the real server, scanning upward from `start` for the first free port.
8
+ The listener that wins a port is the one that keeps it: unlike probing a
9
+ throwaway server and releasing it before the real bind, this leaves no window
10
+ for the chosen port to be stolen in between — the gap that crashed boot on
11
+ EADDRINUSE instead of stepping to the next port. `bindAt` does the actual
12
+ Bun.serve; only an in-use port is retried, any other failure propagates. After
13
+ SCAN_RANGE occupied ports it binds port 0 so the kernel assigns any free port.
14
+ */
15
+ export function listenOnOpenPort(
16
+ bindAt: (port: number) => Server<unknown>,
17
+ start: number,
18
+ ): Server<unknown> {
19
+ for (let port = start; port < start + SCAN_RANGE; port++) {
20
+ try {
21
+ return bindAt(port)
22
+ } catch (error) {
23
+ if (!isAddressInUse(error)) {
24
+ throw error
25
+ }
26
+ // port in use — try the next one up
27
+ }
28
+ }
29
+ // every candidate was taken; bind to 0 so the kernel picks a free port
30
+ return bindAt(0)
31
+ }
32
+
33
+ // Bun reports a taken port as an Error carrying code 'EADDRINUSE'.
34
+ function isAddressInUse(error: unknown): boolean {
35
+ return error instanceof Error && (error as { code?: string }).code === 'EADDRINUSE'
36
+ }
@@ -0,0 +1,162 @@
1
+ import type { Pages } from '../../browser/types/Pages.ts'
2
+ import { log } from '../../shared/log.ts'
3
+ import type { NormalizedLayoutPrefix } from '../../shared/nearestLayoutPrefix.ts'
4
+ import { nearestLayoutPrefix } from '../../shared/nearestLayoutPrefix.ts'
5
+ import { verbRegistry } from '../rpc/verbRegistry.ts'
6
+ import { socketRegistry } from '../sockets/socketRegistry.ts'
7
+ import { ensureRegistriesLoaded } from './registryManifests.ts'
8
+
9
+ // Cell glyphs: PRESENT, ABSENT.
10
+ const PRESENT = '✓'
11
+ const ABSENT = '·'
12
+ const COLUMN_GAP = 2
13
+
14
+ const hasColor = typeof Bun !== 'undefined' && Bun.enableANSIColors
15
+ // Red foreground then `\x1b[39m` (default-foreground, not full reset) so the enclosing dim survives.
16
+ const redden = (text: string): string =>
17
+ hasColor ? `${Bun.color('red', 'ansi-256')}${text}\x1b[39m` : text
18
+
19
+ /*
20
+ A declared inputSchema is what makes mcp/cli safe to advertise (see defineVerb /
21
+ defineSocket), so a missing schema gets a red `·` to flag the declaration whose
22
+ machine surfaces are gated behind it.
23
+ */
24
+ const schemaCell = (hasSchema: boolean): string => (hasSchema ? PRESENT : redden(ABSENT))
25
+ const flag = (on: boolean): string => (on ? PRESENT : ABSENT)
26
+
27
+ // Display width ignoring ANSI color escapes, so colored glyphs don't inflate alignment.
28
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: the ESC (\x1b) is the intended match — stripping the color escape to measure visible width
29
+ const displayWidth = (cell: string): number => cell.replace(/\x1b\[[0-9;]*m/g, '').length
30
+
31
+ // A titled table: its header row plus data rows, all sharing the column layout below.
32
+ type SurfaceTable = { title: string; header: string[]; rows: string[][] }
33
+
34
+ // Per-column width: the widest cell's display width across every row given.
35
+ function columnWidths(rows: string[][]): number[] {
36
+ const columnCount = Math.max(...rows.map((row) => row.length))
37
+ return Array.from({ length: columnCount }, (_, column) =>
38
+ Math.max(...rows.map((row) => displayWidth(row[column] ?? ''))),
39
+ )
40
+ }
41
+
42
+ /*
43
+ Left-aligns one row's cells into the given fixed column widths plus a gap.
44
+ Padding is explicit spaces measured by displayWidth so embedded color escapes
45
+ don't inflate the width and break alignment. Two-space indented.
46
+ */
47
+ function renderRow(row: string[], widths: number[]): string {
48
+ return (
49
+ ' ' +
50
+ row
51
+ .map(
52
+ (cell, column) =>
53
+ cell + ' '.repeat(widths[column] - displayWidth(cell) + COLUMN_GAP),
54
+ )
55
+ .join('')
56
+ .trimEnd()
57
+ )
58
+ }
59
+
60
+ /*
61
+ Prints every non-empty table under one shared set of column widths, so the
62
+ identifier and glyph columns line up vertically across the page/socket/rpc
63
+ tables rather than each table aligning only within itself. Each table keeps its
64
+ own dim header row.
65
+ */
66
+ function logTables(tables: SurfaceTable[]): void {
67
+ const present = tables.filter((table) => table.rows.length > 0)
68
+ if (present.length === 0) {
69
+ return
70
+ }
71
+ const widths = columnWidths(present.flatMap((table) => [table.header, ...table.rows]))
72
+ present.forEach((table) => {
73
+ log.info(`${table.title}:`)
74
+ log.detail([table.header, ...table.rows].map((row) => renderRow(row, widths)).join('\n'))
75
+ })
76
+ }
77
+
78
+ /*
79
+ Boot-time surface map: every page, socket, and rpc with the surfaces it is
80
+ exposed on, so belte's routing and multimodal-by-default exposure are auditable
81
+ rather than implicit. Three aligned tables — scan a column to spot a missing
82
+ surface, a row to see one declaration's reach:
83
+
84
+ - pages: each route with the nearest layout and error boundary wrapping it.
85
+ - sockets: name + client surfaces (schema/browser/mcp/cli) and whether clients
86
+ may publish.
87
+ - rpcs: method+path (headed `http` since http/openapi are unconditional) +
88
+ per-declaration client surfaces.
89
+
90
+ All three render under one shared column grid (see logTables): the identifier
91
+ leads flush-left in every table — page route, socket name, or rpc method+path —
92
+ then the surface columns, so the glyphs line up vertically across the tables.
93
+ rpc folds its method into a left-aligned prefix of the identifier cell, so paths
94
+ still start at a shared column. For sockets and rpcs the `schema`
95
+ column leads: it's what unlocks the non-browser surfaces, so a missing schema
96
+ reddens to flag the gated declaration. Loads the full registry, so it runs once
97
+ at boot and only under `belte` debug logging (DEBUG=belte) to avoid forcing
98
+ eager imports in production. Best-effort: enumeration failures are swallowed,
99
+ this is diagnostic only.
100
+ */
101
+ export async function logExposedSurfaces(routing: {
102
+ pages: Pages
103
+ layoutPrefixes: NormalizedLayoutPrefix[]
104
+ errorPrefixes: NormalizedLayoutPrefix[]
105
+ }): Promise<void> {
106
+ try {
107
+ await ensureRegistriesLoaded()
108
+ } catch {
109
+ return
110
+ }
111
+
112
+ const pageRows = Object.keys(routing.pages)
113
+ .map((route) => [
114
+ route,
115
+ nearestLayoutPrefix(route, routing.layoutPrefixes) ?? ABSENT,
116
+ nearestLayoutPrefix(route, routing.errorPrefixes) ?? ABSENT,
117
+ ])
118
+ .sort()
119
+
120
+ const socketRows = Array.from(socketRegistry.values(), (entry) => [
121
+ entry.socket.name,
122
+ schemaCell(Boolean(entry.schema)),
123
+ flag(entry.clients.browser),
124
+ flag(entry.clients.mcp),
125
+ flag(entry.clients.cli),
126
+ flag(entry.allowClientPublish),
127
+ ]).sort()
128
+
129
+ /*
130
+ rpc identifier = method left-aligned to a shared width then the path, so the
131
+ methods line up and every path starts at the same column — while the cell as
132
+ a whole still leads flush-left like the page route and socket name.
133
+ */
134
+ const methodWidth = Math.max(
135
+ 'http'.length,
136
+ ...Array.from(verbRegistry.values(), (entry) => entry.remote.method.length),
137
+ )
138
+ const withMethod = (method: string, identifier: string): string =>
139
+ method.padEnd(methodWidth + COLUMN_GAP) + identifier
140
+
141
+ const rpcRows = Array.from(verbRegistry.values(), (entry) => [
142
+ withMethod(entry.remote.method, entry.remote.url),
143
+ schemaCell(Boolean(entry.inputSchema)),
144
+ flag(entry.clients.browser),
145
+ flag(entry.clients.mcp),
146
+ flag(entry.clients.cli),
147
+ ]).sort()
148
+
149
+ logTables([
150
+ { title: 'pages', header: ['page', 'layout', 'error'], rows: pageRows },
151
+ {
152
+ title: 'sockets',
153
+ header: ['socket', 'schema', 'browser', 'mcp', 'cli', 'publish'],
154
+ rows: socketRows,
155
+ },
156
+ {
157
+ title: 'rpcs',
158
+ header: [withMethod('http', ''), 'schema', 'browser', 'mcp', 'cli'],
159
+ rows: rpcRows,
160
+ },
161
+ ])
162
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ Derives the MIME type from a URL pathname using Bun.file().type, which
3
+ operates on the file extension synchronously without touching the disk. The
4
+ Bun.file ref here is never read from — it exists only to reuse Bun's
5
+ extension-to-MIME table. Cache by extension so repeat hits for the same
6
+ chunk type (.js / .css / .map / .svg / …) skip the BunFile allocation.
7
+ */
8
+ const mimeByExtension = new Map<string, string>()
9
+
10
+ export function mimeForExtension(pathname: string): string {
11
+ const dot = pathname.lastIndexOf('.')
12
+ const extension = dot === -1 ? '' : pathname.slice(dot)
13
+ const cached = mimeByExtension.get(extension)
14
+ if (cached !== undefined) {
15
+ return cached
16
+ }
17
+ const type = Bun.file(pathname).type
18
+ mimeByExtension.set(extension, type)
19
+ return type
20
+ }
@@ -0,0 +1,10 @@
1
+ import { parseBoundedEnvInt } from '../../shared/parseBoundedEnvInt.ts'
2
+
3
+ /*
4
+ Parses BELTE_IDLE_TIMEOUT into Bun's per-connection idle timeout in seconds.
5
+ Bun accepts 0–255 (0 disables the timeout); returns undefined for missing,
6
+ empty, or out-of-range/non-integer input so the caller keeps its default.
7
+ */
8
+ export function parseIdleTimeout(value: string | undefined): number | undefined {
9
+ return parseBoundedEnvInt(value, 0, 255)
10
+ }
@@ -0,0 +1,11 @@
1
+ import { parseBoundedEnvInt } from '../../shared/parseBoundedEnvInt.ts'
2
+
3
+ /*
4
+ Parses a PORT env value into a usable TCP port (0–65535), returning undefined
5
+ for missing, empty, or out-of-range/non-integer input so the caller can fall
6
+ back to a default. A bare Number() would turn '' into 0 (a random
7
+ kernel-assigned port) and 'abc' into NaN, both silently wrong.
8
+ */
9
+ export function parsePort(value: string | undefined): number | undefined {
10
+ return parseBoundedEnvInt(value, 0, 65535)
11
+ }
@@ -0,0 +1,66 @@
1
+ import type { PromptRoutes } from '../prompts/types/PromptRoutes.ts'
2
+ import type { RemoteRoutes } from '../rpc/types/RemoteRoutes.ts'
3
+ import type { SocketRoutes } from '../sockets/types/SocketRoutes.ts'
4
+
5
+ /*
6
+ Process-wide slot for the rpc + sockets + prompts manifests. createServer
7
+ assigns once at boot (right after the route table is built); the MCP
8
+ server, the OpenAPI emitter, and prompt enumeration read it on first
9
+ request so they can lazy-import every module and walk the
10
+ verb/socket/prompt registries.
11
+
12
+ The slot pattern (mirrors getActiveServer) lets the framework-generated
13
+ McpServer bind to the manifests at module scope while the loaders stay
14
+ lazy until the first enumeration request.
15
+ */
16
+ type RegistryManifests = {
17
+ rpc: RemoteRoutes
18
+ sockets: SocketRoutes
19
+ prompts: PromptRoutes
20
+ }
21
+
22
+ let manifests: RegistryManifests | undefined
23
+ let loading: Promise<void> | undefined
24
+
25
+ export function setRegistryManifests(value: RegistryManifests): void {
26
+ manifests = value
27
+ loading = undefined
28
+ }
29
+
30
+ /*
31
+ On first call, eagerly imports every rpc + socket + prompt module so
32
+ defineVerb / defineSocket / definePrompt fire and populate the
33
+ registries. Idempotent — repeat calls reuse the same in-flight promise,
34
+ so concurrent first requests (e.g. /openapi.json + an MCP tools/list)
35
+ trigger exactly one load instead of racing to fire the full import set
36
+ each. Eager loading is acceptable here because enumeration (MCP
37
+ tool/resource/prompt lists, the OpenAPI document) fundamentally requires
38
+ the full surface; the alternative of per-call lazy loading produces flaky
39
+ first-call latency.
40
+ */
41
+ export function ensureRegistriesLoaded(): Promise<void> {
42
+ if (!manifests) {
43
+ return Promise.resolve()
44
+ }
45
+ if (!loading) {
46
+ const { rpc, sockets, prompts } = manifests
47
+ loading = Promise.all([
48
+ ...Object.values(rpc).map((loader) => loader()),
49
+ ...Object.values(sockets).map((loader) => loader()),
50
+ ...Object.values(prompts).map((loader) => loader()),
51
+ ])
52
+ .then(() => undefined)
53
+ /*
54
+ Clear the memo on failure so a transient import error (a
55
+ module that throws at load, fixed by the next HMR pass)
56
+ doesn't poison every later enumeration request for the
57
+ process lifetime. The rejection still propagates to this
58
+ caller; the reset only affects subsequent calls.
59
+ */
60
+ .catch((error) => {
61
+ loading = undefined
62
+ throw error
63
+ })
64
+ }
65
+ return loading
66
+ }
@@ -0,0 +1,5 @@
1
+ // AsyncLocalStorage is canonical via node:async_hooks — Bun has no separate API
2
+ import { AsyncLocalStorage } from 'node:async_hooks'
3
+ import type { RequestStore } from './types/RequestStore.ts'
4
+
5
+ export const requestContext = new AsyncLocalStorage<RequestStore>()
@@ -0,0 +1,29 @@
1
+ import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
2
+ import { streamCacheResolutions } from './streamCacheResolutions.ts'
3
+ import { streamFromIterator } from './streamFromIterator.ts'
4
+ import { takePendingStream } from './streamStash.ts'
5
+
6
+ /*
7
+ The out-of-band resolution stream. The browser opens this once per streamed page
8
+ (token from `__SSR__.streamToken`) and reads newline-delimited StreamedResolution
9
+ objects as each pending {#await} fetch lands — draining the SAME in-flight
10
+ promises stashed during SSR, so handlers run once. A missing/expired token
11
+ (404) tells the client to re-fetch its placeholders live.
12
+
13
+ Returned directly (not via dispatchRequest), so it inherits the configured
14
+ `idleTimeout` as a bounded cap rather than the long-lived-stream disable; a cut
15
+ is recovered client-side off the fetch reader's clean EOF.
16
+ */
17
+ export function resolveStreamResponse(token: string): Response {
18
+ const stash = takePendingStream(token)
19
+ if (!stash) {
20
+ return new Response('', { status: 404 })
21
+ }
22
+ const body = streamFromIterator(streamCacheResolutions(stash.store, stash.pending), {
23
+ encodeFrame: (resolution) => `${JSON.stringify(resolution)}\n`,
24
+ encodeError: () => '',
25
+ })
26
+ return new Response(body, {
27
+ headers: { 'Content-Type': 'application/x-ndjson', 'Cache-Control': NO_STORE },
28
+ })
29
+ }
@@ -0,0 +1,57 @@
1
+ import { createCacheStore } from '../../shared/createCacheStore.ts'
2
+ import { log } from '../../shared/log.ts'
3
+ import type { AppModule } from '../AppModule.ts'
4
+ import { internalErrorResponse } from './internalErrorResponse.ts'
5
+ import { requestContext } from './requestContext.ts'
6
+ import type { RequestStore } from './types/RequestStore.ts'
7
+
8
+ /*
9
+ Establishes the per-request scope and runs `body` inside it: a fresh
10
+ CacheStore plus request metadata published through the AsyncLocalStorage
11
+ RequestStore (so cache() and request()/server() resolve without threading
12
+ args), the app's handleError — or the framework's 500 fallback — on a thrown
13
+ body, and optional request logging. The single seam every dynamic route
14
+ crosses; extracted from createServer so the scope, error, and logging
15
+ behaviour is exercisable through this interface without booting a Bun server.
16
+ */
17
+ export function runWithRequestScope(
18
+ req: Request,
19
+ options: { app?: AppModule; logRequests: boolean },
20
+ body: (store: RequestStore) => Promise<Response>,
21
+ ): Promise<Response> {
22
+ const url = new URL(req.url)
23
+ const store: RequestStore = {
24
+ url,
25
+ req,
26
+ cache: createCacheStore(),
27
+ }
28
+ return requestContext.run(store, async () => {
29
+ const start = options.logRequests ? Bun.nanoseconds() : 0
30
+ let response: Response
31
+ try {
32
+ response = await body(store)
33
+ } catch (error) {
34
+ if (options.app?.handleError) {
35
+ response = await options.app.handleError(error, req)
36
+ } else {
37
+ log.error(error)
38
+ response = internalErrorResponse(error)
39
+ }
40
+ }
41
+ /*
42
+ Flush any cookies the handler set onto the outgoing response. Only when
43
+ a jar was materialized (cookies() was called) and only via append, so a
44
+ Set-Cookie the handler already placed on the response init survives.
45
+ */
46
+ if (store.cookies) {
47
+ store.cookies.toSetCookieHeaders().forEach((header) => {
48
+ response.headers.append('set-cookie', header)
49
+ })
50
+ }
51
+ if (options.logRequests) {
52
+ const ms = (Bun.nanoseconds() - start) / 1e6
53
+ log.request(req.method, `${url.pathname}${url.search}`, response.status, ms)
54
+ }
55
+ return response
56
+ })
57
+ }
@@ -0,0 +1,17 @@
1
+ /*
2
+ Escapes characters that could prematurely terminate the surrounding <script>
3
+ tag or be interpreted as HTML comment delimiters when a JSON literal is
4
+ inlined into an HTML document. U+2028 (LS) and U+2029 (PS) are valid in JSON
5
+ but break a `<script>` tag's inline content because the JavaScript lexer
6
+ treats them as line terminators; encode them as Unicode escapes.
7
+ */
8
+ const LINE_SEPARATOR = String.fromCharCode(0x2028)
9
+ const PARAGRAPH_SEPARATOR = String.fromCharCode(0x2029)
10
+
11
+ export function safeJsonForScript(value: unknown): string {
12
+ return JSON.stringify(value)
13
+ .replace(/</g, '\\u003c')
14
+ .replace(/-->/g, '--\\u003e')
15
+ .replaceAll(LINE_SEPARATOR, '\\u2028')
16
+ .replaceAll(PARAGRAPH_SEPARATOR, '\\u2029')
17
+ }