@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,333 @@
|
|
|
1
|
+
import { HttpError } from '../server/HttpError.ts'
|
|
2
|
+
import { decodeResponse } from './decodeResponse.ts'
|
|
3
|
+
import { STREAMING_CONTENT_TYPES } from './streamingContentTypes.ts'
|
|
4
|
+
import type { Subscribable } from './types/Subscribable.ts'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Turns a Response into an AsyncIterable of frames. Used by
|
|
8
|
+
`fn.stream(args)` to give callers a uniform iterator regardless of the
|
|
9
|
+
handler's chosen body format. Three shapes are handled:
|
|
10
|
+
|
|
11
|
+
- text/event-stream (SSE): emits the JSON-parsed `data:` payload of
|
|
12
|
+
each event. The `event: error\ndata: {message}` frame the `sse()`
|
|
13
|
+
helper emits on generator throws is mapped back to a thrown Error so
|
|
14
|
+
consumers see the failure mid-iteration.
|
|
15
|
+
- application/jsonl + application/x-ndjson: emits one JSON value per
|
|
16
|
+
line. The trailing `{"$error":"..."}` line the `jsonl()` helper
|
|
17
|
+
emits on generator throws is likewise re-thrown.
|
|
18
|
+
- everything else: one-shot — yields the Content-Type-decoded body
|
|
19
|
+
once, then completes. Lets `fn.stream(args)` work uniformly on every
|
|
20
|
+
rpc handler, not just the streaming ones.
|
|
21
|
+
|
|
22
|
+
Non-2xx responses surface as a thrown HttpError on the first pull,
|
|
23
|
+
mirroring the plain `fn(args)` decode path.
|
|
24
|
+
*/
|
|
25
|
+
function streamResponse<T>(response: Response): AsyncIterable<T> {
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
return errorIterable(new HttpError(response))
|
|
28
|
+
}
|
|
29
|
+
const contentType = (response.headers.get('content-type') ?? '').toLowerCase()
|
|
30
|
+
if (contentType.startsWith('text/event-stream')) {
|
|
31
|
+
return parseSse<T>(response)
|
|
32
|
+
}
|
|
33
|
+
if (STREAMING_CONTENT_TYPES.some((type) => contentType.startsWith(type))) {
|
|
34
|
+
return parseJsonLines<T>(response)
|
|
35
|
+
}
|
|
36
|
+
return oneShot<T>(response)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function errorIterable<T>(error: Error): AsyncIterable<T> {
|
|
40
|
+
return {
|
|
41
|
+
[Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
|
|
42
|
+
let done = false
|
|
43
|
+
return {
|
|
44
|
+
async next() {
|
|
45
|
+
if (done) {
|
|
46
|
+
return { value: undefined, done: true }
|
|
47
|
+
}
|
|
48
|
+
done = true
|
|
49
|
+
throw error
|
|
50
|
+
},
|
|
51
|
+
async return() {
|
|
52
|
+
done = true
|
|
53
|
+
return { value: undefined, done: true }
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
One-shot iterator over a non-streaming Response: decodes the body once
|
|
62
|
+
via the same Content-Type sniffing the plain call uses, yields it, then
|
|
63
|
+
completes. Makes `fn.stream(args)` symmetrical across streaming and
|
|
64
|
+
non-streaming handlers — callers can pick the iteration shape without
|
|
65
|
+
worrying about which body the handler returned.
|
|
66
|
+
*/
|
|
67
|
+
function oneShot<T>(response: Response): AsyncIterable<T> {
|
|
68
|
+
return {
|
|
69
|
+
[Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
|
|
70
|
+
let yielded = false
|
|
71
|
+
return {
|
|
72
|
+
async next() {
|
|
73
|
+
if (yielded) {
|
|
74
|
+
return { value: undefined, done: true }
|
|
75
|
+
}
|
|
76
|
+
yielded = true
|
|
77
|
+
const value = (await decodeResponse(response)) as T
|
|
78
|
+
return { value, done: false }
|
|
79
|
+
},
|
|
80
|
+
async return() {
|
|
81
|
+
yielded = true
|
|
82
|
+
return { value: undefined, done: true }
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
SSE parser: reads the response body as text frames separated by blank
|
|
91
|
+
lines, splits each frame into `event:` / `data:` lines, and yields the
|
|
92
|
+
JSON-parsed data payload. The `sse()` respond helper emits an
|
|
93
|
+
`event: error\ndata: {"message":...}` frame when the source generator
|
|
94
|
+
throws, which we surface as a thrown Error so consumer loops can
|
|
95
|
+
surface mid-stream failure rather than silently stopping.
|
|
96
|
+
*/
|
|
97
|
+
function parseSse<T>(response: Response): AsyncIterable<T> {
|
|
98
|
+
return {
|
|
99
|
+
[Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
|
|
100
|
+
const body = response.body
|
|
101
|
+
if (!body) {
|
|
102
|
+
return emptyIterator<T>()
|
|
103
|
+
}
|
|
104
|
+
const reader = body.pipeThrough(new TextDecoderStream()).getReader()
|
|
105
|
+
let buffer = ''
|
|
106
|
+
let bufferStart = 0
|
|
107
|
+
const pending: Array<{ event: string; data: string }> = []
|
|
108
|
+
let done = false
|
|
109
|
+
|
|
110
|
+
async function pullFrames(): Promise<void> {
|
|
111
|
+
while (pending.length === 0 && !done) {
|
|
112
|
+
const { value, done: streamDone } = await reader.read()
|
|
113
|
+
if (streamDone) {
|
|
114
|
+
done = true
|
|
115
|
+
if (bufferStart < buffer.length) {
|
|
116
|
+
const frame = parseFrame(buffer.slice(bufferStart))
|
|
117
|
+
if (frame) {
|
|
118
|
+
pending.push(frame)
|
|
119
|
+
}
|
|
120
|
+
buffer = ''
|
|
121
|
+
bufferStart = 0
|
|
122
|
+
}
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
/*
|
|
126
|
+
Compact only when the unread region is small relative to
|
|
127
|
+
the consumed prefix — keeps amortised work O(n) instead
|
|
128
|
+
of quadratic slicing per frame boundary.
|
|
129
|
+
*/
|
|
130
|
+
if (bufferStart > buffer.length / 2) {
|
|
131
|
+
buffer = buffer.slice(bufferStart) + value
|
|
132
|
+
bufferStart = 0
|
|
133
|
+
} else {
|
|
134
|
+
buffer += value
|
|
135
|
+
}
|
|
136
|
+
let boundary = buffer.indexOf('\n\n', bufferStart)
|
|
137
|
+
while (boundary !== -1) {
|
|
138
|
+
const raw = buffer.slice(bufferStart, boundary)
|
|
139
|
+
bufferStart = boundary + 2
|
|
140
|
+
const frame = parseFrame(raw)
|
|
141
|
+
if (frame) {
|
|
142
|
+
pending.push(frame)
|
|
143
|
+
}
|
|
144
|
+
boundary = buffer.indexOf('\n\n', bufferStart)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
async next() {
|
|
151
|
+
while (true) {
|
|
152
|
+
if (pending.length > 0) {
|
|
153
|
+
const next = pending.shift() as { event: string; data: string }
|
|
154
|
+
if (next.event === 'error') {
|
|
155
|
+
try {
|
|
156
|
+
const decoded = JSON.parse(next.data) as { message?: string }
|
|
157
|
+
throw new Error(decoded?.message ?? 'sse stream error')
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err instanceof SyntaxError) {
|
|
160
|
+
throw new Error(next.data || 'sse stream error')
|
|
161
|
+
}
|
|
162
|
+
throw err
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const value = JSON.parse(next.data) as T
|
|
166
|
+
return { value, done: false }
|
|
167
|
+
}
|
|
168
|
+
if (done) {
|
|
169
|
+
return { value: undefined, done: true }
|
|
170
|
+
}
|
|
171
|
+
await pullFrames()
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
async return() {
|
|
175
|
+
done = true
|
|
176
|
+
await reader.cancel().catch(() => undefined)
|
|
177
|
+
return { value: undefined, done: true }
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseFrame(raw: string): { event: string; data: string } | undefined {
|
|
185
|
+
const lines = raw.split('\n').filter((line) => line.length > 0 && !line.startsWith(':'))
|
|
186
|
+
if (lines.length === 0) {
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
let event = 'message'
|
|
190
|
+
const dataLines: string[] = []
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
const colon = line.indexOf(':')
|
|
193
|
+
const field = colon === -1 ? line : line.slice(0, colon)
|
|
194
|
+
const value = colon === -1 ? '' : line.slice(colon + 1).replace(/^ /, '')
|
|
195
|
+
if (field === 'event') {
|
|
196
|
+
event = value
|
|
197
|
+
} else if (field === 'data') {
|
|
198
|
+
dataLines.push(value)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (dataLines.length === 0) {
|
|
202
|
+
return undefined
|
|
203
|
+
}
|
|
204
|
+
return { event, data: dataLines.join('\n') }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/*
|
|
208
|
+
JSONL/NDJSON parser: reads the response body as text, splits on `\n`,
|
|
209
|
+
parses each non-empty line as JSON, and yields the value. The `jsonl()`
|
|
210
|
+
respond helper emits a trailing `{"$error":"<message>"}` line when the
|
|
211
|
+
source generator throws — that's surfaced here as a thrown Error so
|
|
212
|
+
consumer loops can react to mid-stream failure.
|
|
213
|
+
*/
|
|
214
|
+
function parseJsonLines<T>(response: Response): AsyncIterable<T> {
|
|
215
|
+
return {
|
|
216
|
+
[Symbol.asyncIterator](): AsyncIterator<T, void, undefined> {
|
|
217
|
+
const body = response.body
|
|
218
|
+
if (!body) {
|
|
219
|
+
return emptyIterator<T>()
|
|
220
|
+
}
|
|
221
|
+
const reader = body.pipeThrough(new TextDecoderStream()).getReader()
|
|
222
|
+
let buffer = ''
|
|
223
|
+
let bufferStart = 0
|
|
224
|
+
const pending: string[] = []
|
|
225
|
+
let done = false
|
|
226
|
+
|
|
227
|
+
async function pullLines(): Promise<void> {
|
|
228
|
+
while (pending.length === 0 && !done) {
|
|
229
|
+
const { value, done: streamDone } = await reader.read()
|
|
230
|
+
if (streamDone) {
|
|
231
|
+
done = true
|
|
232
|
+
if (bufferStart < buffer.length) {
|
|
233
|
+
pending.push(buffer.slice(bufferStart))
|
|
234
|
+
buffer = ''
|
|
235
|
+
bufferStart = 0
|
|
236
|
+
}
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (bufferStart > buffer.length / 2) {
|
|
240
|
+
buffer = buffer.slice(bufferStart) + value
|
|
241
|
+
bufferStart = 0
|
|
242
|
+
} else {
|
|
243
|
+
buffer += value
|
|
244
|
+
}
|
|
245
|
+
let newline = buffer.indexOf('\n', bufferStart)
|
|
246
|
+
while (newline !== -1) {
|
|
247
|
+
const line = buffer.slice(bufferStart, newline)
|
|
248
|
+
bufferStart = newline + 1
|
|
249
|
+
if (line.length > 0) {
|
|
250
|
+
pending.push(line)
|
|
251
|
+
}
|
|
252
|
+
newline = buffer.indexOf('\n', bufferStart)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
async next() {
|
|
259
|
+
while (true) {
|
|
260
|
+
if (pending.length > 0) {
|
|
261
|
+
const line = pending.shift() as string
|
|
262
|
+
const parsed = JSON.parse(line) as Record<string, unknown> & {
|
|
263
|
+
$error?: string
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
parsed &&
|
|
267
|
+
typeof parsed === 'object' &&
|
|
268
|
+
typeof parsed.$error === 'string'
|
|
269
|
+
) {
|
|
270
|
+
throw new Error(parsed.$error)
|
|
271
|
+
}
|
|
272
|
+
return { value: parsed as T, done: false }
|
|
273
|
+
}
|
|
274
|
+
if (done) {
|
|
275
|
+
return { value: undefined, done: true }
|
|
276
|
+
}
|
|
277
|
+
await pullLines()
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
async return() {
|
|
281
|
+
done = true
|
|
282
|
+
await reader.cancel().catch(() => undefined)
|
|
283
|
+
return { value: undefined, done: true }
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function emptyIterator<T>(): AsyncIterator<T, void, undefined> {
|
|
291
|
+
return {
|
|
292
|
+
async next() {
|
|
293
|
+
return { value: undefined, done: true }
|
|
294
|
+
},
|
|
295
|
+
async return() {
|
|
296
|
+
return { value: undefined, done: true }
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/*
|
|
302
|
+
Builds the Subscribable returned by `fn.stream(args)`. The carried
|
|
303
|
+
`name` is the cache-style key for (method, url, args) so subscribe()
|
|
304
|
+
dedupes multiple subscribers to identical args into one underlying
|
|
305
|
+
fetch. The fetch is deferred until the first iterator pull so simply
|
|
306
|
+
constructing the Subscribable (which happens on every $derived
|
|
307
|
+
re-evaluation) doesn't open a connection — subscribe()'s registry
|
|
308
|
+
short-circuits the second instance before it iterates.
|
|
309
|
+
*/
|
|
310
|
+
export function subscribableFromResponse<T>(
|
|
311
|
+
name: string,
|
|
312
|
+
fetchResponse: () => Promise<Response>,
|
|
313
|
+
): Subscribable<T> {
|
|
314
|
+
return {
|
|
315
|
+
name,
|
|
316
|
+
[Symbol.asyncIterator]() {
|
|
317
|
+
let inner: AsyncIterator<T, void, undefined> | undefined
|
|
318
|
+
return {
|
|
319
|
+
async next() {
|
|
320
|
+
if (!inner) {
|
|
321
|
+
const response = await fetchResponse()
|
|
322
|
+
inner = streamResponse<T>(response)[Symbol.asyncIterator]()
|
|
323
|
+
}
|
|
324
|
+
return inner.next()
|
|
325
|
+
},
|
|
326
|
+
async return() {
|
|
327
|
+
await inner?.return?.(undefined)
|
|
328
|
+
return { value: undefined, done: true }
|
|
329
|
+
},
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseRouteSegments } from './parseRouteSegments.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Translates a belte route URL (`/media/[id]/[...rest]`) into the pattern Bun
|
|
5
|
+
needs (`/media/:id/*`) for `Bun.serve({ routes })`. Returns the catch-all
|
|
6
|
+
segment's original name alongside so the server can rename Bun's `*` param
|
|
7
|
+
back to that name on the way out, keeping page-prop destructuring consistent
|
|
8
|
+
with the route file path.
|
|
9
|
+
*/
|
|
10
|
+
export function toBunRoutePattern(routeUrl: string): {
|
|
11
|
+
pattern: string
|
|
12
|
+
catchAllName: string | undefined
|
|
13
|
+
} {
|
|
14
|
+
let catchAllName: string | undefined
|
|
15
|
+
const pattern = parseRouteSegments(routeUrl)
|
|
16
|
+
.map((segment) => {
|
|
17
|
+
if (segment.kind === 'literal') {
|
|
18
|
+
return segment.value
|
|
19
|
+
}
|
|
20
|
+
if (segment.catchAll) {
|
|
21
|
+
catchAllName = segment.name
|
|
22
|
+
return '*'
|
|
23
|
+
}
|
|
24
|
+
return `:${segment.name}`
|
|
25
|
+
})
|
|
26
|
+
.join('/')
|
|
27
|
+
return { pattern, catchAllName }
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Stored shape per cache key. `request` is retained so SSR snapshot
|
|
3
|
+
serialization can record the URL and method without re-deriving them from
|
|
4
|
+
the function. `ttl`/`expiresAt` drive eviction: expiresAt = undefined means
|
|
5
|
+
"no TTL" (lives forever); ttl = 0 means "dedupe only" (entry is pruned as
|
|
6
|
+
soon as the promise settles). The stored promise resolves to the raw
|
|
7
|
+
Response so the snapshot can read its status/headers/body; the cache
|
|
8
|
+
layer hands callers a decoded view derived from this same promise.
|
|
9
|
+
*/
|
|
10
|
+
export type CacheEntry = {
|
|
11
|
+
key: string
|
|
12
|
+
promise: Promise<Response>
|
|
13
|
+
request: Request
|
|
14
|
+
ttl: number | undefined
|
|
15
|
+
expiresAt: number | undefined
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Options for cache(). `key` overrides the auto-derived WeakMap key — useful
|
|
3
|
+
when sharing entries across calls or stripping noisy args. `ttl` is the
|
|
4
|
+
milliseconds-past-resolve that the entry stays live: omitted = forever, 0 =
|
|
5
|
+
dedupe only (entry dropped once the promise settles), any other number = TTL.
|
|
6
|
+
*/
|
|
7
|
+
export type CacheOptions = {
|
|
8
|
+
key?: string | unknown[] | Record<string, unknown>
|
|
9
|
+
ttl?: number
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Wire format for a single cached response shipped from SSR to client hydration.
|
|
3
|
+
Only GET/DELETE entries with a textual Content-Type are emitted — POST/PUT
|
|
4
|
+
bodies can't be reconstructed without shipping the original request body,
|
|
5
|
+
and binary bodies don't survive a JSON round-trip.
|
|
6
|
+
*/
|
|
7
|
+
export type CacheSnapshotEntry = {
|
|
8
|
+
key: string
|
|
9
|
+
url: string
|
|
10
|
+
method: 'GET' | 'DELETE'
|
|
11
|
+
status: number
|
|
12
|
+
statusText: string
|
|
13
|
+
headers: Array<[string, string]>
|
|
14
|
+
body: string
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CacheEntry } from './CacheEntry.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Cache map paired with a Svelte-aware per-key subscriber. Calling
|
|
5
|
+
`subscribe(key)` from inside a tracking scope ($derived / $effect) registers
|
|
6
|
+
that scope to re-run when the entry is invalidated; called outside tracking
|
|
7
|
+
it's a no-op. Subscribers live for the lifetime of the store: the server
|
|
8
|
+
uses a fresh store per request (so subscribers die with the response), the
|
|
9
|
+
client uses a single module-level store (so subscribers persist for the tab).
|
|
10
|
+
*/
|
|
11
|
+
export type CacheStore = {
|
|
12
|
+
entries: Map<string, CacheEntry>
|
|
13
|
+
events: EventTarget
|
|
14
|
+
subscribe: (key: string) => void
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Which client surfaces a verb or socket is exposed to. Browser is the
|
|
3
|
+
historical default; MCP and CLI flip on automatically when the
|
|
4
|
+
declaration carries a Standard Schema (the schema is what makes the
|
|
5
|
+
non-browser surfaces safe to advertise). Explicit values always win.
|
|
6
|
+
*/
|
|
7
|
+
export type ClientFlags = {
|
|
8
|
+
browser: boolean
|
|
9
|
+
mcp: boolean
|
|
10
|
+
cli: boolean
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The thing `subscribe()` reads from: an AsyncIterable carrying a stable
|
|
3
|
+
`name` used as the subscription registry key. Both `Socket<T>` (the
|
|
4
|
+
declared broadcast primitive) and the result of `fn.stream(args)`
|
|
5
|
+
(per-call HTTP stream consumer) satisfy this shape, so subscribe() can
|
|
6
|
+
share one iterator across multiple readers regardless of source.
|
|
7
|
+
|
|
8
|
+
The name on a Socket comes from the file path under `src/server/sockets/`.
|
|
9
|
+
The name on an fn.stream(args) result is `keyForRemoteCall(method, url,
|
|
10
|
+
args)` — the same key cache() uses — so two subscribers to the same
|
|
11
|
+
remote-call args dedupe to one underlying fetch.
|
|
12
|
+
*/
|
|
13
|
+
export interface Subscribable<T> extends AsyncIterable<T> {
|
|
14
|
+
readonly name: string
|
|
15
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { pageUrlForFile } from './pageUrlForFile.ts'
|
|
2
|
+
import { parseRouteSegments } from './parseRouteSegments.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Walks a `[name]` / `[...rest]` route URL and returns the param shape it
|
|
6
|
+
declares. Catch-all segments map to `string` under their declared name —
|
|
7
|
+
the server's toBunRoutePattern renames Bun's `*` key back to that name
|
|
8
|
+
when dispatching, so the page component sees `params.rest`, not
|
|
9
|
+
`params['*']`.
|
|
10
|
+
*/
|
|
11
|
+
function paramsForRoute(routeUrl: string): Record<string, 'string'> {
|
|
12
|
+
const params: Record<string, 'string'> = {}
|
|
13
|
+
for (const segment of parseRouteSegments(routeUrl)) {
|
|
14
|
+
if (segment.kind === 'param') {
|
|
15
|
+
params[segment.name] = 'string'
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return params
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderParamsShape(shape: Record<string, 'string'>): string {
|
|
22
|
+
const keys = Object.keys(shape)
|
|
23
|
+
if (keys.length === 0) {
|
|
24
|
+
return 'Record<string, never>'
|
|
25
|
+
}
|
|
26
|
+
return `{ ${keys.map((key) => `${JSON.stringify(key)}: string`).join('; ')} }`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
Emits a `.d.ts` that augments belte's `Routes` interface with one entry per
|
|
31
|
+
page file in the project. Page picks this up as a discriminated union keyed
|
|
32
|
+
on `route`, so `if (page.route === '/media/[id]') page.params.id` is typed
|
|
33
|
+
automatically without consumers writing route types by hand.
|
|
34
|
+
The file is written to `src/.belte/routes.d.ts` so the consumer's existing
|
|
35
|
+
src tsconfig include picks it up with no extra configuration.
|
|
36
|
+
*/
|
|
37
|
+
export async function writeRoutesDts({
|
|
38
|
+
cwd,
|
|
39
|
+
pageFiles,
|
|
40
|
+
}: {
|
|
41
|
+
cwd: string
|
|
42
|
+
pageFiles: string[]
|
|
43
|
+
}): Promise<void> {
|
|
44
|
+
const entries = pageFiles
|
|
45
|
+
.map((file) => ({
|
|
46
|
+
route: pageUrlForFile(file),
|
|
47
|
+
params: paramsForRoute(pageUrlForFile(file)),
|
|
48
|
+
}))
|
|
49
|
+
.toSorted((a, b) => a.route.localeCompare(b.route))
|
|
50
|
+
.map(
|
|
51
|
+
({ route, params }) => ` ${JSON.stringify(route)}: ${renderParamsShape(params)}`,
|
|
52
|
+
)
|
|
53
|
+
.join('\n')
|
|
54
|
+
const contents = `// Generated by belte. Do not edit by hand.
|
|
55
|
+
declare module 'belte/browser/page' {
|
|
56
|
+
interface Routes {
|
|
57
|
+
${entries}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export {}
|
|
62
|
+
`
|
|
63
|
+
await Bun.write(`${cwd}/src/.belte/routes.d.ts`, contents)
|
|
64
|
+
}
|
package/src/preload.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { plugin } from 'bun'
|
|
2
|
+
import { belteResolverPlugin } from './belteResolverPlugin.ts'
|
|
3
|
+
import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
|
|
4
|
+
import { sveltePlugin } from './sveltePlugin.ts'
|
|
5
|
+
|
|
6
|
+
const mode = (process.env.BELTE_SVELTE_MODE ?? 'server') as 'server' | 'client'
|
|
7
|
+
const svelteConfig = await loadSvelteConfig()
|
|
8
|
+
|
|
9
|
+
await plugin(sveltePlugin({ generate: mode, svelteConfig }))
|
|
10
|
+
await plugin(belteResolverPlugin({ target: mode }))
|
|
11
|
+
|
|
12
|
+
await plugin({
|
|
13
|
+
name: 'css-noop',
|
|
14
|
+
setup(build) {
|
|
15
|
+
build.onLoad({ filter: /\.css$/ }, () => ({
|
|
16
|
+
contents: 'export default {};',
|
|
17
|
+
loader: 'js',
|
|
18
|
+
}))
|
|
19
|
+
},
|
|
20
|
+
})
|
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Glob } from 'bun'
|
|
2
|
+
import { log } from './lib/shared/log.ts'
|
|
3
|
+
|
|
4
|
+
const TEMPLATE_DIR = new URL('../template', import.meta.url).pathname
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Copies the bundled template directory into `${cwd}/${name}`. Refuses to write
|
|
8
|
+
into a non-empty directory so an accidental run doesn't overwrite real work.
|
|
9
|
+
*/
|
|
10
|
+
export async function scaffold({
|
|
11
|
+
cwd = process.cwd(),
|
|
12
|
+
name,
|
|
13
|
+
}: {
|
|
14
|
+
cwd?: string
|
|
15
|
+
name: string
|
|
16
|
+
}): Promise<string> {
|
|
17
|
+
const trimmed = name.trim()
|
|
18
|
+
if (trimmed === '') {
|
|
19
|
+
throw new Error('[belte] project name is required: bunx belte scaffold <name>')
|
|
20
|
+
}
|
|
21
|
+
const target = resolveTarget(cwd, trimmed)
|
|
22
|
+
if (await targetIsNonEmpty(target)) {
|
|
23
|
+
throw new Error(`[belte] target directory is not empty: ${target}`)
|
|
24
|
+
}
|
|
25
|
+
if (!(await Bun.file(`${TEMPLATE_DIR}/package.json`).exists())) {
|
|
26
|
+
throw new Error(`[belte] template missing at ${TEMPLATE_DIR}`)
|
|
27
|
+
}
|
|
28
|
+
await copyTree(TEMPLATE_DIR, target)
|
|
29
|
+
log.success(`scaffolded belte project at ${target}`)
|
|
30
|
+
log.detail(' next steps:')
|
|
31
|
+
if (target !== cwd) {
|
|
32
|
+
log.detail(` cd ${trimmed}`)
|
|
33
|
+
}
|
|
34
|
+
log.detail(' bun install')
|
|
35
|
+
log.detail(' bun run dev')
|
|
36
|
+
return target
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
Copies every file under `from` into `to`, preserving relative paths. Uses
|
|
41
|
+
Bun.Glob to enumerate (dotfiles included) and Bun.write to materialize each
|
|
42
|
+
file — Bun.write auto-creates parent directories.
|
|
43
|
+
*/
|
|
44
|
+
async function copyTree(from: string, to: string): Promise<void> {
|
|
45
|
+
const files = await Array.fromAsync(
|
|
46
|
+
new Glob('**/*').scan({ cwd: from, onlyFiles: true, dot: true }),
|
|
47
|
+
)
|
|
48
|
+
await Promise.all(
|
|
49
|
+
files.map(async (relativePath) => {
|
|
50
|
+
const source = Bun.file(`${from}/${relativePath}`)
|
|
51
|
+
await Bun.write(`${to}/${relativePath}`, source)
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
Resolves the user-supplied name against the working directory. Absolute
|
|
58
|
+
paths (`/tmp/foo`) and `~`-prefixed paths are used as-is; relative names
|
|
59
|
+
are joined onto `cwd`.
|
|
60
|
+
*/
|
|
61
|
+
function resolveTarget(cwd: string, name: string): string {
|
|
62
|
+
if (name === '.' || name === './') {
|
|
63
|
+
return cwd
|
|
64
|
+
}
|
|
65
|
+
if (name.startsWith('/')) {
|
|
66
|
+
return name
|
|
67
|
+
}
|
|
68
|
+
if (name.startsWith('~/')) {
|
|
69
|
+
const home = process.env.HOME ?? ''
|
|
70
|
+
return `${home}${name.slice(1)}`
|
|
71
|
+
}
|
|
72
|
+
return `${cwd}/${name}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
True when the target exists and contains at least one entry. Uses Bun.Glob
|
|
77
|
+
rather than fs.readdir to honor the project's "Bun-first" rule. A missing
|
|
78
|
+
directory is reported as empty so first-time scaffolds proceed.
|
|
79
|
+
*/
|
|
80
|
+
async function targetIsNonEmpty(target: string): Promise<boolean> {
|
|
81
|
+
try {
|
|
82
|
+
for await (const _ of new Glob('*').scan({ cwd: target, onlyFiles: false, dot: true })) {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
throw error
|
|
90
|
+
}
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
2
|
+
import * as appMod from './_virtual/app.ts'
|
|
3
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
4
|
+
import { appInfo } from './_virtual/app-info.ts'
|
|
5
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
6
|
+
import { assets } from './_virtual/assets.ts'
|
|
7
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
8
|
+
import cliProgramName from './_virtual/cli-name.ts'
|
|
9
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
10
|
+
import { layouts } from './_virtual/layouts.ts'
|
|
11
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
12
|
+
import mcp from './_virtual/mcp.ts'
|
|
13
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
14
|
+
import { mcpResources } from './_virtual/mcp-resources.ts'
|
|
15
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
16
|
+
import { pages } from './_virtual/pages.ts'
|
|
17
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
18
|
+
import { prompts } from './_virtual/prompts.ts'
|
|
19
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
20
|
+
import { publicAssets } from './_virtual/public-assets.ts'
|
|
21
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
22
|
+
import { rpc } from './_virtual/rpc.ts'
|
|
23
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
24
|
+
import { shell } from './_virtual/shell.ts'
|
|
25
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
26
|
+
import { sockets } from './_virtual/sockets.ts'
|
|
27
|
+
import { createServer } from './lib/server/runtime/createServer.ts'
|
|
28
|
+
import { requestContext } from './lib/server/runtime/requestContext.ts'
|
|
29
|
+
import { setCacheStoreResolver } from './lib/shared/setCacheStoreResolver.ts'
|
|
30
|
+
|
|
31
|
+
setCacheStoreResolver(() => requestContext.getStore()?.cache)
|
|
32
|
+
|
|
33
|
+
await createServer({
|
|
34
|
+
pages,
|
|
35
|
+
rpc,
|
|
36
|
+
sockets,
|
|
37
|
+
prompts,
|
|
38
|
+
layouts,
|
|
39
|
+
shell,
|
|
40
|
+
app: appMod,
|
|
41
|
+
assets,
|
|
42
|
+
publicAssets,
|
|
43
|
+
mcpResources,
|
|
44
|
+
mcp,
|
|
45
|
+
cliProgramName,
|
|
46
|
+
appInfo,
|
|
47
|
+
})
|