@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,17 @@
1
+ /*
2
+ Holds the AbortController for the current page's resolution stream so a
3
+ client-side navigation can cancel it — freeing the connection and stopping the
4
+ server drain — instead of letting it run to completion for a page that's gone.
5
+ Setting a new controller aborts any prior one.
6
+ */
7
+ let current: AbortController | undefined
8
+
9
+ export function setPageStreamController(controller: AbortController): void {
10
+ current?.abort()
11
+ current = controller
12
+ }
13
+
14
+ export function abortPageStream(): void {
15
+ current?.abort()
16
+ current = undefined
17
+ }
@@ -0,0 +1,12 @@
1
+ import type { StreamingDeferred } from './types/StreamingDeferred.ts'
2
+
3
+ /*
4
+ The single placeholder-recovery primitive: settles a deferred {#await} read with
5
+ a live re-fetch of its request. Used whenever a streamed resolution can't supply
6
+ warm data — a `{ key, miss }` marker (non-snapshottable body) or a placeholder
7
+ the stream never settled (clean EOF with leftovers, or a cut). Keeping the
8
+ re-fetch policy in one place means the apply path and the flush path can't drift.
9
+ */
10
+ export function refetchPlaceholder(deferred: StreamingDeferred): void {
11
+ deferred.resolve(fetch(deferred.request))
12
+ }
@@ -0,0 +1,37 @@
1
+ import { browserClientFlags } from '../shared/browserClientFlags.ts'
2
+ import { buildRpcRequest } from '../shared/buildRpcRequest.ts'
3
+ import { createRemoteFunction } from '../shared/createRemoteFunction.ts'
4
+ import type { HttpVerb } from '../shared/types/HttpVerb.ts'
5
+ import type { RemoteFunction } from '../shared/types/RemoteFunction.ts'
6
+
7
+ /*
8
+ Client-side substitute for a verb-defined handler. The bundler emits one
9
+ call per verb export inside an `$rpc/**` module (GET / POST / …): server
10
+ target uses defineVerb (real handler), browser target uses remoteProxy
11
+ (fetch over the network). Both paths produce identical RemoteFunction
12
+ shapes and identical WeakMap metadata so cache() works the same on either
13
+ side.
14
+
15
+ `url` is the flat rpc route. Args go in the JSON body (POST/PUT/PATCH) or
16
+ the query string (GET/DELETE/HEAD). Plain `fn(args)` decodes the Response
17
+ by Content-Type and throws HttpError on non-2xx; `.raw(args)` is the
18
+ escape hatch that returns the Response untouched.
19
+ */
20
+ export function remoteProxy<Args, Return>(
21
+ method: HttpVerb,
22
+ url: string,
23
+ ): RemoteFunction<Args, Return> {
24
+ return createRemoteFunction<Args, Return>({
25
+ method,
26
+ url,
27
+ clients: browserClientFlags,
28
+ buildRequest: (args) =>
29
+ buildRpcRequest({ method, url, args, baseUrl: window.location.href }),
30
+ /*
31
+ Forcing `getRequest()` once builds the Request and seeds the
32
+ cache meta thunk in createRemoteFunction with the same instance,
33
+ so cache() readers don't reconstruct it.
34
+ */
35
+ invoke: (_args, getRequest) => fetch(getRequest()),
36
+ })
37
+ }
@@ -0,0 +1,192 @@
1
+ import type { SocketClientFrame } from '../server/sockets/types/SocketClientFrame.ts'
2
+ import type { SocketServerFrame } from '../server/sockets/types/SocketServerFrame.ts'
3
+
4
+ type SubCallbacks = {
5
+ onMessage(message: unknown): void
6
+ onError(message: string): void
7
+ onEnd(): void
8
+ }
9
+
10
+ type Channel = {
11
+ subscribe(
12
+ sub: string,
13
+ socket: string,
14
+ replay: number | undefined,
15
+ callbacks: SubCallbacks,
16
+ ): void
17
+ unsubscribe(sub: string): void
18
+ publish(socket: string, message: unknown): void
19
+ }
20
+
21
+ const SOCKETS_PATH = '/__belte/sockets'
22
+
23
+ let singleton: Channel | undefined
24
+
25
+ /*
26
+ Lazily opens the single multiplexed ws used by every socket proxy on
27
+ the page. Routes inbound frames:
28
+ `msg` → all local subs of that socket
29
+ `end` → the matching sub
30
+ `err` → the matching sub
31
+
32
+ `msg` frames carry no sub id: one publish from the server fans out to
33
+ every connected ws via Bun's native publish, and each ws delivers the
34
+ message to every local sub of that socket. `end`/`err` are per-sub
35
+ because they're subscription-lifecycle events, not data.
36
+
37
+ Outbound frames sent before `ws.onopen` fires are queued and flushed
38
+ on open. The channel reconnects on close with bounded backoff;
39
+ in-flight subs are torn down with a synthetic error so consumers'
40
+ `for await` loops can surface the disconnect, then the connection
41
+ comes back up and fresh subs can be opened. We intentionally do not
42
+ silently re-subscribe across a reconnect — most socket consumers need
43
+ to reconcile state on a fresh connection (e.g. re-fetch a snapshot
44
+ before reapplying deltas), so the framework hands the disconnect to
45
+ user code instead of papering over it.
46
+ */
47
+ export function getSocketChannel(): Channel {
48
+ if (singleton) {
49
+ return singleton
50
+ }
51
+ const subs = new Map<string, { socket: string; callbacks: SubCallbacks }>()
52
+ const subsBySocket = new Map<string, Set<string>>()
53
+ let ws: WebSocket | undefined
54
+ let pendingSends: string[] = []
55
+ let backoffMs = 250
56
+
57
+ function flushPending(): void {
58
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
59
+ return
60
+ }
61
+ for (const message of pendingSends) {
62
+ ws.send(message)
63
+ }
64
+ pendingSends = []
65
+ }
66
+
67
+ function send(frame: SocketClientFrame): void {
68
+ const message = JSON.stringify(frame)
69
+ if (ws && ws.readyState === WebSocket.OPEN) {
70
+ ws.send(message)
71
+ return
72
+ }
73
+ pendingSends.push(message)
74
+ connect()
75
+ }
76
+
77
+ function connect(): void {
78
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
79
+ return
80
+ }
81
+ const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
82
+ ws = new WebSocket(`${scheme}//${window.location.host}${SOCKETS_PATH}`)
83
+ ws.addEventListener('open', () => {
84
+ backoffMs = 250
85
+ flushPending()
86
+ })
87
+ ws.addEventListener('message', (event) => {
88
+ let frame: SocketServerFrame
89
+ try {
90
+ frame = JSON.parse(event.data) as SocketServerFrame
91
+ } catch {
92
+ return
93
+ }
94
+ if (frame.type === 'msg') {
95
+ /*
96
+ One Bun-published frame fans out to every local sub of
97
+ that socket on this ws — addressed by socket name, not
98
+ per-sub id.
99
+ */
100
+ const targets = subsBySocket.get(frame.socket)
101
+ if (!targets) {
102
+ return
103
+ }
104
+ for (const subId of targets) {
105
+ subs.get(subId)?.callbacks.onMessage(frame.message)
106
+ }
107
+ return
108
+ }
109
+ if (frame.type === 'end') {
110
+ const sub = subs.get(frame.sub)
111
+ if (!sub) {
112
+ return
113
+ }
114
+ dropSub(frame.sub)
115
+ sub.callbacks.onEnd()
116
+ return
117
+ }
118
+ if (frame.type === 'err') {
119
+ const sub = subs.get(frame.sub)
120
+ if (!sub) {
121
+ return
122
+ }
123
+ dropSub(frame.sub)
124
+ sub.callbacks.onError(frame.message)
125
+ return
126
+ }
127
+ })
128
+ ws.addEventListener('close', () => {
129
+ const active = [...subs.entries()]
130
+ subs.clear()
131
+ subsBySocket.clear()
132
+ for (const [, sub] of active) {
133
+ sub.callbacks.onError('socket channel disconnected')
134
+ }
135
+ /*
136
+ Drop any queued frames too. We've just torn down every local
137
+ sub, so replaying their `sub`/`unsub`/`pub` frames on
138
+ reconnect would open ghost subscriptions on the server that
139
+ no client object tracks (and never gets an `unsub`). This
140
+ keeps the "no silent re-subscribe across a reconnect"
141
+ contract above honest — consumers re-open fresh subs.
142
+ */
143
+ const hadPending = pendingSends.length > 0
144
+ pendingSends = []
145
+ ws = undefined
146
+ if (active.length === 0 && !hadPending) {
147
+ return
148
+ }
149
+ setTimeout(connect, backoffMs)
150
+ backoffMs = Math.min(backoffMs * 2, 5000)
151
+ })
152
+ }
153
+
154
+ function dropSub(id: string): void {
155
+ const entry = subs.get(id)
156
+ if (!entry) {
157
+ return
158
+ }
159
+ subs.delete(id)
160
+ const set = subsBySocket.get(entry.socket)
161
+ if (set) {
162
+ set.delete(id)
163
+ if (set.size === 0) {
164
+ subsBySocket.delete(entry.socket)
165
+ }
166
+ }
167
+ }
168
+
169
+ singleton = {
170
+ subscribe(id, socket, replay, callbacks) {
171
+ subs.set(id, { socket, callbacks })
172
+ let set = subsBySocket.get(socket)
173
+ if (!set) {
174
+ set = new Set()
175
+ subsBySocket.set(socket, set)
176
+ }
177
+ set.add(id)
178
+ send({ type: 'sub', sub: id, socket, replay })
179
+ },
180
+ unsubscribe(id) {
181
+ if (!subs.has(id)) {
182
+ return
183
+ }
184
+ dropSub(id)
185
+ send({ type: 'unsub', sub: id })
186
+ },
187
+ publish(socket, message) {
188
+ send({ type: 'pub', socket, message })
189
+ },
190
+ }
191
+ return singleton
192
+ }
@@ -0,0 +1,57 @@
1
+ import type { Socket } from '../server/sockets/types/Socket.ts'
2
+ import { browserClientFlags } from '../shared/browserClientFlags.ts'
3
+ import { createPushIterator } from '../shared/createPushIterator.ts'
4
+ import { getSocketChannel } from './socketChannel.ts'
5
+
6
+ let nextId = 0
7
+
8
+ /*
9
+ Client-side substitute for a server-declared Socket. The bundler emits
10
+ one call per socket export under `src/server/sockets/`: server target uses
11
+ defineSocket (real fan-out), browser target uses socketProxy (subscribe
12
+ over the multiplexed ws channel). Both paths produce identical Socket
13
+ shapes so user code reads the same on either side.
14
+
15
+ Bare iteration opens a subscription with full history replay; `.tail(n)`
16
+ opens one that replays the last `n` items (default `0`, clamped server-
17
+ side to the topic's history max). Each subscription mints its own id
18
+ used to route lifecycle frames (`end`, `err`). Calling `.publish` sends
19
+ a `pub` frame the server validates against the topic's
20
+ `allowClientPublish` policy — there is no client-side enforcement, so a
21
+ publish attempt on a server-only topic is silently dropped server-side.
22
+
23
+ Backpressure is unbounded — a slow consumer with a chatty socket will
24
+ grow the per-iterator buffer; bounded policies belong in a future
25
+ socketProxy API, not the wire layer.
26
+ */
27
+ export function socketProxy<T>(name: string): Socket<T> {
28
+ /*
29
+ replay === undefined → full history replay (bare for-await);
30
+ replay: number → trailing-n replay, clamped by the server.
31
+ */
32
+ function iterate(replay: number | undefined): AsyncIterable<T> {
33
+ return {
34
+ [Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
35
+ const id = `s${++nextId}`
36
+ const channel = getSocketChannel()
37
+ const iter = createPushIterator<T>(() => channel.unsubscribe(id))
38
+ channel.subscribe(id, name, replay, {
39
+ onMessage: (value) => iter.push(value as T),
40
+ onEnd: () => iter.end(),
41
+ onError: (message) => iter.error(message),
42
+ })
43
+ return iter
44
+ },
45
+ }
46
+ }
47
+
48
+ return {
49
+ name,
50
+ clients: browserClientFlags,
51
+ publish(message: T) {
52
+ getSocketChannel().publish(name, message)
53
+ },
54
+ tail: (count = 0) => iterate(count),
55
+ [Symbol.asyncIterator]: () => iterate(undefined)[Symbol.asyncIterator](),
56
+ }
57
+ }
@@ -0,0 +1,153 @@
1
+ import { hydrate } from 'svelte'
2
+ import App from '../../App.svelte'
3
+ import { createCacheStore } from '../shared/createCacheStore.ts'
4
+ import { setCacheStoreResolver } from '../shared/setCacheStoreResolver.ts'
5
+ import { setGlobalCacheStoreResolver } from '../shared/setGlobalCacheStoreResolver.ts'
6
+ import type { CacheSnapshotEntry } from '../shared/types/CacheSnapshotEntry.ts'
7
+ import type { CacheStore } from '../shared/types/CacheStore.ts'
8
+ import type { StreamingPlaceholder } from '../shared/types/StreamingPlaceholder.ts'
9
+ import { cacheEntryFromSnapshot } from './cacheEntryFromSnapshot.ts'
10
+ import { installStreamingPlaceholders } from './installStreamingPlaceholders.ts'
11
+ import { openResolveStream } from './openResolveStream.ts'
12
+ import { bindPage, handlePopstate, navigate, page, renderState } from './page.svelte.ts'
13
+ import type { Layouts } from './types/Layouts.ts'
14
+ import type { Pages } from './types/Pages.ts'
15
+
16
+ declare global {
17
+ interface Window {
18
+ __SSR__: {
19
+ route: string
20
+ params: Record<string, string>
21
+ cache?: CacheSnapshotEntry[]
22
+ /* Pending {#await} keys the client pre-creates placeholders for. */
23
+ streaming?: StreamingPlaceholder[]
24
+ /* Single-use token for the out-of-band resolution stream. */
25
+ streamToken?: string
26
+ /* A server-rendered error.svelte page — static, nothing to hydrate. */
27
+ error?: boolean
28
+ }
29
+ }
30
+ }
31
+
32
+ /*
33
+ Pre-populates the client cache store with response entries captured during SSR.
34
+ Each becomes an already-resolved Response so the first hydration pass finds the
35
+ data via cache() without a network round-trip.
36
+ */
37
+ function hydrateCacheFromSnapshot(store: CacheStore, snapshot: CacheSnapshotEntry[]): void {
38
+ for (const entry of snapshot) {
39
+ store.entries.set(entry.key, cacheEntryFromSnapshot(entry))
40
+ }
41
+ }
42
+
43
+ function isInternalLinkEvent(event: MouseEvent): HTMLAnchorElement | undefined {
44
+ if (event.defaultPrevented) {
45
+ return undefined
46
+ }
47
+ if (event.button !== 0) {
48
+ return undefined
49
+ }
50
+ if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
51
+ return undefined
52
+ }
53
+ const anchor = (event.target as HTMLElement | null)?.closest?.('a')
54
+ if (!anchor) {
55
+ return undefined
56
+ }
57
+ if (anchor.target && anchor.target !== '_self') {
58
+ return undefined
59
+ }
60
+ if (anchor.hasAttribute('download')) {
61
+ return undefined
62
+ }
63
+ if (anchor.getAttribute('rel')?.includes('external')) {
64
+ return undefined
65
+ }
66
+ const href = anchor.getAttribute('href')
67
+ if (!href || href.startsWith('#')) {
68
+ return undefined
69
+ }
70
+ const url = new URL(href, window.location.href)
71
+ if (url.origin !== window.location.origin) {
72
+ return undefined
73
+ }
74
+ return anchor
75
+ }
76
+
77
+ /*
78
+ Hydrates the SSR'd document against the SSR payload on `window.__SSR__`,
79
+ then intercepts internal link clicks (delegating to navigate) and popstate
80
+ events. The page module owns the route/Page/Layout state and the
81
+ URL-resolution logic; this entry just wires the cache store, runs the
82
+ initial bind, and attaches the global listeners. App.svelte receives the
83
+ public `page` proxy plus the internal renderState so the same reactive
84
+ objects update across navigations.
85
+ */
86
+ export async function startClient({
87
+ pages,
88
+ layouts,
89
+ }: {
90
+ pages: Pages
91
+ layouts?: Layouts
92
+ }): Promise<void> {
93
+ const target = document.getElementById('app')
94
+ if (!target) {
95
+ throw new Error('[belte] missing #app target')
96
+ }
97
+
98
+ /*
99
+ A server-rendered error.svelte (404 / page-render failure) ships static HTML
100
+ with no route to hydrate against — leave the markup as-is and wire nothing.
101
+ */
102
+ if (window.__SSR__.error) {
103
+ return
104
+ }
105
+
106
+ const cacheStore = createCacheStore()
107
+ setCacheStoreResolver(() => cacheStore)
108
+ /* One tab store: cache(fn, { global: true }) shares it, so global is a no-op here. */
109
+ setGlobalCacheStoreResolver(() => cacheStore)
110
+ if (window.__SSR__.cache) {
111
+ hydrateCacheFromSnapshot(cacheStore, window.__SSR__.cache)
112
+ }
113
+
114
+ /*
115
+ Install placeholders for pending {#await} keys before hydrate(), so cache()
116
+ reads hit a placeholder on first evaluation instead of firing their own
117
+ fetch, then open the out-of-band resolution stream to settle them. The fetch
118
+ runs in the background — hydration doesn't wait on it.
119
+ */
120
+ const deferreds = installStreamingPlaceholders(cacheStore, window.__SSR__.streaming ?? [])
121
+ if (window.__SSR__.streamToken && deferreds.size > 0) {
122
+ void openResolveStream(window.__SSR__.streamToken, cacheStore, deferreds)
123
+ }
124
+
125
+ try {
126
+ await bindPage({ pages, layouts, ssr: window.__SSR__ })
127
+ hydrate(App, { target, props: { state: { page, render: renderState } } })
128
+ } catch (err) {
129
+ console.error('[belte] initial hydration failed', err)
130
+ }
131
+
132
+ document.addEventListener('click', (event) => {
133
+ const anchor = isInternalLinkEvent(event)
134
+ if (!anchor) {
135
+ return
136
+ }
137
+ const url = new URL(anchor.href, window.location.href)
138
+ /*
139
+ Hash-only same-page navigations fall through to the browser so the
140
+ native scroll-into-view for `#anchor` targets keeps working.
141
+ Anything else (pathname, search, or pathname+hash combo) goes
142
+ through navigate() — it pushes history, refreshes page state, and
143
+ short-circuits the JSON resolve when only search/hash differ.
144
+ */
145
+ if (url.pathname === window.location.pathname && url.search === window.location.search) {
146
+ return
147
+ }
148
+ event.preventDefault()
149
+ void navigate(`${url.pathname}${url.search}${url.hash}`)
150
+ })
151
+
152
+ window.addEventListener('popstate', handlePopstate)
153
+ }
@@ -0,0 +1,131 @@
1
+ import { createSubscriber } from 'svelte/reactivity'
2
+ import type { Subscribable } from '../shared/types/Subscribable.ts'
3
+
4
+ type SubscriptionStatus = 'pending' | 'open' | 'done' | 'error'
5
+
6
+ type Entry<T> = {
7
+ latest: T | undefined
8
+ error: Error | undefined
9
+ status: SubscriptionStatus
10
+ tap: () => void
11
+ }
12
+
13
+ const registry = new Map<string, Entry<unknown>>()
14
+
15
+ /*
16
+ Reactive consumer for streaming sources. Takes a Subscribable<T> — the
17
+ shape both `Socket<T>` (declared under src/server/sockets/) and the result of
18
+ `fn.stream(args)` satisfy:
19
+
20
+ const latest = $derived(subscribe(chat)) // socket
21
+ const latest = $derived(subscribe(tickFeed.stream())) // rpc stream (no args)
22
+ const latest = $derived(subscribe(countLog.stream({ to: 5 }))) // rpc stream
23
+
24
+ Lifecycle mirrors cache(): the entry's tracker is a Svelte
25
+ createSubscriber, so the first $derived read in a tracking scope opens
26
+ the underlying iterator (with history replay on a Socket, or a fresh
27
+ fetch on an rpc stream), and the last $derived to stop reading closes
28
+ it. Many $deriveds reading the same source share one underlying
29
+ subscription — the registry dedupes by `subscribable.name`, which is
30
+ the socket name for declared sockets and `keyForRemoteCall(method, url,
31
+ args)` for rpc streams. So passing fresh `fn.stream(args)` Subscribables
32
+ across re-renders is safe: same args → same key → shared subscription.
33
+
34
+ Subscribe is a no-op on the server (returns undefined) — SSR can't
35
+ keep a stream open across the request boundary. Pages that want a
36
+ seeded value in the initial HTML should fetch via cache() against an
37
+ HTTP rpc handler and layer subscribe() on top for live updates after
38
+ hydration.
39
+
40
+ Errors are surfaced through subscribe.error(x) rather than thrown, so
41
+ reading `latest` from a $derived can't crash the component. Status
42
+ distinguishes "haven't received the first frame" (pending) from
43
+ "stream ended cleanly" (done) and "wire layer surfaced an error"
44
+ (error).
45
+ */
46
+ export function subscribe<T>(subscribable: Subscribable<T>): T | undefined {
47
+ return readField(subscribable, 'latest') as T | undefined
48
+ }
49
+
50
+ subscribe.error = function subscribeError<T>(subscribable: Subscribable<T>): Error | undefined {
51
+ return readField(subscribable, 'error') as Error | undefined
52
+ }
53
+
54
+ subscribe.status = function subscribeStatus<T>(subscribable: Subscribable<T>): SubscriptionStatus {
55
+ return (readField(subscribable, 'status') as SubscriptionStatus | undefined) ?? 'pending'
56
+ }
57
+
58
+ function readField<T, K extends keyof Entry<T>>(
59
+ subscribable: Subscribable<T>,
60
+ field: K,
61
+ ): Entry<T>[K] | undefined {
62
+ if (typeof window === 'undefined') {
63
+ if (field === 'status') {
64
+ return 'pending' as Entry<T>[K]
65
+ }
66
+ return undefined
67
+ }
68
+ const entry = getOrCreateEntry(subscribable) as Entry<T>
69
+ entry.tap()
70
+ return entry[field]
71
+ }
72
+
73
+ function getOrCreateEntry<T>(subscribable: Subscribable<T>): Entry<T> {
74
+ const key = subscribable.name
75
+ const cached = registry.get(key) as Entry<T> | undefined
76
+ if (cached) {
77
+ return cached
78
+ }
79
+ const entry: Entry<T> = {
80
+ latest: undefined,
81
+ error: undefined,
82
+ status: 'pending',
83
+ tap: () => undefined,
84
+ }
85
+ entry.tap = createSubscriber((update) => {
86
+ entry.latest = undefined
87
+ entry.error = undefined
88
+ entry.status = 'pending'
89
+ const iterator = subscribable[Symbol.asyncIterator]()
90
+ let cancelled = false
91
+ ;(async () => {
92
+ try {
93
+ while (!cancelled) {
94
+ const next = await iterator.next()
95
+ if (next.done) {
96
+ if (!cancelled) {
97
+ if (entry.status !== 'error') {
98
+ entry.status = 'done'
99
+ }
100
+ update()
101
+ }
102
+ return
103
+ }
104
+ entry.latest = next.value
105
+ entry.status = 'open'
106
+ update()
107
+ }
108
+ } catch (error) {
109
+ if (!cancelled) {
110
+ entry.error = error instanceof Error ? error : new Error(String(error))
111
+ entry.status = 'error'
112
+ update()
113
+ }
114
+ }
115
+ })()
116
+ return () => {
117
+ cancelled = true
118
+ iterator.return?.(undefined)?.catch(() => undefined)
119
+ /*
120
+ Identity-guard the eviction: if a fresh Subscribable with the
121
+ same name has already replaced us in the registry, this stale
122
+ cleanup must not nuke the new entry.
123
+ */
124
+ if (registry.get(key) === (entry as Entry<unknown>)) {
125
+ registry.delete(key)
126
+ }
127
+ }
128
+ })
129
+ registry.set(key, entry as Entry<unknown>)
130
+ return entry
131
+ }
@@ -0,0 +1,9 @@
1
+ import type { Component } from 'svelte'
2
+
3
+ /*
4
+ Manifest of directory prefix → error.svelte module loader. The deepest prefix
5
+ that is an ancestor of the failed path wins (nearest-only, like layouts). An
6
+ error.svelte renders on the server for an unknown route (404) or a throw during
7
+ a page render; the component receives `{ status, message }` props.
8
+ */
9
+ export type Errors = Record<string, () => Promise<{ default: Component }>>
@@ -0,0 +1,7 @@
1
+ import type { Component } from 'svelte'
2
+
3
+ /*
4
+ Manifest of directory prefix → layout.svelte module loader. The deepest
5
+ prefix that is an ancestor of a route wins (no stacking).
6
+ */
7
+ export type Layouts = Record<string, () => Promise<{ default: Component }>>
@@ -0,0 +1,7 @@
1
+ import type { Component } from 'svelte'
2
+
3
+ /*
4
+ Manifest of route URL → page.svelte module loader. Produced by the resolver
5
+ plugin from `page.svelte` files anywhere under src/browser/pages.
6
+ */
7
+ export type Pages = Record<string, () => Promise<{ default: Component }>>
@@ -0,0 +1,9 @@
1
+ /*
2
+ A pending {#await} read's deferred entry. `resolve` settles the placeholder
3
+ promise the cache handed out (with the streamed Response or a live re-fetch);
4
+ `request` is what to re-fetch on a miss or a cut stream.
5
+ */
6
+ export type StreamingDeferred = {
7
+ resolve: (response: Promise<Response>) => void
8
+ request: Request
9
+ }
@@ -0,0 +1,11 @@
1
+ import type { BundleMenuItem } from './BundleMenuItem.ts'
2
+
3
+ /*
4
+ A top-level bundle menu, inserted into the macOS menu bar between the standard
5
+ Edit and Window menus. `label` titles the menu; `items` are its entries top to
6
+ bottom.
7
+ */
8
+ export type BundleMenu = {
9
+ label: string
10
+ items: BundleMenuItem[]
11
+ }
@@ -0,0 +1,24 @@
1
+ /*
2
+ A single entry in a bundle menu. Serializable data — the native shim builds the
3
+ matching NSMenuItem. Either a divider or a clickable item that dispatches a
4
+ `belte:menu` CustomEvent into the page (detail `{ name }`); the app's own code
5
+ handles it:
6
+
7
+ window.addEventListener('belte:menu', (event) => {
8
+ if (event.detail.name === 'sync') syncNow()
9
+ })
10
+
11
+ Emitting an event (rather than calling a verb directly) is what lets a menu
12
+ drive parameterised work: a click carries no arguments, so the app computes
13
+ them and makes the call itself. `shortcut` is the key for the Cmd-based
14
+ equivalent (e.g. `'r'` → Cmd-R).
15
+
16
+ A `navigate` item moves the window instead of talking to the page: clicking it
17
+ calls `webview_navigate` with the given URL (the native side, on the UI thread).
18
+ That's how the built-in Server menu drives the connect screen — `emit` reaches
19
+ the loaded page, `navigate` repoints the window itself.
20
+ */
21
+ export type BundleMenuItem =
22
+ | { separator: true }
23
+ | { label: string; shortcut?: string; emit: string }
24
+ | { label: string; shortcut?: string; navigate: string }