@abide/abide 0.124.1 → 0.125.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/ABIDE.md +4 -0
- package/CHANGELOG.md +36 -0
- package/bin/abide.ts +3 -2
- package/docs/REGISTRY.md +66 -40
- package/docs/RULEBOOK.md +202 -18
- package/docs/abide.md +391 -67
- package/package.json +1 -1
- package/src/compiler/appMap.ts +14 -1
- package/src/compiler/declaredExports.ts +41 -0
- package/src/compiler/handlersUnder.ts +1 -0
- package/src/compiler/hookCalls.ts +1 -0
- package/src/compiler/projectShadow.ts +52 -2
- package/src/compiler/readsIn.ts +39 -34
- package/src/server/PageRender.ts +7 -8
- package/src/server/abideModules.ts +6 -0
- package/src/server/answerRequest.ts +115 -52
- package/src/server/answerSocket.ts +6 -5
- package/src/server/answerWorker.ts +34 -23
- package/src/server/assistant.ts +141 -35
- package/src/server/banner.ts +9 -5
- package/src/server/bridgeRelay.ts +3 -3
- package/src/server/callerOf.ts +5 -0
- package/src/server/claudeCode.ts +20 -11
- package/src/server/commands.ts +14 -7
- package/src/server/createApp.ts +65 -12
- package/src/server/devWorker.ts +18 -16
- package/src/server/generatedSurfaces.ts +6 -5
- package/src/server/handlerFileMount.ts +2 -0
- package/src/server/hooks.ts +54 -8
- package/src/server/index.ts +18 -4
- package/src/server/invokeHandler.ts +1 -1
- package/src/server/mountHandlers.ts +6 -0
- package/src/server/principal.ts +3 -2
- package/src/server/render.ts +11 -2
- package/src/server/requestScope.ts +61 -1
- package/src/server/responseRelay.ts +9 -3
- package/src/server/serveDev.ts +18 -3
- package/src/server/sqliteStore.ts +65 -0
- package/src/server/testMount.ts +1 -0
- package/src/shared/AssistantEvent.ts +40 -0
- package/src/shared/BrowserHooks.ts +73 -0
- package/src/shared/CURRENT_ROUTER.ts +3 -1
- package/src/shared/Issues.ts +11 -3
- package/src/shared/Middleware.ts +9 -9
- package/src/shared/UNEXPECTED.ts +1 -1
- package/src/shared/answerOf.ts +9 -4
- package/src/shared/imagePiece.ts +85 -0
- package/src/shared/index.ts +17 -0
- package/src/shared/isServer.ts +15 -0
- package/src/shared/navigate.ts +10 -3
- package/src/shared/onError.ts +51 -0
- package/src/shared/onRefusal.ts +47 -0
- package/src/shared/online.ts +5 -1
- package/src/shared/reactive/Keyed.ts +12 -1
- package/src/shared/reactive/Reactive.ts +53 -25
- package/src/shared/reactive/cellShape.ts +4 -3
- package/src/shared/reactive/poll.ts +49 -0
- package/src/shared/reactive/scoped.ts +10 -1
- package/src/shared/reactive/scoping.ts +14 -0
- package/src/shared/reactive/watch.ts +19 -8
- package/src/shared/resultsOf.ts +21 -0
- package/src/shared/route.ts +18 -4
- package/src/shared/rpc.ts +112 -26
- package/src/shared/seededDocument.ts +5 -6
- package/src/shared/storedValue.ts +32 -0
- package/src/shared/webStore.ts +54 -0
- package/src/ui/Router.ts +42 -9
- package/src/ui/outboxQueue.ts +1 -1
- package/src/ui/wire.ts +4 -4
- package/src/shared/KEEP_OPENED.ts +0 -7
- /package/src/{server → shared}/IMAGE_TYPES.ts +0 -0
package/package.json
CHANGED
package/src/compiler/appMap.ts
CHANGED
|
@@ -16,6 +16,8 @@ type MappedRoute = {
|
|
|
16
16
|
kind: 'page' | 'handler' | 'private'
|
|
17
17
|
/** 23.26 — each matcher its address names, to the anchored pattern it takes a part by. */
|
|
18
18
|
matchers: Record<string, string>
|
|
19
|
+
/** 38.135 — the rungs a handler's `middleware` names, `[]` for none, `null` where unread. */
|
|
20
|
+
middleware: string[] | null
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/** One hook call, the route it is filed at, and the file making it. */
|
|
@@ -42,6 +44,8 @@ export async function appMap(
|
|
|
42
44
|
file: route.source,
|
|
43
45
|
kind: 'page',
|
|
44
46
|
matchers: route.matchers,
|
|
47
|
+
// A page declares no rungs of its own: what runs over it is filed by a hook (37.2).
|
|
48
|
+
middleware: [],
|
|
45
49
|
})
|
|
46
50
|
for (const mount of app.mounts)
|
|
47
51
|
routes.push({
|
|
@@ -50,6 +54,7 @@ export async function appMap(
|
|
|
50
54
|
file: mount.source,
|
|
51
55
|
kind: mount.private ? 'private' : 'handler',
|
|
52
56
|
matchers: mount.matchers,
|
|
57
|
+
middleware: mount.middleware,
|
|
53
58
|
})
|
|
54
59
|
routes.sort((one, two) =>
|
|
55
60
|
one.address === two.address
|
|
@@ -81,7 +86,7 @@ export function mapText(map: Awaited<ReturnType<typeof appMap>>): string {
|
|
|
81
86
|
const addresses = width(map.routes.map((route) => route.address))
|
|
82
87
|
for (const route of map.routes)
|
|
83
88
|
lines.push(
|
|
84
|
-
`${route.method.padEnd(methods)} ${route.address.padEnd(addresses)} ${route.file}${route.kind === 'private' ? ' (private)' : ''}`,
|
|
89
|
+
`${route.method.padEnd(methods)} ${route.address.padEnd(addresses)} ${route.file}${route.kind === 'private' ? ' (private)' : ''}${rungsText(route.middleware)}`,
|
|
85
90
|
)
|
|
86
91
|
// 23.26 — each matcher once, whichever routes name it: what a part must match to be taken.
|
|
87
92
|
const matchers = new Map<string, string>()
|
|
@@ -105,3 +110,11 @@ export function mapText(map: Awaited<ReturnType<typeof appMap>>): string {
|
|
|
105
110
|
}
|
|
106
111
|
return lines.join('\n')
|
|
107
112
|
}
|
|
113
|
+
|
|
114
|
+
/** 38.135 — a handler's rungs as the text map prints them after its file, or nothing for none. */
|
|
115
|
+
function rungsText(middleware: string[] | null): string {
|
|
116
|
+
if (middleware === null) return ' middleware: unread'
|
|
117
|
+
return middleware.length === 0
|
|
118
|
+
? ''
|
|
119
|
+
: ` middleware: ${middleware.join(', ')}`
|
|
120
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isObjectLiteralExpression,
|
|
11
11
|
isPropertyAccessExpression,
|
|
12
12
|
isPropertyAssignment,
|
|
13
|
+
isSpreadElement,
|
|
13
14
|
isStringLiteral,
|
|
14
15
|
isVariableDeclaration,
|
|
15
16
|
} from 'typescript/unstable/ast/is'
|
|
@@ -179,6 +180,10 @@ async function exportsIn(
|
|
|
179
180
|
cli: declared?.cli ?? true,
|
|
180
181
|
mcp: declared?.mcp ?? true,
|
|
181
182
|
description: declared?.description,
|
|
183
|
+
middleware:
|
|
184
|
+
declared === undefined
|
|
185
|
+
? []
|
|
186
|
+
: declared.middleware,
|
|
182
187
|
copy: declared?.copy,
|
|
183
188
|
defaults: declared?.defaults,
|
|
184
189
|
args:
|
|
@@ -228,6 +233,7 @@ type DeclaredExport = Pick<
|
|
|
228
233
|
| 'args'
|
|
229
234
|
| 'result'
|
|
230
235
|
| 'description'
|
|
236
|
+
| 'middleware'
|
|
231
237
|
> & {
|
|
232
238
|
method: Declared | undefined
|
|
233
239
|
/** 18.15 — what its copy's options declare and the build could not read. */
|
|
@@ -249,6 +255,7 @@ async function declaringMethod(
|
|
|
249
255
|
cli: boolean
|
|
250
256
|
mcp: boolean
|
|
251
257
|
description: string | undefined
|
|
258
|
+
middleware: string[] | null
|
|
252
259
|
copy: CopyOptions | undefined
|
|
253
260
|
unread: Node[]
|
|
254
261
|
defaulted?: string[]
|
|
@@ -321,6 +328,7 @@ async function declaringMethod(
|
|
|
321
328
|
cli: declaredFor(call, 'cli') ?? !answersResponse,
|
|
322
329
|
mcp: declaredFor(call, 'mcp') ?? !answersResponse,
|
|
323
330
|
description,
|
|
331
|
+
middleware: rungsNamed(call),
|
|
324
332
|
...declared,
|
|
325
333
|
defaulted: defaultedMembers(body),
|
|
326
334
|
defaults: literalDefaults(body) ?? undefined,
|
|
@@ -602,6 +610,39 @@ function describedAs(
|
|
|
602
610
|
: undefined
|
|
603
611
|
}
|
|
604
612
|
|
|
613
|
+
/**
|
|
614
|
+
* The rung each entry of a handler call's inline `middleware` names, or `null` where its options are
|
|
615
|
+
* not an object written inline.
|
|
616
|
+
*
|
|
617
|
+
* 38.135 — AS WRITTEN, which is what the build can read without running it: an entry is named by
|
|
618
|
+
* the function it calls or the name it refers to, a spread by what it spreads, and an inline function
|
|
619
|
+
* `<inline>`. A value that is not an array is read as one entry, so `middleware: homeAccess(…)` names
|
|
620
|
+
* `homeAccess`.
|
|
621
|
+
*/
|
|
622
|
+
function rungsNamed(
|
|
623
|
+
call: Node & { arguments?: readonly Node[] },
|
|
624
|
+
): string[] | null {
|
|
625
|
+
const options = call.arguments?.[1]
|
|
626
|
+
if (options === undefined) return []
|
|
627
|
+
if (!isObjectLiteralExpression(options)) return null
|
|
628
|
+
const written = optionNamed(options, 'middleware')
|
|
629
|
+
if (written === undefined) return []
|
|
630
|
+
const entries = isArrayLiteralExpression(written)
|
|
631
|
+
? written.elements
|
|
632
|
+
: [written]
|
|
633
|
+
const names: string[] = []
|
|
634
|
+
for (const entry of entries)
|
|
635
|
+
names.push(rungName(isSpreadElement(entry) ? entry.expression : entry))
|
|
636
|
+
return names
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function rungName(entry: Node): string {
|
|
640
|
+
const named = isCallExpression(entry) ? entry.expression : entry
|
|
641
|
+
return isIdentifier(named) || isPropertyAccessExpression(named)
|
|
642
|
+
? named.getText()
|
|
643
|
+
: '<inline>'
|
|
644
|
+
}
|
|
645
|
+
|
|
605
646
|
/**
|
|
606
647
|
* What a handler call's inline `clients` declares for `surface`, or `undefined` where it does not.
|
|
607
648
|
*
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
SOURCE_SUFFIX,
|
|
21
21
|
} from './splitImports.ts'
|
|
22
22
|
import type {
|
|
23
|
+
Attribute,
|
|
23
24
|
Body,
|
|
24
25
|
Branch,
|
|
25
26
|
ElementNode,
|
|
@@ -489,7 +490,10 @@ function project(file: AbideFile): Projection {
|
|
|
489
490
|
const [kind, open] = attribute.lowerName.startsWith(
|
|
490
491
|
'bind:',
|
|
491
492
|
)
|
|
492
|
-
? ([
|
|
493
|
+
? ([
|
|
494
|
+
'elementBind',
|
|
495
|
+
bindOpenerOf(node, attribute.lowerName),
|
|
496
|
+
] as const)
|
|
493
497
|
: openerOf(node.tag, attribute.lowerName)
|
|
494
498
|
slots.set(
|
|
495
499
|
attribute,
|
|
@@ -636,7 +640,7 @@ function project(file: AbideFile): Projection {
|
|
|
636
640
|
* implicit `any`; an attribute on a component whose tag is no name a call can spell (`invocation`
|
|
637
641
|
* has the rest) is typed by nothing the shadow can see, and says so with `any` rather than with an
|
|
638
642
|
* error the author did not make. A `bind:` operand is not asked about here: the caller opens it
|
|
639
|
-
* with `
|
|
643
|
+
* with `bindOpenerOf`.
|
|
640
644
|
*/
|
|
641
645
|
function openerOf(
|
|
642
646
|
tag: string,
|
|
@@ -648,6 +652,44 @@ function openerOf(
|
|
|
648
652
|
return ['attribute', '(']
|
|
649
653
|
}
|
|
650
654
|
|
|
655
|
+
/**
|
|
656
|
+
* What a `bind:` operand on an element is opened with: the bind's `set` held to what the element
|
|
657
|
+
* writes back (32.78), or `BIND` where the element's write is no type the shadow knows.
|
|
658
|
+
*
|
|
659
|
+
* A `type` or a `multiple` written as an expression, or one a spread may write, is read as each it
|
|
660
|
+
* could be, so the `set` is held to the union of their writes.
|
|
661
|
+
*/
|
|
662
|
+
function bindOpenerOf(node: ElementNode, lowerName: string): string {
|
|
663
|
+
const kind = writesOf(node, lowerName)
|
|
664
|
+
return kind === undefined
|
|
665
|
+
? `${BIND}(`
|
|
666
|
+
: `${BIND_WRITES}(${JSON.stringify(kind)})(`
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Which write a `bind:` on an element stands for, as `BIND_WRITES` names its kinds. */
|
|
670
|
+
function writesOf(node: ElementNode, lowerName: string): string | undefined {
|
|
671
|
+
if (lowerName === 'bind:checked' || lowerName === 'bind:open')
|
|
672
|
+
return 'boolean'
|
|
673
|
+
if (lowerName === 'bind:files') return 'files'
|
|
674
|
+
if (lowerName !== 'bind:value') return undefined
|
|
675
|
+
if (node.lowerTag === 'textarea') return 'text'
|
|
676
|
+
if (node.lowerTag !== 'input' && node.lowerTag !== 'select')
|
|
677
|
+
return undefined
|
|
678
|
+
const named = node.lowerTag === 'input' ? 'type' : 'multiple'
|
|
679
|
+
let written: Attribute | undefined
|
|
680
|
+
let spread = false
|
|
681
|
+
for (const attribute of node.attributes) {
|
|
682
|
+
if (attribute.lowerName === named) written = attribute
|
|
683
|
+
else if (attribute.name === '...') spread = true
|
|
684
|
+
}
|
|
685
|
+
if (written?.expression || (written === undefined && spread))
|
|
686
|
+
return node.lowerTag === 'input' ? 'input' : 'select'
|
|
687
|
+
if (node.lowerTag === 'select')
|
|
688
|
+
return written === undefined ? 'option' : 'options'
|
|
689
|
+
const type = written?.value.trim().toLowerCase()
|
|
690
|
+
return type === 'number' || type === 'range' ? 'number' : 'text'
|
|
691
|
+
}
|
|
692
|
+
|
|
651
693
|
const PROP = '__prop'
|
|
652
694
|
// What an inline component's function names the record it is called with, and abide's `props` as
|
|
653
695
|
// its head calls it (32.40) — bound here, since the author imports `props` only where they call it.
|
|
@@ -669,6 +711,8 @@ const ON = '__on'
|
|
|
669
711
|
const ANY = '__any'
|
|
670
712
|
/** 31.3 — what a `bind:` operand is wrapped in on an element and on a component. */
|
|
671
713
|
const BIND = '__bind'
|
|
714
|
+
/** 32.78 — what an element's `bind:` is opened with where the shadow knows what the element writes. */
|
|
715
|
+
const BIND_WRITES = '__bindWrites'
|
|
672
716
|
const BOUND = '__bound'
|
|
673
717
|
|
|
674
718
|
/** 33.6 — the type a written prop's cell is checked as, which is the framework's own. */
|
|
@@ -696,6 +740,12 @@ declare function ${ANY}(value: any): void
|
|
|
696
740
|
declare function ${PROP}<Value>(value: Value): Value
|
|
697
741
|
declare function ${BIND}<Value>(value: { get(): Value; set(value: Value): unknown }): void
|
|
698
742
|
declare function ${BIND}(value: any): void
|
|
743
|
+
type ${BIND_WRITES}Number<Value> = [Extract<Value, number>] extends [never] ? never : number | undefined
|
|
744
|
+
type ${BIND_WRITES}Text<Value> = [Extract<Value, number>] extends [never] ? string : [Exclude<Value, number>] extends [never] ? number | undefined : string | number | undefined
|
|
745
|
+
type ${BIND_WRITES}Option<Value> = [Extract<Value, string>] extends [never] ? ${BIND_WRITES}Text<Value> : Extract<Value, string> | ${BIND_WRITES}Number<Value>
|
|
746
|
+
type ${BIND_WRITES}Options<Value> = Value extends readonly (infer Item)[] ? [Extract<Item, string>] extends [never] ? string[] : Extract<Item, string>[] : string[]
|
|
747
|
+
type ${BIND_WRITES}Of<Kind, Value> = Kind extends 'boolean' ? boolean : Kind extends 'files' ? File[] : Kind extends 'number' ? number | undefined : Kind extends 'input' ? string | number | undefined : Kind extends 'option' ? ${BIND_WRITES}Option<Value> : Kind extends 'options' ? ${BIND_WRITES}Options<Value> : Kind extends 'select' ? ${BIND_WRITES}Option<Value> | ${BIND_WRITES}Options<Value> : ${BIND_WRITES}Text<Value>
|
|
748
|
+
declare function ${BIND_WRITES}<Kind extends string>(kind: Kind): <Value>(value: { (): Value; set: (value: ${BIND_WRITES}Of<Kind, Value>) => unknown } | { get(): Value; set: (value: ${BIND_WRITES}Of<Kind, Value>) => unknown }) => void
|
|
699
749
|
declare function ${BOUND}<Value>(value: ${REACTIVE}<Value, any, any, any>): Value
|
|
700
750
|
declare function ${BOUND}<Value>(value: { get(): Value; set(value: Value): unknown }): Value
|
|
701
751
|
declare function __restValues<Accessors>(record: Accessors): { [Key in keyof Accessors]: NonNullable<Accessors[Key]> extends () => infer Value ? Value : never }
|
package/src/compiler/readsIn.ts
CHANGED
|
@@ -447,6 +447,14 @@ const NULLISH =
|
|
|
447
447
|
TypeFlags.Any |
|
|
448
448
|
TypeFlags.Unknown
|
|
449
449
|
|
|
450
|
+
/** A union's members, or the type alone, with every `NULLISH` one aside. */
|
|
451
|
+
async function presentMembers(type: Type): Promise<Type[]> {
|
|
452
|
+
const present: Type[] = []
|
|
453
|
+
for (const held of await unionMembers(type))
|
|
454
|
+
if ((held.flags & NULLISH) === 0) present.push(held)
|
|
455
|
+
return present
|
|
456
|
+
}
|
|
457
|
+
|
|
450
458
|
async function admitsNullish(type: Type): Promise<boolean> {
|
|
451
459
|
for (const member of await unionMembers(type))
|
|
452
460
|
if ((member.flags & NULLISH) !== 0) return true
|
|
@@ -737,11 +745,11 @@ function valueLeaves(node: Node, leaves: Node[]): void {
|
|
|
737
745
|
}
|
|
738
746
|
}
|
|
739
747
|
|
|
740
|
-
// 23.1 — the
|
|
741
|
-
const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'navigating'])
|
|
748
|
+
// 23.1 — the five values `route` holds, each a `Reactive` of its own.
|
|
749
|
+
const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'index', 'navigating'])
|
|
742
750
|
|
|
743
751
|
/**
|
|
744
|
-
* 31.15 — `route`'s
|
|
752
|
+
* 31.15 — `route`'s five values, each read by name as a name bound to a `Reactive` is.
|
|
745
753
|
*
|
|
746
754
|
* `route.params.id` reads `route.params().id`, and the explicit call keeps compiling (31.7). Told
|
|
747
755
|
* by the symbol, as `props` is, so an app's own `route` is not abide's.
|
|
@@ -1224,11 +1232,10 @@ async function ownMembers(
|
|
|
1224
1232
|
asked.push(
|
|
1225
1233
|
valueOf(node).then(async (value) => {
|
|
1226
1234
|
if (value === undefined) return
|
|
1227
|
-
for (const held of await
|
|
1235
|
+
for (const held of await presentMembers(value))
|
|
1228
1236
|
if (
|
|
1229
|
-
(held.flags & NULLISH) === 0 &&
|
|
1230
1237
|
(await checker.getPropertyOfType(held, member)) !==
|
|
1231
|
-
|
|
1238
|
+
undefined
|
|
1232
1239
|
)
|
|
1233
1240
|
return void owned.add(node)
|
|
1234
1241
|
}),
|
|
@@ -1282,7 +1289,9 @@ async function liftedMutators(
|
|
|
1282
1289
|
asked.push(
|
|
1283
1290
|
valueOf(node).then(async (value) => {
|
|
1284
1291
|
if (value === undefined) return
|
|
1285
|
-
|
|
1292
|
+
// `null` and `undefined` aside at the root as at every step down: `drafts?.push(d)`
|
|
1293
|
+
// over a `Reactive<Draft[] | undefined>` is a mutator on an array.
|
|
1294
|
+
let types = await presentMembers(value)
|
|
1286
1295
|
for (const step of steps)
|
|
1287
1296
|
types = await stepTypes(checker, types, step)
|
|
1288
1297
|
if (types.length === 0) return
|
|
@@ -1310,34 +1319,30 @@ async function stepTypes(
|
|
|
1310
1319
|
step: string | undefined,
|
|
1311
1320
|
): Promise<Type[]> {
|
|
1312
1321
|
const next: Type[] = []
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
continue
|
|
1325
|
-
}
|
|
1326
|
-
if (await checker.isArrayType(held)) {
|
|
1327
|
-
const [element] = await checker.getTypeArguments(
|
|
1328
|
-
held as TypeReference,
|
|
1329
|
-
)
|
|
1330
|
-
if (element !== undefined)
|
|
1331
|
-
next.push(...(await unionMembers(element)))
|
|
1332
|
-
continue
|
|
1333
|
-
}
|
|
1334
|
-
for (const index of await checker.getIndexInfosOfType(held))
|
|
1335
|
-
next.push(...(await unionMembers(index.valueType)))
|
|
1322
|
+
// `types` is present members already: the root's, or this walk's own.
|
|
1323
|
+
for (const held of types) {
|
|
1324
|
+
if (step !== undefined) {
|
|
1325
|
+
const member = await checker.getPropertyOfType(held, step)
|
|
1326
|
+
const stepped =
|
|
1327
|
+
member === undefined
|
|
1328
|
+
? undefined
|
|
1329
|
+
: await checker.getTypeOfSymbol(member)
|
|
1330
|
+
if (stepped !== undefined)
|
|
1331
|
+
next.push(...(await presentMembers(stepped)))
|
|
1332
|
+
continue
|
|
1336
1333
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1334
|
+
if (await checker.isArrayType(held)) {
|
|
1335
|
+
const [element] = await checker.getTypeArguments(
|
|
1336
|
+
held as TypeReference,
|
|
1337
|
+
)
|
|
1338
|
+
if (element !== undefined)
|
|
1339
|
+
next.push(...(await presentMembers(element)))
|
|
1340
|
+
continue
|
|
1341
|
+
}
|
|
1342
|
+
for (const index of await checker.getIndexInfosOfType(held))
|
|
1343
|
+
next.push(...(await presentMembers(index.valueType)))
|
|
1344
|
+
}
|
|
1345
|
+
return next
|
|
1341
1346
|
}
|
|
1342
1347
|
|
|
1343
1348
|
/**
|
package/src/server/PageRender.ts
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import { EARLY_CALLS } from '#shared/EARLY_CALLS.ts'
|
|
12
12
|
import { HandlerRoute } from '#shared/HandlerRoute.ts'
|
|
13
13
|
import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
|
|
14
|
-
import { KEEP_OPENED } from '#shared/KEEP_OPENED.ts'
|
|
15
14
|
import type { Rpc } from '#shared/rpc.ts'
|
|
16
15
|
import { type RpcRequest, rpcRequest } from '#shared/rpcRequest.ts'
|
|
17
16
|
|
|
@@ -141,18 +140,18 @@ export class PageRender {
|
|
|
141
140
|
* one `principal.caller` reads for the rest of the request. 41.13 — every call the render makes is
|
|
142
141
|
* left for the browser's own call to be answered from. 41.29 — BUT NOT A PAGE A SCRIPT FETCHED, which
|
|
143
142
|
* nothing hydrates: its entries would answer the next call this caller makes with what the render
|
|
144
|
-
* saw, a sign-in since included
|
|
145
|
-
*
|
|
146
|
-
* what a later open of it starts.
|
|
143
|
+
* saw, a sign-in since included. The worker asks for the navigation it answers as that navigation
|
|
144
|
+
* (45.25), so the page that hydrates is never one a script fetched. 41.30 — SAVE ONE THE WORKER
|
|
145
|
+
* KEEPS, whose early calls are what a later open of it starts.
|
|
147
146
|
*/
|
|
148
147
|
export function beginPageRender(request: Request): PageRender {
|
|
149
148
|
const caller = callerOrMint()
|
|
150
149
|
const keep = request.headers.get(KEEP_HEADER)
|
|
151
|
-
const hydrated =
|
|
152
|
-
keep === KEEP_OPENED ||
|
|
153
|
-
request.headers.get('sec-fetch-dest') !== 'empty'
|
|
154
150
|
const page = new PageRender(
|
|
155
|
-
|
|
151
|
+
// A WORKER'S NAVIGATION IS STILL ONE: forwarded unchanged (45.25) it arrives `navigate` with
|
|
152
|
+
// its destination `empty`, and no script's fetch can ask in that mode.
|
|
153
|
+
request.headers.get('sec-fetch-dest') !== 'empty' ||
|
|
154
|
+
request.headers.get('sec-fetch-mode') === 'navigate'
|
|
156
155
|
? seedFor(caller)
|
|
157
156
|
: keep !== null
|
|
158
157
|
? callsOnly()
|
|
@@ -489,11 +489,17 @@ function compiledAt(served: Held, path: string): CompiledFile {
|
|
|
489
489
|
// what an app's own module calls is a function of plain props answering a `Component` (35.4), which
|
|
490
490
|
// `render` invokes with nothing. The component is the module's one export (34.17).
|
|
491
491
|
// 38.119 — and it carries what `mount` hydrates it with: the same accessors, and its browser module.
|
|
492
|
+
// 38.134 — a prop written as a getter is read through it on every read, as a parent's would be.
|
|
492
493
|
function boundModule(path: string): string {
|
|
493
494
|
return `import component from ${JSON.stringify(path)}
|
|
494
495
|
export default (props = {}) => {
|
|
495
496
|
const accessors = {}
|
|
496
497
|
for (const key in props) {
|
|
498
|
+
const live = Object.getOwnPropertyDescriptor(props, key)?.get
|
|
499
|
+
if (live !== undefined) {
|
|
500
|
+
accessors[key] = () => live.call(props)
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
497
503
|
const value = props[key]
|
|
498
504
|
accessors[key] = () => value
|
|
499
505
|
}
|
|
@@ -33,7 +33,12 @@ import { answerBridge } from './bridgeRelay.ts'
|
|
|
33
33
|
import { compressAnswer } from './compressAnswer.ts'
|
|
34
34
|
import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
|
|
35
35
|
import { answerMcp, openApi } from './generatedSurfaces.ts'
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
type AppContext,
|
|
38
|
+
failureOf,
|
|
39
|
+
type Lane,
|
|
40
|
+
type LaneRung,
|
|
41
|
+
} from './hooks.ts'
|
|
37
42
|
import { type Mounted, mountedOwn } from './mountHandlers.ts'
|
|
38
43
|
import { muxAdmission } from './muxUpgrade.ts'
|
|
39
44
|
import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
|
|
@@ -95,10 +100,7 @@ export type RouteRow = {
|
|
|
95
100
|
* 16.14 — composed once with the table. Every server block is imported before it is built
|
|
96
101
|
* (37.1), so these are the rungs the boot found, as the root's are.
|
|
97
102
|
*/
|
|
98
|
-
answers: Map<
|
|
99
|
-
Ranked | Mounted,
|
|
100
|
-
(ctx: { request: Request }) => Promise<Response>
|
|
101
|
-
>
|
|
103
|
+
answers: Map<Ranked | Mounted, (ctx: AppContext) => Promise<Response>>
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/** A public file, read once at boot: where the bytes are, the etag off them, and its type. */
|
|
@@ -171,7 +173,7 @@ export async function answerRequest(
|
|
|
171
173
|
// 20.5, 20.9, 36.7 — a surface of abide's own, or a 404 where it answers nothing.
|
|
172
174
|
const surface = SURFACES.get(asked)
|
|
173
175
|
if (surface !== undefined) {
|
|
174
|
-
const answer = await surface(app, request)
|
|
176
|
+
const answer = await surface.answer(app, request)
|
|
175
177
|
if (answer === undefined) return notFound()
|
|
176
178
|
// 21.4 — an answer about the app's surfaces is never cached.
|
|
177
179
|
return answerValue(
|
|
@@ -187,11 +189,7 @@ export async function answerRequest(
|
|
|
187
189
|
// 37.3 — THE MUX'S UPGRADE PASSES THE APP LANE as every request does (`muxUpgrade`).
|
|
188
190
|
const admitted = muxAdmission(asked, request, app)
|
|
189
191
|
if (admitted !== undefined) return admitted
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
asked.startsWith(`${ABIDE_ADDRESS}/`) &&
|
|
193
|
-
!asked.startsWith(`${PRIVATE_HANDLERS_ADDRESS}/`)
|
|
194
|
-
) {
|
|
192
|
+
if (isFrameworkAddress(asked)) {
|
|
195
193
|
const answer = app.framework(asked, request)
|
|
196
194
|
if (answer === undefined) return notFound()
|
|
197
195
|
return readsOnly(request) ? answer : methodRefused(app, asked, READS)
|
|
@@ -217,38 +215,7 @@ export async function answerRequest(
|
|
|
217
215
|
refuse(400, 'The address is not a valid path.'),
|
|
218
216
|
)
|
|
219
217
|
|
|
220
|
-
|
|
221
|
-
// its preflight names (16.24). A route that matches and answers some other method is held, so
|
|
222
|
-
// an address nothing answers the method at is a 405 naming what is answered there (15.17).
|
|
223
|
-
const preflight = request.method === 'OPTIONS'
|
|
224
|
-
// `null` for an `OPTIONS` that names no method, which is no preflight (Fetch's CORS-preflight
|
|
225
|
-
// request carries one) and so no handler's: the route answers 15.17's 405 with what it allows.
|
|
226
|
-
const asking = preflight
|
|
227
|
-
? request.headers.get('access-control-request-method')
|
|
228
|
-
: method
|
|
229
|
-
let allowed: Set<string> | undefined
|
|
230
|
-
let found:
|
|
231
|
-
| { route: RouteRow; params: Params; handler: Mounted | undefined }
|
|
232
|
-
| undefined
|
|
233
|
-
for (const route of app.routes) {
|
|
234
|
-
const params = matchOf(route.segments, parts)
|
|
235
|
-
if (params === undefined) continue
|
|
236
|
-
const handler =
|
|
237
|
-
asking === null ? undefined : route.handlers.get(asking as Method)
|
|
238
|
-
if (
|
|
239
|
-
handler !== undefined ||
|
|
240
|
-
(!preflight && asking === 'GET' && route.page !== undefined)
|
|
241
|
-
) {
|
|
242
|
-
found = { route, params, handler }
|
|
243
|
-
break
|
|
244
|
-
}
|
|
245
|
-
allowed ??= new Set()
|
|
246
|
-
if (route.page !== undefined) allowed.add('GET').add('HEAD')
|
|
247
|
-
for (const method of route.handlers.keys()) {
|
|
248
|
-
allowed.add(method)
|
|
249
|
-
if (method === 'GET') allowed.add('HEAD')
|
|
250
|
-
}
|
|
251
|
-
}
|
|
218
|
+
const { found, allowed } = routed(app, parts, request)
|
|
252
219
|
if (found === undefined) {
|
|
253
220
|
if (allowed !== undefined)
|
|
254
221
|
return methodRefused(app, asked, [...allowed], request)
|
|
@@ -270,8 +237,90 @@ export async function answerRequest(
|
|
|
270
237
|
scope.params = found.params
|
|
271
238
|
const answer = found.route.answers.get(
|
|
272
239
|
found.handler ?? (found.route.page as Ranked),
|
|
273
|
-
) as (ctx:
|
|
274
|
-
return answer({
|
|
240
|
+
) as (ctx: AppContext) => Promise<Response>
|
|
241
|
+
return answer({
|
|
242
|
+
request,
|
|
243
|
+
lane: found.handler === undefined ? 'page' : 'handler',
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* What `answerRequest` would answer `request` with, by the arm of its dispatch it takes. 37.19.
|
|
249
|
+
*
|
|
250
|
+
* ASKED IN THE DISPATCH'S OWN ORDER, over the same tables, so a gate reading it before routing
|
|
251
|
+
* (37.3) is told what routing will choose.
|
|
252
|
+
*/
|
|
253
|
+
export function laneOf(app: AppSources, request: Request): Lane {
|
|
254
|
+
const asked = new URL(request.url).pathname
|
|
255
|
+
if (OWN_HANDLERS.has(asked)) return 'handler'
|
|
256
|
+
const surface = SURFACES.get(asked)
|
|
257
|
+
if (surface !== undefined) return surface.lane
|
|
258
|
+
if (muxAdmission(asked, request, app) !== undefined) return 'socket'
|
|
259
|
+
if (isFrameworkAddress(asked)) return 'abide'
|
|
260
|
+
if (app.published.has(decodedOrKept(asked))) return 'file'
|
|
261
|
+
const parts = partsOf(asked)
|
|
262
|
+
if (parts === undefined) return 'page'
|
|
263
|
+
return routed(app, parts, request).found?.handler === undefined
|
|
264
|
+
? 'page'
|
|
265
|
+
: 'handler'
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Whether `asked` is an address abide answers out of the command's own files.
|
|
270
|
+
*
|
|
271
|
+
* 16.70 — a private handler is answered by the route table, below abide's own namespace.
|
|
272
|
+
*/
|
|
273
|
+
function isFrameworkAddress(asked: string): boolean {
|
|
274
|
+
return (
|
|
275
|
+
asked.startsWith(`${ABIDE_ADDRESS}/`) &&
|
|
276
|
+
!asked.startsWith(`${PRIVATE_HANDLERS_ADDRESS}/`)
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The route answering `request` at `parts`, or the methods answered there where none answers its own.
|
|
282
|
+
*
|
|
283
|
+
* 19.20 — THE MOST SPECIFIC ROUTE ANSWERING THE METHOD, an `OPTIONS` asking about the method
|
|
284
|
+
* its preflight names (16.24). A route that matches and answers some other method is held, so
|
|
285
|
+
* an address nothing answers the method at is a 405 naming what is answered there (15.17).
|
|
286
|
+
*/
|
|
287
|
+
function routed(
|
|
288
|
+
app: AppSources,
|
|
289
|
+
parts: string[],
|
|
290
|
+
request: Request,
|
|
291
|
+
): {
|
|
292
|
+
found:
|
|
293
|
+
| { route: RouteRow; params: Params; handler: Mounted | undefined }
|
|
294
|
+
| undefined
|
|
295
|
+
allowed: Set<string> | undefined
|
|
296
|
+
} {
|
|
297
|
+
const preflight = request.method === 'OPTIONS'
|
|
298
|
+
// `null` for an `OPTIONS` that names no method, which is no preflight (Fetch's CORS-preflight
|
|
299
|
+
// request carries one) and so no handler's: the route answers 15.17's 405 with what it allows.
|
|
300
|
+
const asking = preflight
|
|
301
|
+
? request.headers.get('access-control-request-method')
|
|
302
|
+
: request.method === 'HEAD'
|
|
303
|
+
? 'GET'
|
|
304
|
+
: request.method
|
|
305
|
+
let allowed: Set<string> | undefined
|
|
306
|
+
for (const route of app.routes) {
|
|
307
|
+
const params = matchOf(route.segments, parts)
|
|
308
|
+
if (params === undefined) continue
|
|
309
|
+
const handler =
|
|
310
|
+
asking === null ? undefined : route.handlers.get(asking as Method)
|
|
311
|
+
if (
|
|
312
|
+
handler !== undefined ||
|
|
313
|
+
(!preflight && asking === 'GET' && route.page !== undefined)
|
|
314
|
+
)
|
|
315
|
+
return { found: { route, params, handler }, allowed: undefined }
|
|
316
|
+
allowed ??= new Set()
|
|
317
|
+
if (route.page !== undefined) allowed.add('GET').add('HEAD')
|
|
318
|
+
for (const method of route.handlers.keys()) {
|
|
319
|
+
allowed.add(method)
|
|
320
|
+
if (method === 'GET') allowed.add('HEAD')
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return { found: undefined, allowed }
|
|
275
324
|
}
|
|
276
325
|
|
|
277
326
|
/**
|
|
@@ -332,7 +381,8 @@ async function answerMatched(
|
|
|
332
381
|
// would otherwise render one address and hydrate against another.
|
|
333
382
|
const at = new URL(request.url)
|
|
334
383
|
at.pathname = mounted(at.pathname)
|
|
335
|
-
|
|
384
|
+
// 23.29 — a request is a document load, which arrives at an entry of its own.
|
|
385
|
+
arrive(at.href, page.pattern, params, 0)
|
|
336
386
|
// 11.32 — the page renders in a scope of its own, so what its values hold is this request's —
|
|
337
387
|
// and 41.13, every call it makes is recorded into its render.
|
|
338
388
|
beginPageRender(request)
|
|
@@ -369,18 +419,31 @@ const OWN_HANDLERS = new Map([
|
|
|
369
419
|
|
|
370
420
|
// THE ADDRESSES ABIDE ANSWERS ITSELF, looked up once rather than asked in turn: each to its value,
|
|
371
421
|
// a `Response` of its own, or `undefined` for a 404.
|
|
422
|
+
// 37.19 — each with the lane a gate is told it is.
|
|
372
423
|
const SURFACES = new Map<
|
|
373
424
|
string,
|
|
374
|
-
|
|
425
|
+
{
|
|
426
|
+
lane: Lane
|
|
427
|
+
answer: (app: AppSources, request: Request) => unknown
|
|
428
|
+
}
|
|
375
429
|
>([
|
|
376
|
-
[
|
|
430
|
+
[
|
|
431
|
+
OPENAPI_ADDRESS,
|
|
432
|
+
{ lane: 'openapi', answer: (app) => openApi(app.handlers) },
|
|
433
|
+
],
|
|
377
434
|
// Filled by the app answering this request, so never `undefined` here.
|
|
378
435
|
[
|
|
379
436
|
MCP_ADDRESS,
|
|
380
|
-
|
|
381
|
-
|
|
437
|
+
{
|
|
438
|
+
lane: 'mcp',
|
|
439
|
+
answer: (_app, request) =>
|
|
440
|
+
answerMcp((within('mcp').app as AnsweringApp).mcp, request),
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
[
|
|
444
|
+
BRIDGE_ADDRESS,
|
|
445
|
+
{ lane: 'bridge', answer: (_app, request) => answerBridge(request) },
|
|
382
446
|
],
|
|
383
|
-
[BRIDGE_ADDRESS, (_app, request) => answerBridge(request)],
|
|
384
447
|
])
|
|
385
448
|
|
|
386
449
|
/**
|
|
@@ -395,7 +458,7 @@ export function answerInside(
|
|
|
395
458
|
app: AppSources,
|
|
396
459
|
found: Ranked | Mounted,
|
|
397
460
|
rungs: LaneRung[],
|
|
398
|
-
): (ctx:
|
|
461
|
+
): (ctx: AppContext) => Promise<Response> {
|
|
399
462
|
return compose(rungs, ({ request }) => answerMatched(app, request, found))
|
|
400
463
|
}
|
|
401
464
|
|