@abide/abide 0.102.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/abide.ts +31 -28
  3. package/docs/REGISTRY.md +40 -36
  4. package/docs/RULEBOOK.md +113 -92
  5. package/docs/abide.md +788 -554
  6. package/package.json +2 -1
  7. package/scaffold/AGENTS.md +13 -6
  8. package/src/compiler/TypeSession.ts +108 -35
  9. package/src/compiler/appMap.ts +91 -0
  10. package/src/compiler/argsSchemaOf.ts +1 -1
  11. package/src/compiler/checkApp.ts +35 -18
  12. package/src/compiler/checkedShadow.ts +40 -7
  13. package/src/compiler/compileApp.ts +271 -76
  14. package/src/compiler/compiledEntry.ts +10 -6
  15. package/src/compiler/componentGraph.ts +6 -7
  16. package/src/compiler/declaredExports.ts +3 -3
  17. package/src/compiler/emitClient.ts +3 -3
  18. package/src/compiler/emitComponent.ts +9 -9
  19. package/src/compiler/emitSource.ts +5 -9
  20. package/src/compiler/handlersUnder.ts +114 -73
  21. package/src/compiler/hooksCalled.ts +35 -0
  22. package/src/compiler/mountsOf.ts +30 -18
  23. package/src/compiler/parseAbide.ts +12 -12
  24. package/src/compiler/projectShadow.ts +71 -46
  25. package/src/compiler/readsIn.ts +2 -2
  26. package/src/compiler/scaffold.ts +24 -21
  27. package/src/compiler/serveLanguage.ts +0 -24
  28. package/src/compiler/sharedModuleWrites.ts +2 -2
  29. package/src/compiler/splitImports.ts +4 -4
  30. package/src/compiler/writeDefaultProject.ts +28 -1
  31. package/src/server/MODULE_QUERIES.ts +12 -3
  32. package/src/server/abideModules.ts +156 -53
  33. package/src/server/answerRequest.ts +9 -4
  34. package/src/server/answerRpc.ts +18 -16
  35. package/src/server/appTree.ts +92 -25
  36. package/src/server/commands.ts +5 -4
  37. package/src/server/componentModules.d.ts +1 -2
  38. package/src/server/createApp.ts +23 -11
  39. package/src/server/devWorker.ts +5 -1
  40. package/src/server/fenceServer.ts +36 -13
  41. package/src/server/generatedSurfaces.ts +12 -53
  42. package/src/server/hooks.ts +17 -4
  43. package/src/server/index.ts +1 -0
  44. package/src/server/installSourceMaps.ts +6 -1
  45. package/src/server/launcherOptions.ts +12 -13
  46. package/src/server/launcherPage.ts +2 -2
  47. package/src/server/nativeMenu.ts +3 -3
  48. package/src/server/preload.ts +13 -0
  49. package/src/server/runDesktop.ts +11 -16
  50. package/src/server/serveDev.ts +8 -2
  51. package/src/server/surfaceOf.ts +2 -0
  52. package/src/server/versionOf.ts +1 -2
  53. package/src/server/withAbideSources.ts +1 -1
  54. package/src/shared/ADDRESSES.ts +3 -0
  55. package/src/shared/LAUNCHER_FILES.ts +9 -0
  56. package/src/shared/RESERVED_DIRECTORIES.ts +19 -4
  57. package/src/shared/index.ts +2 -1
  58. package/src/{ui → shared}/menuBridge.ts +1 -1
  59. package/src/shared/menuItem.ts +2 -2
  60. package/src/shared/onMenu.ts +58 -0
  61. package/src/shared/reactive/Cursor.ts +66 -123
  62. package/src/shared/reactive/Reactive.ts +74 -28
  63. package/src/shared/reactive/callLoad.ts +4 -18
  64. package/src/shared/reactive/chunksOfStream.ts +10 -34
  65. package/src/{ui → shared}/reportMenu.ts +7 -9
  66. package/src/shared/routePattern.ts +74 -23
  67. package/src/shared/rpc.ts +1 -1
  68. package/src/ui/Router.ts +0 -6
  69. package/src/compiler/syncTypes.ts +0 -32
  70. package/src/compiler/writeTypes.ts +0 -82
  71. package/src/server/LAUNCHER_FILES.ts +0 -9
  72. /package/scaffold/src/{page.abide → index.abide} +0 -0
  73. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
package/docs/abide.md CHANGED
@@ -135,7 +135,7 @@ In abide it is the handler, and a page that reads it. You declare what a value *
135
135
 
136
136
  **Read an invoice** — A row from your database on the page, with nothing between them. One file: the handler you would have written anyway, and the markup that reads it.
137
137
 
138
- ```abide Invoice.abide
138
+ ```abide lib/Invoice.abide
139
139
  <script server>
140
140
  import { GET, notFound } from '@abide/abide/server'
141
141
 
@@ -143,7 +143,7 @@ const INVOICES = new Map([
143
143
  ['42', { number: 'INV-0042', total: '$1,240.00', dueOn: '2026-09-14' }],
144
144
  ])
145
145
 
