@abide/abide 0.124.0 → 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.
Files changed (73) hide show
  1. package/ABIDE.md +4 -0
  2. package/CHANGELOG.md +43 -0
  3. package/bin/abide.ts +3 -2
  4. package/docs/REGISTRY.md +67 -41
  5. package/docs/RULEBOOK.md +207 -20
  6. package/docs/abide.md +392 -68
  7. package/package.json +1 -1
  8. package/src/compiler/Lowering.ts +29 -4
  9. package/src/compiler/appMap.ts +14 -1
  10. package/src/compiler/declaredExports.ts +41 -0
  11. package/src/compiler/handlersUnder.ts +1 -0
  12. package/src/compiler/hookCalls.ts +1 -0
  13. package/src/compiler/projectShadow.ts +53 -2
  14. package/src/compiler/readsIn.ts +39 -34
  15. package/src/server/BRIDGE_VERSION.ts +1 -1
  16. package/src/server/PageRender.ts +7 -8
  17. package/src/server/abideModules.ts +6 -0
  18. package/src/server/answerRequest.ts +115 -52
  19. package/src/server/answerSocket.ts +6 -5
  20. package/src/server/answerWorker.ts +34 -23
  21. package/src/server/assistant.ts +141 -35
  22. package/src/server/banner.ts +9 -5
  23. package/src/server/bridgeRelay.ts +3 -3
  24. package/src/server/callerOf.ts +5 -0
  25. package/src/server/claudeCode.ts +20 -11
  26. package/src/server/commands.ts +14 -7
  27. package/src/server/createApp.ts +65 -12
  28. package/src/server/devWorker.ts +18 -16
  29. package/src/server/generatedSurfaces.ts +6 -5
  30. package/src/server/handlerFileMount.ts +2 -0
  31. package/src/server/hooks.ts +54 -8
  32. package/src/server/index.ts +18 -4
  33. package/src/server/invokeHandler.ts +1 -1
  34. package/src/server/mountHandlers.ts +6 -0
  35. package/src/server/principal.ts +3 -2
  36. package/src/server/render.ts +11 -2
  37. package/src/server/requestScope.ts +61 -1
  38. package/src/server/responseRelay.ts +9 -3
  39. package/src/server/serveDev.ts +18 -3
  40. package/src/server/sqliteStore.ts +65 -0
  41. package/src/server/testMount.ts +1 -0
  42. package/src/shared/AssistantEvent.ts +40 -0
  43. package/src/shared/BrowserHooks.ts +73 -0
  44. package/src/shared/CURRENT_ROUTER.ts +3 -1
  45. package/src/shared/Issues.ts +11 -3
  46. package/src/shared/Middleware.ts +9 -9
  47. package/src/shared/UNEXPECTED.ts +1 -1
  48. package/src/shared/answerOf.ts +9 -4
  49. package/src/shared/imagePiece.ts +85 -0
  50. package/src/shared/index.ts +17 -0
  51. package/src/shared/isServer.ts +15 -0
  52. package/src/shared/navigate.ts +10 -3
  53. package/src/shared/onError.ts +51 -0
  54. package/src/shared/onRefusal.ts +47 -0
  55. package/src/shared/online.ts +5 -1
  56. package/src/shared/reactive/Keyed.ts +12 -1
  57. package/src/shared/reactive/Reactive.ts +65 -26
  58. package/src/shared/reactive/cellShape.ts +4 -3
  59. package/src/shared/reactive/poll.ts +49 -0
  60. package/src/shared/reactive/scoped.ts +10 -1
  61. package/src/shared/reactive/scoping.ts +14 -0
  62. package/src/shared/reactive/watch.ts +19 -8
  63. package/src/shared/resultsOf.ts +21 -0
  64. package/src/shared/route.ts +18 -4
  65. package/src/shared/rpc.ts +112 -26
  66. package/src/shared/seededDocument.ts +5 -6
  67. package/src/shared/storedValue.ts +32 -0
  68. package/src/shared/webStore.ts +54 -0
  69. package/src/ui/Router.ts +42 -9
  70. package/src/ui/outboxQueue.ts +1 -1
  71. package/src/ui/wire.ts +4 -4
  72. package/src/shared/KEEP_OPENED.ts +0 -7
  73. /package/src/{server → shared}/IMAGE_TYPES.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.124.0",
3
+ "version": "0.125.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -371,7 +371,7 @@ export class Lowering {
371
371
  */
372
372
  reference(holder: object): string {
373
373
  const span = this.spanOf(holder)
374
- return this.boundPath(span) ?? grouped(this.lowerChunk(span, true))
374
+ return grouped(this.lowerChunk(span, true))
375
375
  }
376
376
 
377
377
  /**
@@ -383,9 +383,17 @@ export class Lowering {
383
383
  * A COMPUTED STEP IS EMITTED SOURCE, as 31.8's is: `params[s.k]` writes through `s.k` lowered,
384
384
  * so the key is read when the input writes and the get reads it too — the binding follows the
385
385
  * key. A `!` is a type and no step. `undefined` for an operand that is not a name followed by
386
- * steps, which `reference` lowers as it stands — save 32.76's optional step, which is refused.
386
+ * steps, which `lower` lowers as it stands — save 32.76's optional step, which is refused.
387
+ *
388
+ * IN `lower`, SO THE CHECKER READS THE PAIR THE BUILD EMITS. Taken only where the build asked
389
+ * (`reference`), the shadow lowered the same path as a read, and a component's `bind:` held
390
+ * `__bound` to the string it read — TS2769 on a binding the build compiles and runs.
387
391
  */
