@abide/abide 0.111.0 → 0.113.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 +24 -0
- package/docs/REGISTRY.md +14 -14
- package/docs/RULEBOOK.md +35 -6
- package/docs/abide.md +54 -48
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +5 -0
- package/src/compiler/appLayout.ts +16 -1
- package/src/compiler/classifyTemplate.ts +134 -39
- package/src/compiler/emitClient.ts +173 -16
- package/src/compiler/renderMarkup.ts +14 -8
- package/src/server/commands.ts +2 -1
- package/src/server/config.ts +13 -1
- package/src/server/createApp.ts +18 -6
- package/src/server/preload.ts +5 -0
- package/src/server/principal.ts +14 -0
- package/src/server/render.ts +2 -2
- package/src/shared/ASCII_WHITESPACE.ts +6 -0
- package/src/shared/FOREIGN_ATTRIBUTES.ts +24 -0
- package/src/shared/INTEGRATION_POINTS.ts +20 -0
- package/src/shared/IN_PROCESS_ORIGIN.ts +5 -0
- package/src/shared/reactive/Keyed.ts +41 -14
- package/src/shared/reactive/Reactive.ts +34 -2
- package/src/shared/rpc.ts +2 -1
- package/src/ui/Level.ts +43 -95
- package/src/ui/Router.ts +12 -13
- package/src/ui/bindGroup.ts +41 -0
- package/src/ui/bindProperty.ts +1 -21
- package/src/ui/bindSpread.ts +4 -2
- package/src/ui/index.ts +7 -5
- package/src/ui/parseMarkup.ts +20 -25
- package/src/ui/reconcileHead.ts +215 -0
- package/src/ui/setAttribute.ts +25 -1
- package/src/ui/setClassGroup.ts +79 -0
- package/src/ui/setStyleGroup.ts +130 -0
- package/src/ui/applyHead.ts +0 -214
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @abide/abide
|
|
2
2
|
|
|
3
|
+
## 0.113.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3c32bf6: a Store may watch — each change it reports re-reads get without running the body, and the watch stops when the value is released ([`a5539eb`](https://github.com/briancray/abide/commit/a5539eb9d1673a3b4dd98c0e35cc8d8dfb8c5e96))
|
|
8
|
+
- 3c32bf6: App.run takes a principal — the claims its scope resolves from through onPrincipal, with no cookie written ([`c0db93c`](https://github.com/briancray/abide/commit/c0db93cb8f41533137a7cb3f833627bebbe9d65f))
|
|
9
|
+
- 3c32bf6: under bun test with the test preload, an app that declares no ABIDE_PRINCIPAL_SECRET signs in with one the preload supplies ([`db09b9d`](https://github.com/briancray/abide/commit/db09b9d3aceeffe3d385ae295601ca9158dc2b86))
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 3c32bf6: an element the client builds takes the namespace the parser gives it there — a component inside an svg draws — and an xlink:href it writes is XLink's; the harness counts createElementNS and setAttributeNS ([`0dd560a`](https://github.com/briancray/abide/commit/0dd560a35d54a86d8ff5d3bc7762ed6c6968c71f))
|
|
14
|
+
- 3c32bf6: a Keyed holds no entry for args its gate refuses, and the gate runs once per entry rather than on every refresh ([`1560749`](https://github.com/briancray/abide/commit/1560749419b1c571130c812fb0f6c3e2bac2a021))
|
|
15
|
+
- 3c32bf6: abide check takes a url literal naming one of the framework's own fetched addresses — openapi, mcp, health, principal ([`c8af3dc`](https://github.com/briancray/abide/commit/c8af3dc746766fc5cbe322dbde551b15137c7330))
|
|
16
|
+
|
|
17
|
+
## 0.112.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- cabe8d1: a class or style attribute beside class: and style: directives keeps moving and writes only its own classes and declarations, and an element where a bind: and something else both write its value is a build error ([`bbbfd81`](https://github.com/briancray/abide/commit/bbbfd810d817235ddb51b8c605756187e473719e))
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- cabe8d1: one writer owns document.head — a keyless head node stands in its contributors' order, an equal one another contributor made stays when the first leaves, and a page's head leaves with its layout's slot ([`f21f687`](https://github.com/briancray/abide/commit/f21f68742efb463f7873a6aafc4482fd572363e7))
|
|
26
|
+
|
|
3
27
|
## 0.111.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/docs/REGISTRY.md
CHANGED
|
@@ -68,7 +68,7 @@ Accepted, Failures>` is the inner value's own `Accepted`. Then `Failures`, then
|
|
|
68
68
|
| `reactive` | `<Accepted = undefined, Shaped = Accepted, Gate = Schema<Accepted>>(initial: undefined, options?: Omit<ReactiveOptions<Accepted, Kept<Shaped>, never>, 'schema'> & { transform?: (value: Accepted) => Shaped; schema?: Gate & Schema<Accepted> }) => Reactive<Kept<Shaped>, Accepted, Refused<Shaped> \| Gated<Gate, Accepted>>` | The undefined overload, for a value opening at `undefined`. | 4.15, 4.16 |
|
|
69
69
|
| `reactive` | `<Accepted>() => Reactive<Kept<Accepted>, Accepted, Refused<Accepted>>` | The empty overload, for a value nothing has written yet. | 4.14 |
|
|
70
70
|
| `Transformer` | `<Accepted, Stored, Failures = never>(value: Accepted) => Stored \| Failures` | The shape of a `transform`. | 4.6, 4.7, 4.8 |
|
|
71
|
-
| `Store` | `{ get: () => Stored \| undefined \| Promise<Stored \| undefined>; set: (value: Stored, retention: { ttl: number }) => void \| Promise<void
|
|
71
|
+
| `Store` | `{ get: () => Stored \| undefined \| Promise<Stored \| undefined>; set: (value: Stored, retention: { ttl: number }) => void \| Promise<void>; watch?: (changed: () => void) => () => void }` | Where a value lives when this process does not. | 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 8.10, 8.11, 8.12, 8.13, 8.14, 8.15, 8.16, 8.17, 8.18, 8.19, 8.20, 8.21 |
|
|
72
72
|
| `Shared` | `interface Shared {}` | The app's declaration-merged registry of shared keys. | 10.3 |
|
|
73
73
|
| `reactive.share` | `<Key extends string, Value>(key: Key, create: () => Reactive<Value>) => Key extends keyof Shared ? Reactive<Shared[Key]> : Reactive<Value>` | Get-or-create a reactive value in the component's scope by key. | 10.1, 10.2, 10.4, 10.5, 10.6, 10.7 |
|
|
74
74
|
|
|
@@ -94,7 +94,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
94
94
|
| `structural` | `(next: unknown, previous: unknown) => boolean` | The by-value comparator an app declares on `identity`. | 5.4, 5.5, 18.4 |
|
|
95
95
|
| `by` | `(keyof Element & string) \| ((element: Element) => unknown)`, over the `Element` a `Stored` array holds | What tells one element of the value from another. | 5.22, 18.4, 18.15, 32.32 |
|
|
96
96
|
| `ttl` | `number` | The life of a production, or of a keyed entry. | 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 6.10, 11.6, 11.35, 11.45, 11.48, 11.49, 11.69 |
|
|
97
|
-
| `args` | `Schema<Args>` | The gate on a keyed value's args. | 9.4, 11.28, 11.30, 11.40, 11.41, 11.56, 11.60, 11.68 |
|
|
97
|
+
| `args` | `Schema<Args>` | The gate on a keyed value's args. | 9.4, 11.28, 11.30, 11.40, 11.41, 11.56, 11.60, 11.68, 11.72 |
|
|
98
98
|
| `global` | `boolean` | Opts out of the default scope into one cache shared by every caller in the process. | 11.34, 11.35, 11.36, 11.37, 11.38, 11.39, 11.53, 11.67 |
|
|
99
99
|
| `tags` | `string[] \| ((args: Args) => string[])` | What a `Selection` matches on. | 11.43, 11.44, 18.4, 18.15 |
|
|
100
100
|
| `throttle` | `number` | A ceiling on how often the value changes. | 5.8, 5.9, 5.11, 5.12, 5.14, 5.15, 5.17, 5.18, 5.19, 5.20, 5.21, 5.23, 5.24 |
|
|
@@ -529,18 +529,18 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
529
529
|
| `{expr}` | Reactive text, escaped. | 32.1, 32.23 |
|
|
530
530
|
| formatting whitespace | What a run of whitespace renders as. | 33.17, 33.18 |
|
|
531
531
|
| `{raw(...)}` | Raw HTML. | 32.2, 32.25, 32.26, 32.30, 32.31, 32.70 |
|
|
532
|
-
| `name={expr}` | A reactive attribute or property. | 32.3, 32.20, 32.23, 32.37 |
|
|
532
|
+
| `name={expr}` | A reactive attribute or property. | 32.3, 32.20, 32.23, 32.37, 32.70, 32.71 |
|
|
533
533
|
| `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | 32.4 |
|
|
534
|
-
| `name="…{expr}…"` | An interpolated quoted value. | 32.3 |
|
|
535
|
-
| `bind:value` | Two-way bind on an element. | 5.16, 32.5, 32.58, 32.59, 32.60, 32.61, 32.62 |
|
|
534
|
+
| `name="…{expr}…"` | An interpolated quoted value. | 32.3, 32.71 |
|
|
535
|
+
| `bind:value` | Two-way bind on an element. | 5.16, 32.5, 32.58, 32.59, 32.60, 32.61, 32.62, 32.72 |
|
|
536
536
|
| `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. | 32.6, 33.6, 33.8, 14.18 |
|
|
537
|
-
| `bind:checked` | A boolean bind on an input. | 32.7 |
|
|
538
|
-
| `bind:open` | The same, on a `<details>`. | 32.7 |
|
|
539
|
-
| `bind:group` | Radio or checkbox membership. | 32.8 |
|
|
537
|
+
| `bind:checked` | A boolean bind on an input. | 32.7, 32.72 |
|
|
538
|
+
| `bind:open` | The same, on a `<details>`. | 32.7, 32.72 |
|
|
539
|
+
| `bind:group` | Radio or checkbox membership. | 32.8, 32.72 |
|
|
540
540
|
| `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | 32.5, 32.6 |
|
|
541
541
|
| `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | 32.9, 32.38, 32.66, 32.68, 32.69 |
|
|
542
|
-
| `class:name={cond}` | Toggles a class. | 32.10 |
|
|
543
|
-
| `style:prop={value}` | Sets one style property. | 32.10, 32.19 |
|
|
542
|
+
| `class:name={cond}` | Toggles a class. | 32.10, 32.71 |
|
|
543
|
+
| `style:prop={value}` | Sets one style property. | 32.10, 32.19, 32.71 |
|
|
544
544
|
| `{...expr}` | Spreads props or attributes. | 32.11, 32.37, 33.12, 33.13 |
|
|
545
545
|
| static template text | What an author writes outside a slot, in body text and in an attribute value. | 32.27, 32.28 |
|
|
546
546
|
|
|
@@ -564,10 +564,10 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
564
564
|
| Spelling | Meaning | Rules |
|
|
565
565
|
| --- | --- | --- |
|
|
566
566
|
| `{#component Name(pattern)}` | An inline component. | 32.39, 32.40, 32.41, 32.42, 32.43, 32.44, 32.45, 32.46, 32.47, 32.48, 32.49, 32.50, 32.51, 32.52, 32.53, 32.54, 32.55, 32.63, 32.64 |
|
|
567
|
-
| `<Name/>` | A component invocation. | 32.17, 32.34, 32.35, 32.36, 33.19, 33.21, 41.24, 41.28 |
|
|
567
|
+
| `<Name/>` | A component invocation. | 32.17, 32.34, 32.35, 32.36, 32.70, 33.19, 33.21, 41.24, 41.28 |
|
|
568
568
|
| `<slot/>` | The position children render into. | 32.18 |
|
|
569
569
|
| `<slot>fallback</slot>` | Renders children, or a fallback. | 32.18 |
|
|
570
|
-
| `<Tag>…</Tag>` | Children passed to a component's slot. | 32.18 |
|
|
570
|
+
| `<Tag>…</Tag>` | Children passed to a component's slot. | 32.18, 32.70 |
|
|
571
571
|
|
|
572
572
|
## Props
|
|
573
573
|
|
|
@@ -629,7 +629,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
629
629
|
| `APP_NAME` | `string` | The app's name, and `log`'s default namespace. | 27.3, 28.4 |
|
|
630
630
|
| `APP_VERSION` | `string` | The version beside that name. | 27.3 |
|
|
631
631
|
| `APP_DATA_DIR` | `string` | The platform's per-user data directory. | 27.3, 36.4 |
|
|
632
|
-
| `ABIDE_PRINCIPAL_SECRET` | `string \| null` | What seals the principal cookie. | 26.11, 36.5 |
|
|
632
|
+
| `ABIDE_PRINCIPAL_SECRET` | `string \| null` | What seals the principal cookie. | 26.11, 36.5, 38.120 |
|
|
633
633
|
| `ABIDE_PRINCIPAL_TTL` | `number` | The principal cookie's life, in ms. | 26.15, 26.16 |
|
|
634
634
|
| `ABIDE_APP_TOKEN` | `string \| null` | The bearer the remote CLI sends. | 36.6 |
|
|
635
635
|
| `ABIDE_APP_URL` | `string \| null` | Names an app somewhere else, for the remote CLI. | 36.6 |
|
|
@@ -706,7 +706,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
706
706
|
| `createApp` | `() => Promise<App>` | The app built and booted, with no socket bound. | 42.1, 42.2, 42.3, 42.4, 42.16, 37.4, 37.6, 38.100 |
|
|
707
707
|
| `App` | `interface App` | What a host, a test or a script holds the app as. | 42.12, 42.13 |
|
|
708
708
|
| `App.fetch` | `(request: Request) => Promise<Response>` | The whole request path, entered in process. | 42.5, 42.6 |
|
|
709
|
-
| `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request }) => Promise<Result>` | One scope around a function, and nothing else. | 42.7, 42.8, 11.38 |
|
|
709
|
+
| `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request; principal?: Record<string, unknown> }) => Promise<Result>` | One scope around a function, and nothing else. | 42.7, 42.8, 42.19, 11.38 |
|
|
710
710
|
| `App.listen` | `(options?: { port?: number; hostname?: string; unix?: string }) => Promise<void>` | The socket, bound. | 42.9, 16.23, 16.56, 18.28, 18.29, 18.30 |
|
|
711
711
|
| `App.stop` | `() => Promise<void>` | The drain and the teardown. | 42.10, 42.11, 42.15, 42.17, 37.4 |
|
|
712
712
|
| `App[Symbol.asyncDispose]` | `() => Promise<void>` | `App.stop`, under the name `await using` calls. | 42.14 |
|
package/docs/RULEBOOK.md
CHANGED
|
@@ -413,6 +413,12 @@ the value of that write standing in its place.
|
|
|
413
413
|
|
|
414
414
|
8.19 A `Store`'s `get` answering a miss with nothing left to serve MUST land an error on `s.error`.
|
|
415
415
|
|
|
416
|
+
8.20 A `Store`'s `watch` MUST be called once, where the `Reactive` is constructed, and each call of
|
|
417
|
+
the function it is handed MUST run `get` again as a restore, per 8.18, and MUST NOT run the body.
|
|
418
|
+
|
|
419
|
+
8.21 What a `Store`'s `watch` returns MUST be called once the `Reactive` is released: a `Keyed`
|
|
420
|
+
entry evicted, or the setup or request that built it, or its `Keyed`, ended.
|
|
421
|
+
|
|
416
422
|
# 9. Streams and wired values
|
|
417
423
|
|
|
418
424
|
9.1 A streaming producer's `Stored` MUST be the chunks accumulated so far, materialized on the
|
|
@@ -641,6 +647,9 @@ start again when the load ends.
|
|
|
641
647
|
11.71 An unkeyed `reactive` with a `store`, built on a server outside every request, MUST NOT run
|
|
642
648
|
the `store`'s `get` before it is reached.
|
|
643
649
|
|
|
650
|
+
11.72 A `Keyed` MUST NOT hold an entry for an `Args` its `args` gate refuses, and the gate MUST run
|
|
651
|
+
once per entry.
|
|
652
|
+
|
|
644
653
|
# 12. Effects
|
|
645
654
|
|
|
646
655
|
12.1 A `Disposer` MUST run before each rerun of its `Effect`, and once more at teardown.
|
|
@@ -1725,8 +1734,8 @@ the page shown, and MUST NOT where only its query or fragment does.
|
|
|
1725
1734
|
30.25 A `navigate` that does not remount the page MUST NOT move the scroll, except to the element a
|
|
1726
1735
|
fragment it carries names.
|
|
1727
1736
|
|
|
1728
|
-
30.26 `abide check` MUST refuse a `url` literal that no page route of the app (23.3) is filed under
|
|
1729
|
-
|
|
1737
|
+
30.26 `abide check` MUST refuse a `url` literal that no page route of the app (23.3) is filed under,
|
|
1738
|
+
no public file of the app is served at, and that is none of the four addresses 21.4 names.
|
|
1730
1739
|
|
|
1731
1740
|
30.27 A `navigate` whose request fails, or whose route's chunks fail to load, MUST leave the address
|
|
1732
1741
|
to the document, save where 30.32 holds.
|
|
@@ -2001,7 +2010,18 @@ reading layout, querying its descendants or calling `focus()` acts on a connecte
|
|
|
2001
2010
|
32.69 `bind:element` on a component MUST be a build error.
|
|
2002
2011
|
|
|
2003
2012
|
32.70 Markup the client parses for a slot holding a value branded by `raw`, a `{#if}` arm or a
|
|
2004
|
-
`{#for}` row MUST be parsed as the document's parser parses it inside the element it goes into
|
|
2013
|
+
`{#for}` row MUST be parsed as the document's parser parses it inside the element it goes into, and
|
|
2014
|
+
an element the client builds — a component's own, or children a caller hands one — MUST take the
|
|
2015
|
+
namespace the document's parser gives it there, as MUST an attribute the client writes on it.
|
|
2016
|
+
|
|
2017
|
+
32.71 A `class` or `style` attribute beside a `class:name` or `style:prop` MUST keep moving (32.3)
|
|
2018
|
+
and MUST write only its own classes or declarations; a class either of them holds MUST stay on the
|
|
2019
|
+
element, and a style property MUST resolve as the server's merged attribute does — the attribute's
|
|
2020
|
+
declarations first, then each directive's in the tag's order.
|
|
2021
|
+
|
|
2022
|
+
32.72 An element MUST be a build error where a `bind:` and something else both write its value: the
|
|
2023
|
+
attribute a `bind:name` names, `checked` beside `bind:group`, an `<option selected>` inside a
|
|
2024
|
+
`<select bind:value>`, or text inside a `<textarea bind:value>`.
|
|
2005
2025
|
|
|
2006
2026
|
# 33. Props
|
|
2007
2027
|
|
|
@@ -2663,6 +2683,10 @@ it, MUST hydrate it there with that component's browser module, and MUST run eve
|
|
|
2663
2683
|
hydration woke before it settles; it MUST throw where the component was not imported from a
|
|
2664
2684
|
`.abide` and bound, or where the process has no DOM.
|
|
2665
2685
|
|
|
2686
|
+
38.120 Under `bun test` with `@abide/abide/test` preloaded, an app that declares no
|
|
2687
|
+
`ABIDE_PRINCIPAL_SECRET` MUST seal and verify the principal cookie with a secret the preload
|
|
2688
|
+
supplies, and `abide start` MUST NOT supply that secret.
|
|
2689
|
+
|
|
2666
2690
|
# 39. Head and view transitions
|
|
2667
2691
|
|
|
2668
2692
|
39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
|
|
@@ -2674,8 +2698,9 @@ hydration woke before it settles; it MUST throw where the component was not impo
|
|
|
2674
2698
|
39.4 `<head>` contributions MUST be merged by key, and the deepest contributor MUST win. The shell's
|
|
2675
2699
|
`<head>` MUST be the outermost contributor.
|
|
2676
2700
|
|
|
2677
|
-
39.5 A `<head>` contribution with no merge key MUST append
|
|
2678
|
-
|
|
2701
|
+
39.5 A `<head>` contribution with no merge key MUST append, and MUST stand in its contributors'
|
|
2702
|
+
order — each level's own, then its components' in the order they mounted, outermost level first — a
|
|
2703
|
+
component mounted after its level included.
|
|
2679
2704
|
|
|
2680
2705
|
39.6 On a client navigation the incoming page's keyed `<head>` contributions MUST replace the
|
|
2681
2706
|
outgoing page's by key, and a layout's MUST NOT move.
|
|
@@ -2709,7 +2734,7 @@ contributor has that key.
|
|
|
2709
2734
|
MUST replace, in place, the one that same contributor wrote before.
|
|
2710
2735
|
|
|
2711
2736
|
39.16 A `<head>` contribution with no merge key MUST be removed when the page, layout or component
|
|
2712
|
-
that made it leaves.
|
|
2737
|
+
that made it leaves, and an equal one another contributor made MUST stay.
|
|
2713
2738
|
|
|
2714
2739
|
39.17 A `<script>` a `<head>` contribution writes in a browser MUST run when it is written, as the
|
|
2715
2740
|
one a server paints does, and MUST NOT run again while the same contributor writes it unchanged.
|
|
@@ -2980,6 +3005,10 @@ connection still answering, and MUST then run `onStop` without waiting on what i
|
|
|
2980
3005
|
|
|
2981
3006
|
42.18 `ABIDE_STOP_TIMEOUT` MUST default to 5000.
|
|
2982
3007
|
|
|
3008
|
+
42.19 A `principal` on `App.run` MUST be the claims its scope's `principal` resolves from, through
|
|
3009
|
+
`onPrincipal` as a sealed cookie's are, MUST NOT write the principal cookie, and MUST give the scope
|
|
3010
|
+
a request where `request` is not given.
|
|
3011
|
+
|
|
2983
3012
|
# 43. The measurement harness
|
|
2984
3013
|
|
|
2985
3014
|
43.1 `@abide/harness/measure`, `@abide/harness/engine` and `@abide/harness/server` MUST each be
|
package/docs/abide.md
CHANGED
|
@@ -2382,6 +2382,8 @@ test('the view picked is the view the next visit opens on', async () => {
|
|
|
2382
2382
|
|
|
2383
2383
|
A `Store` is a `get` and a `set`. The value reads the store when it is built and falls back to `'board'` on a miss, and every change is written back. The reads and writes in the markup do not change.
|
|
2384
2384
|
|
|
2385
|
+
Where the stored value can change from outside the app, such as a file another program edits, the store adds `watch`. It is handed a function to call when that happens, and returns the function that stops watching. The value reads `get` again, and the body does not run.
|
|
2386
|
+
|
|
2385
2387
|
This store goes through two handlers, so the server and the browser read the same preference and the first paint is already right. A store over `localStorage` works too, but only in a browser, so the server paints the fallback first. After a write, `set` invalidates the read it made, so the next read asks the server rather than this tab's copy.
|
|
2386
2388
|
|
|
2387
2389
|
## Next
|
|
@@ -3718,7 +3720,6 @@ export default POST(() => principal.clear(), { description: 'Signs out.' })
|
|
|
3718
3720
|
import { expect, test } from 'bun:test'
|
|
3719
3721
|
import { principal } from '@abide/abide'
|
|
3720
3722
|
import { createApp } from '@abide/abide/server'
|
|
3721
|
-
import { asMember } from './signedIn.ts'
|
|
3722
3723
|
|
|
3723
3724
|
const signIn = (email: string) =>
|
|
3724
3725
|
new Request('http://crm.invalid/account/signIn', {
|
|
@@ -3727,17 +3728,28 @@ const signIn = (email: string) =>
|
|
|
3727
3728
|
body: JSON.stringify({ email }),
|
|
3728
3729
|
})
|
|
3729
3730
|
|
|
3730
|
-
test('signing in seals the
|
|
3731
|
+
test('signing in seals the id into an HttpOnly cookie', async () => {
|
|
3731
3732
|
await using app = await createApp()
|
|
3732
3733
|
const answer = await app.fetch(signIn('maya@crm.example'))
|
|
3733
3734
|
const [sealed = ''] = answer.headers.getSetCookie()
|
|
3734
3735
|
expect(sealed).toStartWith('abide-principal=')
|
|
3735
3736
|
expect(sealed).toContain('HttpOnly')
|
|
3737
|
+
|
|
3738
|
+
// The seal is a JWT; its payload is the id, and the times abide adds.
|
|
3739
|
+
const token = sealed.split(';')[0]?.split('=')[1] ?? ''
|
|
3740
|
+
const payload = JSON.parse(
|
|
3741
|
+
Buffer.from(token.split('.')[1] ?? '', 'base64url').toString(),
|
|
3742
|
+
)
|
|
3743
|
+
expect(Object.keys(payload).sort()).toEqual(['exp', 'iat', 'id'])
|
|
3736
3744
|
})
|
|
3737
3745
|
|
|
3738
3746
|
test('a request carrying the cookie is signed in', async () => {
|
|
3739
3747
|
await using app = await createApp()
|
|
3740
|
-
const
|
|
3748
|
+
const answer = await app.fetch(signIn('maya@crm.example'))
|
|
3749
|
+
const [sealed = ''] = answer.headers.getSetCookie()
|
|
3750
|
+
const request = new Request('http://crm.invalid/', {
|
|
3751
|
+
headers: { cookie: sealed.split(';')[0] ?? '' },
|
|
3752
|
+
})
|
|
3741
3753
|
const signedIn = await app.run(async () => await principal.authenticated, {
|
|
3742
3754
|
request,
|
|
3743
3755
|
})
|
|
@@ -3749,7 +3761,7 @@ test('a request carrying the cookie is signed in', async () => {
|
|
|
3749
3761
|
|
|
3750
3762
|
The answer also tells the browser the caller changed. Every tab of the site reads `principal` again, so the form's own button flips with nothing passed to it.
|
|
3751
3763
|
|
|
3752
|
-
The seal is a JWT signed with `ABIDE_PRINCIPAL_SECRET`, and it lives for `ABIDE_PRINCIPAL_TTL` milliseconds, a week unless you set it. The chapter app keeps a development secret in its `.env`, so set your own wherever the app is deployed. A comma-separated list rotates a secret in: the first signs, and every one verifies.
|
|
3764
|
+
The seal is a JWT signed with `ABIDE_PRINCIPAL_SECRET`, and it lives for `ABIDE_PRINCIPAL_TTL` milliseconds, a week unless you set it. The chapter app keeps a development secret in its `.env`, so set your own wherever the app is deployed. A comma-separated list rotates a secret in: the first signs, and every one verifies. Under `bun test` an app that sets no secret is given one, so a test signs in from a fresh clone.
|
|
3753
3765
|
|
|
3754
3766
|
| Read | What it is |
|
|
3755
3767
|
| --- | --- |
|
|
@@ -3808,21 +3820,11 @@ onPrincipal((claims) => {
|
|
|
3808
3820
|
import { expect, test } from 'bun:test'
|
|
3809
3821
|
import { principal } from '@abide/abide'
|
|
3810
3822
|
import { createApp } from '@abide/abide/server'
|
|
3811
|
-
import { asMember } from './signedIn.ts'
|
|
3812
3823
|
|
|
3813
|
-
test('the
|
|
3824
|
+
test('the claims resolve to the member they name', async () => {
|
|
3814
3825
|
await using app = await createApp()
|
|
3815
|
-
const request = await asMember(app, 'theo@crm.example')
|
|
3816
|
-
|
|
3817
|
-
// The seal is a JWT; its payload is the id, and the times abide adds.
|
|
3818
|
-
const sealed = request.headers.get('cookie') ?? ''
|
|
3819
|
-
const payload = JSON.parse(
|
|
3820
|
-
Buffer.from(sealed.split('.')[1] ?? '', 'base64url').toString(),
|
|
3821
|
-
)
|
|
3822
|
-
expect(Object.keys(payload).sort()).toEqual(['exp', 'iat', 'id'])
|
|
3823
|
-
|
|
3824
3826
|
const member = await app.run(async () => await principal.resolved, {
|
|
3825
|
-
|
|
3827
|
+
principal: { id: 'theo' },
|
|
3826
3828
|
})
|
|
3827
3829
|
expect(member.name).toBe('Theo Park')
|
|
3828
3830
|
expect(member.role).toBe('sales')
|
|
@@ -3831,6 +3833,8 @@ test('the cookie holds an id, and the request reads the member', async () => {
|
|
|
3831
3833
|
|
|
3832
3834
|
`onPrincipal` is handed the claims and returns your half of the caller. It runs on the server, at most once per request, where the team records are. So the cookie stays small, and a rename shows on the next request rather than at the next sign-in. Filed in any handler file or `.abide`, it resolves the caller for every route.
|
|
3833
3835
|
|
|
3836
|
+
A test signs in without the cookie. `app.run(fn, { principal: { id: 'theo' } })` hands those claims to `onPrincipal` as a sealed cookie's are, and the card's test reads the member they name.
|
|
3837
|
+
|
|
3834
3838
|
What it returns is read as `principal.resolved`, typed by `Principal`. Declare your fields on that interface, as the app's `lib/TEAM.server.ts` does beside the team it reads. abide's own three fields are `authenticated`, `expiresAt` and `error`, and they win any collision, so a resolver cannot sign a caller in by returning `authenticated: true`. A resolver that throws fills `error` and leaves the caller signed out.
|
|
3835
3839
|
|
|
3836
3840
|
The tabs on this card, the deal-value card and the export card are `lib/SignInAs.abide`, which calls the same `signIn` handler.
|
|
@@ -3934,19 +3938,20 @@ export default GET(
|
|
|
3934
3938
|
import { expect, test } from 'bun:test'
|
|
3935
3939
|
import { createApp } from '@abide/abide/server'
|
|
3936
3940
|
import getContact from '../getContact.ts'
|
|
3937
|
-
import { asMember } from './signedIn.ts'
|
|
3938
3941
|
|
|
3939
3942
|
test('an admin reads the deal value', async () => {
|
|
3940
3943
|
await using app = await createApp()
|
|
3941
|
-
const
|
|
3942
|
-
|
|
3944
|
+
const contact = await app.run(async () => await getContact(), {
|
|
3945
|
+
principal: { id: 'maya' },
|
|
3946
|
+
})
|
|
3943
3947
|
expect(contact.dealValue).toBe('$48,000')
|
|
3944
3948
|
})
|
|
3945
3949
|
|
|
3946
3950
|
test('sales reads the rest of the contact', async () => {
|
|
3947
3951
|
await using app = await createApp()
|
|
3948
|
-
const
|
|
3949
|
-
|
|
3952
|
+
const contact = await app.run(async () => await getContact(), {
|
|
3953
|
+
principal: { id: 'theo' },
|
|
3954
|
+
})
|
|
3950
3955
|
expect(contact.name).toBe('Grace Hopper')
|
|
3951
3956
|
expect(contact.dealValue).toBeUndefined()
|
|
3952
3957
|
})
|
|
@@ -4009,23 +4014,24 @@ export default POST(async () => CONTACTS, {
|
|
|
4009
4014
|
import { expect, test } from 'bun:test'
|
|
4010
4015
|
import { createApp } from '@abide/abide/server'
|
|
4011
4016
|
import exportContacts from '../exportContacts.ts'
|
|
4012
|
-
import { asMember } from './signedIn.ts'
|
|
4013
4017
|
|
|
4014
4018
|
test('an admin exports every contact', async () => {
|
|
4015
4019
|
await using app = await createApp()
|
|
4016
|
-
const request = await asMember(app, 'maya@crm.example')
|
|
4017
4020
|
const contacts = await app.run(async () => await exportContacts(), {
|
|
4018
|
-
|
|
4021
|
+
principal: { id: 'maya' },
|
|
4019
4022
|
})
|
|
4020
4023
|
expect(contacts).toHaveLength(3)
|
|
4021
4024
|
})
|
|
4022
4025
|
|
|
4023
4026
|
test('sales and a signed-out caller are refused 403', async () => {
|
|
4024
4027
|
await using app = await createApp()
|
|
4025
|
-
|
|
4026
|
-
|
|
4028
|
+
const callers = [
|
|
4029
|
+
{ principal: { id: 'theo' } },
|
|
4030
|
+
{ request: new Request('http://crm.invalid/') },
|
|
4031
|
+
]
|
|
4032
|
+
for (const caller of callers) {
|
|
4027
4033
|
try {
|
|
4028
|
-
await app.run(async () => await exportContacts(),
|
|
4034
|
+
await app.run(async () => await exportContacts(), caller)
|
|
4029
4035
|
throw new Error('the export was not refused')
|
|
4030
4036
|
} catch (refusal) {
|
|
4031
4037
|
if (!exportContacts.isError(refusal, 'HttpError')) throw refusal
|
|
@@ -4961,13 +4967,13 @@ A handler file already carries an address, a method, an argument type and a desc
|
|
|
4961
4967
|
|
|
4962
4968
|
```abide src/assistant/lib/OpenApiOperation.abide
|
|
4963
4969
|
<script>
|
|
4964
|
-
import { reactive,
|
|
4970
|
+
import { reactive, url } from '@abide/abide'
|
|
4965
4971
|
|
|
4966
4972
|
type Operation = { description: string; parameters: object[] }
|
|
4967
4973
|
let operation = reactive('')
|
|
4968
4974
|
|
|
4969
4975
|
async function readDocument() {
|
|
4970
|
-
const answer = await fetch(url('/__abide/openapi.json'
|
|
4976
|
+
const answer = await fetch(url('/__abide/openapi.json'))
|
|
4971
4977
|
const document: { paths: Record<string, { get: Operation }> } =
|
|
4972
4978
|
await answer.json()
|
|
4973
4979
|
for (const [path, { get }] of Object.entries(document.paths))
|
|
@@ -5046,7 +5052,7 @@ abide openapi > openapi.json
|
|
|
5046
5052
|
|
|
5047
5053
|
```abide src/assistant/lib/ModelToolCall.abide
|
|
5048
5054
|
<script>
|
|
5049
|
-
import { reactive,
|
|
5055
|
+
import { reactive, url } from '@abide/abide'
|
|
5050
5056
|
|
|
5051
5057
|
let tool = reactive('—')
|
|
5052
5058
|
let description = reactive('—')
|
|
@@ -5054,7 +5060,7 @@ let answer = reactive('—')
|
|
|
5054
5060
|
|
|
5055
5061
|
// One JSON-RPC message to the app's MCP endpoint, as a model's client sends it.
|
|
5056
5062
|
async function ask(method: string, params: object) {
|
|
5057
|
-
const reply = await fetch(url('/__abide/mcp'
|
|
5063
|
+
const reply = await fetch(url('/__abide/mcp'), {
|
|
5058
5064
|
method: 'POST',
|
|
5059
5065
|
headers: { 'content-type': 'application/json' },
|
|
5060
5066
|
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
|
@@ -5147,14 +5153,14 @@ abide mcp --url https://crm.example
|
|
|
5147
5153
|
|
|
5148
5154
|
```abide src/assistant/lib/WithheldExport.abide
|
|
5149
5155
|
<script>
|
|
5150
|
-
import { reactive,
|
|
5156
|
+
import { reactive, url } from '@abide/abide'
|
|
5151
5157
|
import exportContacts from '../exportContacts.ts'
|
|
5152
5158
|
|
|
5153
5159
|
let listed = reactive('—')
|
|
5154
5160
|
let answers = reactive('—')
|
|
5155
5161
|
|
|
5156
5162
|
async function check() {
|
|
5157
|
-
const reply = await fetch(url('/__abide/mcp'
|
|
5163
|
+
const reply = await fetch(url('/__abide/mcp'), {
|
|
5158
5164
|
method: 'POST',
|
|
5159
5165
|
headers: { 'content-type': 'application/json' },
|
|
5160
5166
|
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
|
|
@@ -5743,7 +5749,7 @@ test('the summary answers in process as it does on the wire', async () => {
|
|
|
5743
5749
|
| --- | --- |
|
|
5744
5750
|
| `App` | what `createApp` hands back, to a test, a script or a host |
|
|
5745
5751
|
| `App.fetch` | one request, through the whole app, in this process |
|
|
5746
|
-
| `App.run` | runs a function inside one request's scope, so two reads of one key load once |
|
|
5752
|
+
| `App.run` | runs a function inside one request's scope, so two reads of one key load once, signed in as the `principal` claims you give it |
|
|
5747
5753
|
| `App.listen` | binds the socket, for a test that needs a real `WebSocket` |
|
|
5748
5754
|
| `App.stop` | drains what is in flight, then runs `onStop` |
|
|
5749
5755
|
| `App[Symbol.asyncDispose]` | `stop`, under the name `await using` calls |
|
|
@@ -6827,7 +6833,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6827
6833
|
|
|
6828
6834
|
| Name | Signature | Meaning | Rules |
|
|
6829
6835
|
| --- | --- | --- | --- |
|
|
6830
|
-
| `Store` | `{ get: () => Stored \| undefined \| Promise<Stored \| undefined>; set: (value: Stored, retention: { ttl: number }) => void \| Promise<void
|
|
6836
|
+
| `Store` | `{ get: () => Stored \| undefined \| Promise<Stored \| undefined>; set: (value: Stored, retention: { ttl: number }) => void \| Promise<void>; watch?: (changed: () => void) => () => void }` | Where a value lives when this process does not. | [8.1](/rulebook#8.1), [8.2](/rulebook#8.2), [8.3](/rulebook#8.3), [8.4](/rulebook#8.4), [8.5](/rulebook#8.5), [8.6](/rulebook#8.6), [8.7](/rulebook#8.7), [8.8](/rulebook#8.8), [8.9](/rulebook#8.9), [8.10](/rulebook#8.10), [8.11](/rulebook#8.11), [8.12](/rulebook#8.12), [8.13](/rulebook#8.13), [8.14](/rulebook#8.14), [8.15](/rulebook#8.15), [8.16](/rulebook#8.16), [8.17](/rulebook#8.17), [8.18](/rulebook#8.18), [8.19](/rulebook#8.19), [8.20](/rulebook#8.20), [8.21](/rulebook#8.21) |
|
|
6831
6837
|
|
|
6832
6838
|
## Taught in
|
|
6833
6839
|
|
|
@@ -6869,7 +6875,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6869
6875
|
| `identity` | `(next: Stored, previous: Stored) => unknown` | What makes it the same value. | [5.1](/rulebook#5.1), [5.2](/rulebook#5.2), [5.3](/rulebook#5.3), [5.4](/rulebook#5.4), [5.5](/rulebook#5.5), [9.3](/rulebook#9.3), [11.17](/rulebook#11.17), [18.4](/rulebook#18.4) |
|
|
6870
6876
|
| `by` | `(keyof Element & string) \| ((element: Element) => unknown)`, over the `Element` a `Stored` array holds | What tells one element of the value from another. | [5.22](/rulebook#5.22), [18.4](/rulebook#18.4), [18.15](/rulebook#18.15), [32.32](/rulebook#32.32) |
|
|
6871
6877
|
| `ttl` | `number` | The life of a production, or of a keyed entry. | [6.1](/rulebook#6.1), [6.2](/rulebook#6.2), [6.3](/rulebook#6.3), [6.4](/rulebook#6.4), [6.5](/rulebook#6.5), [6.6](/rulebook#6.6), [6.7](/rulebook#6.7), [6.8](/rulebook#6.8), [6.9](/rulebook#6.9), [6.10](/rulebook#6.10), [11.6](/rulebook#11.6), [11.35](/rulebook#11.35), [11.45](/rulebook#11.45), [11.48](/rulebook#11.48), [11.49](/rulebook#11.49), [11.69](/rulebook#11.69) |
|
|
6872
|
-
| `args` | `Schema<Args>` | The gate on a keyed value's args. | [9.4](/rulebook#9.4), [11.28](/rulebook#11.28), [11.30](/rulebook#11.30), [11.40](/rulebook#11.40), [11.41](/rulebook#11.41), [11.56](/rulebook#11.56), [11.60](/rulebook#11.60), [11.68](/rulebook#11.68) |
|
|
6878
|
+
| `args` | `Schema<Args>` | The gate on a keyed value's args. | [9.4](/rulebook#9.4), [11.28](/rulebook#11.28), [11.30](/rulebook#11.30), [11.40](/rulebook#11.40), [11.41](/rulebook#11.41), [11.56](/rulebook#11.56), [11.60](/rulebook#11.60), [11.68](/rulebook#11.68), [11.72](/rulebook#11.72) |
|
|
6873
6879
|
| `global` | `boolean` | Opts out of the default scope into one cache shared by every caller in the process. | [11.34](/rulebook#11.34), [11.35](/rulebook#11.35), [11.36](/rulebook#11.36), [11.37](/rulebook#11.37), [11.38](/rulebook#11.38), [11.39](/rulebook#11.39), [11.53](/rulebook#11.53), [11.67](/rulebook#11.67) |
|
|
6874
6880
|
| `tags` | `string[] \| ((args: Args) => string[])` | What a `Selection` matches on. | [11.43](/rulebook#11.43), [11.44](/rulebook#11.44), [18.4](/rulebook#18.4), [18.15](/rulebook#18.15) |
|
|
6875
6881
|
| `throttle` | `number` | A ceiling on how often the value changes. | [5.8](/rulebook#5.8), [5.9](/rulebook#5.9), [5.11](/rulebook#5.11), [5.12](/rulebook#5.12), [5.14](/rulebook#5.14), [5.15](/rulebook#5.15), [5.17](/rulebook#5.17), [5.18](/rulebook#5.18), [5.19](/rulebook#5.19), [5.20](/rulebook#5.20), [5.21](/rulebook#5.21), [5.23](/rulebook#5.23), [5.24](/rulebook#5.24) |
|
|
@@ -8628,18 +8634,18 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8628
8634
|
| `{expr}` | Reactive text, escaped. | [32.1](/rulebook#32.1), [32.23](/rulebook#32.23) |
|
|
8629
8635
|
| formatting whitespace | What a run of whitespace renders as. | [33.17](/rulebook#33.17), [33.18](/rulebook#33.18) |
|
|
8630
8636
|
| `{raw(...)}` | Raw HTML. | [32.2](/rulebook#32.2), [32.25](/rulebook#32.25), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70) |
|
|
8631
|
-
| `name={expr}` | A reactive attribute or property. | [32.3](/rulebook#32.3), [32.20](/rulebook#32.20), [32.23](/rulebook#32.23), [32.37](/rulebook#32.37) |
|
|
8637
|
+
| `name={expr}` | A reactive attribute or property. | [32.3](/rulebook#32.3), [32.20](/rulebook#32.20), [32.23](/rulebook#32.23), [32.37](/rulebook#32.37), [32.70](/rulebook#32.70), [32.71](/rulebook#32.71) |
|
|
8632
8638
|
| `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | [32.4](/rulebook#32.4) |
|
|
8633
|
-
| `name="…{expr}…"` | An interpolated quoted value. | [32.3](/rulebook#32.3) |
|
|
8634
|
-
| `bind:value` | Two-way bind on an element. | [5.16](/rulebook#5.16), [32.5](/rulebook#32.5), [32.58](/rulebook#32.58), [32.59](/rulebook#32.59), [32.60](/rulebook#32.60), [32.61](/rulebook#32.61), [32.62](/rulebook#32.62) |
|
|
8639
|
+
| `name="…{expr}…"` | An interpolated quoted value. | [32.3](/rulebook#32.3), [32.71](/rulebook#32.71) |
|
|
8640
|
+
| `bind:value` | Two-way bind on an element. | [5.16](/rulebook#5.16), [32.5](/rulebook#32.5), [32.58](/rulebook#32.58), [32.59](/rulebook#32.59), [32.60](/rulebook#32.60), [32.61](/rulebook#32.61), [32.62](/rulebook#32.62), [32.72](/rulebook#32.72) |
|
|
8635
8641
|
| `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. | [32.6](/rulebook#32.6), [33.6](/rulebook#33.6), [33.8](/rulebook#33.8), [14.18](/rulebook#14.18) |
|
|
8636
|
-
| `bind:checked` | A boolean bind on an input. | [32.7](/rulebook#32.7) |
|
|
8637
|
-
| `bind:open` | The same, on a `<details>`. | [32.7](/rulebook#32.7) |
|
|
8638
|
-
| `bind:group` | Radio or checkbox membership. | [32.8](/rulebook#32.8) |
|
|
8642
|
+
| `bind:checked` | A boolean bind on an input. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72) |
|
|
8643
|
+
| `bind:open` | The same, on a `<details>`. | [32.7](/rulebook#32.7), [32.72](/rulebook#32.72) |
|
|
8644
|
+
| `bind:group` | Radio or checkbox membership. | [32.8](/rulebook#32.8), [32.72](/rulebook#32.72) |
|
|
8639
8645
|
| `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | [32.5](/rulebook#32.5), [32.6](/rulebook#32.6) |
|
|
8640
8646
|
| `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | [32.9](/rulebook#32.9), [32.38](/rulebook#32.38), [32.66](/rulebook#32.66), [32.68](/rulebook#32.68), [32.69](/rulebook#32.69) |
|
|
8641
|
-
| `class:name={cond}` | Toggles a class. | [32.10](/rulebook#32.10) |
|
|
8642
|
-
| `style:prop={value}` | Sets one style property. | [32.10](/rulebook#32.10), [32.19](/rulebook#32.19) |
|
|
8647
|
+
| `class:name={cond}` | Toggles a class. | [32.10](/rulebook#32.10), [32.71](/rulebook#32.71) |
|
|
8648
|
+
| `style:prop={value}` | Sets one style property. | [32.10](/rulebook#32.10), [32.19](/rulebook#32.19), [32.71](/rulebook#32.71) |
|
|
8643
8649
|
| `{...expr}` | Spreads props or attributes. | [32.11](/rulebook#32.11), [32.37](/rulebook#32.37), [33.12](/rulebook#33.12), [33.13](/rulebook#33.13) |
|
|
8644
8650
|
| static template text | What an author writes outside a slot, in body text and in an attribute value. | [32.27](/rulebook#32.27), [32.28](/rulebook#32.28) |
|
|
8645
8651
|
|
|
@@ -8656,10 +8662,10 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8656
8662
|
| Spelling | Meaning | Rules |
|
|
8657
8663
|
| --- | --- | --- |
|
|
8658
8664
|
| `{#component Name(pattern)}` | An inline component. | [32.39](/rulebook#32.39), [32.40](/rulebook#32.40), [32.41](/rulebook#32.41), [32.42](/rulebook#32.42), [32.43](/rulebook#32.43), [32.44](/rulebook#32.44), [32.45](/rulebook#32.45), [32.46](/rulebook#32.46), [32.47](/rulebook#32.47), [32.48](/rulebook#32.48), [32.49](/rulebook#32.49), [32.50](/rulebook#32.50), [32.51](/rulebook#32.51), [32.52](/rulebook#32.52), [32.53](/rulebook#32.53), [32.54](/rulebook#32.54), [32.55](/rulebook#32.55), [32.63](/rulebook#32.63), [32.64](/rulebook#32.64) |
|
|
8659
|
-
| `<Name/>` | A component invocation. | [32.17](/rulebook#32.17), [32.34](/rulebook#32.34), [32.35](/rulebook#32.35), [32.36](/rulebook#32.36), [33.19](/rulebook#33.19), [33.21](/rulebook#33.21), [41.24](/rulebook#41.24), [41.28](/rulebook#41.28) |
|
|
8665
|
+
| `<Name/>` | A component invocation. | [32.17](/rulebook#32.17), [32.34](/rulebook#32.34), [32.35](/rulebook#32.35), [32.36](/rulebook#32.36), [32.70](/rulebook#32.70), [33.19](/rulebook#33.19), [33.21](/rulebook#33.21), [41.24](/rulebook#41.24), [41.28](/rulebook#41.28) |
|
|
8660
8666
|
| `<slot/>` | The position children render into. | [32.18](/rulebook#32.18) |
|
|
8661
8667
|
| `<slot>fallback</slot>` | Renders children, or a fallback. | [32.18](/rulebook#32.18) |
|
|
8662
|
-
| `<Tag>…</Tag>` | Children passed to a component's slot. | [32.18](/rulebook#32.18) |
|
|
8668
|
+
| `<Tag>…</Tag>` | Children passed to a component's slot. | [32.18](/rulebook#32.18), [32.70](/rulebook#32.70) |
|
|
8663
8669
|
|
|
8664
8670
|
## Script / style blocks
|
|
8665
8671
|
|
|
@@ -8837,7 +8843,7 @@ Configuration: what is written in place rather than imported.
|
|
|
8837
8843
|
| `APP_NAME` | `string` | The app's name, and `log`'s default namespace. | [27.3](/rulebook#27.3), [28.4](/rulebook#28.4) |
|
|
8838
8844
|
| `APP_VERSION` | `string` | The version beside that name. | [27.3](/rulebook#27.3) |
|
|
8839
8845
|
| `APP_DATA_DIR` | `string` | The platform's per-user data directory. | [27.3](/rulebook#27.3), [36.4](/rulebook#36.4) |
|
|
8840
|
-
| `ABIDE_PRINCIPAL_SECRET` | `string \| null` | What seals the principal cookie. | [26.11](/rulebook#26.11), [36.5](/rulebook#36.5) |
|
|
8846
|
+
| `ABIDE_PRINCIPAL_SECRET` | `string \| null` | What seals the principal cookie. | [26.11](/rulebook#26.11), [36.5](/rulebook#36.5), [38.120](/rulebook#38.120) |
|
|
8841
8847
|
| `ABIDE_PRINCIPAL_TTL` | `number` | The principal cookie's life, in ms. | [26.15](/rulebook#26.15), [26.16](/rulebook#26.16) |
|
|
8842
8848
|
| `ABIDE_APP_TOKEN` | `string \| null` | The bearer the remote CLI sends. | [36.6](/rulebook#36.6) |
|
|
8843
8849
|
| `ABIDE_APP_URL` | `string \| null` | Names an app somewhere else, for the remote CLI. | [36.6](/rulebook#36.6) |
|
|
@@ -9177,7 +9183,7 @@ Configuration: what is written in place rather than imported.
|
|
|
9177
9183
|
| --- | --- | --- | --- |
|
|
9178
9184
|
| `App` | `interface App` | What a host, a test or a script holds the app as. | [42.12](/rulebook#42.12), [42.13](/rulebook#42.13) |
|
|
9179
9185
|
| `App.fetch` | `(request: Request) => Promise<Response>` | The whole request path, entered in process. | [42.5](/rulebook#42.5), [42.6](/rulebook#42.6) |
|
|
9180
|
-
| `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request }) => Promise<Result>` | One scope around a function, and nothing else. | [42.7](/rulebook#42.7), [42.8](/rulebook#42.8), [11.38](/rulebook#11.38) |
|
|
9186
|
+
| `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request; principal?: Record<string, unknown> }) => Promise<Result>` | One scope around a function, and nothing else. | [42.7](/rulebook#42.7), [42.8](/rulebook#42.8), [42.19](/rulebook#42.19), [11.38](/rulebook#11.38) |
|
|
9181
9187
|
| `App.listen` | `(options?: { port?: number; hostname?: string; unix?: string }) => Promise<void>` | The socket, bound. | [42.9](/rulebook#42.9), [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [18.28](/rulebook#18.28), [18.29](/rulebook#18.29), [18.30](/rulebook#18.30) |
|
|
9182
9188
|
| `App.stop` | `() => Promise<void>` | The drain and the teardown. | [42.10](/rulebook#42.10), [42.11](/rulebook#42.11), [42.15](/rulebook#42.15), [42.17](/rulebook#42.17), [37.4](/rulebook#37.4) |
|
|
9183
9189
|
| `App[Symbol.asyncDispose]` | `() => Promise<void>` | `App.stop`, under the name `await using` calls. | [42.14](/rulebook#42.14) |
|
package/package.json
CHANGED
package/src/compiler/Lowering.ts
CHANGED
|
@@ -222,6 +222,11 @@ export class Lowering {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/** 32.67 — whether a position reads any row's item. */
|
|
226
|
+
readsAnItem(holder: object): boolean {
|
|
227
|
+
return this.holdsRead(holder, (read) => read.item !== -1)
|
|
228
|
+
}
|
|
229
|
+
|
|
225
230
|
/** 32.67 — whether a position reads `block`'s item. */
|
|
226
231
|
readsItem(holder: object, block: object): boolean {
|
|
227
232
|
const item = this.itemOf(block)
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { relative as relativeTo } from 'node:path' // bun exposes no path helpers of its own
|
|
2
2
|
import { appFiles } from '#server/appTree.ts'
|
|
3
3
|
import { type Placement, placement, routeOf } from '#server/placement.ts'
|
|
4
|
+
import {
|
|
5
|
+
HEALTH_ADDRESS,
|
|
6
|
+
MCP_ADDRESS,
|
|
7
|
+
OPENAPI_ADDRESS,
|
|
8
|
+
PRINCIPAL_ADDRESS,
|
|
9
|
+
} from '#shared/ADDRESSES.ts'
|
|
4
10
|
import { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
|
|
5
11
|
import {
|
|
6
12
|
coversRoute,
|
|
@@ -76,7 +82,7 @@ export type AppLayout = {
|
|
|
76
82
|
errors: AppRoute[]
|
|
77
83
|
/** Every public file's source path to its address. 36.11. */
|
|
78
84
|
published: Map<string, string>
|
|
79
|
-
/** 30.26 — every address a link may name: a page's,
|
|
85
|
+
/** 30.26 — every address a link may name: a page's, a public file's, and 21.4's four. */
|
|
80
86
|
addresses: string[]
|
|
81
87
|
/** What the tree refuses, each at its file; a build stops naming every one. */
|
|
82
88
|
refused: Diagnosis[]
|
|
@@ -150,6 +156,7 @@ export async function appLayout(root: string): Promise<AppLayout> {
|
|
|
150
156
|
const addresses = new Set<string>()
|
|
151
157
|
for (const route of routes) addresses.add(route.pattern)
|
|
152
158
|
for (const address of published.values()) addresses.add(address)
|
|
159
|
+
for (const address of FETCHED_ADDRESSES) addresses.add(address)
|
|
153
160
|
return {
|
|
154
161
|
root,
|
|
155
162
|
sources,
|
|
@@ -165,6 +172,14 @@ export async function appLayout(root: string): Promise<AppLayout> {
|
|
|
165
172
|
}
|
|
166
173
|
}
|
|
167
174
|
|
|
175
|
+
// 30.26, 21.4 — the framework's own addresses an app fetches, which every app answers at.
|
|
176
|
+
const FETCHED_ADDRESSES = [
|
|
177
|
+
OPENAPI_ADDRESS,
|
|
178
|
+
MCP_ADDRESS,
|
|
179
|
+
HEALTH_ADDRESS,
|
|
180
|
+
PRINCIPAL_ADDRESS,
|
|
181
|
+
]
|
|
182
|
+
|
|
168
183
|
/** The file at each of `paths`, read. The reads overlap. */
|
|
169
184
|
function textsAt(
|
|
170
185
|
paths: readonly string[],
|