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