@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// SECTION 20 — WHAT IS GENERATED OFF THE ROUTE TABLE: the OpenAPI document
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// SECTION 20 — WHAT IS GENERATED OFF THE ROUTE TABLE: the OpenAPI document and the MCP endpoint. Each
|
|
2
|
+
// is derived from the handlers the app mounted (20.1) and none is authored beside them; each is open
|
|
3
|
+
// exactly where the route table lists a handler for it, and a closed one is a 404 (20.7, 36.7).
|
|
4
4
|
//
|
|
5
5
|
// THE SURFACES ANSWER INSIDE THE APP'S OWN MIDDLEWARE (20.8), because they are answered from the one
|
|
6
6
|
// request path everything under `/__abide` is — and a tool call is a request through it again (20.10).
|
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
// `cli` to false for a handler returning a `Response`, 20.4's `ui: false`, and 20.11's type-derived
|
|
14
14
|
// schema (19.9) as `argsDocument`.
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { isFailed } from '#shared/Failed.ts'
|
|
17
|
+
import { namedLogger } from '#shared/log.ts'
|
|
17
18
|
import { type Reactive, reactive } from '#shared/reactive/index.ts'
|
|
18
|
-
import {
|
|
19
|
-
import { GET } from '#shared/rpc.ts'
|
|
19
|
+
import { parseRoutePattern } from '#shared/routePattern.ts'
|
|
20
20
|
import { argsInURL, rpcRequest } from '#shared/rpcRequest.ts'
|
|
21
21
|
import { answered } from './answerFailure.ts'
|
|
22
22
|
import type { Mounted } from './answerRpc.ts'
|
|
23
23
|
import type { ToolDefinition } from './assistant.ts'
|
|
24
|
-
import { appIdentity
|
|
24
|
+
import { appIdentity } from './config.ts'
|
|
25
25
|
import { crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
|
|
26
26
|
import { MCP_PROTOCOL_VERSION } from './MCP_PROTOCOL_VERSION.ts'
|
|
27
27
|
import { type Listed, surfaceOf } from './surfaceOf.ts'
|
|
@@ -29,14 +29,8 @@ import { type Listed, surfaceOf } from './surfaceOf.ts'
|
|
|
29
29
|
const OPENAPI_LOG = namedLogger('abide:openapi')
|
|
30
30
|
const MCP_LOG = namedLogger('abide:mcp')
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return (config.peek()?.[name] as unknown) === true
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// What MCP and OpenAPI add to a listing: whether the handler is keyed, and its name where only
|
|
38
|
-
// letters, digits, `_` and `-` are admitted — a tool's, an operation's.
|
|
39
|
-
type Published = Listed & { keyed: boolean; name: string }
|
|
32
|
+
// What MCP and OpenAPI add to a listing: whether the handler is keyed.
|
|
33
|
+
type Published = Listed & { keyed: boolean }
|
|
40
34
|
|
|
41
35
|
/** Every handler `surface` lists (`surfaceOf`). */
|
|
42
36
|
function listed(
|
|
@@ -53,7 +47,6 @@ function listed(
|
|
|
53
47
|
found.push({
|
|
54
48
|
...one,
|
|
55
49
|
keyed: mounted.handler.keyed,
|
|
56
|
-
name: one.path.replace(/[^a-zA-Z0-9_-]/g, '_'),
|
|
57
50
|
})
|
|
58
51
|
}
|
|
59
52
|
return found
|
|
@@ -76,8 +69,27 @@ const PROBLEM_SCHEMA = {
|
|
|
76
69
|
},
|
|
77
70
|
}
|
|
78
71
|
|
|
72
|
+
/** `schema` without the members the path carries, or `schema` itself where it carries none. */
|
|
73
|
+
function withoutMembers(
|
|
74
|
+
schema: { properties?: Record<string, unknown>; required?: string[] },
|
|
75
|
+
names: Set<string>,
|
|
76
|
+
): unknown {
|
|
77
|
+
if (names.size === 0 || schema.properties === undefined) return schema
|
|
78
|
+
const properties: Record<string, unknown> = {}
|
|
79
|
+
for (const name in schema.properties)
|
|
80
|
+
if (!names.has(name)) properties[name] = schema.properties[name]
|
|
81
|
+
const required: string[] = []
|
|
82
|
+
for (const name of schema.required ?? [])
|
|
83
|
+
if (!names.has(name)) required.push(name)
|
|
84
|
+
return { ...schema, properties, required }
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
/** The OpenAPI 3.1 document, each handler's `JsonSchema` published as it stands. 20.6. */
|
|
80
|
-
function openApiOf(handlers: Map<string, Mounted>):
|
|
88
|
+
function openApiOf(handlers: Map<string, Mounted>): {
|
|
89
|
+
openapi: string
|
|
90
|
+
info: { title: string; version: string }
|
|
91
|
+
paths: Record<string, Record<string, unknown>>
|
|
92
|
+
} {
|
|
81
93
|
const paths: Record<string, Record<string, unknown>> = {}
|
|
82
94
|
for (const each of listed(handlers, 'openapi')) {
|
|
83
95
|
const operation: Record<string, unknown> = {
|
|
@@ -102,36 +114,60 @@ function openApiOf(handlers: Map<string, Mounted>): unknown {
|
|
|
102
114
|
OPENAPI_LOG.warning(
|
|
103
115
|
`${each.address} has no JSON Schema to publish for its args.`,
|
|
104
116
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
// 16.65 — a route's parameters are the path's, whatever the method, published with or
|
|
118
|
+
// without a schema to type them. OpenAPI has no optional path parameter, so an optional or
|
|
119
|
+
// rest segment is published present and says it may be left out (30.4, 30.5).
|
|
120
|
+
const schema = each.schema as
|
|
121
|
+
| { properties?: Record<string, unknown>; required?: string[] }
|
|
122
|
+
| undefined
|
|
123
|
+
const inPath = new Set<string>()
|
|
124
|
+
const parameters: unknown[] = []
|
|
125
|
+
for (const segment of parseRoutePattern(each.address)) {
|
|
126
|
+
if (segment.kind === 'static') continue
|
|
127
|
+
inPath.add(segment.name)
|
|
128
|
+
const parameter: Record<string, unknown> = {
|
|
129
|
+
name: segment.name,
|
|
130
|
+
in: 'path',
|
|
131
|
+
required: true,
|
|
132
|
+
schema: schema?.properties?.[segment.name] ?? {
|
|
133
|
+
type: 'string',
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
if (segment.kind === 'optional')
|
|
137
|
+
parameter.description = 'May be left out, with its segment.'
|
|
138
|
+
else if (segment.kind === 'rest')
|
|
139
|
+
parameter.description =
|
|
140
|
+
'The rest of the path, any number of segments, and may be left out with its segment.'
|
|
141
|
+
parameters.push(parameter)
|
|
142
|
+
}
|
|
143
|
+
const queried = schema !== undefined && argsInURL(each.method)
|
|
144
|
+
if (queried) {
|
|
145
|
+
const properties = schema.properties ?? {}
|
|
146
|
+
const required = new Set(schema.required ?? [])
|
|
147
|
+
for (const [name, member] of Object.entries(properties))
|
|
148
|
+
if (!inPath.has(name))
|
|
149
|
+
parameters.push({
|
|
119
150
|
name,
|
|
120
151
|
in: 'query',
|
|
121
152
|
required: required.has(name),
|
|
122
|
-
schema,
|
|
123
|
-
})
|
|
124
|
-
)
|
|
125
|
-
} else
|
|
126
|
-
operation.requestBody = {
|
|
127
|
-
required: true,
|
|
128
|
-
content: { 'application/json': { schema: each.schema } },
|
|
129
|
-
}
|
|
153
|
+
schema: member,
|
|
154
|
+
})
|
|
130
155
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
156
|
+
if (parameters.length > 0) operation.parameters = parameters
|
|
157
|
+
if (schema !== undefined && !queried)
|
|
158
|
+
operation.requestBody = {
|
|
159
|
+
required: true,
|
|
160
|
+
content: {
|
|
161
|
+
'application/json': {
|
|
162
|
+
schema: withoutMembers(schema, inPath),
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
// OpenAPI spells a parameter `{id}` where a route spells it `[id]`.
|
|
167
|
+
const path = each.address.replace(/\[+(?:\.\.\.)?([^\]]+)\]+/g, '{$1}')
|
|
168
|
+
paths[path] ??= {}
|
|
169
|
+
;(paths[path] as Record<string, unknown>)[each.method.toLowerCase()] =
|
|
170
|
+
operation
|
|
135
171
|
}
|
|
136
172
|
const { name, version } = appIdentity()
|
|
137
173
|
return {
|
|
@@ -163,15 +199,17 @@ function overTable<Built>(
|
|
|
163
199
|
return found
|
|
164
200
|
}
|
|
165
201
|
|
|
166
|
-
const documents = new WeakMap<
|
|
202
|
+
const documents = new WeakMap<
|
|
203
|
+
Map<string, Mounted>,
|
|
204
|
+
Reactive<ReturnType<typeof openApiOf>>
|
|
205
|
+
>()
|
|
167
206
|
|
|
168
|
-
/** The OpenAPI document over the route table, or `undefined` where
|
|
207
|
+
/** The OpenAPI document over the route table, or `undefined` where it lists no handler. 20.7. */
|
|
169
208
|
export function openApi(
|
|
170
209
|
handlers: Map<string, Mounted>,
|
|
171
210
|
): Reactive<unknown> | undefined {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
: undefined
|
|
211
|
+
const document = overTable(documents, handlers, openApiOf)
|
|
212
|
+
return Object.keys(document().paths).length > 0 ? document : undefined
|
|
175
213
|
}
|
|
176
214
|
|
|
177
215
|
/** What the MCP surface publishes: a tool per call that takes args or writes, a resource per read. */
|
|
@@ -224,9 +262,10 @@ export class McpSurface {
|
|
|
224
262
|
this.#answer = answer
|
|
225
263
|
}
|
|
226
264
|
|
|
227
|
-
/** Whether
|
|
265
|
+
/** Whether the route table publishes any handler to it. 20.7. */
|
|
228
266
|
get opened(): boolean {
|
|
229
|
-
|
|
267
|
+
const { tools, resources } = this.#published()
|
|
268
|
+
return tools.length > 0 || resources.length > 0
|
|
230
269
|
}
|
|
231
270
|
|
|
232
271
|
#published(): Tools {
|
|
@@ -274,7 +313,14 @@ export class McpSurface {
|
|
|
274
313
|
args: Record<string, unknown> | undefined,
|
|
275
314
|
asker: Request,
|
|
276
315
|
): Promise<{ text: string; failed: boolean }> {
|
|
277
|
-
|
|
316
|
+
let answer: Response
|
|
317
|
+
try {
|
|
318
|
+
answer = await this.#answer(callOf(asker, one, args))
|
|
319
|
+
} catch (thrown) {
|
|
320
|
+
// 15.8 — a path parameter no address carries, refused as the wire's 422 is (`rpcRequest`).
|
|
321
|
+
if (!isFailed(thrown)) throw thrown
|
|
322
|
+
answer = answered(thrown)
|
|
323
|
+
}
|
|
278
324
|
return { text: await answer.text(), failed: answer.status >= 400 }
|
|
279
325
|
}
|
|
280
326
|
|
|
@@ -355,7 +401,7 @@ export class McpSurface {
|
|
|
355
401
|
}
|
|
356
402
|
|
|
357
403
|
/**
|
|
358
|
-
* Answers one MCP JSON-RPC request over `surface`, or `undefined` where
|
|
404
|
+
* Answers one MCP JSON-RPC request over `surface`, or `undefined` where it publishes nothing.
|
|
359
405
|
*
|
|
360
406
|
* 20.9 — THE STREAMABLE-HTTP MCP ENDPOINT, answering JSON-RPC over `POST`, as the request's own
|
|
361
407
|
* caller: a handler that takes no args is a resource, read at its address; one that takes args, and
|
|
@@ -406,7 +452,7 @@ function callOf(
|
|
|
406
452
|
)
|
|
407
453
|
const headers = new Headers(asked.headers)
|
|
408
454
|
headers.delete('content-length')
|
|
409
|
-
// 38.
|
|
455
|
+
// 38.68 — the answer is read here rather than sent on, and nothing in process decodes it.
|
|
410
456
|
headers.delete('accept-encoding')
|
|
411
457
|
if (call.body !== undefined) headers.set('content-type', 'application/json')
|
|
412
458
|
return new Request(new URL(call.url, asked.url), {
|
|
@@ -415,16 +461,3 @@ function callOf(
|
|
|
415
461
|
body: call.body ?? null,
|
|
416
462
|
})
|
|
417
463
|
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* The `GET` over `log.records`, mounted at `/__abide/logs`: closed until `ABIDE_LOGS` opens it.
|
|
421
|
-
*
|
|
422
|
-
* 36.7, 28.2 — A `GET` LIKE ANY OTHER, so it is followed as any `global` value is (18.20): the lines
|
|
423
|
-
* held, then each line written as an `add` at `/-`. The rung is the gate, and makes it a guarded
|
|
424
|
-
* `GET` — run once as a follow opens (18.8), and answering 404 while `ABIDE_LOGS` is closed.
|
|
425
|
-
*/
|
|
426
|
-
export const LOGS_HANDLER = GET(log.records, {
|
|
427
|
-
middleware: [
|
|
428
|
-
(next, ctx) => (opened('ABIDE_LOGS') ? next(ctx) : refuse(404)),
|
|
429
|
-
],
|
|
430
|
-
})
|
package/src/server/hooks.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
// SECTION 37 — THE LIFECYCLE HOOKS, called from
|
|
2
|
-
// (37.1). Each call files its function here
|
|
3
|
-
// is filed once
|
|
1
|
+
// SECTION 37 — THE LIFECYCLE HOOKS, called from a page's or a layout's `<script server>` rather
|
|
2
|
+
// than exported from it (37.1). Each call files its function here, at the ROUTE of the file that
|
|
3
|
+
// called it, and hands back what unfiles it; `createApp` reads what is filed once every module
|
|
4
|
+
// holding a server block has run.
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
6
|
+
// WHICH ROUTE IS ASKED OF THE MODULE BEING EVALUATED, not of the hook: the compiled server module
|
|
7
|
+
// calls `fileRoute` with its route before its server block's body and with `undefined` after it,
|
|
8
|
+
// and a server block cannot await at its top level (34.3), so the window is synchronous. A hook
|
|
9
|
+
// called outside every server block — a test, a script — files at the root.
|
|
10
|
+
//
|
|
11
|
+
// PROCESS-WIDE, like the modules that call them: each is evaluated once per process, so a second
|
|
12
|
+
// `App` in it boots under the same hooks (42.12's sharing, from the other side).
|
|
7
13
|
|
|
8
14
|
import { type Failed, isFailed } from '#shared/Failed.ts'
|
|
9
15
|
import { namedLogger } from '#shared/log.ts'
|
|
10
16
|
import type { Middleware } from '#shared/Middleware.ts'
|
|
11
17
|
import { aborted, refuse } from '#shared/refuse.ts'
|
|
18
|
+
import {
|
|
19
|
+
coversRoute,
|
|
20
|
+
parseRoutePattern,
|
|
21
|
+
type Segment,
|
|
22
|
+
} from '#shared/routePattern.ts'
|
|
12
23
|
import type { Schema } from '#shared/Schema.ts'
|
|
13
24
|
import { config } from './config.ts'
|
|
14
25
|
import { currentRequestScope } from './requestScope.ts'
|
|
@@ -21,40 +32,103 @@ export type ConfigOptions = { schema?: Schema<unknown> }
|
|
|
21
32
|
|
|
22
33
|
type Wrapper = (inner: () => Promise<void>) => void | Promise<void>
|
|
23
34
|
|
|
24
|
-
/**
|
|
35
|
+
/** One hook, at the route of the file that filed it. 37.1. */
|
|
36
|
+
type Filed<T> = {
|
|
37
|
+
route: string
|
|
38
|
+
segments: Segment[]
|
|
39
|
+
item: T
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Everything the lifecycle hooks filed, for `createApp` to read once the app's modules have run. */
|
|
25
43
|
export const HOOKS = {
|
|
26
|
-
rungs: [] as AppRung[],
|
|
27
|
-
starts: [] as Wrapper[],
|
|
28
|
-
stops: [] as Wrapper[],
|
|
29
|
-
errors: [] as (
|
|
30
|
-
health: [] as (
|
|
31
|
-
config:
|
|
32
|
-
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
rungs: [] as Filed<AppRung>[],
|
|
45
|
+
starts: [] as Filed<Wrapper>[],
|
|
46
|
+
stops: [] as Filed<Wrapper>[],
|
|
47
|
+
errors: [] as Filed<(error: unknown) => unknown>[],
|
|
48
|
+
health: [] as Filed<() => unknown>[],
|
|
49
|
+
config: [] as Filed<{
|
|
50
|
+
defaults: ConfigDefaults | null
|
|
51
|
+
options: ConfigOptions | undefined
|
|
52
|
+
}>[],
|
|
53
|
+
principal: [] as Filed<(claims: unknown) => unknown | Promise<unknown>>[],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ROOT = '/'
|
|
57
|
+
|
|
58
|
+
// The route a hook called now is filed at — the server block being evaluated, or the root.
|
|
59
|
+
let filing: string | undefined
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sets the route the hooks called next are filed at, or the root again for `undefined`.
|
|
63
|
+
*
|
|
64
|
+
* 37.1 — what a compiled server module calls around its `<script server>`; nothing an app writes.
|
|
65
|
+
*/
|
|
66
|
+
export function fileRoute(route: string | undefined): void {
|
|
67
|
+
filing = route
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
const LIFECYCLE_LOG = namedLogger('abide:lifecycle')
|
|
43
|
-
const CONFIG_LOG = namedLogger('abide:config')
|
|
44
|
-
const PRINCIPAL_LOG = namedLogger('abide:principal')
|
|
45
71
|
|
|
46
|
-
function filed<T>(list: T[], item: T): () => void {
|
|
47
|
-
|
|
72
|
+
function filed<T>(list: Filed<T>[], item: T): () => void {
|
|
73
|
+
const route = filing ?? ROOT
|
|
74
|
+
const entry: Filed<T> = {
|
|
75
|
+
route,
|
|
76
|
+
segments: parseRoutePattern(route),
|
|
77
|
+
item,
|
|
78
|
+
}
|
|
79
|
+
list.push(entry)
|
|
80
|
+
// 37.7 — by route depth, then by route, then in the order called: kept sorted as it is filed,
|
|
81
|
+
// so a reader walks it in order and sorts nothing per request. The sort is stable and the
|
|
82
|
+
// entry lands last, so a tie stays in the order called.
|
|
83
|
+
list.sort(byRoute)
|
|
48
84
|
return () => {
|
|
49
|
-
const at = list.indexOf(
|
|
85
|
+
const at = list.indexOf(entry)
|
|
50
86
|
if (at !== -1) list.splice(at, 1)
|
|
51
87
|
}
|
|
52
88
|
}
|
|
53
89
|
|
|
90
|
+
function byRoute(one: Filed<unknown>, two: Filed<unknown>): number {
|
|
91
|
+
if (one.segments.length !== two.segments.length)
|
|
92
|
+
return one.segments.length - two.segments.length
|
|
93
|
+
if (one.route !== two.route) return one.route < two.route ? -1 : 1
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** What `list` filed, in 37.7's order. */
|
|
98
|
+
export function inOrder<T>(list: readonly Filed<T>[]): T[] {
|
|
99
|
+
const items: T[] = []
|
|
100
|
+
for (const entry of list) items.push(entry.item)
|
|
101
|
+
return items
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The rungs filed at the root route, which run before routing. 37.3. */
|
|
105
|
+
export function rootRungs(): AppRung[] {
|
|
106
|
+
const rungs: AppRung[] = []
|
|
107
|
+
for (const entry of HOOKS.rungs)
|
|
108
|
+
if (entry.route === ROOT) rungs.push(entry.item)
|
|
109
|
+
return rungs
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The rungs filed below the root over `route`, outermost first.
|
|
114
|
+
*
|
|
115
|
+
* 37.2 — every rung filed at `route` or at a route that is its front, the root's aside: those ran
|
|
116
|
+
* before routing (37.3).
|
|
117
|
+
*/
|
|
118
|
+
export function rungsOver(route: string): AppRung[] {
|
|
119
|
+
const segments = parseRoutePattern(route)
|
|
120
|
+
const rungs: AppRung[] = []
|
|
121
|
+
for (const entry of HOOKS.rungs)
|
|
122
|
+
if (entry.route !== ROOT && coversRoute(entry.segments, segments))
|
|
123
|
+
rungs.push(entry.item)
|
|
124
|
+
return rungs
|
|
125
|
+
}
|
|
126
|
+
|
|
54
127
|
/**
|
|
55
|
-
* Files middleware rungs the
|
|
128
|
+
* Files middleware rungs over the calling file's route; returns what unfiles them.
|
|
56
129
|
*
|
|
57
|
-
* 37.3 — pre-routing, over the request and the response, in the order registered.
|
|
130
|
+
* 37.3 at the root — pre-routing, over the request and the response, in the order registered. 37.2
|
|
131
|
+
* below it — after routing, over what that route and those below it answer.
|
|
58
132
|
*/
|
|
59
133
|
export function middleware(...rungs: AppRung[]): () => void {
|
|
60
134
|
const unfile: (() => void)[] = []
|
|
@@ -87,7 +161,7 @@ export function onStop(
|
|
|
87
161
|
}
|
|
88
162
|
|
|
89
163
|
/**
|
|
90
|
-
* Files a hook handed every unexpected error; returns what unfiles it.
|
|
164
|
+
* Files a hook handed every unexpected error its route answers; returns what unfiles it.
|
|
91
165
|
*
|
|
92
166
|
* 37.5.
|
|
93
167
|
*/
|
|
@@ -98,52 +172,40 @@ export function onError(fn: (error: unknown) => unknown): () => void {
|
|
|
98
172
|
/**
|
|
99
173
|
* Files a health reporter whose account is merged over the baseline; returns what unfiles it.
|
|
100
174
|
*
|
|
101
|
-
* 25.2.
|
|
175
|
+
* 25.2, 37.7.
|
|
102
176
|
*/
|
|
103
177
|
export function onHealth(report: () => unknown | Promise<unknown>): () => void {
|
|
104
178
|
return filed(HOOKS.health, report)
|
|
105
179
|
}
|
|
106
180
|
|
|
107
181
|
/**
|
|
108
|
-
*
|
|
182
|
+
* Files config defaults and a schema, composed with every other; returns what unfiles them.
|
|
109
183
|
*
|
|
110
|
-
* 27.6, 37.7 —
|
|
111
|
-
*
|
|
184
|
+
* 27.6, 37.7 — merged in route order rather than replacing. 27.10 — the read after either is
|
|
185
|
+
* resolved under what is filed now, not what was filed when `config` was last read.
|
|
112
186
|
*/
|
|
113
187
|
export function onConfig(
|
|
114
188
|
fn: ConfigDefaults | null,
|
|
115
189
|
options?: ConfigOptions,
|
|
116
190
|
): () => void {
|
|
117
|
-
|
|
118
|
-
CONFIG_LOG.warning(
|
|
119
|
-
'a second onConfig replaced the first, and its schema with it. RULEBOOK 27.6.',
|
|
120
|
-
)
|
|
121
|
-
const entry = { defaults: fn, options }
|
|
122
|
-
HOOKS.config = entry
|
|
191
|
+
const unfile = filed(HOOKS.config, { defaults: fn, options })
|
|
123
192
|
config.invalidate()
|
|
124
193
|
return () => {
|
|
125
|
-
|
|
126
|
-
HOOKS.config = undefined
|
|
194
|
+
unfile()
|
|
127
195
|
config.invalidate()
|
|
128
196
|
}
|
|
129
197
|
}
|
|
130
198
|
|
|
131
199
|
/**
|
|
132
|
-
*
|
|
200
|
+
* Files a function resolving a principal from its claims, composed with every other; returns what
|
|
201
|
+
* unfiles it.
|
|
133
202
|
*
|
|
134
|
-
* 37.7 —
|
|
203
|
+
* 37.7 — each is handed the claims, and what they answer is merged in route order.
|
|
135
204
|
*/
|
|
136
205
|
export function onPrincipal(
|
|
137
206
|
resolve: (claims: unknown) => unknown | Promise<unknown>,
|
|
138
207
|
): () => void {
|
|
139
|
-
|
|
140
|
-
PRINCIPAL_LOG.warning(
|
|
141
|
-
'a second onPrincipal replaced the first. RULEBOOK 37.7.',
|
|
142
|
-
)
|
|
143
|
-
HOOKS.principal = resolve
|
|
144
|
-
return () => {
|
|
145
|
-
if (HOOKS.principal === resolve) HOOKS.principal = undefined
|
|
146
|
-
}
|
|
208
|
+
return filed(HOOKS.principal, resolve)
|
|
147
209
|
}
|
|
148
210
|
|
|
149
211
|
/**
|
|
@@ -163,12 +225,22 @@ export function reportUnexpected(error: unknown): void {
|
|
|
163
225
|
* 12.6 — the half of 37.5 a failure already warned on its own namespace is owed.
|
|
164
226
|
*/
|
|
165
227
|
export function handOnError(error: unknown): void {
|
|
166
|
-
|
|
228
|
+
// 37.5 — NEAREST ROUTE FIRST, over the route the request matched; outside every request, or
|
|
229
|
+
// before one matched, the root's alone.
|
|
230
|
+
const route = currentRequestScope()?.route
|
|
231
|
+
const segments = route === undefined ? undefined : parseRoutePattern(route)
|
|
232
|
+
for (let at = HOOKS.errors.length - 1; at >= 0; at -= 1) {
|
|
233
|
+
const entry = HOOKS.errors[at] as Filed<(error: unknown) => unknown>
|
|
234
|
+
const reaches =
|
|
235
|
+
entry.route === ROOT ||
|
|
236
|
+
(segments !== undefined && coversRoute(entry.segments, segments))
|
|
237
|
+
if (!reaches) continue
|
|
167
238
|
try {
|
|
168
|
-
|
|
239
|
+
entry.item(error)
|
|
169
240
|
} catch (thrown) {
|
|
170
241
|
LIFECYCLE_LOG.error('an onError hook threw', thrown)
|
|
171
242
|
}
|
|
243
|
+
}
|
|
172
244
|
}
|
|
173
245
|
|
|
174
246
|
/**
|