@abide/abide 0.116.3 → 0.117.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.117.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3bb5cc9: `abide dev`, `build`, `start`, `check`, `routes`, `compile`, `bundle`, `openapi`, `mcp` and the shell now refuse to run on a bun outside abide's `engines.bun`, naming the bun running and the range it needs (RULEBOOK 38.123)
8
+
9
+ ### Patch Changes
10
+
11
+ - 3bb5cc9: a component whose markup reads a bare `busy()` or `ready()` is now built on the server after the turn, once the siblings after it have started their loads, so it paints the arm the browser chooses at hydration rather than one that is rebuilt (RULEBOOK 41.28, 41.31)
12
+ - 3bb5cc9: the rulebook now states that the effects one turn wakes, a slot's own update among them, run in no guaranteed order (12.15) — a `watch` that post-processes what a sibling `{raw(...)}` slot renders cannot count on the slot having updated first
13
+ - 3bb5cc9: markdown imported `with { type: 'text' }` into an app's `.ts` now checks as a `string` under `abide check`, as it already did in a `.abide` — no more hand-written `declare module '*.md'`
14
+ - 3bb5cc9: the `bunfig.toml` `abide scaffold` writes now sets `[install] peer = false`, so a dependency's peer on `bun` (`bun-plugin-tailwind`'s) cannot install an older bun that shadows the system one inside `bun run` (RULEBOOK 38.122)
15
+ - 3bb5cc9: the rulebook states three things that held but were never written down: a `raw` slot writes its markup unchecked (32.77), a fragment-only step moves `route.url` (30.33), and a `refresh` over `tags` reruns no synchronous derivation that carries none of them (13.20) — and the registry says `route.params` is `Params` outside a page or layout
16
+
17
+ ## 0.116.4
18
+
19
+ ### Patch Changes
20
+
21
+ - f4acbfd: a handler whose args schema is derived from its type annotation (no `args` gate) now refuses bad args in process as it does over the wire — a direct call, a call from another handler or `app.run`, and every call under `mount` answer 422 `ValidationError` with the same `Issues` before the body runs, where before the body ran with the bad value
22
+ - be88738: a reader that probed a handler before its first call wakes when the call starts — on a server and under `mount`, `disabled={save.busy()}` read before any call stayed enabled for the whole write, because the unreached answer (0.116.3) subscribed the reader to nothing. It now subscribes the reader to the first call in its scope, and still builds nothing and runs no handler (RULEBOOK 3.3, 13.19)
23
+
3
24
  ## 0.116.3
4
25
 
5
26
  ### Patch Changes
package/bin/abide.ts CHANGED
@@ -41,12 +41,17 @@ import { executableName } from '#server/executableName.ts'
41
41
  import { iconAccent } from '#server/iconAccent.ts'
42
42
  import { namesUnder } from '#server/namesUnder.ts'
43
43
  import { DEFAULT_DOCUMENT, intoHead } from '#server/render.ts'
44
+ import { runtimeRefusal } from '#server/runtimeRefusal.ts'
44
45
  import { pathsUnder, serveBuild } from '#server/serveBuild.ts'
45
46
  import { serveDev } from '#server/serveDev.ts'
46
47
  import { escapeText } from '#shared/escapeText.ts'
