@briancray/belte 0.3.0 → 0.4.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/package.json +1 -1
- package/src/bundleApp.ts +12 -2
- package/src/discoveryEntry.ts +58 -11
- package/src/lib/browser/cache.ts +29 -6
- package/src/lib/browser/startClient.ts +24 -1
- package/src/lib/bundle/disconnected.svelte +18 -19
- package/src/lib/bundle/onMenu.ts +20 -5
- package/src/lib/bundle/openWebview.ts +9 -2
- package/src/lib/bundle/signMacApp.ts +35 -0
- package/src/lib/cli/createClient.ts +65 -27
- package/src/lib/cli/runCli.ts +37 -15
- package/src/lib/cli/types/CliManifestEntry.ts +7 -2
- package/src/lib/mcp/annotationsForMethod.ts +29 -0
- package/src/lib/mcp/createMcpServer.ts +10 -8
- package/src/lib/mcp/dispatchMcpRequest.ts +115 -33
- package/src/lib/mcp/toolResultFromResponse.ts +66 -0
- package/src/lib/server/jsonl.ts +2 -1
- package/src/lib/server/rpc/defineVerb.ts +30 -17
- package/src/lib/server/rpc/parseArgs.ts +2 -1
- package/src/lib/server/rpc/types/VerbHelper.ts +19 -11
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +14 -6
- package/src/lib/server/runtime/buildOpenApiSpec.ts +17 -6
- package/src/lib/server/runtime/createPublicAssetServer.ts +17 -6
- package/src/lib/server/runtime/createServer.ts +37 -9
- package/src/lib/server/runtime/globToPathSet.ts +29 -0
- package/src/lib/server/runtime/logBrowserOnlyRoutes.ts +44 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +71 -8
- package/src/lib/server/sockets/defineSocket.ts +7 -1
- package/src/lib/server/sockets/recentHistory.ts +11 -0
- package/src/lib/server/sockets/socketOperations.ts +35 -0
- package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
- package/src/lib/server/sse.ts +2 -1
- package/src/lib/shared/buildRpcRequest.ts +2 -1
- package/src/lib/shared/carriesBodyArgs.ts +13 -0
- package/src/lib/shared/isReadOnlyMethod.ts +14 -0
- package/src/lib/shared/isStreamingResponse.ts +11 -0
- package/src/lib/shared/jsonlErrorFrame.ts +24 -0
- package/src/lib/shared/keyForRemoteCall.ts +2 -1
- package/src/lib/shared/resolveClientFlags.ts +8 -6
- package/src/lib/shared/responseErrorText.ts +9 -0
- package/src/lib/shared/sseErrorFrame.ts +29 -0
- package/src/lib/shared/streamResponse.ts +168 -0
- package/src/lib/shared/subscribableFromResponse.ts +1 -172
- package/src/lib/shared/types/CacheEntry.ts +6 -0
- package/template/src/bundle/icon.png +0 -0
- package/template/src/server/rpc/getHello.ts +5 -3
- package/src/lib/shared/belteImportName.test.ts +0 -58
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { HttpError } from '../server/HttpError.ts'
|
|
2
|
+
import { decodeResponse } from './decodeResponse.ts'
|
|
3
|
+
import { jsonlErrorFrame } from './jsonlErrorFrame.ts'
|
|
4
|
+
import { sseErrorFrame } from './sseErrorFrame.ts'
|
|
5
|
+
import { STREAMING_CONTENT_TYPES } from './streamingContentTypes.ts'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
Turns a Response into an AsyncIterable of frames, regardless of the
|
|
9
|
+
handler's chosen body format. Shared by `fn.stream(args)` (via
|
|
10
|
+
subscribableFromResponse), the CLI's streaming print path, and the MCP
|
|
11
|
+
tool dispatcher's stream drain — so every surface consumes sse/jsonl
|
|
12
|
+
identically. Three shapes are handled:
|
|
13
|
+
|
|
14
|
+
- text/event-stream (SSE): emits the JSON-parsed `data:` payload of
|
|
15
|
+
each event. The `event: error\ndata: {message}` frame the `sse()`
|
|
16
|
+
helper emits on generator throws is mapped back to a thrown Error so
|
|
17
|
+
consumers see the failure mid-iteration.
|
|
18
|
+
- application/jsonl + application/x-ndjson: emits one JSON value per
|
|
19
|
+
line. The trailing `{"$error":"..."}` line the `jsonl()` helper
|
|
20
|
+
emits on generator throws is likewise re-thrown.
|
|
21
|
+
- everything else: one-shot — yields the Content-Type-decoded body
|
|
22
|
+
once, then completes. Lets callers iterate uniformly on every rpc
|
|
23
|
+
handler, not just the streaming ones.
|
|
24
|
+
|
|
25
|
+
Non-2xx responses surface as a thrown HttpError on the first pull,
|
|
26
|
+
mirroring the plain `fn(args)` decode path.
|
|
27
|
+
*/
|
|
28
|
+
export function streamResponse<T>(response: Response): AsyncIterable<T> {
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
return errorIterable<T>(new HttpError(response))
|
|
31
|
+
}
|
|
32
|
+
const contentType = (response.headers.get('content-type') ?? '').toLowerCase()
|
|
33
|
+
if (contentType.startsWith('text/event-stream')) {
|
|
34
|
+
return parseSse<T>(response)
|
|
35
|
+
}
|
|
36
|
+
if (STREAMING_CONTENT_TYPES.some((type) => contentType.startsWith(type))) {
|
|
37
|
+
return parseJsonLines<T>(response)
|
|
38
|
+
}
|
|
39
|
+
return oneShot<T>(response)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Surfaces a non-2xx response (or any pre-stream failure) as a thrown error on the first pull. */
|
|
43
|
+
async function* errorIterable<T>(error: Error): AsyncGenerator<T> {
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
One-shot iterator over a non-streaming Response: decodes the body once
|
|
49
|
+
via the same Content-Type sniffing the plain call uses, yields it, then
|
|
50
|
+
completes. Makes streaming symmetrical across streaming and
|
|
51
|
+
non-streaming handlers — callers can pick the iteration shape without
|
|
52
|
+
worrying about which body the handler returned.
|
|
53
|
+
*/
|
|
54
|
+
async function* oneShot<T>(response: Response): AsyncGenerator<T> {
|
|
55
|
+
yield (await decodeResponse(response)) as T
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
Reads a streaming text Response and yields raw frame strings split on
|
|
60
|
+
`delimiter` (`\n\n` for SSE events, `\n` for JSON lines). Owns the whole
|
|
61
|
+
buffering lifecycle: incremental decode, amortised-O(n) compaction, a
|
|
62
|
+
final flush of the trailing partial frame, and reader cancellation when
|
|
63
|
+
the consumer stops iterating (the generator's `finally` runs on
|
|
64
|
+
`return()`). The SSE and jsonl parsers layer their per-frame parsing on
|
|
65
|
+
top of this single machine so the two can't drift.
|
|
66
|
+
*/
|
|
67
|
+
async function* frameReader(response: Response, delimiter: string): AsyncGenerator<string> {
|
|
68
|
+
const body = response.body
|
|
69
|
+
if (!body) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
const reader = body.pipeThrough(new TextDecoderStream()).getReader()
|
|
73
|
+
let buffer = ''
|
|
74
|
+
let bufferStart = 0
|
|
75
|
+
try {
|
|
76
|
+
while (true) {
|
|
77
|
+
const { value, done } = await reader.read()
|
|
78
|
+
if (done) {
|
|
79
|
+
if (bufferStart < buffer.length) {
|
|
80
|
+
yield buffer.slice(bufferStart)
|
|
81
|
+
}
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
/*
|
|
85
|
+
Compact only when the unread region is small relative to the
|
|
86
|
+
consumed prefix — keeps amortised work O(n) instead of
|
|
87
|
+
quadratic slicing per frame boundary.
|
|
88
|
+
*/
|
|
89
|
+
if (bufferStart > buffer.length / 2) {
|
|
90
|
+
buffer = buffer.slice(bufferStart) + value
|
|
91
|
+
bufferStart = 0
|
|
92
|
+
} else {
|
|
93
|
+
buffer += value
|
|
94
|
+
}
|
|
95
|
+
let boundary = buffer.indexOf(delimiter, bufferStart)
|
|
96
|
+
while (boundary !== -1) {
|
|
97
|
+
yield buffer.slice(bufferStart, boundary)
|
|
98
|
+
bufferStart = boundary + delimiter.length
|
|
99
|
+
boundary = buffer.indexOf(delimiter, bufferStart)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
await reader.cancel().catch(() => undefined)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
SSE parser: yields the JSON-parsed `data` payload of each `event:`/`data:`
|
|
109
|
+
frame. The error sentinel (`sseErrorFrame`) the `sse()` helper emits on a
|
|
110
|
+
generator throw is surfaced as a thrown Error so consumer loops can react
|
|
111
|
+
to mid-stream failure rather than silently stopping.
|
|
112
|
+
*/
|
|
113
|
+
async function* parseSse<T>(response: Response): AsyncGenerator<T> {
|
|
114
|
+
for await (const raw of frameReader(response, '\n\n')) {
|
|
115
|
+
const frame = parseFrame(raw)
|
|
116
|
+
if (!frame) {
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const errorMessage = sseErrorFrame.decode(frame.event, frame.data)
|
|
120
|
+
if (errorMessage !== undefined) {
|
|
121
|
+
throw new Error(errorMessage)
|
|
122
|
+
}
|
|
123
|
+
yield JSON.parse(frame.data) as T
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseFrame(raw: string): { event: string; data: string } | undefined {
|
|
128
|
+
const lines = raw.split('\n').filter((line) => line.length > 0 && !line.startsWith(':'))
|
|
129
|
+
if (lines.length === 0) {
|
|
130
|
+
return undefined
|
|
131
|
+
}
|
|
132
|
+
let event = 'message'
|
|
133
|
+
const dataLines: string[] = []
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
const colon = line.indexOf(':')
|
|
136
|
+
const field = colon === -1 ? line : line.slice(0, colon)
|
|
137
|
+
const value = colon === -1 ? '' : line.slice(colon + 1).replace(/^ /, '')
|
|
138
|
+
if (field === 'event') {
|
|
139
|
+
event = value
|
|
140
|
+
} else if (field === 'data') {
|
|
141
|
+
dataLines.push(value)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (dataLines.length === 0) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
return { event, data: dataLines.join('\n') }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
JSONL/NDJSON parser: parses each non-empty line as JSON and yields the
|
|
152
|
+
value. The error sentinel (`jsonlErrorFrame`) the `jsonl()` helper emits as
|
|
153
|
+
a trailing line on a generator throw is surfaced here as a thrown Error so
|
|
154
|
+
consumer loops can react to mid-stream failure.
|
|
155
|
+
*/
|
|
156
|
+
async function* parseJsonLines<T>(response: Response): AsyncGenerator<T> {
|
|
157
|
+
for await (const raw of frameReader(response, '\n')) {
|
|
158
|
+
if (raw.length === 0) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
const parsed = JSON.parse(raw)
|
|
162
|
+
const errorMessage = jsonlErrorFrame.decode(parsed)
|
|
163
|
+
if (errorMessage !== undefined) {
|
|
164
|
+
throw new Error(errorMessage)
|
|
165
|
+
}
|
|
166
|
+
yield parsed as T
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -1,177 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { decodeResponse } from './decodeResponse.ts'
|
|
3
|
-
import { STREAMING_CONTENT_TYPES } from './streamingContentTypes.ts'
|
|
1
|
+
import { streamResponse } from './streamResponse.ts'
|
|
4
2
|
import type { Subscribable } from './types/Subscribable.ts'
|
|
5
3
|
|
|
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<T>(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
|
-
/* Surfaces a non-2xx response (or any pre-stream failure) as a thrown error on the first pull. */
|
|
40
|
-
async function* errorIterable<T>(error: Error): AsyncGenerator<T> {
|
|
41
|
-
throw error
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/*
|
|
45
|
-
One-shot iterator over a non-streaming Response: decodes the body once
|
|
46
|
-
via the same Content-Type sniffing the plain call uses, yields it, then
|
|
47
|
-
completes. Makes `fn.stream(args)` symmetrical across streaming and
|
|
48
|
-
non-streaming handlers — callers can pick the iteration shape without
|
|
49
|
-
worrying about which body the handler returned.
|
|
50
|
-
*/
|
|
51
|
-
async function* oneShot<T>(response: Response): AsyncGenerator<T> {
|
|
52
|
-
yield (await decodeResponse(response)) as T
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/*
|
|
56
|
-
Reads a streaming text Response and yields raw frame strings split on
|
|
57
|
-
`delimiter` (`\n\n` for SSE events, `\n` for JSON lines). Owns the whole
|
|
58
|
-
buffering lifecycle: incremental decode, amortised-O(n) compaction, a
|
|
59
|
-
final flush of the trailing partial frame, and reader cancellation when
|
|
60
|
-
the consumer stops iterating (the generator's `finally` runs on
|
|
61
|
-
`return()`). The SSE and jsonl parsers layer their per-frame parsing on
|
|
62
|
-
top of this single machine so the two can't drift.
|
|
63
|
-
*/
|
|
64
|
-
async function* frameReader(response: Response, delimiter: string): AsyncGenerator<string> {
|
|
65
|
-
const body = response.body
|
|
66
|
-
if (!body) {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
const reader = body.pipeThrough(new TextDecoderStream()).getReader()
|
|
70
|
-
let buffer = ''
|
|
71
|
-
let bufferStart = 0
|
|
72
|
-
try {
|
|
73
|
-
while (true) {
|
|
74
|
-
const { value, done } = await reader.read()
|
|
75
|
-
if (done) {
|
|
76
|
-
if (bufferStart < buffer.length) {
|
|
77
|
-
yield buffer.slice(bufferStart)
|
|
78
|
-
}
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
/*
|
|
82
|
-
Compact only when the unread region is small relative to the
|
|
83
|
-
consumed prefix — keeps amortised work O(n) instead of
|
|
84
|
-
quadratic slicing per frame boundary.
|
|
85
|
-
*/
|
|
86
|
-
if (bufferStart > buffer.length / 2) {
|
|
87
|
-
buffer = buffer.slice(bufferStart) + value
|
|
88
|
-
bufferStart = 0
|
|
89
|
-
} else {
|
|
90
|
-
buffer += value
|
|
91
|
-
}
|
|
92
|
-
let boundary = buffer.indexOf(delimiter, bufferStart)
|
|
93
|
-
while (boundary !== -1) {
|
|
94
|
-
yield buffer.slice(bufferStart, boundary)
|
|
95
|
-
bufferStart = boundary + delimiter.length
|
|
96
|
-
boundary = buffer.indexOf(delimiter, bufferStart)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} finally {
|
|
100
|
-
await reader.cancel().catch(() => undefined)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/*
|
|
105
|
-
SSE parser: yields the JSON-parsed `data` payload of each `event:`/`data:`
|
|
106
|
-
frame. The `sse()` respond helper emits an `event: error\ndata:
|
|
107
|
-
{"message":...}` frame when the source generator throws, which we surface
|
|
108
|
-
as a thrown Error so consumer loops can react to mid-stream failure
|
|
109
|
-
rather than silently stopping.
|
|
110
|
-
*/
|
|
111
|
-
async function* parseSse<T>(response: Response): AsyncGenerator<T> {
|
|
112
|
-
for await (const raw of frameReader(response, '\n\n')) {
|
|
113
|
-
const frame = parseFrame(raw)
|
|
114
|
-
if (!frame) {
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
if (frame.event === 'error') {
|
|
118
|
-
try {
|
|
119
|
-
const decoded = JSON.parse(frame.data) as { message?: string }
|
|
120
|
-
throw new Error(decoded?.message ?? 'sse stream error')
|
|
121
|
-
} catch (err) {
|
|
122
|
-
if (err instanceof SyntaxError) {
|
|
123
|
-
throw new Error(frame.data || 'sse stream error')
|
|
124
|
-
}
|
|
125
|
-
throw err
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
yield JSON.parse(frame.data) as T
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function parseFrame(raw: string): { event: string; data: string } | undefined {
|
|
133
|
-
const lines = raw.split('\n').filter((line) => line.length > 0 && !line.startsWith(':'))
|
|
134
|
-
if (lines.length === 0) {
|
|
135
|
-
return undefined
|
|
136
|
-
}
|
|
137
|
-
let event = 'message'
|
|
138
|
-
const dataLines: string[] = []
|
|
139
|
-
for (const line of lines) {
|
|
140
|
-
const colon = line.indexOf(':')
|
|
141
|
-
const field = colon === -1 ? line : line.slice(0, colon)
|
|
142
|
-
const value = colon === -1 ? '' : line.slice(colon + 1).replace(/^ /, '')
|
|
143
|
-
if (field === 'event') {
|
|
144
|
-
event = value
|
|
145
|
-
} else if (field === 'data') {
|
|
146
|
-
dataLines.push(value)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (dataLines.length === 0) {
|
|
150
|
-
return undefined
|
|
151
|
-
}
|
|
152
|
-
return { event, data: dataLines.join('\n') }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/*
|
|
156
|
-
JSONL/NDJSON parser: parses each non-empty line as JSON and yields the
|
|
157
|
-
value. The `jsonl()` respond helper emits a trailing
|
|
158
|
-
`{"$error":"<message>"}` line when the source generator throws — that's
|
|
159
|
-
surfaced here as a thrown Error so consumer loops can react to mid-stream
|
|
160
|
-
failure.
|
|
161
|
-
*/
|
|
162
|
-
async function* parseJsonLines<T>(response: Response): AsyncGenerator<T> {
|
|
163
|
-
for await (const raw of frameReader(response, '\n')) {
|
|
164
|
-
if (raw.length === 0) {
|
|
165
|
-
continue
|
|
166
|
-
}
|
|
167
|
-
const parsed = JSON.parse(raw) as Record<string, unknown> & { $error?: string }
|
|
168
|
-
if (parsed && typeof parsed === 'object' && typeof parsed.$error === 'string') {
|
|
169
|
-
throw new Error(parsed.$error)
|
|
170
|
-
}
|
|
171
|
-
yield parsed as T
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
4
|
/*
|
|
176
5
|
Builds the Subscribable returned by `fn.stream(args)`. The carried
|
|
177
6
|
`name` is the cache-style key for (method, url, args) so subscribe()
|
|
@@ -6,6 +6,11 @@ the function. `ttl`/`expiresAt` drive eviction: expiresAt = undefined means
|
|
|
6
6
|
soon as the promise settles). The stored promise resolves to the raw
|
|
7
7
|
Response so the snapshot can read its status/headers/body; the cache
|
|
8
8
|
layer hands callers a decoded view derived from this same promise.
|
|
9
|
+
|
|
10
|
+
`value` is set only for entries hydrated from the SSR snapshot: the
|
|
11
|
+
snapshot body is pre-decoded synchronously so the first client render can
|
|
12
|
+
read it without a microtask hop and byte-match the SSR DOM. Live fetches
|
|
13
|
+
leave it undefined and take the async decode path.
|
|
9
14
|
*/
|
|
10
15
|
export type CacheEntry = {
|
|
11
16
|
key: string
|
|
@@ -13,4 +18,5 @@ export type CacheEntry = {
|
|
|
13
18
|
request: Request
|
|
14
19
|
ttl: number | undefined
|
|
15
20
|
expiresAt: number | undefined
|
|
21
|
+
value?: unknown
|
|
16
22
|
}
|
|
Binary file
|
|
@@ -12,9 +12,11 @@ decoding) is inferred from the handler's return type via the
|
|
|
12
12
|
`TypedResponse<T>` brand on `json`/`error`/`redirect`/`jsonl`/`sse`, so
|
|
13
13
|
plain `GET(() => json({...}))` already types end-to-end.
|
|
14
14
|
|
|
15
|
-
For inbound validation pass a Standard Schema
|
|
16
|
-
second argument: `GET(fn, {
|
|
17
|
-
output type and the server replies with 422 on validation
|
|
15
|
+
For inbound validation pass a Standard Schema as `inputSchema` in the
|
|
16
|
+
second argument: `GET(fn, { inputSchema })`. Args then infers from the
|
|
17
|
+
schema's output type and the server replies with 422 on validation
|
|
18
|
+
failure. An optional `outputSchema` describes the success body for the
|
|
19
|
+
OpenAPI 200 response and the MCP tool output.
|
|
18
20
|
|
|
19
21
|
`json(...)` from `belte/server/json` is a thin wrapper over `Response.json`
|
|
20
22
|
that defaults `Cache-Control: no-store`, since intermediary caches shouldn't
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { afterAll, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { belteImportName } from './belteImportName.ts'
|
|
5
|
-
|
|
6
|
-
const roots: string[] = []
|
|
7
|
-
afterAll(() => roots.forEach((root) => rmSync(root, { recursive: true, force: true })))
|
|
8
|
-
|
|
9
|
-
// Writes a package.json into a fresh temp dir and returns the dir.
|
|
10
|
-
async function projectWith(packageJson: unknown): Promise<string> {
|
|
11
|
-
const root = mkdtempSync(`${tmpdir()}/belte-import-name-`)
|
|
12
|
-
roots.push(root)
|
|
13
|
-
await Bun.write(`${root}/package.json`, JSON.stringify(packageJson))
|
|
14
|
-
return root
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
test('uses the canonical name for a direct dependency', async () => {
|
|
18
|
-
const cwd = await projectWith({ dependencies: { '@briancray/belte': '^0.2.0' } })
|
|
19
|
-
expect(await belteImportName(cwd)).toBe('@briancray/belte')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('uses the `belte` alias key for an npm alias', async () => {
|
|
23
|
-
const cwd = await projectWith({ dependencies: { belte: 'npm:@briancray/belte@^0.2.0' } })
|
|
24
|
-
expect(await belteImportName(cwd)).toBe('belte')
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('uses the `belte` alias key for a workspace alias', async () => {
|
|
28
|
-
const cwd = await projectWith({ dependencies: { belte: 'workspace:@briancray/belte@*' } })
|
|
29
|
-
expect(await belteImportName(cwd)).toBe('belte')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('uses a non-`belte` alias key when that is how belte is declared', async () => {
|
|
33
|
-
const cwd = await projectWith({ dependencies: { framework: 'npm:@briancray/belte' } })
|
|
34
|
-
expect(await belteImportName(cwd)).toBe('framework')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('prefers the `belte` alias over a direct canonical dependency', async () => {
|
|
38
|
-
const cwd = await projectWith({
|
|
39
|
-
dependencies: { '@briancray/belte': '^0.2.0', belte: 'npm:@briancray/belte@^0.2.0' },
|
|
40
|
-
})
|
|
41
|
-
expect(await belteImportName(cwd)).toBe('belte')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('finds the alias in devDependencies', async () => {
|
|
45
|
-
const cwd = await projectWith({ devDependencies: { belte: 'npm:@briancray/belte@^0.2.0' } })
|
|
46
|
-
expect(await belteImportName(cwd)).toBe('belte')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('falls back to the canonical name when belte is absent', async () => {
|
|
50
|
-
const cwd = await projectWith({ dependencies: { svelte: '^5.0.0' } })
|
|
51
|
-
expect(await belteImportName(cwd)).toBe('@briancray/belte')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('falls back to the canonical name when package.json is missing', async () => {
|
|
55
|
-
const root = mkdtempSync(`${tmpdir()}/belte-import-name-`)
|
|
56
|
-
roots.push(root)
|
|
57
|
-
expect(await belteImportName(root)).toBe('@briancray/belte')
|
|
58
|
-
})
|