@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,89 @@
|
|
|
1
|
+
import type { HttpVerb } from '../server/rpc/types/HttpVerb.ts'
|
|
2
|
+
import type { RawRemoteFunction } from '../server/rpc/types/RawRemoteFunction.ts'
|
|
3
|
+
import type { RemoteFunction } from '../server/rpc/types/RemoteFunction.ts'
|
|
4
|
+
import { decodeResponse } from './decodeResponse.ts'
|
|
5
|
+
import { keyForRemoteCall } from './keyForRemoteCall.ts'
|
|
6
|
+
import { recordRemoteMeta } from './recordRemoteMeta.ts'
|
|
7
|
+
import { subscribableFromResponse } from './subscribableFromResponse.ts'
|
|
8
|
+
import type { ClientFlags } from './types/ClientFlags.ts'
|
|
9
|
+
import type { Subscribable } from './types/Subscribable.ts'
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
Assembles the public RemoteFunction shape used identically by the
|
|
13
|
+
server-side defineVerb (in-process handler invocation) and the
|
|
14
|
+
client-side remoteProxy (network fetch). Centralising the wiring here
|
|
15
|
+
keeps the call/raw/stream/fetch semantics — including WeakMap meta
|
|
16
|
+
recording, Content-Type decode, and Subscribable derivation — in one
|
|
17
|
+
place so the two halves can't drift.
|
|
18
|
+
|
|
19
|
+
- `buildRequest(args)` synthesizes the Request a meta reader (cache()) or
|
|
20
|
+
the client invoke needs. Server uses the inbound request's URL as the
|
|
21
|
+
base; client uses window.location. The result is memoised inside the
|
|
22
|
+
per-call `getRequest` thunk so the Request is built at most once per
|
|
23
|
+
call regardless of how many readers pull on it.
|
|
24
|
+
- `invoke(args, getRequest)` actually runs the call: server defineVerb
|
|
25
|
+
runs the handler and ignores `getRequest`; client remoteProxy calls
|
|
26
|
+
`fetch(getRequest())`. The thunk lets the server skip the Request
|
|
27
|
+
allocation entirely on the SSR hot path — the only consumer that ever
|
|
28
|
+
forces it is cache(), via the meta thunk recorded below.
|
|
29
|
+
- `parseArgsForFetch` is optional and only set by the server, so the
|
|
30
|
+
framework's router can call `.fetch(inboundRequest)` and have the
|
|
31
|
+
handler receive parsed args. Client `remoteProxy.fetch` just
|
|
32
|
+
forwards the request through invoke().
|
|
33
|
+
*/
|
|
34
|
+
export function createRemoteFunction<Args, Return>(opts: {
|
|
35
|
+
method: HttpVerb
|
|
36
|
+
url: string
|
|
37
|
+
clients: ClientFlags
|
|
38
|
+
buildRequest: (args: Args | undefined) => Request
|
|
39
|
+
invoke: (args: Args | undefined, getRequest: () => Request) => Promise<Response>
|
|
40
|
+
parseArgsForFetch?: (request: Request) => Promise<Args | undefined>
|
|
41
|
+
}): RemoteFunction<Args, Return> {
|
|
42
|
+
const { method, url, clients, buildRequest, invoke, parseArgsForFetch } = opts
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
Dispatch is the one-stop entry for both the plain call (no prebuilt
|
|
46
|
+
Request) and the fetch path (router hands us the inbound Request as
|
|
47
|
+
`prebuilt`). The `getRequest` thunk lazily synthesizes — or
|
|
48
|
+
short-circuits to the prebuilt one — and caches the result so the
|
|
49
|
+
client invoke + the cache meta reader share a single Request.
|
|
50
|
+
*/
|
|
51
|
+
function dispatch(args: Args | undefined, prebuilt?: Request): Promise<Response> {
|
|
52
|
+
let cached = prebuilt
|
|
53
|
+
function getRequest(): Request {
|
|
54
|
+
return cached ?? (cached = buildRequest(args))
|
|
55
|
+
}
|
|
56
|
+
const promise = invoke(args, getRequest)
|
|
57
|
+
recordRemoteMeta(promise, getRequest)
|
|
58
|
+
return promise
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rawCall(args: Args): Promise<Response> {
|
|
62
|
+
return dispatch(args)
|
|
63
|
+
}
|
|
64
|
+
rawCall.method = method
|
|
65
|
+
rawCall.url = url
|
|
66
|
+
const raw = rawCall as RawRemoteFunction<Args>
|
|
67
|
+
|
|
68
|
+
function callable(args: Args): Promise<Return> {
|
|
69
|
+
return raw(args).then(decodeResponse) as Promise<Return>
|
|
70
|
+
}
|
|
71
|
+
callable.method = method
|
|
72
|
+
callable.url = url
|
|
73
|
+
callable.clients = clients
|
|
74
|
+
callable.raw = raw
|
|
75
|
+
callable.stream = (args?: Args): Subscribable<Return> => {
|
|
76
|
+
return subscribableFromResponse(keyForRemoteCall(method, url, args), () =>
|
|
77
|
+
raw(args as Args),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
callable.fetch = parseArgsForFetch
|
|
81
|
+
? async (request: Request): Promise<Response> => {
|
|
82
|
+
const args = await parseArgsForFetch(request)
|
|
83
|
+
return dispatch(args, request)
|
|
84
|
+
}
|
|
85
|
+
: (request: Request): Promise<Response> => {
|
|
86
|
+
return dispatch(undefined, request)
|
|
87
|
+
}
|
|
88
|
+
return callable as RemoteFunction<Args, Return>
|
|
89
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { HttpError } from '../server/HttpError.ts'
|
|
2
|
+
import { STREAMING_CONTENT_TYPES } from './streamingContentTypes.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Decodes a Response into the natural body value based on Content-Type:
|
|
6
|
+
application/json (or `*\/+json`) → parsed JSON
|
|
7
|
+
text/* → string
|
|
8
|
+
204 No Content / empty body → undefined
|
|
9
|
+
everything else → Blob
|
|
10
|
+
|
|
11
|
+
Non-2xx responses throw HttpError so the happy path never has to check
|
|
12
|
+
`.ok` — error handling moves into try/catch (or unhandled exception
|
|
13
|
+
propagation), and the success path types as Promise<Return> cleanly.
|
|
14
|
+
|
|
15
|
+
Streaming Content-Types (SSE / JSONL / NDJSON) throw a clear error
|
|
16
|
+
rather than silently doing the wrong thing: response.text() would hang
|
|
17
|
+
forever on a never-ending body and response.json() would fail mid-parse.
|
|
18
|
+
The error points callers at the right tools — `subscribe(fn.stream)(args)`
|
|
19
|
+
for a shared reactive view, or `fn.stream(args)` directly for a fresh
|
|
20
|
+
per-call AsyncIterable — both of which know how to consume the body
|
|
21
|
+
frame-by-frame.
|
|
22
|
+
|
|
23
|
+
Callers that need headers, streaming, or per-status branching should use
|
|
24
|
+
the `.raw(args)` escape hatch on the remote function instead — that
|
|
25
|
+
returns the underlying Response untouched.
|
|
26
|
+
*/
|
|
27
|
+
export async function decodeResponse(response: Response): Promise<unknown> {
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new HttpError(response)
|
|
30
|
+
}
|
|
31
|
+
if (response.status === 204) {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
const contentType = (response.headers.get('content-type') ?? '').toLowerCase()
|
|
35
|
+
if (STREAMING_CONTENT_TYPES.some((type) => contentType.startsWith(type))) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`[belte] response at ${response.url} is a stream (${contentType}) — use subscribe(fn.stream)(args) for a reactive view, or fn.stream(args) for per-call iteration, instead of awaiting the bare call or cache()`,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
if (contentType.includes('json')) {
|
|
41
|
+
return response.json()
|
|
42
|
+
}
|
|
43
|
+
if (contentType.startsWith('text/')) {
|
|
44
|
+
return response.text()
|
|
45
|
+
}
|
|
46
|
+
return response.blob()
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CompileTarget } from '../server/runtime/types/CompileTarget.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Picks the Bun compile target matching the current host. Throws if the
|
|
5
|
+
platform/arch pair isn't one of the supported Bun standalone targets — the
|
|
6
|
+
CLI's `--target` flag is the escape hatch for cross-compilation.
|
|
7
|
+
*/
|
|
8
|
+
const HOST_TO_TARGET: Record<string, CompileTarget> = {
|
|
9
|
+
'darwin-arm64': 'bun-darwin-arm64',
|
|
10
|
+
'darwin-x64': 'bun-darwin-x64',
|
|
11
|
+
'linux-arm64': 'bun-linux-arm64',
|
|
12
|
+
'linux-x64': 'bun-linux-x64',
|
|
13
|
+
'win32-x64': 'bun-windows-x64',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function detectTarget(
|
|
17
|
+
platform: NodeJS.Platform = process.platform,
|
|
18
|
+
arch: NodeJS.Architecture = process.arch,
|
|
19
|
+
): CompileTarget {
|
|
20
|
+
const target = HOST_TO_TARGET[`${platform}-${arch}`]
|
|
21
|
+
if (!target) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`[belte] unsupported host platform ${platform}/${arch}. Pass --target=<bun-...> explicitly.`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
return target
|
|
27
|
+
}
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Scans a module source character-by-character — skipping strings,
|
|
3
|
+
templates, comments, and TypeScript generics — for an
|
|
4
|
+
`export const <name> = <IDENT>(...)` binding the caller cares about.
|
|
5
|
+
On a match returns the identifier text, the export name, and the byte
|
|
6
|
+
ranges of the call's open and close parens; the $rpc and $sockets
|
|
7
|
+
rewriters splice their runtime bindings into those ranges.
|
|
8
|
+
|
|
9
|
+
The scanner enforces a single matching export per module: a second match
|
|
10
|
+
throws `singleExportError` so each $rpc / $sockets file is required to
|
|
11
|
+
declare exactly one remote function / socket.
|
|
12
|
+
|
|
13
|
+
A regex pass would be tidier but it can't tell a `GET` mention inside a
|
|
14
|
+
docstring or template literal from the real call, and it can't follow
|
|
15
|
+
nested generics like `GET<Map<K, V>>(`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type ExportCallSite = {
|
|
19
|
+
ident: string
|
|
20
|
+
exportName: string
|
|
21
|
+
callStart: number
|
|
22
|
+
parenStart: number
|
|
23
|
+
parenEnd: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function findExportCallSite(
|
|
27
|
+
source: string,
|
|
28
|
+
matchIdent: (ident: string) => boolean,
|
|
29
|
+
singleExportError: string,
|
|
30
|
+
): ExportCallSite | undefined {
|
|
31
|
+
let found: ExportCallSite | undefined
|
|
32
|
+
const len = source.length
|
|
33
|
+
let i = 0
|
|
34
|
+
while (i < len) {
|
|
35
|
+
const c = source[i]
|
|
36
|
+
const next = source[i + 1]
|
|
37
|
+
if (c === '/' && next === '/') {
|
|
38
|
+
const newline = source.indexOf('\n', i + 2)
|
|
39
|
+
i = newline === -1 ? len : newline + 1
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (c === '/' && next === '*') {
|
|
43
|
+
const end = source.indexOf('*/', i + 2)
|
|
44
|
+
i = end === -1 ? len : end + 2
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if (c === '/' && isRegexContext(source, i)) {
|
|
48
|
+
i = skipRegex(source, i + 1)
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if (c === '"' || c === "'") {
|
|
52
|
+
i = skipString(source, i + 1, c)
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
if (c === '`') {
|
|
56
|
+
i = skipTemplate(source, i + 1)
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (isIdentStart(c) && !isIdentPart(source[i - 1])) {
|
|
60
|
+
let j = i + 1
|
|
61
|
+
while (j < len && isIdentPart(source[j])) {
|
|
62
|
+
j++
|
|
63
|
+
}
|
|
64
|
+
const ident = source.slice(i, j)
|
|
65
|
+
if (matchIdent(ident)) {
|
|
66
|
+
const tail = matchCallTail(source, j)
|
|
67
|
+
if (tail !== undefined) {
|
|
68
|
+
const exportName = detectExportName(source, i)
|
|
69
|
+
if (exportName !== undefined) {
|
|
70
|
+
if (found !== undefined) {
|
|
71
|
+
throw new Error(singleExportError)
|
|
72
|
+
}
|
|
73
|
+
const parenEnd = findCallEnd(source, tail)
|
|
74
|
+
if (parenEnd === undefined) {
|
|
75
|
+
throw new Error(`[belte] unmatched \`(\` after \`${ident}\` identifier`)
|
|
76
|
+
}
|
|
77
|
+
found = {
|
|
78
|
+
ident,
|
|
79
|
+
exportName,
|
|
80
|
+
callStart: i,
|
|
81
|
+
parenStart: tail,
|
|
82
|
+
parenEnd,
|
|
83
|
+
}
|
|
84
|
+
i = parenEnd + 1
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
i = j
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
i++
|
|
93
|
+
}
|
|
94
|
+
return found
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function skipString(source: string, start: number, quote: string): number {
|
|
98
|
+
let i = start
|
|
99
|
+
while (i < source.length) {
|
|
100
|
+
const c = source[i]
|
|
101
|
+
if (c === '\\') {
|
|
102
|
+
i += 2
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if (c === quote) {
|
|
106
|
+
return i + 1
|
|
107
|
+
}
|
|
108
|
+
if (c === '\n') {
|
|
109
|
+
return i
|
|
110
|
+
}
|
|
111
|
+
i++
|
|
112
|
+
}
|
|
113
|
+
return source.length
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function skipTemplate(source: string, start: number): number {
|
|
117
|
+
let i = start
|
|
118
|
+
while (i < source.length) {
|
|
119
|
+
const c = source[i]
|
|
120
|
+
if (c === '\\') {
|
|
121
|
+
i += 2
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (c === '`') {
|
|
125
|
+
return i + 1
|
|
126
|
+
}
|
|
127
|
+
if (c === '$' && source[i + 1] === '{') {
|
|
128
|
+
i = skipTemplateExpression(source, i + 2)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
i++
|
|
132
|
+
}
|
|
133
|
+
return source.length
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function skipTemplateExpression(source: string, start: number): number {
|
|
137
|
+
let depth = 1
|
|
138
|
+
let i = start
|
|
139
|
+
while (i < source.length && depth > 0) {
|
|
140
|
+
const c = source[i]
|
|
141
|
+
if (c === '{') {
|
|
142
|
+
depth++
|
|
143
|
+
i++
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (c === '}') {
|
|
147
|
+
depth--
|
|
148
|
+
i++
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (c === '"' || c === "'") {
|
|
152
|
+
i = skipString(source, i + 1, c)
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (c === '`') {
|
|
156
|
+
i = skipTemplate(source, i + 1)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
if (c === '/' && source[i + 1] === '/') {
|
|
160
|
+
const newline = source.indexOf('\n', i + 2)
|
|
161
|
+
i = newline === -1 ? source.length : newline + 1
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
if (c === '/' && source[i + 1] === '*') {
|
|
165
|
+
const end = source.indexOf('*/', i + 2)
|
|
166
|
+
i = end === -1 ? source.length : end + 2
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if (c === '/' && isRegexContext(source, i)) {
|
|
170
|
+
i = skipRegex(source, i + 1)
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
i++
|
|
174
|
+
}
|
|
175
|
+
return i
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function matchCallTail(source: string, after: number): number | undefined {
|
|
179
|
+
let j = after
|
|
180
|
+
while (j < source.length && isWhitespace(source[j])) {
|
|
181
|
+
j++
|
|
182
|
+
}
|
|
183
|
+
if (source[j] === '<') {
|
|
184
|
+
const closed = skipGenerics(source, j)
|
|
185
|
+
if (closed === undefined) {
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
j = closed
|
|
189
|
+
while (j < source.length && isWhitespace(source[j])) {
|
|
190
|
+
j++
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return source[j] === '(' ? j : undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
Returns the index immediately after the matching `>` for a generic
|
|
198
|
+
argument list starting at `start`. TypeScript type literals inside the
|
|
199
|
+
generic (`<{ a: string; b: number }>`, function types `<() => X>`,
|
|
200
|
+
tuples `<[A, B]>`, etc.) bring their own paired brackets and
|
|
201
|
+
semicolons, so track depth across `<>`, `()`, `{}`, and `[]` and only
|
|
202
|
+
count a closing `>` when every other bracket is balanced.
|
|
203
|
+
Arrow-function `=>` is treated as a single token so the `>` doesn't
|
|
204
|
+
prematurely close the generic.
|
|
205
|
+
*/
|
|
206
|
+
function skipGenerics(source: string, start: number): number | undefined {
|
|
207
|
+
let angleDepth = 0
|
|
208
|
+
let parenDepth = 0
|
|
209
|
+
let braceDepth = 0
|
|
210
|
+
let bracketDepth = 0
|
|
211
|
+
let i = start
|
|
212
|
+
while (i < source.length) {
|
|
213
|
+
const c = source[i]
|
|
214
|
+
if (c === '"' || c === "'") {
|
|
215
|
+
i = skipString(source, i + 1, c)
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
if (c === '`') {
|
|
219
|
+
i = skipTemplate(source, i + 1)
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
if (c === '<') {
|
|
223
|
+
angleDepth++
|
|
224
|
+
} else if (c === '>') {
|
|
225
|
+
const isArrow = source[i - 1] === '='
|
|
226
|
+
if (!isArrow && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
|
|
227
|
+
angleDepth--
|
|
228
|
+
if (angleDepth === 0) {
|
|
229
|
+
return i + 1
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} else if (c === '(') {
|
|
233
|
+
parenDepth++
|
|
234
|
+
} else if (c === ')') {
|
|
235
|
+
parenDepth--
|
|
236
|
+
} else if (c === '{') {
|
|
237
|
+
braceDepth++
|
|
238
|
+
} else if (c === '}') {
|
|
239
|
+
braceDepth--
|
|
240
|
+
} else if (c === '[') {
|
|
241
|
+
bracketDepth++
|
|
242
|
+
} else if (c === ']') {
|
|
243
|
+
bracketDepth--
|
|
244
|
+
}
|
|
245
|
+
i++
|
|
246
|
+
}
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
Walks the call body, skipping strings/templates/comments and respecting
|
|
252
|
+
nested `()` so brackets inside object literals or nested calls don't
|
|
253
|
+
throw the depth count.
|
|
254
|
+
*/
|
|
255
|
+
function findCallEnd(source: string, parenStart: number): number | undefined {
|
|
256
|
+
let depth = 1
|
|
257
|
+
let i = parenStart + 1
|
|
258
|
+
while (i < source.length) {
|
|
259
|
+
const c = source[i]
|
|
260
|
+
if (c === '"' || c === "'") {
|
|
261
|
+
i = skipString(source, i + 1, c)
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
if (c === '`') {
|
|
265
|
+
i = skipTemplate(source, i + 1)
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
if (c === '/' && source[i + 1] === '/') {
|
|
269
|
+
const newline = source.indexOf('\n', i + 2)
|
|
270
|
+
i = newline === -1 ? source.length : newline + 1
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (c === '/' && source[i + 1] === '*') {
|
|
274
|
+
const end = source.indexOf('*/', i + 2)
|
|
275
|
+
i = end === -1 ? source.length : end + 2
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
if (c === '/' && isRegexContext(source, i)) {
|
|
279
|
+
i = skipRegex(source, i + 1)
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
if (c === '(') {
|
|
283
|
+
depth++
|
|
284
|
+
} else if (c === ')') {
|
|
285
|
+
depth--
|
|
286
|
+
if (depth === 0) {
|
|
287
|
+
return i
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
i++
|
|
291
|
+
}
|
|
292
|
+
return undefined
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
Looks backwards from a `<IDENT>(` callStart to confirm it was bound by
|
|
297
|
+
`export const <name> = ...`. Returns the identifier in `<name>` if so,
|
|
298
|
+
undefined otherwise — used to skip mentions of an identifier that
|
|
299
|
+
isn't the module's declared export.
|
|
300
|
+
*/
|
|
301
|
+
function detectExportName(source: string, callStart: number): string | undefined {
|
|
302
|
+
let i = callStart - 1
|
|
303
|
+
while (i >= 0 && isWhitespace(source[i])) {
|
|
304
|
+
i--
|
|
305
|
+
}
|
|
306
|
+
if (source[i] !== '=') {
|
|
307
|
+
return undefined
|
|
308
|
+
}
|
|
309
|
+
i--
|
|
310
|
+
while (i >= 0 && isWhitespace(source[i])) {
|
|
311
|
+
i--
|
|
312
|
+
}
|
|
313
|
+
const nameEnd = i + 1
|
|
314
|
+
while (i >= 0 && isIdentPart(source[i])) {
|
|
315
|
+
i--
|
|
316
|
+
}
|
|
317
|
+
const nameStart = i + 1
|
|
318
|
+
if (nameStart === nameEnd) {
|
|
319
|
+
return undefined
|
|
320
|
+
}
|
|
321
|
+
const name = source.slice(nameStart, nameEnd)
|
|
322
|
+
while (i >= 0 && isWhitespace(source[i])) {
|
|
323
|
+
i--
|
|
324
|
+
}
|
|
325
|
+
if (!matchesBackwards(source, i, 'const')) {
|
|
326
|
+
return undefined
|
|
327
|
+
}
|
|
328
|
+
i -= 'const'.length
|
|
329
|
+
while (i >= 0 && isWhitespace(source[i])) {
|
|
330
|
+
i--
|
|
331
|
+
}
|
|
332
|
+
if (!matchesBackwards(source, i, 'export')) {
|
|
333
|
+
return undefined
|
|
334
|
+
}
|
|
335
|
+
return name
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function matchesBackwards(source: string, end: number, keyword: string): boolean {
|
|
339
|
+
const start = end - keyword.length + 1
|
|
340
|
+
if (start < 0) {
|
|
341
|
+
return false
|
|
342
|
+
}
|
|
343
|
+
if (source.slice(start, end + 1) !== keyword) {
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
return start === 0 || !isIdentPart(source[start - 1])
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isIdentStart(c: string | undefined): boolean {
|
|
350
|
+
if (c === undefined) {
|
|
351
|
+
return false
|
|
352
|
+
}
|
|
353
|
+
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_' || c === '$'
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function isIdentPart(c: string | undefined): boolean {
|
|
357
|
+
if (c === undefined) {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
return isIdentStart(c) || (c >= '0' && c <= '9')
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function isWhitespace(c: string | undefined): boolean {
|
|
364
|
+
return c === ' ' || c === '\t' || c === '\n' || c === '\r'
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/*
|
|
368
|
+
A `/` starts a regex literal when the prior expression context expects an
|
|
369
|
+
expression rather than a value — after an open delimiter, operator, or
|
|
370
|
+
expression-prefix keyword (return, typeof, instanceof, in, of, delete,
|
|
371
|
+
void, await, yield, new, throw, case, do). Otherwise `/` is division.
|
|
372
|
+
Without this disambiguation a regex like `/^\//` reads as `/` (division),
|
|
373
|
+
then `^`, `\`, `/`, `/` — and the final `//` pair fakes a line comment
|
|
374
|
+
that swallows the rest of the line, eating any `)` that closes the
|
|
375
|
+
enclosing call.
|
|
376
|
+
*/
|
|
377
|
+
const REGEX_PREFIX_KEYWORDS = new Set([
|
|
378
|
+
'return',
|
|
379
|
+
'typeof',
|
|
380
|
+
'instanceof',
|
|
381
|
+
'in',
|
|
382
|
+
'of',
|
|
383
|
+
'delete',
|
|
384
|
+
'void',
|
|
385
|
+
'await',
|
|
386
|
+
'yield',
|
|
387
|
+
'new',
|
|
388
|
+
'throw',
|
|
389
|
+
'case',
|
|
390
|
+
'do',
|
|
391
|
+
])
|
|
392
|
+
|
|
393
|
+
const REGEX_PUNCTUATION = new Set([
|
|
394
|
+
'(',
|
|
395
|
+
'[',
|
|
396
|
+
'{',
|
|
397
|
+
',',
|
|
398
|
+
';',
|
|
399
|
+
':',
|
|
400
|
+
'?',
|
|
401
|
+
'!',
|
|
402
|
+
'&',
|
|
403
|
+
'|',
|
|
404
|
+
'^',
|
|
405
|
+
'~',
|
|
406
|
+
'+',
|
|
407
|
+
'-',
|
|
408
|
+
'*',
|
|
409
|
+
'%',
|
|
410
|
+
'<',
|
|
411
|
+
'>',
|
|
412
|
+
'=',
|
|
413
|
+
'/',
|
|
414
|
+
])
|
|
415
|
+
|
|
416
|
+
function isRegexContext(source: string, slashIndex: number): boolean {
|
|
417
|
+
let i = slashIndex - 1
|
|
418
|
+
while (i >= 0 && isWhitespace(source[i])) {
|
|
419
|
+
i--
|
|
420
|
+
}
|
|
421
|
+
if (i < 0) {
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
424
|
+
const prev = source[i] as string
|
|
425
|
+
if (REGEX_PUNCTUATION.has(prev)) {
|
|
426
|
+
return true
|
|
427
|
+
}
|
|
428
|
+
if (isIdentPart(prev)) {
|
|
429
|
+
let start = i
|
|
430
|
+
while (start > 0 && isIdentPart(source[start - 1])) {
|
|
431
|
+
start--
|
|
432
|
+
}
|
|
433
|
+
return REGEX_PREFIX_KEYWORDS.has(source.slice(start, i + 1))
|
|
434
|
+
}
|
|
435
|
+
return false
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/*
|
|
439
|
+
Walks past a regex literal body, respecting character classes (`[...]`
|
|
440
|
+
where `/` is literal) and backslash escapes, then consumes trailing
|
|
441
|
+
flag identifiers. Returns the index immediately after the regex. An
|
|
442
|
+
unterminated regex (newline before closing `/`) returns the newline
|
|
443
|
+
position so the outer scanner can resume normally on the next line.
|
|
444
|
+
*/
|
|
445
|
+
function skipRegex(source: string, start: number): number {
|
|
446
|
+
let i = start
|
|
447
|
+
let inClass = false
|
|
448
|
+
while (i < source.length) {
|
|
449
|
+
const c = source[i]
|
|
450
|
+
if (c === '\\') {
|
|
451
|
+
i += 2
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
454
|
+
if (c === '\n') {
|
|
455
|
+
return i
|
|
456
|
+
}
|
|
457
|
+
if (inClass) {
|
|
458
|
+
if (c === ']') {
|
|
459
|
+
inClass = false
|
|
460
|
+
}
|
|
461
|
+
i++
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
if (c === '[') {
|
|
465
|
+
inClass = true
|
|
466
|
+
i++
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
if (c === '/') {
|
|
470
|
+
let j = i + 1
|
|
471
|
+
while (j < source.length && isIdentPart(source[j])) {
|
|
472
|
+
j++
|
|
473
|
+
}
|
|
474
|
+
return j
|
|
475
|
+
}
|
|
476
|
+
i++
|
|
477
|
+
}
|
|
478
|
+
return source.length
|
|
479
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Headers belte forwards from an inbound HTTP/MCP request onto every
|
|
3
|
+
synthesized in-process rpc Request — cookies + bearer auth + the four
|
|
4
|
+
forwarding hints proxies set when terminating TLS in front of the app.
|
|
5
|
+
defineVerb uses this when an SSR pass calls a verb in-process; the MCP
|
|
6
|
+
dispatcher uses it when piping a tool invocation through verb.fetch.
|
|
7
|
+
|
|
8
|
+
Centralised so both call sites can't drift on which headers are
|
|
9
|
+
considered "auth/identity" context.
|
|
10
|
+
*/
|
|
11
|
+
export const FORWARDED_HEADERS = [
|
|
12
|
+
'cookie',
|
|
13
|
+
'authorization',
|
|
14
|
+
'x-forwarded-for',
|
|
15
|
+
'x-forwarded-proto',
|
|
16
|
+
'x-forwarded-host',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export function forwardHeaders(source: Headers): Headers {
|
|
20
|
+
const headers = new Headers()
|
|
21
|
+
for (const name of FORWARDED_HEADERS) {
|
|
22
|
+
const value = source.get(name)
|
|
23
|
+
if (value) {
|
|
24
|
+
headers.set(name, value)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return headers
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Matches the conventions of the `debug` npm package.
|
|
3
|
+
DEBUG="belte" → enables "belte"
|
|
4
|
+
DEBUG="belte:*" → enables "belte" and "belte:anything"
|
|
5
|
+
DEBUG="*" → enables everything
|
|
6
|
+
DEBUG="a,belte" → comma-separated list
|
|
7
|
+
*/
|
|
8
|
+
export function isDebugEnabled(name: string, env: string | undefined = process.env.DEBUG): boolean {
|
|
9
|
+
if (!env) {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
return env.split(',').some((raw) => {
|
|
13
|
+
const pattern = raw.trim()
|
|
14
|
+
if (pattern === '*') {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
if (pattern.endsWith(':*')) {
|
|
18
|
+
const prefix = pattern.slice(0, -2)
|
|
19
|
+
return name === prefix || name.startsWith(`${prefix}:`)
|
|
20
|
+
}
|
|
21
|
+
return pattern === name
|
|
22
|
+
})
|
|
23
|
+
}
|