47
48
  import { ICON_ADDRESSES, LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
48
49
  import abidePackage from '../package.json' with { type: 'json' }
49
50
 
51
+ // 38.123 — the bun this process runs on, held to abide's `engines.bun` before anything is built or
52
+ // booted. A compiled app carries the bun `abide compile` ran on, which this held already.
53
+ const RUNTIME_REFUSAL = runtimeRefusal(Bun.version, abidePackage.engines.bun)
54
+
50
55
  /** Where the app a command builds or serves is, and where its build goes — both absolute. */
51
56
  type AppDirectories = { root: string; out: string }
52
57
 
@@ -59,14 +64,18 @@ const DISK: Host = {
59
64
  version: abidePackage.version,
60
65
  // 38.100 — built in this process rather than a child, where nobody built it, and said on
61
66
  // stderr: the command's stdout is its own — `openapi`'s document, `mcp`'s messages.
62
- app: ({ values }) =>
63
- appOnDisk(directoriesOf(values), async (root, out) =>
67
+ app: ({ values }) => {
68
+ const handle = appOnDisk(directoriesOf(values), async (root, out) =>
64
69
  (await serveBuild(root, out, (root, out) =>
65
70
  writeBuild({ root, out }, console.error),
66
71
  ))
67
72
  ? out
68
73
  : undefined,
69
- ),
74
+ )
75
+ return RUNTIME_REFUSAL === undefined
76
+ ? handle
77
+ : { ...handle, boot: async () => RUNTIME_REFUSAL }
78
+ },
70
79
  source: {
71
80
  scaffold: scaffoldCommand,
72
81
  dev: inApp(dev),
@@ -645,6 +654,10 @@ function inApp<Answer>(
645
654
  body: (app: AppDirectories, given: Given) => Promise<Answer>,
646
655
  ): (given: Given) => Promise<Answer | number> {
647
656
  return async (given) => {
657
+ if (RUNTIME_REFUSAL !== undefined) {
658
+ console.error(`abide ${given.positionals[0]}: ${RUNTIME_REFUSAL}`)
659
+ return 1
660
+ }
648
661
  const app = directoriesOf(given.values)
649
662
  if (app === undefined) {
650
663
  console.error(
package/docs/REGISTRY.md CHANGED
@@ -160,7 +160,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
160
160
  | --- | --- | --- | --- |
161
161
  | `Disposer` | `() => void` | What tears down the previous run. | 12.1 |
162
162
  | `Effect` | `() => void \| Disposer` | What runs on change. | 12.2, 12.8, 14.6 |
163
- | `watch` | `(effect: Effect) => () => void` | Begins a watch over whatever the effect reads. | 12.2, 12.4, 12.5, 12.6, 12.7, 12.10, 12.13 |
163
+ | `watch` | `(effect: Effect) => () => void` | Begins a watch over whatever the effect reads. | 12.2, 12.4, 12.5, 12.6, 12.7, 12.10, 12.13, 12.15 |
164
164
  | `watch` | `(sources: Reactive<unknown, never, unknown, unknown> \| Reactive<unknown, never, unknown, unknown>[], effect: Effect) => () => void` | The narrowed form, over named sources. | 12.3 |
165
165
  | `flushSync` | `() => void` | Ends the turn: every `Effect` a write woke runs now. | 9.10, 12.11, 12.12 |
166
166
 
@@ -169,10 +169,10 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
169
169
  | Name | Signature | Meaning | Rules |
170
170
  | --- | --- | --- | --- |
171
171
  | `Selection` | `Reactive<any, any, any, any> \| Keyed<any, any, any, any> \| Rpc<any, any, any, any> \| { tags: string[] }` | What is being asked about. | 13.1, 13.8, 13.9, 13.13, 31.14 |
172
- | `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | 13.2, 13.3, 13.5, 13.12, 46.11 |
173
- | `ready` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection has a value to serve. | 13.3, 13.5, 13.11, 13.12 |
172
+ | `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | 13.2, 13.3, 13.5, 13.12, 41.28, 41.31, 46.11 |
173
+ | `ready` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection has a value to serve. | 13.3, 13.5, 13.11, 13.12, 41.28, 41.31 |
174
174
  | `synced` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection holds what its producer made. | 13.12, 13.18 |
175
- | `refresh` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Stale-while-revalidate over the selection. | 13.4, 13.7, 13.10, 13.12, 13.14, 13.15, 13.16, 13.17 |
175
+ | `refresh` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Stale-while-revalidate over the selection. | 13.4, 13.7, 13.10, 13.12, 13.14, 13.15, 13.16, 13.17, 13.20 |
176
176
  | `invalidate` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Marks the selection stale. | 13.4, 13.6, 13.10, 13.12, 13.15, 13.17 |
177
177
 
178
178
 
@@ -380,8 +380,8 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
380
380
 
381
381
  | Name | Signature | Meaning | Rules |
382
382
  | --- | --- | --- | --- |
383
- | `route.url` | `Reactive<URL>` | Where we are, fragment included. | 23.1, 23.2, 23.3, 23.4, 30.20, 30.21, 30.23, 19.16, 31.16 |
384
- | `route.params` | `Reactive<Params>` | The matched route's segments. | 23.1, 23.5, 23.9, 30.23, 31.16 |
383
+ | `route.url` | `Reactive<URL>` | Where we are, fragment included. | 23.1, 23.2, 23.3, 23.4, 30.20, 30.21, 30.23, 30.33, 19.16, 31.16 |
384
+ | `route.params` | `Reactive<Params>` | The matched route's segments — typed by its own route in a page or a layout, and `Params` in any other file. | 23.1, 23.5, 23.9, 30.23, 31.16 |
385
385
  | `Params` | `Record<string, string \| string[]>` | What a matched route's segments are. | 23.5 |
386
386
  | `route.name` | `Reactive<string>` | The resolution path, the same under any mount path. | 23.1, 23.6, 23.14, 31.16 |
387
387
  | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | 23.1, 31.16 |
@@ -571,7 +571,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
571
571
 
572
572
  | Name | Signature | Meaning | Rules |
573
573
  | --- | --- | --- | --- |
574
- | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | 32.2, 32.24, 32.26, 32.30, 32.31, 32.70 |
574
+ | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | 32.2, 32.24, 32.26, 32.30, 32.31, 32.70, 32.77 |
575
575
  | `Raw` | `class Raw { html: string }` | What `raw` hands back. | 32.24 |
576
576
 
577
577
  ### Control flow
@@ -629,7 +629,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
629
629
  | Name | Signature | Meaning | Rules |
630
630
  | --- | --- | --- | --- |
631
631
  | `Component` | `interface Component {}` | A component bound to its props. | 32.17, 34.16, 35.4 |
632
- | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | 29.10, 35.4, 35.5, 35.6, 35.15, 35.16, 41.1, 41.2, 41.3, 41.4, 41.9, 41.13, 41.16, 41.17, 41.18, 41.19, 41.20, 41.28, 35.11, 41.29, 41.30 |
632
+ | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | 29.10, 35.4, 35.5, 35.6, 35.15, 35.16, 41.1, 41.2, 41.3, 41.4, 41.9, 41.13, 41.16, 41.17, 41.18, 41.19, 41.20, 41.28, 35.11, 41.29, 41.30, 41.31 |
633
633
 
634
634
  ## Sinks
635
635
 
@@ -849,20 +849,20 @@ rows a runner prints, and an adapter for an abide app.
849
849
 
850
850
  | Command | Meaning | Rules |
851
851
  | --- | --- | --- |
852
- | `abide scaffold <name>` | Writes a starter project. | 38.1, 38.42, 38.103, 38.104, 38.106, 38.112 |
852
+ | `abide scaffold <name>` | Writes a starter project. | 38.1, 38.42, 38.103, 38.104, 38.106, 38.112, 38.122 |
853
853
  | `abide run <file> [args…]` | Runs a script under the abide runtime. | 38.2, 38.57 |
854
- | `abide check [dir…]` | The type-checker over a `.abide` tree. | 38.3, 38.31, 38.32, 38.33, 38.40, 38.105 |
855
- | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | 38.108 |
856
- | `abide dev [--port <n>]` | Watches the project and keeps the app up. | 16.23, 16.56, 37.8, 37.9, 38.4, 38.14, 38.15, 38.16, 38.17, 38.19, 38.24, 38.25, 38.26, 38.28, 38.30, 38.35, 38.36, 38.37, 38.38, 38.41, 38.43, 38.58, 38.63, 38.64, 38.65, 38.94, 38.96, 38.97 |
857
- | `abide build` | Builds the client into content-hashed chunks and a manifest. | 38.43, 38.5, 38.18, 38.25, 38.26, 38.27, 38.30, 38.66, 38.94 |
858
- | `abide start [--port <n>]` | Boots the built app. | 37.8, 37.10, 37.11, 37.12, 38.6, 38.100, 38.20, 38.21, 38.22, 38.23, 38.25, 38.26, 38.27, 38.28, 38.29, 38.58, 38.67, 38.68, 38.95, 38.97, 38.110 |
859
- | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | 37.8, 37.10, 37.11, 37.12, 38.6, 38.7, 38.53, 38.54, 38.55, 38.58, 38.59, 38.60, 38.62 |
854
+ | `abide check [dir…]` | The type-checker over a `.abide` tree. | 38.3, 38.31, 38.32, 38.33, 38.40, 38.105, 38.123 |
855
+ | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | 38.108, 38.123 |
856
+ | `abide dev [--port <n>]` | Watches the project and keeps the app up. | 16.23, 16.56, 37.8, 37.9, 38.4, 38.14, 38.15, 38.16, 38.17, 38.19, 38.24, 38.25, 38.26, 38.28, 38.30, 38.35, 38.36, 38.37, 38.38, 38.41, 38.43, 38.58, 38.63, 38.64, 38.65, 38.94, 38.96, 38.97, 38.123 |
857
+ | `abide build` | Builds the client into content-hashed chunks and a manifest. | 38.43, 38.5, 38.18, 38.25, 38.26, 38.27, 38.30, 38.66, 38.94, 38.123 |
858
+ | `abide start [--port <n>]` | Boots the built app. | 37.8, 37.10, 37.11, 37.12, 38.6, 38.100, 38.20, 38.21, 38.22, 38.23, 38.25, 38.26, 38.27, 38.28, 38.29, 38.58, 38.67, 38.68, 38.95, 38.97, 38.110, 38.123 |
859
+ | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | 37.8, 37.10, 37.11, 37.12, 38.6, 38.7, 38.53, 38.54, 38.55, 38.58, 38.59, 38.60, 38.62, 38.123 |
860
860
  | `abide call <address> [args]` | One call to one handler. | 38.8, 38.61, 38.110 |
861
- | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. | 38.9, 38.25, 38.110, 38.111 |
862
- | `abide mcp [--url <origin>]` | The app's MCP server over stdio. | 38.9, 38.13, 38.25, 38.110 |
861
+ | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. | 38.9, 38.25, 38.110, 38.111, 38.123 |
862
+ | `abide mcp [--url <origin>]` | The app's MCP server over stdio. | 38.9, 38.13, 38.25, 38.110, 38.123 |
863
863
  | `abide watch <address> [args]` | A live subscription to one of the app's followed `GET`s. | 38.69 |
864
- | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | 37.8, 37.10, 37.11, 37.12, 38.10, 38.25, 38.26, 38.44, 38.45, 38.46, 38.47, 38.48, 38.49, 38.50, 38.51, 38.52, 38.56, 38.93, 38.97 |
865
- | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | 38.10, 38.25, 38.26, 38.70, 38.71, 38.72, 38.73, 38.74, 38.75, 38.76, 38.77, 38.78, 38.79, 38.80, 38.81, 38.82, 38.83, 38.84, 38.85, 38.86, 38.87, 38.88, 38.89, 38.90, 38.91, 38.92, 38.101, 38.102 |
864
+ | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | 37.8, 37.10, 37.11, 37.12, 38.10, 38.25, 38.26, 38.44, 38.45, 38.46, 38.47, 38.48, 38.49, 38.50, 38.51, 38.52, 38.56, 38.93, 38.97, 38.123 |
865
+ | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | 38.10, 38.25, 38.26, 38.70, 38.71, 38.72, 38.73, 38.74, 38.75, 38.76, 38.77, 38.78, 38.79, 38.80, 38.81, 38.82, 38.83, 38.84, 38.85, 38.86, 38.87, 38.88, 38.89, 38.90, 38.91, 38.92, 38.101, 38.102, 38.123 |
866
866
  | `abide lsp` | The `.abide` language server, over stdio. | 38.11, 38.34, 38.39, 38.98, 38.116, 38.117, 38.118 |
867
867
  | `--help` · `-h` · `abide help` | Usage, generated from the command list. | 38.12, 38.113, 38.114 |
868
868
  | `--version` · `-v` | The version of abide, or of the app a compiled executable holds. | 38.115 |
package/docs/RULEBOOK.md CHANGED
@@ -700,6 +700,9 @@ holding that arm or row is removed.
700
700
  `watch`, MUST stop reading its sources once that owner stops and nothing else reads it, and MUST
701
701
  answer a later read with what its body computes then.
702
702
 
703
+ 12.15 The `Effect` values one turn wakes, a slot's own update among them, MAY run in any order,
704
+ save where a clause orders them.
705
+
703
706
  # 13. Selections
704
707
 
705
708
  13.1 A `Selection` MUST be one `Reactive`, one `Keyed`, one `Rpc`, or a set of `tags`.
@@ -754,7 +757,11 @@ mutation's `declare` built while its write is in flight.
754
757
  where every value it sweeps is.
755
758
 
756
759
  13.19 `busy`, `ready`, `synced`, `refresh` and `invalidate` over an `Rpc` no call has reached in the
757
- current scope MUST answer as over a `Selection` with no entries.
760
+ current scope MUST answer as over a `Selection` with no entries, and reading `busy`, `ready` or
761
+ `synced` there MUST subscribe the reader to the first call that reaches it in that scope.
762
+
763
+ 13.20 `refresh` over a set of `tags` MUST NOT rerun a synchronous derivation that carries none of
764
+ them, which MUST recompute from the load it reads.
758
765
 
759
766
  # 14. Tracking
760
767
 
@@ -1818,6 +1825,9 @@ to `middleware`.
1818
1825
  30.32 A `navigate` that is not a history step, whose request gets no answer while `online` is false,
1819
1826
  MUST leave the page and the address where they stand, save where 45.12 holds.
1820
1827
 
1828
+ 30.33 A history step under 30.20, and a `navigate` under 30.21, MUST move `route.url` to the address
1829
+ it steps to.
1830
+
1821
1831
  # 31. Reading and writing by name
1822
1832
 
1823
1833
  31.1 Inside a `.abide` file a name bound to a `Reactive` MUST be read wherever 31.3 does not hold it.
@@ -2098,6 +2108,9 @@ element, written or spread, runs for the same event, wherever the two are writte
2098
2108
  32.76 A `bind:` whose operand is a path into a value that takes an optional step (`?.`) MUST be a
2099
2109
  build error.
2100
2110
 
2111
+ 32.77 A slot holding a value branded by `raw` MUST write its markup as given, with no element or
2112
+ attribute in it removed and no URL in it scheme-checked.
2113
+
2101
2114
  # 33. Props
2102
2115
 
2103
2116
  33.1 `props` MUST be callable from a `<script>` block only, and calling it from a `<script module>`
@@ -2765,6 +2778,12 @@ supplies, and `abide start` MUST NOT supply that secret.
2765
2778
  38.121 Under `mount`, a call the component's browser module makes to a private handler MUST be
2766
2779
  answered in the test's process by the handler the component's server module declares.
2767
2780
 
2781
+ 38.122 The project `abide scaffold` writes MUST NOT install a dependency's peer dependencies.
2782
+
2783
+ 38.123 A command `abide` builds, checks or boots the app for MUST be refused where the bun running it
2784
+ is outside the range abide's `engines.bun` names, and the refusal MUST name that bun's version and
2785
+ the range.
2786
+
2768
2787
  # 39. Head and view transitions
2769
2788
 
2770
2789
  39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
@@ -3030,13 +3049,18 @@ has answered or failed.
3030
3049
  adopts the page, however early its answer arrived.
3031
3050
 
3032
3051
  41.28 A `render` MUST let the turn's queued microtasks run between setting up a `<Name/>` whose setup
3033
- made an `Rpc` call and building that component's markup.
3052
+ made an `Rpc` call, or whose markup reads a bare `busy` or `ready`, and building that component's
3053
+ markup.
3034
3054
 
3035
3055
  41.29 A `render` answering a request whose `sec-fetch-dest` is `empty` MUST NOT fill the seed buffer,
3036
3056
  save one whose `abide-keep` header names `open`.
3037
3057
 
3038
3058
  41.30 A `render` answering a request carrying `abide-keep` MUST write 41.20's script.
3039
3059
 
3060
+ 41.31 On a server, a bare `busy` or `ready` MUST answer over the loads set up before its component's
3061
+ markup is built, and a load a component set up later, one only a pending value places, MAY have it
3062
+ choose another arm at hydration under 41.24.
3063
+
3040
3064
  # 42. The app object
3041
3065
 
3042
3066
  42.1 `createApp` MUST build the route table and compose every `Middleware` rung, per 16.14.
package/docs/abide.md CHANGED
@@ -773,7 +773,7 @@ A hole is `{}` around an expression, and it updates the one thing it names and n
773
773
  | You write | Where it lands |
774
774
  | --- | --- |
775
775
  | `{expr}` | a text node, escaped |
776
- | `{raw(...)}` | markup, unescaped: `raw` brands a string as a `Raw` |
776
+ | `{raw(...)}` | markup, unescaped and unchecked (32.77): `raw` brands a string as a `Raw`, so hand it markup you wrote, never a model's reply or a fetched page as it came |
777
777
  | `name={expr}` | one attribute, whole |
778
778
  | `name="…{expr}…"` | one attribute, interpolated |
779
779
  | `class:name={cond}` | one class, there while the condition holds |
@@ -2996,7 +2996,7 @@ test('one file answers for every company, reading its id', async () => {
2996
2996
  | --- | --- |
2997
2997
  | `route.name` | the pattern that matched, `/companies/[id]` for every company, so a log line keys on the page rather than on each address |
2998
2998
  | `Params` | what the segments matched; a query string is never in it |
2999
- | `RouteParams<P>` | the type `route.params` has in a page filed under `P`, so `id` is text and a key the route lacks is an error |
2999
+ | `RouteParams<P>` | the type `route.params` has in a page or layout filed under `P`, so `id` is text and a key the route lacks is an error. A component under `lib/` has no route of its own and reads the loose `Params`: pass it the value as a prop, or narrow it with `RouteParams<'/…'>` |
3000
3000
 
3001
3001
  ## A layout holds what the companies pages share
3002
3002
 
@@ -3888,7 +3888,7 @@ test('the server paints the density the cookie holds', async () => {
3888
3888
 
3889
3889
  `cookies()` is the request's cookies on the server and the document's in the browser, as a `CookieMap`. Reading a cookie by name follows it, so the list changes on the click. The server render reads the same cookie off the request, so a reload paints compact rather than flashing comfortable first.
3890
3890
 
3891
- The principal cookie is `HttpOnly`, so script never sees the seal. Use `cookies()` for the ones that are yours: a density, a theme, a dismissed banner.
3891
+ `cookies()` reads and writes any cookie the request carries: a density, a theme, or another system's session cookie your server checks. The principal cookie is `HttpOnly`, so in the browser `cookies()` never sees the seal.
3892
3892
 
3893
3893
  ## A handler decides which fields a caller may read
3894
3894
 
@@ -7112,7 +7112,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7112
7112
 
7113
7113
  | Name | Signature | Meaning | Rules |
7114
7114
  | --- | --- | --- | --- |
7115
- | `watch` | `(effect: Effect) => () => void` | Begins a watch over whatever the effect reads. | [12.2](/rulebook#12.2), [12.4](/rulebook#12.4), [12.5](/rulebook#12.5), [12.6](/rulebook#12.6), [12.7](/rulebook#12.7), [12.10](/rulebook#12.10), [12.13](/rulebook#12.13) |
7115
+ | `watch` | `(effect: Effect) => () => void` | Begins a watch over whatever the effect reads. | [12.2](/rulebook#12.2), [12.4](/rulebook#12.4), [12.5](/rulebook#12.5), [12.6](/rulebook#12.6), [12.7](/rulebook#12.7), [12.10](/rulebook#12.10), [12.13](/rulebook#12.13), [12.15](/rulebook#12.15) |
7116
7116
  | `watch` | `(sources: Reactive<unknown, never, unknown, unknown> \| Reactive<unknown, never, unknown, unknown>[], effect: Effect) => () => void` | The narrowed form, over named sources. | [12.3](/rulebook#12.3) |
7117
7117
 
7118
7118
  ## Taught in
@@ -7167,7 +7167,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7167
7167
 
7168
7168
  | Name | Signature | Meaning | Rules |
7169
7169
  | --- | --- | --- | --- |
7170
- | `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.12](/rulebook#13.12), [46.11](/rulebook#46.11) |
7170
+ | `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.12](/rulebook#13.12), [41.28](/rulebook#41.28), [41.31](/rulebook#41.31), [46.11](/rulebook#46.11) |
7171
7171
 
7172
7172
  ## Taught in
7173
7173
 
@@ -7185,7 +7185,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7185
7185
 
7186
7186
  | Name | Signature | Meaning | Rules |
7187
7187
  | --- | --- | --- | --- |
7188
- | `ready` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection has a value to serve. | [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.11](/rulebook#13.11), [13.12](/rulebook#13.12) |
7188
+ | `ready` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection has a value to serve. | [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.11](/rulebook#13.11), [13.12](/rulebook#13.12), [41.28](/rulebook#41.28), [41.31](/rulebook#41.31) |
7189
7189
 
7190
7190
  ## Taught in
7191
7191
 
@@ -7221,7 +7221,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7221
7221
 
7222
7222
  | Name | Signature | Meaning | Rules |
7223
7223
  | --- | --- | --- | --- |
7224
- | `refresh` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Stale-while-revalidate over the selection. | [13.4](/rulebook#13.4), [13.7](/rulebook#13.7), [13.10](/rulebook#13.10), [13.12](/rulebook#13.12), [13.14](/rulebook#13.14), [13.15](/rulebook#13.15), [13.16](/rulebook#13.16), [13.17](/rulebook#13.17) |
7224
+ | `refresh` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Stale-while-revalidate over the selection. | [13.4](/rulebook#13.4), [13.7](/rulebook#13.7), [13.10](/rulebook#13.10), [13.12](/rulebook#13.12), [13.14](/rulebook#13.14), [13.15](/rulebook#13.15), [13.16](/rulebook#13.16), [13.17](/rulebook#13.17), [13.20](/rulebook#13.20) |
7225
7225
 
7226
7226
  ## Taught in
7227
7227
 
@@ -8196,8 +8196,8 @@ Generated surfaces: what is written in place rather than imported.
8196
8196
 
8197
8197
  | Name | Signature | Meaning | Rules |
8198
8198
  | --- | --- | --- | --- |
8199
- | `route.url` | `Reactive<URL>` | Where we are, fragment included. | [23.1](/rulebook#23.1), [23.2](/rulebook#23.2), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.23](/rulebook#30.23), [19.16](/rulebook#19.16), [31.16](/rulebook#31.16) |
8200
- | `route.params` | `Reactive<Params>` | The matched route's segments. | [23.1](/rulebook#23.1), [23.5](/rulebook#23.5), [23.9](/rulebook#23.9), [30.23](/rulebook#30.23), [31.16](/rulebook#31.16) |
8199
+ | `route.url` | `Reactive<URL>` | Where we are, fragment included. | [23.1](/rulebook#23.1), [23.2](/rulebook#23.2), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.23](/rulebook#30.23), [30.33](/rulebook#30.33), [19.16](/rulebook#19.16), [31.16](/rulebook#31.16) |
8200
+ | `route.params` | `Reactive<Params>` | The matched route's segments — typed by its own route in a page or a layout, and `Params` in any other file. | [23.1](/rulebook#23.1), [23.5](/rulebook#23.5), [23.9](/rulebook#23.9), [30.23](/rulebook#30.23), [31.16](/rulebook#31.16) |
8201
8201
  | `route.name` | `Reactive<string>` | The resolution path, the same under any mount path. | [23.1](/rulebook#23.1), [23.6](/rulebook#23.6), [23.14](/rulebook#23.14), [31.16](/rulebook#31.16) |
8202
8202
  | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | [23.1](/rulebook#23.1), [31.16](/rulebook#31.16) |
8203
8203
 
@@ -8961,7 +8961,7 @@ Generated surfaces: what is written in place rather than imported.
8961
8961
 
8962
8962
  | Name | Signature | Meaning | Rules |
8963
8963
  | --- | --- | --- | --- |
8964
- | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | [32.2](/rulebook#32.2), [32.24](/rulebook#32.24), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70) |
8964
+ | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | [32.2](/rulebook#32.2), [32.24](/rulebook#32.24), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70), [32.77](/rulebook#32.77) |
8965
8965
 
8966
8966
  ## Taught in
8967
8967
 
@@ -9084,7 +9084,7 @@ Pages and routing: what is written in place rather than imported.
9084
9084
 
9085
9085
  | Name | Signature | Meaning | Rules |
9086
9086
  | --- | --- | --- | --- |
9087
- | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | [29.10](/rulebook#29.10), [35.4](/rulebook#35.4), [35.5](/rulebook#35.5), [35.6](/rulebook#35.6), [35.15](/rulebook#35.15), [35.16](/rulebook#35.16), [41.1](/rulebook#41.1), [41.2](/rulebook#41.2), [41.3](/rulebook#41.3), [41.4](/rulebook#41.4), [41.9](/rulebook#41.9), [41.13](/rulebook#41.13), [41.16](/rulebook#41.16), [41.17](/rulebook#41.17), [41.18](/rulebook#41.18), [41.19](/rulebook#41.19), [41.20](/rulebook#41.20), [41.28](/rulebook#41.28), [35.11](/rulebook#35.11), [41.29](/rulebook#41.29), [41.30](/rulebook#41.30) |
9087
+ | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | [29.10](/rulebook#29.10), [35.4](/rulebook#35.4), [35.5](/rulebook#35.5), [35.6](/rulebook#35.6), [35.15](/rulebook#35.15), [35.16](/rulebook#35.16), [41.1](/rulebook#41.1), [41.2](/rulebook#41.2), [41.3](/rulebook#41.3), [41.4](/rulebook#41.4), [41.9](/rulebook#41.9), [41.13](/rulebook#41.13), [41.16](/rulebook#41.16), [41.17](/rulebook#41.17), [41.18](/rulebook#41.18), [41.19](/rulebook#41.19), [41.20](/rulebook#41.20), [41.28](/rulebook#41.28), [35.11](/rulebook#35.11), [41.29](/rulebook#41.29), [41.30](/rulebook#41.30), [41.31](/rulebook#41.31) |
9088
9088
 
9089
9089
  ## Taught in
9090
9090
 
@@ -10375,20 +10375,20 @@ CLI: what is written in place rather than imported.
10375
10375
 
10376
10376
  | Command | Meaning | Rules |
10377
10377
  | --- | --- | --- |
10378
- | `abide scaffold <name>` | Writes a starter project. | [38.1](/rulebook#38.1), [38.42](/rulebook#38.42), [38.103](/rulebook#38.103), [38.104](/rulebook#38.104), [38.106](/rulebook#38.106), [38.112](/rulebook#38.112) |
10378
+ | `abide scaffold <name>` | Writes a starter project. | [38.1](/rulebook#38.1), [38.42](/rulebook#38.42), [38.103](/rulebook#38.103), [38.104](/rulebook#38.104), [38.106](/rulebook#38.106), [38.112](/rulebook#38.112), [38.122](/rulebook#38.122) |
10379
10379
  | `abide run <file> [args…]` | Runs a script under the abide runtime. | [38.2](/rulebook#38.2), [38.57](/rulebook#38.57) |
10380
- | `abide check [dir…]` | The type-checker over a `.abide` tree. | [38.3](/rulebook#38.3), [38.31](/rulebook#38.31), [38.32](/rulebook#38.32), [38.33](/rulebook#38.33), [38.40](/rulebook#38.40), [38.105](/rulebook#38.105) |
10381
- | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | [38.108](/rulebook#38.108) |
10382
- | `abide dev [--port <n>]` | Watches the project and keeps the app up. | [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [37.8](/rulebook#37.8), [37.9](/rulebook#37.9), [38.4](/rulebook#38.4), [38.14](/rulebook#38.14), [38.15](/rulebook#38.15), [38.16](/rulebook#38.16), [38.17](/rulebook#38.17), [38.19](/rulebook#38.19), [38.24](/rulebook#38.24), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.28](/rulebook#38.28), [38.30](/rulebook#38.30), [38.35](/rulebook#38.35), [38.36](/rulebook#38.36), [38.37](/rulebook#38.37), [38.38](/rulebook#38.38), [38.41](/rulebook#38.41), [38.43](/rulebook#38.43), [38.58](/rulebook#38.58), [38.63](/rulebook#38.63), [38.64](/rulebook#38.64), [38.65](/rulebook#38.65), [38.94](/rulebook#38.94), [38.96](/rulebook#38.96), [38.97](/rulebook#38.97) |
10383
- | `abide build` | Builds the client into content-hashed chunks and a manifest. | [38.43](/rulebook#38.43), [38.5](/rulebook#38.5), [38.18](/rulebook#38.18), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.30](/rulebook#38.30), [38.66](/rulebook#38.66), [38.94](/rulebook#38.94) |
10384
- | `abide start [--port <n>]` | Boots the built app. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.100](/rulebook#38.100), [38.20](/rulebook#38.20), [38.21](/rulebook#38.21), [38.22](/rulebook#38.22), [38.23](/rulebook#38.23), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.28](/rulebook#38.28), [38.29](/rulebook#38.29), [38.58](/rulebook#38.58), [38.67](/rulebook#38.67), [38.68](/rulebook#38.68), [38.95](/rulebook#38.95), [38.97](/rulebook#38.97), [38.110](/rulebook#38.110) |
10385
- | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.7](/rulebook#38.7), [38.53](/rulebook#38.53), [38.54](/rulebook#38.54), [38.55](/rulebook#38.55), [38.58](/rulebook#38.58), [38.59](/rulebook#38.59), [38.60](/rulebook#38.60), [38.62](/rulebook#38.62) |
10380
+ | `abide check [dir…]` | The type-checker over a `.abide` tree. | [38.3](/rulebook#38.3), [38.31](/rulebook#38.31), [38.32](/rulebook#38.32), [38.33](/rulebook#38.33), [38.40](/rulebook#38.40), [38.105](/rulebook#38.105), [38.123](/rulebook#38.123) |
10381
+ | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | [38.108](/rulebook#38.108), [38.123](/rulebook#38.123) |
10382
+ | `abide dev [--port <n>]` | Watches the project and keeps the app up. | [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [37.8](/rulebook#37.8), [37.9](/rulebook#37.9), [38.4](/rulebook#38.4), [38.14](/rulebook#38.14), [38.15](/rulebook#38.15), [38.16](/rulebook#38.16), [38.17](/rulebook#38.17), [38.19](/rulebook#38.19), [38.24](/rulebook#38.24), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.28](/rulebook#38.28), [38.30](/rulebook#38.30), [38.35](/rulebook#38.35), [38.36](/rulebook#38.36), [38.37](/rulebook#38.37), [38.38](/rulebook#38.38), [38.41](/rulebook#38.41), [38.43](/rulebook#38.43), [38.58](/rulebook#38.58), [38.63](/rulebook#38.63), [38.64](/rulebook#38.64), [38.65](/rulebook#38.65), [38.94](/rulebook#38.94), [38.96](/rulebook#38.96), [38.97](/rulebook#38.97), [38.123](/rulebook#38.123) |
10383
+ | `abide build` | Builds the client into content-hashed chunks and a manifest. | [38.43](/rulebook#38.43), [38.5](/rulebook#38.5), [38.18](/rulebook#38.18), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.30](/rulebook#38.30), [38.66](/rulebook#38.66), [38.94](/rulebook#38.94), [38.123](/rulebook#38.123) |
10384
+ | `abide start [--port <n>]` | Boots the built app. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.100](/rulebook#38.100), [38.20](/rulebook#38.20), [38.21](/rulebook#38.21), [38.22](/rulebook#38.22), [38.23](/rulebook#38.23), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.28](/rulebook#38.28), [38.29](/rulebook#38.29), [38.58](/rulebook#38.58), [38.67](/rulebook#38.67), [38.68](/rulebook#38.68), [38.95](/rulebook#38.95), [38.97](/rulebook#38.97), [38.110](/rulebook#38.110), [38.123](/rulebook#38.123) |
10385
+ | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.7](/rulebook#38.7), [38.53](/rulebook#38.53), [38.54](/rulebook#38.54), [38.55](/rulebook#38.55), [38.58](/rulebook#38.58), [38.59](/rulebook#38.59), [38.60](/rulebook#38.60), [38.62](/rulebook#38.62), [38.123](/rulebook#38.123) |
10386
10386
  | `abide call <address> [args]` | One call to one handler. | [38.8](/rulebook#38.8), [38.61](/rulebook#38.61), [38.110](/rulebook#38.110) |
10387
- | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. | [38.9](/rulebook#38.9), [38.25](/rulebook#38.25), [38.110](/rulebook#38.110), [38.111](/rulebook#38.111) |
10388
- | `abide mcp [--url <origin>]` | The app's MCP server over stdio. | [38.9](/rulebook#38.9), [38.13](/rulebook#38.13), [38.25](/rulebook#38.25), [38.110](/rulebook#38.110) |
10387
+ | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. | [38.9](/rulebook#38.9), [38.25](/rulebook#38.25), [38.110](/rulebook#38.110), [38.111](/rulebook#38.111), [38.123](/rulebook#38.123) |
10388
+ | `abide mcp [--url <origin>]` | The app's MCP server over stdio. | [38.9](/rulebook#38.9), [38.13](/rulebook#38.13), [38.25](/rulebook#38.25), [38.110](/rulebook#38.110), [38.123](/rulebook#38.123) |
10389
10389
  | `abide watch <address> [args]` | A live subscription to one of the app's followed `GET`s. | [38.69](/rulebook#38.69) |
10390
- | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.44](/rulebook#38.44), [38.45](/rulebook#38.45), [38.46](/rulebook#38.46), [38.47](/rulebook#38.47), [38.48](/rulebook#38.48), [38.49](/rulebook#38.49), [38.50](/rulebook#38.50), [38.51](/rulebook#38.51), [38.52](/rulebook#38.52), [38.56](/rulebook#38.56), [38.93](/rulebook#38.93), [38.97](/rulebook#38.97) |
10391
- | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.70](/rulebook#38.70), [38.71](/rulebook#38.71), [38.72](/rulebook#38.72), [38.73](/rulebook#38.73), [38.74](/rulebook#38.74), [38.75](/rulebook#38.75), [38.76](/rulebook#38.76), [38.77](/rulebook#38.77), [38.78](/rulebook#38.78), [38.79](/rulebook#38.79), [38.80](/rulebook#38.80), [38.81](/rulebook#38.81), [38.82](/rulebook#38.82), [38.83](/rulebook#38.83), [38.84](/rulebook#38.84), [38.85](/rulebook#38.85), [38.86](/rulebook#38.86), [38.87](/rulebook#38.87), [38.88](/rulebook#38.88), [38.89](/rulebook#38.89), [38.90](/rulebook#38.90), [38.91](/rulebook#38.91), [38.92](/rulebook#38.92), [38.101](/rulebook#38.101), [38.102](/rulebook#38.102) |
10390
+ | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.44](/rulebook#38.44), [38.45](/rulebook#38.45), [38.46](/rulebook#38.46), [38.47](/rulebook#38.47), [38.48](/rulebook#38.48), [38.49](/rulebook#38.49), [38.50](/rulebook#38.50), [38.51](/rulebook#38.51), [38.52](/rulebook#38.52), [38.56](/rulebook#38.56), [38.93](/rulebook#38.93), [38.97](/rulebook#38.97), [38.123](/rulebook#38.123) |
10391
+ | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.70](/rulebook#38.70), [38.71](/rulebook#38.71), [38.72](/rulebook#38.72), [38.73](/rulebook#38.73), [38.74](/rulebook#38.74), [38.75](/rulebook#38.75), [38.76](/rulebook#38.76), [38.77](/rulebook#38.77), [38.78](/rulebook#38.78), [38.79](/rulebook#38.79), [38.80](/rulebook#38.80), [38.81](/rulebook#38.81), [38.82](/rulebook#38.82), [38.83](/rulebook#38.83), [38.84](/rulebook#38.84), [38.85](/rulebook#38.85), [38.86](/rulebook#38.86), [38.87](/rulebook#38.87), [38.88](/rulebook#38.88), [38.89](/rulebook#38.89), [38.90](/rulebook#38.90), [38.91](/rulebook#38.91), [38.92](/rulebook#38.92), [38.101](/rulebook#38.101), [38.102](/rulebook#38.102), [38.123](/rulebook#38.123) |
10392
10392
  | `abide lsp` | The `.abide` language server, over stdio. | [38.11](/rulebook#38.11), [38.34](/rulebook#38.34), [38.39](/rulebook#38.39), [38.98](/rulebook#38.98), [38.116](/rulebook#38.116), [38.117](/rulebook#38.117), [38.118](/rulebook#38.118) |
10393
10393
  | `--help` · `-h` · `abide help` | Usage, generated from the command list. | [38.12](/rulebook#38.12), [38.113](/rulebook#38.113), [38.114](/rulebook#38.114) |
10394
10394
  | `--version` · `-v` | The version of abide, or of the app a compiled executable holds. | [38.115](/rulebook#38.115) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.116.3",
3
+ "version": "0.117.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -915,8 +915,9 @@ export class TypeSession {
915
915
  checked.push({ source, map, refusals: shadow.refusals })
916
916
  }
917
917
  // 34.6 — WHAT THE BUNDLER RESOLVES AND THE CHECKER CANNOT: a stylesheet imported for its
918
- // effect. Declared in a file of its own at the app's root, which is a script, because a
919
- // pattern module can only be declared outside a module.
918
+ // effect, and (34.21) markdown imported as text into a `.ts`, which bun's own types leave
919
+ // out where they declare `.txt`. Declared in a file of its own at the app's root, which is
920
+ // a script, because a pattern module can only be declared outside a module.
920
921
  this.register(
921
922
  `${this.canonical(within)}/${AMBIENT}`,
922
923
  AMBIENT_DECLARATIONS,
@@ -1484,6 +1485,10 @@ function routesDeclaration(addresses: readonly string[]): string {
1484
1485
  return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
1485
1486
  }
1486
1487
  const AMBIENT_DECLARATIONS = `declare module '*.css' {}
1488
+ declare module '*.md' {
1489
+ const text: string
1490
+ export default text
1491
+ }
1487
1492
  `
1488
1493
 
1489
1494
  /**
@@ -11,6 +11,7 @@ import { COMPONENT_QUERY } from '#server/MODULE_QUERIES.ts'
11
11
  import { ROUTES_TABLE, SERVER_TABLE } from '#server/TABLES.ts'
12
12
  import { VIRTUAL_DIRECTORY } from '#server/VIRTUAL_DIRECTORY.ts'
13
13
  import { CLIENT_ADDRESS } from '#shared/ADDRESSES.ts'
14
+ import type { JsonValue } from '#shared/JsonValue.ts'
14
15
  import { ROUTING_SPECIFIER, WIRE_SPECIFIER } from '#shared/SPECIFIERS.ts'
15
16
  import {
16
17
  type AppLayout,
@@ -317,6 +318,7 @@ async function emitApp(
317
318
  refuseSharedRoutes(routes, mounts, refusals)
318
319
  refuseAddressCollision(routes, mounts, layout.published, refusals)
319
320
  const standIns = standInsOf(mounts)
321
+ const derived = derivedOf(mounts)
320
322
  // 37.1 — every module a server block or a handler file runs in, for the boot to import before
321
323
  // `onStart`.
322
324
  const servers: string[] = []
@@ -329,6 +331,7 @@ async function emitApp(
329
331
  entry,
330
332
  standIns.get(entry.relative),
331
333
  entry.text,
334
+ derived.get(entry.relative),
332
335
  )
333
336
  } catch (thrown) {
334
337
  refusals.at(entry.path, thrown)
@@ -368,6 +371,7 @@ async function emitApp(
368
371
  handlers[mount.source] = {
369
372
  scope: mount.scope,
370
373
  client: standIns.get(mount.source) ?? '',
374
+ args: mount.args,
371
375
  }
372
376
  if (routes.length > 0) {
373
377
  tables[ROUTES_TABLE] = clientTable(routes, transitions)
@@ -624,6 +628,21 @@ function standInsOf(mounts: Mount[]): Map<string, string> {
624
628
  return standIns
625
629
  }
626
630
 
631
+ /**
632
+ * 19.9 — the schema each private handler's annotation derives, by its binding, by source: what its
633
+ * server module holds it to (15.8). A handler file's is its face's (`HandlerFace`).
634
+ */
635
+ function derivedOf(mounts: Mount[]): Map<string, Record<string, JsonValue>> {
636
+ const derived = new Map<string, Record<string, JsonValue>>()
637
+ for (const mount of mounts) {
638
+ if (!mount.private || mount.args === undefined) continue
639
+ const held = derived.get(mount.source) ?? {}
640
+ held[mount.name] = mount.args
641
+ derived.set(mount.source, held)
642
+ }
643
+ return derived
644
+ }
645
+
627
646
  /**
628
647
  * One handler's wrapper, bound as `binding` writes it.
629
648
  *
@@ -1,8 +1,9 @@
1
- import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
1
+ import type { JsonValue } from '#shared/JsonValue.ts'
2
+ import { SERVER_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
2
3
  import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
3
4
  import { Cursor, placeTemplate } from './Cursor.ts'
4
5
  import { type LoweredChunk, Lowering, PROPS } from './Lowering.ts'
5
- import type { AbideFile } from './parseAbide.ts'
6
+ import { type AbideFile, scriptBodies } from './parseAbide.ts'
6
7
  import { privateExports } from './projectShadow.ts'
7
8
  import {
8
9
  concat,
@@ -14,7 +15,7 @@ import {
14
15
  SERVER_LANE,
15
16
  } from './renderMarkup.ts'
16
17
  import { insideShare, SETTLED_AT, shareScope } from './shareScope.ts'
17
- import { splitImports, withoutProps } from './splitImports.ts'
18
+ import { importStatements, splitImports, withoutProps } from './splitImports.ts'
18
19
  import type { TemplateNode } from './TemplateNode.ts'
19
20
 
20
21
  // The name the body is bound to while a component's head contributions land (`HEADS`).
@@ -40,7 +41,8 @@ const CALLS_BEFORE = '__callsBefore'
40
41
  * `settles` IS A PAGE OR A LAYOUT, which `render` builds a level at a time (41.19): its setup runs
41
42
  * at the call and its markup is built on the first read of `head` or `body`, so the calls the
42
43
  * setup started are given the turn to answer. Every other component answers the same way only where
43
- * its setup made a call (41.28), and builds where it is called otherwise.
44
+ * its setup made a call or its markup reads a bare `busy` or `ready` (41.28), and builds where it is
45
+ * called otherwise.
44
46
  */
45
47
  export function emitComponent(
46
48
  file: AbideFile,
@@ -50,6 +52,8 @@ export function emitComponent(
50
52
  route = '/',
51
53
  /** 38.97 — where given, every chunk the module is written around (`sourceMapOf`). */
52
54
  chunks?: LoweredChunk[],
55
+ /** 19.9 — the schema each private handler's annotation derives, by its binding. */
56
+ derived: Readonly<Record<string, JsonValue>> = {},
53
57
  ): string {
54
58
  // Only what the page actually uses. An emitted module carrying an import nothing calls is a
55
59
  // name the app's own tooling reports as unused, in a file nobody wrote.
@@ -91,7 +95,7 @@ export function emitComponent(
91
95
  : `${lowering.use('fileRoute')}(${JSON.stringify(route)})
92
96
  ${serverSetup.body}
93
97
  ${lowering.use('fileRoute')}(undefined)
94
- ${privateExports(file.servers.join('\n'))}`
98
+ ${heldToAnnotations(lowering, derived)}${privateExports(file.servers.join('\n'))}`
95
99
  const setup = splitImports(lowering.scripts())
96
100
  const componentBody = setup.body
97
101
 
@@ -141,16 +145,22 @@ ${privateExports(file.servers.join('\n'))}`
141
145
  // made a call, and then answers as a page does, for `renderComponent` to build after the turn.
142
146
  // THE BUILD IS WRITTEN TWICE rather than always run through `__build`: the closure measured ~4%
143
147
  // a row on `bench:component`'s card under JSC, the second copy nothing but server module bytes.
144
- // One with no setup cannot call, and is written once.
145
- const waits = !settles && componentBody.trim() !== ''
146
- const level = settles
147
- ? deferred()
148
- : waits
149
- ? ` if (${lowering.use('callsInRender')}() !== ${CALLS_BEFORE}) {
148
+ // One with no setup cannot call, and is written once. One whose markup reads a bare `busy` or
149
+ // `ready` answers as a page does always: built where it is called, it would answer before the
150
+ // siblings after it had started a load, where the browser's answers once every setup has run.
151
+ const probes =
152
+ !settles &&
153
+ readsBareProbe(file, `${head}\n${declared}\n${body.join('\n')}`)
154
+ const waits = !settles && !probes && componentBody.trim() !== ''
155
+ const level =
156
+ settles || probes
157
+ ? deferred()
158
+ : waits
159
+ ? ` if (${lowering.use('callsInRender')}() !== ${CALLS_BEFORE}) {
150
160
  ${deferred()}
151
161
  }
152
162
  ${build}`
153
- : build
163
+ : build
154
164
  return `${moduleImports(lowering, () => SERVER_SPECIFIER, [moduleSetup, serverSetup, setup])}
155
165
  ${moduleSetup.body === '' ? '' : `${moduleSetup.body}\n`}${serverBody}export default function Component(${lowering.takesProps() ? `${PROPS} = {}` : ''}) {
156
166
  ${waits ? ` const ${CALLS_BEFORE} = ${lowering.use('callsInRender')}()\n` : ''}${opened}${componentBody}
@@ -159,6 +169,44 @@ ${level}${closed}
159
169
  `
160
170
  }
161
171
 
172
+ /**
173
+ * Whether `markup`, a component's emitted head and body, calls `busy` or `ready` with no argument,
174
+ * under any name the file's scripts import either by from `@abide/abide`. A member's (`s.busy()`)
175
+ * is not bare.
176
+ */
177
+ function readsBareProbe(file: AbideFile, markup: string): boolean {
178
+ let names = ''
179
+ for (const script of scriptBodies(file))
180
+ for (const statement of importStatements(script))
181
+ if (statement.specifier === SHARED_SPECIFIER && !statement.typeOnly)
182
+ for (const binding of statement.bindings)
183
+ if (
184
+ !binding.typeOnly &&
185
+ (binding.imported === 'busy' ||
186
+ binding.imported === 'ready')
187
+ )
188
+ names += `${names === '' ? '' : '|'}${binding.local}`
189
+ return (
190
+ names !== '' &&
191
+ new RegExp(`(?<![\\w$.])(?:${names})\\(\\s*\\)`).test(markup)
192
+ )
193
+ }
194
+
195
+ /**
196
+ * 15.8, 19.9 — each private handler held to the schema its annotation derives, once its server block
197
+ * has built it: the module is what the server mounts and what a component under `mount` calls
198
+ * (38.121), so a call is held to it whoever makes it.
199
+ */
200
+ function heldToAnnotations(
201
+ lowering: Lowering,
202
+ derived: Readonly<Record<string, JsonValue>>,
203
+ ): string {
204
+ let out = ''
205
+ for (const name in derived)
206
+ out += `${lowering.use('derivedArgs')}(${name}, ${JSON.stringify(derived[name])})\n`
207
+ return out
208
+ }
209
+
162
210
  /**
163
211
  * An emitted module's imports: each framework name `lowering` used, from the specifier it ships
164
212
  * under, and the author's own from both script blocks, already pointed where they landed
@@ -1,4 +1,5 @@
1
1
  import { STYLE_QUERY } from '#server/MODULE_QUERIES.ts'
2
+ import type { JsonValue } from '#shared/JsonValue.ts'
2
3
  import type { Placement } from '#server/placement.ts'
3
4
  import type { CheckerAnswer } from './CheckerAnswer.ts'
4
5
  import { emitClient } from './emitClient.ts'
@@ -35,6 +36,8 @@ export function emitSource(
35
36
  standIns = '',
36
37
  /** 38.97 — the `.abide`'s text, where the build wants each module's source map. */
37
38
  text?: string,
39
+ /** 19.9 — the schema each private handler it declares derives off its annotation, by name. */
40
+ derived?: Readonly<Record<string, JsonValue>>,
38
41
  ): {
39
42
  server: string
40
43
  client: string
@@ -109,6 +112,7 @@ export function emitSource(
109
112
  kind !== 'component',
110
113
  source.scope,
111
114
  serverChunks,
115
+ derived,
112
116
  )
113
117
  const name = relative.slice(relative.lastIndexOf('/') + 1)
114
118
  const browser = withStandIns(
@@ -35,15 +35,17 @@ const SOURCE_ALIAS = { '#src/*': ['./src/*', './src/*.ts'] }
35
35
  * 38.109, 38.119 — the starter's `bunfig.toml`: the harness's abide preload, which is a DOM a test
36
36
  * can mount into and measure, then abide's own — the app's `.abide` files imported as modules, with
37
37
  * nothing booted first. Its test root is `src`, so a browser test under `e2e` is playwright's alone.
38
+ * 38.122 — and no peer installed: `bun-plugin-tailwind` names `bun` as one, and an installed copy
39
+ * lands first on the PATH inside `bun run`, shadowing the bun that runs everything else.
38
40
  */
39
- export const TEST_PRELOAD =
40
- '# 38.109, 38.119 — a DOM to mount into and measure, then the app\'s `.abide` files as modules.\n[test]\nroot = "src"\npreload = ["@abide/harness/abide/preload"]\n'
41
+ export const BUNFIG =
42
+ '# 38.122 — a peer is not installed: an installed `bun` would shadow this one inside `bun run`.\n[install]\npeer = false\n\n# 38.109, 38.119 — a DOM to mount into and measure, then the app\'s `.abide` files as modules.\n[test]\nroot = "src"\npreload = ["@abide/harness/abide/preload"]\n'
41
43
 
42
44
  // WRITTEN, NOT COPIED, beside the manifests: a packer drops a `bunfig.toml` or a `.gitignore` from
43
45
  // the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file.
44
46
  function filesFor(name: string): Record<string, string> {
45
47
  return {
46
- 'bunfig.toml': TEST_PRELOAD,
48
+ 'bunfig.toml': BUNFIG,
47
49
  '.gitignore': 'node_modules\n.abide\n',
48
50
  'package.json': `${JSON.stringify(
49
51
  {
@@ -120,7 +120,8 @@ export function serveCompilation(root: string, compilation: Compilation): void {
120
120
  const real = realOrAsGiven(root)
121
121
  const handlerFiles = addCompilation(real, compilation, RUNTIME)
122
122
  installSourceMaps(serverMapsOf(compilation, real), real)
123
- if (handlerFiles.length !== 0) Bun.plugin(handlerFilesPlugin(handlerFiles))
123
+ if (handlerFiles.length !== 0)
124
+ Bun.plugin(handlerFilesPlugin(handlerFiles, RUNTIME.handlers))
124
125
  if (registered) return
125
126
  registered = true
126
127
  Bun.plugin(modulesPlugin(RUNTIME))
@@ -132,7 +133,13 @@ export function serveCompilation(root: string, compilation: Compilation): void {
132
133
  // plugin per compilation because the filter is fixed at registration, and the runtime's `onLoad`
133
134
  // cannot fall through, so the filter names the handler files and nothing else. The face it names
134
135
  // is resolved by `modulesPlugin`, registered once.
135
- function handlerFilesPlugin(paths: string[]): Bun.BunPlugin {
136
+ // 15.8, 19.9 — AND HOLDS ITS HANDLER TO THE SCHEMA ITS ANNOTATION DERIVES, once the face has built it:
137
+ // this module is what every import of the file is answered with — the server's mount, a test's own
138
+ // import, a component's under `mount` — so a call is held to it whoever makes it.
139
+ function handlerFilesPlugin(
140
+ paths: string[],
141
+ faces: ReadonlyMap<string, HandlerFace>,
142
+ ): Bun.BunPlugin {
136
143
  return {
137
144
  name: 'abide-handler-files',
138
145
  setup(build) {
@@ -142,10 +149,18 @@ function handlerFilesPlugin(paths: string[]): Bun.BunPlugin {
142
149
  `^(?:${paths.map(RegExp.escape).join('|')})$`,
143
150
  ),
144
151
  },
145
- (args) => ({
146
- contents: `export { default } from ${JSON.stringify(`${args.path}${HANDLER_SUFFIX}`)}\n`,
147
- loader: 'js',
148
- }),
152
+ (args) => {
153
+ const face = JSON.stringify(`${args.path}${HANDLER_SUFFIX}`)
154
+ const schema = faces.get(args.path)?.args
155
+ return {
156
+ contents:
157
+ `export { default } from ${face}\n` +
158
+ (schema === undefined
159
+ ? ''
160
+ : `import __handler from ${face}\nimport { derivedArgs as __derivedArgs } from '${SERVER_SPECIFIER}'\n__derivedArgs(__handler, ${JSON.stringify(schema)})\n`),
161
+ loader: 'js',
162
+ }
163
+ },
149
164
  )
150
165
  },
151
166
  }
@@ -16,7 +16,7 @@ import type { Gated } from '#shared/reactive/argsContract.ts'
16
16
  import { deadlineOf, heldGenerationOf } from '#shared/reactive/index.ts'
17
17
  import type { Defaults } from '#shared/reactive/parameterDefaults.ts'
18
18
  import { notFound, refuse } from '#shared/refuse.ts'
19
- import type { Method, Rpc } from '#shared/rpc.ts'
19
+ import type { HeldToAnnotation, Method, Rpc } from '#shared/rpc.ts'
20
20
  import { argsInURL } from '#shared/rpcRequest.ts'
21
21
  import { VERSION_HEADER } from '#shared/VERSION_HEADER.ts'
22
22
  import { versionInEtag } from '#shared/versionInEtag.ts'
@@ -118,20 +118,13 @@ export type Mounted = Mount & {
118
118
  * subscription to it both pass through (37.2, 18.26).
119
119
  */
120
120
  rungs: AppRung[]
121
- handler: Rpc<unknown, unknown> & Gated<unknown>
121
+ handler: Rpc<unknown, unknown> & Gated<unknown> & HeldToAnnotation
122
122
  /**
123
123
  * The args' JSON Schema, probed once at mount.
124
124
  *
125
125
  * 19.7, 20.11 — a third-party schema converts itself on every probe.
126
126
  */
127
127
  argsDocument: Document | undefined
128
- /**
129
- * What the wire holds args to where the handler declares no `args` gate of its own.
130
- *
131
- * 19.9 — the annotation's schema. An in-process call already holds the type, so the handler
132
- * never runs it.
133
- */
134
- wireArgs: JsonValue | undefined
135
128
  }
136
129
 
137
130
  /**
@@ -172,7 +165,6 @@ export function mountedOwn(
172
165
  result: undefined,
173
166
  handler: handler as Mounted['handler'],
174
167
  argsDocument: undefined,
175
- wireArgs: undefined,
176
168
  },
177
169
  ]
178
170
  }
@@ -198,7 +190,7 @@ export async function mountHandlers(
198
190
  for (let at = 0; at < table.length; at += 1) {
199
191
  const mount = table[at] as Mount
200
192
  const exported = (modules[at] as Record<string, unknown>)[mount.export]
201
- const handler = exported as Rpc<unknown, unknown> & Gated<unknown>
193
+ const handler = exported as Mounted['handler']
202
194
  const declared = argsDocumentOf(handler)
203
195
  // 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
204
196
  // page's render turns a call it made into the browser's request by (41.20).
@@ -210,7 +202,6 @@ export async function mountHandlers(
210
202
  rungs: rungsOver(mount.scope),
211
203
  handler,
212
204
  argsDocument: declared ?? (mount.args as Document | undefined),
213
- wireArgs: declared === undefined ? mount.args : undefined,
214
205
  })
215
206
  }
216
207
  return { handlers }
@@ -2,9 +2,9 @@
2
2
  // off the wire: over http (16.2) and a `GET` followed over the socket mux (18.3). Each lane reads the
3
3
  // args off its own transport and answers a refusal in its own form.
4
4
 
5
+ import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
5
6
  import { normalized } from '#shared/reactive/argsContract.ts'
6
7
  import { validationError } from '#shared/refuse.ts'
7
- import { validateJson } from '#shared/validateJson.ts'
8
8
  import type { Mounted } from './answerRpc.ts'
9
9
  import { coerceArgs } from './coerceArgs.ts'
10
10
 
@@ -16,11 +16,10 @@ import { coerceArgs } from './coerceArgs.ts'
16
16
  * 19.7 — the text as the types the handler's `args` declares. 11.27 — then the defaults, ahead of
17
17
  * every key taken after it: the seed a render filed and the `global` entry standing were both filed
18
18
  * under them (11.29). 19.9 — THE WIRE HELD TO THE ANNOTATION, where no `args` schema is declared to
19
- * hold it. Args that did not come are held as an empty record: a schema is only derived where the
20
- * handler's args are a required record, so a required member missing is refused, not read off nothing.
19
+ * hold it — the check the handler carries (`derivedArgs`), which its in-process call makes too (15.8).
21
20
  */
22
21
  export function argsFor(
23
- found: Pick<Mounted, 'handler' | 'argsDocument' | 'wireArgs'>,
22
+ found: Pick<Mounted, 'handler' | 'argsDocument'>,
24
23
  sent: unknown,
25
24
  query: string | undefined,
26
25
  ): unknown {
@@ -33,9 +32,7 @@ export function argsFor(
33
32
  query,
34
33
  )
35
34
  const args = normalized(found.handler, coerced)
36
- if (found.wireArgs !== undefined) {
37
- const issues = validateJson(found.wireArgs, args ?? {})
38
- if (issues !== null) throw validationError(issues)
39
- }
35
+ const issues = found.handler[DERIVED_ARGS]?.(args) ?? null
36
+ if (issues !== null) throw validationError(issues)
40
37
  return args
41
38
  }
@@ -1,4 +1,5 @@
1
- // 41.28 — A CHILD WHOSE SETUP MADE A CALL IS BUILT AS A PAGE IS: after the turn.
1
+ // 41.28 — A CHILD WHOSE SETUP MADE A CALL IS BUILT AS A PAGE IS: after the turn — and so is one whose
2
+ // markup reads a bare `busy` or `ready`, which the compiler writes to answer as a page does always.
2
3
  // A component is set up where its parent's markup invokes it, so its build cannot wait inline; it is
3
4
  // left as a marker in the parent's head and body, built once the turn has run, and written in where
4
5
  // its marker stands. A child whose setup made no call builds where it stands, as before, and pays a
@@ -0,0 +1,25 @@
1
+ import { DERIVED_ARGS } from '#shared/DERIVED_ARGS.ts'
2
+ import type { JsonValue } from '#shared/JsonValue.ts'
3
+ import type { Gated } from '#shared/reactive/argsContract.ts'
4
+ import type { HeldToAnnotation } from '#shared/rpc.ts'
5
+ import { validateJson } from '#shared/validateJson.ts'
6
+ import { argsDocumentOf } from './coerceArgs.ts'
7
+
8
+ /**
9
+ * Holds `handler`'s args to `schema`, the one the build derived off its annotation, for every call
10
+ * of it from now on: in process and over the wire (15.8, 19.9).
11
+ *
12
+ * What the module a handler is loaded through calls once its handler is built — a handler file's
13
+ * face and a `<script server>`'s module — so the schema is there wherever the handler can be
14
+ * called, whether or not an app mounted it. Not where its `args` gate publishes a document of its
15
+ * own, which holds the args itself (19.7). Args that did not come are held as an empty record: a
16
+ * schema is only derived where the handler's args are a required record, so a required member
17
+ * missing is refused, not read off nothing.
18
+ */
19
+ export function derivedArgs(
20
+ handler: HeldToAnnotation & Gated<unknown>,
21
+ schema: JsonValue,
22
+ ): void {
23
+ if (argsDocumentOf(handler) !== undefined) return
24
+ handler[DERIVED_ARGS] = (args) => validateJson(schema, args ?? {})
25
+ }
@@ -3,6 +3,7 @@
3
3
  // adapter that hands bun's hooks to it, because bun decides which hook asks (an `onResolve` for a
4
4
  // specifier, an `onLoad` for a path), and this decides what the answer is.
5
5
 
6
+ import type { JsonValue } from '#shared/JsonValue.ts'
6
7
  import { HANDLER_SUFFIX, SERVER_QUERY } from './MODULE_QUERIES.ts'
7
8
 
8
9
  // A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
@@ -45,8 +46,15 @@ export type Asked =
45
46
  | 'abide'
46
47
  | 'handler'
47
48
 
48
- /** One handler file, as `Compilation.handlers` holds it. */
49
- export type HandlerFace = { scope: string; client: string }
49
+ /**
50
+ * One handler file, as `Compilation.handlers` holds it — and 19.9's schema its default's annotation
51
+ * derives, which the module at the file's path holds it to (`derivedArgs`).
52
+ */
53
+ export type HandlerFace = {
54
+ scope: string
55
+ client: string
56
+ args: JsonValue | undefined
57
+ }
50
58
 
51
59
  /**
52
60
  * The face an import of `file` is answered with, asked `asked` from `importer` in `lane` — or
@@ -52,6 +52,9 @@ export { type App, createApp } from './createApp.ts'
52
52
  // 41.28's run-time half: an emitted component reads it on either side of its setup, and one whose
53
53
  // setup made a call answers as a page does. Not an app's name.
54
54
  export { callsInRender } from './deferredBuilds.ts'
55
+ // 19.9's run-time half: what the module a handler is loaded through calls to hold it to the schema
56
+ // its annotation derives (15.8). Not an app's name.
57
+ export { derivedArgs } from './derivedArgs.ts'
55
58
  // 37.1's run-time half: what a compiled server module calls around its `<script server>`, so the
56
59
  // hooks it makes are filed at its route.
57
60
  export {
@@ -30,8 +30,9 @@ export function renderComponent(
30
30
  const rendered = (
31
31
  component as (props: Record<string, () => unknown>) => Rendered
32
32
  )(props)
33
- // 41.28 — A CHILD WHOSE SETUP MADE A CALL ANSWERS AS A PAGE DOES, with a `scope` and its build
34
- // left for the first read, and is built after the turn where a page is being built.
33
+ // 41.28 — A CHILD WHOSE SETUP MADE A CALL, OR WHOSE MARKUP READS A BARE PROBE, ANSWERS AS A PAGE
34
+ // DOES, with a `scope` and its build left for the first read, and is built after the turn where a
35
+ // page is being built.
35
36
  if ('scope' in rendered) {
36
37
  const marker = deferBuild(rendered, heads, callsFrom)
37
38
  if (marker !== undefined) return marker
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Why the bun at `version` cannot run abide, which asks for `range` — or `undefined` where it can.
3
+ *
4
+ * 38.123 — named in full, both versions and the way out, since an older bun fails later and
5
+ * somewhere unrelated, if at all.
6
+ */
7
+ export function runtimeRefusal(
8
+ version: string,
9
+ range: string,
10
+ ): string | undefined {
11
+ if (Bun.semver.satisfies(version, range)) return undefined
12
+ return `this is bun ${version}, and abide needs bun ${range}. Run \`bun upgrade\`, or install a bun in that range. RULEBOOK 38.123.`
13
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The member of an `Rpc` holding its args to the schema its annotation derives (19.9): stamped by the
3
+ * module its handler is loaded through, on the server alone, and read by every call of it — in
4
+ * process and over the wire alike (15.8).
5
+ */
6
+ export const DERIVED_ARGS: unique symbol = Symbol.for('abide.derivedArgs')
@@ -15,9 +15,10 @@
15
15
  import { contractOf, type Gated } from './argsContract.ts'
16
16
  import { CELL } from './CELL.ts'
17
17
  import { KEYED_BRAND } from './KEYED_BRAND.ts'
18
- import { brandedPrototype, MEMBER_KEYS } from './Reactive.ts'
18
+ import { brandedPrototype, MEMBER_KEYS, signalMoved } from './Reactive.ts'
19
19
  import { SELECTION_MEMBERS } from './SELECTION_MEMBERS.ts'
20
20
  import { currentScope, runInScope, SCOPING } from './scoping.ts'
21
+ import { newTracked, TRACKING, type Tracked, trackProbe } from './tracking.ts'
21
22
 
22
23
  const UNBUILT: unique symbol = Symbol('abide.unbuilt')
23
24
 
@@ -36,11 +37,18 @@ class Filed {
36
37
  home: unknown
37
38
  make: () => unknown
38
39
  global: boolean
40
+ /**
41
+ * 3.3, 13.19 — what a probe asked before the value was built here subscribes to, moved by the
42
+ * build: the home's. A scope files its own in `instances` under this one as its key, and drops
43
+ * it with the scope.
44
+ */
45
+ unbuilt: Tracked
39
46
 
40
47
  constructor(make: () => unknown, lazy: boolean, global: boolean) {
41
48
  this.make = make
42
49
  this.home = lazy ? UNBUILT : make()
43
50
  this.global = global
51
+ this.unbuilt = newTracked()
44
52
  }
45
53
  }
46
54
 
@@ -56,14 +64,21 @@ type Forwarder = { [FILED]: Filed }
56
64
  function instanceOf(filed: Filed): Record<PropertyKey, unknown> {
57
65
  const scope = filed.global ? undefined : currentScope()
58
66
  if (scope === undefined) {
59
- if (filed.home === UNBUILT)
67
+ if (filed.home === UNBUILT) {
60
68
  filed.home = runInScope(undefined, () => filed.make(), undefined)
69
+ signalMoved(filed.unbuilt)
70
+ }
61
71
  return filed.home as Record<PropertyKey, unknown>
62
72
  }
63
73
  let instance = scope.instances.get(filed)
64
74
  if (instance === undefined) {
65
75
  instance = filed.make()
66
76
  scope.instances.set(filed, instance)
77
+ const asked = scope.instances.get(filed.unbuilt) as Tracked | undefined
78
+ if (asked !== undefined) {
79
+ scope.instances.delete(filed.unbuilt)
80
+ signalMoved(asked)
81
+ }
67
82
  }
68
83
  return instance as Record<PropertyKey, unknown>
69
84
  }
@@ -152,14 +167,33 @@ export function scoped(
152
167
  /**
153
168
  * Whether the value `value` forwards to is built for the scope that is current — always, for a
154
169
  * value that is no forwarder. Asking builds nothing, where `instanceOf` builds what it finds unbuilt.
170
+ *
171
+ * 3.3 — AND WHERE IT IS NOT, ASKING SUBSCRIBES THE READER TO ITS BUILD: a guard that probed before
172
+ * the first call has no cell to hear the call start from, so `disabled={save.busy()}` stayed
173
+ * enabled through the whole write. One signal per value per scope, never one per entry (13.3).
155
174
  */
156
175
  export function builtHere(value: unknown): boolean {
157
176
  const filed = (value as Partial<Forwarder> | null)?.[FILED]
158
177
  if (filed === undefined) return true
159
178
  const scope = filed.global ? undefined : currentScope()
160
- return scope === undefined
161
- ? filed.home !== UNBUILT
162
- : scope.instances.has(filed)
179
+ if (
180
+ scope === undefined
181
+ ? filed.home !== UNBUILT
182
+ : scope.instances.has(filed)
183
+ )
184
+ return true
185
+ const reader = TRACKING.reader
186
+ if (reader === null) return false
187
+ let asked = filed.unbuilt
188
+ if (scope !== undefined) {
189
+ asked = scope.instances.get(filed.unbuilt) as Tracked
190
+ if (asked === undefined) {
191
+ asked = newTracked()
192
+ scope.instances.set(filed.unbuilt, asked)
193
+ }
194
+ }
195
+ trackProbe(asked, reader)
196
+ return false
163
197
  }
164
198
 
165
199
  /**
package/src/shared/rpc.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  // the call hands back a value whose body runs the onion once per args, per request, over the
14
14
  // handler's own.
15
15
 
16
+ import { DERIVED_ARGS } from './DERIVED_ARGS.ts'
16
17
  import { type Failed, isFailedNamed } from './Failed.ts'
17
18
  import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
18
19
  import { HandlerRoute } from './HandlerRoute.ts'
@@ -52,7 +53,7 @@ import { MUTATION } from './reactive/MUTATION.ts'
52
53
  import { dropAnswered } from './reactive/Reactive.ts'
53
54
  import { SELECTION_MEMBERS } from './reactive/SELECTION_MEMBERS.ts'
54
55
  import { builtHere, isForwarder } from './reactive/scoped.ts'
55
- import { checkArgs } from './refuse.ts'
56
+ import { checkArgs, validationError } from './refuse.ts'
56
57
  import { rpcRequest } from './rpcRequest.ts'
57
58
  import type { Schema } from './Schema.ts'
58
59
 
@@ -162,6 +163,16 @@ type Derived<Member, Whole = Member> = Member extends
162
163
  : never
163
164
  : never
164
165
 
166
+ /**
167
+ * 19.9 — the issues an `Rpc`'s args leave against the schema its annotation derives, `null` where
168
+ * they hold: `undefined` until the module its handler is loaded through stamps it (`derivedArgs`),
169
+ * and so in a browser always. A function rather than the schema, so this seam never carries the
170
+ * validator (19.13).
171
+ */
172
+ export type HeldToAnnotation = {
173
+ [DERIVED_ARGS]: ((args: unknown) => Issues<unknown> | null) | undefined
174
+ }
175
+
165
176
  /** Which generated surfaces carry a handler (20.1). */
166
177
  export type Clients = {
167
178
  ui?: boolean
@@ -440,6 +451,11 @@ export function declare(method: Method): Declare {
440
451
  }
441
452
  const call = (given?: unknown, options?: CallOptions): unknown => {
442
453
  const args = normalized(contract, given)
454
+ // 15.8, 19.9 — AND HELD TO ITS ANNOTATION, where the build derived a schema off it: a
455
+ // type an in-process caller wrote holds nothing at run time (`as never`, a value off
456
+ // JSON), so the call is refused as the wire refuses it rather than handed to the body.
457
+ const issues = rpc[DERIVED_ARGS]?.(args) ?? null
458
+ if (issues !== null) return reactive(() => validationError(issues))
443
459
  const route =
444
460
  rpc.address === undefined ? undefined : routeAt(rpc.address)
445
461
  if (route?.segments !== undefined) {
@@ -469,7 +485,7 @@ export function declare(method: Method): Declare {
469
485
  // 13.19 — AND NOT YET BUILT, NONE OF THEM BUILDS IT: building an unkeyed value starts its body
470
486
  // (1.7), so `save.busy()` over a server's never-called `POST(async () => …)` ran the write and
471
487
  // reported it in flight. Until a call reaches it the handler has no entry, and answers as one
472
- // with no entries does.
488
+ // with no entries does — the asking reader subscribed to the call that builds it (`builtHere`).
473
489
  const built = value as Record<
474
490
  (typeof SELECTION_MEMBERS)[number],
475
491
  (pattern?: unknown) => unknown
@@ -487,6 +503,7 @@ export function declare(method: Method): Declare {
487
503
  maxBodySize: options?.maxBodySize,
488
504
  clients: options?.clients ?? {},
489
505
  guarded: rungs.length > 0,
506
+ [DERIVED_ARGS]: undefined as HeldToAnnotation[typeof DERIVED_ARGS],
490
507
  isError: isFailedNamed,
491
508
  url(args?: Record<string, unknown>): string {
492
509
  return rpcRequest(method, routeAt(addressOf(rpc)), args).url