@abide/abide 0.101.0 → 0.103.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/bin/abide.ts +226 -164
- package/docs/REGISTRY.md +730 -0
- package/docs/RULEBOOK.md +2768 -0
- package/docs/abide.md +13127 -0
- package/package.json +6 -3
- package/scaffold/AGENTS.md +38 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +341 -71
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +53 -8
- package/src/compiler/checkedShadow.ts +73 -6
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +598 -200
- package/src/compiler/compiledEntry.ts +48 -40
- package/src/compiler/componentGraph.ts +57 -71
- package/src/compiler/declaredExports.ts +26 -9
- package/src/compiler/emitClient.ts +142 -72
- package/src/compiler/emitComponent.ts +41 -12
- package/src/compiler/emitSource.ts +102 -13
- package/src/compiler/handlersUnder.ts +164 -76
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +150 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +250 -19
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +62 -8
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +37 -15
- package/src/compiler/serveLanguage.ts +101 -34
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +29 -2
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/MODULE_QUERIES.ts +27 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +457 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +241 -107
- package/src/server/answerRpc.ts +107 -33
- package/src/server/answerSocket.ts +65 -20
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +238 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +38 -37
- package/src/server/componentModules.d.ts +2 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +77 -79
- package/src/server/devWorker.ts +118 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +57 -32
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +137 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +12 -7
- package/src/server/installSourceMaps.ts +93 -0
- package/src/server/launcherOptions.ts +33 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +6 -6
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/preload.ts +13 -0
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +48 -28
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +79 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +20 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +10 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +2 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/{ui → shared}/menuBridge.ts +2 -2
- package/src/shared/menuItem.ts +4 -4
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/{ui → shared}/reportMenu.ts +8 -10
- package/src/shared/routePattern.ts +122 -48
- package/src/shared/rpc.ts +37 -8
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +37 -36
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
package/src/server/answerRpc.ts
CHANGED
|
@@ -30,7 +30,12 @@ import { followAsEvents } from './answerSocket.ts'
|
|
|
30
30
|
import { answerValue, namesEtag, sseHeaders } from './answerValue.ts'
|
|
31
31
|
import { argsFor } from './argsFor.ts'
|
|
32
32
|
import { callerOf } from './callerOf.ts'
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
argsDocumentOf,
|
|
35
|
+
coerceArgs,
|
|
36
|
+
collapsed,
|
|
37
|
+
type Document,
|
|
38
|
+
} from './coerceArgs.ts'
|
|
34
39
|
import { positiveSetting } from './config.ts'
|
|
35
40
|
import {
|
|
36
41
|
allows,
|
|
@@ -38,7 +43,7 @@ import {
|
|
|
38
43
|
crossingOrigin,
|
|
39
44
|
crossingRefusal,
|
|
40
45
|
} from './crossingOrigin.ts'
|
|
41
|
-
import { failureOf } from './hooks.ts'
|
|
46
|
+
import { type AppRung, failureOf, rungsOver } from './hooks.ts'
|
|
42
47
|
import { prefers } from './prefers.ts'
|
|
43
48
|
import {
|
|
44
49
|
currentRequestScope,
|
|
@@ -51,7 +56,8 @@ import { type Outcome, outcomeOf, takeSeed } from './seedBuffer.ts'
|
|
|
51
56
|
import { versionOf } from './versionOf.ts'
|
|
52
57
|
|
|
53
58
|
/**
|
|
54
|
-
* One row of the table the build writes
|
|
59
|
+
* One row of the handler table the build writes into the server's table (`SERVER_TABLE`): where a
|
|
60
|
+
* handler answers, and its module.
|
|
55
61
|
*
|
|
56
62
|
* The module is relative to the app's root, which both commands that serve already know.
|
|
57
63
|
*/
|
|
@@ -59,10 +65,17 @@ export type Mount = {
|
|
|
59
65
|
address: string
|
|
60
66
|
method: Method
|
|
61
67
|
source: string
|
|
68
|
+
/** The handler file's name, or the private handler's binding. */
|
|
62
69
|
name: string
|
|
70
|
+
/** What the source's module exports it as: `default` for a handler file. */
|
|
71
|
+
export: string
|
|
72
|
+
/** The route its hooks are filed at, which its rungs are read over (37.1, 16.72). */
|
|
73
|
+
scope: string
|
|
74
|
+
/** Whether it is a private handler, listed nowhere (16.71). */
|
|
75
|
+
private: boolean
|
|
63
76
|
/** Whether a browser is handed a wrapper for it. 20.4. */
|
|
64
77
|
ui: boolean
|
|
65
|
-
/** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.
|
|
78
|
+
/** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.62. */
|
|
66
79
|
cli: boolean
|
|
67
80
|
/**
|
|
68
81
|
* Whether MCP lists it where its runtime `clients` does not say.
|
|
@@ -88,9 +101,20 @@ export type Mount = {
|
|
|
88
101
|
args: JsonValue | undefined
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
/**
|
|
105
|
+
* A row of the table the build wrote, with the import of the module its handler is declared in —
|
|
106
|
+
* the handler file itself, or a private handler's `.abide` server module, the one the route table
|
|
107
|
+
* loads too, so a page and a call reach one handler.
|
|
108
|
+
*/
|
|
109
|
+
export type MountRow = Mount & { load: () => Promise<Record<string, unknown>> }
|
|
110
|
+
|
|
91
111
|
/** A handler's row, mounted: the handler imported and its args schema probed. */
|
|
92
112
|
export type Mounted = Mount & {
|
|
93
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The rungs filed over `scope`, read once at mount (16.14) — what an http call and a socket
|
|
115
|
+
* subscription to it both pass through (37.2, 18.26).
|
|
116
|
+
*/
|
|
117
|
+
rungs: AppRung[]
|
|
94
118
|
handler: Rpc<unknown, unknown> & Gated<unknown>
|
|
95
119
|
/**
|
|
96
120
|
* The args' JSON Schema, probed once at mount.
|
|
@@ -127,8 +151,14 @@ export function mountedOwn(
|
|
|
127
151
|
{
|
|
128
152
|
address,
|
|
129
153
|
method: 'GET',
|
|
154
|
+
// The root's rungs run before routing (37.3), and nothing is filed below a route
|
|
155
|
+
// of abide's own.
|
|
156
|
+
rungs: [],
|
|
130
157
|
source: '',
|
|
131
158
|
name,
|
|
159
|
+
export: name,
|
|
160
|
+
scope: '/',
|
|
161
|
+
private: false,
|
|
132
162
|
ui: false,
|
|
133
163
|
cli: false,
|
|
134
164
|
mcp: false,
|
|
@@ -149,32 +179,31 @@ export function environmentTimeout(): number | undefined {
|
|
|
149
179
|
}
|
|
150
180
|
|
|
151
181
|
/**
|
|
152
|
-
* The table's handlers, imported and keyed by address.
|
|
182
|
+
* The table's handlers, imported and keyed by method and address.
|
|
153
183
|
*
|
|
154
|
-
* Imported
|
|
155
|
-
* one module and so one handler. AT BOOT rather than on the
|
|
156
|
-
* reaches a handler through the page's own import and has to
|
|
157
|
-
* called it over the wire.
|
|
184
|
+
* Imported through each row's own loader, which the route table shares, so a page rendering on the
|
|
185
|
+
* server and a call over the wire reach one module and so one handler. AT BOOT rather than on the
|
|
186
|
+
* first call, because a page's render reaches a handler through the page's own import and has to
|
|
187
|
+
* know its address before anything has called it over the wire.
|
|
158
188
|
*/
|
|
159
189
|
export async function mountHandlers(
|
|
160
|
-
table:
|
|
161
|
-
moduleOf: (source: string) => Promise<Record<string, unknown>>,
|
|
190
|
+
table: MountRow[],
|
|
162
191
|
): Promise<{ handlers: Map<string, Mounted> }> {
|
|
163
|
-
const modules = await Promise.all(
|
|
164
|
-
table.map((mount) => moduleOf(mount.source)),
|
|
165
|
-
)
|
|
192
|
+
const modules = await Promise.all(table.map((mount) => mount.load()))
|
|
166
193
|
const handlers = new Map<string, Mounted>()
|
|
167
194
|
for (let at = 0; at < table.length; at += 1) {
|
|
168
195
|
const mount = table[at] as Mount
|
|
169
|
-
const exported = (modules[at] as Record<string, unknown>)[mount.
|
|
196
|
+
const exported = (modules[at] as Record<string, unknown>)[mount.export]
|
|
170
197
|
const handler = exported as Rpc<unknown, unknown> & Gated<unknown>
|
|
171
198
|
const declared = argsDocumentOf(handler)
|
|
172
199
|
// 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
|
|
173
200
|
// page's render turns a call it made into the browser's request by (41.20).
|
|
174
201
|
handler.address = underMount(mount.address)
|
|
175
|
-
handlers.set(mount.address, {
|
|
202
|
+
handlers.set(handlerKey(mount.method, mount.address), {
|
|
176
203
|
...mount,
|
|
177
|
-
|
|
204
|
+
// 37.1, 16.72 — the route its file's hooks are filed at, which the build read off
|
|
205
|
+
// where the file answers (23.19's mount for a package's), never its place on disk.
|
|
206
|
+
rungs: rungsOver(mount.scope),
|
|
178
207
|
handler,
|
|
179
208
|
argsDocument: declared ?? (mount.args as Document | undefined),
|
|
180
209
|
wireArgs: declared === undefined ? mount.args : undefined,
|
|
@@ -183,30 +212,34 @@ export async function mountHandlers(
|
|
|
183
212
|
return { handlers }
|
|
184
213
|
}
|
|
185
214
|
|
|
186
|
-
/**
|
|
215
|
+
/** What a handler is keyed by among the mounted: its method and its route. 19.1. */
|
|
216
|
+
export function handlerKey(method: string, address: string): string {
|
|
217
|
+
return `${method} ${address}`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The `Response` to a call of `found` made over http, `params` being what its route matched.
|
|
222
|
+
*
|
|
223
|
+
* `found` answers the request's method — or, for an `OPTIONS`, the method its preflight asks
|
|
224
|
+
* about: which handler a request reaches is the route table's to say (19.20).
|
|
225
|
+
*/
|
|
187
226
|
export async function answerRpc(
|
|
188
|
-
|
|
227
|
+
found: Mounted,
|
|
228
|
+
params: Record<string, string | string[]>,
|
|
189
229
|
url: URL,
|
|
190
230
|
request: Request,
|
|
191
231
|
// 16.38 — what paints a refusal for a request that came as a navigation.
|
|
192
232
|
pages: ErrorPages,
|
|
193
233
|
): Promise<Response> {
|
|
194
234
|
const pathname = url.pathname
|
|
195
|
-
const found = handlers.get(pathname)
|
|
196
235
|
const origin = request.headers.get('origin')
|
|
197
236
|
// 16.24 — THE PREFLIGHT, answered by abide where the handler declared `crossOrigin`.
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
allows(found.handler.crossOrigin, origin)
|
|
203
|
-
)
|
|
204
|
-
return preflight(found, origin, request)
|
|
237
|
+
if (request.method === 'OPTIONS')
|
|
238
|
+
return origin !== null && allows(found.handler.crossOrigin, origin)
|
|
239
|
+
? preflight(found, origin, request)
|
|
240
|
+
: answered(notFound({ path: pathname, method: request.method }))
|
|
205
241
|
// 16.50 — a `HEAD` is the `GET`'s answer, which the server sends without its body.
|
|
206
242
|
const head = request.method === 'HEAD'
|
|
207
|
-
// 15.7 — no handler at this address AND this method is one refusal, not two.
|
|
208
|
-
if (found === undefined || found.method !== (head ? 'GET' : request.method))
|
|
209
|
-
return answered(notFound({ path: pathname, method: request.method }))
|
|
210
243
|
const handler = found.handler
|
|
211
244
|
const inUrl = argsInURL(found.method)
|
|
212
245
|
const form = isForm(request)
|
|
@@ -226,10 +259,12 @@ export async function answerRpc(
|
|
|
226
259
|
return answered(overCeiling(ceiling))
|
|
227
260
|
const raw = await argsOf(request, url, inUrl, form, ceiling)
|
|
228
261
|
if (isFailed(raw)) return answered(raw)
|
|
262
|
+
const sent = withParameters(raw, params, inUrl || form, found)
|
|
263
|
+
if (isFailed(sent)) return answered(sent)
|
|
229
264
|
let args: unknown
|
|
230
265
|
try {
|
|
231
266
|
// A URL's and a form's args are text, and the query tells a bare name from `name=` (19.7).
|
|
232
|
-
args = argsFor(found,
|
|
267
|
+
args = argsFor(found, sent, inUrl ? url.search : form ? '' : undefined)
|
|
233
268
|
} catch (refused) {
|
|
234
269
|
if (isFailed(refused)) return answered(refused)
|
|
235
270
|
throw refused
|
|
@@ -436,6 +471,39 @@ async function argsOf(
|
|
|
436
471
|
return parsed
|
|
437
472
|
}
|
|
438
473
|
|
|
474
|
+
/**
|
|
475
|
+
* The args a request sent, with the parameters its route matched laid in.
|
|
476
|
+
*
|
|
477
|
+
* 16.65 — the path's, whatever the method. 16.66 — a parameter the query or the body names as
|
|
478
|
+
* well is refused rather than one of the two chosen. A JSON body's members are typed already, so the
|
|
479
|
+
* path's text is coerced to the declared types before it joins them (19.7); text args are coerced
|
|
480
|
+
* with the rest of the text.
|
|
481
|
+
*/
|
|
482
|
+
function withParameters(
|
|
483
|
+
sent: unknown,
|
|
484
|
+
params: Record<string, string | string[]>,
|
|
485
|
+
text: boolean,
|
|
486
|
+
found: Mounted,
|
|
487
|
+
): unknown | Failed<'HttpError', undefined> {
|
|
488
|
+
let none = true
|
|
489
|
+
for (const name in params) {
|
|
490
|
+
none = false
|
|
491
|
+
if (isRecord(sent) && name in sent)
|
|
492
|
+
return refuse(
|
|
493
|
+
400,
|
|
494
|
+
`The parameter '${name}' is in the path and in the request as well.`,
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
if (none) return sent
|
|
498
|
+
const fromPath = text
|
|
499
|
+
? params
|
|
500
|
+
: (coerceArgs(params, found.argsDocument, '') as Record<
|
|
501
|
+
string,
|
|
502
|
+
unknown
|
|
503
|
+
>)
|
|
504
|
+
return isRecord(sent) ? { ...sent, ...fromPath } : { ...fromPath }
|
|
505
|
+
}
|
|
506
|
+
|
|
439
507
|
/**
|
|
440
508
|
* The request's body read up to `ceiling`, as a `Response`, or a 413 refusal past it.
|
|
441
509
|
*
|
|
@@ -487,6 +555,12 @@ function preflight(found: Mounted, origin: string, request: Request): Response {
|
|
|
487
555
|
* 16.57 — `Infinity` is never reaped, which bun spells 0; any finite bound is capped at the 255
|
|
488
556
|
* seconds bun's idle timeout can hold.
|
|
489
557
|
*
|
|
558
|
+
* FOUR SECONDS PAST THE TIMEOUT, NOT AT IT: a load that goes its timeout is refused at that moment,
|
|
559
|
+
* and a stream's refusal is a frame written then (16.68). bun's sockets check an idle timeout on a
|
|
560
|
+
* tick four seconds wide, so an idle timeout of the same second — or one past it — fired anywhere in
|
|
561
|
+
* the tick before it and reaped the connection ahead of the frame, one stream in two under `abide
|
|
562
|
+
* dev`. Four is the tick, so the refusal always goes out first.
|
|
563
|
+
*
|
|
490
564
|
* ON THE REQUEST THE LISTENER RECEIVED, never the one being answered: under a mount, or past a
|
|
491
565
|
* rung that handed `next` its own, that is a copy, and bun's `timeout` does nothing for a request
|
|
492
566
|
* it was not handed itself.
|
|
@@ -496,7 +570,7 @@ export function raiseTimeout(milliseconds: number): void {
|
|
|
496
570
|
const received = scope?.received
|
|
497
571
|
if (scope === undefined || received === undefined) return
|
|
498
572
|
const seconds = Number.isFinite(milliseconds)
|
|
499
|
-
? Math.min(255, Math.ceil(milliseconds / 1000))
|
|
573
|
+
? Math.min(255, Math.ceil(milliseconds / 1000) + 4)
|
|
500
574
|
: Number.POSITIVE_INFINITY
|
|
501
575
|
if (seconds <= scope.timeout) return
|
|
502
576
|
scope.timeout = seconds
|
|
@@ -10,18 +10,20 @@
|
|
|
10
10
|
// built per request, since a trigger names args rather than a value. A `global` value is also fed its productions, a guarded one only to a
|
|
11
11
|
// subscription its rungs admitted when it opened (18.8) — they run once, in the connection's request.
|
|
12
12
|
|
|
13
|
-
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
14
13
|
import { type Problem, problemOf } from '#shared/Failed.ts'
|
|
15
14
|
import { GLOBAL_GETS } from '#shared/GLOBAL_GETS.ts'
|
|
16
15
|
import { namedLogger } from '#shared/log.ts'
|
|
16
|
+
import { compose } from '#shared/Middleware.ts'
|
|
17
|
+
import { unmounted } from '#shared/mount.ts'
|
|
18
|
+
import type { BrowserFrame, ServerFrame } from '#shared/muxFrames.ts'
|
|
17
19
|
import { FOLLOWED } from '#shared/reactive/FOLLOWED.ts'
|
|
18
20
|
import { servedCellOf } from '#shared/reactive/index.ts'
|
|
19
21
|
import { matchesPattern } from '#shared/reactive/matchesPattern.ts'
|
|
20
22
|
import { PATCH_SINKS } from '#shared/reactive/PATCH_SINKS.ts'
|
|
21
23
|
import { unowned } from '#shared/reactive/watch.ts'
|
|
22
24
|
import { refuse } from '#shared/refuse.ts'
|
|
23
|
-
import
|
|
24
|
-
import type
|
|
25
|
+
import { rpcRequest } from '#shared/rpcRequest.ts'
|
|
26
|
+
import { handlerKey, type Mounted } from './answerRpc.ts'
|
|
25
27
|
import { argsFor } from './argsFor.ts'
|
|
26
28
|
import { closedTo, crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
|
|
27
29
|
import { failureOf } from './hooks.ts'
|
|
@@ -38,7 +40,7 @@ import { patchesSince, versionOf } from './versionOf.ts'
|
|
|
38
40
|
* The far end of one connection to the mux, which its frames are written to.
|
|
39
41
|
*
|
|
40
42
|
* Bun's socket under `abide start`, and under `abide dev` a stand-in that posts each frame to the
|
|
41
|
-
* listener holding the real one (38.
|
|
43
|
+
* listener holding the real one (38.14).
|
|
42
44
|
*/
|
|
43
45
|
export type Peer = {
|
|
44
46
|
send(text: string): unknown
|
|
@@ -141,10 +143,10 @@ export function feedFromServed(
|
|
|
141
143
|
* Only where that entry is not the one it is fed from already.
|
|
142
144
|
*/
|
|
143
145
|
function attach(rpc: Mounted['handler'], follower: Follower): unknown {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
// 18.8, 18.26 — what its own rungs or its route's refused is sent its triggers alone.
|
|
147
|
+
const serving = !follower.admitted
|
|
148
|
+
? undefined
|
|
149
|
+
: GLOBAL_GETS.get(rpc)?.at(follower.key)
|
|
148
150
|
if (serving === follower.serving) return serving
|
|
149
151
|
follower.stopFeed?.()
|
|
150
152
|
// A version is one entry's count, so what it was sent of another says nothing about this one.
|
|
@@ -250,8 +252,8 @@ export class MuxConnection {
|
|
|
250
252
|
return
|
|
251
253
|
}
|
|
252
254
|
const id = frame.id ?? 0
|
|
253
|
-
// THE ADDRESS
|
|
254
|
-
//
|
|
255
|
+
// THE ADDRESS BELOW THE MOUNT, which is what the handlers are keyed by: a browser names it
|
|
256
|
+
// as its wrapper resolved it, which is a whole URL, or a path under the app's mount.
|
|
255
257
|
const address = addressIn(frame.address ?? '')
|
|
256
258
|
const subscriptions = this.subscriptions
|
|
257
259
|
if (frame.op === 'unsub') {
|
|
@@ -260,7 +262,9 @@ export class MuxConnection {
|
|
|
260
262
|
return
|
|
261
263
|
}
|
|
262
264
|
const handler =
|
|
263
|
-
frame.op === 'sub'
|
|
265
|
+
frame.op === 'sub'
|
|
266
|
+
? this.mux.handlers.get(handlerKey('GET', address))
|
|
267
|
+
: undefined
|
|
264
268
|
const followed = handler?.ui ? handler : undefined
|
|
265
269
|
if (followed === undefined) {
|
|
266
270
|
send(this.peer, {
|
|
@@ -295,16 +299,24 @@ export class MuxConnection {
|
|
|
295
299
|
// 18.8 — A GUARDED `GET`'s RUNGS RUN ONCE, as the subscription opens, in this
|
|
296
300
|
// connection's request: one they admit is fed the value, and one they refuse is sent its
|
|
297
301
|
// triggers alone.
|
|
298
|
-
|
|
302
|
+
// 18.26 — AND THE MIDDLEWARE ITS FILE'S ROUTE FILED, over the request a `GET` of it
|
|
303
|
+
// would be: one a rung answers in the handler's place is not fed, as an http call it
|
|
304
|
+
// answered would never reach the handler.
|
|
305
|
+
const routed = followed.rungs.length > 0
|
|
306
|
+
let admitted = !followed.handler.guarded && !routed
|
|
299
307
|
if (!admitted) {
|
|
300
308
|
subscriptions.get(id)?.()
|
|
301
309
|
subscriptions.set(id, pending)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
// No route rungs, no turn: a guarded `GET`'s rungs start in this frame's own run.
|
|
311
|
+
admitted =
|
|
312
|
+
!routed || (await routeAdmits(this.scope, followed, args))
|
|
313
|
+
if (admitted && followed.handler.guarded)
|
|
314
|
+
admitted = await Promise.resolve(
|
|
315
|
+
followed.handler(args as never) as PromiseLike<unknown>,
|
|
316
|
+
).then(
|
|
317
|
+
() => true,
|
|
318
|
+
() => false,
|
|
319
|
+
)
|
|
308
320
|
if (subscriptions.get(id) !== pending) return
|
|
309
321
|
}
|
|
310
322
|
follow(this, id, followed, args, frame.version, admitted)
|
|
@@ -315,6 +327,39 @@ export class MuxConnection {
|
|
|
315
327
|
}
|
|
316
328
|
}
|
|
317
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Whether the rungs over `mounted`'s route let a `GET` of it with `args` through, asked over the
|
|
332
|
+
* upgrade's own request at the handler's address. 18.26.
|
|
333
|
+
*/
|
|
334
|
+
async function routeAdmits(
|
|
335
|
+
scope: RequestScope,
|
|
336
|
+
mounted: Mounted,
|
|
337
|
+
args: unknown,
|
|
338
|
+
): Promise<boolean> {
|
|
339
|
+
const upgrade = scope.request as Request
|
|
340
|
+
const asked = rpcRequest(
|
|
341
|
+
'GET',
|
|
342
|
+
mounted.address,
|
|
343
|
+
args as Record<string, unknown> | undefined,
|
|
344
|
+
)
|
|
345
|
+
const request = new Request(new URL(asked.url, upgrade.url), {
|
|
346
|
+
headers: upgrade.headers,
|
|
347
|
+
signal: upgrade.signal,
|
|
348
|
+
})
|
|
349
|
+
// REACHED BY A FLAG, not by the answer's identity: a rung that wraps or re-heads what `next`
|
|
350
|
+
// answered still let the call through, and a shared answer would carry every rung's headers.
|
|
351
|
+
let reached = false
|
|
352
|
+
try {
|
|
353
|
+
const answered = await compose(mounted.rungs, async () => {
|
|
354
|
+
reached = true
|
|
355
|
+
return new Response(null, { status: 204 })
|
|
356
|
+
})({ request })
|
|
357
|
+
return reached && answered.ok
|
|
358
|
+
} catch {
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
318
363
|
/**
|
|
319
364
|
* Follows a handler for `id`: its triggers always, its value where `global` and unguarded.
|
|
320
365
|
*
|
|
@@ -564,8 +609,8 @@ function sendAll(serving: unknown, fed: Iterable<Follower>): void {
|
|
|
564
609
|
}
|
|
565
610
|
|
|
566
611
|
function addressIn(named: string): string {
|
|
567
|
-
const
|
|
568
|
-
return
|
|
612
|
+
const path = URL.parse(named)?.pathname ?? named
|
|
613
|
+
return unmounted(path) ?? path
|
|
569
614
|
}
|
|
570
615
|
|
|
571
616
|
/**
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// (16.35), a document where `page` named one (17.2), a stream — a streaming handler's chunks, or
|
|
4
4
|
// `Values` a handler returned — framed one chunk per pull (16.32, 16.44–16.47).
|
|
5
5
|
|
|
6
|
+
import { isFailed, problemOf } from '#shared/Failed.ts'
|
|
7
|
+
import { refusalFrame } from '#shared/framing.ts'
|
|
6
8
|
import { compressAnswer } from './compressAnswer.ts'
|
|
7
9
|
import { prefers } from './prefers.ts'
|
|
8
10
|
import {
|
|
@@ -158,7 +160,7 @@ export function linesType(request: Request): string {
|
|
|
158
160
|
: 'application/jsonl'
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
/** A stream answered at a framing already decided — a handler's
|
|
163
|
+
/** A stream answered at a framing already decided — a handler's. */
|
|
162
164
|
export function framed(
|
|
163
165
|
chunks: AsyncIterator<unknown>,
|
|
164
166
|
framing: Framing,
|
|
@@ -185,7 +187,7 @@ export function framed(
|
|
|
185
187
|
* was reading in its scope.
|
|
186
188
|
*
|
|
187
189
|
* AN EVENT STREAM OPENS ON A COMMENT: a streamed answer's head goes out with its first byte, so a
|
|
188
|
-
* stream with nothing yet to say
|
|
190
|
+
* stream with nothing yet to say held its reader's headers until it did.
|
|
189
191
|
*
|
|
190
192
|
* `first` is a result already pulled — the one a framing was decided from — and is sent ahead of
|
|
191
193
|
* the rest.
|
|
@@ -230,7 +232,20 @@ export function bodyOf(
|
|
|
230
232
|
next = await chunks.next()
|
|
231
233
|
} catch (thrown) {
|
|
232
234
|
if (ended) return
|
|
233
|
-
|
|
235
|
+
// 16.68 — A REFUSAL AFTER THE STATUS WAS SENT is the stream's last frame, so the
|
|
236
|
+
// reader refuses with it rather than reading a dropped connection. Bytes carry no
|
|
237
|
+
// framing to hold one, and anything else is not a refusal to send.
|
|
238
|
+
if (framing !== 'bytes' && isFailed(thrown)) {
|
|
239
|
+
controller.enqueue(
|
|
240
|
+
ENCODER.encode(
|
|
241
|
+
refusalFrame(
|
|
242
|
+
problemOf(thrown),
|
|
243
|
+
framing === 'sse' ? 'data: ' : '',
|
|
244
|
+
),
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
controller.close()
|
|
248
|
+
} else controller.error(thrown)
|
|
234
249
|
release()
|
|
235
250
|
return
|
|
236
251
|
}
|
|
@@ -314,12 +329,10 @@ export function answerBlobTagged(
|
|
|
314
329
|
})
|
|
315
330
|
}
|
|
316
331
|
}
|
|
317
|
-
// A RANGE DECLINED — `If-Range` naming another version, or one this does not parse
|
|
318
|
-
// ranges a file-backed
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
range === null ? blob : blob.stream().pipeThrough(new TransformStream())
|
|
322
|
-
return new Response(body, { status, headers })
|
|
332
|
+
// A RANGE DECLINED — `If-Range` naming another version, or one this does not parse — goes out
|
|
333
|
+
// whole as the blob itself: Bun.serve ranges a file-backed body on its own, but declines exactly
|
|
334
|
+
// these, so a file still leaves from disk.
|
|
335
|
+
return new Response(blob, { status, headers })
|
|
323
336
|
}
|
|
324
337
|
|
|
325
338
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The directories an app's own files stand in: `root`, and the one above it.
|
|
3
3
|
*
|
|
4
|
-
* `root` is the
|
|
4
|
+
* `root` is the app's root — `src/` — and the one above it is the package beside
|
|
5
5
|
* it. The app's `package.json` and `bunfig.toml` are found in the first that has one.
|
|
6
6
|
*/
|
|
7
7
|
export function appDirectories(root: string): string[] {
|
package/src/server/appPlugins.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { appDirectories } from './appDirectories.ts'
|
|
|
3
3
|
/**
|
|
4
4
|
* The app's own bundler plugins, loaded from `bunfig.toml`'s `[serve.static] plugins`.
|
|
5
5
|
*
|
|
6
|
-
* 38.
|
|
6
|
+
* 38.43 — declared where bun already has an app declare them, the key `bun-plugin-tailwind`
|
|
7
7
|
* documents. `Bun.build` reads no bunfig, so both client builds read this one — in
|
|
8
8
|
* `appDirectories`, as `config` finds the app's `package.json`.
|
|
9
9
|
*/
|