@briancray/belte 0.1.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/LICENSE +21 -0
- package/README.md +49 -0
- package/bin/belte.ts +136 -0
- package/package.json +80 -0
- package/src/App.svelte +31 -0
- package/src/assets/app.html +14 -0
- package/src/belteResolverPlugin.ts +832 -0
- package/src/build.ts +144 -0
- package/src/buildCli.ts +160 -0
- package/src/cliEntry.ts +31 -0
- package/src/clientEntry.ts +7 -0
- package/src/compile.ts +64 -0
- package/src/devEntry.ts +33 -0
- package/src/discoveryEntry.ts +33 -0
- package/src/lib/browser/cache.ts +191 -0
- package/src/lib/browser/page.svelte.ts +215 -0
- package/src/lib/browser/remoteProxy.ts +44 -0
- package/src/lib/browser/socketChannel.ts +182 -0
- package/src/lib/browser/socketProxy.ts +64 -0
- package/src/lib/browser/startClient.ts +132 -0
- package/src/lib/browser/subscribe.ts +131 -0
- package/src/lib/browser/types/Layouts.ts +7 -0
- package/src/lib/browser/types/Pages.ts +7 -0
- package/src/lib/cli/createClient.ts +126 -0
- package/src/lib/cli/loadEnvFromBinaryDir.ts +44 -0
- package/src/lib/cli/parseArgvForRpc.ts +97 -0
- package/src/lib/cli/printHelp.ts +70 -0
- package/src/lib/cli/runCli.ts +88 -0
- package/src/lib/cli/types/CliManifest.ts +9 -0
- package/src/lib/cli/types/CliManifestEntry.ts +12 -0
- package/src/lib/mcp/createMcpResourceServer.ts +101 -0
- package/src/lib/mcp/createMcpServer.ts +40 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +294 -0
- package/src/lib/mcp/mcpResourceServerSlot.ts +18 -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 +25 -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/HttpError.ts +19 -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/cli/buildEnvContent.ts +18 -0
- package/src/lib/server/cli/createTarGz.ts +76 -0
- package/src/lib/server/cli/handleCliDownload.ts +124 -0
- package/src/lib/server/cli/handleCliInstall.ts +20 -0
- package/src/lib/server/cli/installScript.ts +29 -0
- package/src/lib/server/cli/maxSourceMtime.ts +27 -0
- package/src/lib/server/error.ts +56 -0
- package/src/lib/server/json.ts +28 -0
- package/src/lib/server/jsonl.ts +40 -0
- package/src/lib/server/prompt.ts +30 -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/types/Prompt.ts +14 -0
- package/src/lib/server/prompts/types/PromptMessage.ts +10 -0
- package/src/lib/server/prompts/types/PromptOptions.ts +17 -0
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +15 -0
- package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
- package/src/lib/server/redirect.ts +37 -0
- package/src/lib/server/request.ts +18 -0
- package/src/lib/server/rpc/defineVerb.ts +103 -0
- package/src/lib/server/rpc/parseArgs.ts +60 -0
- package/src/lib/server/rpc/registerVerb.ts +6 -0
- package/src/lib/server/rpc/types/HttpVerb.ts +1 -0
- package/src/lib/server/rpc/types/RawRemoteFunction.ts +13 -0
- package/src/lib/server/rpc/types/RemoteFunction.ts +35 -0
- package/src/lib/server/rpc/types/RemoteHandler.ts +22 -0
- package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
- package/src/lib/server/rpc/types/StandardSchemaV1.ts +57 -0
- package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
- package/src/lib/server/rpc/types/VerbHelper.ts +39 -0
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +17 -0
- package/src/lib/server/rpc/unprocessed.ts +14 -0
- package/src/lib/server/rpc/verbRegistry.ts +11 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +66 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +17 -0
- package/src/lib/server/runtime/containsTraversal.ts +37 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +66 -0
- package/src/lib/server/runtime/createServer.ts +555 -0
- package/src/lib/server/runtime/getActiveServer.ts +6 -0
- package/src/lib/server/runtime/mimeForExtension.ts +20 -0
- package/src/lib/server/runtime/registryManifests.ts +48 -0
- package/src/lib/server/runtime/requestContext.ts +5 -0
- package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +84 -0
- package/src/lib/server/runtime/serverSlot.ts +13 -0
- package/src/lib/server/runtime/setActiveServer.ts +6 -0
- package/src/lib/server/runtime/streamFromIterator.ts +76 -0
- package/src/lib/server/runtime/types/Assets.ts +1 -0
- package/src/lib/server/runtime/types/CompileTarget.ts +6 -0
- package/src/lib/server/runtime/types/RequestStore.ts +15 -0
- package/src/lib/server/runtime/types/SvelteConfig.ts +5 -0
- package/src/lib/server/server.ts +19 -0
- package/src/lib/server/socket.ts +31 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +267 -0
- package/src/lib/server/sockets/defineSocket.ts +160 -0
- package/src/lib/server/sockets/lookupSocket.ts +6 -0
- package/src/lib/server/sockets/registerSocket.ts +6 -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/SocketOptions.ts +22 -0
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +18 -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 +47 -0
- package/src/lib/shared/activeCacheStore.ts +20 -0
- package/src/lib/shared/buildRpcRequest.ts +61 -0
- package/src/lib/shared/cacheControlValues.ts +8 -0
- package/src/lib/shared/cacheStoreSlot.ts +16 -0
- package/src/lib/shared/canonicalJson.ts +24 -0
- package/src/lib/shared/commandNameForUrl.ts +17 -0
- package/src/lib/shared/createCacheStore.ts +42 -0
- package/src/lib/shared/createPushIterator.ts +77 -0
- package/src/lib/shared/createRemoteFunction.ts +89 -0
- package/src/lib/shared/decodeResponse.ts +47 -0
- package/src/lib/shared/detectTarget.ts +27 -0
- package/src/lib/shared/findExportCallSite.ts +479 -0
- package/src/lib/shared/forwardHeaders.ts +28 -0
- package/src/lib/shared/getRemoteMeta.ts +5 -0
- package/src/lib/shared/isDebugEnabled.ts +23 -0
- package/src/lib/shared/jsonSchemaForSchema.ts +38 -0
- package/src/lib/shared/keyForRemoteCall.ts +38 -0
- package/src/lib/shared/loadSvelteConfig.ts +18 -0
- package/src/lib/shared/log.ts +104 -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/parseRouteSegments.ts +22 -0
- package/src/lib/shared/preparePromptModule.ts +36 -0
- package/src/lib/shared/prepareRpcModule.ts +51 -0
- package/src/lib/shared/prepareSocketModule.ts +37 -0
- package/src/lib/shared/programNameForPackage.ts +14 -0
- package/src/lib/shared/promptNameForFile.ts +10 -0
- package/src/lib/shared/recordRemoteMeta.ts +5 -0
- package/src/lib/shared/remoteMetaStore.ts +16 -0
- package/src/lib/shared/resolveClientFlags.ts +18 -0
- package/src/lib/shared/rpcUrlForFile.ts +19 -0
- package/src/lib/shared/setCacheStoreResolver.ts +6 -0
- package/src/lib/shared/socketNameForFile.ts +11 -0
- package/src/lib/shared/streamingContentTypes.ts +11 -0
- package/src/lib/shared/stripImport.ts +27 -0
- package/src/lib/shared/subscribableFromResponse.ts +333 -0
- package/src/lib/shared/toBunRoutePattern.ts +28 -0
- package/src/lib/shared/types/CacheEntry.ts +16 -0
- package/src/lib/shared/types/CacheOptions.ts +10 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
- package/src/lib/shared/types/CacheStore.ts +15 -0
- package/src/lib/shared/types/ClientFlags.ts +11 -0
- package/src/lib/shared/types/Subscribable.ts +15 -0
- package/src/lib/shared/writeRoutesDts.ts +64 -0
- package/src/preload.ts +20 -0
- package/src/scaffold.ts +92 -0
- package/src/serverEntry.ts +47 -0
- package/src/sveltePlugin.ts +58 -0
- package/src/tailwindStylePreprocessor.ts +62 -0
- package/template/package.json +16 -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/cli/banner.txt +3 -0
- package/template/src/cli/footer.txt +1 -0
- package/template/src/server/rpc/getHello.ts +33 -0
- package/template/svelte.config.js +12 -0
- package/template/tsconfig.json +18 -0
- package/tsconfig.app.json +16 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '../server/rpc/types/StandardSchemaV1.ts'
|
|
2
|
+
|
|
3
|
+
const OPAQUE = { type: 'object', additionalProperties: true } as const
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Resolves a JSON Schema for an MCP tool's `inputSchema` or a resource's
|
|
7
|
+
payload type. Priority:
|
|
8
|
+
|
|
9
|
+
1. Explicit `jsonSchema` field on the verb/socket opts (user-supplied)
|
|
10
|
+
2. `schema.toJsonSchema()` (Arktype 2+)
|
|
11
|
+
3. `schema.toJSONSchema()` (Zod 4, Effect Schema, etc.)
|
|
12
|
+
4. Opaque object — the tool still works, the model just gets no shape hint
|
|
13
|
+
|
|
14
|
+
Returns a fresh object each call; callers can mutate (e.g. add a
|
|
15
|
+
description) without aliasing.
|
|
16
|
+
*/
|
|
17
|
+
export function jsonSchemaForSchema(
|
|
18
|
+
schema: StandardSchemaV1 | undefined,
|
|
19
|
+
jsonSchema: Record<string, unknown> | undefined,
|
|
20
|
+
): Record<string, unknown> {
|
|
21
|
+
if (jsonSchema) {
|
|
22
|
+
return { ...jsonSchema }
|
|
23
|
+
}
|
|
24
|
+
if (!schema) {
|
|
25
|
+
return { ...OPAQUE }
|
|
26
|
+
}
|
|
27
|
+
const candidate = schema as unknown as {
|
|
28
|
+
toJsonSchema?: () => Record<string, unknown>
|
|
29
|
+
toJSONSchema?: () => Record<string, unknown>
|
|
30
|
+
}
|
|
31
|
+
if (typeof candidate.toJsonSchema === 'function') {
|
|
32
|
+
return candidate.toJsonSchema()
|
|
33
|
+
}
|
|
34
|
+
if (typeof candidate.toJSONSchema === 'function') {
|
|
35
|
+
return candidate.toJSONSchema()
|
|
36
|
+
}
|
|
37
|
+
return { ...OPAQUE }
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { HttpVerb } from '../server/rpc/types/HttpVerb.ts'
|
|
2
|
+
import { canonicalJson } from './canonicalJson.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Derives a cache key from a verb-defined remote function and its args. The
|
|
6
|
+
prefix is `${method} ${url}` where `url` is the route template. GET/DELETE
|
|
7
|
+
serialise args onto the URL as `?key=value` with keys sorted so the order
|
|
8
|
+
the caller assembled the object doesn't change the key; POST/PUT/PATCH join
|
|
9
|
+
args after a space as canonical JSON. Sorted key/value pairs are walked once
|
|
10
|
+
and concatenated directly so the hot GET-cache path doesn't allocate per
|
|
11
|
+
intermediate (entries / filtered / URLSearchParams).
|
|
12
|
+
*/
|
|
13
|
+
export function keyForRemoteCall(method: HttpVerb, url: string, args: unknown): string {
|
|
14
|
+
const prefix = `${method} ${url}`
|
|
15
|
+
if (method === 'GET' || method === 'DELETE') {
|
|
16
|
+
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
|
17
|
+
const record = args as Record<string, unknown>
|
|
18
|
+
const keys = Object.keys(record).sort()
|
|
19
|
+
let search = ''
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
const value = record[key]
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
search += search ? '&' : ''
|
|
26
|
+
search += `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
|
|
27
|
+
}
|
|
28
|
+
if (search.length > 0) {
|
|
29
|
+
return `${prefix}?${search}`
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return prefix
|
|
33
|
+
}
|
|
34
|
+
if (args === undefined) {
|
|
35
|
+
return prefix
|
|
36
|
+
}
|
|
37
|
+
return `${prefix} ${canonicalJson(args)}`
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SvelteConfig } from '../server/runtime/types/SvelteConfig.ts'
|
|
2
|
+
|
|
3
|
+
const EXTENSIONS = ['js', 'mjs', 'ts'] as const
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Looks for `svelte.config.{js,mjs,ts}` in `cwd` and returns its default export.
|
|
7
|
+
Falls back to an empty config if no file is found.
|
|
8
|
+
*/
|
|
9
|
+
export async function loadSvelteConfig(cwd: string = process.cwd()): Promise<SvelteConfig> {
|
|
10
|
+
for (const extension of EXTENSIONS) {
|
|
11
|
+
const path = `${cwd}/svelte.config.${extension}`
|
|
12
|
+
if (await Bun.file(path).exists()) {
|
|
13
|
+
const module = await import(path)
|
|
14
|
+
return module.default as SvelteConfig
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return {}
|
|
18
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { isDebugEnabled } from './isDebugEnabled.ts'
|
|
2
|
+
|
|
3
|
+
const hasBun = typeof Bun !== 'undefined'
|
|
4
|
+
const useColor = hasBun && Bun.enableANSIColors
|
|
5
|
+
const RESET = '\x1b[0m'
|
|
6
|
+
const BOLD = '\x1b[1m'
|
|
7
|
+
const DIM = '\x1b[2m'
|
|
8
|
+
|
|
9
|
+
// Wraps `text` in a Bun-resolved ANSI color escape; no-op when colors are disabled or unavailable (browser).
|
|
10
|
+
function paint(color: string, text: string): string {
|
|
11
|
+
if (!useColor) {
|
|
12
|
+
return text
|
|
13
|
+
}
|
|
14
|
+
return `${Bun.color(color, 'ansi-256')}${text}${RESET}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Applies the ANSI dim attribute; no-op when colors are disabled.
|
|
18
|
+
function dim(text: string): string {
|
|
19
|
+
if (!useColor) {
|
|
20
|
+
return text
|
|
21
|
+
}
|
|
22
|
+
return `${DIM}${text}${RESET}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Prefers a full stack trace when the value is an Error so logs include the call site.
|
|
26
|
+
function formatError(value: unknown): string {
|
|
27
|
+
if (value instanceof Error) {
|
|
28
|
+
return value.stack ?? value.message
|
|
29
|
+
}
|
|
30
|
+
return String(value)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Maps an HTTP status code to a color that matches the usual server-log convention.
|
|
34
|
+
function colorStatus(status: number): string {
|
|
35
|
+
if (status >= 500) {
|
|
36
|
+
return paint('red', String(status))
|
|
37
|
+
}
|
|
38
|
+
if (status >= 400) {
|
|
39
|
+
return paint('yellow', String(status))
|
|
40
|
+
}
|
|
41
|
+
if (status >= 300) {
|
|
42
|
+
return paint('cyan', String(status))
|
|
43
|
+
}
|
|
44
|
+
return paint('green', String(status))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Maps an HTTP method to a color so the request log line is easy to scan.
|
|
48
|
+
function colorMethod(method: string): string {
|
|
49
|
+
const upper = method.toUpperCase()
|
|
50
|
+
if (upper === 'GET') {
|
|
51
|
+
return paint('green', upper)
|
|
52
|
+
}
|
|
53
|
+
if (upper === 'POST') {
|
|
54
|
+
return paint('blue', upper)
|
|
55
|
+
}
|
|
56
|
+
if (upper === 'PUT' || upper === 'PATCH') {
|
|
57
|
+
return paint('yellow', upper)
|
|
58
|
+
}
|
|
59
|
+
if (upper === 'DELETE') {
|
|
60
|
+
return paint('red', upper)
|
|
61
|
+
}
|
|
62
|
+
return paint('magenta', upper)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const BELTE = useColor ? `${BOLD}${Bun.color('magenta', 'ansi-256')}[belte]${RESET}` : '[belte]'
|
|
66
|
+
|
|
67
|
+
// Browser console already has its own DEBUG storage convention, but for the shared logger
|
|
68
|
+
// we honor the same DEBUG env. In the browser `process.env.DEBUG` may not exist.
|
|
69
|
+
const debugEnv = typeof process !== 'undefined' ? process.env.DEBUG : undefined
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
Shared logger used by both the build pipeline and the request handler.
|
|
73
|
+
Wraps console.* with ANSI coloring, a `[belte]` prefix, and a per-method/
|
|
74
|
+
per-status palette for `request()`. console.* is the side effect — logging
|
|
75
|
+
is intentionally impure.
|
|
76
|
+
*/
|
|
77
|
+
export const log = {
|
|
78
|
+
info(message: string): void {
|
|
79
|
+
console.log(`${BELTE} ${message}`)
|
|
80
|
+
},
|
|
81
|
+
warn(message: string): void {
|
|
82
|
+
console.warn(`${BELTE} ${paint('yellow', message)}`)
|
|
83
|
+
},
|
|
84
|
+
error(value: unknown): void {
|
|
85
|
+
console.error(`${BELTE} ${paint('red', formatError(value))}`)
|
|
86
|
+
},
|
|
87
|
+
success(message: string): void {
|
|
88
|
+
console.log(`${BELTE} ${paint('green', message)}`)
|
|
89
|
+
},
|
|
90
|
+
detail(message: string): void {
|
|
91
|
+
console.log(dim(message))
|
|
92
|
+
},
|
|
93
|
+
debug(scope: string, message: string): void {
|
|
94
|
+
if (!isDebugEnabled(scope, debugEnv)) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
console.log(`${dim(`[${scope}]`)} ${dim(message)}`)
|
|
98
|
+
},
|
|
99
|
+
request(method: string, path: string, status: number, durationMs: number): void {
|
|
100
|
+
console.log(
|
|
101
|
+
`${colorMethod(method)} ${path} ${colorStatus(status)} ${dim(`${durationMs.toFixed(2)}ms`)}`,
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Given a route URL and a list of directory prefixes that have a
|
|
3
|
+
layout.svelte, returns the deepest prefix that is an ancestor of the route.
|
|
4
|
+
Returns undefined when no layout applies. Implements the "nearest-only"
|
|
5
|
+
resolution from the plan — no stacking.
|
|
6
|
+
*/
|
|
7
|
+
export type NormalizedLayoutPrefix = {
|
|
8
|
+
prefix: string
|
|
9
|
+
dir: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeLayoutPrefixes(prefixes: Iterable<string>): NormalizedLayoutPrefix[] {
|
|
13
|
+
const out: NormalizedLayoutPrefix[] = []
|
|
14
|
+
for (const prefix of prefixes) {
|
|
15
|
+
out.push({ prefix, dir: prefix === '/' ? '' : prefix.replace(/^\//, '') })
|
|
16
|
+
}
|
|
17
|
+
return out
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function nearestLayoutPrefix(
|
|
21
|
+
routeUrl: string,
|
|
22
|
+
layoutPrefixes: NormalizedLayoutPrefix[],
|
|
23
|
+
): string | undefined {
|
|
24
|
+
const normalized = routeUrl === '/' ? '' : routeUrl.replace(/^\//, '')
|
|
25
|
+
let best: string | undefined
|
|
26
|
+
let bestLen = -1
|
|
27
|
+
for (const { prefix, dir } of layoutPrefixes) {
|
|
28
|
+
if (dir === '' || normalized === dir || normalized.startsWith(`${dir}/`)) {
|
|
29
|
+
if (dir.length > bestLen) {
|
|
30
|
+
best = prefix
|
|
31
|
+
bestLen = dir.length
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return best
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CompileTarget } from '../server/runtime/types/CompileTarget.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Prepends the `bun-` prefix if missing so CLI users can pass either
|
|
5
|
+
`darwin-arm64` or the canonical `bun-darwin-arm64` form to `--target`.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeTarget(input: string): CompileTarget {
|
|
8
|
+
const normalized = input.startsWith('bun-') ? input : `bun-${input}`
|
|
9
|
+
return normalized as CompileTarget
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Maps a page-relative path (under `src/browser/pages/`) to its URL route. Pages are
|
|
3
|
+
folder-based: every leaf is `page.svelte` or `layout.svelte`, and the URL
|
|
4
|
+
is the directory path. Pages mount at the directory path; layouts mount at
|
|
5
|
+
the directory prefix. Dynamic segments keep their `[name]` / `[...rest]`
|
|
6
|
+
shape — translation to Bun's `:name` / `*` happens at server registration
|
|
7
|
+
via toBunRoutePattern; consumers see the readable form in `nav.route`.
|
|
8
|
+
*/
|
|
9
|
+
export function pageUrlForFile(relPath: string): string {
|
|
10
|
+
const segments = relPath.split('/')
|
|
11
|
+
segments.pop()
|
|
12
|
+
const path = segments.filter(Boolean).join('/')
|
|
13
|
+
return path === '' ? '/' : `/${path}`
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type RouteSegment =
|
|
2
|
+
| { kind: 'literal'; value: string }
|
|
3
|
+
| { kind: 'param'; name: string; catchAll: boolean }
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Splits a belte route URL into typed segments. `[name]` becomes a param,
|
|
7
|
+
`[...rest]` becomes a catch-all param, anything else is a literal. Used
|
|
8
|
+
by toBunRoutePattern (server-side Bun pattern emission) and writeRoutesDts
|
|
9
|
+
(client-side `Routes` type augmentation) so the two consumers can't drift
|
|
10
|
+
on what counts as a param.
|
|
11
|
+
*/
|
|
12
|
+
export function parseRouteSegments(routeUrl: string): RouteSegment[] {
|
|
13
|
+
return routeUrl.split('/').map((segment) => {
|
|
14
|
+
if (segment.startsWith('[...') && segment.endsWith(']')) {
|
|
15
|
+
return { kind: 'param', name: segment.slice(4, -1), catchAll: true }
|
|
16
|
+
}
|
|
17
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
18
|
+
return { kind: 'param', name: segment.slice(1, -1), catchAll: false }
|
|
19
|
+
}
|
|
20
|
+
return { kind: 'literal', value: segment }
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { findExportCallSite } from './findExportCallSite.ts'
|
|
2
|
+
import { stripImport } from './stripImport.ts'
|
|
3
|
+
|
|
4
|
+
const SINGLE_EXPORT_ERROR =
|
|
5
|
+
'[belte] prompts module contains more than one `prompt(...)` export — each file must declare exactly one prompt'
|
|
6
|
+
|
|
7
|
+
export type PreparedPromptModule = {
|
|
8
|
+
exportName: string
|
|
9
|
+
rewriteForServer: (name: string) => string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Scans a `src/server/prompts/**` module once and returns its declared
|
|
14
|
+
export name plus a closure that, given the prompt name, emits the
|
|
15
|
+
server-side rewrite (`__belteDefinePrompt__("<name>", opts)` spliced into
|
|
16
|
+
the original source). Mirrors prepareSocketModule — a single tokenizer
|
|
17
|
+
pass so a `prompt` mention inside a string or comment is left alone.
|
|
18
|
+
*/
|
|
19
|
+
export function preparePromptModule(source: string): PreparedPromptModule | undefined {
|
|
20
|
+
const stripped = stripImport(source, 'belte/server/prompt')
|
|
21
|
+
const site = findExportCallSite(stripped, (ident) => ident === 'prompt', SINGLE_EXPORT_ERROR)
|
|
22
|
+
if (!site) {
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
exportName: site.exportName,
|
|
27
|
+
rewriteForServer(name: string): string {
|
|
28
|
+
const inner = stripped.slice(site.parenStart + 1, site.parenEnd).trim()
|
|
29
|
+
const binding =
|
|
30
|
+
inner.length === 0
|
|
31
|
+
? `__belteDefinePrompt__(${JSON.stringify(name)})`
|
|
32
|
+
: `__belteDefinePrompt__(${JSON.stringify(name)}, ${stripped.slice(site.parenStart + 1, site.parenEnd)})`
|
|
33
|
+
return stripped.slice(0, site.callStart) + binding + stripped.slice(site.parenEnd + 1)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { HttpVerb } from '../server/rpc/types/HttpVerb.ts'
|
|
2
|
+
import { findExportCallSite } from './findExportCallSite.ts'
|
|
3
|
+
import { stripImport } from './stripImport.ts'
|
|
4
|
+
|
|
5
|
+
const VERB_NAMES = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const
|
|
6
|
+
const VERB_SET = new Set<string>(VERB_NAMES)
|
|
7
|
+
const VERB_IMPORT_PATHS = VERB_NAMES.map((verb) => `belte/server/${verb}`)
|
|
8
|
+
|
|
9
|
+
const SINGLE_EXPORT_ERROR =
|
|
10
|
+
'[belte] $rpc module contains more than one `<VERB>(...)` export — each file must declare exactly one remote function'
|
|
11
|
+
|
|
12
|
+
export type PreparedRpcModule = {
|
|
13
|
+
verb: HttpVerb
|
|
14
|
+
exportName: string
|
|
15
|
+
rewriteForServer: (url: string) => string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
Scans an `$rpc/**` module once and returns its declared verb + export
|
|
20
|
+
name plus a closure that, given the route URL, emits the server-side
|
|
21
|
+
rewrite (`__belteDefineVerb__("VERB", "<url>", … )` spliced into the
|
|
22
|
+
original source). The single scan replaces the prior separate
|
|
23
|
+
extract + rewrite passes, so the resolver plugin only walks each source
|
|
24
|
+
character-by-character once.
|
|
25
|
+
|
|
26
|
+
A regex pass would be tidier but it can't tell a `GET` mention inside a
|
|
27
|
+
docstring or template literal from the real call, and it can't follow
|
|
28
|
+
nested generics like `GET<Map<K, V>>(`.
|
|
29
|
+
*/
|
|
30
|
+
export function prepareRpcModule(source: string): PreparedRpcModule | undefined {
|
|
31
|
+
/*
|
|
32
|
+
The "no barrels" surface places each verb at its own path
|
|
33
|
+
(`belte/server/GET`, `belte/server/POST`, …) — strip every one so
|
|
34
|
+
the user's verb import doesn't linger and side-effect-load the
|
|
35
|
+
stub module into the server bundle.
|
|
36
|
+
*/
|
|
37
|
+
const stripped = VERB_IMPORT_PATHS.reduce((current, path) => stripImport(current, path), source)
|
|
38
|
+
const site = findExportCallSite(stripped, (ident) => VERB_SET.has(ident), SINGLE_EXPORT_ERROR)
|
|
39
|
+
if (!site) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
const verb = site.ident as HttpVerb
|
|
43
|
+
return {
|
|
44
|
+
verb,
|
|
45
|
+
exportName: site.exportName,
|
|
46
|
+
rewriteForServer(url: string): string {
|
|
47
|
+
const binding = `__belteDefineVerb__(${JSON.stringify(verb)}, ${JSON.stringify(url)}, `
|
|
48
|
+
return stripped.slice(0, site.callStart) + binding + stripped.slice(site.parenStart + 1)
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { findExportCallSite } from './findExportCallSite.ts'
|
|
2
|
+
import { stripImport } from './stripImport.ts'
|
|
3
|
+
|
|
4
|
+
const SINGLE_EXPORT_ERROR =
|
|
5
|
+
'[belte] $sockets module contains more than one `socket(...)` export — each file must declare exactly one socket'
|
|
6
|
+
|
|
7
|
+
export type PreparedSocketModule = {
|
|
8
|
+
exportName: string
|
|
9
|
+
rewriteForServer: (name: string) => string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Scans a `$sockets/**` module once and returns its declared export name
|
|
14
|
+
plus a closure that, given the socket name, emits the server-side
|
|
15
|
+
rewrite (`__belteDefineSocket__("<name>"[, opts])` spliced into the
|
|
16
|
+
original source). The single scan replaces the prior separate
|
|
17
|
+
extract + rewrite passes, so the resolver plugin only walks each source
|
|
18
|
+
character-by-character once.
|
|
19
|
+
*/
|
|
20
|
+
export function prepareSocketModule(source: string): PreparedSocketModule | undefined {
|
|
21
|
+
const stripped = stripImport(source, 'belte/server/socket')
|
|
22
|
+
const site = findExportCallSite(stripped, (ident) => ident === 'socket', SINGLE_EXPORT_ERROR)
|
|
23
|
+
if (!site) {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
exportName: site.exportName,
|
|
28
|
+
rewriteForServer(name: string): string {
|
|
29
|
+
const inner = stripped.slice(site.parenStart + 1, site.parenEnd).trim()
|
|
30
|
+
const binding =
|
|
31
|
+
inner.length === 0
|
|
32
|
+
? `__belteDefineSocket__(${JSON.stringify(name)})`
|
|
33
|
+
: `__belteDefineSocket__(${JSON.stringify(name)}, ${stripped.slice(site.parenStart + 1, site.parenEnd)})`
|
|
34
|
+
return stripped.slice(0, site.callStart) + binding + stripped.slice(site.parenEnd + 1)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Derives the CLI program/binary name from a package.json `name` field.
|
|
3
|
+
Scoped names (`@scope/tool`) keep only the final segment so the value is
|
|
4
|
+
safe as a filesystem path, tar entry name, and CLI display name — a raw
|
|
5
|
+
`/` would otherwise nest the binary into an unexpected directory and break
|
|
6
|
+
the `/__belte/cli/<platform>` download route's path lookup. Falls back to
|
|
7
|
+
`app` when the name is absent or empty.
|
|
8
|
+
*/
|
|
9
|
+
export function programNameForPackage(name: string | undefined): string {
|
|
10
|
+
if (name === undefined || name === '') {
|
|
11
|
+
return 'app'
|
|
12
|
+
}
|
|
13
|
+
return name.split('/').pop() || 'app'
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Translates a prompt file path under `src/server/prompts/` into the
|
|
3
|
+
prompt's MCP name. Strips `.ts` and joins nested folder segments with `-`
|
|
4
|
+
(e.g. `code/review.ts` → `code-review`) so two prompts with the same stem
|
|
5
|
+
in different folders don't collide and the name stays a single valid MCP
|
|
6
|
+
prompt identifier.
|
|
7
|
+
*/
|
|
8
|
+
export function promptNameForFile(relativePath: string): string {
|
|
9
|
+
return relativePath.replace(/\.ts$/, '').replaceAll('/', '-')
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
WeakMap that records how to obtain the synthesized Request for a verb
|
|
3
|
+
call. The cache layer reads it to populate an entry's stored request
|
|
4
|
+
without rebuilding from scratch.
|
|
5
|
+
|
|
6
|
+
Stored as a thunk rather than the Request itself so SSR pages that fire
|
|
7
|
+
dozens of in-process verb calls without ever reaching cache() don't pay
|
|
8
|
+
the URL + Headers + Request allocation per call. The thunk memoises its
|
|
9
|
+
own first call inside createRemoteFunction, so cache() and any future
|
|
10
|
+
meta reader see the same Request instance.
|
|
11
|
+
|
|
12
|
+
method/url/key are intentionally not stored — they're derivable from
|
|
13
|
+
the RemoteFunction itself, and cache() does that derivation
|
|
14
|
+
independently.
|
|
15
|
+
*/
|
|
16
|
+
export const remoteMetaStore = new WeakMap<Promise<unknown>, () => Request>()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ClientFlags } from './types/ClientFlags.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Fills in the missing keys of a user-supplied `clients` option. Browser
|
|
5
|
+
defaults to true (the historical surface); mcp/cli default to true only
|
|
6
|
+
when a schema is attached, since exposing an unvalidated handler as a
|
|
7
|
+
tool / shell command is a foot-gun.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveClientFlags(
|
|
10
|
+
flags: Partial<ClientFlags> | undefined,
|
|
11
|
+
hasSchema: boolean,
|
|
12
|
+
): ClientFlags {
|
|
13
|
+
return {
|
|
14
|
+
browser: flags?.browser ?? true,
|
|
15
|
+
mcp: flags?.mcp ?? hasSchema,
|
|
16
|
+
cli: flags?.cli ?? hasSchema,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Maps an rpc-relative path (under `src/server/rpc/`) to its URL. Each file
|
|
3
|
+
is one endpoint at `/rpc/<file path>`, dropping the `.ts` extension.
|
|
4
|
+
$rpc URLs are flat function-call endpoints (args go in query or body), so
|
|
5
|
+
bracket-style `[name]` / `[...rest]` segments are rejected — those belong
|
|
6
|
+
in `src/browser/pages/` where they map to dynamic path params.
|
|
7
|
+
*/
|
|
8
|
+
export function rpcUrlForFile(relPath: string): string {
|
|
9
|
+
const withoutExt = relPath.replace(/\.ts$/, '')
|
|
10
|
+
const segments = withoutExt.split('/').filter(Boolean)
|
|
11
|
+
for (const segment of segments) {
|
|
12
|
+
if (segment.startsWith('[')) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`[belte] src/server/rpc/${relPath} has a dynamic segment '${segment}' — $rpc URLs are flat; pass identifiers via args, not the path`,
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return `/rpc/${segments.join('/')}`
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Translates a socket file path under `src/server/sockets/` into the socket's
|
|
3
|
+
identity used on the wire. The name is the file path minus `.ts` so
|
|
4
|
+
nested paths (e.g. `orders/new.ts`) become `orders/new`. Sockets don't
|
|
5
|
+
need a URL prefix the way rpc routes do — they multiplex over the
|
|
6
|
+
framework-owned `/__belte/sockets` ws and are dispatched by name in the
|
|
7
|
+
registry, not by HTTP path.
|
|
8
|
+
*/
|
|
9
|
+
export function socketNameForFile(relativePath: string): string {
|
|
10
|
+
return relativePath.replace(/\.ts$/, '')
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Content-Type prefixes belte treats as streaming bodies — SSE for the
|
|
3
|
+
`sse()` helper, JSONL / NDJSON for the `jsonl()` helper. Used by
|
|
4
|
+
decodeResponse to refuse a buffered decode and by streamResponse to
|
|
5
|
+
choose the frame parser.
|
|
6
|
+
*/
|
|
7
|
+
export const STREAMING_CONTENT_TYPES = [
|
|
8
|
+
'text/event-stream',
|
|
9
|
+
'application/jsonl',
|
|
10
|
+
'application/x-ndjson',
|
|
11
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Strips the user's `import { … } from '<moduleName>'` declaration from a
|
|
3
|
+
module source. Used by the $rpc / $sockets rewriters to remove the
|
|
4
|
+
verb / `socket` import after its call site has been replaced by the
|
|
5
|
+
runtime-injected binding (defineVerb / defineSocket). Without this
|
|
6
|
+
strip the dead import would still side-effect-load the verb/socket
|
|
7
|
+
helper module into the server bundle for every $rpc / $sockets file.
|
|
8
|
+
|
|
9
|
+
The braced body is `[^}]*` rather than `[\s\S]*?` so the lazy match
|
|
10
|
+
can't backtrack across a `}` and accidentally swallow a preceding
|
|
11
|
+
import whose `from` clause doesn't match (e.g. stripping
|
|
12
|
+
`import { GET } from 'belte/server/GET'` from a file that also has
|
|
13
|
+
`import { json } from 'belte/server/json'` on the line above). `[^}]`
|
|
14
|
+
includes newlines, so multi-line braced imports like
|
|
15
|
+
import {
|
|
16
|
+
GET,
|
|
17
|
+
} from 'belte/server/GET'
|
|
18
|
+
still match — the body just can't contain another `}` to bound it.
|
|
19
|
+
*/
|
|
20
|
+
export function stripImport(source: string, moduleName: string): string {
|
|
21
|
+
const escaped = moduleName.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
|
|
22
|
+
const pattern = new RegExp(
|
|
23
|
+
`^\\s*import\\s*\\{[^}]*\\}\\s*from\\s*['"]${escaped}['"]\\s*;?\\s*$`,
|
|
24
|
+
'gm',
|
|
25
|
+
)
|
|
26
|
+
return source.replace(pattern, '')
|
|
27
|
+
}
|