388
- private boundPath(span: Span): string | undefined {
392
+ private boundPath(
393
+ span: Span,
394
+ origins: Origin[] | undefined,
395
+ base: number,
396
+ ): string | undefined {
389
397
  const source = this.projection.text
390
398
  const text = source.slice(span.start, span.end)
391
399
  const from = span.start + (text.length - text.trimStart().length)
@@ -433,7 +441,20 @@ export class Lowering {
433
441
  }
434
442
  if (segments.length === 0) return undefined
435
443
  const name = this.nameOf(read)
436
- return `{ get: () => ${this.lowerChunk({ start: from, end: to })}, set: (${STORED}) => ${name}.set(${this.use('writePath')}(${name}(), [${segments.join(', ')}], ${STORED})) }`
444
+ const open = '{ get: () => '
445
+ origins?.push({ at: base, source: from, length: 0 })
446
+ const got = this.lower(
447
+ { start: from, end: to },
448
+ false,
449
+ origins,
450
+ base + open.length,
451
+ )
452
+ origins?.push({
453
+ at: base + open.length + got.length,
454
+ source: to,
455
+ length: 0,
456
+ })
457
+ return `${open}${got}, set: (${STORED}) => ${name}.set(${this.use('writePath')}(${name}(), [${segments.join(', ')}], ${STORED})) }`
437
458
  }
438
459
 
439
460
  /**
@@ -567,6 +588,10 @@ export class Lowering {
567
588
  const text = source.slice(from, to)
568
589
  const wholeFrom = from + (text.length - text.trimStart().length)
569
590
  const wholeTo = from + text.trimEnd().length
591
+ if (holds) {
592
+ const path = this.boundPath(span, origins, base)
593
+ if (path !== undefined) return path
594
+ }
570
595
  const reads = this.answer.reads
571
596
  let read = this.answer.firstRead(from)
572
597
  if (read === reads.length || (reads[read] as Read).start >= to) {
@@ -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
  *
@@ -128,6 +128,7 @@ export async function handlersUnder(
128
128
  cli: false,
129
129
  mcp: false,
130
130
  description: each.description,
131
+ middleware: each.middleware,
131
132
  copy: each.copy,
132
133
  defaults: each.defaults,
133
134
  args: each.args,
@@ -18,6 +18,7 @@ export const HOOK_NAMES: ReadonlySet<string> = new Set([
18
18
  'onPrincipal',
19
19
  'onLauncher',
20
20
  'onMenu',
21
+ 'onRefusal',
21
22
  ])
22
23
 
23
24
  /** One call of one of abide's hooks. */
@@ -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
- ? (['elementBind', `${BIND}(`] as const)
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 `BIND`, which types it as `any` too.
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. */
@@ -694,7 +738,14 @@ const POSITION_DECLARATIONS = `
694
738
  declare function ${ON}<Name extends string>(name: Name, handler: ((event: Name extends keyof HTMLElementEventMap ? HTMLElementEventMap[Name] : Event) => unknown) | null | undefined | false): void
695
739
  declare function ${ANY}(value: any): void
696
740
  declare function ${PROP}<Value>(value: Value): Value
741
+ declare function ${BIND}<Value>(value: { get(): Value; set(value: Value): unknown }): void
697
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
698
749
  declare function ${BOUND}<Value>(value: ${REACTIVE}<Value, any, any, any>): Value
699
750
  declare function ${BOUND}<Value>(value: { get(): Value; set(value: Value): unknown }): Value
700
751
  declare function __restValues<Accessors>(record: Accessors): { [Key in keyof Accessors]: NonNullable<Accessors[Key]> extends () => infer Value ? Value : never }
@@ -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 four values `route` holds, each a `Reactive` of its own.
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 four values, each read by name as a name bound to a `Reactive` is.
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 unionMembers(value))
1235
+ for (const held of await presentMembers(value))
1228
1236
  if (
1229
- (held.flags & NULLISH) === 0 &&
1230
1237
  (await checker.getPropertyOfType(held, member)) !==
1231
- undefined
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
- let types: Type[] = [value]
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
- for (const type of types)
1314
- for (const held of await unionMembers(type)) {
1315
- if ((held.flags & NULLISH) !== 0) continue
1316
- if (step !== undefined) {
1317
- const member = await checker.getPropertyOfType(held, step)
1318
- const stepped =
1319
- member === undefined
1320
- ? undefined
1321
- : await checker.getTypeOfSymbol(member)
1322
- if (stepped !== undefined)
1323
- next.push(...(await unionMembers(stepped)))
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
- const present: Type[] = []
1338
- for (const type of next)
1339
- if ((type.flags & NULLISH) === 0) present.push(type)
1340
- return present
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
  /**
@@ -1,3 +1,3 @@
1
1
  // The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
2
2
  // server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
3
- export const BRIDGE_VERSION = '0.5.1'
3
+ export const BRIDGE_VERSION = '0.5.2'
@@ -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 — save the one the worker fetched for the navigation it answers,
145
- * which is the page that hydrates (45.23). 41.30 — SAVE ONE THE WORKER KEEPS, whose early calls are
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
- hydrated
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
  }