@abide/abide 0.101.0 → 0.102.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 +16 -0
- package/bin/abide.ts +223 -164
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +5 -3
- package/scaffold/AGENTS.md +31 -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 +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -59
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +102 -30
- 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 +179 -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 +37 -37
- package/src/server/componentModules.d.ts +3 -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 +59 -73
- package/src/server/devWorker.ts +114 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -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 +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- 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 +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +73 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -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/RESERVED_DIRECTORIES.ts +15 -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 +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- 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/shared/routePattern.ts +82 -46
- 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 +38 -31
- 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/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.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 → page.abide} +0 -0
package/src/server/answerRpc.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { Gated } from '#shared/reactive/argsContract.ts'
|
|
|
16
16
|
import { deadlineOf, heldGenerationOf } from '#shared/reactive/index.ts'
|
|
17
17
|
import type { Defaults } from '#shared/reactive/parameterDefaults.ts'
|
|
18
18
|
import { notFound, refuse } from '#shared/refuse.ts'
|
|
19
|
+
import { routeOf } from '#shared/routePattern.ts'
|
|
19
20
|
import type { Method, Rpc } from '#shared/rpc.ts'
|
|
20
21
|
import { argsInURL } from '#shared/rpcRequest.ts'
|
|
21
22
|
import { VERSION_HEADER } from '#shared/VERSION_HEADER.ts'
|
|
@@ -30,7 +31,12 @@ import { followAsEvents } from './answerSocket.ts'
|
|
|
30
31
|
import { answerValue, namesEtag, sseHeaders } from './answerValue.ts'
|
|
31
32
|
import { argsFor } from './argsFor.ts'
|
|
32
33
|
import { callerOf } from './callerOf.ts'
|
|
33
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
argsDocumentOf,
|
|
36
|
+
coerceArgs,
|
|
37
|
+
collapsed,
|
|
38
|
+
type Document,
|
|
39
|
+
} from './coerceArgs.ts'
|
|
34
40
|
import { positiveSetting } from './config.ts'
|
|
35
41
|
import {
|
|
36
42
|
allows,
|
|
@@ -38,7 +44,7 @@ import {
|
|
|
38
44
|
crossingOrigin,
|
|
39
45
|
crossingRefusal,
|
|
40
46
|
} from './crossingOrigin.ts'
|
|
41
|
-
import { failureOf } from './hooks.ts'
|
|
47
|
+
import { type AppRung, failureOf, rungsOver } from './hooks.ts'
|
|
42
48
|
import { prefers } from './prefers.ts'
|
|
43
49
|
import {
|
|
44
50
|
currentRequestScope,
|
|
@@ -51,7 +57,8 @@ import { type Outcome, outcomeOf, takeSeed } from './seedBuffer.ts'
|
|
|
51
57
|
import { versionOf } from './versionOf.ts'
|
|
52
58
|
|
|
53
59
|
/**
|
|
54
|
-
* One row of the table the build writes
|
|
60
|
+
* One row of the handler table the build writes into the server's table (`SERVER_TABLE`): where a
|
|
61
|
+
* handler answers, and its module.
|
|
55
62
|
*
|
|
56
63
|
* The module is relative to the app's root, which both commands that serve already know.
|
|
57
64
|
*/
|
|
@@ -62,7 +69,7 @@ export type Mount = {
|
|
|
62
69
|
name: string
|
|
63
70
|
/** Whether a browser is handed a wrapper for it. 20.4. */
|
|
64
71
|
ui: boolean
|
|
65
|
-
/** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.
|
|
72
|
+
/** Whether the command line lists it — the shell's listing and completion, never whether `call` reaches it. 20.3, 38.62. */
|
|
66
73
|
cli: boolean
|
|
67
74
|
/**
|
|
68
75
|
* Whether MCP lists it where its runtime `clients` does not say.
|
|
@@ -88,9 +95,23 @@ export type Mount = {
|
|
|
88
95
|
args: JsonValue | undefined
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
/**
|
|
99
|
+
* A row of the table the build wrote, with the import of the module its handler is declared in —
|
|
100
|
+
* the file's server module, the one the route table loads too, so a page and a call reach one
|
|
101
|
+
* handler.
|
|
102
|
+
*/
|
|
103
|
+
export type MountRow = Mount & { load: () => Promise<Record<string, unknown>> }
|
|
104
|
+
|
|
91
105
|
/** A handler's row, mounted: the handler imported and its args schema probed. */
|
|
92
106
|
export type Mounted = Mount & {
|
|
93
107
|
method: Method
|
|
108
|
+
/** The route of the file declaring it, which its hooks are filed at (37.1, 37.2). */
|
|
109
|
+
route: string
|
|
110
|
+
/**
|
|
111
|
+
* The rungs filed over `route`, read once at mount (16.14) — what an http call and a socket
|
|
112
|
+
* subscription to it both pass through (37.2, 18.26).
|
|
113
|
+
*/
|
|
114
|
+
rungs: AppRung[]
|
|
94
115
|
handler: Rpc<unknown, unknown> & Gated<unknown>
|
|
95
116
|
/**
|
|
96
117
|
* The args' JSON Schema, probed once at mount.
|
|
@@ -127,6 +148,10 @@ export function mountedOwn(
|
|
|
127
148
|
{
|
|
128
149
|
address,
|
|
129
150
|
method: 'GET',
|
|
151
|
+
route: '/',
|
|
152
|
+
// The root's rungs run before routing (37.3), and nothing is filed below a route
|
|
153
|
+
// of abide's own.
|
|
154
|
+
rungs: [],
|
|
130
155
|
source: '',
|
|
131
156
|
name,
|
|
132
157
|
ui: false,
|
|
@@ -149,20 +174,17 @@ export function environmentTimeout(): number | undefined {
|
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
/**
|
|
152
|
-
* The table's handlers, imported and keyed by address.
|
|
177
|
+
* The table's handlers, imported and keyed by method and address.
|
|
153
178
|
*
|
|
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.
|
|
179
|
+
* Imported through each row's own loader, which the route table shares, so a page rendering on the
|
|
180
|
+
* server and a call over the wire reach one module and so one handler. AT BOOT rather than on the
|
|
181
|
+
* first call, because a page's render reaches a handler through the page's own import and has to
|
|
182
|
+
* know its address before anything has called it over the wire.
|
|
158
183
|
*/
|
|
159
184
|
export async function mountHandlers(
|
|
160
|
-
table:
|
|
161
|
-
moduleOf: (source: string) => Promise<Record<string, unknown>>,
|
|
185
|
+
table: MountRow[],
|
|
162
186
|
): Promise<{ handlers: Map<string, Mounted> }> {
|
|
163
|
-
const modules = await Promise.all(
|
|
164
|
-
table.map((mount) => moduleOf(mount.source)),
|
|
165
|
-
)
|
|
187
|
+
const modules = await Promise.all(table.map((mount) => mount.load()))
|
|
166
188
|
const handlers = new Map<string, Mounted>()
|
|
167
189
|
for (let at = 0; at < table.length; at += 1) {
|
|
168
190
|
const mount = table[at] as Mount
|
|
@@ -172,9 +194,14 @@ export async function mountHandlers(
|
|
|
172
194
|
// 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
|
|
173
195
|
// page's render turns a call it made into the browser's request by (41.20).
|
|
174
196
|
handler.address = underMount(mount.address)
|
|
175
|
-
|
|
197
|
+
// 23.19 — the route its address answers below, which is its mount's for a package's file,
|
|
198
|
+
// never the file's place on disk.
|
|
199
|
+
const route = routeOf(mount.address)
|
|
200
|
+
handlers.set(handlerKey(mount.method, mount.address), {
|
|
176
201
|
...mount,
|
|
177
202
|
method: mount.method,
|
|
203
|
+
route,
|
|
204
|
+
rungs: rungsOver(route),
|
|
178
205
|
handler,
|
|
179
206
|
argsDocument: declared ?? (mount.args as Document | undefined),
|
|
180
207
|
wireArgs: declared === undefined ? mount.args : undefined,
|
|
@@ -183,30 +210,34 @@ export async function mountHandlers(
|
|
|
183
210
|
return { handlers }
|
|
184
211
|
}
|
|
185
212
|
|
|
186
|
-
/**
|
|
213
|
+
/** What a handler is keyed by among the mounted: its method and its route. 19.1. */
|
|
214
|
+
export function handlerKey(method: string, address: string): string {
|
|
215
|
+
return `${method} ${address}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The `Response` to a call of `found` made over http, `params` being what its route matched.
|
|
220
|
+
*
|
|
221
|
+
* `found` answers the request's method — or, for an `OPTIONS`, the method its preflight asks
|
|
222
|
+
* about: which handler a request reaches is the route table's to say (19.21).
|
|
223
|
+
*/
|
|
187
224
|
export async function answerRpc(
|
|
188
|
-
|
|
225
|
+
found: Mounted,
|
|
226
|
+
params: Record<string, string | string[]>,
|
|
189
227
|
url: URL,
|
|
190
228
|
request: Request,
|
|
191
229
|
// 16.38 — what paints a refusal for a request that came as a navigation.
|
|
192
230
|
pages: ErrorPages,
|
|
193
231
|
): Promise<Response> {
|
|
194
232
|
const pathname = url.pathname
|
|
195
|
-
const found = handlers.get(pathname)
|
|
196
233
|
const origin = request.headers.get('origin')
|
|
197
234
|
// 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)
|
|
235
|
+
if (request.method === 'OPTIONS')
|
|
236
|
+
return origin !== null && allows(found.handler.crossOrigin, origin)
|
|
237
|
+
? preflight(found, origin, request)
|
|
238
|
+
: answered(notFound({ path: pathname, method: request.method }))
|
|
205
239
|
// 16.50 — a `HEAD` is the `GET`'s answer, which the server sends without its body.
|
|
206
240
|
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
241
|
const handler = found.handler
|
|
211
242
|
const inUrl = argsInURL(found.method)
|
|
212
243
|
const form = isForm(request)
|
|
@@ -226,10 +257,12 @@ export async function answerRpc(
|
|
|
226
257
|
return answered(overCeiling(ceiling))
|
|
227
258
|
const raw = await argsOf(request, url, inUrl, form, ceiling)
|
|
228
259
|
if (isFailed(raw)) return answered(raw)
|
|
260
|
+
const sent = withParameters(raw, params, inUrl || form, found)
|
|
261
|
+
if (isFailed(sent)) return answered(sent)
|
|
229
262
|
let args: unknown
|
|
230
263
|
try {
|
|
231
264
|
// 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,
|
|
265
|
+
args = argsFor(found, sent, inUrl ? url.search : form ? '' : undefined)
|
|
233
266
|
} catch (refused) {
|
|
234
267
|
if (isFailed(refused)) return answered(refused)
|
|
235
268
|
throw refused
|
|
@@ -436,6 +469,39 @@ async function argsOf(
|
|
|
436
469
|
return parsed
|
|
437
470
|
}
|
|
438
471
|
|
|
472
|
+
/**
|
|
473
|
+
* The args a request sent, with the parameters its route matched laid in.
|
|
474
|
+
*
|
|
475
|
+
* 16.65 — the path's, whatever the method. 16.66 — a parameter the query or the body names as
|
|
476
|
+
* well is refused rather than one of the two chosen. A JSON body's members are typed already, so the
|
|
477
|
+
* path's text is coerced to the declared types before it joins them (19.7); text args are coerced
|
|
478
|
+
* with the rest of the text.
|
|
479
|
+
*/
|
|
480
|
+
function withParameters(
|
|
481
|
+
sent: unknown,
|
|
482
|
+
params: Record<string, string | string[]>,
|
|
483
|
+
text: boolean,
|
|
484
|
+
found: Mounted,
|
|
485
|
+
): unknown | Failed<'HttpError', undefined> {
|
|
486
|
+
let none = true
|
|
487
|
+
for (const name in params) {
|
|
488
|
+
none = false
|
|
489
|
+
if (isRecord(sent) && name in sent)
|
|
490
|
+
return refuse(
|
|
491
|
+
400,
|
|
492
|
+
`The parameter '${name}' is in the path and in the request as well.`,
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
if (none) return sent
|
|
496
|
+
const fromPath = text
|
|
497
|
+
? params
|
|
498
|
+
: (coerceArgs(params, found.argsDocument, '') as Record<
|
|
499
|
+
string,
|
|
500
|
+
unknown
|
|
501
|
+
>)
|
|
502
|
+
return isRecord(sent) ? { ...sent, ...fromPath } : { ...fromPath }
|
|
503
|
+
}
|
|
504
|
+
|
|
439
505
|
/**
|
|
440
506
|
* The request's body read up to `ceiling`, as a `Response`, or a 413 refusal past it.
|
|
441
507
|
*
|
|
@@ -487,6 +553,12 @@ function preflight(found: Mounted, origin: string, request: Request): Response {
|
|
|
487
553
|
* 16.57 — `Infinity` is never reaped, which bun spells 0; any finite bound is capped at the 255
|
|
488
554
|
* seconds bun's idle timeout can hold.
|
|
489
555
|
*
|
|
556
|
+
* FOUR SECONDS PAST THE TIMEOUT, NOT AT IT: a load that goes its timeout is refused at that moment,
|
|
557
|
+
* and a stream's refusal is a frame written then (16.68). bun's sockets check an idle timeout on a
|
|
558
|
+
* tick four seconds wide, so an idle timeout of the same second — or one past it — fired anywhere in
|
|
559
|
+
* the tick before it and reaped the connection ahead of the frame, one stream in two under `abide
|
|
560
|
+
* dev`. Four is the tick, so the refusal always goes out first.
|
|
561
|
+
*
|
|
490
562
|
* ON THE REQUEST THE LISTENER RECEIVED, never the one being answered: under a mount, or past a
|
|
491
563
|
* rung that handed `next` its own, that is a copy, and bun's `timeout` does nothing for a request
|
|
492
564
|
* it was not handed itself.
|
|
@@ -496,7 +568,7 @@ export function raiseTimeout(milliseconds: number): void {
|
|
|
496
568
|
const received = scope?.received
|
|
497
569
|
if (scope === undefined || received === undefined) return
|
|
498
570
|
const seconds = Number.isFinite(milliseconds)
|
|
499
|
-
? Math.min(255, Math.ceil(milliseconds / 1000))
|
|
571
|
+
? Math.min(255, Math.ceil(milliseconds / 1000) + 4)
|
|
500
572
|
: Number.POSITIVE_INFINITY
|
|
501
573
|
if (seconds <= scope.timeout) return
|
|
502
574
|
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
|
*/
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// 23.14 — WHERE AN APP'S ADDRESSES ARE, read off the tree in one walk. Every directory below the root
|
|
2
|
+
// is a segment of an address, and a `page.abide`, `layout.abide` or `error.abide` in it answers there;
|
|
3
|
+
// a `server` or `public` directory is never one (23.15) — the first fences a graph and the second
|
|
4
|
+
// holds files answered as authored, at the path of the directories above it. So an app is organized
|
|
5
|
+
// by subject (`media/[id]/page.abide`, `media/server`) and the addresses follow.
|
|
6
|
+
//
|
|
7
|
+
// ONE WALK for the sources and the public files `abide build` copies and `abide dev` answers, and
|
|
8
|
+
// the refusals (23.16, 23.17, 23.18) — and ONE SKIP RULE, `skipped`, for every walk of the tree,
|
|
9
|
+
// beside `placeOf`, which answers the same question of one path.
|
|
10
|
+
//
|
|
11
|
+
// A WALK RATHER THAN A GLOB: a glob descends `node_modules` and the build output before a filter
|
|
12
|
+
// can drop them.
|
|
13
|
+
|
|
14
|
+
import { type Dirent, readdirSync } from 'node:fs' // bun has no directory listing of its own
|
|
15
|
+
import { ABIDE_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
16
|
+
import {
|
|
17
|
+
PUBLIC_DIRECTORY,
|
|
18
|
+
RESERVED_DIRECTORIES,
|
|
19
|
+
} from '#shared/RESERVED_DIRECTORIES.ts'
|
|
20
|
+
import { fileKind, routeOf, skipped } from '#shared/routePattern.ts'
|
|
21
|
+
|
|
22
|
+
/** What one walk of the app finds, every path relative to the root. */
|
|
23
|
+
export type AppFiles = {
|
|
24
|
+
/** Every `.abide`, in path order. */
|
|
25
|
+
abide: string[]
|
|
26
|
+
/** Every file under a `public` directory, to the address it is answered at below the mount. */
|
|
27
|
+
published: Map<string, string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Told of each thing the tree refuses, at the path relative to the root. */
|
|
31
|
+
type Refuse = (path: string, message: string) => void
|
|
32
|
+
|
|
33
|
+
const THROW: Refuse = (_path, message) => {
|
|
34
|
+
throw new Error(message)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The app's `.abide` sources and public files, refusing a tree 23.16, 23.17 or 23.18 forbids.
|
|
39
|
+
*
|
|
40
|
+
* `refuse` throws by default, which is the build's answer; `abide check` collects instead (38.33),
|
|
41
|
+
* and the checker's route table ignores them, since a refused tree is the build's to report.
|
|
42
|
+
*
|
|
43
|
+
* A missing root is an app with nothing in it, not a fault.
|
|
44
|
+
*/
|
|
45
|
+
export function appFiles(root: string, refuse: Refuse = THROW): AppFiles {
|
|
46
|
+
const files: AppFiles = { abide: [], published: new Map() }
|
|
47
|
+
walk(root, '', undefined, files, refuse)
|
|
48
|
+
files.abide.sort()
|
|
49
|
+
return files
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// `reserved` is the nearest reserved directory above `relative` — its path and its name — or
|
|
53
|
+
// `undefined` where none is.
|
|
54
|
+
function walk(
|
|
55
|
+
directory: string,
|
|
56
|
+
relative: string,
|
|
57
|
+
reserved: { path: string; name: string } | undefined,
|
|
58
|
+
files: AppFiles,
|
|
59
|
+
refuse: Refuse,
|
|
60
|
+
): void {
|
|
61
|
+
let entries: Dirent[]
|
|
62
|
+
try {
|
|
63
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
64
|
+
} catch (thrown) {
|
|
65
|
+
if ((thrown as { code?: unknown }).code === 'ENOENT') return
|
|
66
|
+
throw thrown
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (skipped(entry.name)) continue
|
|
70
|
+
const path = `${relative}${entry.name}`
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
let within = reserved
|
|
73
|
+
if (RESERVED_DIRECTORIES.has(entry.name)) {
|
|
74
|
+
if (reserved !== undefined)
|
|
75
|
+
refuse(
|
|
76
|
+
path,
|
|
77
|
+
`abide: ${path} is a ${entry.name} directory under the ${reserved.name} directory ${reserved.path}. RULEBOOK 23.16.`,
|
|
78
|
+
)
|
|
79
|
+
within = { path, name: entry.name }
|
|
80
|
+
}
|
|
81
|
+
walk(
|
|
82
|
+
`${directory}/${entry.name}`,
|
|
83
|
+
`${path}/`,
|
|
84
|
+
within,
|
|
85
|
+
files,
|
|
86
|
+
refuse,
|
|
87
|
+
)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (path.endsWith('.abide')) files.abide.push(path)
|
|
91
|
+
if (reserved === undefined) {
|
|
92
|
+
// 23.17 — anything that could answer here would answer in abide's own namespace.
|
|
93
|
+
if (path.endsWith('.abide'))
|
|
94
|
+
refuseFrameworkRoute(routeOf(path), path, refuse)
|
|
95
|
+
} else if (fileKind(entry.name) !== 'component')
|
|
96
|
+
refuse(
|
|
97
|
+
path,
|
|
98
|
+
`abide: ${path} is a ${entry.name} under the ${reserved.name} directory ${reserved.path}, where nothing has an address. RULEBOOK 23.18.`,
|
|
99
|
+
)
|
|
100
|
+
else if (reserved.name === PUBLIC_DIRECTORY)
|
|
101
|
+
files.published.set(path, publicAddress(path, reserved.path))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Refuses `source` where `route` lies in the framework's own namespace. 23.17. */
|
|
106
|
+
export function refuseFrameworkRoute(
|
|
107
|
+
route: string,
|
|
108
|
+
source: string,
|
|
109
|
+
refuse: Refuse = THROW,
|
|
110
|
+
): void {
|
|
111
|
+
if (route === ABIDE_ADDRESS || route.startsWith(`${ABIDE_ADDRESS}/`))
|
|
112
|
+
refuse(
|
|
113
|
+
source,
|
|
114
|
+
`abide: ${source} answers at ${route}, which is under ${ABIDE_ADDRESS}, where abide's own addresses are. RULEBOOK 23.17.`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 36.11 — the directories above `public`, then the file's own path below it.
|
|
119
|
+
function publicAddress(path: string, directory: string): string {
|
|
120
|
+
const above = directory.slice(0, -PUBLIC_DIRECTORY.length)
|
|
121
|
+
return `/${above}${path.slice(directory.length + 1)}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The public files as the server reads them: each address, without its leading `/`, to the file.
|
|
126
|
+
*
|
|
127
|
+
* 38.19 — out of the source tree, where `abide dev` answers them.
|
|
128
|
+
*/
|
|
129
|
+
export function publishedUnder(
|
|
130
|
+
root: string,
|
|
131
|
+
published: Map<string, string>,
|
|
132
|
+
): Map<string, string> {
|
|
133
|
+
const paths = new Map<string, string>()
|
|
134
|
+
for (const [source, address] of published)
|
|
135
|
+
paths.set(address.slice(1), `${root}/${source}`)
|
|
136
|
+
return paths
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Every `.abide` under `root`, relative to it, in path order — the app's own, and none a
|
|
141
|
+
* dependency ships.
|
|
142
|
+
*
|
|
143
|
+
* Refuses nothing: the language server lists them on every check, and a tree 23.16 forbids is the
|
|
144
|
+
* build's to refuse (`appFiles`).
|
|
145
|
+
*/
|
|
146
|
+
export function abideUnder(root: string): string[] {
|
|
147
|
+
const found: string[] = []
|
|
148
|
+
for (const path of abideFilesUnder(root, '')) found.push(path)
|
|
149
|
+
return found.sort()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Whether a `.abide` lies anywhere under `root` — stopping at the first.
|
|
154
|
+
*
|
|
155
|
+
* 38.26's question, asked of a root before anything is built from it, so asking it of a large tree
|
|
156
|
+
* is as cheap as the tree allows.
|
|
157
|
+
*/
|
|
158
|
+
export function holdsAbide(root: string): boolean {
|
|
159
|
+
return !abideFilesUnder(root, '').next().done
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function* abideFilesUnder(
|
|
163
|
+
directory: string,
|
|
164
|
+
relative: string,
|
|
165
|
+
): Generator<string> {
|
|
166
|
+
let entries: Dirent[]
|
|
167
|
+
try {
|
|
168
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
169
|
+
} catch {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (skipped(entry.name)) continue
|
|
174
|
+
const path = `${relative}${entry.name}`
|
|
175
|
+
if (entry.isDirectory())
|
|
176
|
+
yield* abideFilesUnder(`${directory}/${entry.name}`, `${path}/`)
|
|
177
|
+
else if (path.endsWith('.abide')) yield path
|
|
178
|
+
}
|
|
179
|
+
}
|