@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.
- package/CHANGELOG.md +313 -0
- package/LICENSE +21 -0
- package/README.md +559 -0
- package/bin/belte.ts +183 -0
- package/package.json +110 -0
- package/src/App.svelte +31 -0
- package/src/appEntry.ts +151 -0
- package/src/assets/app.html +14 -0
- package/src/belteResolverPlugin.ts +858 -0
- package/src/build.ts +147 -0
- package/src/buildCli.ts +129 -0
- package/src/buildDisconnected.ts +122 -0
- package/src/bundleApp.ts +149 -0
- package/src/bundleDisconnectedEntry.ts +17 -0
- package/src/cliEntry.ts +25 -0
- package/src/clientBuildPlugins.ts +41 -0
- package/src/clientEntry.ts +7 -0
- package/src/compile.ts +64 -0
- package/src/controlServerWorker.ts +422 -0
- package/src/dedupeSveltePlugin.ts +66 -0
- package/src/devEntry.ts +169 -0
- package/src/discoveryEntry.ts +81 -0
- package/src/lib/browser/applyStreamedResolution.ts +33 -0
- package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
- package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
- package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
- package/src/lib/browser/openResolveStream.ts +42 -0
- package/src/lib/browser/page.svelte.ts +258 -0
- package/src/lib/browser/pageStreamController.ts +17 -0
- package/src/lib/browser/refetchPlaceholder.ts +12 -0
- package/src/lib/browser/remoteProxy.ts +37 -0
- package/src/lib/browser/socketChannel.ts +192 -0
- package/src/lib/browser/socketProxy.ts +57 -0
- package/src/lib/browser/startClient.ts +153 -0
- package/src/lib/browser/subscribe.ts +131 -0
- package/src/lib/browser/types/Errors.ts +9 -0
- package/src/lib/browser/types/Layouts.ts +7 -0
- package/src/lib/browser/types/Pages.ts +7 -0
- package/src/lib/browser/types/StreamingDeferred.ts +9 -0
- package/src/lib/bundle/BundleMenu.ts +11 -0
- package/src/lib/bundle/BundleMenuItem.ts +24 -0
- package/src/lib/bundle/BundleWindow.ts +36 -0
- package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
- package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
- package/src/lib/bundle/bindConnectedFlag.ts +29 -0
- package/src/lib/bundle/bindRequestNavigate.ts +31 -0
- package/src/lib/bundle/buildWebviewLib.ts +111 -0
- package/src/lib/bundle/disconnected.css +9 -0
- package/src/lib/bundle/disconnected.svelte +386 -0
- package/src/lib/bundle/ensureWebviewLib.ts +20 -0
- package/src/lib/bundle/exitWithParent.ts +28 -0
- package/src/lib/bundle/infoPlist.ts +46 -0
- package/src/lib/bundle/installDownloads.ts +24 -0
- package/src/lib/bundle/installMacMenu.ts +39 -0
- package/src/lib/bundle/listenLocalControlServer.ts +19 -0
- package/src/lib/bundle/native/belteMenu.mm +422 -0
- package/src/lib/bundle/native/webview.h +4557 -0
- package/src/lib/bundle/onMenu.ts +41 -0
- package/src/lib/bundle/openWebview.ts +104 -0
- package/src/lib/bundle/pngToIcns.ts +47 -0
- package/src/lib/bundle/probeBelteServer.ts +34 -0
- package/src/lib/bundle/resolveServerBinary.ts +12 -0
- package/src/lib/bundle/resolveWebviewLib.ts +53 -0
- package/src/lib/bundle/serverBinaryFilename.ts +8 -0
- package/src/lib/bundle/signMacApp.ts +35 -0
- package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
- package/src/lib/bundle/stableLocalPort.ts +19 -0
- package/src/lib/bundle/waitForServer.ts +23 -0
- package/src/lib/bundle/webviewCachePath.ts +23 -0
- package/src/lib/bundle/webviewLibName.ts +11 -0
- package/src/lib/cli/connectToServer.ts +23 -0
- package/src/lib/cli/createClient.ts +170 -0
- package/src/lib/cli/dispatchCommand.ts +71 -0
- package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
- package/src/lib/cli/parseArgvForRpc.ts +97 -0
- package/src/lib/cli/printHelp.ts +119 -0
- package/src/lib/cli/printSessionHelp.ts +27 -0
- package/src/lib/cli/printSessionStatus.ts +21 -0
- package/src/lib/cli/printTrimmed.ts +8 -0
- package/src/lib/cli/printValue.ts +10 -0
- package/src/lib/cli/resolveCliTarget.ts +48 -0
- package/src/lib/cli/runCli.ts +139 -0
- package/src/lib/cli/runSession.ts +105 -0
- package/src/lib/cli/startLocalInstance.ts +14 -0
- package/src/lib/cli/tokenizeLine.ts +51 -0
- package/src/lib/cli/types/CliManifest.ts +9 -0
- package/src/lib/cli/types/CliManifestEntry.ts +17 -0
- package/src/lib/cli/types/CliTarget.ts +13 -0
- package/src/lib/mcp/annotationsForMethod.ts +29 -0
- package/src/lib/mcp/createMcpResourceServer.ts +101 -0
- package/src/lib/mcp/createMcpServer.ts +42 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
- package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
- package/src/lib/mcp/mcpSurface.ts +265 -0
- package/src/lib/mcp/toolResultFromResponse.ts +66 -0
- package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
- package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
- package/src/lib/mcp/types/McpResourceContents.ts +10 -0
- package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
- package/src/lib/mcp/types/McpResourceServer.ts +12 -0
- package/src/lib/mcp/types/McpServer.ts +9 -0
- package/src/lib/mcp/types/McpServerOptions.ts +16 -0
- package/src/lib/server/AppModule.ts +33 -0
- package/src/lib/server/DELETE.ts +9 -0
- package/src/lib/server/GET.ts +9 -0
- package/src/lib/server/HEAD.ts +9 -0
- package/src/lib/server/PATCH.ts +9 -0
- package/src/lib/server/POST.ts +9 -0
- package/src/lib/server/PUT.ts +9 -0
- package/src/lib/server/agent.ts +76 -0
- package/src/lib/server/appDataDir.ts +15 -0
- package/src/lib/server/cli/buildEnvContent.ts +19 -0
- package/src/lib/server/cli/createTarGz.ts +76 -0
- package/src/lib/server/cli/handleCliDownload.ts +153 -0
- package/src/lib/server/cli/handleCliInstall.ts +37 -0
- package/src/lib/server/cli/installScript.ts +29 -0
- package/src/lib/server/cli/maxSourceMtime.ts +26 -0
- package/src/lib/server/cookies.ts +29 -0
- package/src/lib/server/env.ts +50 -0
- package/src/lib/server/error.ts +70 -0
- package/src/lib/server/json.ts +28 -0
- package/src/lib/server/jsonl.ts +46 -0
- package/src/lib/server/prompts/definePrompt.ts +20 -0
- package/src/lib/server/prompts/promptRegistry.ts +9 -0
- package/src/lib/server/prompts/registerPrompt.ts +6 -0
- package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
- package/src/lib/server/prompts/types/Prompt.ts +13 -0
- package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
- package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
- package/src/lib/server/redirect.ts +42 -0
- package/src/lib/server/request.ts +18 -0
- package/src/lib/server/rpc/defineVerb.ts +133 -0
- package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
- package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
- package/src/lib/server/rpc/parseArgs.ts +95 -0
- package/src/lib/server/rpc/registerVerb.ts +6 -0
- package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
- package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
- package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
- package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
- package/src/lib/server/rpc/unprocessed.ts +14 -0
- package/src/lib/server/rpc/verbRegistry.ts +11 -0
- package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
- package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
- package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
- package/src/lib/server/runtime/acceptsZstd.ts +8 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
- package/src/lib/server/runtime/containsTraversal.ts +37 -0
- package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
- package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
- package/src/lib/server/runtime/createServer.ts +692 -0
- package/src/lib/server/runtime/devReloadResponse.ts +35 -0
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
- package/src/lib/server/runtime/envSchemaStore.ts +15 -0
- package/src/lib/server/runtime/findOpenPort.ts +35 -0
- package/src/lib/server/runtime/getActiveServer.ts +6 -0
- package/src/lib/server/runtime/globToPathSet.ts +29 -0
- package/src/lib/server/runtime/inProcessServer.ts +20 -0
- package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
- package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
- package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
- package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
- package/src/lib/server/runtime/mimeForExtension.ts +20 -0
- package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
- package/src/lib/server/runtime/parsePort.ts +11 -0
- package/src/lib/server/runtime/registryManifests.ts +66 -0
- package/src/lib/server/runtime/requestContext.ts +5 -0
- package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
- package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
- package/src/lib/server/runtime/serverSlot.ts +13 -0
- package/src/lib/server/runtime/setActiveServer.ts +6 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
- package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
- package/src/lib/server/runtime/streamFromIterator.ts +86 -0
- package/src/lib/server/runtime/streamStash.ts +64 -0
- package/src/lib/server/runtime/types/Assets.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +27 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
- package/src/lib/server/server.ts +32 -0
- package/src/lib/server/socket.ts +31 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
- package/src/lib/server/sockets/defineSocket.ts +167 -0
- package/src/lib/server/sockets/lookupSocket.ts +6 -0
- package/src/lib/server/sockets/recentHistory.ts +11 -0
- package/src/lib/server/sockets/registerSocket.ts +6 -0
- package/src/lib/server/sockets/socketOperations.ts +35 -0
- package/src/lib/server/sockets/socketRegistry.ts +9 -0
- package/src/lib/server/sockets/types/Socket.ts +21 -0
- package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
- package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
- package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
- package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
- package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
- package/src/lib/server/sse.ts +53 -0
- package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
- package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
- package/src/lib/shared/HttpError.ts +19 -0
- package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
- package/src/lib/shared/activeCacheStore.ts +20 -0
- package/src/lib/shared/appDataDir.ts +34 -0
- package/src/lib/shared/belteImportName.ts +44 -0
- package/src/lib/shared/browserClientFlags.ts +10 -0
- package/src/lib/shared/buildRpcRequest.ts +70 -0
- package/src/lib/shared/bundleLayout.ts +36 -0
- package/src/lib/shared/bundled.ts +34 -0
- package/src/lib/shared/cache.ts +559 -0
- package/src/lib/shared/cacheStoreSlot.ts +16 -0
- package/src/lib/shared/canonicalJson.ts +63 -0
- package/src/lib/shared/carriesBodyArgs.ts +13 -0
- package/src/lib/shared/clearLastConnection.ts +7 -0
- package/src/lib/shared/commandNameForUrl.ts +17 -0
- package/src/lib/shared/createCacheStore.ts +75 -0
- package/src/lib/shared/createPushIterator.ts +93 -0
- package/src/lib/shared/createRemoteFunction.ts +99 -0
- package/src/lib/shared/decodeResponse.ts +47 -0
- package/src/lib/shared/detectTarget.ts +27 -0
- package/src/lib/shared/exeSuffix.ts +9 -0
- package/src/lib/shared/exitOnBuildFailure.ts +17 -0
- package/src/lib/shared/extraForwardHeaders.ts +16 -0
- package/src/lib/shared/fileStem.ts +9 -0
- package/src/lib/shared/findExportCallSite.ts +479 -0
- package/src/lib/shared/forwardHeaders.ts +41 -0
- package/src/lib/shared/getRemoteMeta.ts +5 -0
- package/src/lib/shared/globalCacheStore.ts +15 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
- package/src/lib/shared/importNamesToStrip.ts +13 -0
- package/src/lib/shared/invalidateEvent.ts +11 -0
- package/src/lib/shared/isCompileTarget.ts +15 -0
- package/src/lib/shared/isDebugEnabled.ts +23 -0
- package/src/lib/shared/isModuleNotFound.ts +16 -0
- package/src/lib/shared/isReadOnlyMethod.ts +14 -0
- package/src/lib/shared/isStreamingResponse.ts +11 -0
- package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
- package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
- package/src/lib/shared/jsonlErrorFrame.ts +24 -0
- package/src/lib/shared/keyForRemoteCall.ts +29 -0
- package/src/lib/shared/lastConnectionPath.ts +7 -0
- package/src/lib/shared/loadEnvFile.ts +17 -0
- package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
- package/src/lib/shared/loadSvelteConfig.ts +18 -0
- package/src/lib/shared/log.ts +104 -0
- package/src/lib/shared/manifestModule.ts +39 -0
- package/src/lib/shared/memoizeByKey.ts +24 -0
- package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
- package/src/lib/shared/normalizeTarget.ts +10 -0
- package/src/lib/shared/pageUrlForFile.ts +14 -0
- package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
- package/src/lib/shared/parseEnv.ts +30 -0
- package/src/lib/shared/parsePromptMarkdown.ts +34 -0
- package/src/lib/shared/parseRouteSegments.ts +22 -0
- package/src/lib/shared/prepareRpcModule.ts +59 -0
- package/src/lib/shared/prepareSocketModule.ts +49 -0
- package/src/lib/shared/programNameForPackage.ts +14 -0
- package/src/lib/shared/promptNameForFile.ts +10 -0
- package/src/lib/shared/queryStringFromArgs.ts +27 -0
- package/src/lib/shared/readEnvFile.ts +15 -0
- package/src/lib/shared/readLastConnection.ts +18 -0
- package/src/lib/shared/readPackageJson.ts +9 -0
- package/src/lib/shared/recordRemoteMeta.ts +5 -0
- package/src/lib/shared/remoteMetaStore.ts +16 -0
- package/src/lib/shared/resolveClientFlags.ts +20 -0
- package/src/lib/shared/responseErrorText.ts +9 -0
- package/src/lib/shared/rpcUrlForFile.ts +19 -0
- package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
- package/src/lib/shared/serializeEnv.ts +18 -0
- package/src/lib/shared/setCacheStoreResolver.ts +6 -0
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
- package/src/lib/shared/socketNameForFile.ts +11 -0
- package/src/lib/shared/sseErrorFrame.ts +29 -0
- package/src/lib/shared/streamResponse.ts +169 -0
- package/src/lib/shared/stripImport.ts +27 -0
- package/src/lib/shared/subscribableFromResponse.ts +51 -0
- package/src/lib/shared/toBunRoutePattern.ts +28 -0
- package/src/lib/shared/types/CacheEntry.ts +63 -0
- package/src/lib/shared/types/CacheInvalidation.ts +9 -0
- package/src/lib/shared/types/CacheOptions.ts +33 -0
- package/src/lib/shared/types/CacheSnapshot.ts +16 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
- package/src/lib/shared/types/CacheStore.ts +32 -0
- package/src/lib/shared/types/ClientFlags.ts +11 -0
- package/src/lib/shared/types/CompileTarget.ts +6 -0
- package/src/lib/shared/types/HttpVerb.ts +1 -0
- package/src/lib/shared/types/LastConnection.ts +9 -0
- package/src/lib/shared/types/PromptArgument.ts +12 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
- package/src/lib/shared/types/RemoteFunction.ts +42 -0
- package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
- package/src/lib/shared/types/StreamedResolution.ts +10 -0
- package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
- package/src/lib/shared/types/Subscribable.ts +15 -0
- package/src/lib/shared/types/SvelteConfig.ts +5 -0
- package/src/lib/shared/withJsonSchema.ts +20 -0
- package/src/lib/shared/writeLastConnection.ts +13 -0
- package/src/lib/shared/writeRoutesDts.ts +67 -0
- package/src/lib/test/clearVerbRegistry.ts +11 -0
- package/src/lib/test/createTestClient.ts +78 -0
- package/src/preload.ts +20 -0
- package/src/scaffold.ts +92 -0
- package/src/serverBuildPlugins.ts +25 -0
- package/src/serverEntry.ts +94 -0
- package/src/sveltePlugin.ts +58 -0
- package/src/tailwindStylePreprocessor.ts +62 -0
- package/template/bunfig.toml +4 -0
- package/template/package.json +19 -0
- package/template/src/app.ts +23 -0
- package/template/src/browser/app.css +21 -0
- package/template/src/browser/app.html +24 -0
- package/template/src/browser/pages/about/page.svelte +5 -0
- package/template/src/browser/pages/layout.svelte +26 -0
- package/template/src/browser/pages/page.svelte +20 -0
- package/template/src/bundle/icon.png +0 -0
- package/template/src/cli/banner.txt +3 -0
- package/template/src/cli/footer.txt +1 -0
- package/template/src/server/config.ts +17 -0
- package/template/src/server/rpc/getHello.ts +35 -0
- package/template/svelte.config.js +12 -0
- package/template/tsconfig.json +18 -0
- package/tsconfig.app.json +16 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import type { BunRequest, Server } from 'bun'
|
|
2
|
+
import type { Component } from 'svelte'
|
|
3
|
+
import { render } from 'svelte/server'
|
|
4
|
+
import App from '../../../App.svelte'
|
|
5
|
+
import type { Errors } from '../../browser/types/Errors.ts'
|
|
6
|
+
import type { Layouts } from '../../browser/types/Layouts.ts'
|
|
7
|
+
import type { Pages } from '../../browser/types/Pages.ts'
|
|
8
|
+
import { createMcpResourceServer } from '../../mcp/createMcpResourceServer.ts'
|
|
9
|
+
import { setMcpResourceServer } from '../../mcp/mcpResourceServerSlot.ts'
|
|
10
|
+
import type { McpServer } from '../../mcp/types/McpServer.ts'
|
|
11
|
+
import { NO_STORE, SSR_CACHE_CONTROL } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
12
|
+
import { extraForwardHeaders } from '../../shared/extraForwardHeaders.ts'
|
|
13
|
+
import { isDebugEnabled } from '../../shared/isDebugEnabled.ts'
|
|
14
|
+
import { log } from '../../shared/log.ts'
|
|
15
|
+
import { nearestLayoutPrefix, normalizeLayoutPrefixes } from '../../shared/nearestLayoutPrefix.ts'
|
|
16
|
+
import { RESOLVE_STREAM_PATH } from '../../shared/RESOLVE_STREAM_PATH.ts'
|
|
17
|
+
import { toBunRoutePattern } from '../../shared/toBunRoutePattern.ts'
|
|
18
|
+
import type { AppModule } from '../AppModule.ts'
|
|
19
|
+
import { handleCliDownload } from '../cli/handleCliDownload.ts'
|
|
20
|
+
import { handleCliInstall } from '../cli/handleCliInstall.ts'
|
|
21
|
+
import type { PromptRoutes } from '../prompts/types/PromptRoutes.ts'
|
|
22
|
+
import type { RemoteRoutes } from '../rpc/types/RemoteRoutes.ts'
|
|
23
|
+
import { createSocketDispatcher } from '../sockets/createSocketDispatcher.ts'
|
|
24
|
+
import type { SocketRoutes } from '../sockets/types/SocketRoutes.ts'
|
|
25
|
+
import { acceptsZstd } from './acceptsZstd.ts'
|
|
26
|
+
import { buildOpenApiSpec } from './buildOpenApiSpec.ts'
|
|
27
|
+
import { cacheControlForAsset } from './cacheControlForAsset.ts'
|
|
28
|
+
import { containsTraversal } from './containsTraversal.ts'
|
|
29
|
+
import { createAssetHeaderCache } from './createAssetHeaderCache.ts'
|
|
30
|
+
import { createPublicAssetServer } from './createPublicAssetServer.ts'
|
|
31
|
+
import { createRouteDispatcher } from './createRouteDispatcher.ts'
|
|
32
|
+
import { DEFAULT_PORT } from './DEFAULT_PORT.ts'
|
|
33
|
+
import { DEV_REBUILD_MESSAGE } from './DEV_REBUILD_MESSAGE.ts'
|
|
34
|
+
import { DEV_RELOAD_CLIENT_SCRIPT } from './DEV_RELOAD_CLIENT_SCRIPT.ts'
|
|
35
|
+
import { devReloadResponse } from './devReloadResponse.ts'
|
|
36
|
+
import { disableIdleTimeoutForStream } from './disableIdleTimeoutForStream.ts'
|
|
37
|
+
import { globToPathSet } from './globToPathSet.ts'
|
|
38
|
+
import { internalErrorResponse } from './internalErrorResponse.ts'
|
|
39
|
+
import { isCrossOriginUpgrade } from './isCrossOriginUpgrade.ts'
|
|
40
|
+
import { listenOnOpenPort } from './listenOnOpenPort.ts'
|
|
41
|
+
import { logExposedSurfaces } from './logExposedSurfaces.ts'
|
|
42
|
+
import { parseIdleTimeout } from './parseIdleTimeout.ts'
|
|
43
|
+
import { parsePort } from './parsePort.ts'
|
|
44
|
+
import { ensureRegistriesLoaded, setRegistryManifests } from './registryManifests.ts'
|
|
45
|
+
import { resolveStreamResponse } from './resolveStreamResponse.ts'
|
|
46
|
+
import { runWithRequestScope } from './runWithRequestScope.ts'
|
|
47
|
+
import { safeJsonForScript } from './safeJsonForScript.ts'
|
|
48
|
+
import { serializeCacheSnapshot } from './serializeCacheSnapshot.ts'
|
|
49
|
+
import { setActiveServer } from './setActiveServer.ts'
|
|
50
|
+
import { stashPendingStream } from './streamStash.ts'
|
|
51
|
+
import type { Assets } from './types/Assets.ts'
|
|
52
|
+
import type { RequestStore } from './types/RequestStore.ts'
|
|
53
|
+
|
|
54
|
+
function wantsJson(req: Request): boolean {
|
|
55
|
+
return (req.headers.get('accept') ?? '').includes('application/json')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// SSR placeholders the shell carries; filled in a single pass per render.
|
|
59
|
+
const SSR_MARKER = /<!--ssr:(head|body|state)-->/g
|
|
60
|
+
|
|
61
|
+
const IDENTITY_PATH = '/__belte/identity'
|
|
62
|
+
const SOCKETS_PATH = '/__belte/sockets'
|
|
63
|
+
const SOCKETS_REST_PREFIX = '/__belte/sockets/'
|
|
64
|
+
const MCP_PATH = '/__belte/mcp'
|
|
65
|
+
const CLI_PATH = '/__belte/cli'
|
|
66
|
+
const CLI_DOWNLOAD_PREFIX = '/__belte/cli/'
|
|
67
|
+
// Dev-only live-reload SSE channel; mounted only when `dev` (see devEntry orchestrator).
|
|
68
|
+
const DEV_RELOAD_PATH = '/__belte/dev'
|
|
69
|
+
// Dev-only manual rebuild trigger; POSTing signals the orchestrator to rebuild + restart.
|
|
70
|
+
const DEV_REBUILD_PATH = '/__belte/reload'
|
|
71
|
+
/*
|
|
72
|
+
Unlike the framework's own plumbing routes above (the socket multiplex, MCP
|
|
73
|
+
endpoint, CLI download), the OpenAPI document describes the app's public HTTP
|
|
74
|
+
surface — the /rpc/* verbs — rather than belte internals, so it sits at the
|
|
75
|
+
conventional root path where external tooling and scanners expect to find it
|
|
76
|
+
(/openapi.json, alongside /swagger.json, /.well-known/*) rather than under the
|
|
77
|
+
/__belte/ namespace.
|
|
78
|
+
*/
|
|
79
|
+
const OPENAPI_PATH = '/openapi.json'
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
Starts a Bun HTTP server that ties together the framework conventions:
|
|
83
|
+
page.svelte + layout.svelte under src/browser/pages/ for views, one named export
|
|
84
|
+
per file under src/server/rpc/ for verb-bound remote functions, one named export
|
|
85
|
+
per file under src/server/sockets/ for broadcast sockets, and an optional
|
|
86
|
+
app.ts for boot-time setup, request middleware, and error fallback. Page
|
|
87
|
+
URLs and rpc URLs live in disjoint spaces — pages mount at the folder
|
|
88
|
+
path, rpc files mount at `/rpc/<file path>` — so each registered URL
|
|
89
|
+
resolves to exactly one thing. Per request, an AsyncLocalStorage
|
|
90
|
+
RequestStore carries the cache store and request metadata.
|
|
91
|
+
*/
|
|
92
|
+
export async function createServer({
|
|
93
|
+
pages,
|
|
94
|
+
rpc,
|
|
95
|
+
sockets,
|
|
96
|
+
prompts,
|
|
97
|
+
layouts,
|
|
98
|
+
errors,
|
|
99
|
+
shell,
|
|
100
|
+
app,
|
|
101
|
+
assets,
|
|
102
|
+
publicAssets,
|
|
103
|
+
mcpResources,
|
|
104
|
+
mcp,
|
|
105
|
+
cliProgramName,
|
|
106
|
+
appInfo,
|
|
107
|
+
distDir = `${process.cwd()}/dist`,
|
|
108
|
+
publicDir = `${process.cwd()}/src/browser/public`,
|
|
109
|
+
resourcesDir = `${process.cwd()}/src/mcp/resources`,
|
|
110
|
+
// A configured PORT is honored as-is; left undefined, the real listener
|
|
111
|
+
// scans upward from 3000 at bind time (see buildServer / listenOnOpenPort).
|
|
112
|
+
port = parsePort(process.env.PORT),
|
|
113
|
+
/*
|
|
114
|
+
Bun's per-connection idle timeout in seconds (its own default is 10).
|
|
115
|
+
Surfaced for apps whose unary handlers legitimately compute longer than
|
|
116
|
+
that; streaming responses opt out per-request via disableIdleTimeoutForStream
|
|
117
|
+
regardless of this floor.
|
|
118
|
+
*/
|
|
119
|
+
idleTimeout = parseIdleTimeout(process.env.BELTE_IDLE_TIMEOUT) ?? 10,
|
|
120
|
+
// Under `belte dev` the orchestrator sets this: mount the live-reload SSE
|
|
121
|
+
// channel and inject its client into the served shell.
|
|
122
|
+
dev = false,
|
|
123
|
+
}: {
|
|
124
|
+
pages: Pages
|
|
125
|
+
rpc: RemoteRoutes
|
|
126
|
+
sockets: SocketRoutes
|
|
127
|
+
prompts: PromptRoutes
|
|
128
|
+
layouts?: Layouts
|
|
129
|
+
errors?: Errors
|
|
130
|
+
shell: string
|
|
131
|
+
app?: AppModule
|
|
132
|
+
assets?: Assets
|
|
133
|
+
publicAssets?: Assets
|
|
134
|
+
mcpResources?: Assets
|
|
135
|
+
mcp?: McpServer
|
|
136
|
+
cliProgramName?: string
|
|
137
|
+
appInfo?: { name: string; version: string }
|
|
138
|
+
distDir?: string
|
|
139
|
+
publicDir?: string
|
|
140
|
+
resourcesDir?: string
|
|
141
|
+
port?: number
|
|
142
|
+
idleTimeout?: number
|
|
143
|
+
dev?: boolean
|
|
144
|
+
}): Promise<Server<unknown>> {
|
|
145
|
+
// In dev, append the live-reload client to the shell so every rendered
|
|
146
|
+
// page reconnects to /__belte/dev and reloads after a restart.
|
|
147
|
+
const activeShell = dev ? shell.replace('</body>', `${DEV_RELOAD_CLIENT_SCRIPT}</body>`) : shell
|
|
148
|
+
setRegistryManifests({ rpc, sockets, prompts })
|
|
149
|
+
setMcpResourceServer(createMcpResourceServer({ resourcesDir, mcpResources }))
|
|
150
|
+
const cliName = cliProgramName ?? 'app'
|
|
151
|
+
const cliCwd = process.cwd()
|
|
152
|
+
const servePublicAsset = await createPublicAssetServer({ publicDir, publicAssets })
|
|
153
|
+
const layoutPrefixes = layouts ? normalizeLayoutPrefixes(Object.keys(layouts)) : []
|
|
154
|
+
const errorPrefixes = errors ? normalizeLayoutPrefixes(Object.keys(errors)) : []
|
|
155
|
+
|
|
156
|
+
/*
|
|
157
|
+
Snapshot the precompressed `.zst` siblings the build wrote next to each
|
|
158
|
+
`_app` asset, keyed by the asset's request path, so a zstd-capable
|
|
159
|
+
client gets the precompressed bytes without on-the-fly compression. Only
|
|
160
|
+
in disk mode (`belte start` / dev); the compiled binary serves from the
|
|
161
|
+
embedded `assets` map instead.
|
|
162
|
+
*/
|
|
163
|
+
const diskZstdPaths = assets
|
|
164
|
+
? new Set<string>()
|
|
165
|
+
: await globToPathSet(
|
|
166
|
+
`${distDir}/_app`,
|
|
167
|
+
'**/*.zst',
|
|
168
|
+
(file) => `/_app/${file.replace(/\.zst$/, '')}`,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const logRequests = isDebugEnabled('belte')
|
|
172
|
+
|
|
173
|
+
// App-configured headers extend the in-process forward allowlist for the process lifetime.
|
|
174
|
+
extraForwardHeaders.set(app?.forwardHeaders ?? [])
|
|
175
|
+
|
|
176
|
+
// Per-pathname asset header bundles, hashed-chunk-aware Cache-Control.
|
|
177
|
+
const headersForAsset = createAssetHeaderCache(cacheControlForAsset)
|
|
178
|
+
|
|
179
|
+
async function serveStaticAsset(req: Request, url: URL): Promise<Response> {
|
|
180
|
+
/*
|
|
181
|
+
Defence-in-depth path-traversal check against the raw request URL.
|
|
182
|
+
The WHATWG URL parser decodes `%2E%2E` to `..` and then normalises
|
|
183
|
+
dot-segments away before `url.pathname` is even visible, so an
|
|
184
|
+
attacker's traversal sequence would be invisible if we only looked
|
|
185
|
+
at the parsed pathname. Inspecting `req.url` instead catches the
|
|
186
|
+
encoded forms before normalization eats them; `%2F` (encoded slash)
|
|
187
|
+
is preserved in the pathname but still flagged here for clarity.
|
|
188
|
+
*/
|
|
189
|
+
if (containsTraversal(req.url)) {
|
|
190
|
+
return new Response('Not Found', {
|
|
191
|
+
status: 404,
|
|
192
|
+
headers: { 'Cache-Control': NO_STORE },
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
const wantsZstd = acceptsZstd(req)
|
|
196
|
+
const { base: baseHeaders, zstd: zstdHeaders } = headersForAsset(url.pathname)
|
|
197
|
+
if (assets) {
|
|
198
|
+
const compressed = assets[url.pathname]
|
|
199
|
+
if (!compressed) {
|
|
200
|
+
return new Response('Not Found', {
|
|
201
|
+
status: 404,
|
|
202
|
+
headers: { 'Cache-Control': NO_STORE },
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
if (wantsZstd) {
|
|
206
|
+
return new Response(compressed, { headers: zstdHeaders })
|
|
207
|
+
}
|
|
208
|
+
return new Response(await Bun.zstdDecompress(compressed), { headers: baseHeaders })
|
|
209
|
+
}
|
|
210
|
+
const diskPath = distDir + url.pathname
|
|
211
|
+
if (wantsZstd && diskZstdPaths.has(url.pathname)) {
|
|
212
|
+
return new Response(Bun.file(`${diskPath}.zst`), { headers: zstdHeaders })
|
|
213
|
+
}
|
|
214
|
+
return new Response(Bun.file(diskPath), { headers: baseHeaders })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Splices the rendered head/body and a state <script> into the shell's SSR markers. */
|
|
218
|
+
function fillShell(rendered: Awaited<ReturnType<typeof render>>, stateTag: string): string {
|
|
219
|
+
const fills: Record<string, string> = {
|
|
220
|
+
head: rendered.head,
|
|
221
|
+
body: rendered.body,
|
|
222
|
+
state: stateTag,
|
|
223
|
+
}
|
|
224
|
+
return activeShell.replace(SSR_MARKER, (_match, key: string) => fills[key])
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
Renders the nearest error.svelte for a failed page navigation — an unknown
|
|
229
|
+
route (404) or a throw during a page render. Walks up from the requested
|
|
230
|
+
pathname to the deepest error.svelte ancestor (nearest-only, like layouts)
|
|
231
|
+
and renders it inside the nearest layout with `{ status, message, stack }`
|
|
232
|
+
props. Returns undefined when no error.svelte covers the path, so the caller
|
|
233
|
+
falls back to its plain Response (the 404 text) or rethrows (→
|
|
234
|
+
app.handleError). The document is static — it ships `__SSR__.error` so the
|
|
235
|
+
client skips hydration (there is no client route for an error view to
|
|
236
|
+
hydrate against).
|
|
237
|
+
|
|
238
|
+
The full message and stack are passed through; nothing is serialized to
|
|
239
|
+
`__SSR__`, so they reach the browser only where the author's template
|
|
240
|
+
actually renders them — a bare error.svelte leaks neither. Withholding them
|
|
241
|
+
would just deny an author who wants a dev stack, so exposure stays the
|
|
242
|
+
author's call (and the cause is logged server-side regardless).
|
|
243
|
+
*/
|
|
244
|
+
async function renderError(
|
|
245
|
+
status: number,
|
|
246
|
+
message: string,
|
|
247
|
+
store: RequestStore,
|
|
248
|
+
stack?: string,
|
|
249
|
+
): Promise<Response | undefined> {
|
|
250
|
+
if (!errors) {
|
|
251
|
+
return undefined
|
|
252
|
+
}
|
|
253
|
+
const pathname = store.url.pathname
|
|
254
|
+
const errorPrefix = nearestLayoutPrefix(pathname, errorPrefixes)
|
|
255
|
+
if (!errorPrefix) {
|
|
256
|
+
return undefined
|
|
257
|
+
}
|
|
258
|
+
const layoutPrefix = nearestLayoutPrefix(pathname, layoutPrefixes)
|
|
259
|
+
const [errorMod, layoutMod] = await Promise.all([
|
|
260
|
+
errors[errorPrefix](),
|
|
261
|
+
layoutPrefix && layouts ? layouts[layoutPrefix]() : Promise.resolve(undefined),
|
|
262
|
+
])
|
|
263
|
+
const ErrorView = errorMod.default as Component
|
|
264
|
+
const Layout = layoutMod?.default as Component | undefined
|
|
265
|
+
const rendered = await render(App, {
|
|
266
|
+
props: {
|
|
267
|
+
state: {
|
|
268
|
+
page: {
|
|
269
|
+
route: pathname,
|
|
270
|
+
// status is a number (and stack optional); the page-params
|
|
271
|
+
// shape is string-keyed generically, so the error props
|
|
272
|
+
// ride through to the component as-is.
|
|
273
|
+
params: { status, message, stack } as unknown as Record<string, string>,
|
|
274
|
+
url: store.url,
|
|
275
|
+
},
|
|
276
|
+
render: { Layout, Page: ErrorView },
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
const stateTag = `<script>window.__SSR__ = ${safeJsonForScript({ error: true })};</script>`
|
|
281
|
+
const html = fillShell(rendered, stateTag)
|
|
282
|
+
return new Response(html, {
|
|
283
|
+
status,
|
|
284
|
+
headers: {
|
|
285
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
286
|
+
'Cache-Control': NO_STORE,
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function renderPage(
|
|
292
|
+
routeUrl: string,
|
|
293
|
+
params: Record<string, string>,
|
|
294
|
+
store: RequestStore,
|
|
295
|
+
): Promise<Response> {
|
|
296
|
+
const json = wantsJson(store.req)
|
|
297
|
+
if (json) {
|
|
298
|
+
return Response.json(
|
|
299
|
+
{ route: routeUrl, params },
|
|
300
|
+
{
|
|
301
|
+
headers: {
|
|
302
|
+
Vary: 'Accept',
|
|
303
|
+
'Cache-Control': SSR_CACHE_CONTROL,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
return await renderPageHtml(routeUrl, params, store)
|
|
310
|
+
} catch (error) {
|
|
311
|
+
/*
|
|
312
|
+
A page render failed (module import, component throw, or a settled
|
|
313
|
+
cache read). error.svelte wins for page renders: render the nearest
|
|
314
|
+
one with a 500, logging the cause it's about to swallow into a
|
|
315
|
+
presentable page. With no error.svelte covering the path, rethrow so
|
|
316
|
+
app.handleError — or the framework 500 — takes it.
|
|
317
|
+
*/
|
|
318
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
319
|
+
const stack = error instanceof Error ? error.stack : undefined
|
|
320
|
+
const rendered = await renderError(500, message, store, stack)
|
|
321
|
+
if (rendered) {
|
|
322
|
+
log.error(error)
|
|
323
|
+
return rendered
|
|
324
|
+
}
|
|
325
|
+
throw error
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function renderPageHtml(
|
|
330
|
+
routeUrl: string,
|
|
331
|
+
params: Record<string, string>,
|
|
332
|
+
store: RequestStore,
|
|
333
|
+
): Promise<Response> {
|
|
334
|
+
const layoutPrefix = nearestLayoutPrefix(routeUrl, layoutPrefixes)
|
|
335
|
+
const [pageMod, layoutMod] = await Promise.all([
|
|
336
|
+
pages[routeUrl](),
|
|
337
|
+
layoutPrefix && layouts ? layouts[layoutPrefix]() : Promise.resolve(undefined),
|
|
338
|
+
])
|
|
339
|
+
const Page = pageMod.default as Component
|
|
340
|
+
const Layout = layoutMod?.default as Component | undefined
|
|
341
|
+
const rendered = await render(App, {
|
|
342
|
+
props: {
|
|
343
|
+
state: {
|
|
344
|
+
page: {
|
|
345
|
+
route: routeUrl,
|
|
346
|
+
params,
|
|
347
|
+
url: store.url,
|
|
348
|
+
},
|
|
349
|
+
render: { Layout, Page },
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
/*
|
|
354
|
+
Settled entries (awaited reads render() blocked on) inline into the
|
|
355
|
+
first chunk; pending entries ({#await} reads) stream a resolve script
|
|
356
|
+
each as their fetch lands. A page with no pending reads stays a plain
|
|
357
|
+
buffered Response — no streaming overhead when nothing's deferred.
|
|
358
|
+
*/
|
|
359
|
+
const { inline, pending } = await serializeCacheSnapshot(store.cache)
|
|
360
|
+
/*
|
|
361
|
+
Settled reads inline into `__SSR__`. Pending {#await} reads ship their
|
|
362
|
+
keys in `__SSR__.streaming` (so the client pre-creates placeholders that
|
|
363
|
+
cache() hits instead of re-fetching) plus a single-use `streamToken`; the
|
|
364
|
+
in-flight promises are stashed under that token for the out-of-band
|
|
365
|
+
resolve endpoint to drain. The document itself is a plain buffered
|
|
366
|
+
response — it closes immediately, so hydration isn't gated on the stream.
|
|
367
|
+
*/
|
|
368
|
+
const streaming = pending.map((entry) => ({
|
|
369
|
+
key: entry.key,
|
|
370
|
+
/* serializeCacheSnapshot only ever yields request-bearing (remote) entries here. */
|
|
371
|
+
url: entry.request?.url ?? '',
|
|
372
|
+
method: entry.request?.method ?? 'GET',
|
|
373
|
+
}))
|
|
374
|
+
const streamToken =
|
|
375
|
+
pending.length > 0 ? stashPendingStream(store.cache, pending) : undefined
|
|
376
|
+
const stateTag = `<script>window.__SSR__ = ${safeJsonForScript({
|
|
377
|
+
route: routeUrl,
|
|
378
|
+
params,
|
|
379
|
+
cache: inline,
|
|
380
|
+
streaming,
|
|
381
|
+
streamToken,
|
|
382
|
+
})};</script>`
|
|
383
|
+
const html = fillShell(rendered, stateTag)
|
|
384
|
+
return new Response(html, {
|
|
385
|
+
headers: {
|
|
386
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
387
|
+
Vary: 'Accept',
|
|
388
|
+
'Cache-Control': SSR_CACHE_CONTROL,
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/*
|
|
394
|
+
Route dispatch — rpc-vs-page-vs-404 resolution and method matching — lives
|
|
395
|
+
behind createRouteDispatcher; renderPage is injected so those decisions stay
|
|
396
|
+
testable without SSR. buildRoutes() below binds the returned handler per URL.
|
|
397
|
+
*/
|
|
398
|
+
const buildRouteHandler = createRouteDispatcher({ pages, rpc, renderPage })
|
|
399
|
+
|
|
400
|
+
/*
|
|
401
|
+
Page URLs (folder paths, e.g. `/media/[id]`) get translated to Bun's
|
|
402
|
+
pattern syntax (`/media/:id`) at registration. Bun's `*` wildcard
|
|
403
|
+
matches but does not capture into req.params, so for `[...rest]`
|
|
404
|
+
routes the catch-all value is reconstructed from the request URL by
|
|
405
|
+
slicing the pathname segments after the catch-all's pattern index.
|
|
406
|
+
The reconstructed value is set under the original name (e.g. `rest`)
|
|
407
|
+
so the page component's $props destructure stays consistent with the
|
|
408
|
+
file path. Page URLs and rpc URLs (always `/rpc/...`, flat) are
|
|
409
|
+
disjoint by construction, so a plain object needs no deduplication.
|
|
410
|
+
*/
|
|
411
|
+
const routes: Record<string, (req: BunRequest) => Promise<Response>> = {}
|
|
412
|
+
for (const routeUrl of Object.keys(pages)) {
|
|
413
|
+
const handler = buildRouteHandler(routeUrl)
|
|
414
|
+
const { pattern, catchAllName } = toBunRoutePattern(routeUrl)
|
|
415
|
+
const catchAllIndex = catchAllName
|
|
416
|
+
? routeUrl.split('/').findIndex((segment) => segment.startsWith('[...'))
|
|
417
|
+
: -1
|
|
418
|
+
routes[pattern] = (req) => {
|
|
419
|
+
const pathParams = { ...((req.params as Record<string, string> | undefined) ?? {}) }
|
|
420
|
+
if (catchAllName && catchAllIndex !== -1) {
|
|
421
|
+
const pathSegments = new URL(req.url).pathname.split('/')
|
|
422
|
+
pathParams[catchAllName] = pathSegments.slice(catchAllIndex).join('/')
|
|
423
|
+
}
|
|
424
|
+
return dispatchRequest(req, pathParams, handler)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const routeUrl of Object.keys(rpc)) {
|
|
428
|
+
const handler = buildRouteHandler(routeUrl)
|
|
429
|
+
routes[routeUrl] = (req) => dispatchRequest(req, {}, handler)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function dispatchRequest(
|
|
433
|
+
req: Request,
|
|
434
|
+
pathParams: Record<string, string>,
|
|
435
|
+
handler: (
|
|
436
|
+
req: Request,
|
|
437
|
+
pathParams: Record<string, string>,
|
|
438
|
+
store: RequestStore,
|
|
439
|
+
) => Promise<Response>,
|
|
440
|
+
): Promise<Response> {
|
|
441
|
+
return runWithRequestScope(req, { app, logRequests }, async (store) => {
|
|
442
|
+
const response = app?.handle
|
|
443
|
+
? await app.handle(req, (next) => handler(next, pathParams, store))
|
|
444
|
+
: await handler(req, pathParams, store)
|
|
445
|
+
// Streaming bodies (sse/jsonl, socket tail) opt out of the idle timeout.
|
|
446
|
+
return disableIdleTimeoutForStream(server, req, response)
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/*
|
|
451
|
+
Belte's only native WebSocket surface is the sockets hub: every Socket
|
|
452
|
+
declared under src/server/sockets/ multiplexes onto one framework-owned
|
|
453
|
+
connection per client at /__belte/sockets. The dispatcher owns the
|
|
454
|
+
open/message/close handlers below; user code never sees the raw ws
|
|
455
|
+
lifecycle. Steady-state fan-out rides Bun's native server.publish so
|
|
456
|
+
a busy socket doesn't iterate JS per subscriber per message.
|
|
457
|
+
*/
|
|
458
|
+
const socketDispatcher = createSocketDispatcher(sockets)
|
|
459
|
+
|
|
460
|
+
/*
|
|
461
|
+
Bind the real server on `boundPort`. Only the port varies between scan
|
|
462
|
+
attempts, so the rest of the config lives inline and just the port is spread
|
|
463
|
+
in — passing the literal straight to Bun.serve keeps contextual typing of the
|
|
464
|
+
websocket handlers (and Server<unknown> pins Bun's WebSocketData generic so
|
|
465
|
+
upgrade({ data: {} }) typechecks).
|
|
466
|
+
*/
|
|
467
|
+
const bindAt = (boundPort: number): Server<unknown> =>
|
|
468
|
+
Bun.serve({
|
|
469
|
+
port: boundPort,
|
|
470
|
+
idleTimeout,
|
|
471
|
+
|
|
472
|
+
websocket: {
|
|
473
|
+
open(ws) {
|
|
474
|
+
socketDispatcher.open(ws)
|
|
475
|
+
},
|
|
476
|
+
message(ws, data) {
|
|
477
|
+
socketDispatcher.message(ws, data)
|
|
478
|
+
},
|
|
479
|
+
close(ws) {
|
|
480
|
+
socketDispatcher.close(ws)
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
routes,
|
|
485
|
+
|
|
486
|
+
async fetch(req, bunServer) {
|
|
487
|
+
const url = new URL(req.url)
|
|
488
|
+
/*
|
|
489
|
+
Identity probe — answered directly, ahead of any app.handle middleware,
|
|
490
|
+
so the bundle's connect screen can confirm a URL really is a belte
|
|
491
|
+
server (and which app) before pointing the desktop window at it. It
|
|
492
|
+
must stay reachable even when the app guards everything behind auth,
|
|
493
|
+
hence the early return that bypasses dispatchRequest.
|
|
494
|
+
*/
|
|
495
|
+
if (url.pathname === IDENTITY_PATH) {
|
|
496
|
+
return Response.json(
|
|
497
|
+
{
|
|
498
|
+
belte: true,
|
|
499
|
+
name: appInfo?.name ?? cliName,
|
|
500
|
+
version: appInfo?.version ?? '0.0.0',
|
|
501
|
+
},
|
|
502
|
+
{ headers: { 'Cache-Control': NO_STORE } },
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
/*
|
|
506
|
+
Dev live-reload channel — answered directly, ahead of app.handle,
|
|
507
|
+
so a restart-driven reconnect always lands even when the app guards
|
|
508
|
+
everything behind auth. Only mounted under `belte dev`.
|
|
509
|
+
*/
|
|
510
|
+
if (dev && url.pathname === DEV_RELOAD_PATH) {
|
|
511
|
+
// Long-lived SSE: opt out of the idle timeout, else Bun reaps
|
|
512
|
+
// it and the reconnect triggers a spurious reload loop.
|
|
513
|
+
return disableIdleTimeoutForStream(bunServer, req, devReloadResponse())
|
|
514
|
+
}
|
|
515
|
+
/*
|
|
516
|
+
Manual rebuild trigger: signal the orchestrator parent over IPC to
|
|
517
|
+
rebuild + restart. Same-origin sibling of the live-reload channel, so
|
|
518
|
+
a script refreshes on the app's own port. process.send exists only when
|
|
519
|
+
the dev orchestrator spawned us with ipc; the optional chain no-ops on a
|
|
520
|
+
bare server.
|
|
521
|
+
*/
|
|
522
|
+
if (dev && req.method === 'POST' && url.pathname === DEV_REBUILD_PATH) {
|
|
523
|
+
process.send?.(DEV_REBUILD_MESSAGE)
|
|
524
|
+
return new Response('rebuilding\n')
|
|
525
|
+
}
|
|
526
|
+
if (url.pathname === SOCKETS_PATH) {
|
|
527
|
+
// Reject cross-origin upgrades (CSWSH) before handing off to Bun.
|
|
528
|
+
if (isCrossOriginUpgrade(req, url)) {
|
|
529
|
+
return new Response('Forbidden', { status: 403 })
|
|
530
|
+
}
|
|
531
|
+
if (bunServer.upgrade(req, { data: {} })) {
|
|
532
|
+
return undefined as unknown as Response
|
|
533
|
+
}
|
|
534
|
+
return new Response('Upgrade failed', { status: 400 })
|
|
535
|
+
}
|
|
536
|
+
/*
|
|
537
|
+
HTTP face of a socket (`/__belte/sockets/<name>`) — tail over
|
|
538
|
+
SSE / JSON and publish — for the CLI and MCP. Runs through
|
|
539
|
+
dispatchRequest so app.handle auth applies, like the rpc paths.
|
|
540
|
+
The socket name may contain `/` (nested files), so it's the
|
|
541
|
+
whole remaining pathname, percent-decoded.
|
|
542
|
+
*/
|
|
543
|
+
if (url.pathname.startsWith(SOCKETS_REST_PREFIX)) {
|
|
544
|
+
const name = decodeURIComponent(url.pathname.slice(SOCKETS_REST_PREFIX.length))
|
|
545
|
+
return dispatchRequest(req, {}, async () => socketDispatcher.rest(req, name))
|
|
546
|
+
}
|
|
547
|
+
/*
|
|
548
|
+
Out-of-band resolution stream for a streamed page's pending
|
|
549
|
+
{#await} reads. Answered directly (no app.handle / request scope):
|
|
550
|
+
it drains promises stashed during SSR, and the random single-use
|
|
551
|
+
token gates access. Returning here bypasses
|
|
552
|
+
disableIdleTimeoutForStream so it inherits the bounded idleTimeout
|
|
553
|
+
rather than the long-lived-stream disable.
|
|
554
|
+
*/
|
|
555
|
+
if (url.pathname.startsWith(RESOLVE_STREAM_PATH)) {
|
|
556
|
+
return resolveStreamResponse(url.pathname.slice(RESOLVE_STREAM_PATH.length))
|
|
557
|
+
}
|
|
558
|
+
if (url.pathname === MCP_PATH && mcp) {
|
|
559
|
+
return dispatchRequest(req, {}, async () => mcp.handle(req))
|
|
560
|
+
}
|
|
561
|
+
if (url.pathname === CLI_PATH) {
|
|
562
|
+
return dispatchRequest(req, {}, async () => handleCliInstall(req, cliName))
|
|
563
|
+
}
|
|
564
|
+
if (url.pathname.startsWith(CLI_DOWNLOAD_PREFIX)) {
|
|
565
|
+
const platform = url.pathname.slice(CLI_DOWNLOAD_PREFIX.length)
|
|
566
|
+
return dispatchRequest(req, {}, async () =>
|
|
567
|
+
handleCliDownload(req, platform, cliName, cliCwd),
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
if (url.pathname === OPENAPI_PATH) {
|
|
571
|
+
return dispatchRequest(req, {}, async () => {
|
|
572
|
+
await ensureRegistriesLoaded()
|
|
573
|
+
const spec = buildOpenApiSpec({
|
|
574
|
+
title: appInfo?.name ?? cliName,
|
|
575
|
+
version: appInfo?.version ?? '0.0.0',
|
|
576
|
+
})
|
|
577
|
+
return Response.json(spec, { headers: { 'Cache-Control': NO_STORE } })
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
/*
|
|
581
|
+
Static assets sidestep ALS + the per-request CacheStore + the
|
|
582
|
+
app.handle middleware: they have no need for cache() and the
|
|
583
|
+
allocation overhead matters on a cold page load that pulls
|
|
584
|
+
dozens of chunks. The global server.error() handler still
|
|
585
|
+
catches anything that goes wrong inside serveStaticAsset.
|
|
586
|
+
*/
|
|
587
|
+
if (url.pathname.startsWith('/_app/')) {
|
|
588
|
+
if (!logRequests) {
|
|
589
|
+
return serveStaticAsset(req, url)
|
|
590
|
+
}
|
|
591
|
+
const start = Bun.nanoseconds()
|
|
592
|
+
const response = await serveStaticAsset(req, url)
|
|
593
|
+
const ms = (Bun.nanoseconds() - start) / 1e6
|
|
594
|
+
log.request(req.method, `${url.pathname}${url.search}`, response.status, ms)
|
|
595
|
+
return response
|
|
596
|
+
}
|
|
597
|
+
/*
|
|
598
|
+
Files under public/ are served at the site root, sidestepping
|
|
599
|
+
ALS + middleware like the /_app/ assets do. A miss returns
|
|
600
|
+
undefined so the request falls through to the 404 / middleware
|
|
601
|
+
path below.
|
|
602
|
+
*/
|
|
603
|
+
const publicResponse = await servePublicAsset(req, url)
|
|
604
|
+
if (publicResponse) {
|
|
605
|
+
if (logRequests) {
|
|
606
|
+
log.request(
|
|
607
|
+
req.method,
|
|
608
|
+
`${url.pathname}${url.search}`,
|
|
609
|
+
publicResponse.status,
|
|
610
|
+
0,
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
return publicResponse
|
|
614
|
+
}
|
|
615
|
+
/*
|
|
616
|
+
Unknown routes still run through dispatchRequest so user-defined
|
|
617
|
+
app.handle middleware can rewrite the request, serve a custom
|
|
618
|
+
404, or branch on the URL. The inner handler returns the
|
|
619
|
+
framework's default 404 when nothing intervenes.
|
|
620
|
+
*/
|
|
621
|
+
return dispatchRequest(req, {}, async (_req, _pathParams, store) => {
|
|
622
|
+
return (
|
|
623
|
+
(await renderError(404, 'Not Found', store)) ??
|
|
624
|
+
new Response('Not Found', {
|
|
625
|
+
status: 404,
|
|
626
|
+
headers: { 'Cache-Control': NO_STORE },
|
|
627
|
+
})
|
|
628
|
+
)
|
|
629
|
+
})
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
error(err) {
|
|
633
|
+
log.error(err)
|
|
634
|
+
return internalErrorResponse(err)
|
|
635
|
+
},
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
/*
|
|
639
|
+
A configured PORT binds that exact port — a collision surfaces loudly rather
|
|
640
|
+
than silently moving, since something connecting to the app needs a known
|
|
641
|
+
address. With none set, scan upward from 3000 binding the real listener, so
|
|
642
|
+
whichever server wins the port keeps it (no probe-release gap to lose it in,
|
|
643
|
+
which used to crash boot on EADDRINUSE instead of stepping to the next port).
|
|
644
|
+
*/
|
|
645
|
+
const server: Server<unknown> =
|
|
646
|
+
port === undefined ? listenOnOpenPort(bindAt, DEFAULT_PORT) : bindAt(port)
|
|
647
|
+
|
|
648
|
+
/*
|
|
649
|
+
Publishes the live server through `belte/server` before invoking the
|
|
650
|
+
user's init() hook. The exported `server()` function reads from this
|
|
651
|
+
slot and throws on access before the slot is set, so init() callers
|
|
652
|
+
can hold the import at module scope and still see the real instance
|
|
653
|
+
once boot completes.
|
|
654
|
+
*/
|
|
655
|
+
setActiveServer(server)
|
|
656
|
+
|
|
657
|
+
const cleanup = app?.init ? await app.init({ server }) : undefined
|
|
658
|
+
/*
|
|
659
|
+
Close the listener deterministically on shutdown. Always registered (even
|
|
660
|
+
with no init cleanup) so the socket is released via server.stop rather than
|
|
661
|
+
left to abrupt process exit — which leaves the port in TIME_WAIT and races
|
|
662
|
+
a fast restart. A watchdog force-exits if a user cleanup hangs, so a stuck
|
|
663
|
+
cleanup can't keep the process (and its port) alive.
|
|
664
|
+
*/
|
|
665
|
+
const shutdown = async () => {
|
|
666
|
+
server.stop(true)
|
|
667
|
+
if (typeof cleanup === 'function') {
|
|
668
|
+
setTimeout(() => process.exit(0), 3000).unref()
|
|
669
|
+
try {
|
|
670
|
+
await cleanup()
|
|
671
|
+
} catch (err) {
|
|
672
|
+
log.error(err)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
process.exit(0)
|
|
676
|
+
}
|
|
677
|
+
process.once('SIGINT', shutdown)
|
|
678
|
+
process.once('SIGTERM', shutdown)
|
|
679
|
+
|
|
680
|
+
/*
|
|
681
|
+
Diagnostic only, and only under `belte` debug logging — eager-loads the
|
|
682
|
+
registry to print the page/socket/rpc surface maps (routing + which
|
|
683
|
+
declarations reach mcp/cli/openapi), making belte's multimodal-by-default
|
|
684
|
+
exposure auditable. Awaited so `ready` lands after all of belte's own
|
|
685
|
+
startup output rather than interleaving with it.
|
|
686
|
+
*/
|
|
687
|
+
if (logRequests) {
|
|
688
|
+
await logExposedSurfaces({ pages, layoutPrefixes, errorPrefixes })
|
|
689
|
+
}
|
|
690
|
+
log.success(`ready at http://localhost:${server.port}`)
|
|
691
|
+
return server
|
|
692
|
+
}
|