146
- export const getInvoice = GET(
146
+ const getInvoice = GET(
147
147
  async ({ id }: { id: string }) => {
148
148
  await Bun.sleep(450) // a slow database, so the wait is on screen
149
149
  const invoice = INVOICES.get(id)
@@ -179,7 +179,7 @@ Read on: [Templates](/templates.md) · [Values by name](/templates/read-and-writ
179
179
 
180
180
  ## Data from the server
181
181
 
182
- A handler a `<script server>` exports is callable. The method you declare it with is the whole difference between a read and a write.
182
+ A handler you declare is callable. The method you declare it with is the whole difference between a read and a write.
183
183
 
184
184
  Read on: [Data from the server](/data.md) · [Reading data](/data/read-data-without-writing-an-api.md) · [Mutations](/data/change-something-on-the-server.md) · [Live updates](/data/keep-a-room-of-callers-in-sync.md)
185
185
 
@@ -243,7 +243,7 @@ abide has one answer.
243
243
 
244
244
  **`Reactive` — four producers, one way to ask** — A contact and its notes. Watch which rows say “…” on arrival, then type a note and add it.
245
245
 
246
- ```abide Contact.abide
246
+ ```abide lib/Contact.abide
247
247
  <script server>
248
248
  import { GET } from '@abide/abide/server'
249
249
 
@@ -251,7 +251,7 @@ const CONTACTS = new Map([
251
251
  ['1', { id: '1', name: 'Ada Lovelace', email: 'ada@example.com' }],
252
252
  ])
253
253
 
254
- export const getContact = GET(
254
+ const getContact = GET(
255
255
  async ({ id }: { id: string }) => {
256
256
  await Bun.sleep(900) // a slow database, so the wait is on screen
257
257
  const row = CONTACTS.get(id)
@@ -336,10 +336,8 @@ Address, method, argument and result schemas, description — all of it already
336
336
 
337
337
  That is why failures are strict. A refusal is a **named value carrying data**, not a status code and a sentence:
338
338
 
339
- ```abide src/groups/page.abide — excerpt
340
- <script server>
341
- export const join = POST(() => notMember({ group: 'staff' }))
342
- </script>
339
+ ```ts src/groups/join.ts — excerpt
340
+ export default POST(() => notMember({ group: 'staff' }))
343
341
  ```
344
342
 
345
343
  The caller narrows it by name and gets `.data` typed — your own template and a model on the far side of an HTTP request, identically.
@@ -365,13 +363,13 @@ The caller narrows it by name and gets `.data` typed — your own template and a
365
363
 
366
364
  *Put something on screen, change it, and see where the code that changed it lives.*
367
365
 
368
- A page is a file. Put a `page.abide` in a directory under `src` and the path it sits at is the address it answers on — there is no route table to register it in, and no build step to tell about it.
366
+ A page is a file, and its path below `src` is the address it answers on: `src/index.abide` is `/`, and `src/about.abide` is `/about`. There is no route table to register it in, and no build step to tell about it.
369
367
 
370
- ```abide src/page.abide
368
+ ```abide src/index.abide
371
369
  <script server>
372
370
  import { GET } from '@abide/abide/server'
373
371
 
374
- export const getGreeting = GET(() => `Hello from Bun ${Bun.version}`)
372
+ const getGreeting = GET(() => `Hello from Bun ${Bun.version}`)
375
373
  </script>
376
374
 
377
375
  <script>
@@ -385,30 +383,34 @@ let count = reactive(0)
385
383
  <button onclick={() => count += 1}>Clicked {count} times</button>
386
384
  ```
387
385
 
388
- *1 file, 15 lines* — and the app serves `/`, with `getGreeting` answering at `/getGreeting` beside it.
386
+ *1 file, 15 lines* — and the app serves `/`, with `getGreeting` a handler only this page calls.
389
387
 
390
388
  One file holds a function that runs on the server, the values, the markup that shows them, and the click that changes one. There is no API between the two halves, no store to declare, no component to register, and no re-render to ask for.
391
389
 
392
- ## Where the file goes decides the address
390
+ ## A file's path is the address it answers
393
391
 
394
- The directory is the path, and a `page.abide` makes that directory a page.
392
+ A file's path below `src` is its address. Nothing else names a route.
395
393
 
396
394
  | File | Answers on |
397
395
  | --- | --- |
398
- | `src/page.abide` | `/` |
399
- | `src/about/page.abide` | `/about` |
400
- | `src/invoices/page.abide` | `/invoices` |
401
- | `src/invoices/[id]/page.abide` | `/invoices/42`, with `42` readable as `route.params.id` |
396
+ | `src/index.abide` | `/` |
397
+ | `src/about.abide` | `/about` |
398
+ | `src/invoices/index.abide` | `/invoices` |
399
+ | `src/invoices/[id].abide` | `/invoices/42`, with `42` readable as `route.params.id` |
400
+ | `src/invoices/getInvoice.ts` | `GET /invoices/getInvoice`, the handler its default export declares |
401
+ | `src/invoices/lib/Row.abide` | no address: a component the pages import |
402
402
 
403
- Every directory under `src` is a segment of an address, so `src/billing/page.abide` is `/billing`. Group an app by subject, and the addresses follow.
403
+ An `index.abide` answers its directory's own address, and any other `.abide` adds its name. A `.ts` beside the pages is a handler file, and its name is its last segment.
404
404
 
405
- A handler answers below its file's directory, under the name it was exported as: `getGreeting` in `src/page.abide` is `/getGreeting`, and the same export in `src/invoices/page.abide` would be `/invoices/getGreeting`.
405
+ Code that is not an address goes in one of four directories that never are: `lib` for components and modules, `tests`, `server` and `public`. Anything else beside the pages fails the build, so listing a directory lists its routes. Group an app by subject, and the addresses follow.
406
406
 
407
407
  Read on: [Routes](/pages/add-a-page.md) · [Sub-path mounting](/pages/serve-the-app-under-a-sub-path.md)
408
408
 
409
409
  ## `<script server>` runs on the server, `<script>` sets up the page
410
410
 
411
- `<script server>` runs on the server alone, once. What it exports is a handler — `GET` declares one — and the rest of the file calls it by the name it was declared with. The browser is never sent the block: the build generates a call under the same name in its place, so `Bun.version` is read where Bun is.
411
+ `<script server>` runs on the server alone, once. What it declares with `GET` is a handler, and the rest of the file calls it by the name it was declared with. The browser is never sent the block: the build generates a call under the same name in its place, so `Bun.version` is read where Bun is.
412
+
413
+ No block of a `.abide` exports anything. The component is the file's one export, and a handler its `<script server>` declares is called by that file alone.
412
414
 
413
415
  The `<script>` block is the page's setup: ordinary imports, ordinary TypeScript, run once for this instance of the page. Everything after it is the markup, and the file shares one scope — the template resolves names against its two scripts and nothing else.
414
416
 
@@ -426,7 +428,7 @@ Inside a `.abide` file, a reactive value is read and written under its own name.
426
428
 
427
429
  Naming `count` in an expression **reads** it; the explicit `count()` and `count.set(v)` keep compiling everywhere the sugar does, because the sugar is over those forms rather than instead of them. A `.ts` file — which has no template around it — writes them out:
428
430
 
429
- ```ts src/ui/counters.ts
431
+ ```ts src/lib/counters.ts
430
432
  import { reactive } from '@abide/abide'
431
433
 
432
434
  export const count = reactive(0)
@@ -441,12 +443,12 @@ Read on: [Local state](/values/show-a-value-that-changes.md) · [Values by name]
441
443
 
442
444
  The same page reaches your database through a handler its own `<script server>` declares, and reads it like any other value.
443
445
 
444
- ```abide src/invoices/[id]/page.abide — excerpt
446
+ ```abide src/invoices/[id].abide — excerpt
445
447
  <script server>
446
448
  import { GET } from '@abide/abide/server'
447
449
  import { database } from '#src/server/database'
448
450
 
449
- export const getInvoice = GET(
451
+ const getInvoice = GET(
450
452
  ({ id }: { id: string }) => database.invoice.find(id),
451
453
  { description: 'One invoice, by id.' },
452
454
  )
@@ -466,7 +468,7 @@ No `fetch`, no client module, and no second copy of the invoice type. The browse
466
468
 
467
469
  Reading `invoice` starts the load, and the document goes out before the row lands: the two values are holes that fill when it does. That is the default, and it is why neither line above has a loading branch in it.
468
470
 
469
- A handler several pages share goes in a file of its own: any `.abide` can hold a `<script server>`, it answers at its directory's address, and a page imports it like any other module.
471
+ That handler is this page's alone, and no other file can import it. One a second file calls goes in a handler file: `src/invoices/getInvoice.ts`, holding `export default GET(…)`, answers at `/invoices/getInvoice`, and a page imports it like any other module. It is the app's public surface, so the command line and MCP list it too.
470
472
 
471
473
  Read on: [Reading data](/data/read-data-without-writing-an-api.md) · [Loading states](/values/show-a-value-that-isnt-there-yet.md)
472
474
 
@@ -508,7 +510,7 @@ A request lands, and bytes start leaving before anything it needs has arrived. T
508
510
  | | Stage | What runs |
509
511
  | --- | --- | --- |
510
512
  | 1 | the request arrives | each `middleware` rung the root layout files |
511
- | 2 | the route resolves | the `page.abide` at that path, inside its layouts |
513
+ | 2 | the route resolves | the page file at that path, inside its layouts |
512
514
  | 3 | the render starts | every load the page reads, all of them at once |
513
515
  | 4 | the first bytes leave | the head, then the markup as far as it goes |
514
516
  | 5 | the document closes | as soon as the walk is done |
@@ -547,7 +549,7 @@ Read on: [Auth & principal](/app/know-who-is-calling.md) · [Authorization](/dat
547
549
 
548
550
  ## 2. The route resolves
549
551
 
550
- The path picks a `page.abide`, and the layouts in the directories above it wrap it. All of that renders into one document, and the root `src/layout.abide` writes it: written inside `<html>`, its `<head>` is the document's head and its `<body>` holds the chrome and the slot every page goes in. The attributes on the two are the document's own, and live as on any element.
552
+ The path picks the page file at it, and the layouts in the directories above it wrap it. All of that renders into one document, and the root `src/layout.abide` writes it: written inside `<html>`, its `<head>` is the document's head and its `<body>` holds the chrome and the slot every page goes in. The attributes on the two are the document's own, and live as on any element.
551
553
 
552
554
  ```abide src/layout.abide — excerpt
553
555
  <html lang="en">
@@ -577,9 +579,9 @@ The total time is the slowest one, not the sum — and the first byte does not w
577
579
 
578
580
  The head goes first, then the markup. A read of a value still in flight renders nothing and opens a **sink**, and the hole fills when the value lands. The markup after it keeps going out.
579
581
 
580
- ```abide src/invoices/[id]/page.abide — excerpt
582
+ ```abide src/invoices/[id].abide — excerpt
581
583
  <script server>
582
- export const getInvoice = GET(({ id }: { id: string }) =>
584
+ const getInvoice = GET(({ id }: { id: string }) =>
583
585
  database.invoice.find(id),
584
586
  )
585
587
  </script>
@@ -652,7 +654,7 @@ Read on: [Links & navigation](/pages/link-to-another-page.md) · [View transitio
652
654
  <head>
653
655
  <meta charset="utf-8">
654
656
  <script type="application/json" data-abide-seed>
655
- [["GET","/invoices/42/getInvoice",{}]]
657
+ [["GET","/__abide/handlers/invoices/id/getInvoice",{"id":"42"}]]
656
658
  </script>
657
659
  <script>/* issues each call, stashes each Promise under its key */</script>
658
660
  <link rel="stylesheet" href="/_abide/app.7f3c9a1e.css">
@@ -685,20 +687,26 @@ Those filler scripts are **byte-identical** — every one of them, in every docu
685
687
 
686
688
  ## The directory a file is in decides where it runs
687
689
 
688
- `src/` is the app, and every file in it runs on both sides unless its directory says otherwise. Group it by subject — `src/billing/page.abide`, `src/billing/server`, `src/billing/ui` — and the build enforces the split wherever the directories sit.
690
+ `src/` is the app, and every file in it runs on both sides unless its directory says otherwise. Group it by subject — `src/billing/index.abide`, `src/billing/server`, `src/billing/lib` — and the build enforces the split wherever the directories sit.
689
691
 
690
692
  | Directory | What is in it | Reached by |
691
693
  | --- | --- | --- |
692
- | `src/**/` | addresses: pages, layouts, and the handlers a `<script server>` exports | anything; the browser gets a handler's client |
694
+ | `src/**/` | addresses: pages, layouts, error pages and handler files | anything; the browser gets a handler's client |
695
+ | `**/lib/**` | components and modules, which are no address | anything |
693
696
  | `**/server/**` | code the browser never loads | the server alone; a page importing it is a compile error |
697
+ | `**/tests/**` | the app's tests | `bun test`; never served |
698
+ | `**/public/**` | files answered as they are | a request for one, at its path below the directory |
699
+
700
+ A directory holds addresses and those four alone. Any other file directly in it fails the build, naming the file and the `lib` to move it into, so what a directory lists is what it answers.
694
701
 
695
702
  `#src/…` is the one import alias, mapping onto `src/`. Two files are addresses rather than names, and both are optional:
696
703
 
697
704
  | File | What it is |
698
705
  | --- | --- |
699
706
  | `src/layout.abide` | the root layout: the document and the chrome every page shares, and the app's own `<script server>` |
707
+ | `src/launcher.abide` | the desktop app's launcher |
700
708
 
701
- Read on: [Config](/app/configure-the-app.md) · [Lifecycle](/app/run-code-at-start-and-stop.md)
709
+ Read on: [Routes](/pages/add-a-page.md) · [Config](/app/configure-the-app.md) · [Lifecycle](/app/run-code-at-start-and-stop.md)
702
710
 
703
711
  ## Next
704
712
 
@@ -827,7 +835,7 @@ The options are `schema`, `transform`, `store`, `identity`, `by`, `ttl`, `thrott
827
835
 
828
836
  **`reactive(v)` — a value the page owns** — A contact's handle. Type into the field and watch the heading.
829
837
 
830
- ```abide OwnedHandle.abide
838
+ ```abide lib/OwnedHandle.abide
831
839
  <script>
832
840
  import { reactive } from '@abide/abide'
833
841
 
@@ -853,7 +861,7 @@ Read on: [Values by name](/templates/read-and-write-a-value-by-name.md)
853
861
 
854
862
  **`reactive(load)` — the settled value, never the promise** — A contact fetched when the page is built. Reload the page to watch it land again.
855
863
 
856
- ```abide Record.abide
864
+ ```abide lib/Record.abide
857
865
  <script server>
858
866
  import { refuse } from '@abide/abide'
859
867
  import { GET } from '@abide/abide/server'
@@ -862,7 +870,7 @@ const CONTACTS: Record<string, { name: string; role: string }> = {
862
870
  '42': { name: 'Ada Lovelace', role: 'Engineer' },
863
871
  }
864
872
 
865
- export const getContact = GET(
873
+ const getContact = GET(
866
874
  async (args: { id: string }) => {
867
875
  // A slow service, so the wait is on screen.
868
876
  await Bun.sleep(900)
@@ -900,7 +908,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Reloa
900
908
 
901
909
  **`reactive(v, { transform })` — what is stored is not what you typed** — A contact's handle, normalized on the way in. Type a name with capitals and a space.
902
910
 
903
- ```abide TransformHandle.abide
911
+ ```abide lib/TransformHandle.abide
904
912
  <script>
905
913
  import { reactive } from '@abide/abide'
906
914
 
@@ -958,7 +966,7 @@ Both take `schema`, `transform`, `store`, `global`, `tags`, `throttle` and `debo
958
966
 
959
967
  **`reactive(() => …)` — an auto-tracked derivation** — A deal, and the total that follows its seats. Step the seats.
960
968
 
961
- ```abide Deal.abide
969
+ ```abide lib/Deal.abide
962
970
  <script>
963
971
  import { reactive } from '@abide/abide'
964
972
 
@@ -1007,7 +1015,7 @@ There is one `set` in the design and it is `Reactive`'s, so writing to a derived
1007
1015
 
1008
1016
  **`reactive(() => call(args))` — a load the value adopts** — A profile panel behind two tabs. Open the other person, then come back to the first.
1009
1017
 
1010
- ```abide Profile.abide
1018
+ ```abide lib/Profile.abide
1011
1019
  <script server>
1012
1020
  import { GET } from '@abide/abide/server'
1013
1021
 
@@ -1020,7 +1028,7 @@ const PEOPLE: Record<string, { name: string; role: string }> = {
1020
1028
  const OPENED: Record<string, number> = {}
1021
1029
 
1022
1030
  /** Runs once per request that reaches it, so `opened` counts loads. */
1023
- export const getProfile = GET(
1031
+ const getProfile = GET(
1024
1032
  async (args: { id: string }) => {
1025
1033
  // A slow service, so the wait is on screen.
1026
1034
  await Bun.sleep(450)
@@ -1082,12 +1090,12 @@ Read on: [Reading data](/data/read-data-without-writing-an-api.md) · [Caching](
1082
1090
 
1083
1091
  **A write to a derived value stands until the next recompute** — An edit box seeded from a person the server sent. Type into it, then reload.
1084
1092
 
1085
- ```abide Draft.abide
1093
+ ```abide lib/Draft.abide
1086
1094
  <script server>
1087
1095
  import { GET } from '@abide/abide/server'
1088
1096
 
1089
1097
  /** The same person on every call, so a refill changes no field. */
1090
- export const getPerson = GET(
1098
+ const getPerson = GET(
1091
1099
  async (args: { id: string }) => {
1092
1100
  // A slow service, so the wait is on screen.
1093
1101
  await Bun.sleep(450)
@@ -1135,7 +1143,7 @@ A write of a settled value is not a load — `ready()` stays true and `busy()` n
1135
1143
 
1136
1144
  **`reactive((args) => …)` — a factory, one entry per key** — The contact search, filtering as you type. Type toward Ada, then backspace over what you typed.
1137
1145
 
1138
- ```abide Search.abide
1146
+ ```abide lib/Search.abide
1139
1147
  <script server>
1140
1148
  import { reactive } from '@abide/abide'
1141
1149
  import { GET } from '@abide/abide/server'
@@ -1150,7 +1158,7 @@ const matching = reactive(async (args: { query: string }) => {
1150
1158
  return PEOPLE.filter((one) => one.toLowerCase().includes(word))
1151
1159
  })
1152
1160
 
1153
- export const searchContacts = GET(matching, {
1161
+ const searchContacts = GET(matching, {
1154
1162
  description: 'Contacts whose name contains a query.',
1155
1163
  })
1156
1164
  </script>
@@ -1187,7 +1195,7 @@ Read on: [Caching](/values/load-once-per-set-of-arguments.md) · [Throttle & deb
1187
1195
 
1188
1196
  **A read after an `await` registers nothing** — Two counts under one contact filter, differing in one line. Change the filter.
1189
1197
 
1190
- ```abide Counts.abide
1198
+ ```abide lib/Counts.abide
1191
1199
  <script>
1192
1200
  import { reactive } from '@abide/abide'
1193
1201
 
@@ -1239,12 +1247,12 @@ Read on: [Reloading](/values/decide-when-a-value-reloads.md)
1239
1247
 
1240
1248
  **`reactive(() => gate ? … : …)` — a branch on a gate that has not landed** — A contact an auditor may read and not edit. Watch the top Save button, and reload the page to see it again.
1241
1249
 
1242
- ```abide Contact.abide
1250
+ ```abide lib/Contact.abide
1243
1251
  <script server>
1244
1252
  import { GET } from '@abide/abide/server'
1245
1253
 
1246
1254
  /** What this caller may do with the record, which only the server knows. */
1247
- export const getViewer = GET(
1255
+ const getViewer = GET(
1248
1256
  async () => {
1249
1257
  // A slow service, so the wait is on screen.
1250
1258
  await Bun.sleep(1600)
@@ -1277,7 +1285,7 @@ The hole is where the wait shows. `mode === 'locked'` throws the same way, and a
1277
1285
 
1278
1286
  Only you know which answer is safe before there is one, so say it with the probe:
1279
1287
 
1280
- ```abide Contact.abide — excerpt
1288
+ ```abide lib/Contact.abide — excerpt
1281
1289
  <button disabled={!mode.ready() || mode === 'locked'}>Save</button></li>
1282
1290
  ```
1283
1291
 
@@ -1327,7 +1335,7 @@ Every other option is the one any reactive value takes. `args` refuses a key (11
1327
1335
 
1328
1336
  **`reactive<Turn[]>([])` — a channel anything can reach** — A conversation's composer, and the line above it that reports who spoke last. Press Send and watch a component the composer never names.
1329
1337
 
1330
- ```ts onboarding.ts
1338
+ ```ts lib/onboarding.ts
1331
1339
  import { reactive } from '@abide/abide'
1332
1340
 
1333
1341
  export type Turn = { id: number; speaker: string; text: string }
@@ -1336,7 +1344,7 @@ export type Turn = { id: number; speaker: string; text: string }
1336
1344
  export const onboarding = reactive<Turn[]>([], { by: 'id' })
1337
1345
  ```
1338
1346
 
1339
- ```abide Chat.abide
1347
+ ```abide lib/Chat.abide
1340
1348
  <script>
1341
1349
  import { reactive } from '@abide/abide'
1342
1350
  import { onboarding } from './onboarding.ts'
@@ -1359,7 +1367,7 @@ const draft = reactive('How do I rotate an API key?')
1359
1367
  </div>
1360
1368
  ```
1361
1369
 
1362
- ```abide LastSpoken.abide
1370
+ ```abide lib/LastSpoken.abide
1363
1371
  <script>
1364
1372
  import { reactive } from '@abide/abide'
1365
1373
  // The same import reaches the same channel. No prop, no provider.
@@ -1395,7 +1403,7 @@ Read on: [Live updates](/data/keep-a-room-of-callers-in-sync.md) · [Loading sta
1395
1403
 
1396
1404
  **`reactive((args) => [])` — a channel per key** — Two conversations behind one declaration. Send something in onboarding, switch to billing, send there, and come back.
1397
1405
 
1398
- ```abide Conversation.abide
1406
+ ```abide lib/Conversation.abide
1399
1407
  <script>
1400
1408
  import { reactive } from '@abide/abide'
1401
1409
 
@@ -1449,7 +1457,7 @@ Read on: [Caching](/values/load-once-per-set-of-arguments.md) · [Schemas](/data
1449
1457
 
1450
1458
  **`patch` with a trim — a channel that keeps three turns** — A transcript pane over a conversation already three turns old. Send a fourth and watch the top of it.
1451
1459
 
1452
- ```abide TrimmedChat.abide
1460
+ ```abide lib/TrimmedChat.abide
1453
1461
  <script>
1454
1462
  import { reactive } from '@abide/abide'
1455
1463
 
@@ -1512,7 +1520,7 @@ Read on: [Recent values](/values/keep-the-last-few-values.md) · [Lists](/templa
1512
1520
 
1513
1521
  **`{ transform }` on a channel — a message refused by name** — The composer holding a message longer than the conversation takes. Press Send, then cut it down and press again.
1514
1522
 
1515
- ```abide LimitedChat.abide
1523
+ ```abide lib/LimitedChat.abide
1516
1524
  <script>
1517
1525
  import { reactive, refuse } from '@abide/abide'
1518
1526
  import { z } from 'zod'
@@ -1589,11 +1597,11 @@ Read on: [Throttle & debounce](/values/slow-down-a-value-that-changes-too-fast.m
1589
1597
 
1590
1598
  ## Following a channel sees turns, not every message
1591
1599
 
1592
- `for await (const turns of onboarding)` walks the channel's cursor. On a value with no streaming producer the cursor hands back the standing value once per turn, however many writes landed since the last, and a patch in place is one of them (2.6). A burst of three messages in one turn is one step, holding all three.
1600
+ `onboarding.watch((turns) => …)` follows the channel. The effect runs once at the end of a turn, however many writes landed in it, and a patch in place is one of them (12.10). A burst of three messages in one turn is one run, holding all three.
1593
1601
 
1594
- So there is no exactly-once tail of a channel. Code that has to act on every message acts where it writes it — in `say`, beside the push. The cursor never hands back an `undefined` value (6.10).
1602
+ So there is no exactly-once tail of a channel. Code that has to act on every message acts where it writes it — in `say`, beside the push.
1595
1603
 
1596
- A stream is the other case: its cursor yields from the start of the run in flight and then live, one step per chunk (2.5), and a capped window still delivers every chunk (5.10).
1604
+ `for await` is not a follow. Over a value it yields the settled value once and ends, as `await` does (2.5). Over a stream it yields each chunk of the run and ends at the close (2.6), and a capped window still delivers every chunk (5.10).
1597
1605
 
1598
1606
  Read on: [Watching values](/values/do-something-when-a-value-changes.md) · [Streaming data](/data/send-data-as-it-arrives.md)
1599
1607
 
@@ -1659,7 +1667,7 @@ A list arrives once and then changes for the rest of the session. Refetching it
1659
1667
 
1660
1668
  **`s.patch(mutate)` — change in place, and wake the readers** — A regional dashboard whose order counts arrive one region at a time.
1661
1669
 
1662
- ```abide Regions.abide
1670
+ ```abide lib/Regions.abide
1663
1671
  <script>
1664
1672
  import { reactive } from '@abide/abide'
1665
1673
 
@@ -1716,7 +1724,7 @@ Read on: [Local state](/values/show-a-value-that-changes.md) · [Watching values
1716
1724
 
1717
1725
  **`applyOrderCount(byId, …)` — the same fold, reached without `patch`** — The same dashboard, and a helper handed the held table directly.
1718
1726
 
1719
- ```abide UnpatchedRegions.abide
1727
+ ```abide lib/UnpatchedRegions.abide
1720
1728
  <script>
1721
1729
  import { reactive } from '@abide/abide'
1722
1730
 
@@ -1787,7 +1795,7 @@ Read on: [Channels](/values/let-anything-publish-and-anything-read.md) · [Watch
1787
1795
 
1788
1796
  **`reactive(() => rows, { transform })` — an array in, an index held** — The regions table arriving from the server as a list, on a dashboard whose updates name ids.
1789
1797
 
1790
- ```abide LoadedRegions.abide
1798
+ ```abide lib/LoadedRegions.abide
1791
1799
  <script server>
1792
1800
  import { GET } from '@abide/abide/server'
1793
1801
 
@@ -1800,7 +1808,7 @@ const BOOKED: Region[] = [
1800
1808
  { id: 'west', name: 'West', orders: 91 },
1801
1809
  ]
1802
1810
 
1803
- export const getRegions = GET(
1811
+ const getRegions = GET(
1804
1812
  async (): Promise<Region[]> => {
1805
1813
  // A slow service, so the wait is on screen.
1806
1814
  await Bun.sleep(450)
@@ -1915,7 +1923,7 @@ Two booleans, four states, and the states an interface actually shows are the co
1915
1923
 
1916
1924
  **A read starts the load and leaves a hole** — An invoice with no loading branch anywhere in it. Reload the page and watch the three fields fill.
1917
1925
 
1918
- ```abide Invoice.abide
1926
+ ```abide lib/Invoice.abide
1919
1927
  <script server>
1920
1928
  import { GET } from '@abide/abide/server'
1921
1929
 
@@ -1929,7 +1937,7 @@ const INVOICES: Record<string, Invoice> = {
1929
1937
  },
1930
1938
  }
1931
1939
 
1932
- export const getInvoice = GET(
1940
+ const getInvoice = GET(
1933
1941
  async ({ id }: { id: string }) => {
1934
1942
  // A slow database, so the wait is on screen.
1935
1943
  await Bun.sleep(900)
@@ -1977,7 +1985,7 @@ Read on: [Streaming HTML](/pages/send-the-page-before-the-data-lands.md) · [Exp
1977
1985
 
1978
1986
  **`NotLanded` — a read before the value arrives** — A contact's email, with a Copy button. Open another contact and press Copy before the address lands.
1979
1987
 
1980
- ```abide CopyEmail.abide
1988
+ ```abide lib/CopyEmail.abide
1981
1989
  <script server>
1982
1990
  import { GET } from '@abide/abide/server'
1983
1991
 
@@ -1987,7 +1995,7 @@ const EMAILS: Record<string, string> = {
1987
1995
  katherine: 'katherine@example.com',
1988
1996
  }
1989
1997
 
1990
- export const getContact = GET(
1998
+ const getContact = GET(
1991
1999
  async ({ id }: { id: string }) => {
1992
2000
  // A slow directory, so there is time to press Copy before it lands.
1993
2001
  await Bun.sleep(1500)
@@ -2042,14 +2050,14 @@ Outside the render flow there is no hole to leave, so the throw reaches your cod
2042
2050
 
2043
2051
  **The first load and every one after are `ready()` apart** — An invoice with a Refresh beside it. Wait for the figures, then press Refresh and watch what does not disappear.
2044
2052
 
2045
- ```abide RefreshInvoice.abide
2053
+ ```abide lib/RefreshInvoice.abide
2046
2054
  <script server>
2047
2055
  import { GET } from '@abide/abide/server'
2048
2056
 
2049
2057
  // Counts the loads: the total never moves, so this is what shows a refresh.
2050
2058
  let sent = 0
2051
2059
 
2052
- export const getInvoiceTotal = GET(
2060
+ const getInvoiceTotal = GET(
2053
2061
  async (_args: { id: string }) => {
2054
2062
  // A slow database, so the wait is on screen.
2055
2063
  await Bun.sleep(900)
@@ -2098,7 +2106,7 @@ Read on: [Reloading](/values/decide-when-a-value-reloads.md)
2098
2106
 
2099
2107
  **A derived value is busy while what it reads is** — A count of unpaid invoices, derived from a list still loading. Reload the page and watch the figure before the list lands.
2100
2108
 
2101
- ```abide Unpaid.abide
2109
+ ```abide lib/Unpaid.abide
2102
2110
  <script server>
2103
2111
  import { GET } from '@abide/abide/server'
2104
2112
 
@@ -2108,7 +2116,7 @@ const INVOICES = [
2108
2116
  { id: '4023', total: '$76.50', paid: false },
2109
2117
  ]
2110
2118
 
2111
- export const listInvoices = GET(
2119
+ const listInvoices = GET(
2112
2120
  async () => {
2113
2121
  // A slow database, so the wait is on screen.
2114
2122
  await Bun.sleep(900)
@@ -2150,7 +2158,7 @@ Read on: [Derived values](/values/derive-a-value-from-other-values.md)
2150
2158
 
2151
2159
  **`s.error()` hands the failure back rather than throwing** — An invoice whose billing service is down. The page renders the message instead of letting it reach the error page.
2152
2160
 
2153
- ```abide UnreachableInvoice.abide
2161
+ ```abide lib/UnreachableInvoice.abide
2154
2162
  <script server>
2155
2163
  import { refuse } from '@abide/abide'
2156
2164
  import { GET } from '@abide/abide/server'
@@ -2170,7 +2178,7 @@ const INVOICES: Record<string, { total: string }> = {
2170
2178
  '4020': { total: '$980.00' },
2171
2179
  }
2172
2180
 
2173
- export const getBilledInvoice = GET(
2181
+ const getBilledInvoice = GET(
2174
2182
  async ({ id }: { id: string }) => {
2175
2183
  // A slow billing service, so the wait is on screen.
2176
2184
  await Bun.sleep(600)
@@ -2278,8 +2286,7 @@ Calling it hands back a `Reactive` for that key, so a probe on the result asks a
2278
2286
 
2279
2287
  **`reactive((args) => …)` — identity per set of arguments** — Two tiles on one dashboard, each on its own range. Put them both on 30d, then send one back to 7d.
2280
2288
 
2281
- ```abide Tiles.abide
2282
- <script server>
2289
+ ```ts getMetrics.ts
2283
2290
  import { reactive } from '@abide/abide'
2284
2291
  import { GET } from '@abide/abide/server'
2285
2292
 
@@ -2300,13 +2307,13 @@ const metricsFor = reactive(async ({ range }: { range: string }) => {
2300
2307
  return { orders: found.orders, average: found.average, computed }
2301
2308
  })
2302
2309
 
2303
- export const getMetrics = GET(metricsFor, {
2310
+ export default GET(metricsFor, {
2304
2311
  description: 'Metrics for a range.',
2305
2312
  })
2306
- </script>
2313
+ ```
2307
2314
 
2315
+ ```abide lib/Tiles.abide
2308
2316
  <script>
2309
- import { reactive } from '@abide/abide'
2310
2317
  import Average from './Average.abide'
2311
2318
  import Total from './Total.abide'
2312
2319
  </script>
@@ -2315,10 +2322,10 @@ import Total from './Total.abide'
2315
2322
  <Average/>
2316
2323
  ```
2317
2324
 
2318
- ```abide Total.abide
2325
+ ```abide lib/Total.abide
2319
2326
  <script>
2320
2327
  import { reactive } from '@abide/abide'
2321
- import { getMetrics } from './Tiles.abide'
2328
+ import getMetrics from '../getMetrics.ts'
2322
2329
 
2323
2330
  const RANGES = ['7d', '30d']
2324
2331
 
@@ -2344,10 +2351,10 @@ const metrics = reactive(() => getMetrics({ range }))
2344
2351
  </ul>
2345
2352
  ```
2346
2353
 
2347
- ```abide Average.abide
2354
+ ```abide lib/Average.abide
2348
2355
  <script>
2349
2356
  import { reactive } from '@abide/abide'
2350
- import { getMetrics } from './Tiles.abide'
2357
+ import getMetrics from '../getMetrics.ts'
2351
2358
 
2352
2359
  const RANGES = ['7d', '30d']
2353
2360
 
@@ -2386,7 +2393,7 @@ Read on: [Derived values](/values/derive-a-value-from-other-values.md) · [Reloa
2386
2393
 
2387
2394
  **One entry per canonical args key** — One panel, and three ways to spell the query behind it. Change the spelling, then change the region.
2388
2395
 
2389
- ```abide Query.abide
2396
+ ```abide lib/Query.abide
2390
2397
  <script server>
2391
2398
  import { reactive } from '@abide/abide'
2392
2399
  import { GET } from '@abide/abide/server'
@@ -2406,7 +2413,7 @@ const queryMetricsFor = reactive(async (query: Query) => {
2406
2413
  return { orders: ORDERS[query.region] ?? 0, computed }
2407
2414
  })
2408
2415
 
2409
- export const getQueryMetrics = GET(queryMetricsFor, {
2416
+ const getQueryMetrics = GET(queryMetricsFor, {
2410
2417
  description: 'Metrics for a range and a region.',
2411
2418
  })
2412
2419
  </script>
@@ -2499,7 +2506,7 @@ Two costs, and both are yours rather than abide's.
2499
2506
 
2500
2507
  And a global value is bounded by its own `ttl` and the app's `invalidate` and by nothing else. There is no byte ceiling and no eviction count: measuring an arbitrary value costs an O(size) walk, and an LRU would evict an entry a reader is mid-flight on. `global: true` with `ttl: Infinity` is an unbounded cache — unbounded because the app asked for it, the bound being a number the app knows and abide does not.
2501
2508
 
2502
- A global value over a stream fans one producer out to many readers, and it needs no retention to do it. A reader arriving mid-stream gets the run in flight from its start and then goes live (2.5), and a reader leaving does not cancel the producer for the ones that remain (11.39).
2509
+ A global value over a stream fans one producer out to many readers, and it needs no retention to do it. A reader arriving mid-stream gets the run in flight from its first chunk to its close (2.6), and a reader leaving does not cancel the producer for the ones that remain (11.39).
2503
2510
 
2504
2511
  Read on: [Recent values](/values/keep-the-last-few-values.md) · [Auth & principal](/app/know-who-is-calling.md) · [Authorization](/data/decide-who-may-call-what.md) · [Response types](/data/answer-with-something-other-than-json.md)
2505
2512
 
@@ -2507,7 +2514,7 @@ Read on: [Recent values](/values/keep-the-last-few-values.md) · [Auth & princip
2507
2514
 
2508
2515
  **`tags` — a selection that reaches across keyed values** — Two regions, each with revenue and recent orders behind keyed values that never mention one another. Press one region's button and watch the other stand still.
2509
2516
 
2510
- ```abide Regions.abide
2517
+ ```abide lib/Regions.abide
2511
2518
  <script>
2512
2519
  import Region from './Region.abide'
2513
2520
  </script>
@@ -2516,7 +2523,7 @@ import Region from './Region.abide'
2516
2523
  <Region region="south"/>
2517
2524
  ```
2518
2525
 
2519
- ```abide Region.abide
2526
+ ```abide lib/Region.abide
2520
2527
  <script server>
2521
2528
  import { GET } from '@abide/abide/server'
2522
2529
 
@@ -2530,7 +2537,7 @@ const COUNT: Record<string, number> = { north: 128, south: 94 }
2530
2537
  const computed: Record<string, number> = { north: 0, south: 0 }
2531
2538
  const loaded: Record<string, number> = { north: 0, south: 0 }
2532
2539
 
2533
- export const getTotals = GET(
2540
+ const getTotals = GET(
2534
2541
  async ({ region }: { region: string }) => {
2535
2542
  // A slow database, so the wait is on screen.
2536
2543
  await Bun.sleep(450)
@@ -2543,7 +2550,7 @@ export const getTotals = GET(
2543
2550
  { description: 'Order totals for a region.' },
2544
2551
  )
2545
2552
 
2546
- export const getOrders = GET(
2553
+ const getOrders = GET(
2547
2554
  async ({ region }: { region: string }) => {
2548
2555
  await Bun.sleep(450)
2549
2556
  loaded[region] = (loaded[region] ?? 0) + 1
@@ -2637,7 +2644,7 @@ There are three answers, and they are three because they fail differently.
2637
2644
 
2638
2645
  **`invalidate()` marks stale; `refresh()` reloads now** — A shelf count with both triggers under it. Press Mark stale and watch nothing happen, then press Reload now.
2639
2646
 
2640
- ```abide Level.abide
2647
+ ```abide lib/Level.abide
2641
2648
  <script server>
2642
2649
  import { reactive } from '@abide/abide'
2643
2650
  import { GET } from '@abide/abide/server'
@@ -2654,7 +2661,7 @@ const levelOfShelf = reactive(async (_shelf: Shelf) => {
2654
2661
  return { onHand: 12, counted }
2655
2662
  })
2656
2663
 
2657
- export const getLevel = GET(levelOfShelf, {
2664
+ const getLevel = GET(levelOfShelf, {
2658
2665
  description: 'Stock on a shelf, by warehouse and sku.',
2659
2666
  })
2660
2667
  </script>
@@ -2747,7 +2754,7 @@ There is no bare form wider than the scope. A probe reports and a trigger acts,
2747
2754
 
2748
2755
  **`k.refresh(Partial<Args>)` — a slice of the args space** — Two warehouses holding the same shelf. Book one out of east and watch which of the two loads again.
2749
2756
 
2750
- ```abide Shelves.abide
2757
+ ```abide lib/Shelves.abide
2751
2758
  <script server>
2752
2759
  import { reactive } from '@abide/abide'
2753
2760
  import { GET, POST } from '@abide/abide/server'
@@ -2766,11 +2773,11 @@ const stockForShelf = reactive(async ({ warehouse, sku }: Shelf) => {
2766
2773
  return { onHand: ON_HAND[shelf] ?? 0, loads: loads[shelf] }
2767
2774
  })
2768
2775
 
2769
- export const getStock = GET(stockForShelf, {
2776
+ const getStock = GET(stockForShelf, {
2770
2777
  description: 'Stock on a shelf, by warehouse and sku.',
2771
2778
  })
2772
2779
 
2773
- export const bookOut = POST(
2780
+ const bookOut = POST(
2774
2781
  ({ warehouse, sku }: Shelf) => {
2775
2782
  const shelf = `${warehouse}/${sku}`
2776
2783
  ON_HAND[shelf] = (ON_HAND[shelf] ?? 0) - 1
@@ -2835,7 +2842,7 @@ Read on: [Offline](/app/know-when-the-browser-goes-offline.md) · [Watching valu
2835
2842
 
2836
2843
  **`{ ttl }` — expires with nobody asking** — A shelf count that keeps for three seconds. Press Read it again straight away, then wait and press it once more.
2837
2844
 
2838
- ```abide Shelf.abide
2845
+ ```abide lib/Shelf.abide
2839
2846
  <script server>
2840
2847
  import { reactive } from '@abide/abide'
2841
2848
  import { GET } from '@abide/abide/server'
@@ -2856,7 +2863,7 @@ const stockForShelf = reactive(
2856
2863
  { ttl: 3_000 },
2857
2864
  )
2858
2865
 
2859
- export const getCachedStock = GET(stockForShelf, {
2866
+ const getCachedStock = GET(stockForShelf, {
2860
2867
  description: 'Stock on a shelf, by warehouse and sku.',
2861
2868
  })
2862
2869
  </script>
@@ -2953,7 +2960,7 @@ Declaring both is a build error naming the value, there being no reading under w
2953
2960
 
2954
2961
  **`{ debounce }` — one load once the typing stops** — A figure filtered by a region you type. Clear the box and type north, and watch the figure rather than the field.
2955
2962
 
2956
- ```abide Orders.abide
2963
+ ```abide lib/Orders.abide
2957
2964
  <script server>
2958
2965
  import { GET } from '@abide/abide/server'
2959
2966
 
@@ -2965,7 +2972,7 @@ const BY_REGION: Record<string, number> = {
2965
2972
  north: 128,
2966
2973
  }
2967
2974
 
2968
- export const countOrders = GET(
2975
+ const countOrders = GET(
2969
2976
  async (args: { region: string }) => {
2970
2977
  // A slow service, so the wait is on screen.
2971
2978
  await Bun.sleep(450)
@@ -3013,7 +3020,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Reloa
3013
3020
 
3014
3021
  **`{ throttle }` — the first arrival, then a ceiling** — A feed arriving twenty times a second beside the tile that shows it. Watch the two figures drift apart.
3015
3022
 
3016
- ```abide Live.abide
3023
+ ```abide lib/Live.abide
3017
3024
  <script>
3018
3025
  import { reactive, watch } from '@abide/abide'
3019
3026
 
@@ -3070,13 +3077,13 @@ Read on: [Caching](/values/load-once-per-set-of-arguments.md) · [Form binding](
3070
3077
 
3071
3078
  **`identity: structural` — an equal reload wakes nobody** — Two regional totals and the chart drawn from them. Record a sale and the chart redraws; refresh with nothing new and it does not.
3072
3079
 
3073
- ```abide Chart.abide
3080
+ ```abide lib/Chart.abide
3074
3081
  <script server>
3075
3082
  import { GET, POST } from '@abide/abide/server'
3076
3083
 
3077
3084
  const totals = { north: 1240, south: 980 }
3078
3085
 
3079
- export const getSales = GET(
3086
+ const getSales = GET(
3080
3087
  async () => {
3081
3088
  await Bun.sleep(150)
3082
3089
  // A fresh record on every load, the way a query result is.
@@ -3085,7 +3092,7 @@ export const getSales = GET(
3085
3092
  { description: 'Sales by region.' },
3086
3093
  )
3087
3094
 
3088
- export const recordSale = POST(
3095
+ const recordSale = POST(
3089
3096
  () => {
3090
3097
  totals.north += 120
3091
3098
  },
@@ -3174,7 +3181,7 @@ What comes back is whatever the thunk built, which is an ordinary `Reactive` wit
3174
3181
 
3175
3182
  **`reactive.share(key, build)` — get-or-create in this scope** — A now-playing bar over a library of tracks. Press a title and watch the bar, which the library never speaks to.
3176
3183
 
3177
- ```abide Library.abide
3184
+ ```abide lib/Library.abide
3178
3185
  <script>
3179
3186
  import { reactive } from '@abide/abide'
3180
3187
  import NowPlaying from './NowPlaying.abide'
@@ -3195,7 +3202,7 @@ reactive.share('nowPlaying', () => reactive('nothing'))
3195
3202
  </div>
3196
3203
  ```
3197
3204
 
3198
- ```abide NowPlaying.abide
3205
+ ```abide lib/NowPlaying.abide
3199
3206
  <script>
3200
3207
  import { reactive } from '@abide/abide'
3201
3208
 
@@ -3208,7 +3215,7 @@ const playing = reactive.share('nowPlaying', () => reactive('nothing'))
3208
3215
  </ul>
3209
3216
  ```
3210
3217
 
3211
- ```abide TrackRow.abide
3218
+ ```abide lib/TrackRow.abide
3212
3219
  <script>
3213
3220
  import { props, reactive } from '@abide/abide'
3214
3221
 
@@ -3234,7 +3241,7 @@ Read on: [Components](/templates/reuse-a-piece-of-markup.md) · [Form binding](/
3234
3241
 
3235
3242
  **`reactive.share` hands back the one that won** — One key asked for twice, once properly and once with the value built outside the call. Step the volume and watch the third tile.
3236
3243
 
3237
- ```abide Volume.abide
3244
+ ```abide lib/Volume.abide
3238
3245
  <script>
3239
3246
  import { type Reactive, reactive } from '@abide/abide'
3240
3247
 
@@ -3265,7 +3272,7 @@ Building the value outside the call and passing it in fails silently, which is w
3265
3272
 
3266
3273
  `Shared` is your app's own registry, widened by declaration merging — and an **opt-in tightening**, not a precondition. A key it declares is checked against it; a key it does not is inferred from the thunk, so `reactive.share` works before you have written one.
3267
3274
 
3268
- ```ts src/shared.ts — excerpt
3275
+ ```ts src/lib/shared.ts — excerpt
3269
3276
  declare module '@abide/abide' {
3270
3277
  interface Shared {
3271
3278
  nowPlaying: Reactive<string>
@@ -3283,7 +3290,7 @@ Read on: [Channels](/values/let-anything-publish-and-anything-read.md) · [Live
3283
3290
 
3284
3291
  **A shared value lives where it was created, and below** — Two shelves rendered from one component, each with a control below it asking for the same key. Queue on one shelf and watch the other.
3285
3292
 
3286
- ```abide Shelves.abide
3293
+ ```abide lib/Shelves.abide
3287
3294
  <script>
3288
3295
  import Shelf from './Shelf.abide'
3289
3296
  </script>
@@ -3292,7 +3299,7 @@ import Shelf from './Shelf.abide'
3292
3299
  <Shelf name="ambient"/>
3293
3300
  ```
3294
3301
 
3295
- ```abide Shelf.abide
3302
+ ```abide lib/Shelf.abide
3296
3303
  <script>
3297
3304
  import { props, reactive } from '@abide/abide'
3298
3305
  import QueueControl from './QueueControl.abide'
@@ -3311,7 +3318,7 @@ let queued = reactive.share('queued', () => reactive('nothing'))
3311
3318
  <QueueControl name={name}/>
3312
3319
  ```
3313
3320
 
3314
- ```abide QueueControl.abide
3321
+ ```abide lib/QueueControl.abide
3315
3322
  <script>
3316
3323
  import { props, reactive } from '@abide/abide'
3317
3324
 
@@ -3370,7 +3377,7 @@ The effect is an `Effect`, and what it may return is a `Disposer`. Both calls ha
3370
3377
 
3371
3378
  **`watch(effect)` — run something outside the page on change** — A contact's handle, and the browser tab that names what you are editing. Type into the field, then press Reset.
3372
3379
 
3373
- ```abide TabTitle.abide
3380
+ ```abide lib/TabTitle.abide
3374
3381
  <script>
3375
3382
  import { reactive, watch } from '@abide/abide'
3376
3383
 
@@ -3412,7 +3419,7 @@ Read on: [Derived values](/values/derive-a-value-from-other-values.md) · [Scrip
3412
3419
 
3413
3420
  **`() => Disposer` — each rerun tears down the one before it** — Notes on a contact, saved a beat after you stop typing. Type a few characters quickly and watch the saved line.
3414
3421
 
3415
- ```abide Autosave.abide
3422
+ ```abide lib/Autosave.abide
3416
3423
  <script>
3417
3424
  import { reactive, watch } from '@abide/abide'
3418
3425
 
@@ -3457,7 +3464,7 @@ Read on: [Throttle & debounce](/values/slow-down-a-value-that-changes-too-fast.m
3457
3464
 
3458
3465
  **`watch(sources, effect)` — reads both, wakes on one** — An audit line that should follow a reassignment and not every keystroke. Type into the notes, then change the owner.
3459
3466
 
3460
- ```abide Assignment.abide
3467
+ ```abide lib/Assignment.abide
3461
3468
  <script>
3462
3469
  import { reactive, watch } from '@abide/abide'
3463
3470
 
@@ -3536,7 +3543,7 @@ Read on: [Derived values](/values/derive-a-value-from-other-values.md)
3536
3543
 
3537
3544
  **`flushSync()` — measure the page the write left** — A contact's notes in a box that scrolls. Add a note — the handler measures the list straight after the write, and lands on the note it just added.
3538
3545
 
3539
- ```abide Notes.abide
3546
+ ```abide lib/Notes.abide
3540
3547
  <script>
3541
3548
  import { flushSync, reactive } from '@abide/abide'
3542
3549
 
@@ -3604,7 +3611,7 @@ The window is an array the value holds. You append to it and trim it in one `pat
3604
3611
 
3605
3612
  **`patch` with a trim — a window onto the recent past** — A pane over an answer arriving one token at a time. Press Next token a few times and watch the top of the list.
3606
3613
 
3607
- ```abide TokenWindow.abide
3614
+ ```abide lib/TokenWindow.abide
3608
3615
  <script>
3609
3616
  import { reactive } from '@abide/abide'
3610
3617
 
@@ -3645,7 +3652,7 @@ Read on: [Patching a list](/values/patch-a-list-from-a-live-feed.md) · [Lists](
3645
3652
 
3646
3653
  Every `Reactive` holds its standing value and nothing before it. Where you want the ones before it, hold them: make the value the array, and append where you write.
3647
3654
 
3648
- A reader cannot build the window for you. `for await` over a value hands back the standing value once per turn, and a burst of writes in one turn is one step (2.6), so a window a reader assembles misses whatever landed in the same turn. The writer sees every value, which is why the window is built where the write is.
3655
+ A reader cannot build the window for you. A `watch` runs once per turn, and a burst of writes in one turn is one run (12.10), so a window a reader assembles misses whatever landed in the same turn. The writer sees every value, which is why the window is built where the write is.
3649
3656
 
3650
3657
  A channel is this same shape with many writers — a `reactive` over an array that everyone connected reads, trimmed the same way.
3651
3658
 
@@ -3655,7 +3662,7 @@ Read on: [Channels](/values/let-anything-publish-and-anything-read.md)
3655
3662
 
3656
3663
  **An undo stack is history the app keeps** — A composer draft with an Undo beside it. Type a few edits, then press Undo more than once.
3657
3664
 
3658
- ```ts undoStack.ts
3665
+ ```ts lib/undoStack.ts
3659
3666
  /** An undo is itself a write, so the position lives over a snapshot. */
3660
3667
  export function undoStack<T>(limit: number) {
3661
3668
  const held: T[] = []
@@ -3682,7 +3689,7 @@ export function undoStack<T>(limit: number) {
3682
3689
  }
3683
3690
  ```
3684
3691
 
3685
- ```abide Draft.abide
3692
+ ```abide lib/Draft.abide
3686
3693
  <script>
3687
3694
  import { reactive, watch } from '@abide/abide'
3688
3695
  import { undoStack } from './undoStack.ts'
@@ -3758,14 +3765,14 @@ A `Reactive` lives and dies with its scope unless it is told otherwise. A `store
3758
3765
 
3759
3766
  **`{ store }` — where the value lives when the process does not** — A volume the player keeps between visits. Step it up, then reload the page.
3760
3767
 
3761
- ```abide Volume.abide
3768
+ ```abide lib/Volume.abide
3762
3769
  <script server>
3763
3770
  import { GET, POST } from '@abide/abide/server'
3764
3771
 
3765
3772
  // A setting never written answers `null`, the miss.
3766
3773
  const SETTINGS: Record<string, string> = {}
3767
3774
 
3768
- export const readSetting = GET(
3775
+ const readSetting = GET(
3769
3776
  async ({ name }: { name: string }) => {
3770
3777
  // A slow database, so the wait is on screen.
3771
3778
  await Bun.sleep(450)
@@ -3774,7 +3781,7 @@ export const readSetting = GET(
3774
3781
  { description: 'A setting, by name.' },
3775
3782
  )
3776
3783
 
3777
- export const writeSetting = POST(
3784
+ const writeSetting = POST(
3778
3785
  (args: { name: string; value: string }) => {
3779
3786
  SETTINGS[args.name] = args.value
3780
3787
  },
@@ -3786,7 +3793,7 @@ export const writeSetting = POST(
3786
3793
  import type { Store } from '@abide/abide'
3787
3794
 
3788
3795
  /** Both halves or neither: `Store` has no spelling for one alone. */
3789
- export function settingsStore<T>(name: string): Store<T> {
3796
+ function settingsStore<T>(name: string): Store<T> {
3790
3797
  return {
3791
3798
  // Stored as JSON; a setting never written answers `null`, the miss.
3792
3799
  get: async () => {
@@ -3865,7 +3872,7 @@ A deploy that restarts every instance no longer stampedes the thing behind it.
3865
3872
 
3866
3873
  **One store per args key, derived from the key** — Two profiles whose settings live at two addresses. Both are read at once, and neither reads back the other's.
3867
3874
 
3868
- ```abide Profiles.abide
3875
+ ```abide lib/Profiles.abide
3869
3876
  <script server>
3870
3877
  import { reactive, type Store } from '@abide/abide'
3871
3878
  import { GET } from '@abide/abide/server'
@@ -3899,7 +3906,7 @@ const settingsCache = reactive(
3899
3906
  { store: ({ profile }) => redisStore<Settings>(`settings:${profile}`) },
3900
3907
  )
3901
3908
 
3902
- export const getSettings = GET(settingsCache, {
3909
+ const getSettings = GET(settingsCache, {
3903
3910
  description: 'The settings of a profile.',
3904
3911
  })
3905
3912
  </script>
@@ -3967,9 +3974,9 @@ Read on: [Channels](/values/let-anything-publish-and-anything-read.md) · [Recen
3967
3974
 
3968
3975
  # Data from the server
3969
3976
 
3970
- *A handler a server block exports is callable, and the method you declare it with is the difference.*
3977
+ *A handler is callable, and the method you declare it with is the difference.*
3971
3978
 
3972
- A handler a `<script server>` exports is callable. The method you declare it with is the whole difference between a read and a write.
3979
+ A handler you declare is callable. The method you declare it with is the whole difference between a read and a write.
3973
3980
 
3974
3981
  | You declare | Means |
3975
3982
  | --- | --- |
@@ -3977,13 +3984,8 @@ A handler a `<script server>` exports is callable. The method you declare it wit
3977
3984
  | `POST` / `PUT` / `PATCH` / `DELETE` | a mutation, retaining nothing by default |
3978
3985
  | a `GET` over a `global` value, patched by a `POST` | live state every caller follows |
3979
3986
 
3980
- ```abide src/invoices/api.abide — excerpt
3981
- <script server>
3982
- export const listInvoices = GET(() => database.invoice.all())
3983
- export const payInvoice = POST(
3984
- ({ id }: { id: string }) => database.invoice.pay(id),
3985
- )
3986
- </script>
3987
+ ```ts src/invoices/listInvoices.ts — excerpt
3988
+ export default GET(() => database.invoice.all())
3987
3989
  ```
3988
3990
 
3989
3991
  ## Reading data
@@ -4068,14 +4070,13 @@ Read on: [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
4068
4070
 
4069
4071
  You have a database and a page that needs a row. Most stacks make that four artifacts: a handler, a route registration, a client function, and a shared type nothing enforces. Three exist only because the halves are in different processes.
4070
4072
 
4071
- Here you write the handler.
4073
+ A `.ts` beside your pages is a handler, and its path is its address. A handler only one file calls can stay in that file instead. Either way, the page calls it by name.
4072
4074
 
4073
- ## A handler is an export of a `<script server>`
4075
+ ## A handler is a file's default export
4074
4076
 
4075
4077
  **`GET(fn)` — a handler a page imports** — An invoice read straight out of the database by a component. Reload the page and watch it land.
4076
4078
 
4077
- ```abide Invoice.abide
4078
- <script server>
4079
+ ```ts getInvoice.ts
4079
4080
  import { GET } from '@abide/abide/server'
4080
4081
 
4081
4082
  type InvoiceRow = { number: string; dueOn: string; total: string }
@@ -4084,8 +4085,8 @@ const INVOICES: Record<string, InvoiceRow> = {
4084
4085
  '4021': { number: 'INV-4021', dueOn: '2026-09-14', total: '$1,240.00' },
4085
4086
  }
4086
4087
 
4087
- /** Address from the directory and the export; schemas from the annotation. */
4088
- export const getInvoice = GET(
4088
+ /** Address from the file's path; schemas from the annotation. */
4089
+ export default GET(
4089
4090
  async ({ id }: { id: string }) => {
4090
4091
  await Bun.sleep(700) // a slow database, so the wait is on screen
4091
4092
  const row = INVOICES[id]
@@ -4094,10 +4095,12 @@ export const getInvoice = GET(
4094
4095
  },
4095
4096
  { description: 'An invoice, by id.' },
4096
4097
  )
4097
- </script>
4098
+ ```
4098
4099
 
4100
+ ```abide lib/Invoice.abide
4099
4101
  <script>
4100
4102
  import { reactive } from '@abide/abide'
4103
+ import getInvoice from '../getInvoice.ts'
4101
4104
 
4102
4105
  const invoice = reactive(() => getInvoice({ id: '4021' }))
4103
4106
  </script>
@@ -4109,28 +4112,28 @@ const invoice = reactive(() => getInvoice({ id: '4021' }))
4109
4112
  </ul>
4110
4113
  ```
4111
4114
 
4112
- *1 file, 0 API in it* — against a route, a fetch, a client wrapper and a type kept in step by hand.
4115
+ *2 files, 0 API in them* — against a route, a fetch, a client wrapper and a type kept in step by hand.
4113
4116
 
4114
- A `<script server>` block runs on the server alone. Write one in any `.abide`, wrap a function in `GET` and export it. `GET` derives the argument and result schemas from the annotations you already wrote.
4117
+ A handler file runs on the server alone. Put a `.ts` in a directory under `src`, wrap a function in `GET` and make it the default export. `GET` derives the argument and result schemas from the annotations you already wrote.
4115
4118
 
4116
- The address is the directory the file sits in, then the export's name:
4119
+ The address is the file's path, and the file's name is its last segment:
4117
4120
 
4118
- | File | Export | Answers at |
4119
- | --- | --- | --- |
4120
- | `src/data/read-data-without-writing-an-api/Invoice.abide` | `getInvoice` | `/server/read-data-without-writing-an-api/getInvoice` |
4121
- | `src/users/page.abide` | `getUser` | `/users/getUser` |
4122
- | `src/layout.abide` | `health` | `/health` |
4121
+ | File | Answers at |
4122
+ | --- | --- |
4123
+ | `src/data/read-data-without-writing-an-api/getInvoice.ts` | `/data/read-data-without-writing-an-api/getInvoice` |
4124
+ | `src/users/getUser.ts` | `/users/getUser` |
4125
+ | `src/health.ts` | `/health` |
4123
4126
 
4124
- The file's own name is never part of it. Split a directory's handlers across as many files as you like, or keep them in the page that calls them. Two files exporting one name there is a build error naming both.
4127
+ One file is one handler, and exporting anything else from it is a build error (16.73). A handler file is the app's API: a page imports it, a script calls its address, and the CLI and MCP offer it as a command and a tool.
4125
4128
 
4126
- You never write that address down. It is there so a log line or a network tab tells you which export you are looking at.
4129
+ Inside the app you never write that address down. It is there so a log line or a network tab tells you which handler you are looking at.
4127
4130
 
4128
4131
  ## A page reaches the handler through an ordinary import
4129
4132
 
4130
- ```abide src/invoices/[id]/page.abide — excerpt
4133
+ ```abide src/invoices/[id]/index.abide — excerpt
4131
4134
  <script>
4132
4135
  import { reactive, route } from '@abide/abide'
4133
- import { getInvoice } from '../Invoice.abide'
4136
+ import getInvoice from '../getInvoice.ts'
4134
4137
 
4135
4138
  const invoice = reactive(() => getInvoice({ id: route.params.id }))
4136
4139
  </script>
@@ -4143,23 +4146,65 @@ The import is real — name, argument type and return type are the ones you just
4143
4146
 
4144
4147
  The page reads `invoice`, which starts the load. The document goes out immediately with holes where the values are. The holes fill when the row lands.
4145
4148
 
4146
- ## The browser receives a generated client, not your server block
4149
+ ## A handler only one file calls is declared in that file
4150
+
4151
+ **`const getTerms = GET(fn)` — a handler its own file calls** — An invoice's payment terms, which only this card shows. The handler sits in the card's own server block, and nothing else in the app can import it.
4152
+
4153
+ ```abide lib/InvoiceTerms.abide
4154
+ <script server>
4155
+ import { GET } from '@abide/abide/server'
4156
+
4157
+ const TERMS: Record<string, { due: string; lateFee: string }> = {
4158
+ '4021': { due: 'Net 30', lateFee: '1.5% a month' },
4159
+ }
4160
+
4161
+ // No export: this file is its one caller.
4162
+ const getTerms = GET(
4163
+ async ({ id }: { id: string }) => {
4164
+ await Bun.sleep(400) // a slow database, so the wait is on screen
4165
+ const row = TERMS[id]
4166
+ if (row === undefined) throw new Error(`no terms for ${id}`)
4167
+ return row
4168
+ },
4169
+ { description: "An invoice's payment terms, by id." },
4170
+ )
4171
+ </script>
4172
+
4173
+ <script>
4174
+ import { reactive } from '@abide/abide'
4175
+
4176
+ const terms = reactive(() => getTerms({ id: '4021' }))
4177
+ </script>
4178
+
4179
+ <ul class="fields stacked">
4180
+ <li><span>terms</span><strong>{terms.due}</strong></li>
4181
+ <li><span>late fee</span><strong>{terms.lateFee}</strong></li>
4182
+ </ul>
4183
+ ```
4147
4184
 
4148
- The browser never sees the `<script server>`. Its copy of the file **generates** one export per handler in the block's place, carrying:
4185
+ A `<script server>` declares `getTerms` as a `const`, and the same file's `<script>` and template call it by name. A `.abide` exports nothing (34.17), so no other file can reach it.
4186
+
4187
+ It still answers over http — the browser's copy of the card calls it — at its file's path below `/__abide/handlers` and then its name: `/__abide/handlers/data/read-data-without-writing-an-api/lib/InvoiceTerms/getTerms` (16.70). That address is listed on no surface: not OpenAPI, not MCP, not the CLI (16.71). It takes nothing from the path, and it runs under the middleware filed at this page's route, the directory holding the card's `lib` (16.72).
4188
+
4189
+ When a second file needs it, or a script does, move it into a handler file. The call is unchanged: `const getTerms =` becomes `export default`, and the file's name becomes its address.
4190
+
4191
+ ## The browser receives a generated client, not your server code
4192
+
4193
+ The browser never sees a handler file or a `<script server>`. Importing a handler file into a page hands the browser a **generated** call in its place (16.74), and a `.abide`'s own copy carries one per handler its server block declares (16.70). Each carries:
4149
4194
 
4150
4195
  - the HTTP method
4151
4196
  - the mount-relative address
4152
4197
  - the description
4153
4198
 
4154
- Nothing else. Your database import and the block's own module-level work were never part of the module the browser got — so "no server code shipped" is how the build works, not something an optimizer is trusted to have achieved.
4199
+ Nothing else. Your database import and the module-level work around the handler were never part of the module the browser got — so "no server code shipped" is how the build works, not something an optimizer is trusted to have achieved.
4155
4200
 
4156
- A server block exports handlers alone, and anything else is a build error. Import a module under a `server` directory into a page and you get a compile error naming it. Never a stub that silently does nothing.
4201
+ Import a module under a `server` directory into a page and you get a compile error naming it. Never a stub that silently does nothing.
4157
4202
 
4158
4203
  ## You get a `Reactive`, not a promise
4159
4204
 
4160
4205
  **A call hands back a `Reactive`, not a promise** — An invoice and the reminder button that must not fire before it lands. Wait for the total, then press Reload.
4161
4206
 
4162
- ```abide InvoiceReminder.abide
4207
+ ```abide lib/InvoiceReminder.abide
4163
4208
  <script server>
4164
4209
  import { GET } from '@abide/abide/server'
4165
4210
 
@@ -4167,7 +4212,7 @@ const INVOICES: Record<string, { total: string }> = {
4167
4212
  '4021': { total: '$1,240.00' },
4168
4213
  }
4169
4214
 
4170
- export const getInvoiceTotal = GET(
4215
+ const getInvoiceTotal = GET(
4171
4216
  async ({ id }: { id: string }) => {
4172
4217
  await Bun.sleep(700) // a slow database, so the wait is on screen
4173
4218
  const row = INVOICES[id]
@@ -4207,7 +4252,7 @@ See [Loading states](/values/show-a-value-that-isnt-there-yet.md).
4207
4252
 
4208
4253
  **A `GET` over a `global` value follows the server** — The open deals, and a button that moves one on the server. Open this page in a second tab and press it in either: both move, and nothing on the page reloads the list.
4209
4254
 
4210
- ```abide Deals.abide
4255
+ ```abide lib/Deals.abide
4211
4256
  <script server>
4212
4257
  import { GET, POST, reactive } from '@abide/abide/server'
4213
4258
 
@@ -4222,11 +4267,11 @@ const pipeline = reactive(
4222
4267
  { global: true },
4223
4268
  )
4224
4269
 
4225
- export const openDeals = GET(pipeline, {
4270
+ const openDeals = GET(pipeline, {
4226
4271
  description: 'The open deals, and the stage each is at.',
4227
4272
  })
4228
4273
 
4229
- export const advanceDeal = POST(
4274
+ const advanceDeal = POST(
4230
4275
  ({ id }: { id: string }) => {
4231
4276
  pipeline.patch((deals) => {
4232
4277
  const deal = deals.find((each) => each.id === id)
@@ -4261,9 +4306,9 @@ A browser that reads a `GET` stays subscribed to it over one web socket the whol
4261
4306
  The first read and the socket are two paths, and a change landing between them would be lost. So the answer names its version as an ordinary `ETag`, the browser names that version when it subscribes, and the server sends the value only if it has moved since (18.6). The same `ETag` serves any reader: a `GET` carrying `If-None-Match` with the version it holds is answered `304 Not
4262
4307
  Modified` and no body (18.19), so `curl` and a browser's cache revalidate the way the page does.
4263
4308
 
4264
- The follow itself is standard too. Ask the same address for `text/event-stream` and it is a stream of server-sent events: `event:
4265
- value` with the value and its version as the event `id`, then `event:
4266
- patch` with each change as JSON Patch (18.20). An `EventSource` reconnecting sends the last `id` back as `Last-Event-ID` and is sent only what moved after it — so `curl -N -H 'Accept: text/event-stream'` follows a value with nothing abide-shaped on its side, and `abide watch` does the same from a shell.
4309
+ The follow itself is standard too. `openDeals` is the card's own, so its address is one no listing names. To hand the follow to a script, make it a handler file — `src/deals/openDeals.ts`, answering at `/deals/openDeals` — with the pipeline in a module under `server` that it and `advanceDeal` both import. Ask that address for `text/event-stream` and it is a stream of server-sent events: `event: value` with the value and its version as the event `id`, then `event: patch` with each change as JSON Patch (18.20). An `EventSource` reconnecting sends the last `id` back as `Last-Event-ID` and is sent only what moved after it.
4310
+
4311
+ So `curl -N -H 'Accept: text/event-stream'` follows a value with nothing abide-shaped on its side, and `abide watch` does the same from a shell.
4267
4312
 
4268
4313
  A trigger reaches browsers too, for any `GET`: `getContact.invalidate({ id })` inside a handler marks every browser's copy of that contact stale, whether or not the value is `global`.
4269
4314
 
@@ -4275,16 +4320,14 @@ The `reactive` a handler becomes is not ceremony. A keyed value is identity per
4275
4320
 
4276
4321
  Name the caching yourself in the same object as the description:
4277
4322
 
4278
- ```abide src/invoices/api.abide — excerpt
4279
- <script server>
4280
- export const getInvoiceTotals = GET(
4323
+ ```ts src/invoices/getInvoiceTotals.ts — excerpt
4324
+ export default GET(
4281
4325
  ({ id }: { id: string }) => database.invoice.totals(id),
4282
4326
  {
4283
4327
  ttl: 30_000,
4284
4328
  description: "One invoice's totals, by id. Cached for 30 seconds.",
4285
4329
  },
4286
4330
  )
4287
- </script>
4288
4331
  ```
4289
4332
 
4290
4333
  `GET` accepts three things:
@@ -4333,20 +4376,20 @@ A read is safe to repeat. A write is not — and a browser repeats one for you:
4333
4376
  | `PATCH(handler, options?)` | the same, for a partial change |
4334
4377
  | `DELETE(handler, options?)` | a mutation whose arguments travel in the URL |
4335
4378
 
4336
- Each takes exactly what [`GET`](/data/read-data-without-writing-an-api.md) takes — a `reactive`, a channel, or a plain function that becomes a `reactive` — mounts at the same file-path address, and accepts the same options. The method is the whole per-method difference, and what it changes is retention, how the arguments travel, and whether a browser may cache the answer. Every signature is in the [Transports](/reference/transports.md) reference.
4379
+ Each takes exactly what [`GET`](/data/read-data-without-writing-an-api.md) takes — a `reactive`, a channel, or a plain function that becomes a `reactive` — answers where a `GET` would, and accepts the same options. The method is the whole per-method difference, and what it changes is retention, how the arguments travel, and whether a browser may cache the answer. Every signature is in the [Transports](/reference/transports.md) reference.
4337
4380
 
4338
4381
  ## Two presses inside one flight are one write
4339
4382
 
4340
4383
  **`POST(handler)` — a write the browser cannot repeat** — An invoice being paid. Press Pay twice, quickly — one payment lands on the record, not two.
4341
4384
 
4342
- ```abide PayInvoice.abide
4385
+ ```abide lib/PayInvoice.abide
4343
4386
  <script server>
4344
4387
  import { POST } from '@abide/abide/server'
4345
4388
 
4346
4389
  // The writes that landed: a double press must not bill twice.
4347
4390
  let payments = 0
4348
4391
 
4349
- export const payInvoice = POST(
4392
+ const payInvoice = POST(
4350
4393
  async ({ id }: { id: string }) => {
4351
4394
  await Bun.sleep(1500) // a slow database, so the wait is on screen
4352
4395
  payments += 1
@@ -4392,14 +4435,12 @@ Read on: [Caching](/values/load-once-per-set-of-arguments.md) · [Failures](/dat
4392
4435
 
4393
4436
  Where a write genuinely must not run twice in a period, declare the `ttl` on the `reactive` and hand that to the method:
4394
4437
 
4395
- ```abide src/invoices/api.abide — excerpt
4396
- <script server>
4438
+ ```ts src/invoices/remindAboutInvoice.ts — excerpt
4397
4439
  const sendReminder = reactive(({ id }: { id: string }) => mailer.remind(id), {
4398
4440
  ttl: 3_600_000,
4399
4441
  })
4400
4442
 
4401
- export const remindAboutInvoice = POST(sendReminder)
4402
- </script>
4443
+ export default POST(sendReminder)
4403
4444
  ```
4404
4445
 
4405
4446
  One reminder per invoice per hour, whoever asks and however often.
@@ -4414,7 +4455,7 @@ The shapes wanted the repair anyway. A read and a write have different `Args`.
4414
4455
 
4415
4456
  **`PATCH`, `PUT` and `DELETE` — the verb says what the write does** — A contact's phone changed, a tag taken off, the tags put back: three writes, each declared with the method that names it.
4416
4457
 
4417
- ```abide Contact.abide
4458
+ ```abide lib/Contact.abide
4418
4459
  <script server>
4419
4460
  import { DELETE, GET, PATCH, PUT } from '@abide/abide/server'
4420
4461
 
@@ -4442,23 +4483,23 @@ const contacts = {
4442
4483
  },
4443
4484
  }
4444
4485
 
4445
- export const getContact = GET(({ id }: { id: string }) => contacts.find(id), {
4486
+ const getContact = GET(({ id }: { id: string }) => contacts.find(id), {
4446
4487
  description: 'A contact, by id.',
4447
4488
  })
4448
4489
 
4449
- export const updatePhone = PATCH(
4490
+ const updatePhone = PATCH(
4450
4491
  ({ id, phone }: { id: string; phone: string }) =>
4451
4492
  contacts.update(id, { phone }),
4452
4493
  { description: 'Changes the phone of a contact.' },
4453
4494
  )
4454
4495
 
4455
- export const replaceTags = PUT(
4496
+ const replaceTags = PUT(
4456
4497
  ({ id, tags }: { id: string; tags: string[] }) =>
4457
4498
  contacts.update(id, { tags }),
4458
4499
  { description: 'Replaces the tags of a contact.' },
4459
4500
  )
4460
4501
 
4461
- export const removeTag = DELETE(
4502
+ const removeTag = DELETE(
4462
4503
  async ({ id, tag }: { id: string; tag: string }) => {
4463
4504
  const { tags } = await contacts.find(id)
4464
4505
  return contacts.update(id, {
@@ -4517,16 +4558,18 @@ Three body encodings are accepted, which lets a plain `<form>` reach the same ad
4517
4558
  | `multipart/form-data` | a form with a file in it |
4518
4559
  | `application/x-www-form-urlencoded` | a form without one |
4519
4560
 
4520
- ## A folder is part of the address
4561
+ ## A write answers where its file says
4521
4562
 
4522
- A handler answers at the route of the directory it is declared in, then its name. Nesting nests the address, and nothing else changes:
4563
+ Both cards declare their writes in their own `<script server>`, so each write is that card's alone. `payInvoice` answers at `/__abide/handlers/data/change-something-on-the-server/lib/PayInvoice/payInvoice`, listed on no surface, and runs under this page's middleware (16.70, 16.71, 16.72). A write only the button beside it sends belongs there.
4523
4564
 
4524
- | File | Export | Answers at |
4525
- | --- | --- | --- |
4526
- | `src/invoices/api.abide` | `payInvoice` | `/invoices/payInvoice` |
4527
- | `src/admin/page.abide` | `recent` | `/admin/recent` |
4565
+ A write another page, a script or an agent sends is a handler file, and answers at its path. Nesting nests the address, and nothing else changes:
4566
+
4567
+ | File | Answers at |
4568
+ | --- | --- |
4569
+ | `src/invoices/payInvoice.ts` | `/invoices/payInvoice` |
4570
+ | `src/admin/recent.ts` | `/admin/recent` |
4528
4571
 
4529
- An address answers each method once. Two files in one directory exporting `recent` answer at the same address, and that is a build error naming both files rather than whichever one won at the first request. A `POST` beside a page is another method, and is the form that page posts to.
4572
+ An address answers each method once. A page `src/admin/recent.abide` beside a `GET` in `recent.ts` answers at the same address, and that is a build error naming both files rather than whichever one won at the first request. A `POST` at a page's address is another method, and is the form that page posts to.
4530
4573
 
4531
4574
  Read on: [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
4532
4575
 
@@ -4595,7 +4638,7 @@ Read on: [Response types](/data/answer-with-something-other-than-json.md)
4595
4638
 
4596
4639
  **`rpc.isError(e, name)` — the refusal, matched by name** — Three invoices on one account. Open the second and the third — each is refused for a different reason.
4597
4640
 
4598
- ```abide AccountInvoice.abide
4641
+ ```abide lib/AccountInvoice.abide
4599
4642
  <script server>
4600
4643
  import { refuse } from '@abide/abide'
4601
4644
  import { GET } from '@abide/abide/server'
@@ -4624,7 +4667,7 @@ const INVOICES: Record<string, Row> = {
4624
4667
  '4312': { total: '$512.00', mine: true, owner: 'ada', replacedBy: '4313' },
4625
4668
  }
4626
4669
 
4627
- export const getInvoice = GET(
4670
+ const getInvoice = GET(
4628
4671
  async ({ id }: { id: string }) => {
4629
4672
  await Bun.sleep(450) // a slow database, so the wait is on screen
4630
4673
  const invoice = INVOICES[id]
@@ -4685,7 +4728,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Condi
4685
4728
 
4686
4729
  **`s.isError(e, name)` — a refusal the page made itself** — A contact's phone, normalized on the way in. Take a digit out and press Save.
4687
4730
 
4688
- ```abide Phone.abide
4731
+ ```abide lib/Phone.abide
4689
4732
  <script>
4690
4733
  import { reactive, refuse } from '@abide/abide'
4691
4734
  import { z } from 'zod'
@@ -4788,11 +4831,11 @@ Read on: [Schemas](/data/check-what-callers-send-you.md)
4788
4831
 
4789
4832
  **`aborted` — what a load reads once every caller stopped waiting** — A contact's year statement from a slow ledger. Load it and cancel before it lands.
4790
4833
 
4791
- ```abide Statement.abide
4834
+ ```abide lib/Statement.abide
4792
4835
  <script server>
4793
4836
  import { GET } from '@abide/abide/server'
4794
4837
 
4795
- export const getStatement = GET(
4838
+ const getStatement = GET(
4796
4839
  async ({ id }: { id: string }) => {
4797
4840
  // A slow ledger, so there is time to cancel.
4798
4841
  await Bun.sleep(2500)
@@ -4895,7 +4938,7 @@ That is the default framing and not the only one.
4895
4938
 
4896
4939
  **`sse(values)` — events, whoever asks** — A wallboard by the sales floor reads the visits feed. It sends no Accept header, and what comes back is events anyway.
4897
4940
 
4898
- ```abide VisitsFeed.abide
4941
+ ```abide lib/VisitsFeed.abide
4899
4942
  <script server>
4900
4943
  import { GET, sse } from '@abide/abide/server'
4901
4944
 
@@ -4913,7 +4956,7 @@ async function* latestVisits() {
4913
4956
  }
4914
4957
  }
4915
4958
 
4916
- export const visits = GET(() => sse(latestVisits()), {
4959
+ const visits = GET(() => sse(latestVisits()), {
4917
4960
  description: 'Live visits, as server-sent events.',
4918
4961
  })
4919
4962
  </script>
@@ -4951,7 +4994,7 @@ Read on: [Response types](/data/answer-with-something-other-than-json.md)
4951
4994
 
4952
4995
  **`jsonl(values)` — a list sent one value per line** — The warehouse loads yesterday's sales a line at a time. The database hands back an array, and the export still goes out as lines.
4953
4996
 
4954
- ```abide WarehouseExport.abide
4997
+ ```abide lib/WarehouseExport.abide
4955
4998
  <script server>
4956
4999
  import { GET, jsonl } from '@abide/abide/server'
4957
5000
 
@@ -4965,7 +5008,7 @@ const BY_DAY: Record<string, Sale[]> = {
4965
5008
  ],
4966
5009
  }
4967
5010
 
4968
- export const salesForWarehouse = GET(
5011
+ const salesForWarehouse = GET(
4969
5012
  async ({ day }: { day: string }) => {
4970
5013
  // Read whole: the export is a list before it is a stream.
4971
5014
  await Bun.sleep(400)
@@ -5004,7 +5047,7 @@ An array is a value, so returned bare it goes out as one JSON array (16.30) —
5004
5047
 
5005
5048
  **`bytes(values)` — a file written as its rows are read** — Sales by region as a CSV download. Each line goes into the file as the cursor reaches its row.
5006
5049
 
5007
- ```abide SalesCsv.abide
5050
+ ```abide lib/SalesCsv.abide
5008
5051
  <script server>
5009
5052
  import { bytes, GET } from '@abide/abide/server'
5010
5053
 
@@ -5027,7 +5070,7 @@ async function* csvOf(year: number): AsyncGenerator<Uint8Array> {
5027
5070
  }
5028
5071
  }
5029
5072
 
5030
- export const salesCsv = GET(
5073
+ const salesCsv = GET(
5031
5074
  ({ year }: { year: number }) => bytes(csvOf(year)),
5032
5075
  { description: 'Sales by region for a year, as CSV.' },
5033
5076
  )
@@ -5071,7 +5114,7 @@ One input type across `page`, `jsonl`, `sse` and `bytes`. A source that is alrea
5071
5114
 
5072
5115
  **`{#for row of call()}` — the rows, and the whole of them** — Sales per region, a row at a time. Watch the list fill while the count still has nothing to say.
5073
5116
 
5074
- ```abide RegionReport.abide
5117
+ ```abide lib/RegionReport.abide
5075
5118
  <script server>
5076
5119
  import { GET } from '@abide/abide/server'
5077
5120
 
@@ -5083,7 +5126,7 @@ const BY_YEAR: Record<string, { name: string; total: string }[]> = {
5083
5126
  ],
5084
5127
  }
5085
5128
 
5086
- export const salesByRegion = GET(
5129
+ const salesByRegion = GET(
5087
5130
  async function* ({ year }: { year: number }) {
5088
5131
  for (const row of BY_YEAR[String(year)] ?? []) {
5089
5132
  await Bun.sleep(1200) // a slow database: each row lands alone
@@ -5133,7 +5176,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Lists
5133
5176
 
5134
5177
  ## A reader arriving mid-run replays the run
5135
5178
 
5136
- There is no retention option on the transport and none is owed. What a late reader needs is the answer being built, and a cursor over the stream hands it the run in flight from its start before it goes live (2.5). So a reader arriving halfway through still paints every row.
5179
+ There is no retention option on the transport and none is owed. What a late reader needs is the answer being built, and a `for await` over the stream hands it the run in flight from its first chunk to its close (2.6). So a reader arriving halfway through still paints every row.
5137
5180
 
5138
5181
  ## A stream is not a channel
5139
5182
 
@@ -5141,7 +5184,7 @@ Both change more than once, and how the producer produces separates them. A chan
5141
5184
 
5142
5185
  What they share is keying. A `{#for}` over a stream's accumulation is positional unless something names a key (32.13) — the block's own `by`, or a `by` the value declares (32.32) — and a row with an identity of its own says which:
5143
5186
 
5144
- ```abide src/reports/page.abide — excerpt
5187
+ ```abide src/reports/index.abide — excerpt
5145
5188
  {#for region of salesByRegion({ year: 2026 }) by region.code}
5146
5189
  <tr><td>{region.name}</td><td>{region.total}</td></tr>
5147
5190
  {/for}
@@ -5179,8 +5222,8 @@ Nothing on the page polls and nothing on it opens a connection. A page that read
5179
5222
  | You write | What it is |
5180
5223
  | --- | --- |
5181
5224
  | `reactive(fn, { global: true })` | the value every caller shares, one per args key where `fn` declares a parameter |
5182
- | `export const room = GET(rooms)` | its address — a browser reading it follows it |
5183
- | `export const say = POST(…)` | the way in from outside, a handler that patches the value |
5225
+ | `const room = GET(rooms)` | its address — a browser reading it follows it |
5226
+ | `const say = POST(…)` | the way in, a handler that patches the value |
5184
5227
 
5185
5228
  `global: true` puts the value in one process-wide cache that outlives every request (11.34). Leave it out and the value is request-local on a server (11.32), so every caller gets one of their own and nobody hears anybody. Every signature is in the [Transports](/reference/transports.md) reference, and what a channel is before it has an address is [Channels](/values/let-anything-publish-and-anything-read.md).
5186
5229
 
@@ -5188,7 +5231,7 @@ Nothing on the page polls and nothing on it opens a connection. A page that read
5188
5231
 
5189
5232
  **`GET` + `POST` — your room, every tab** — A support room per browser: a `GET` every tab follows and a `POST` that adds a turn. Open this page in a second tab, send from either, and the last turn changes in both — and in nobody else's.
5190
5233
 
5191
- ```abide SupportRoom.abide
5234
+ ```abide lib/SupportRoom.abide
5192
5235
  <script server>
5193
5236
  import { reactive } from '@abide/abide'
5194
5237
  import { GET, POST } from '@abide/abide/server'
@@ -5208,10 +5251,10 @@ const rooms = reactive(
5208
5251
  )
5209
5252
 
5210
5253
  /** Read, and followed by every tab reading it. */
5211
- export const room = GET(rooms, { description: "A support room's turns." })
5254
+ const room = GET(rooms, { description: "A support room's turns." })
5212
5255
 
5213
5256
  /** The server writes the turn, so what a tab sends is only its text. */
5214
- export const say = POST(
5257
+ const say = POST(
5215
5258
  ({ id, text }: { id: string; text: string }) => {
5216
5259
  rooms({ id }).patch((turns) => {
5217
5260
  turns.push({ id: crypto.randomUUID(), speaker: 'you', text })
@@ -5263,7 +5306,7 @@ Each turn carries an id the server gives it, and the room declares `by: 'id'`. A
5263
5306
  `say` takes `{ id, text }` and nothing else. The turn's id, its speaker and anything else it carries are written by the handler, so a caller can only ever add a turn that says what they typed. The example writes `'you'`; an app with sign-in writes who is signed in:
5264
5307
 
5265
5308
  ```ts server
5266
- export const say = POST(({ id, text }: { id: string; text: string }) => {
5309
+ const say = POST(({ id, text }: { id: string; text: string }) => {
5267
5310
  const { name } = principal.resolved()
5268
5311
  rooms({ id }).patch((turns) => {
5269
5312
  turns.push({ id: crypto.randomUUID(), speaker: name, text })
@@ -5286,7 +5329,7 @@ Read on: [Mutations](/data/change-something-on-the-server.md) · [Auth & princip
5286
5329
  A room anyone can follow is a room anyone can read. Put a rung on the `GET`:
5287
5330
 
5288
5331
  ```ts server
5289
- export const room = GET(rooms, {
5332
+ const room = GET(rooms, {
5290
5333
  middleware: [
5291
5334
  async (next, ctx) =>
5292
5335
  (await isMember(ctx.request, ctx.args())) ? next(ctx) : refuse(403),
@@ -5313,11 +5356,11 @@ A `patch` on the room is sent to every follower standing at the version it was m
5313
5356
 
5314
5357
  A write to a followed copy stays in that tab — a `say` is how a turn reaches the room — and lasts until the server next changes the room.
5315
5358
 
5316
- The same follow is open to anything that speaks http. Ask the `GET`'s address for `text/event-stream` and it is server-sent events, each frame's op its `event`, its version the `id`, and the value or the JSON Patch its `data` (18.20):
5359
+ The same follow is open to anything that speaks http, from a handler file. The card's `room` is its own, at an address no listing names; to hand the room to a script, put the `GET` in `src/support/room.ts` as its default export, with `rooms` in a module under `server` that it and `say` both import. Ask `/support/room` for `text/event-stream` and it is server-sent events, each frame's op its `event`, its version the `id`, and the value or the JSON Patch its `data` (18.20):
5317
5360
 
5318
5361
  ```
5319
5362
  curl -N -H 'Accept: text/event-stream' \
5320
- 'https://app.example/server/keep-a-room-of-callers-in-sync/room?id=lobby'
5363
+ 'https://app.example/support/room?id=lobby'
5321
5364
  ```
5322
5365
 
5323
5366
  An `EventSource` reconnecting sends the last `id` back as `Last-Event-ID`, and is sent only what moved after it.
@@ -5343,7 +5386,7 @@ The call hands back the same `Reactive` the handler had, `Produced` included (16
5343
5386
 
5344
5387
  Writing to that copy is not how to send. A `set` on it stands only until the next production replaces it (9.4), so a turn goes through `say`.
5345
5388
 
5346
- `room.invalidate({ id })` and `room.refresh({ id })` inside a handler reach every browser's copy of that room (7.10, 7.18). A `for await` over the room hands back the standing array once per turn, a burst collapsing to the latest (2.6), so code that has to act on every message acts in `say`.
5389
+ `room.invalidate({ id })` and `room.refresh({ id })` inside a handler reach every browser's copy of that room (7.10, 7.18). A `watch` on the room runs once per turn, a burst collapsing to the latest (12.10), so code that has to act on every message acts in `say`.
5347
5390
 
5348
5391
  ## Each instance holds its own room
5349
5392
 
@@ -5386,8 +5429,7 @@ Deriving from an annotation means the build resolves types, so build time is typ
5386
5429
 
5387
5430
  That is the trade "the shape is never declared twice" is bought with, and the repair is an option at each **position**. `args` gates the arguments a keyed value is called with:
5388
5431
 
5389
- ```abide src/invoices/api.abide — excerpt
5390
- <script server>
5432
+ ```ts src/invoices/createInvoice.ts — excerpt
5391
5433
  import { z } from 'zod'
5392
5434
 
5393
5435
  const invoices = reactive(
@@ -5395,13 +5437,12 @@ const invoices = reactive(
5395
5437
  { args: z.object({ lines: z.array(lineSchema).min(1) }) },
5396
5438
  )
5397
5439
 
5398
- export const createInvoice = POST(invoices)
5399
- </script>
5440
+ export default POST(invoices)
5400
5441
  ```
5401
5442
 
5402
5443
  `transform` shapes what gets stored:
5403
5444
 
5404
- ```ts src/ledger.ts — excerpt
5445
+ ```ts src/lib/ledger.ts — excerpt
5405
5446
  const paidIds = reactive(() => listInvoices({ year }), {
5406
5447
  transform: (all) => new Set(all.filter((r) => r.paid).map((r) => r.id)),
5407
5448
  })
@@ -5419,7 +5460,7 @@ Either option pins a shape you will not have widened, and either is where a cons
5419
5460
 
5420
5461
  **`reactive(v, { schema })` — a check with no server in it** — A contact's email. Take the dot out of the domain, and watch the record rather than the field.
5421
5462
 
5422
- ```abide Email.abide
5463
+ ```abide lib/Email.abide
5423
5464
  <script>
5424
5465
  import { reactive } from '@abide/abide'
5425
5466
  import { z } from 'zod'
@@ -5477,7 +5518,7 @@ A JSON Schema document is the **native** shape, and it enters as the Standard fo
5477
5518
 
5478
5519
  **`jsonSchema(document)` — a JSON Schema document as a gate** — The seats on a deal, capped at fifty by a JSON Schema document. Press + past the cap and the deal keeps what it had.
5479
5520
 
5480
- ```abide Seats.abide
5521
+ ```abide lib/Seats.abide
5481
5522
  <script>
5482
5523
  import { jsonSchema, reactive } from '@abide/abide'
5483
5524
 
@@ -5511,7 +5552,7 @@ A document is data, so something has to read it, and `jsonSchema` is what brings
5511
5552
 
5512
5553
  **`Issues<T>` — a message under the field it belongs to** — The invoice search. Empty the query, or ask for four — each message lands under its own field.
5513
5554
 
5514
- ```abide InvoiceSearch.abide
5555
+ ```abide lib/InvoiceSearch.abide
5515
5556
  <script server>
5516
5557
  import { reactive } from '@abide/abide'
5517
5558
  import { GET } from '@abide/abide/server'
@@ -5540,7 +5581,7 @@ const matching = reactive(
5540
5581
  },
5541
5582
  )
5542
5583
 
5543
- export const searchInvoices = GET(matching, {
5584
+ const searchInvoices = GET(matching, {
5544
5585
  description: 'Invoices matching a query.',
5545
5586
  })
5546
5587
  </script>
@@ -5602,7 +5643,7 @@ Read on: [OpenAPI](/machines/describe-your-api-without-writing-a-spec.md)
5602
5643
 
5603
5644
  **`validateJson(schema, value)` — the issues, with no value to gate** — A contact row pasted from a partner's export, checked before it is imported. Fix the plan and the seats, and watch each issue leave its own field.
5604
5645
 
5605
- ```abide PastedRow.abide
5646
+ ```abide lib/PastedRow.abide
5606
5647
  <script>
5607
5648
  import { reactive, validateJson } from '@abide/abide'
5608
5649
 
@@ -5698,7 +5739,7 @@ Read on: [Failures](/data/refuse-a-request-and-say-why.md)
5698
5739
 
5699
5740
  **`reactive(fn, { args })` — the gate refuses, the arguments key** — One invoice, spelled two ways. Open the second spelling, then go back to the first.
5700
5741
 
5701
- ```abide InvoiceKey.abide
5742
+ ```abide lib/InvoiceKey.abide
5702
5743
  <script server>
5703
5744
  import { reactive } from '@abide/abide'
5704
5745
  import { GET } from '@abide/abide/server'
@@ -5720,7 +5761,7 @@ const invoiceById = reactive(
5720
5761
  { args: z.object({ id: z.string().toLowerCase() }) },
5721
5762
  )
5722
5763
 
5723
- export const getInvoice = GET(invoiceById, {
5764
+ const getInvoice = GET(invoiceById, {
5724
5765
  description: 'An invoice, by id.',
5725
5766
  })
5726
5767
  </script>
@@ -5760,7 +5801,7 @@ In channels keyed by their args the same cost is sharper: two spellings of a cha
5760
5801
 
5761
5802
  A channel is an array, so its `schema` checks the whole array on every `set` — the app's own included, because validity is not a question about who is asking:
5762
5803
 
5763
- ```ts src/channels.ts — excerpt
5804
+ ```ts src/lib/channels.ts — excerpt
5764
5805
  export const announcements = reactive<Post[]>([], {
5765
5806
  schema: z.array(messageSchema),
5766
5807
  global: true,
@@ -5771,7 +5812,7 @@ What it does not see is a `patch`. An append through `patch` mutates the array i
5771
5812
 
5772
5813
  A channel keyed by its args also has a **channel key**, and that is the parameter its body declares, so it is `args`. The key picks which channel while the messages flow:
5773
5814
 
5774
- ```ts src/channels.ts — excerpt
5815
+ ```ts src/lib/channels.ts — excerpt
5775
5816
  export const thread = reactive((_args: { id: string }): Post[] => [], {
5776
5817
  args: z.object({ id: z.string().min(1) }),
5777
5818
  schema: z.array(messageSchema),
@@ -5814,12 +5855,12 @@ type Middleware<Ctx, Result> = (
5814
5855
 
5815
5856
  | Lane | `Ctx` | Result | Runs |
5816
5857
  | --- | --- | --- | --- |
5817
- | app | `{ request }` | `Response` | pre-routing, on every request |
5858
+ | app | `{ request }` | `Response` | over every request its file's route answers, before routing at the root |
5818
5859
  | rpc | `{ request, args }` | `Value \| Failures` | on every call to that handler |
5819
5860
 
5820
- Two lanes, one type. `Ctx` is a record naming what that lane actually **has** — the app lane runs before the route resolves, so it has a request and no arguments; the rpc lane is typed to one handler's own arguments.
5861
+ Two lanes, one type. `Ctx` is a record naming what that lane actually **has** — the app lane runs around a whole route rather than one handler, so it has a request and no arguments; the rpc lane is typed to one handler's own arguments.
5821
5862
 
5822
- The **Result** column is the difference that matters. The rpc lane hands back what the handler would have handed back, not a `Response`, and that lets the same rungs run on an **in-process** call. A page rendering on the server reads through the handler, so a rung that authorizes on an id in the arguments cannot be reachable by a browser and missed by the render. The app lane stays `Response`-shaped because pre-routing there is no value yet, which also makes it the lane where a rung that must set a header belongs.
5863
+ The **Result** column is the difference that matters. The rpc lane hands back what the handler would have handed back, not a `Response`, and that lets the same rungs run on an **in-process** call. A page rendering on the server reads through the handler, so a rung that authorizes on an id in the arguments cannot be reachable by a browser and missed by the render. The app lane stays `Response`-shaped because what it wraps is a whole answer rather than a value, which also makes it the lane where a rung that must set a header belongs.
5823
5864
 
5824
5865
  A rung's refusals join the handler's own in what the caller can narrow, so `rpc.isError(error, 'NotYours')` reaches a name the rung declared and never the handler.
5825
5866
 
@@ -5850,7 +5891,7 @@ A rung that wants to observe both — a timer, a log line — calls `next` insid
5850
5891
 
5851
5892
  **`middleware(rung)` — one rung in front of a route's answers** — A CRM behind a load balancer, and the question of which instance answered. Ask, and read a header the handler never wrote.
5852
5893
 
5853
- ```abide Ping.abide
5894
+ ```abide lib/Ping.abide
5854
5895
  <script server>
5855
5896
  import { GET, middleware } from '@abide/abide/server'
5856
5897
 
@@ -5864,7 +5905,7 @@ middleware(async (next) => {
5864
5905
  })
5865
5906
 
5866
5907
  /** Writes no header of its own. */
5867
- export const ping = GET(() => 'pong', { description: 'Answers pong.' })
5908
+ const ping = GET(() => 'pong', { description: 'Answers pong.' })
5868
5909
  </script>
5869
5910
 
5870
5911
  <script>
@@ -5886,11 +5927,15 @@ let servedBy = reactive('—')
5886
5927
  ```
5887
5928
 
5888
5929
  `middleware(...)` is a hook you **call** from a `<script
5889
- server>`, and it runs over the route of the file that called it. The root layout's rungs run before routing, over every request. A section's `layout.abide` files rungs that run after routing, over that section's pages and handlers alone — which is where a guard on `/admin` goes. The card's rung is filed beside `ping`, so it sets a header on the way out of every answer this page's route gives, and no handler wrote it. Filed in the root layout, the same rung would stamp every answer the app gives — a page, a built asset, the health check.
5930
+ server>` or a handler file, and it runs over the route of the file that called it and every route below. The root layout's rungs run before routing, over every request. A section's `layout.abide` files rungs that run after routing, over that section's pages and handlers alone — which is where a guard on `/admin` goes.
5931
+
5932
+ The card's file sits in this page's `lib`, so its rung is filed at this page's route (37.1). `ping` is the card's private handler, and its address, `/__abide/handlers/data/decide-who-may-call-what/lib/Ping/ping`, is below no route of this page's. The rung runs over it anyway, because a private handler runs under the middleware filed where its file's hooks are (16.72). No handler wrote the header.
5933
+
5934
+ So a guard on a section covers every private handler its files declare, wherever their addresses point. Filed in the root layout, the same rung would stamp every answer the app gives — a page, a built asset, the health check. `abide routes` lists each hook with the route it is filed at.
5890
5935
 
5891
5936
  Rungs compose in registration order. The call hands back the way off again, so a module that is torn down can remove its own.
5892
5937
 
5893
- Pre-routing means this lane has a request and nothing else. Anything resource-aware — authorizing by an id in the arguments — is necessarily a handler's own rung, which is where `ctx.args()` exists.
5938
+ Around a route this lane has a request and nothing else. Anything resource-aware — authorizing by an id in the arguments — is necessarily a handler's own rung, which is where `ctx.args()` exists.
5894
5939
 
5895
5940
  Read on: [Lifecycle](/app/run-code-at-start-and-stop.md) · [CSP](/app/lock-down-what-the-page-may-load.md)
5896
5941
 
@@ -5898,7 +5943,7 @@ Read on: [Lifecycle](/app/run-code-at-start-and-stop.md) · [CSP](/app/lock-down
5898
5943
 
5899
5944
  **`middleware: [rung]` — a check that runs before the parse** — The recent invoices. Ask for nine while signed out, then sign in and ask again.
5900
5945
 
5901
- ```abide Recent.abide
5946
+ ```abide lib/Recent.abide
5902
5947
  <script server>
5903
5948
  import { principal, refuse } from '@abide/abide'
5904
5949
  import { GET, POST } from '@abide/abide/server'
@@ -5911,7 +5956,7 @@ const INVOICES = [
5911
5956
  { id: '4023', total: '$76.50' },
5912
5957
  ]
5913
5958
 
5914
- export const searchInvoices = GET(
5959
+ const searchInvoices = GET(
5915
5960
  async ({ limit }: { limit: number }) => {
5916
5961
  await Bun.sleep(450) // a slow database, so the wait is on screen
5917
5962
  return INVOICES.slice(0, limit)
@@ -5929,11 +5974,11 @@ export const searchInvoices = GET(
5929
5974
  },
5930
5975
  )
5931
5976
 
5932
- export const signIn = POST(() => principal.set({ id: 'ada' }), {
5977
+ const signIn = POST(() => principal.set({ id: 'ada' }), {
5933
5978
  description: 'Signs in as Ada.',
5934
5979
  })
5935
5980
 
5936
- export const signOut = POST(() => principal.clear(), {
5981
+ const signOut = POST(() => principal.clear(), {
5937
5982
  description: 'Signs out.',
5938
5983
  })
5939
5984
  </script>
@@ -6003,6 +6048,8 @@ Read on: [CORS](/data/let-another-origin-call-you.md) · [Health](/app/tell-a-lo
6003
6048
 
6004
6049
  `clients` withholds a handler from a **surface** — an OpenAPI listing, an MCP tool list. It is not access control: the http address answers exactly as it did. This rung decides who **may** call, the same one that decides it for a browser.
6005
6050
 
6051
+ A private handler is the same. It is listed nowhere and still answers http at its address, so the rungs over its file are what guard it.
6052
+
6006
6053
  Read on: [Withholding a surface](/machines/keep-a-handler-off-a-surface.md) · [Auth & principal](/app/know-who-is-calling.md)
6007
6054
 
6008
6055
  ## Next
@@ -6131,7 +6178,7 @@ Read on: [Mutations](/data/change-something-on-the-server.md)
6131
6178
 
6132
6179
  **`timeout` — milliseconds without progress, not since the first byte** — A sales report streaming a row a second. North takes three seconds under a two-second bound; south has stopped producing.
6133
6180
 
6134
- ```abide SalesReport.abide
6181
+ ```abide lib/SalesReport.abide
6135
6182
  <script server>
6136
6183
  import { GET } from '@abide/abide/server'
6137
6184
 
@@ -6145,7 +6192,7 @@ const BY_REGION: Record<string, { name: string; total: string }[]> = {
6145
6192
  south: [{ name: 'week 1', total: '$2,140.00' }],
6146
6193
  }
6147
6194
 
6148
- export const salesByRegion = GET(
6195
+ const salesByRegion = GET(
6149
6196
  async function* ({ region }: { region: string }) {
6150
6197
  for (const row of BY_REGION[region] ?? []) yield row
6151
6198
  // The stall the per-chunk timeout is against, on one region only.
@@ -6264,7 +6311,7 @@ There is one form and no options. It is a server name, imported from `@abide/abi
6264
6311
 
6265
6312
  **`reachable(host)` — ask once, refuse at once** — Invoice 4310, and a billing service that has stopped answering. Send it twice — the first send waits out the check, the second is refused before it leaves.
6266
6313
 
6267
- ```abide SendInvoice.abide
6314
+ ```abide lib/SendInvoice.abide
6268
6315
  <script server>
6269
6316
  import { POST, reachable, refuse } from '@abide/abide/server'
6270
6317
 
@@ -6283,7 +6330,7 @@ function billing(): string {
6283
6330
  return billingOrigin
6284
6331
  }
6285
6332
 
6286
- export const syncInvoice = POST(
6333
+ const syncInvoice = POST(
6287
6334
  async ({ id }: { id: string }) => {
6288
6335
  const origin = billing()
6289
6336
  const asked = performance.now()
@@ -6390,13 +6437,11 @@ A binary decodes back to a `Uint8Array` on the caller's side, so an rpc that han
6390
6437
 
6391
6438
  A `Blob` has `size` and `slice`, which is the whole of what range serving needs — so abide reads `Range`, honors `If-Range` against the ETag it derives, and answers `206` with `content-range`, `416` where the range is unsatisfiable and `304` on a matching `If-None-Match`. `Bun.file(path)` IS a `Blob`, so serving a file that seeks is returning one:
6392
6439
 
6393
- ```abide src/media/api.abide
6394
- <script server>
6395
- export const playFile = GET(async ({ id }: { id: number }) => {
6440
+ ```ts src/media/playFile.ts — excerpt
6441
+ export default GET(async ({ id }: { id: number }) => {
6396
6442
  const path = await pathFor(id)
6397
6443
  return Bun.file(path)
6398
6444
  })
6399
- </script>
6400
6445
  ```
6401
6446
 
6402
6447
  *0 lines of range parsing* — against the sixty every media server writes once and gets `content-length` on a `206` wrong the first time, a player that will not seek being the only symptom. Reach it from markup with [`playFile.url({ id })`](/data/find-the-url-a-handler-answers-on.md).
@@ -6409,7 +6454,7 @@ Read on: [Reading data](/data/read-data-without-writing-an-api.md) · [Streaming
6409
6454
 
6410
6455
  **`json(value)` — `null` for nobody, not an empty answer** — A sync script reads each contact's account manager as JSON. Grace has none — switch to her.
6411
6456
 
6412
- ```abide AccountManager.abide
6457
+ ```abide lib/AccountManager.abide
6413
6458
  <script server>
6414
6459
  import { GET, json } from '@abide/abide/server'
6415
6460
 
@@ -6418,7 +6463,7 @@ const MANAGERS: Record<string, { name: string } | undefined> = {
6418
6463
  '4022': undefined,
6419
6464
  }
6420
6465
 
6421
- export const accountManager = GET(
6466
+ const accountManager = GET(
6422
6467
  async ({ id }: { id: string }) => {
6423
6468
  await Bun.sleep(300) // a slow database, so the wait is on screen
6424
6469
  return json(MANAGERS[id])
@@ -6466,7 +6511,7 @@ A bare `undefined` is a 204 with no body (16.31), which is right for a write wit
6466
6511
 
6467
6512
  **`page(body)` — an invoice as a document of its own** — The invoice exactly as it was issued, kept as the HTML the customer was sent.
6468
6513
 
6469
- ```abide IssuedInvoice.abide
6514
+ ```abide lib/IssuedInvoice.abide
6470
6515
  <script server>
6471
6516
  import { GET, page } from '@abide/abide/server'
6472
6517
 
@@ -6501,7 +6546,7 @@ Analytical Engines Ltd · issued 2026-09-14 · due 2026-10-14</p>
6501
6546
  </html>`,
6502
6547
  }
6503
6548
 
6504
- export const issuedInvoice = GET(
6549
+ const issuedInvoice = GET(
6505
6550
  async ({ id }: { id: string }) => {
6506
6551
  await Bun.sleep(300) // a slow database, so the wait is on screen
6507
6552
  const html = ISSUED[id]
@@ -6524,7 +6569,7 @@ export const issuedInvoice = GET(
6524
6569
 
6525
6570
  **`redirect(to)` — one address, sent on to another** — Every contact's avatar has one address that never changes. It sends the browser on to the photo's current version.
6526
6571
 
6527
- ```abide Avatar.abide
6572
+ ```abide lib/Avatar.abide
6528
6573
  <script server>
6529
6574
  import { GET, redirect } from '@abide/abide/server'
6530
6575
 
@@ -6542,7 +6587,7 @@ function contactOf(id: string) {
6542
6587
  return contact
6543
6588
  }
6544
6589
 
6545
- export const photo = GET(
6590
+ const photo = GET(
6546
6591
  ({ id, version }: { id: string; version: number }) => {
6547
6592
  const { initials, color } = contactOf(id)
6548
6593
  const svg = [
@@ -6560,7 +6605,7 @@ export const photo = GET(
6560
6605
  { description: 'A contact photo, by id and version.' },
6561
6606
  )
6562
6607
 
6563
- export const avatar = GET(
6608
+ const avatar = GET(
6564
6609
  ({ id }: { id: string }) =>
6565
6610
  redirect(photo.url({ id, version: contactOf(id).photoVersion })),
6566
6611
  { description: 'Redirects to the current photo of a contact.' },
@@ -6627,7 +6672,7 @@ Read on: [Mutations](/data/change-something-on-the-server.md) · [Health](/app/t
6627
6672
 
6628
6673
  ## An app route answers with the same helpers
6629
6674
 
6630
- `src/layout.abide` exports one route of the app's own, asked where no page, handler or public file answered, and it uses this same set — there is no second one. Returning `undefined` hands the request on to the 404.
6675
+ A `middleware` rung the root `src/layout.abide` files runs before routing, so it can answer an address no page, handler or public file has, and it uses this same set — there is no second one. Calling `next()` hands the request on.
6631
6676
 
6632
6677
  Read on: [Lifecycle](/app/run-code-at-start-and-stop.md)
6633
6678
 
@@ -6647,7 +6692,7 @@ Read on: [Lifecycle](/app/run-code-at-start-and-stop.md)
6647
6692
 
6648
6693
  Inside the app you never write an address down: you import the handler and call it. Something outside the app cannot — a payment provider that needs a webhook URL, a `<form action>`, a colleague who wants a `curl` line.
6649
6694
 
6650
- The handler carries the address, so the string is derived rather than typed. Rename the export and every place that reached for it follows, there being no path written down to miss.
6695
+ A handler file's address is its path, so the string is derived rather than typed. Move the file and the address moves with it, there being no path written down to miss. `abide routes` prints every one.
6651
6696
 
6652
6697
  ## The four members an `Rpc` carries
6653
6698
 
@@ -6661,6 +6706,49 @@ The handler carries the address, so the string is derived rather than typed. Ren
6661
6706
  None of the four is a call to the handler. They describe the address rather than reaching it, which is what makes them the surface for everything outside the app — a `<form
6662
6707
  action>`, an `<img src>`, a webhook registration, a `curl` line. Every signature is in the [Transports](/reference/transports.md) reference.
6663
6708
 
6709
+ ## The address is the handler file's path
6710
+
6711
+ | Handler file | Answers at |
6712
+ | --- | --- |
6713
+ | `src/users/getUser.ts` | `/users/getUser` |
6714
+ | `src/admin/recent.ts` | `/admin/recent` |
6715
+ | `src/media/[id]/save.ts` | `/media/42/save`, with `id` in its args |
6716
+
6717
+ The directories are the address and the file's name is its last segment, so the address is readable from the source tree — which is what a log line or a network panel is for. A parameter in the route reaches the handler's args from the path, so its `Args` declares it — one it leaves out is a build error naming the parameter (16.67).
6718
+
6719
+ The address is not a thing to write out. The route table is a build artifact, and two files that answer one method at one address are a build error naming both.
6720
+
6721
+ ## A private handler answers below `/__abide/handlers`
6722
+
6723
+ | Declared in | Answers at |
6724
+ | --- | --- |
6725
+ | `addComment` in `src/media/[id]/lib/Comments.abide` | `/__abide/handlers/media/id/lib/Comments/addComment` |
6726
+ | `markPaid` in `src/crm/[id].abide` | `/__abide/handlers/crm/id/markPaid` |
6727
+
6728
+ A handler a `<script server>` declares is its file's alone, and its address says which file: the file's path without `.abide`, each segment's brackets and dots dropped, then the handler's name (16.70). It takes nothing from the path, so `markPaid` under `[id]` is one address for every id, and the id travels in its args.
6729
+
6730
+ That address is for reading, in a log line or a network panel. It is listed on no surface (16.71), so it is not one to hand to a webhook or a colleague. The file can still put `markPaid.url()` in a `<form action>` of its own. Anything outside the file gets a handler file instead.
6731
+
6732
+ ## `abide routes` prints every address
6733
+
6734
+ In an app with a page per contact, a handler file and two private handlers, it prints each address with its method and the file answering it, and each hook with the route it is filed at (38.108):
6735
+
6736
+ ```
6737
+ GET / index.abide
6738
+ POST /__abide/handlers/crm/id/markPaid crm/[id].abide (private)
6739
+ POST /__abide/handlers/crm/lib/Notes/addNote crm/lib/Notes.abide (private)
6740
+ GET /crm crm/index.abide
6741
+ GET /crm/[id] crm/[id].abide
6742
+ GET /crm/getInvoice crm/getInvoice.ts
6743
+
6744
+ hooks
6745
+ middleware /crm crm/layout.abide
6746
+ ```
6747
+
6748
+ Each private handler is marked. Read its file against the hooks: neither address is below `/crm`, and the `middleware` filed there runs over both anyway, their files being under `crm` (16.72). `abide routes --json` prints the same as one document.
6749
+
6750
+ Read on: [Authorization](/data/decide-who-may-call-what.md)
6751
+
6664
6752
  ## `rpc.url` is derived, not baked
6665
6753
 
6666
6754
  The generated wrapper carries a **mount-relative** address. `url` resolves it at read time:
@@ -6676,7 +6764,7 @@ A mount is chosen after the build — `APP_URL` of `https://abide.com/v2` serves
6676
6764
 
6677
6765
  An address without its arguments is not something a browser can fetch, so the call takes them. They are written in the same canonical wire form the request itself uses, rather than a query string you assemble and the server parses back differently:
6678
6766
 
6679
- ```abide src/media/page.abide — excerpt
6767
+ ```abide src/media/index.abide — excerpt
6680
6768
  <video src={playFile.url({ id: file.id })} controls></video>
6681
6769
  <img src={getPoster.url({ path, width: 250 })} alt="">
6682
6770
  ```
@@ -6691,17 +6779,6 @@ The method the handler was declared with, as a string. Reading it rather than wr
6691
6779
 
6692
6780
  Read on: [Mutations](/data/change-something-on-the-server.md)
6693
6781
 
6694
- ## The address comes from the directory and the export name
6695
-
6696
- | File | Export | Answers at |
6697
- | --- | --- | --- |
6698
- | `src/users/page.abide` | `getUser` | `/users/getUser` |
6699
- | `src/admin/page.abide` | `recent` | `/admin/recent` |
6700
- | `src/media/[id]/page.abide` | `save` | `/media/42/save`, with `id` in its args |
6701
- | `src/media/[id]/comments.abide` | `addComment` | `/media/42/addComment` |
6702
-
6703
- So the address is readable from the source tree, which is what a log line or a network panel is for. A parameter in the route reaches the handler's args from the path, so its `Args` declares it. The address is not a thing to write out: the route table is a build artifact, and two files that answer one method at one address are a build error naming both.
6704
-
6705
6782
  ## `rpc.raw` hands back the `Response`
6706
6783
 
6707
6784
  Where you want the `Response` rather than a decoded value — a header to read, a `Blob` to hand to a download, a body you will frame yourself:
@@ -6725,7 +6802,7 @@ Read on: [Streaming data](/data/send-data-as-it-arrives.md) · [Reading data](/d
6725
6802
  The string you passed as `description`, readable back off the handler. The same one rides onto the OpenAPI operation, the MCP tool and the CLI command — one place to write it, and no surface can carry a different sentence than another.
6726
6803
 
6727
6804
  ```ts server
6728
- export const getInvoice = GET(
6805
+ export default GET(
6729
6806
  ({ id }: { id: string }) => database.invoice.find(id),
6730
6807
  { description: 'One invoice, by id.' },
6731
6808
  )
@@ -6766,8 +6843,8 @@ A file is a route. The parts of the path in brackets become values you can read.
6766
6843
 
6767
6844
  | File | Serves |
6768
6845
  | --- | --- |
6769
- | `src/invoices/page.abide` | `/invoices` |
6770
- | `src/invoices/[id]/page.abide` | `/invoices/42`, as `route.params.id` |
6846
+ | `src/invoices/index.abide` | `/invoices` |
6847
+ | `src/invoices/[id]/index.abide` | `/invoices/42`, as `route.params.id` |
6771
6848
  | `src/layout.abide` | the chrome for everything below it |
6772
6849
  | `src/error.abide` | where a failure renders, 404 included |
6773
6850
 
@@ -6813,38 +6890,87 @@ Read on: [Streaming HTML](/pages/send-the-page-before-the-data-lands.md) · [Hea
6813
6890
 
6814
6891
  *A file becomes a route, and the parts of the path become values you can read.*
6815
6892
 
6816
- A route is a file. Put a `page.abide` in a directory under `src` and the path it sits at is the path it answers on — there is no table to register it in, and nothing to keep in step with the filesystem.
6893
+ A route is a file, and its path below `src` is the path it answers on. There is no table to register it in and nothing to keep in step with the filesystem — listing a directory lists its routes.
6894
+
6895
+ ## A file's path is its address
6896
+
6897
+ | File | Answers on |
6898
+ | --- | --- |
6899
+ | `src/index.abide` | `/` |
6900
+ | `src/invoices/index.abide` | `/invoices` |
6901
+ | `src/invoices/new.abide` | `/invoices/new` |
6902
+ | `src/invoices/[id].abide` | `/invoices/4310` |
6903
+ | `src/invoices/[id]/index.abide` | `/invoices/4310`, the same route |
6904
+ | `src/docs/[...path].abide` | `/docs/start/install` |
6905
+ | `src/invoices/listInvoices.ts` | `GET /invoices/listInvoices` |
6906
+
6907
+ An `index.abide` answers its directory's own address, and any other `.abide` adds its name. A `layout.abide` and an `error.abide` are not pages: they take their directory's route, and wrap or catch what is below it.
6908
+
6909
+ `[id].abide` and `[id]/index.abide` are two spellings of one route, so writing both fails the build, naming the two files. The directory is the one with room. A handler file inside `[id]/` answers below the invoice, at `/invoices/[id]/markPaid`, and `id` reaches its arguments from the path. One beside `[id].abide` answers for the whole subject. Start with the file, and move it into a directory once the invoice has files of its own.
6910
+
6911
+ A `.ts` beside the pages is a handler file. Its default export is the `GET` or the `POST` it declares, it exports nothing else, and its name is its last segment.
6912
+
6913
+ Read on: [Reading data](/data/read-data-without-writing-an-api.md)
6914
+
6915
+ ## A directory lists its routes
6916
+
6917
+ Four directory names are never a segment: `lib` for components and modules, `tests` for tests, `server` for code the browser never loads, and `public` for files served as they are. Nothing under one has an address. Every other file directly in a directory is a page, a layout, an error page or a handler file.
6918
+
6919
+ ```
6920
+ src/
6921
+ ├── index.abide /
6922
+ ├── layout.abide wraps every page
6923
+ └── invoices/
6924
+ ├── index.abide /invoices
6925
+ ├── [id].abide /invoices/[id]
6926
+ ├── layout.abide wraps both
6927
+ ├── listInvoices.ts GET /invoices/listInvoices
6928
+ ├── lib/Row.abide a component, no address
6929
+ └── tests/Row.test.ts a test, no address
6930
+ ```
6931
+
6932
+ So the component a page places goes in a `lib` beside it, and is imported as `./lib/Row.abide`. A stylesheet, a helper or a test written straight beside the pages fails the build, naming the file and the `lib` to move it into. That refusal is what keeps a listing honest: what a directory holds is what it answers.
6933
+
6934
+ ## `abide routes` prints every address and its file
6935
+
6936
+ Run in the app above, it prints each route with the method it answers and the file answering it, then the file filing each hook and the route it covers:
6937
+
6938
+ ```
6939
+ $ abide routes
6940
+ GET / index.abide
6941
+ POST /__abide/handlers/invoices/id/markPaid invoices/[id].abide (private)
6942
+ GET /invoices invoices/index.abide
6943
+ GET /invoices/[id] invoices/[id].abide
6944
+ GET /invoices/listInvoices invoices/listInvoices.ts
6945
+
6946
+ hooks
6947
+ middleware /invoices invoices/layout.abide
6948
+ onStart / layout.abide
6949
+ ```
6950
+
6951
+ The `(private)` line is the `markPaid` that `[id].abide`'s own `<script server>` declares: it is that file's alone, so it answers below `/__abide/handlers/` rather than at an address of the app's. `--json` prints the same map as one document.
6952
+
6953
+ Read on: [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
6817
6954
 
6818
6955
  ## The three dynamic segments
6819
6956
 
6820
- | The directory is named | It matches | In `route.params` |
6957
+ | The file or directory is named | It matches | In `route.params` |
6821
6958
  | --- | --- | --- |
6822
6959
  | `[id]` | exactly one segment, required | a required key |
6823
6960
  | `[[tab]]` | one segment or none | an optional key, absent when it did not match |
6824
6961
  | `[...rest]` | the remaining segments, terminal | a list, empty when it matched nothing |
6825
6962
 
6826
- A rest segment is terminal because there is nothing a segment after it could match against. The other two compose freely, so `invoices/[id]/[[tab]]` is one route answering two addresses rather than two routes agreeing about an invoice.
6963
+ A rest segment is terminal because there is nothing a segment after it could match against. The other two compose freely, so `invoices/[id]/[[tab]].abide` is one route answering two addresses rather than two routes agreeing about an invoice.
6827
6964
 
6828
6965
  An optional segment that did not match is **absent** rather than empty. That is the difference between `const { tab = 'summary' } = route.params` working and a default that never fires, which is a distinction an empty string would quietly lose.
6829
6966
 
6830
- ## What the file path decides
6831
-
6832
- | | |
6833
- | --- | --- |
6834
- | `src/page.abide` | `/` |
6835
- | `src/invoices/page.abide` | `/invoices` |
6836
- | `src/invoices/[id]/page.abide` | `/invoices/4310` |
6837
- | `src/docs/[...path]/page.abide` | `/docs/start/install` |
6838
- | `src/billing/page.abide` | `/billing` |
6839
-
6840
- A subject's pages live with the rest of it, and a directory named `server` or `public` is never a segment of an address. Any other file beside a `page.abide` — a component, a helper — is yours, and is no address.
6967
+ ## `route.params` and `route.name` follow the address
6841
6968
 
6842
6969
  `route.url` comes from the file path and `route.name` is the **resolution path** — the pattern that matched, not the address that arrived. So a log line, a metric and an `{#if}` over the current route all key on `invoices/[id]` for every invoice rather than on one address each.
6843
6970
 
6844
- ```abide src/invoices/[id]/page.abide — excerpt
6971
+ ```abide src/invoices/[id].abide — excerpt
6845
6972
  <script>
6846
6973
  import { reactive, route } from '@abide/abide'
6847
- import { getInvoice } from '#src/invoices/api.abide'
6848
6974
 
6849
6975
  const invoice = reactive(() => getInvoice({ id: route.params.id }))
6850
6976
  </script>
@@ -6855,28 +6981,22 @@ const invoice = reactive(() => getInvoice({ id: route.params.id }))
6855
6981
 
6856
6982
  Reading `route.params` is a live read like any other, so a navigation from one invoice to another moves the value rather than rebuilding the page.
6857
6983
 
6858
- A package can ship pages, layouts, error pages and handlers as `.abide` source, and the address they answer at is yours to choose. Importing one of its directories with `type: 'routes'` puts all of them below the file that imports it:
6859
-
6860
- ```abide abide
6861
- <script module>
6862
- import 'admin-kit/admin' with { type: 'routes' }
6863
- </script>
6864
-
6865
- <section class="tools"><slot/></section>
6866
- ```
6867
-
6868
- Written in `src/tools/layout.abide`, that line makes `admin-kit/admin/users/[id]/page.abide` answer at `/tools/users/[id]`, inside this layout. Moving the line moves the package. Its files route by their path below the directory named, as yours do, and compile with your app on its abide. A directory mounts once, and naming one of your own directories, or one that is not there, fails the build.
6869
-
6870
- Read on: [Links & navigation](/pages/link-to-another-page.md) · [Reading data](/data/read-data-without-writing-an-api.md)
6984
+ Read on: [Links & navigation](/pages/link-to-another-page.md)
6871
6985
 
6872
6986
  ## `Params` is what the segments matched
6873
6987
 
6874
- `Params` carries what `[param]`, `[[optional]]` and `[...rest]` matched: text, or a list for a rest. In a page it is typed from the page's own route rather than declared beside it — `abide check` reads `route.params()` in `invoices/[id]/page.abide` as `RouteParams<'/invoices/[id]'>`, so `id` is text and a key the route does not have is an error (23.9). A layout gets its own segments, and whatever a page below it adds.
6988
+ `Params` carries what `[param]`, `[[optional]]` and `[...rest]` matched: text, or a list for a rest. In a page it is typed from the page's own route rather than declared beside it — `abide check` reads `route.params()` in `invoices/[id].abide` as `RouteParams<'/invoices/[id]'>`, so `id` is text and a key the route does not have is an error (23.9). A layout gets its own segments, and whatever a page below it adds.
6875
6989
 
6876
6990
  Nothing else is in it. A query string is not a param — it is not part of what resolved the route, and treating it as one is how a route starts depending on something that never selected it.
6877
6991
 
6878
6992
  Read on: [Links & navigation](/pages/link-to-another-page.md)
6879
6993
 
6994
+ ## A package's pages answer below the file importing the package
6995
+
6996
+ A package can ship pages, layouts, error pages and handler files as source, and the address they answer at is yours to choose. Written in `src/tools/layout.abide`, `import 'admin-kit/admin' with { type: 'routes' }` puts all of them below that layout: `admin-kit/admin/users/[id].abide` answers at `/tools/users/[id]`, inside it.
6997
+
6998
+ Moving the line moves the package. Its files route by their path below the directory named, as yours do, and compile with your app on its abide. A directory mounts once, and naming one of your own directories, or one that is not there, fails the build.
6999
+
6880
7000
  ## Next
6881
7001
 
6882
7002
  - [Layouts](/pages/give-pages-the-same-chrome.md) — the chrome a page renders inside
@@ -6985,7 +7105,7 @@ That is the one case where a quiet fallback would be the wrong kindness: an erro
6985
7105
 
6986
7106
  **`onError(fn)` — the tracker hears what the caller is not told** — A contacts export whose worker has stopped answering. Run it: the page gets a bare 500, and the hook beside it gets the reason.
6987
7107
 
6988
- ```abide ContactsExport.abide
7108
+ ```abide lib/ContactsExport.abide
6989
7109
  <script server>
6990
7110
  import { GET, onError, POST } from '@abide/abide/server'
6991
7111
 
@@ -7008,12 +7128,12 @@ const exportWorker = {
7008
7128
 
7009
7129
  onError((error) => tracker.capture(error))
7010
7130
 
7011
- export const exportContacts = POST(() => exportWorker.run(), {
7131
+ const exportContacts = POST(() => exportWorker.run(), {
7012
7132
  description: 'Exports every contact.',
7013
7133
  })
7014
7134
 
7015
7135
  /** What the tracker was sent, which the caller of the export never is. */
7016
- export const capturedErrors = GET(() => tracker.captured, {
7136
+ const capturedErrors = GET(() => tracker.captured, {
7017
7137
  description: 'The errors the tracker was sent.',
7018
7138
  })
7019
7139
  </script>
@@ -7062,7 +7182,7 @@ Read on: [Lifecycle](/app/run-code-at-start-and-stop.md) · [Logging](/app/recor
7062
7182
 
7063
7183
  A `<head>` element in a `.abide` file contributes its children to the document head. It is markup rather than a call, so what a page adds to the head sits in the page, next to the values it is built from.
7064
7184
 
7065
- ```abide src/invoices/[id]/page.abide — excerpt
7185
+ ```abide src/invoices/[id]/index.abide — excerpt
7066
7186
  <head>
7067
7187
  <title>Invoice {route.params.id}</title>
7068
7188
  <meta name="description" content={invoice.summary}/>
@@ -7088,7 +7208,7 @@ The head goes out with the page once it is built, and a value still in flight do
7088
7208
 
7089
7209
  Nothing is opt-in here either: a `<title>` is a sink like any other, and it fills when the value lands. Metadata a crawler reads — which does not come back for the second version — is the one case that wants the value before the first flush, and that is the server's business rather than the head's.
7090
7210
 
7091
- ```abide src/invoices/[id]/page.abide — excerpt
7211
+ ```abide src/invoices/[id]/index.abide — excerpt
7092
7212
  <head>
7093
7213
  <title>Invoice {invoice.number}</title>
7094
7214
  </head>
@@ -7120,7 +7240,7 @@ Read on: [Links & navigation](/pages/link-to-another-page.md)
7120
7240
 
7121
7241
  `url` builds an address from a **route literal** — the pattern, not the address. So the thing you write down is the thing the router already knows, and a route that moves takes its links with it.
7122
7242
 
7123
- ```abide src/invoices/page.abide — excerpt
7243
+ ```abide src/invoices/index.abide — excerpt
7124
7244
  <a href={url('/invoices/[id]', { id: invoice.id })}>{invoice.number}</a>
7125
7245
  ```
7126
7246
 
@@ -7159,7 +7279,7 @@ Read on: [Routes](/pages/add-a-page.md)
7159
7279
 
7160
7280
  **`navigate(url)` — a new address, the same page** — Three invoices on one account, each with its own address. Type a note, then open another invoice — the address moves and the note is still there.
7161
7281
 
7162
- ```abide Invoice.abide
7282
+ ```abide lib/Invoice.abide
7163
7283
  <script>
7164
7284
  import { navigate, reactive, route } from '@abide/abide'
7165
7285
 
@@ -7227,7 +7347,7 @@ A view transition is a browser feature, and abide's part in it is to get out of
7227
7347
 
7228
7348
  ## The stylesheet is the switch
7229
7349
 
7230
- ```abide src/invoices/[id]/page.abide — excerpt
7350
+ ```abide src/invoices/[id]/index.abide — excerpt
7231
7351
  <style>
7232
7352
  .invoice-total {
7233
7353
  view-transition-name: invoice-total;
@@ -7367,7 +7487,7 @@ Read on: [Request to paint](/start/how-a-page-becomes-html.md) · [Loading state
7367
7487
 
7368
7488
  **`render(component)` — an invoice preview from the component itself** — The invoice a customer will be sent, previewed before it is issued: the handler renders the app's own invoice component into a document of its own.
7369
7489
 
7370
- ```abide InvoicePreview.abide
7490
+ ```abide lib/InvoicePreview.abide
7371
7491
  <script server>
7372
7492
  import { GET, page, render } from '@abide/abide/server'
7373
7493
  import Document from './InvoiceDocument.abide'
@@ -7403,7 +7523,7 @@ async function findInvoice(id: string) {
7403
7523
  return invoice
7404
7524
  }
7405
7525
 
7406
- export const previewInvoice = GET(
7526
+ const previewInvoice = GET(
7407
7527
  async ({ id }: { id: string }) => {
7408
7528
  const invoice = await findInvoice(id)
7409
7529
  return page(await render(Document({ invoice })))
@@ -7428,7 +7548,7 @@ let id = reactive('4021')
7428
7548
  class="h-56 w-full rounded-md border border-rule"></iframe>
7429
7549
  ```
7430
7550
 
7431
- ```abide InvoiceDocument.abide
7551
+ ```abide lib/InvoiceDocument.abide
7432
7552
  <script>
7433
7553
  import { props } from '@abide/abide'
7434
7554
 
@@ -7473,7 +7593,7 @@ The component `render` is given is the whole document when it is written inside
7473
7593
 
7474
7594
  An email body wants a document of its own — no bundle, no font preloads, inline styles — and that is the whole of what writing one means. Nothing else about the render changes. A component that writes `<html>` is a whole document, so placing one inside another is refused.
7475
7595
 
7476
- ```abide InvoiceDocument.abide — excerpt
7596
+ ```abide lib/InvoiceDocument.abide — excerpt
7477
7597
  <html lang="en">
7478
7598
  <body style="margin: 0; padding: 1.25rem; background: #fff; color: #1d2a28;
7479
7599
  font: 14px/1.5 system-ui, sans-serif">
@@ -7604,7 +7724,7 @@ Read on: [Local state](/values/show-a-value-that-changes.md) · [Loading states]
7604
7724
 
7605
7725
  **`contact.address.city = v` — a write through a path** — A contact's city, two levels down. Type into the field and watch the line above it.
7606
7726
 
7607
- ```abide Contact.abide
7727
+ ```abide lib/Contact.abide
7608
7728
  <script>
7609
7729
  import { reactive } from '@abide/abide'
7610
7730
 
@@ -7676,7 +7796,7 @@ Each of them updates the **one thing it names** and nothing around it. That is t
7676
7796
 
7677
7797
  **`{expr}` and `{raw(expr)}` — escaped, and not** — A note imported with markup in it. Type a tag into the field and watch the two rows disagree.
7678
7798
 
7679
- ```abide Note.abide
7799
+ ```abide lib/Note.abide
7680
7800
  <script>
7681
7801
  import { reactive } from '@abide/abide'
7682
7802
 
@@ -7717,7 +7837,7 @@ Read on: [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
7717
7837
 
7718
7838
  **`class:name={cond}` and `style:prop={value}` — one class, one property** — An invoice's age badge. Press + until it is late, and back down again.
7719
7839
 
7720
- ```abide Invoice.abide
7840
+ ```abide lib/Invoice.abide
7721
7841
  <script>
7722
7842
  import { reactive } from '@abide/abide'
7723
7843
 
@@ -7799,7 +7919,7 @@ An event handler is an attribute whose value is a function. There is no syntheti
7799
7919
 
7800
7920
  **`bind:element` and `on<event>` — the node, and what happens on it** — A contact's notes. Press New note and the field is cleared and waiting for you.
7801
7921
 
7802
- ```abide Note.abide
7922
+ ```abide lib/Note.abide
7803
7923
  <script>
7804
7924
  import { reactive } from '@abide/abide'
7805
7925
 
@@ -7866,7 +7986,7 @@ A form is a value you can see. The usual cost of saying so is a handler per fiel
7866
7986
 
7867
7987
  **`bind:value` — the field and the value are one copy** — A contact's email and plan, with the line underneath showing what a save would send. Type into the field.
7868
7988
 
7869
- ```abide Contact.abide
7989
+ ```abide lib/Contact.abide
7870
7990
  <script>
7871
7991
  import { reactive } from '@abide/abide'
7872
7992
 
@@ -7903,7 +8023,7 @@ Read on: [Local state](/values/show-a-value-that-changes.md)
7903
8023
 
7904
8024
  **`bind:checked` and `bind:open` — a boolean, not a string** — A reminders checkbox and a disclosure. Tick the box and open the panel, and watch the two lines underneath.
7905
8025
 
7906
- ```abide Advanced.abide
8026
+ ```abide lib/Advanced.abide
7907
8027
  <script>
7908
8028
  import { reactive } from '@abide/abide'
7909
8029
 
@@ -7947,7 +8067,7 @@ The readout reads `showAdvanced` like any other value, which is the point of bin
7947
8067
 
7948
8068
  **`{#if}` over a bound value — the field decides what renders** — A contact form that only asks for a billing address when you say there is one. Tick the box.
7949
8069
 
7950
- ```abide BillingContact.abide
8070
+ ```abide lib/BillingContact.abide
7951
8071
  <script>
7952
8072
  import { reactive } from '@abide/abide'
7953
8073
 
@@ -7972,7 +8092,7 @@ What was typed into the address goes with it, the input being unbound. Bind it t
7972
8092
 
7973
8093
  **`bind:group` — one value for radios, an array for checkboxes** — A contact's tier and the regions they cover. Change the tier, then tick and untick a region.
7974
8094
 
7975
- ```abide Assign.abide
8095
+ ```abide lib/Assign.abide
7976
8096
  <script>
7977
8097
  import { reactive } from '@abide/abide'
7978
8098
 
@@ -8042,7 +8162,7 @@ A derived value is writable too, and where its body hands back another `Reactive
8042
8162
 
8043
8163
  A `schema` runs on the way into a value, and a write it refuses is rejected at the boundary rather than stored and checked later:
8044
8164
 
8045
- ```abide src/account/page.abide — excerpt
8165
+ ```abide src/account/index.abide — excerpt
8046
8166
  <script>
8047
8167
  import { reactive } from '@abide/abide'
8048
8168
  import { z } from 'zod'
@@ -8068,7 +8188,7 @@ The field opens empty. An opening `undefined` lands without passing the schema (
8068
8188
 
8069
8189
  Both gates refuse. What you refuse **with** picks between them: a schema refuses as `ValidationError`, and a `transform` refuses as a name of your own — the only way a failure arrives carrying data you chose. `refuse.named` declares it once and resolves on both sides, so the same name serves a form field and a handler.
8070
8190
 
8071
- ```ts src/failures.ts
8191
+ ```ts src/lib/failures.ts
8072
8192
  import { refuse } from '@abide/abide'
8073
8193
  import { z } from 'zod'
8074
8194
 
@@ -8077,10 +8197,10 @@ export const notAnEmail = refuse.named('NotAnEmail', 422, undefined, {
8077
8197
  })
8078
8198
  ```
8079
8199
 
8080
- ```abide src/account/page.abide — excerpt
8200
+ ```abide src/account/index.abide — excerpt
8081
8201
  <script>
8082
8202
  import { reactive } from '@abide/abide'
8083
- import { notAnEmail } from '#src/failures'
8203
+ import { notAnEmail } from '#src/lib/failures'
8084
8204
 
8085
8205
  const email = reactive(undefined, {
8086
8206
  transform: (value: string) =>
@@ -8106,7 +8226,7 @@ The call site decides this, not the child.
8106
8226
 
8107
8227
  A prop is a **live read** of the caller's expression. `count={total}` tracks `total`, `count={total * 2}` tracks it as a derived read, and neither reinstantiates the component. The child declares the prop as the type of its value and writes it by name:
8108
8228
 
8109
- ```abide src/ui/Stepper.abide
8229
+ ```abide src/lib/Stepper.abide
8110
8230
  <script>
8111
8231
  import { props } from '@abide/abide'
8112
8232
 
@@ -8167,7 +8287,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Strea
8167
8287
 
8168
8288
  **`{#try}` — a boundary over a region, not over a line** — An invoice converted at a rate you type. Clear the field and watch what goes with it.
8169
8289
 
8170
- ```abide Total.abide
8290
+ ```abide lib/Total.abide
8171
8291
  <script>
8172
8292
  import { reactive } from '@abide/abide'
8173
8293
 
@@ -8236,7 +8356,7 @@ It iterates **zero times** over an absent subject. A list still loading is not a
8236
8356
 
8237
8357
  **`{#for row of rows by row.id}` — a reorder moves nodes** — Three contacts. Swap the top two, then reverse the lot.
8238
8358
 
8239
- ```abide Contacts.abide
8359
+ ```abide lib/Contacts.abide
8240
8360
  <script>
8241
8361
  import { reactive } from '@abide/abide'
8242
8362
 
@@ -8282,7 +8402,7 @@ Read on: [Patching a list](/values/patch-a-list-from-a-live-feed.md)
8282
8402
 
8283
8403
  **`reactive(contacts, { by: 'id' })` — every list over it moves nodes** — The contacts and the call queue, one list shown twice. Swap the top two.
8284
8404
 
8285
- ```abide ContactsAndQueue.abide
8405
+ ```abide lib/ContactsAndQueue.abide
8286
8406
  <script>
8287
8407
  import { reactive } from '@abide/abide'
8288
8408
 
@@ -8393,7 +8513,7 @@ A component is a `.abide` file, or a block in one. Invoking it is a capitalized
8393
8513
 
8394
8514
  An inline component is TitleCase like any other, and it exists for the piece of markup that is worth naming but not worth a file — a row in one table, a cell shape used twice on one page. The parentheses hold the pattern a `props()` call would destructure, and an annotation on it is that call's type argument:
8395
8515
 
8396
- ```abide src/contacts/[id]/page.abide — excerpt
8516
+ ```abide src/contacts/[id]/index.abide — excerpt
8397
8517
  {#component Field({ label, value }: { label: string; value: string })}
8398
8518
  <li><span>{label}</span><strong>{value}</strong></li>
8399
8519
  {/component}
@@ -8408,11 +8528,11 @@ It can be written anywhere markup can: at the top of the file, inside an element
8408
8528
 
8409
8529
  **It closes over where it is written.** A value the file's `<script>` declares is read by name inside it, and live, the same as anywhere else in the template. Declared in a row, it reads that row's item without being handed it:
8410
8530
 
8411
- ```abide src/contacts/page.abide — excerpt
8531
+ ```abide src/contacts/index.abide — excerpt
8412
8532
  <script>
8413
8533
  import { reactive } from '@abide/abide'
8414
- import { listContacts } from '#src/contacts/api.abide'
8415
- import type { Contact } from '#src/Contact.ts'
8534
+ import listContacts from '#src/contacts/listContacts.ts'
8535
+ import type { Contact } from '#src/lib/Contact.ts'
8416
8536
 
8417
8537
  const contacts = listContacts()
8418
8538
  let masked = reactive(true)
@@ -8434,10 +8554,10 @@ Setting `masked` rewrites each `<strong>` in place. Its own `<script>` is set up
8434
8554
 
8435
8555
  An inline component written directly between a component's tags is **passed to it as a prop** of the same name. The component declares that prop as a function of the props it hands over, and invokes it by name:
8436
8556
 
8437
- ```abide src/ui/ContactTable.abide
8557
+ ```abide src/lib/ContactTable.abide
8438
8558
  <script>
8439
8559
  import { props } from '@abide/abide'
8440
- import type { Contact } from '#src/Contact.ts'
8560
+ import type { Contact } from '#src/lib/Contact.ts'
8441
8561
 
8442
8562
  const { contacts, Row } = props<{
8443
8563
  contacts: Contact[]
@@ -8455,7 +8575,7 @@ const { contacts, Row } = props<{
8455
8575
  <slot/>
8456
8576
  ```
8457
8577
 
8458
- ```abide src/contacts/page.abide — excerpt
8578
+ ```abide src/contacts/index.abide — excerpt
8459
8579
  <ContactTable contacts={contacts}>
8460
8580
  {#component Row({ contact }: { contact: Contact })}
8461
8581
  <td>{contact.name}</td><td>{masked ? '••• ••••' : contact.phone}</td>
@@ -8470,7 +8590,7 @@ The table decides where each row goes and the page decides what is in it, readin
8470
8590
 
8471
8591
  **`props()` — a prop is a live read, not a snapshot** — An invoice's line count and the total a child component renders. Press + and watch the child.
8472
8592
 
8473
- ```abide Invoice.abide
8593
+ ```abide lib/Invoice.abide
8474
8594
  <script>
8475
8595
  import { reactive } from '@abide/abide'
8476
8596
  import Total from './Total.abide'
@@ -8489,7 +8609,7 @@ let lines = reactive(2)
8489
8609
  </div>
8490
8610
  ```
8491
8611
 
8492
- ```abide Total.abide
8612
+ ```abide lib/Total.abide
8493
8613
  <script>
8494
8614
  import { props } from '@abide/abide'
8495
8615
 
@@ -8516,7 +8636,7 @@ Read on: [Values by name](/templates/read-and-write-a-value-by-name.md) · [Form
8516
8636
 
8517
8637
  **`crm-ui/Initials.abide` — a design system's component, imported by name** — The CRM's contacts carry the avatar its design system ships. Nothing in the app compiled it, and it renders with its own styles.
8518
8638
 
8519
- ```abide ContactCard.abide
8639
+ ```abide lib/ContactCard.abide
8520
8640
  <script>
8521
8641
  import Initials from 'crm-ui/Initials.abide'
8522
8642
  </script>
@@ -8558,7 +8678,7 @@ const initials = name
8558
8678
 
8559
8679
  A package ships its components as `.abide` source, and the app imports one by the specifier its `exports` names, the same as a module. It is compiled with the app that imports it, wherever it lies: in `node_modules`, or in a package a workspace links from outside the app's root. Its props are typed by the app's project, its styles are scoped like the app's own, and it runs on the app's abide rather than on any copy the package installed beside it.
8560
8680
 
8561
- What it is not is an address. A `page.abide` inside a package is the package's own, and a handler a package's `<script server>` declares has no address of its own: the app gives it one by re-exporting it from a `<script server>` of its own, which answers at that file's route.
8681
+ What it is not is an address. A page or a handler file inside a package answers nowhere until the app mounts the package's directory with a `type: 'routes'` import, which places them below the file that imports it.
8562
8682
 
8563
8683
  Read on: [Styles](/templates/scope-styles-to-a-component.md) · [Reading data](/data/read-data-without-writing-an-api.md)
8564
8684
 
@@ -8572,7 +8692,7 @@ There is no implicit bag. A component that never calls `props()` takes no props
8572
8692
 
8573
8693
  Whatever a caller puts between the tags arrives as `children`, and `<slot/>` is where it renders. `<slot>fallback</slot>` renders the fallback where a caller passed none, which is how a panel has a default body without the caller having to know what it is:
8574
8694
 
8575
- ```abide src/ui/Panel.abide
8695
+ ```abide src/lib/Panel.abide
8576
8696
  <script>
8577
8697
  import { props } from '@abide/abide'
8578
8698
 
@@ -8582,7 +8702,7 @@ const { title } = props<{ title: string }>()
8582
8702
  <li><span>{title}</span><strong><slot>none yet</slot></strong></li>
8583
8703
  ```
8584
8704
 
8585
- ```abide src/contacts/[id]/page.abide — excerpt
8705
+ ```abide src/contacts/[id]/index.abide — excerpt
8586
8706
  <ul class="fields">
8587
8707
  <Panel title="phone">+1 415 555 0132</Panel>
8588
8708
  <Panel title="notes"/>
@@ -8601,7 +8721,7 @@ Read on: [Expressions](/templates/put-a-value-in-the-markup.md)
8601
8721
 
8602
8722
  A component that wraps a `<button>` usually declares the few props it reads itself and spreads the rest onto the element. `Attributes<'button'>` types that rest as what a `<button>` takes, so a caller is held to the element's own attributes and its `onclick` is handed a `MouseEvent`:
8603
8723
 
8604
- ```abide src/ui/Button.abide
8724
+ ```abide src/lib/Button.abide
8605
8725
  <script>
8606
8726
  import { props, type Attributes } from '@abide/abide'
8607
8727
 
@@ -8648,7 +8768,7 @@ A component's styles are the component's. Writing `.badge` inside one and having
8648
8768
 
8649
8769
  **`<style>` — the same class name in two components** — Two badge components that each style `.badge`, rendered side by side. Neither one reaches the other.
8650
8770
 
8651
- ```abide ScopedBadges.abide
8771
+ ```abide lib/ScopedBadges.abide
8652
8772
  <script>
8653
8773
  import Overdue from './Overdue.abide'
8654
8774
  import Paid from './Paid.abide'
@@ -8660,7 +8780,7 @@ import Paid from './Paid.abide'
8660
8780
  </div>
8661
8781
  ```
8662
8782
 
8663
- ```abide Overdue.abide
8783
+ ```abide lib/Overdue.abide
8664
8784
  <span class="badge">Overdue</span>
8665
8785
 
8666
8786
  <style>
@@ -8672,7 +8792,7 @@ import Paid from './Paid.abide'
8672
8792
  </style>
8673
8793
  ```
8674
8794
 
8675
- ```abide Paid.abide
8795
+ ```abide lib/Paid.abide
8676
8796
  <span class="badge">Paid</span>
8677
8797
 
8678
8798
  <style>
@@ -8696,7 +8816,7 @@ Read on: [Components](/templates/reuse-a-piece-of-markup.md)
8696
8816
 
8697
8817
  **`:global(…)` — the per-selector escape from scoping** — A badge component whose stylesheet also colors a paragraph the page rendered, not the component.
8698
8818
 
8699
- ```abide GlobalLegend.abide
8819
+ ```abide lib/GlobalLegend.abide
8700
8820
  <script>
8701
8821
  import Legend from './Legend.abide'
8702
8822
  </script>
@@ -8707,7 +8827,7 @@ import Legend from './Legend.abide'
8707
8827
  </div>
8708
8828
  ```
8709
8829
 
8710
- ```abide Legend.abide
8830
+ ```abide lib/Legend.abide
8711
8831
  <span class="badge">Overdue</span>
8712
8832
 
8713
8833
  <style>
@@ -8762,24 +8882,29 @@ plugins = ["bun-plugin-tailwind"]
8762
8882
 
8763
8883
  A `.abide` file's `<script>` block is the component's setup. It runs when an instance is created, in order, top to bottom — there is no mount hook to register and nothing to call it from, because the block **is** the hook.
8764
8884
 
8765
- ## The two blocks
8885
+ ## Three script blocks, and when each runs
8766
8886
 
8767
8887
  | You write | Runs |
8768
8888
  | --- | --- |
8769
8889
  | `<script>` | once per instance of the component |
8770
8890
  | `<script module>` | once for the module, on a server as in a browser |
8891
+ | `<script server>` | once for the module, on the server alone |
8771
8892
 
8772
- The difference is what a variable declared in each belongs to. One row's `calls` is that row's; a lookup table every row shares belongs to the module and is built once however many rows there are.
8893
+ The difference between the first two is what a variable declared in each belongs to. One row's `calls` is that row's; a lookup table every row shares belongs to the module and is built once however many rows there are.
8773
8894
 
8774
- A `<script module>` is module scope, the same as the top of a `.ts` file, so what it exports is the module's: another file imports a helper from it as from any module. A `<script>` cannot export, because it runs once per instance and an export belongs to no one instance.
8895
+ The third never reaches a browser. It holds the file's own handlers and its hooks, and the setup calls a handler it declares by name.
8896
+
8897
+ None of the three exports anything — the component is the file's one export — so a helper another file imports goes in a module under `lib`.
8775
8898
 
8776
8899
  On a server, module scope is shared by every request. A `reactive` there is still one per request, as a `reactive` is wherever it is declared. A plain `let` is the process's, so state that changes while a page renders belongs in a `reactive`, as `opened` is below.
8777
8900
 
8901
+ Read on: [Reading data](/data/read-data-without-writing-an-api.md)
8902
+
8778
8903
  ## Per instance, and once
8779
8904
 
8780
8905
  **`<script>` and `<script module>` — per instance, and once** — Two contact rows from one component. Log a call on the first and read both rows.
8781
8906
 
8782
- ```abide Contacts.abide
8907
+ ```abide lib/Contacts.abide
8783
8908
  <script>
8784
8909
  import Row from './Row.abide'
8785
8910
  </script>
@@ -8790,7 +8915,7 @@ import Row from './Row.abide'
8790
8915
  </ul>
8791
8916
  ```
8792
8917
 
8793
- ```abide Row.abide
8918
+ ```abide lib/Row.abide
8794
8919
  <script module>
8795
8920
  import { reactive } from '@abide/abide'
8796
8921
 
@@ -8903,13 +9028,13 @@ An environment variable is a string or it is missing. What an app wants is a num
8903
9028
 
8904
9029
  **`onConfig(fn)` and `config()` — a default the deployment overrides** — A CRM's retry count: 3 beside the handler, 5 in the deployment's .env. The page adds one for the first try and gets 6, not 51.
8905
9030
 
8906
- ```abide Retries.abide
9031
+ ```abide lib/Retries.abide
8907
9032
  <script server>
8908
9033
  import { config, GET, onConfig } from '@abide/abide/server'
8909
9034
 
8910
9035
  onConfig(() => ({ RETRIES: 3 }))
8911
9036
 
8912
- export const retryPolicy = GET(() => ({ retries: config().RETRIES }), {
9037
+ const retryPolicy = GET(() => ({ retries: config().RETRIES }), {
8913
9038
  description: 'How many times a call is retried.',
8914
9039
  })
8915
9040
  </script>
@@ -9018,7 +9143,7 @@ A `<script server>` runs once, on the server, before the first request. The root
9018
9143
 
9019
9144
  **`onStart(fn)` — work the first request cannot race** — A CRM's database pool, connected before the app answers anything. Reload the page and both times stay where the boot put them.
9020
9145
 
9021
- ```abide Pool.abide
9146
+ ```abide lib/Pool.abide
9022
9147
  <script server>
9023
9148
  import { GET, onStart, onStop } from '@abide/abide/server'
9024
9149
 
@@ -9049,7 +9174,7 @@ onStop(async (stop) => {
9049
9174
  })
9050
9175
 
9051
9176
  /** When the pool connected, and when the app came up around it. */
9052
- export const poolState = GET(
9177
+ const poolState = GET(
9053
9178
  () => ({ connectedAt: pool.connectedAt, upAt: pool.upAt }),
9054
9179
  { description: 'When the pool connected, and when the app came up.' },
9055
9180
  )
@@ -9080,7 +9205,7 @@ Failing hard is the only useful answer to a failed migration. An app that starts
9080
9205
 
9081
9206
  ## `onStop` closes what `onStart` opened, after the last request
9082
9207
 
9083
- ```abide Pool.abide — excerpt
9208
+ ```abide lib/Pool.abide — excerpt
9084
9209
  onStop(async (stop) => {
9085
9210
  await stop()
9086
9211
  await pool.close()
@@ -9104,27 +9229,10 @@ Read on: [Config](/app/configure-the-app.md) · [Authorization](/data/decide-who
9104
9229
 
9105
9230
  ## A root rung answers what no route does
9106
9231
 
9107
- `middleware` filed at the root route runs before routing, so a rung there can answer an address no page or handler claims, and hand everything else on with `next()`. These docs serve every page as markdown that way:
9108
-
9109
- ```abide src/downloads.abide — excerpt
9110
- middleware(async (next, { request }) => {
9111
- const { pathname, origin } = new URL(request.url)
9112
- if (!pathname.endsWith('.md') && !pathname.endsWith('/llms.txt'))
9113
- return next()
9114
- if (pathname === '/llms.txt')
9115
- return new Response('# abide\n\nEvery page, as markdown: abide.md\n')
9116
- if (pathname === '/abide.md') {
9117
- bundled ??= bundle(origin)
9118
- return new Response(await bundled, { headers: MARKDOWN })
9119
- }
9120
- for (const group of SITE)
9121
- for (const page of group.pages)
9122
- if (pathname === markdownAddressOf(page.route))
9123
- return new Response(await pageMarkdown(page.route, origin), {
9124
- headers: MARKDOWN,
9125
- })
9126
- return next()
9127
- })
9232
+ `middleware` filed at the root route runs before routing, so a rung there can answer an address no page or handler claims, and hand everything else on with `next()`. These docs serve every page as markdown that way — the rung is a module's export, and the root layout files it with `middleware(serveDownloads)`, since a hook is only called in a page, a layout or a handler file (37.13):
9233
+
9234
+ ```ts src/lib/serveDownloads.ts — excerpt
9235
+ export const serveDownloads: Middleware<
9128
9236
  ```
9129
9237
 
9130
9238
  It uses the same response helpers everything else does, there being no second set.
@@ -9151,7 +9259,7 @@ abide does not know how you authenticate somebody. It knows what to do **after**
9151
9259
 
9152
9260
  **`principal.set` — one seal, read everywhere** — A sign-in on a CRM, and the header above it. Sign in, then sign out, and watch a component the form never names.
9153
9261
 
9154
- ```abide SignIn.abide
9262
+ ```abide lib/SignIn.abide
9155
9263
  <script server>
9156
9264
  import { principal, refuse } from '@abide/abide'
9157
9265
  import { POST } from '@abide/abide/server'
@@ -9167,7 +9275,7 @@ async function userByEmail(email: string) {
9167
9275
  return USERS[email] ?? null
9168
9276
  }
9169
9277
 
9170
- export const signIn = POST(
9278
+ const signIn = POST(
9171
9279
  async ({ email }: { email: string }) => {
9172
9280
  const user = await userByEmail(email)
9173
9281
  if (user === null) return refuse(401, 'No such account.')
@@ -9176,7 +9284,7 @@ export const signIn = POST(
9176
9284
  { description: 'Signs in by email.' },
9177
9285
  )
9178
9286
 
9179
- export const signOut = POST(() => principal.clear(), {
9287
+ const signOut = POST(() => principal.clear(), {
9180
9288
  description: 'Signs out.',
9181
9289
  })
9182
9290
  </script>
@@ -9200,7 +9308,7 @@ let email = reactive('ada@example.com')
9200
9308
  </div>
9201
9309
  ```
9202
9310
 
9203
- ```abide Header.abide
9311
+ ```abide lib/Header.abide
9204
9312
  <script>
9205
9313
  // The seal is ambient, so this reads it with nothing handed down.
9206
9314
  import { principal } from '@abide/abide'
@@ -9240,7 +9348,7 @@ Each is a `Reactive`, so reading one in a template subscribes and a sign-out mov
9240
9348
 
9241
9349
  **`onPrincipal(fn)` — an id in the cookie, a user on the page** — A CRM that seals nothing but an account id. Sign in as either account and the name and role arrive anyway, looked up on the server.
9242
9350
 
9243
- ```abide Badge.abide
9351
+ ```abide lib/Badge.abide
9244
9352
  <script server>
9245
9353
  import { onPrincipal, POST, principal } from '@abide/abide/server'
9246
9354
 
@@ -9270,7 +9378,7 @@ onPrincipal(async (claims) => {
9270
9378
  })
9271
9379
 
9272
9380
  /** Seals the id alone, and answers with what `onPrincipal` made of it. */
9273
- export const signInAs = POST(
9381
+ const signInAs = POST(
9274
9382
  async ({ id }: { id: string }) => {
9275
9383
  await principal.set({ id })
9276
9384
  const { name = '', role = '' } = principal.resolved()
@@ -9322,7 +9430,7 @@ type Principal = { authenticated, expiresAt?, error?, …claims }
9322
9430
 
9323
9431
  **`cookies()` — one jar, read on both sides** — The contact list's density, kept in a cookie. Pick one and reload the page — the server paints the one you picked.
9324
9432
 
9325
- ```abide Density.abide
9433
+ ```abide lib/Density.abide
9326
9434
  <script>
9327
9435
  import { cookies, reactive } from '@abide/abide'
9328
9436
 
@@ -9369,13 +9477,11 @@ Three ambient values, all server-side, all **request-local**: the request being
9369
9477
 
9370
9478
  ## `request()` is the real `Request`
9371
9479
 
9372
- ```abide src/invoices/api.abide — excerpt
9373
- <script server>
9374
- export const greeting = GET(() => {
9480
+ ```ts src/invoices/greeting.ts — excerpt
9481
+ export default GET(() => {
9375
9482
  const locale = request().headers.get('accept-language') ?? 'en'
9376
9483
  return locale.startsWith('fr') ? 'Bonjour' : 'Hello'
9377
9484
  })
9378
- </script>
9379
9485
  ```
9380
9486
 
9381
9487
  It is the platform's own `Request`, not a wrapper — so everything you already know about one applies, and anything that takes a `Request` takes this.
@@ -9390,7 +9496,7 @@ Read on: [Authorization](/data/decide-who-may-call-what.md) · [Schemas](/data/c
9390
9496
 
9391
9497
  **`response()` — a status the handler decides** — An integration adds contacts through the API, and expects 201 and where the new record lives.
9392
9498
 
9393
- ```abide AddContact.abide
9499
+ ```abide lib/AddContact.abide
9394
9500
  <script server>
9395
9501
  import { POST, response } from '@abide/abide/server'
9396
9502
 
@@ -9404,7 +9510,7 @@ async function insertContact(name: string) {
9404
9510
  return { id, name }
9405
9511
  }
9406
9512
 
9407
- export const addContact = POST(
9513
+ const addContact = POST(
9408
9514
  async ({ name }: { name: string }) => {
9409
9515
  const contact = await insertContact(name)
9410
9516
  response().status = 201
@@ -9496,7 +9602,7 @@ log.error('mailer refused', { error })
9496
9602
 
9497
9603
  **`log.named` and `DEBUG` — one namespace on, one off** — A CRM's own log. Send an invoice, then press Poll — only one of them has a namespace that is on.
9498
9604
 
9499
- ```abide Console.abide
9605
+ ```abide lib/Console.abide
9500
9606
  <script>
9501
9607
  import { log } from '@abide/abide'
9502
9608
 
@@ -9569,7 +9675,7 @@ type LogRecord = {
9569
9675
 
9570
9676
  In a template it is a member access rather than a name, so the read is spelled out:
9571
9677
 
9572
- ```abide Console.abide — excerpt
9678
+ ```abide lib/Console.abide — excerpt
9573
9679
  {#for record of log.records().slice(-20)}
9574
9680
  ```
9575
9681
 
@@ -9605,12 +9711,12 @@ Every log line an abide app writes carries a `trace`. It is the id of the operat
9605
9711
 
9606
9712
  **`trace()` — one id for everything one request did** — A contact synced to billing across two modules. Sync it, and both lines it logged carry that request's trace; sync again for a new one.
9607
9713
 
9608
- ```abide Sync.abide
9714
+ ```abide lib/Sync.abide
9609
9715
  <script server>
9610
9716
  import { log, POST, trace } from '@abide/abide/server'
9611
9717
  import { billing } from './billing'
9612
9718
 
9613
- export const syncContact = POST(
9719
+ const syncContact = POST(
9614
9720
  async ({ id }: { id: string }) => {
9615
9721
  log(`contact ${id} loaded`)
9616
9722
  await billing.notify(id)
@@ -9650,7 +9756,7 @@ async function sync() {
9650
9756
  </ul>
9651
9757
  ```
9652
9758
 
9653
- ```ts billing.ts
9759
+ ```ts lib/billing.ts
9654
9760
  import { log } from '@abide/abide/server'
9655
9761
 
9656
9762
  /** Another module the sync reaches, handed the contact and nothing else. */
@@ -9771,7 +9877,7 @@ Only what finished **before the head went out** can be in it. A page sends its h
9771
9877
 
9772
9878
  **`onHealth(fn)` — your fields beside the baseline** — The account a load balancer scrapes, read on a CRM's status panel through health(). The database line is the app's own; the rest is abide's.
9773
9879
 
9774
- ```abide Account.abide
9880
+ ```abide lib/Account.abide
9775
9881
  <script server>
9776
9882
  import { onHealth, onStart, onStop } from '@abide/abide/server'
9777
9883
 
@@ -9823,7 +9929,7 @@ Read on: [Lifecycle](/app/run-code-at-start-and-stop.md) · [Logging](/app/recor
9823
9929
  `health` is an `Rpc`, imported from `@abide/abide` and called the same way on both sides: `health()` in a page, `await
9824
9930
  health()` in a handler. The call is what asks, so a page that never makes it loads no account.
9825
9931
 
9826
- ```abide Account.abide — excerpt
9932
+ ```abide lib/Account.abide — excerpt
9827
9933
  const account = health()
9828
9934
  ```
9829
9935
 
@@ -9856,7 +9962,7 @@ A content security policy is worth having and painful to write, because the hard
9856
9962
 
9857
9963
  **`csp()` — your own scripts run, a stranger's do not** — A support widget somebody pasted into the CRM from another origin. Add it, and the policy this page was served with refuses it.
9858
9964
 
9859
- ```abide Widget.abide
9965
+ ```abide lib/Widget.abide
9860
9966
  <script>
9861
9967
  import { reactive } from '@abide/abide'
9862
9968
 
@@ -9883,21 +9989,24 @@ let refused = reactive('—')
9883
9989
  <script server>
9884
9990
  // The docs app's own hooks; each example files its own in its own file.
9885
9991
  import { csp, middleware, onConfig } from '@abide/abide/server'
9992
+ import { serveDownloads } from '#src/lib/serveDownloads.ts'
9886
9993
 
9887
9994
  // The sign-in examples need a secret: one per process, or the deployment's.
9888
9995
  const PRINCIPAL_SECRET = crypto.randomUUID()
9889
9996
 
9890
9997
  onConfig(() => ({ ABIDE_PRINCIPAL_SECRET: PRINCIPAL_SECRET }))
9891
9998
 
9999
+ middleware(serveDownloads)
10000
+
9892
10001
  // Every docs page's policy; 'self' so the docs can frame invoice previews.
9893
10002
  middleware(csp({ 'frame-ancestors': ["'self'"] }))
9894
10003
  </script>
9895
10004
 
9896
10005
  <script>
9897
- import DocsNav from '#src/docs/DocsNav.abide'
9898
- import SiteBar from '#src/docs/SiteBar.abide'
10006
+ import DocsNav from '#src/lib/DocsNav.abide'
10007
+ import SiteBar from '#src/lib/SiteBar.abide'
9899
10008
  import { url } from '@abide/abide'
9900
- import '#src/app.css'
10009
+ import '#src/lib/app.css'
9901
10010
  </script>
9902
10011
 
9903
10012
  <html lang="en">
@@ -10016,7 +10125,7 @@ There is one form and no options, `online` being ambient rather than declared: i
10016
10125
 
10017
10126
  **`online` — the connection, read by name** — An order form with unsaved changes. Take the browser offline — DevTools' Network panel, or the Wi-Fi — and watch the line.
10018
10127
 
10019
- ```abide OfflineBanner.abide
10128
+ ```abide lib/OfflineBanner.abide
10020
10129
  <script>
10021
10130
  import { online } from '@abide/abide'
10022
10131
  </script>
@@ -10070,7 +10179,7 @@ See [Reloading](/values/decide-when-a-value-reloads.md) for the two in full.
10070
10179
 
10071
10180
  That shapes what `online` is good for. It is right for telling a reader what to expect and wrong as the gate on a write: the load that failed is the honest signal, and `online` explains it. Read the failure, then read `online` to say why.
10072
10181
 
10073
- ```abide src/orders/page.abide — excerpt
10182
+ ```abide src/orders/index.abide — excerpt
10074
10183
  {#if orders.error()}
10075
10184
  <p>{online ? 'Could not load orders.' : 'You are offline.'}</p>
10076
10185
  {/if}
@@ -10107,7 +10216,7 @@ Nothing here asks you to annotate a handler for machines specifically. The piece
10107
10216
 
10108
10217
  | Piece | Where it comes from |
10109
10218
  | --- | --- |
10110
- | address | the file path and the export name |
10219
+ | address | the handler file's path |
10111
10220
  | method | which handler gave it an address |
10112
10221
  | argument schema | the annotations you already wrote, or the schema you named |
10113
10222
  | result schema | the handler's return type |
@@ -10183,11 +10292,11 @@ Read on: [Withholding a surface](/machines/keep-a-handler-off-a-surface.md)
10183
10292
 
10184
10293
  An MCP server is a list of tools with typed arguments and descriptions. A declared handler is a typed argument shape with a description. `/__abide/mcp` is the second read as the first, over streamable http.
10185
10294
 
10186
- ## The method decides tool or resource
10295
+ ## Every handler is a tool
10187
10296
 
10188
- A handler is published as a **tool** or as a **resource** according to which arm `GET` took, and no flag tells them apart. A read is a resource, a mutation is a tool — which is the distinction MCP already draws, arrived at from the declaration rather than from an annotation somebody has to keep true.
10297
+ Every handler the route table publishes is a **tool** — a read that takes no arguments as much as a mutation. None is published as an MCP resource, because a client that only calls tools, Claude Code under `abide-bridge` among them, would never reach a handler listed as one.
10189
10298
 
10190
- That is the whole of the mapping, and it is worth being able to state in one line: the model sees your app's verbs because your app already declared which of its handlers change something.
10299
+ That is the whole of the mapping: the model sees every handler your app publishes, and whether one changes something is the handler's own method, not a second listing.
10191
10300
 
10192
10301
  ## The description is the handler's
10193
10302
 
@@ -10258,8 +10367,7 @@ A `Provider` is one method, `run`, handed the transcript and a `Toolbox` of your
10258
10367
 
10259
10368
  `assistant` has no address of its own. You yield it from a streaming handler, so the address, the middleware and the surfaces are yours to set, as they are for any other handler.
10260
10369
 
10261
- ```abide src/support/api.abide — excerpt
10262
- <script server>
10370
+ ```ts src/support/ask.ts — excerpt
10263
10371
  import {
10264
10372
  assistant,
10265
10373
  type AssistantMessage,
@@ -10269,7 +10377,7 @@ import {
10269
10377
  } from '@abide/abide/server'
10270
10378
  import { onlyStaff } from '#src/server/onlyStaff'
10271
10379
 
10272
- export const ask = POST(
10380
+ export default POST(
10273
10381
  async function* ({ messages }: { messages: AssistantMessage[] }) {
10274
10382
  yield* assistant(
10275
10383
  messages,
@@ -10284,7 +10392,6 @@ export const ask = POST(
10284
10392
  },
10285
10393
  { middleware: [onlyStaff], clients: { mcp: false } },
10286
10394
  )
10287
- </script>
10288
10395
  ```
10289
10396
 
10290
10397
  `clients: { mcp: false }` keeps the assistant out of its own tool list. The page reads `ask` like any stream: each event arrives as a production of the value it hands back.
@@ -10306,23 +10413,16 @@ Changing the provider changes nothing downstream of the handler. A page written
10306
10413
 
10307
10414
  **`assistant(messages, provider, { mcp: true })` — your handlers as its tools** — The support assistant, asked about one invoice. The model here is a recording and its lines are fixed; the tool call it makes runs this site's own handler, as you.
10308
10415
 
10309
- ```abide Chat.abide
10416
+ ```abide lib/Chat.abide
10310
10417
  <script server>
10311
10418
  import {
10312
10419
  type AssistantMessage,
10313
10420
  assistant,
10314
- GET,
10315
10421
  POST,
10316
10422
  type Provider,
10317
10423
  } from '@abide/abide/server'
10318
10424
 
10319
- const PAID = new Set(['INV-4021'])
10320
-
10321
- export const invoiceStatus = GET(
10322
- ({ id }: { id: string }) => ({ id, paid: PAID.has(id) }),
10323
- { description: 'Whether an invoice has been paid, by its number.' },
10324
- )
10325
-
10425
+ // The MCP tool `invoiceStatus.ts` beside the page is listed as.
10326
10426
  const TOOL = 'get_machines_put-a-claude-assistant-on-your-site_invoiceStatus'
10327
10427
 
10328
10428
  // The model is a recording; the tool call it asks for is made for real.
@@ -10337,7 +10437,7 @@ const recorded: Provider = {
10337
10437
  },
10338
10438
  }
10339
10439
 
10340
- export const askWithTools = POST(
10440
+ const askWithTools = POST(
10341
10441
  async function* ({ messages }: { messages: AssistantMessage[] }) {
10342
10442
  yield* assistant(messages, recorded, { mcp: true })
10343
10443
  },
@@ -10381,6 +10481,16 @@ const events = reactive(() =>
10381
10481
  </div>
10382
10482
  ```
10383
10483
 
10484
+ ```ts invoiceStatus.ts
10485
+ import { GET } from '@abide/abide/server'
10486
+
10487
+ const PAID = new Set(['INV-4021'])
10488
+
10489
+ export default GET(({ id }: { id: string }) => ({ id, paid: PAID.has(id) }), {
10490
+ description: 'Whether an invoice has been paid, by its number.',
10491
+ })
10492
+ ```
10493
+
10384
10494
  *1 option, 0 tool definitions* — against the arm's tool table, which spells each tool's name, description and input schema a second time and dispatches the call by name.
10385
10495
 
10386
10496
  `mcp: true` offers the model the tools `/__abide/mcp` lists, so which handlers it may call is decided where it already is — `clients: { mcp: false }` withholds one from the assistant as it does from every other model. Asking for tools in an app that lists no handler for MCP throws and says so.
@@ -10399,7 +10509,7 @@ The cost is the resend: every call carries the whole transcript, and the API bil
10399
10509
 
10400
10510
  **`claudeApi(config)` — the request body you wrote, sent as written** — The support assistant on claudeApi, its baseURL pointed at a stand-in on this site that answers with the body it was sent. Ask, and read the request.
10401
10511
 
10402
- ```abide ApiRequest.abide
10512
+ ```abide lib/ApiRequest.abide
10403
10513
  <script server>
10404
10514
  import {
10405
10515
  type AssistantMessage,
@@ -10408,9 +10518,9 @@ import {
10408
10518
  POST,
10409
10519
  request,
10410
10520
  } from '@abide/abide/server'
10411
- import { messages as standIn } from './v1/messages.abide'
10521
+ import standIn from '../v1/messages.ts'
10412
10522
 
10413
- export const askTheApi = POST(
10523
+ const askTheApi = POST(
10414
10524
  async function* ({ messages }: { messages: AssistantMessage[] }) {
10415
10525
  // This site's stand-in for https://api.anthropic.com.
10416
10526
  const standInAt = new URL(standIn.url({}), request().url)
@@ -10456,12 +10566,11 @@ const events = reactive(() =>
10456
10566
  {/for}
10457
10567
  ```
10458
10568
 
10459
- ```abide v1/messages.abide
10460
- <script server>
10569
+ ```ts v1/messages.ts
10461
10570
  import { POST, sse } from '@abide/abide/server'
10462
10571
 
10463
10572
  // A stand-in for the Messages API, streaming back the body it was sent.
10464
- export const messages = POST(
10573
+ export default POST(
10465
10574
  (asked: Record<string, unknown>) =>
10466
10575
  sse([
10467
10576
  { type: 'message_start', message: { usage: { input_tokens: 0 } } },
@@ -10487,7 +10596,6 @@ export const messages = POST(
10487
10596
  ]),
10488
10597
  { clients: { ui: false, mcp: false, cli: false, openapi: false } },
10489
10598
  )
10490
- </script>
10491
10599
  ```
10492
10600
 
10493
10601
  `claudeApi` takes a Messages API request body as Anthropic documents it, snake_case, with the client's `apiKey` and `baseURL` beside it. abide writes `messages` and `stream`; every other field goes on the body as you wrote it, and the `tools` you list — a server tool like web search — are sent beside your own.
@@ -10502,7 +10610,7 @@ Read on: [Config](/app/configure-the-app.md)
10502
10610
 
10503
10611
  **`claudeCode(config)` — a turn on the reader's own Claude Code** — The support assistant on whatever Claude Code is paired with this browser. Ask first and it is refused; connect, run the command in a terminal, and ask again.
10504
10612
 
10505
- ```abide Bridge.abide
10613
+ ```abide lib/Bridge.abide
10506
10614
  <script server>
10507
10615
  import {
10508
10616
  type AssistantMessage,
@@ -10512,12 +10620,12 @@ import {
10512
10620
  POST,
10513
10621
  } from '@abide/abide/server'
10514
10622
 
10515
- export const bridge = GET(() => claudeCode.bridge(), {
10623
+ const bridge = GET(() => claudeCode.bridge(), {
10516
10624
  timeout: Infinity,
10517
10625
  clients: { mcp: false },
10518
10626
  })
10519
10627
 
10520
- export const askClaudeCode = POST(
10628
+ const askClaudeCode = POST(
10521
10629
  async function* ({ messages }: { messages: AssistantMessage[] }) {
10522
10630
  yield* assistant(
10523
10631
  messages,
@@ -10586,8 +10694,8 @@ The relay at `/__abide/bridge` opens the first time `claudeCode` hands out a com
10586
10694
 
10587
10695
  `claudeCode.bridge()` streams this browser's pairing: unpaired with the command to run, then paired the moment the visitor's bridge connects.
10588
10696
 
10589
- ```abide Bridge.abide — excerpt
10590
- export const bridge = GET(() => claudeCode.bridge(), {
10697
+ ```abide lib/Bridge.abide — excerpt
10698
+ const bridge = GET(() => claudeCode.bridge(), {
10591
10699
  timeout: Infinity,
10592
10700
  clients: { mcp: false },
10593
10701
  })
@@ -10620,20 +10728,26 @@ A handler declared once is reached four ways: imported by a page, listed in an O
10620
10728
  type Clients = { ui?: boolean; mcp?: boolean; cli?: boolean; openapi?: boolean }
10621
10729
  ```
10622
10730
 
10623
- ```abide src/admin/api.abide — excerpt
10624
- <script server>
10625
- export const rebuildSearchIndex = POST(rebuild, {
10731
+ ```ts src/admin/rebuildSearchIndex.ts — excerpt
10732
+ export default POST(rebuild, {
10626
10733
  clients: { ui: false, mcp: false },
10627
10734
  })
10628
- </script>
10629
10735
  ```
10630
10736
 
10631
10737
  ## Every key defaults to true
10632
10738
 
10633
- A handler you declare is on every surface unless you say otherwise. That is the right default because the alternative — opting each handler into each surface — is four decisions per handler and a fifth surface later that nothing is on.
10739
+ A handler file is on every surface unless its `clients` says otherwise (20.2). That is the right default because the alternative — opting each handler into each surface — is four decisions per handler and a fifth surface later that nothing is on.
10634
10740
 
10635
10741
  Every surface is **derived from the route table** rather than authored beside it, so a handler added is a handler listed, and there is no second file to keep in step.
10636
10742
 
10743
+ ## A private handler is on no surface
10744
+
10745
+ A handler a `<script server>` declares is not listed on OpenAPI, on MCP or by the CLI, whatever its `clients` says (16.71). Its file exports nothing (34.17), so no other page can import it either: the one caller is the file that declared it. So `clients` is a handler file's option, and the first decision is which of the two to write: a handler only one file calls stays in that file, and one meant for anything else is a handler file.
10746
+
10747
+ Off every surface is still not off the network. A private handler answers http at its address below `/__abide/handlers`, under the rungs filed over its file (16.72).
10748
+
10749
+ Read on: [Reading data](/data/read-data-without-writing-an-api.md)
10750
+
10637
10751
  ## `clients` decides what is generated, never who may call
10638
10752
 
10639
10753
  **`clients` is not access control.** A handler with `mcp:
@@ -10738,11 +10852,11 @@ abide dev
10738
10852
 
10739
10853
  | | |
10740
10854
  | --- | --- |
10741
- | `src/page.abide` | the page at `/` |
10855
+ | `src/index.abide` | the page at `/` |
10742
10856
  | `src/layout.abide` | the root layout: the document its pages are served in, and the `<script server>` holding the lifecycle hooks |
10743
- | `package.json`, `tsconfig.json`, `biome.json` | the toolchain, configured |
10857
+ | `package.json`, `tsconfig.json`, `biome.json`, `bunfig.toml` | the toolchain, configured — tests included |
10744
10858
 
10745
- `#src/*` is the one import alias, mapping onto `src/`. Grow the app by subject — a `src/billing/` with its own pages, components and `server` — and its pages answer below `/billing`, with no file moved to make room.
10859
+ `#src/*` is the one import alias, mapping onto `src/`. Grow the app by subject — a `src/billing/` with its own pages and handlers, a `lib` for its components and a `server` — and its pages answer below `/billing`, with no file moved to make room.
10746
10860
 
10747
10861
  Nothing generated is a framework file you are asked not to edit. The layout is a real component writing a real HTML document, and the page is a component too.
10748
10862
 
@@ -10797,7 +10911,7 @@ Read on: [Config](/app/configure-the-app.md) · [Build & start](/ship/build-and-
10797
10911
  abide run scripts/backfill.ts --since 2026-01-01
10798
10912
  ```
10799
10913
 
10800
- Runs a script under the abide runtime, which is what lets a one-off import `#src/server/database` and `#src/failures` without a build and without a second tsconfig.
10914
+ Runs a script under the abide runtime, which is what lets a one-off import `#src/server/database` and `#src/lib/failures` without a build and without a second tsconfig.
10801
10915
 
10802
10916
  That is the shape a backfill, a migration and a fixture loader want. They are not endpoints and should not become endpoints just to reach the app's own modules — which is what happens in a stack where the only thing that can import them is the server.
10803
10917
 
@@ -10927,9 +11041,12 @@ Every one of these is caught before the code runs, which is why the ergonomic sp
10927
11041
  | a `Failed` built and discarded | the refusal did not happen |
10928
11042
  | a `reactive` handed to two handlers | names both |
10929
11043
  | a nested object argument on a `GET` or `DELETE` | names the method, and says a `POST` is the fix |
10930
- | two files mounting at one address | names both files |
11044
+ | two files answering one address | names both files |
11045
+ | a stylesheet or a helper directly beside the pages | names the file, and the `lib` to move it into |
11046
+ | a handler file exporting anything but its handler | names the export, and where it belongs instead |
11047
+ | an `export` in a `.abide` | the component is the file's one export, and a handler another file calls is a handler file |
10931
11048
 
10932
- The last three are build errors rather than type errors, and they are here for the same reason: each is a mistake that otherwise fails at the first request, or worse, quietly succeeds.
11049
+ The last six are build errors rather than type errors, and they are here for the same reason: each is a mistake that otherwise fails at the first request, or worse, quietly succeeds.
10933
11050
 
10934
11051
  Read on: [`.abide` files](/reference/abide-files.md) · [Failures](/data/refuse-a-request-and-say-why.md)
10935
11052
 
@@ -10952,13 +11069,20 @@ A hover, a jump to a definition and the members offered after a `.` are answered
10952
11069
 
10953
11070
  Read on: [CLI](/reference/cli.md)
10954
11071
 
10955
- ## `abide sync` types a `.ts` that imports a `.abide`
11072
+ ## A handler another file calls is a `.ts`
11073
+
11074
+ A `.abide` exports its component and nothing else, so a handler a second file calls is a handler file — a `.ts` — and `tsc` reads it as it reads any other. A `.ts` importing it is typed with nothing generated in between and nothing to keep current.
11075
+
11076
+ ## `abide routes` is the table the build will serve
10956
11077
 
10957
11078
  ```
10958
- abide sync
11079
+ abide routes
11080
+ abide routes --json
10959
11081
  ```
10960
11082
 
10961
- `tsc` does not read a `.abide` either, so a `.ts` importing a handler from one would see nothing it exports. `abide sync` writes each file's exported types to `.abide/types`, which the scaffold's `tsconfig.json` already reaches. It parses and nothing more, so the scaffold runs it on `bun install`, and `abide lsp` keeps it current as you edit.
11083
+ Every address the app answers, the method and the file behind each, and the file filing each hook. It is the build's own reading of the tree, so an address you meant to add and did not is missing here before it is missing in production.
11084
+
11085
+ Read on: [Routes](/pages/add-a-page.md) · [Handler URLs](/data/find-the-url-a-handler-answers-on.md)
10962
11086
 
10963
11087
  ## In CI
10964
11088
 
@@ -10966,7 +11090,7 @@ abide sync
10966
11090
  abide check && abide build && bun test
10967
11091
  ```
10968
11092
 
10969
- `check` first, because a type error is the cheapest failure to report and the fastest to get. `build` next, because it catches the mount collisions and the value-shared-between-handlers errors that only a whole route table can see.
11093
+ `check` first, because a type error is the cheapest failure to report and the fastest to get. `build` next, because a bundle that will not build is the next cheapest, and the tests last, since they are the slowest to tell you anything.
10970
11094
 
10971
11095
  Read on: [Build & start](/ship/build-and-serve-the-app.md)
10972
11096
 
@@ -11074,21 +11198,21 @@ The launch remembers the choice in `APP_DATA_DIR`. If you started the app with t
11074
11198
 
11075
11199
  **Disconnect** in the File menu stops the app served here and goes back to the forms, and the next launch opens there too. **Connect…** opens the forms without stopping anything until you choose.
11076
11200
 
11077
- ## `launcher` sets the window and the forms
11201
+ ## `onLauncher` sets the window and the forms
11078
11202
 
11079
11203
  ```abide abide
11080
- <script module>
11081
- import type { LauncherOptions } from '@abide/abide/server'
11204
+ <script server>
11205
+ import { onLauncher } from '@abide/abide/server'
11082
11206
 
11083
- export const launcher: LauncherOptions = {
11207
+ onLauncher(() => ({
11084
11208
  width: 960,
11085
11209
  height: 640,
11086
11210
  connect: false,
11087
- }
11211
+ }))
11088
11212
  </script>
11089
11213
  ```
11090
11214
 
11091
- `src/launcher.abide` sits beside your root layout, and the `launcher` its `<script module>` exports is read when the app launches. A file of that block alone keeps the built-in page. `width` and `height` are the first window's size, and `minWidth` and `minHeight` its floor. After the first launch, the window opens where it was last left.
11215
+ `src/launcher.abide` sits beside your root layout, and what the `onLauncher` its `<script server>` calls answers is read when the app launches. It is the one file that may call it: an `onLauncher` anywhere else fails the build, naming the file. A file of that block alone keeps the built-in page. `width` and `height` are the first window's size, and `minWidth` and `minHeight` its floor. After the first launch, the window opens where it was last left.
11092
11216
 
11093
11217
  `start` and `connect` turn a button off. An app that only ever talks to its server turns `start` off. An app that only runs locally turns `connect` off, and Connect… leaves the File menu with it. Turning both off is a type error, because the launcher would have nothing to offer, and an app that slips one past the checker refuses to launch.
11094
11218
 
@@ -11117,42 +11241,48 @@ const { name, start, fields, failure } = props<Launch>()
11117
11241
  {/if}
11118
11242
  ```
11119
11243
 
11120
- Markup in `src/launcher.abide` replaces the built-in page, and receives the same `Launch` the built-in page is drawn from. `start` and `connect` are the addresses the two forms post to, and each is `undefined` where `launcher` turned that form off. `fields` is one `ConfigField` per input the start form asks for, prefilled. `failure` is what the last attempt was refused with, and `icon` and `accent` are the icon and the color the built-in page uses.
11244
+ Markup in `src/launcher.abide` replaces the built-in page, and receives the same `Launch` the built-in page is drawn from. `start` and `connect` are the addresses the two forms post to, and each is `undefined` where `onLauncher` turned that form off. `fields` is one `ConfigField` per input the start form asks for, prefilled. `failure` is what the last attempt was refused with, and `icon` and `accent` are the icon and the color the built-in page uses.
11121
11245
 
11122
11246
  It is rendered by `render` before the app it belongs to is running, so it is a document rather than a live page: its forms post and its `<style>` applies, but an `onclick` does nothing. Written inside `<html>`, it writes its own document. A launch page that fails to render reaches `onError`, and the built-in page shows in its place.
11123
11247
 
11124
11248
  Read on: [Manual rendering](/pages/render-a-document-yourself.md)
11125
11249
 
11126
- ## A page adds to the menu bar by exporting `menu`
11250
+ ## A page adds to the menu bar with `onMenu`
11127
11251
 
11128
11252
  ```abide abide
11129
- <script module>
11130
- import { menuItem } from '@abide/abide'
11253
+ <script>
11254
+ import { menuItem, onMenu, reactive, watch } from '@abide/abide'
11255
+
11256
+ let selected = reactive<string[]>([])
11257
+
11258
+ const exportInvoices = menuItem({ label: 'Export…', key: 'cmd+e' })
11259
+ const markPaid = menuItem({ label: 'Mark Paid', key: 'cmd+p' })
11131
11260
 
11132
- export const exportInvoices = menuItem({ label: 'Export…', key: 'cmd+e' })
11133
- export const menu = [
11261
+ onMenu(() => [
11134
11262
  { label: 'File', items: ['separator', exportInvoices] },
11135
11263
  {
11136
11264
  label: 'Invoices',
11137
- items: [{ label: 'Overdue', href: '/invoices/overdue' }],
11265
+ items: [
11266
+ { label: 'Overdue', href: '/invoices/overdue' },
11267
+ ...(selected.length > 0 ? [markPaid] : []),
11268
+ ],
11138
11269
  },
11139
- ]
11140
- </script>
11141
-
11142
- <script>
11143
- import { watch } from '@abide/abide'
11270
+ ])
11144
11271
 
11145
11272
  watch(() => exportInvoices.on(() => download()))
11273
+ watch(() => markPaid.on(() => payAll(selected)))
11146
11274
  </script>
11147
11275
  ```
11148
11276
 
11149
- The menu bar is the route's. Every desktop app has the app's own menu, File, Edit, View and Window, and each layout around the page and then the page itself adds what its `<script module>` exports as `menu`. A layout's menus are there on every page under it, and a page's are there only while it is showing. Navigate away and they go.
11277
+ The menu bar is the route's. Every desktop app has the app's own menu, File, Edit, View and Window, and each layout around the page and then the page itself adds what its `onMenu` answers. A layout's menus are there on every page under it, and a page's are there only while it is showing. Navigate away and they go.
11278
+
11279
+ The function is read the way a template is, so the menus follow the page's state. Mark Paid is in the Invoices menu while something is selected, and leaves it when the selection empties — the menu bar is told again each time a value the function read moves.
11150
11280
 
11151
11281
  A menu with the same `label` as one already there adds its items to the end of it, so Export… lands in File. Any other menu stands before Window, where macOS puts an app's own.
11152
11282
 
11153
11283
  | An item | When picked |
11154
11284
  | --- | --- |
11155
- | made by `menuItem` | runs every function its `on` filed, in the page that exported it |
11285
+ | made by `menuItem` | runs every function its `on` filed, in the page that filed it |
11156
11286
  | with an `href` | navigates there, as a link in the page would |
11157
11287
  | with a `role` | does what the platform does: `copy` copies, `quit` stops the app and quits |
11158
11288
  | with `items` | is a menu of its own |
@@ -11244,7 +11374,7 @@ abide
11244
11374
  > watch chat/room id=onboarding
11245
11375
  ```
11246
11376
 
11247
- The address is the handler's route without its leading `/` — the directory under `src` and the export — and the args are `name=value` words or one JSON object, as `call` takes them. Against a deployed app it is a command of its own: `abide watch chat/room id=onboarding --url https://app.example`.
11377
+ The address is the handler's route without its leading `/` — its file's path under `src` — and the args are `name=value` words or one JSON object, as `call` takes them. Against a deployed app it is a command of its own: `abide watch chat/room id=onboarding --url https://app.example`.
11248
11378
 
11249
11379
  ## The channel is the same channel
11250
11380
 
@@ -11313,9 +11443,9 @@ Read on: [Build & start](/ship/build-and-serve-the-app.md) · [Lifecycle](/app/r
11313
11443
 
11314
11444
  **`createApp()` — the app a test holds, with no port** — The test beside the card asks for the pool through app.fetch, then stops the app. Fetch the same handler over the wire and read the same answer.
11315
11445
 
11316
- ```abide Answer.abide
11446
+ ```abide lib/Answer.abide
11317
11447
  <script server>
11318
- import { GET, onStart, onStop } from '@abide/abide/server'
11448
+ import { onStart, onStop } from '@abide/abide/server'
11319
11449
  import { pool } from './pool'
11320
11450
 
11321
11451
  onStart(async (start) => {
@@ -11327,14 +11457,11 @@ onStop(async (stop) => {
11327
11457
  await stop()
11328
11458
  await pool.close()
11329
11459
  })
11330
-
11331
- export const poolState = GET(() => ({ connected: pool.connected }), {
11332
- description: 'Whether the pool is connected.',
11333
- })
11334
11460
  </script>
11335
11461
 
11336
11462
  <script>
11337
11463
  import { reactive } from '@abide/abide'
11464
+ import poolState from '../poolState.ts'
11338
11465
 
11339
11466
  let status = reactive('—')
11340
11467
  let traced = reactive('—')
@@ -11354,7 +11481,16 @@ let traced = reactive('—')
11354
11481
  </ul>
11355
11482
  ```
11356
11483
 
11357
- ```ts pool.ts
11484
+ ```ts poolState.ts
11485
+ import { GET } from '@abide/abide/server'
11486
+ import { pool } from './lib/pool.ts'
11487
+
11488
+ export default GET(() => ({ connected: pool.connected }), {
11489
+ description: 'Whether the pool is connected.',
11490
+ })
11491
+ ```
11492
+
11493
+ ```ts lib/pool.ts
11358
11494
  /** The pool `onStart` opens and `onStop` closes. */
11359
11495
  export const pool = {
11360
11496
  connected: false,
@@ -11367,10 +11503,10 @@ export const pool = {
11367
11503
  }
11368
11504
  ```
11369
11505
 
11370
- ```ts app.test.ts
11506
+ ```ts tests/app.test.ts
11371
11507
  import { expect, test } from 'bun:test'
11372
11508
  import { createApp } from '@abide/abide/server'
11373
- import { pool } from './pool'
11509
+ import { pool } from '../lib/pool'
11374
11510
 
11375
11511
  // Run from the app's own directory: `createApp` finds the app there (38.25).
11376
11512
  test('the pool handler answers in process as it does on the wire', async () => {
@@ -11396,8 +11532,72 @@ A `Request` in and a `Response` out, through everything a served request goes th
11396
11532
 
11397
11533
  Those are the parts a handler called directly does not have, which is the reason to reach for a request at all. Assert on the response and you are asserting on what a browser would have been sent.
11398
11534
 
11535
+ `poolState.ts` is a handler file, so the address the test asks for is its path. The test is `tests/app.test.ts`, beside the page it tests: nothing under a `tests` directory has an address, so a test sits next to the page or the handler it reaches and is never served.
11536
+
11399
11537
  Read on: [Call a handler](/ship/call-a-handler-from-the-terminal.md)
11400
11538
 
11539
+ ## A test renders a `.abide` it imports
11540
+
11541
+ **`render(component)` — the `.abide` a test imports** — The pipeline sums the deals still open. Its test renders the same component and reads the total off the markup.
11542
+
11543
+ ```abide lib/Pipeline.abide
11544
+ <script>
11545
+ import { reactive } from '@abide/abide'
11546
+
11547
+ let deals = reactive([
11548
+ { name: 'Analytical Engines', amount: 8000, won: false },
11549
+ { name: 'Hopper & Co', amount: 4400, won: false },
11550
+ ])
11551
+ const open = reactive(() => deals.filter((deal) => !deal.won))
11552
+ const total = reactive(() => open.reduce((sum, deal) => sum + deal.amount, 0))
11553
+
11554
+ const usd = (amount: number) => `$${amount.toLocaleString('en-US')}`
11555
+
11556
+ function win(name: string) {
11557
+ deals = deals.map((deal) =>
11558
+ deal.name === name ? { ...deal, won: true } : deal,
11559
+ )
11560
+ }
11561
+ </script>
11562
+
11563
+ <ul class="fields">
11564
+ {#for deal of open by deal.name}
11565
+ <li><span>{deal.name}</span><strong>{usd(deal.amount)}</strong>
11566
+ <button onclick={() => win(deal.name)}>Won</button></li>
11567
+ {/for}
11568
+ </ul>
11569
+
11570
+ <ul class="tiles">
11571
+ <li><span>open pipeline</span><strong>{usd(total)}</strong></li>
11572
+ </ul>
11573
+ ```
11574
+
11575
+ ```ts tests/Pipeline.test.ts
11576
+ import { expect, test } from 'bun:test'
11577
+ import { render } from '@abide/abide/server'
11578
+ import Pipeline from '../lib/Pipeline.abide'
11579
+
11580
+ // bunfig.toml preloads `@abide/abide/test`, which makes the import a component.
11581
+ test('the pipeline totals the deals still open', async () => {
11582
+ const html = new TextDecoder().decode(await render(Pipeline({})))
11583
+ expect(html).toContain('Hopper &amp; Co')
11584
+ expect(html).toContain('$12,400')
11585
+ })
11586
+ ```
11587
+
11588
+ The import is the component the build compiles, with no `createApp` run first, and `render` turns it into a document's bytes as a page's render does. The test asserts on markup, so what it checks is what a reader would see.
11589
+
11590
+ One line in the app's `bunfig.toml` makes that so, and the scaffold writes it:
11591
+
11592
+ ```toml bunfig.toml
11593
+ [test]
11594
+ preload = ["@abide/abide/test"]
11595
+ ```
11596
+
11597
+ Without it, `bun test` resolves a `.abide` import to the file's path, and the test fails on `Pipeline is not a function`.
11598
+
11599
+ Read on: [Manual rendering](/pages/render-a-document-yourself.md)
11600
+
11401
11601
  ## `app.run` is a scope and nothing else
11402
11602
 
11403
11603
  Calling a handler by name already runs its body, its rungs and its schema. What it needs from you is a scope — the thing a cache entry is filed under, and the lifetime a request-local value belongs to.
@@ -11437,7 +11637,7 @@ Read on: [Request](/app/read-the-incoming-request.md)
11437
11637
 
11438
11638
  Hold it with `await using` and it stops where the block ends, a failed assertion included. The test's second case stops the app by hand and finds the pool `onStart` opened closed again:
11439
11639
 
11440
- ```ts app.test.ts — excerpt
11640
+ ```ts tests/app.test.ts — excerpt
11441
11641
  test('stopping the app closes the pool onStart opened', async () => {
11442
11642
  const app = await createApp()
11443
11643
  expect(pool.connected).toBe(true)
@@ -11595,8 +11795,8 @@ Every `Reactive` takes these, whichever overload made it.
11595
11795
  | `s.then` | `<Result>(onSettled?: (value: Stored) => Result \| PromiseLike<Result>, onFailed?: (error: unknown) => Result \| PromiseLike<Result>) => Promise<Result>` | The settling, as a promise. Resolves when `busy()` goes false, and rejects with what a read would throw. |
11596
11796
  | `s.catch` | `<Result>(onFailed: (error: unknown) => Result \| PromiseLike<Result>) => Promise<Stored \| Result>` | The settling's refusal path, derived from `then`. Fires for a producer that failed with nothing landed, and for nothing else. |
11597
11797
  | `s.finally` | `(onSettled: () => void) => Promise<Stored>` | The settling's end, whichever way it went. What ends is the settling, not the value — a subscriber's copy of a channel settles at its first production and goes on. |
11598
- | `for await (… of s)` | `AsyncIterable<Produced>` | The live cursor face of a read. On a stream, one turn per chunk. On any other value, the standing value once per turn — a burst collapses to the latest, and an in-place `patch` is a turn of its own. |
11599
- | `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The production in flight from its start, and then what follows it. A chunk is a piece of one value, so a stream's cursor sees every chunk; any other value's cursor sees where the value stands. |
11798
+ | `for await (… of s)` | `AsyncIterable<Produced>` | What one run produced. On a stream, one turn per chunk, ending at the close and throwing what failed the run. On any other value, the settled value once, as `await` gives it. To follow changes, `watch`. |
11799
+ | `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The run being served, from its start to its end. A chunk is a piece of one value, so a stream's cursor sees every chunk; any other value's cursor sees the value it settles on. |
11600
11800
 
11601
11801
  ## Writes
11602
11802
 
@@ -11850,7 +12050,7 @@ export const notMember = refuse.named('NotMember', 403, undefined, {
11850
12050
  ### Returning a refusal
11851
12051
 
11852
12052
  ```ts server
11853
- export const invite = POST(async ({ group }: { group: string }) => {
12053
+ export default POST(async ({ group }: { group: string }) => {
11854
12054
  if (!(await isMember(group))) return notMember({ group })
11855
12055
  return send(group)
11856
12056
  })
@@ -12015,13 +12215,11 @@ There is no `schemas` option on `GET` or `POST`. A shape is a fact about the val
12015
12215
 
12016
12216
  ### A read any surface may call
12017
12217
 
12018
- ```abide src/invoices/page.abide — excerpt
12019
- <script server>
12020
- export const getInvoice = GET(
12218
+ ```ts src/invoices/getInvoice.ts — excerpt
12219
+ export default GET(
12021
12220
  ({ id }: { id: string }) => database.invoice.find(id),
12022
12221
  { description: 'One invoice, by id.' },
12023
12222
  )
12024
- </script>
12025
12223
  ```
12026
12224
 
12027
12225
  ### A value every caller follows
@@ -12032,9 +12230,9 @@ const rooms = reactive((_room: { id: string }): Turn[] => [], {
12032
12230
  by: 'id',
12033
12231
  })
12034
12232
 
12035
- export const room = GET(rooms)
12233
+ const room = GET(rooms)
12036
12234
 
12037
- export const say = POST(({ id, text }: { id: string; text: string }) => {
12235
+ const say = POST(({ id, text }: { id: string; text: string }) => {
12038
12236
  rooms({ id }).patch((turns) => {
12039
12237
  turns.push({ id: crypto.randomUUID(), text })
12040
12238
  })
@@ -12169,9 +12367,9 @@ An ambient that is a `Reactive` is read the way any value is read, so reading it
12169
12367
 
12170
12368
  ### The caller, in a rung
12171
12369
 
12172
- ```abide src/invoices/page.abide — excerpt
12370
+ ```abide src/invoices/index.abide — excerpt
12173
12371
  <script server>
12174
- export const voidInvoice = POST(
12372
+ const voidInvoice = POST(
12175
12373
  ({ id }: { id: string }) => database.invoice.void(id),
12176
12374
  {
12177
12375
  middleware: [
@@ -12361,7 +12559,7 @@ A **`.abide`** file is a component: a template, with `<script>` and `<style>` bl
12361
12559
  </script>
12362
12560
 
12363
12561
  <script server>
12364
- // module scope, on the server alone; exports handlers
12562
+ // module scope, on the server alone; private handlers, hooks
12365
12563
  </script>
12366
12564
 
12367
12565
  <script>
@@ -12376,7 +12574,7 @@ A **`.abide`** file is a component: a template, with `<script>` and `<style>` bl
12376
12574
  ```
12377
12575
 
12378
12576
  Every block is optional and order is free. A file with no `<script>` is markup, which is what a presentational component usually is. A `<script
12379
- server>` is written outside a `server` or `public` directory, and the browser's copy of the file carries a client per handler it exports in its place.
12577
+ server>` is written outside a `server` or `public` directory, and the browser's copy of the file carries a client per handler it declares in its place. No block exports: the component is the file's one export (34.17).
12380
12578
 
12381
12579
  ## Reading and writing by name
12382
12580
 
@@ -12403,25 +12601,22 @@ The rule is **position**. An operand, a text hole, an attribute and a value-type
12403
12601
  | Spelling | Meaning |
12404
12602
  | --- | --- |
12405
12603
  | `{expr}` | Reactive text, escaped. |
12406
-
12407
- ### Expressions
12408
-
12409
- | formatting whitespace | What a run of whitespace renders as. | 33.17, 33.18 |
12410
- | --- | --- | --- |
12411
- | `{raw(...)}` | Raw HTML. | 32.2 |
12412
- | `name={expr}` | A reactive attribute or property. | 32.3, 32.20 |
12413
- | `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | 32.4 |
12414
- | `name="…{expr}…"` | An interpolated quoted value. | 32.3 |
12415
- | `bind:value` | Two-way bind on an element. | 5.16, 32.5 |
12416
- | `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 |
12417
- | `bind:checked` | A boolean bind on an input. | 32.7 |
12418
- | `bind:open` | The same, on a `<details>`. | 32.7 |
12419
- | `bind:group` | Radio or checkbox membership. | 32.8 |
12420
- | `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. | 32.5, 32.6 |
12421
- | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. | 32.9 |
12422
- | `class:name={cond}` | Toggles a class. | 32.10 |
12423
- | `style:prop={value}` | Sets one style property. | 32.10, 32.19 |
12424
- | `{...expr}` | Spreads props or attributes. | 32.11, 33.12, 33.13 |
12604
+ | formatting whitespace | What a run of whitespace renders as. |
12605
+ | `{raw(...)}` | Raw HTML. |
12606
+ | `name={expr}` | A reactive attribute or property. |
12607
+ | `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. |
12608
+ | `name="…{expr}…"` | An interpolated quoted value. |
12609
+ | `bind:value` | Two-way bind on an element. |
12610
+ | `bind:prop={state}` | Hands a component prop the caller's `Reactive`, or a `{get, set}` pair, to write through. |
12611
+ | `bind:checked` | A boolean bind on an input. |
12612
+ | `bind:open` | The same, on a `<details>`. |
12613
+ | `bind:group` | Radio or checkbox membership. |
12614
+ | `bind:value={{get, set}}` | Two-way bind over an explicit accessor pair, on an element or a component. |
12615
+ | `bind:element={Reactive<Element> \| ((element: Element) => void \| Disposer)}` | A node reference. |
12616
+ | `class:name={cond}` | Toggles a class. |
12617
+ | `style:prop={value}` | Sets one style property. |
12618
+ | `{...expr}` | Spreads props or attributes. |
12619
+ | static template text | What an author writes outside a slot, in body text and in an attribute value. |
12425
12620
 
12426
12621
  Static text is markup. An author-written `&amp;` or `&copy;` renders the same in body text and in an attribute value, and only an interpolated value is escaped — the one exception being a URL-typed attribute, where the encoding is what stops an entity forming a scheme.
12427
12622
 
@@ -12466,28 +12661,27 @@ Static text is markup. An author-written `&amp;` or `&copy;` renders the same in
12466
12661
  | --- | --- |
12467
12662
  | `<script>` | Per-instance component setup. |
12468
12663
  | `<script module>` | Module scope. |
12469
- | `<script server>` | Module scope on a server alone, exporting the file's handlers. |
12664
+ | `<script server>` | Module scope on a server alone, declaring its file's private handlers and filing its hooks. |
12470
12665
  | `<style>` | Component-scoped styles. |
12471
12666
  | `import './app.css'` | A stylesheet the component depends on. |
12472
12667
  | `:global(…)` | The per-selector escape from scoping. |
12473
12668
 
12474
- A `.ts` file imports what a `<script module>` or a `<script server>` exports by name, as from any module. TypeScript reads no `.abide`, so abide writes each file's exported types to `.abide/types` — `abide sync` does it alone, the scaffold runs it on `bun install`, every other command and your editor keep it current — and the scaffold's `tsconfig.json` names that directory in `rootDirs` and `paths`.
12669
+ An `export` in any block is a compile error, so what two files share is a module under a `lib`, and a handler two files call is a handler file of its own — a `.ts` that TypeScript reads as it reads any other.
12475
12670
 
12476
12671
  ## Tracking
12477
12672
 
12478
- | branch-local `<script>` | no | Setup, once per item. | 14.8 |
12479
- | --- | --- | --- | --- |
12480
- | `reactive` body, unkeyed | **yes** | Pushes its own subscriber. | 14.3, 14.11, 14.15, 14.16 |
12481
- | `reactive` body, keyed | **no** | Untracked by handler. | 14.4 |
12482
-
12483
- | block body binding | yes | A live read, like a prop. | 14.5 |
12484
- | --- | --- | --- | --- |
12485
- | `watch` effect, bare | **yes** | Pushes its own subscriber. | 14.6, 14.14, 14.16 |
12486
- | `watch` effect, over sources | **no** | The sources decide the reruns. | 12.3, 14.6 |
12487
-
12488
- | event handler | no | Not the flow. | 14.9 |
12489
- | --- | --- | --- | --- |
12490
- | `Transformer`, `Disposer`, middleware, lifecycle hooks | **no** | Not the flow. | 14.10 |
12673
+ | Context | Tracks | Meaning |
12674
+ | --- | --- | --- |
12675
+ | template expression | **yes** | The flow itself. |
12676
+ | branch-local `<script>` | **no** | Setup, once per item. |
12677
+ | `reactive` body, unkeyed | **yes** | Pushes its own subscriber. |
12678
+ | `reactive` body, keyed | **no** | Untracked by handler. |
12679
+ | block body binding | **yes** | A live read, like a prop. |
12680
+ | `watch` effect, bare | **yes** | Pushes its own subscriber. |
12681
+ | `watch` effect, over sources | **no** | The sources decide the reruns. |
12682
+ | component `<script>` setup | **no** | Setup, once per instance. |
12683
+ | event handler | **no** | Not the flow. |
12684
+ | `Transformer`, `Disposer`, middleware, lifecycle hooks | **no** | Not the flow. |
12491
12685
 
12492
12686
  A context that tracks reruns when what it read changes. A context that does not is one where a read is a read — an event handler runs because something happened, and a setup body runs once — so nothing is arranged by accident and the arrangement you want is `reactive` or `watch`.
12493
12687
 
@@ -12497,8 +12691,14 @@ A context that tracks reruns when what it read changes. A context that does not
12497
12691
 
12498
12692
  A run of whitespace carrying newlines but no space or tab is **dropped wherever it stands**, and a run that renders nothing but carries a newline and indentation is dropped at both ends of every block body and of the top-level template. So indentation in a template is layout for the reader rather than text in the document.
12499
12693
 
12694
+ ### Where a `.abide` lives
12695
+
12696
+ A `.abide` directly in a directory under no fence is a page at its path, save a `layout.abide`, an `error.abide` and the root's `launcher.abide` (23.21). A component is a `.abide` under a `lib`, imported by path, and a test under a `tests` directory imports one the same way once `@abide/abide/test` is preloaded (38.109). The [Routing](/reference/routing.md) reference has the whole map.
12697
+
12500
12698
  ### What the compiler refuses
12501
12699
 
12700
+ - An `export` in any block, however the language spells it.
12701
+ - A `<script server>` in a file under a `server` or a `public` directory.
12502
12702
  - A read spelled `foo()` with an argument.
12503
12703
  - `props()` from a `<script module>` block.
12504
12704
  - A prop passed to a component that never calls `props()`.
@@ -12510,7 +12710,7 @@ Each is caught before the code runs, which is why the ergonomic form does not ha
12510
12710
 
12511
12711
  ### A component that owns a value
12512
12712
 
12513
- ```abide src/ui/Handle.abide
12713
+ ```abide src/lib/Handle.abide
12514
12714
  <script>
12515
12715
  import { reactive } from '@abide/abide'
12516
12716
 
@@ -12523,7 +12723,7 @@ let handle = reactive('ada')
12523
12723
 
12524
12724
  ### A component with props and children
12525
12725
 
12526
- ```abide src/ui/Panel.abide
12726
+ ```abide src/lib/Panel.abide
12527
12727
  <script>
12528
12728
  import { props } from '@abide/abide'
12529
12729
 
@@ -12547,7 +12747,7 @@ const { title } = props<{ title: string }>()
12547
12747
 
12548
12748
  *File patterns, precedence, head merging and render.*
12549
12749
 
12550
- Every address an abide app answers on comes from a **file path**. A page is a `page.abide`, and a handler is an export of a `<script server>` in any `.abide`, both answering at the path of their directory under `src`. So there is no route table, and nothing to keep in step with the filesystem.
12750
+ Every address an abide app answers on comes from a **file path**. A page is a `.abide` and a handler is a `.ts`, each directly in a directory under `src` and answering at its own path. So there is no route table, and nothing to keep in step with the filesystem.
12551
12751
 
12552
12752
  ## Page patterns
12553
12753
 
@@ -12564,19 +12764,18 @@ A file or directory named `[id]` matches exactly one segment and is a required k
12564
12764
 
12565
12765
  ## Mount paths
12566
12766
 
12567
- | File | Export | Served at |
12767
+ | File | Declares | Served at |
12568
12768
  | --- | --- | --- |
12569
- | `users/page.abide` | `getUser` | `/users/getUser` |
12570
- | `layout.abide` | `health` | `/health` |
12571
- | `admin/page.abide` | `recent` | `/admin/recent` |
12572
- | `media/[id]/page.abide` | `save` | `/media/[id]/save`, `id` reaching its `Args` |
12573
- | `media/[id]/comments.abide` | `addComment` | `/media/[id]/addComment` |
12769
+ | `users/getUser.ts` | `export default GET(…)` | `/users/getUser` |
12770
+ | `health.ts` | `export default GET(…)` | `/health` |
12771
+ | `media/[id]/save.ts` | `export default POST(…)` | `/media/[id]/save`, `id` reaching its `Args` |
12772
+ | `media/[id]/lib/Comments.abide` | `const addComment = POST(…)` in its `<script server>` | `/__abide/handlers/media/id/lib/Comments/addComment` |
12574
12773
 
12575
- A handler answers at its file's DIRECTORY, then its export's name — the file's own name is never part of it, so a directory's handlers sit in as many files as you like.
12774
+ A handler file answers at its PATH, and declares one handler as its default export, exporting nothing else. A handler a `<script server>` declares is its file's alone: it answers below `/__abide/handlers`, takes nothing from the path, is listed on no other surface, and runs under the rungs filed where its file's hooks are.
12576
12775
 
12577
12776
  A parameter in the route reaches the handler's args from the path, whatever the method, so the handler's `Args` has to declare it — one it does not is a build error naming the parameter. A request that names it in the query or the body as well is refused `400`.
12578
12777
 
12579
- A route answers each method **once**. A page answers `GET` at its own route, so a `GET` handler beside it is a build error naming both files, and a `POST` beside it is the form the page posts to. Two routes that differ only in a parameter's name are one route.
12778
+ A route answers each method **once**. A page answers `GET` at its own route, so a `GET` handler file at that route is a build error naming both files, and a `POST` there is the form the page posts to. Two routes that differ only in a parameter's name are one route.
12580
12779
 
12581
12780
  ## `render`
12582
12781
 
@@ -12608,12 +12807,34 @@ The batching unit of a fill is the **flush**, not the production: a value that p
12608
12807
 
12609
12808
  ## Description
12610
12809
 
12810
+ ### From file to route
12811
+
12812
+ ```
12813
+ src/
12814
+ index.abide GET /
12815
+ layout.abide around every page below /
12816
+ crm/
12817
+ index.abide GET /crm
12818
+ [id].abide GET /crm/[id]
12819
+ getInvoice.ts GET /crm/getInvoice
12820
+ layout.abide around every page and handler below /crm
12821
+ lib/ no route: components and modules
12822
+ Notes.abide its handlers below /__abide/handlers/crm/lib/Notes
12823
+ tests/ no route: tests
12824
+ server/ no route: modules the browser never loads
12825
+ public/
12826
+ robots.txt the file, at /robots.txt
12827
+ ```
12828
+
12829
+ A route is its file's path without the extension, and an `index.abide`, a `layout.abide` and an `error.abide` take their directory's (23.14). Every other `.abide` directly in a directory is a page (23.21), so a component lives under a `lib`. A `.ts` there is a handler file.
12830
+
12831
+ `lib`, `tests`, `server` and `public` are the fences, and nothing under one has a route (23.15). One inside another is a build error, save a `server` inside a `lib` or a `tests`, which keeps code only the server loads beside the components that call it — and so is an `index.abide`, a `layout.abide` or an `error.abide` under one (23.16, 23.18). A directory's top level holds pages, layouts, error pages, handler files, directories and fences alone — any other file there is a build error naming the `lib` to move it into (23.22). So listing a directory lists its routes, and `abide routes` lists the app's.
12832
+
12611
12833
  ### Resolution order
12612
12834
 
12613
12835
  A request goes to the most specific route **answering its method**, a literal segment beating a dynamic one at the same depth — so `POST
12614
- /media/new` reaches the `new` handler a `<script
12615
- server>` in `media/` exports, while `GET
12616
- /media/new` falls through to the page at `media/[id]/page.abide`. `layout.abide` and `error.abide` are both resolved **nearest-ancestor by route**, which is what lets a section answer for itself and the app answer for the rest.
12836
+ /media/new` reaches the handler file `media/new.ts`, while `GET
12837
+ /media/new` falls through to the page at `media/[id]/index.abide`. `layout.abide` and `error.abide` are both resolved **nearest-ancestor by route**, which is what lets a section answer for itself and the app answer for the rest.
12617
12838
 
12618
12839
  ### The head
12619
12840
 
@@ -12623,7 +12844,7 @@ A request goes to the most specific route **answering its method**, a literal se
12623
12844
 
12624
12845
  ### A route with a required and an optional segment
12625
12846
 
12626
- ```abide src/crm/invoices/[id]/[[tab]]/page.abide — excerpt
12847
+ ```abide src/crm/invoices/[id]/[[tab]]/index.abide — excerpt
12627
12848
  <script>
12628
12849
  import { route } from '@abide/abide'
12629
12850
 
@@ -12635,8 +12856,8 @@ const { id, tab = 'summary' } = route.params
12635
12856
 
12636
12857
  ### A document rendered outside the page pipeline
12637
12858
 
12638
- ```abide src/pages/render-a-document-yourself/InvoicePreview.abide — excerpt
12639
- export const previewInvoice = GET(
12859
+ ```abide src/pages/render-a-document-yourself/lib/InvoicePreview.abide — excerpt
12860
+ const previewInvoice = GET(
12640
12861
  async ({ id }: { id: string }) => {
12641
12862
  const invoice = await findInvoice(id)
12642
12863
  return page(await render(Document({ invoice })))
@@ -12648,6 +12869,7 @@ export const previewInvoice = GET(
12648
12869
  ## See also
12649
12870
 
12650
12871
  - [Routes](/pages/add-a-page.md) — the guide to page files
12872
+ - [Handler URLs](/data/find-the-url-a-handler-answers-on.md) — every address a handler answers on, and `abide routes`
12651
12873
  - [Manual rendering](/pages/render-a-document-yourself.md) — `render` in use
12652
12874
  - [Streaming HTML](/pages/send-the-page-before-the-data-lands.md) — what a sink is for
12653
12875
 
@@ -12659,7 +12881,7 @@ export const previewInvoice = GET(
12659
12881
 
12660
12882
  *Environment variables, files and lifecycle hooks.*
12661
12883
 
12662
- An abide app is configured by three things: the **environment** it starts in, the **files** it finds under `src/`, and the **hooks** its `<script server>` blocks register. There is no config file format, because a typed `config()` read is what an app actually wants and an environment variable is what a deployment actually has.
12884
+ An abide app is configured by three things: the **environment** it starts in, the **files** it finds under `src/`, and the **hooks** its server blocks and handler files call. There is no config file format, because a typed `config()` read is what an app actually wants and an environment variable is what a deployment actually has.
12663
12885
 
12664
12886
  ## Environment variables
12665
12887
 
@@ -12694,23 +12916,28 @@ The `ABIDE_`-prefixed ceilings default to no ceiling. That is the honest default
12694
12916
  | --- | --- |
12695
12917
  | `src/**` | The app's source. |
12696
12918
  | `**/server/**` | Source the browser never loads. |
12919
+ | `**/lib/**` | The app's code that is no address: components and modules, on both sides. |
12920
+ | `**/tests/**` | The app's tests. |
12697
12921
  | `**/public/**` | The files answered as authored, at their path below that directory. |
12698
12922
  | `src/public/favicon.ico` | The file answered at `/favicon.ico`. |
12699
12923
  | `src/public/robots.txt` | The file answered at `/robots.txt`. |
12700
12924
  | `src/public/icon.png` · `src/public/icon.icns` | The files answered at `/icon.png` and `/icon.icns`, and the desktop app's icon. |
12701
- | `src/**/` | The app's addresses, each at its route. |
12702
- | `**/page.abide` | A route: its page, and the handlers its `<script server>` exports. |
12925
+ | `src/**/` | A directory of the app's, a segment of the route of every file directly in it. |
12926
+ | `**/index.abide` | The page at its directory's route. |
12927
+ | `**/<name>.abide` | The page at its directory's route and then `<name>`. |
12928
+ | `**/<name>.ts` | A handler file: the handler its default export declares, at its directory's route and then `<name>`. |
12703
12929
  | `**/layout.abide` | A layout, rendering every page below its route through a slot, and the hooks over that route. |
12704
- | `src/launcher.abide` | The desktop launcher, beside the root layout: the `launcher` options its `<script module>` exports, and its page in place of the launcher's own where it has markup. |
12930
+ | `src/launcher.abide` | The desktop launcher, beside the root layout: the `onLauncher` it calls, and its page in place of the launcher's own where it has markup. |
12705
12931
  | `src/layout.abide` | The root layout, whose `<script server>` is the app's own, and whose `<html>` is the document its pages are served in. |
12706
12932
  | `**/error.abide` | The page a refusal renders in. |
12707
12933
  | `**/node_modules/**/*.abide` | A component a dependency ships as source, compiled with the app that imports it. |
12708
- | `import '…' with { type: 'routes' }` | A package's directory mounted at the importing file's directory, its pages, layouts, error pages and handlers answering below it. |
12934
+ | `import '…' with { type: 'routes' }` | A package's directory mounted at the importing file's directory, its pages, layouts, error pages and handler files answering below it. |
12709
12935
  | `bunfig.toml` | Under `[serve.static] plugins`, the bundler plugins the client is built with. |
12936
+ | `preload = ["@abide/abide/test"]` | The `[test]` preload a test imports the app's `.abide` files through. |
12710
12937
 
12711
- Every directory under `src` is a segment of an address, so `src/billing/page.abide` is `/billing`. A `public` or `server` directory can sit anywhere under it and is never a segment: nothing under one has an address, a page there is a build error, and one inside the other is a build error too. A `public` directory's files answer below the path of the directories above it. Everything outside a `server` directory is both sides. `#src/*` is the one import alias, mapping onto `src/`.
12938
+ Every directory under `src` is a segment of an address, so `src/billing/index.abide` is `/billing`. A `server`, `public`, `lib` or `tests` directory can sit anywhere under it and is never a segment: nothing under one has an address, an `index.abide`, a `layout.abide` or an `error.abide` there is a build error, and one inside another is a build error too. Any other directory holds pages, layouts, error pages, handler files and directories alone, and a file of any other kind directly in it is a build error naming the `lib` to move it into. A `public` directory's files answer below the path of the directories above it. Everything outside a `server` directory is both sides. `#src/*` is the one import alias, mapping onto `src/`.
12712
12939
 
12713
- `page.abide`, `layout.abide` and `error.abide` are **addresses rather than names** — the framework's own conventions, where every other file in an app is named after what it exports.
12940
+ A page and a handler file are **addresses rather than names**, and so are `index.abide`, `layout.abide` and `error.abide` — the framework's own conventions. A file under a `lib` is named after what it exports.
12714
12941
 
12715
12942
  ## Lifecycle hooks
12716
12943
 
@@ -12723,8 +12950,11 @@ Every directory under `src` is a segment of an address, so `src/billing/page.abi
12723
12950
  | `onConfig` | `(fn: ConfigDefaults \| null, options?: ConfigOptions) => () => void` | The app's config defaults. |
12724
12951
  | `onHealth` | `(report: () => unknown \| Promise<unknown>) => () => void` | The app's health reporter. |
12725
12952
  | `onPrincipal` | `(resolve: (claims: unknown) => unknown \| Promise<unknown>) => () => void` | Turns claims into the app's half of a principal. |
12953
+ | `onLauncher` | `(options: () => LauncherOptions) => () => void` | The window the desktop app opens and the forms its launcher offers. |
12726
12954
 
12727
- Each is a hook you **call** from a `<script server>` rather than an export the framework looks for, and it is filed at the route of the file that called it. `middleware` runs over that route and the ones below it — before routing at the root, after it anywhere else — and `onError` hears an error there, nearest route first. The rest are the process's wherever they are written, and every one of a kind composes: `onConfig` and `onPrincipal` merge what each answers, the deeper route's over the root's.
12955
+ Each is a hook you **call** rather than an export the framework looks for, a `.abide` exporting nothing. It is filed at the route of the `.abide` or the handler file that called it (37.1), and any other module calling one is a build error (37.13) — export the function and call the hook where it serves. A `.abide` under a `lib` or a `tests` directory files its hooks at the route of the directory holding that one. `onLauncher` is called in `src/launcher.abide` alone.
12956
+
12957
+ `middleware` runs over that route and the ones below it — before routing at the root, after it anywhere else — and `onError` hears an error there, nearest route first. The rest are the process's wherever they are written, and every one of a kind composes: `onConfig` and `onPrincipal` merge what each answers, the deeper route's over the root's. `abide routes` lists every call with the route it is filed at.
12728
12958
 
12729
12959
  Every one hands back the way off again, so a module that is torn down can remove its own.
12730
12960
 
@@ -12738,6 +12968,10 @@ Every one hands back the way off again, so a module that is torn down can remove
12738
12968
 
12739
12969
  `config()` is ambient, and the same name on both sides. On a server it reads the process environment merged over the app's declared defaults; in a browser it reads what the document was served with, which is the subset an app marked as public. A secret is not in that subset, and that is a property of the declaration rather than of a naming convention.
12740
12970
 
12971
+ ### `Config` is typed from the `onConfig` calls
12972
+
12973
+ `abide check` and `abide lsp` type `Config` from the defaults every `onConfig` returns, read where it is called as a statement at the top of a `<script server>` or of a handler file (27.13). A default that returns a union types the keys every member of it has. So a config read is typed without a declaration of its own, wherever in the app the default is written.
12974
+
12741
12975
  ### `NODE_ENV` is verbatim
12742
12976
 
12743
12977
  It is the environment name as given, not a boolean and not a three-way enum. An app comparing it against `'production'` is comparing against the string it was started with.
@@ -12746,17 +12980,17 @@ It is the environment name as given, not a boolean and not a three-way enum. An
12746
12980
 
12747
12981
  ### Declaring the app's own config
12748
12982
 
12749
- ```abide src/app/configure-the-app/Retries.abide — excerpt
12983
+ ```abide src/app/configure-the-app/lib/Retries.abide — excerpt
12750
12984
  onConfig(() => ({ RETRIES: 3 }))
12751
12985
  …
12752
- export const retryPolicy = GET(() => ({ retries: config().RETRIES }), {
12986
+ const retryPolicy = GET(() => ({ retries: config().RETRIES }), {
12753
12987
  description: 'How many times a call is retried.',
12754
12988
  })
12755
12989
  ```
12756
12990
 
12757
12991
  ### Running work around the boot
12758
12992
 
12759
- ```abide src/app/run-code-at-start-and-stop/Pool.abide — excerpt
12993
+ ```abide src/app/run-code-at-start-and-stop/lib/Pool.abide — excerpt
12760
12994
  onStart(async (start) => {
12761
12995
  await pool.connect()
12762
12996
  await start()
@@ -12816,7 +13050,7 @@ abide --help
12816
13050
  | `abide scaffold <name>` | Writes a starter project. |
12817
13051
  | `abide run <file> [args…]` | Runs a script under the abide runtime. |
12818
13052
  | `abide check [dir…]` | Type-checks `.abide`, on the `.abide` line. |
12819
- | `abide sync` | Writes the types a `.ts` reads a `.abide` by. |
13053
+ | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. |
12820
13054
  | `abide dev [--port <n>]` | Watches the project and keeps the app up. |
12821
13055
  | `abide build` | Builds the client into content-hashed chunks and a manifest. |
12822
13056
  | `abide start [--port <n>]` | Boots the built app. |