@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
package/src/compile.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { build } from './build.ts'
2
+ import { detectTarget } from './lib/shared/detectTarget.ts'
3
+ import { exeSuffix } from './lib/shared/exeSuffix.ts'
4
+ import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
5
+ import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
6
+ import { log } from './lib/shared/log.ts'
7
+ import type { CompileTarget } from './lib/shared/types/CompileTarget.ts'
8
+ import { serverBuildPlugins } from './serverBuildPlugins.ts'
9
+
10
+ const SERVER_ENTRY = new URL('./serverEntry.ts', import.meta.url).pathname
11
+
12
+ /*
13
+ Produces a standalone Bun executable for the server. Runs the client `build`
14
+ first so the resolver plugin can embed the zstd-compressed assets into
15
+ the binary, then invokes Bun.build in compile mode against the server
16
+ entry. Defaults
17
+ the target to the host platform and appends `.exe` for windows targets.
18
+ Returns the path of the emitted binary; exits the process on build failure.
19
+ */
20
+ export async function compile({
21
+ cwd = process.cwd(),
22
+ target = detectTarget(),
23
+ outfile,
24
+ buildClient = true,
25
+ }: {
26
+ cwd?: string
27
+ target?: CompileTarget
28
+ outfile?: string
29
+ /*
30
+ Skip the client `build` (which clears dist). Set false when the caller already
31
+ built the platform-independent client once and is compiling several server
32
+ binaries against it — e.g. `belte cli` co-shipping a per-platform server beside
33
+ each CLI binary — so the shared `dist/_app` isn't wiped between targets.
34
+ */
35
+ buildClient?: boolean
36
+ } = {}): Promise<string> {
37
+ const svelteConfig = await loadSvelteConfig(cwd)
38
+ if (buildClient) {
39
+ await build({ cwd, svelteConfig })
40
+ }
41
+
42
+ const outPath = outfile ?? `${cwd}/dist/app${exeSuffix(target)}`
43
+
44
+ const result = await Bun.build({
45
+ entrypoints: [SERVER_ENTRY],
46
+ target: 'bun',
47
+ format: 'esm',
48
+ minify: true,
49
+ /*
50
+ Bytecode embeds precompiled JS module metadata directly into the
51
+ standalone binary, dramatically cutting cold-start time for large
52
+ apps. Requires `target: 'bun'` + an explicit `format` because the
53
+ default for `bytecode` alone is CommonJS; we need ESM bytecode.
54
+ */
55
+ bytecode: true,
56
+ compile: { target, outfile: outPath },
57
+ plugins: serverBuildPlugins({ cwd, svelteConfig, embedAssets: true }),
58
+ })
59
+
60
+ exitOnBuildFailure(result)
61
+
62
+ log.success(`compiled standalone binary: ${outPath} (target: ${target})`)
63
+ return outPath
64
+ }
@@ -0,0 +1,422 @@
1
+ import { mkdir } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import { bindConnectedFlag } from './lib/bundle/bindConnectedFlag.ts'
4
+ import { bindRequestNavigate } from './lib/bundle/bindRequestNavigate.ts'
5
+ import { listenLocalControlServer } from './lib/bundle/listenLocalControlServer.ts'
6
+ import { probeBelteServer } from './lib/bundle/probeBelteServer.ts'
7
+ import { resolveWebviewLib } from './lib/bundle/resolveWebviewLib.ts'
8
+ import { spawnEmbeddedServer } from './lib/bundle/spawnEmbeddedServer.ts'
9
+ import { stableLocalPort } from './lib/bundle/stableLocalPort.ts'
10
+ import { appDataDir } from './lib/shared/appDataDir.ts'
11
+ import { bundleLayout } from './lib/shared/bundleLayout.ts'
12
+ import { clearLastConnection } from './lib/shared/clearLastConnection.ts'
13
+ import { log } from './lib/shared/log.ts'
14
+ import { readEnvFile } from './lib/shared/readEnvFile.ts'
15
+ import { readLastConnection } from './lib/shared/readLastConnection.ts'
16
+ import { serializeEnv } from './lib/shared/serializeEnv.ts'
17
+ import { writeLastConnection } from './lib/shared/writeLastConnection.ts'
18
+
19
+ /*
20
+ The bundle's control server, run in a Worker so it owns its own thread.
21
+
22
+ `webview_run` enters a native UI run loop that blocks the launcher's main thread
23
+ indefinitely (the window owns it until close), which freezes the main thread's
24
+ JS event loop. An in-process `Bun.serve` there can never answer a request, so the
25
+ webview pointed at it would only ever see a hung navigation — a blank window.
26
+
27
+ Running the control server on this Worker thread keeps it answering the whole time
28
+ the window is open. It owns the pieces that must live beside it: the embedded
29
+ server child it spawns, and its own FFI handle to the native menu flag (set here
30
+ because the main thread can't process a postMessage while blocked in webview_run,
31
+ yet the flag is a process-global the main-thread menu still reads).
32
+
33
+ Bun does not apply the launcher build's plugins to a worker entry, so this module
34
+ can't import belte's virtual modules (the connect-screen HTML, app title). The
35
+ launcher — which can — passes them in the `init` message; on `shutdown` it has us
36
+ reap the embedded child before the launcher exits.
37
+
38
+ Once connected it also watches the chosen server's liveness — polling its identity
39
+ endpoint — and, when it stops answering, corrects the menu flag and bounces the
40
+ window back to the connect screen, since a dead server (local crash or remote
41
+ outage) otherwise leaves a frozen page and a menu that still claims connected.
42
+
43
+ GET / → the connect screen (title injected at serve time)
44
+ GET /__belte/config → { schema, values } for the first-run config form
45
+ POST /__belte/config → persist the form's answers to the data-dir .env
46
+ POST /connect {url} → record connected, reply { redirect: url }
47
+ POST /start → spawn the server binary, reply { redirect: localUrl }
48
+ GET /__belte/disconnect → reap the child, clear connected
49
+ */
50
+
51
+ /*
52
+ Init payload from the launcher, plus the per-run state the handlers close over.
53
+ `configSchema` is the JSON Schema derived from the app's BundleWindow.config
54
+ (undefined when none declared), driving the connect screen's first-run form.
55
+ */
56
+ type Init = {
57
+ disconnectedHtml: string
58
+ title: string
59
+ programName: string
60
+ configSchema?: Record<string, unknown>
61
+ }
62
+ let disconnectedHtml = ''
63
+ let title = ''
64
+ let programName = ''
65
+ let configSchema: Record<string, unknown> | undefined
66
+ let flag: ReturnType<typeof bindConnectedFlag> | undefined
67
+ let server: ReturnType<typeof listenLocalControlServer> | undefined
68
+
69
+ // The control-server origin (where the connect screen lives) and the webview
70
+ // handle, forwarded by the launcher — together they let the watch bounce a dead
71
+ // window back to the connect screen.
72
+ let controlOrigin = ''
73
+ let navigate: ReturnType<typeof bindRequestNavigate> | undefined
74
+ let webviewHandle: number | undefined
75
+
76
+ /*
77
+ Liveness watch over the currently-connected server. A recursive timer (not
78
+ setInterval, so a slow probe never overlaps the next) probes the identity endpoint;
79
+ a couple of consecutive misses — tolerating a transient blip or a quick restart —
80
+ count as a death. Cleared whenever we're not connected.
81
+ */
82
+ const LIVENESS_INTERVAL_MS = 4000
83
+ const LIVENESS_FAILURE_LIMIT = 2
84
+ let connectedUrl: string | undefined
85
+ let livenessTimer: ReturnType<typeof setTimeout> | undefined
86
+ let livenessFailures = 0
87
+
88
+ // Embedded-server child, spawned on demand by Start server; undefined when none.
89
+ let serverChild: ReturnType<typeof Bun.spawn> | undefined
90
+
91
+ // Reaps the embedded server child if one is running.
92
+ function killServerChild(): void {
93
+ if (serverChild) {
94
+ serverChild.kill()
95
+ serverChild = undefined
96
+ }
97
+ }
98
+
99
+ // Begin (or restart) watching `url` for liveness once the window points at it.
100
+ function startLivenessWatch(url: string): void {
101
+ stopLivenessWatch()
102
+ connectedUrl = url
103
+ livenessFailures = 0
104
+ livenessTimer = setTimeout(runLivenessProbe, LIVENESS_INTERVAL_MS)
105
+ }
106
+
107
+ // Stop watching — on explicit disconnect, on detected death, or at shutdown.
108
+ function stopLivenessWatch(): void {
109
+ if (livenessTimer) {
110
+ clearTimeout(livenessTimer)
111
+ livenessTimer = undefined
112
+ }
113
+ connectedUrl = undefined
114
+ livenessFailures = 0
115
+ }
116
+
117
+ /*
118
+ One liveness probe of the connected server. Successes reset the miss count;
119
+ LIVENESS_FAILURE_LIMIT consecutive misses declare it dead and hand off to
120
+ handleConnectionLost. Reschedules itself while still connected.
121
+ */
122
+ async function runLivenessProbe(): Promise<void> {
123
+ const url = connectedUrl
124
+ if (!url) {
125
+ return
126
+ }
127
+ const identity = await probeBelteServer(url)
128
+ // A disconnect or reconnect during the await may have moved us on.
129
+ if (connectedUrl !== url) {
130
+ return
131
+ }
132
+ if (identity) {
133
+ livenessFailures = 0
134
+ } else {
135
+ livenessFailures += 1
136
+ if (livenessFailures >= LIVENESS_FAILURE_LIMIT) {
137
+ handleConnectionLost(url)
138
+ return
139
+ }
140
+ }
141
+ livenessTimer = setTimeout(runLivenessProbe, LIVENESS_INTERVAL_MS)
142
+ }
143
+
144
+ /*
145
+ The connected server stopped answering. Reap any (now-dead) embedded child, clear
146
+ the connected flag so the menu stops claiming connected, and bounce the window
147
+ back to the connect screen with a `lost` notice. The flag flip alone keeps the
148
+ menu honest even when the navigate is a no-op (off macOS, or no handle yet).
149
+ */
150
+ function handleConnectionLost(url: string): void {
151
+ log.warn(`connected server stopped responding: ${url}`)
152
+ stopLivenessWatch()
153
+ killServerChild()
154
+ flag?.setConnected(false)
155
+ if (webviewHandle !== undefined) {
156
+ navigate?.requestNavigate(webviewHandle, `${controlOrigin}/?action=lost`)
157
+ }
158
+ }
159
+
160
+ /*
161
+ Boots the embedded server via the shared spawn helper and keeps the child so
162
+ killServerChild can reap it. Any previous child is reaped first so only one
163
+ embedded server runs at a time; returns the URL to point the window at.
164
+ */
165
+ async function startEmbeddedServer(timeoutMs?: number): Promise<string> {
166
+ killServerChild()
167
+ const { url, child } = await spawnEmbeddedServer({ programName, timeoutMs })
168
+ serverChild = child
169
+ return url
170
+ }
171
+
172
+ /*
173
+ Where the window should point on launch, resolved before it ever opens so the
174
+ connect screen never flashes. Repeats the last connection from the launcher-owned
175
+ record (which survives relaunch where the embedded server's fresh port can't):
176
+
177
+ - embedded, config complete → boot it and point at the live server
178
+ - embedded, config missing → the connect screen, so the user can configure
179
+ - remote url, still alive → point straight at it
180
+ - remote url, now dead → the connect screen with a `lost` notice
181
+ - nothing recorded → the connect screen
182
+
183
+ Boot is bounded by a short ceiling: a failed or slow boot falls back to the
184
+ connect screen rather than leaving the launcher window-less, and reaps the child
185
+ so a half-started server doesn't hold its port.
186
+ */
187
+ const AUTO_START_CEILING_MS = 3000
188
+ async function resolveLaunchTarget(): Promise<string> {
189
+ const last = await readLastConnection(programName)
190
+ if (!last) {
191
+ return controlOrigin
192
+ }
193
+ if (last.kind === 'embedded') {
194
+ if (await autoStartBlockedByConfig()) {
195
+ return controlOrigin
196
+ }
197
+ try {
198
+ const url = await startEmbeddedServer(AUTO_START_CEILING_MS)
199
+ flag?.setConnected(true)
200
+ startLivenessWatch(url)
201
+ log.info(`resumed embedded server at ${url}`)
202
+ return url
203
+ } catch (error) {
204
+ killServerChild()
205
+ log.warn(`embedded server did not resume: ${String(error)}`)
206
+ return controlOrigin
207
+ }
208
+ }
209
+ const identity = await probeBelteServer(last.url)
210
+ if (identity) {
211
+ flag?.setConnected(true)
212
+ startLivenessWatch(last.url)
213
+ log.info(`reconnected to ${identity.name} at ${last.url}`)
214
+ return last.url
215
+ }
216
+ log.warn(`saved server did not respond: ${last.url}`)
217
+ return `${controlOrigin}/?action=lost`
218
+ }
219
+
220
+ // True when the app declares required config that nothing yet supplies, so an
221
+ // embedded auto-start would only crash for the lack of it — land on the connect
222
+ // screen (and its setup modal) instead.
223
+ async function autoStartBlockedByConfig(): Promise<boolean> {
224
+ const required = (configSchema?.required as string[] | undefined) ?? []
225
+ if (required.length === 0) {
226
+ return false
227
+ }
228
+ const values = await resolveConfigValues()
229
+ return required.some((key) => !values[key])
230
+ }
231
+
232
+ /*
233
+ Injects the app title into the connect-screen HTML just before serving — the build
234
+ left a `<!--belte:connect-config-->` marker in <head>.
235
+ */
236
+ function renderConnectScreen(): Response {
237
+ const script = `<script>window.__BELTE_TITLE__=${JSON.stringify(title)}</script>`
238
+ const html = disconnectedHtml.replace('<!--belte:connect-config-->', script)
239
+ return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } })
240
+ }
241
+
242
+ // The data-dir `.env` the form writes and the server loads first at boot.
243
+ function dataDirEnvPath(): string {
244
+ return join(appDataDir(programName), '.env')
245
+ }
246
+
247
+ // The bundle's shipped `.env` (its default config layer), resolved from the binary
248
+ // directory — same source loadEnvFromBinaryDir reads at boot (dirname of the running
249
+ // binary): beside the binary in the flat layout, under Resources in a `.app`.
250
+ function binaryDirEnvPath(): string {
251
+ return bundleLayout(dirname(process.execPath)).envPath
252
+ }
253
+
254
+ /*
255
+ Resolves the value to pre-fill each config field with, following the same
256
+ precedence the server applies below the shell: the user's saved data-dir `.env`,
257
+ then the bundle's shipped binary-dir `.env`, then the schema's own `default`.
258
+ Empty string when nothing supplies it — which is how the form spots an unmet
259
+ required field.
260
+ */
261
+ async function resolveConfigValues(): Promise<Record<string, string>> {
262
+ const properties = (configSchema?.properties ?? {}) as Record<string, { default?: unknown }>
263
+ // Independent reads — fetch together; precedence is applied in the merge below.
264
+ const [dataDirEnv, binaryDirEnv] = await Promise.all([
265
+ readEnvFile(dataDirEnvPath()),
266
+ readEnvFile(binaryDirEnvPath()),
267
+ ])
268
+ return Object.fromEntries(
269
+ Object.keys(properties).map((key) => {
270
+ const fallback = properties[key]?.default
271
+ const value =
272
+ dataDirEnv[key] ??
273
+ binaryDirEnv[key] ??
274
+ (fallback === undefined ? '' : String(fallback))
275
+ return [key, value]
276
+ }),
277
+ )
278
+ }
279
+
280
+ /*
281
+ Persists the form's answers to the data-dir `.env`, merged over any existing
282
+ file so keys the form didn't touch survive. Creates the data dir on first run
283
+ (appDataDir only computes the path).
284
+ */
285
+ async function writeConfig(values: Record<string, string>): Promise<void> {
286
+ const path = dataDirEnvPath()
287
+ const merged = { ...(await readEnvFile(path)), ...values }
288
+ await mkdir(appDataDir(programName), { recursive: true })
289
+ await Bun.write(path, serializeEnv(merged))
290
+ }
291
+
292
+ // GET /__belte/config — the form's schema + current values, or null schema to skip the gate.
293
+ async function handleConfigGet(): Promise<Response> {
294
+ if (!configSchema) {
295
+ return Response.json({ schema: null, values: {} })
296
+ }
297
+ return Response.json({ schema: configSchema, values: await resolveConfigValues() })
298
+ }
299
+
300
+ // POST /__belte/config — persist the form's answers to the data-dir `.env`.
301
+ async function handleConfigPost(request: Request): Promise<Response> {
302
+ const { values } = (await request.json()) as { values: Record<string, string> }
303
+ await writeConfig(values)
304
+ return new Response(undefined, { status: 204 })
305
+ }
306
+
307
+ // POST /connect — point the window at a remote belte server after probing it.
308
+ async function handleConnect(request: Request): Promise<Response> {
309
+ const { url: target } = (await request.json()) as { url: string }
310
+ // Verify it's actually a belte server before pointing the window at it.
311
+ const identity = await probeBelteServer(target)
312
+ if (!identity) {
313
+ log.warn(`no belte server responded at ${target}`)
314
+ return Response.json({ error: `No belte server responded at ${target}` }, { status: 502 })
315
+ }
316
+ flag?.setConnected(true)
317
+ startLivenessWatch(target)
318
+ // Record the choice so the next launch reconnects here before opening.
319
+ await writeLastConnection(programName, { kind: 'url', url: target })
320
+ log.info(`connecting to ${identity.name} at ${target}`)
321
+ return Response.json({ redirect: target })
322
+ }
323
+
324
+ // POST /start — boot the embedded server and point the window at it.
325
+ async function handleStart(): Promise<Response> {
326
+ try {
327
+ const localUrl = await startEmbeddedServer()
328
+ flag?.setConnected(true)
329
+ startLivenessWatch(localUrl)
330
+ // Record the choice so the next launch boots the embedded server first.
331
+ await writeLastConnection(programName, { kind: 'embedded' })
332
+ log.info(`started embedded server at ${localUrl}`)
333
+ return Response.json({ redirect: localUrl })
334
+ } catch (error) {
335
+ killServerChild()
336
+ return Response.json({ error: String(error) }, { status: 500 })
337
+ }
338
+ }
339
+
340
+ // GET /__belte/disconnect — tear down the embedded server and forget the auto-resume choice.
341
+ async function handleDisconnect(): Promise<Response> {
342
+ stopLivenessWatch()
343
+ killServerChild()
344
+ flag?.setConnected(false)
345
+ await clearLastConnection(programName)
346
+ return new Response(undefined, { status: 204 })
347
+ }
348
+
349
+ /*
350
+ The control server's routes, keyed by `${method} ${pathname}` (exact match). The
351
+ connect screen owns localStorage + navigation; this worker owns the embedded-
352
+ server process and the native flag.
353
+ */
354
+ const controlRoutes: Record<string, (request: Request) => Promise<Response> | Response> = {
355
+ 'GET /': () => renderConnectScreen(),
356
+ 'GET /__belte/config': handleConfigGet,
357
+ 'POST /__belte/config': handleConfigPost,
358
+ 'POST /connect': handleConnect,
359
+ 'POST /start': handleStart,
360
+ 'GET /__belte/disconnect': handleDisconnect,
361
+ }
362
+
363
+ function handleControlRequest(request: Request): Promise<Response> | Response {
364
+ const { pathname } = new URL(request.url)
365
+ const route = controlRoutes[`${request.method} ${pathname}`]
366
+ return route ? route(request) : new Response('not found', { status: 404 })
367
+ }
368
+
369
+ /*
370
+ Bind the control server to 127.0.0.1 literally (not `localhost`) so the webview
371
+ reaches it without any IPv4/IPv6 name-resolution ambiguity, open the native flag
372
+ handle, then resolve where the window should open before handing back. Resolving
373
+ the launch target here — booting/probing the last connection before `ready` — is
374
+ what lets the launcher open the window straight at the live server, so the
375
+ connect screen never flashes; only an unconfigured, failed, or absent resume
376
+ falls back to it. The launcher gets both `origin` (for the File-menu actions) and
377
+ `target` (where to point the window now).
378
+ */
379
+ async function start(init: Init): Promise<void> {
380
+ disconnectedHtml = init.disconnectedHtml
381
+ title = init.title
382
+ programName = init.programName
383
+ configSchema = init.configSchema
384
+ const libPath = await resolveWebviewLib()
385
+ flag = bindConnectedFlag(libPath)
386
+ navigate = bindRequestNavigate(libPath)
387
+ server = listenLocalControlServer(stableLocalPort(init.programName), handleControlRequest)
388
+ controlOrigin = `http://127.0.0.1:${server.port}`
389
+ log.info(`${title} control server listening at ${controlOrigin}`)
390
+ const target = await resolveLaunchTarget()
391
+ self.postMessage({ type: 'ready', origin: controlOrigin, target })
392
+ }
393
+
394
+ // Reap the child + release the server and FFI handles, then confirm so the
395
+ // launcher can exit cleanly.
396
+ function shutdown(): void {
397
+ stopLivenessWatch()
398
+ killServerChild()
399
+ server?.stop(true)
400
+ flag?.close()
401
+ navigate?.close()
402
+ self.postMessage({ type: 'shutdownDone' })
403
+ }
404
+
405
+ /*
406
+ The launcher drives the lifecycle: `init` (with the data this worker can't import)
407
+ starts the server, `window` forwards the webview handle the liveness watch needs to
408
+ navigate, and `shutdown` tears it all down once the window closes.
409
+ */
410
+ self.addEventListener('message', (event: MessageEvent) => {
411
+ const data = event.data as
412
+ | { type: 'init'; init: Init }
413
+ | { type: 'window'; handle: number }
414
+ | { type: 'shutdown' }
415
+ if (data.type === 'init') {
416
+ void start(data.init)
417
+ } else if (data.type === 'window') {
418
+ webviewHandle = data.handle
419
+ } else if (data.type === 'shutdown') {
420
+ shutdown()
421
+ }
422
+ })
@@ -0,0 +1,66 @@
1
+ import type { BunPlugin } from 'bun'
2
+
3
+ type ExportEntry = string | { [condition: string]: ExportEntry }
4
+
5
+ /*
6
+ Walks a package.json `exports` entry, returning the first leaf string that
7
+ matches the supplied condition list in order. Returns undefined when no
8
+ branch resolves.
9
+ */
10
+ function pickExport(entry: ExportEntry, conditions: string[]): string | undefined {
11
+ if (typeof entry === 'string') {
12
+ return entry
13
+ }
14
+ for (const condition of conditions) {
15
+ if (entry[condition]) {
16
+ const resolved = pickExport(entry[condition], conditions)
17
+ if (resolved) {
18
+ return resolved
19
+ }
20
+ }
21
+ }
22
+ return undefined
23
+ }
24
+
25
+ /*
26
+ Forces every `import 'svelte/...'` (from belte's own source, the consumer's
27
+ source, or any transitive dep) to resolve against the consumer app's svelte
28
+ install, picking the export condition that matches the build target.
29
+ Without this, belte's symlinked source can pick up a second svelte from its
30
+ install location, ship both runtimes, and break hydration. Shared by the
31
+ client build and the bundle connect-screen build.
32
+ */
33
+ export function dedupeSveltePlugin({
34
+ cwd,
35
+ conditions,
36
+ }: {
37
+ cwd: string
38
+ conditions: string[]
39
+ }): BunPlugin {
40
+ const consumerSvelte = `${cwd}/node_modules/svelte`
41
+ return {
42
+ name: 'belte-dedupe-svelte',
43
+ async setup(build) {
44
+ const pkgFile = Bun.file(`${consumerSvelte}/package.json`)
45
+ if (!(await pkgFile.exists())) {
46
+ return
47
+ }
48
+ const consumerPackage = (await pkgFile.json()) as {
49
+ exports: Record<string, ExportEntry>
50
+ }
51
+ build.onResolve({ filter: /^svelte(\/.*)?$/ }, (args) => {
52
+ const subpath =
53
+ args.path === 'svelte' ? '.' : `.${args.path.slice('svelte'.length)}`
54
+ const entry = consumerPackage.exports[subpath]
55
+ if (!entry) {
56
+ return undefined
57
+ }
58
+ const resolvedFile = pickExport(entry, conditions)
59
+ if (!resolvedFile) {
60
+ return undefined
61
+ }
62
+ return { path: `${consumerSvelte}/${resolvedFile.replace(/^\.\//, '')}` }
63
+ })
64
+ },
65
+ }
66
+ }