@abide/abide 0.117.2 → 0.119.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 (156) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/abide.ts +74 -11
  3. package/docs/REGISTRY.md +18 -15
  4. package/docs/RULEBOOK.md +71 -10
  5. package/docs/abide.md +584 -274
  6. package/package.json +1 -1
  7. package/scaffold/AGENTS.md +1 -1
  8. package/scaffold/src/getGreeting.test.ts +2 -1
  9. package/scaffold/src/index.abide +5 -2
  10. package/scaffold/src/index.test.ts +2 -0
  11. package/scaffold/src/layout.abide +6 -6
  12. package/src/compiler/Lowering.ts +20 -2
  13. package/src/compiler/PositionTable.ts +1 -0
  14. package/src/compiler/Refusals.ts +1 -0
  15. package/src/compiler/TypeSession.ts +49 -13
  16. package/src/compiler/aliasedSymbol.ts +1 -1
  17. package/src/compiler/ambientReadsIn.ts +1 -0
  18. package/src/compiler/appLayout.ts +55 -3
  19. package/src/compiler/appMap.ts +15 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -0
  21. package/src/compiler/argsSchemaOf.ts +1 -0
  22. package/src/compiler/checkApp.ts +3 -1
  23. package/src/compiler/checkedShadow.ts +2 -1
  24. package/src/compiler/classifyRead.ts +1 -0
  25. package/src/compiler/classifyTemplate.ts +1 -0
  26. package/src/compiler/compileApp.ts +5 -3
  27. package/src/compiler/compiledEntry.ts +2 -1
  28. package/src/compiler/componentGraph.ts +1 -0
  29. package/src/compiler/declaredExports.ts +4 -2
  30. package/src/compiler/emitClient.ts +19 -14
  31. package/src/compiler/emitComponent.ts +1 -0
  32. package/src/compiler/emitSource.ts +2 -1
  33. package/src/compiler/formatAbide.ts +857 -0
  34. package/src/compiler/formatDiagnosis.ts +1 -0
  35. package/src/compiler/handlersUnder.ts +7 -3
  36. package/src/compiler/hookCalls.ts +1 -0
  37. package/src/compiler/lineOf.ts +17 -0
  38. package/src/compiler/matchersOf.ts +157 -0
  39. package/src/compiler/mountsOf.ts +5 -0
  40. package/src/compiler/paramFile.ts +125 -0
  41. package/src/compiler/parseAbide.ts +48 -28
  42. package/src/compiler/projectShadow.ts +11 -4
  43. package/src/compiler/reactiveOptions.ts +1 -0
  44. package/src/compiler/reactiveTypes.ts +2 -1
  45. package/src/compiler/readsIn.ts +3 -2
  46. package/src/compiler/renderMarkup.ts +3 -2
  47. package/src/compiler/resolverIn.ts +2 -0
  48. package/src/compiler/sameTemplate.ts +150 -0
  49. package/src/compiler/scaffold.ts +4 -11
  50. package/src/compiler/serveLanguage.ts +48 -21
  51. package/src/compiler/sharedModuleWrites.ts +2 -1
  52. package/src/compiler/sourceMapOf.ts +1 -0
  53. package/src/compiler/splitImports.ts +2 -0
  54. package/src/compiler/templateWarnings.ts +2 -2
  55. package/src/compiler/trackingWarnings.ts +1 -0
  56. package/src/compiler/writeDefaultProject.ts +5 -1
  57. package/src/server/BRIDGE_VERSION.ts +1 -1
  58. package/src/server/abideModules.ts +2 -0
  59. package/src/server/answerFailure.ts +1 -0
  60. package/src/server/answerRequest.ts +57 -19
  61. package/src/server/answerRpc.ts +6 -2
  62. package/src/server/answerSocket.ts +2 -1
  63. package/src/server/answerValue.ts +1 -0
  64. package/src/server/answerWorker.ts +1 -0
  65. package/src/server/appTree.ts +18 -9
  66. package/src/server/argsFor.ts +1 -0
  67. package/src/server/assistant.ts +2 -1
  68. package/src/server/banner.ts +2 -0
  69. package/src/server/bridgeRelay.ts +1 -0
  70. package/src/server/callerOf.ts +1 -0
  71. package/src/server/claudeApi.ts +1 -0
  72. package/src/server/claudeCode.ts +1 -0
  73. package/src/server/clientBundle.ts +1 -0
  74. package/src/server/coerceArgs.ts +1 -2
  75. package/src/server/commands.ts +9 -0
  76. package/src/server/compressAnswer.ts +2 -1
  77. package/src/server/config.ts +2 -0
  78. package/src/server/createApp.ts +4 -2
  79. package/src/server/crossingOrigin.ts +1 -0
  80. package/src/server/csp.ts +1 -0
  81. package/src/server/deferredBuilds.ts +1 -0
  82. package/src/server/derivedArgs.ts +1 -0
  83. package/src/server/devWorker.ts +2 -1
  84. package/src/server/faceOf.ts +2 -7
  85. package/src/server/generatedSurfaces.ts +79 -7
  86. package/src/server/hooks.ts +1 -0
  87. package/src/server/iconAccent.ts +1 -1
  88. package/src/server/installServer.ts +1 -0
  89. package/src/server/installSourceMaps.ts +1 -0
  90. package/src/server/launcherPage.ts +2 -2
  91. package/src/server/manifestOf.ts +1 -0
  92. package/src/server/muxUpgrade.ts +1 -0
  93. package/src/server/nativeMenu.ts +2 -0
  94. package/src/server/placement.ts +12 -2
  95. package/src/server/principal.ts +2 -1
  96. package/src/server/remote.ts +7 -2
  97. package/src/server/render.ts +3 -3
  98. package/src/server/renderComponent.ts +1 -0
  99. package/src/server/reportHealth.ts +1 -0
  100. package/src/server/requestScope.ts +3 -1
  101. package/src/server/runDesktop.ts +3 -2
  102. package/src/server/seedBuffer.ts +1 -0
  103. package/src/server/serveDev.ts +3 -2
  104. package/src/server/shell.ts +2 -0
  105. package/src/server/streamEvent.ts +1 -0
  106. package/src/server/surfaceOf.ts +6 -1
  107. package/src/server/testBuild.ts +1 -0
  108. package/src/server/testMount.ts +1 -0
  109. package/src/server/trace.ts +2 -0
  110. package/src/server/withAbideSources.ts +1 -0
  111. package/src/shared/Attributes.ts +12 -8
  112. package/src/shared/cookies.ts +1 -1
  113. package/src/shared/escapeText.ts +1 -1
  114. package/src/shared/filledPath.ts +16 -10
  115. package/src/shared/index.ts +1 -5
  116. package/src/shared/jsonSchema.ts +1 -3
  117. package/src/shared/matchRoute.ts +16 -4
  118. package/src/shared/menuBridge.ts +1 -2
  119. package/src/shared/outbox.ts +1 -1
  120. package/src/shared/reactive/Keyed.ts +2 -2
  121. package/src/shared/reactive/Reactive.ts +12 -16
  122. package/src/shared/reactive/index.ts +3 -4
  123. package/src/shared/reactive/isReactive.ts +1 -1
  124. package/src/shared/reactive/scoped.ts +2 -2
  125. package/src/shared/reactive/share.ts +1 -1
  126. package/src/shared/reactive/watch.ts +4 -4
  127. package/src/shared/refuse.ts +1 -1
  128. package/src/shared/routePattern.ts +66 -14
  129. package/src/shared/rpc.ts +8 -11
  130. package/src/shared/url.ts +24 -7
  131. package/src/shared/validateJson.ts +1 -1
  132. package/src/ui/ArmRegion.ts +1 -0
  133. package/src/ui/Level.ts +1 -0
  134. package/src/ui/List.ts +1 -0
  135. package/src/ui/RawRun.ts +1 -0
  136. package/src/ui/Router.ts +1 -0
  137. package/src/ui/TryRegion.ts +1 -0
  138. package/src/ui/adoptWalk.ts +2 -1
  139. package/src/ui/bindElement.ts +1 -0
  140. package/src/ui/bindFiles.ts +1 -0
  141. package/src/ui/bindGroup.ts +1 -0
  142. package/src/ui/bindProperty.ts +1 -0
  143. package/src/ui/bindSpread.ts +1 -0
  144. package/src/ui/index.ts +1 -6
  145. package/src/ui/outboxQueue.ts +1 -1
  146. package/src/ui/parseMarkup.ts +2 -1
  147. package/src/ui/reconcile.ts +2 -1
  148. package/src/ui/reconcileHead.ts +1 -0
  149. package/src/ui/regions.ts +2 -1
  150. package/src/ui/registerWorker.ts +1 -0
  151. package/src/ui/rows.ts +1 -0
  152. package/src/ui/setMarkup.ts +1 -0
  153. package/src/ui/setText.ts +1 -0
  154. package/src/ui/socketWire.ts +4 -3
  155. package/src/ui/wire.ts +2 -1
  156. /package/scaffold/src/{getGreeting.ts → getGreeting.handler.ts} +0 -0
package/docs/abide.md CHANGED
@@ -310,7 +310,7 @@ A file's path below `src` is its address. Nothing else names a route.
310
310
  | --- | --- |
311
311
  | `src/index.abide` | `/` |
312
312
  | `src/contacts/index.abide` | `/contacts` |
313
- | `src/contacts/listContacts.ts` | `GET /contacts/listContacts`, the handler its default export declares |
313
+ | `src/contacts/listContacts.handler.ts` | `GET /contacts/listContacts`, the handler its default export declares |
314
314
  | `src/contacts/lib/ContactList.abide` | no address: a component the pages import |
315
315
  | `src/contacts/lib/ContactList.test.ts` | no address: what `bun test` runs |
316
316
 
@@ -335,7 +335,8 @@ Any other file beside the pages fails the build, so listing a directory lists it
335
335
  ```abide src/contacts/lib/ContactList.abide
336
336
  <script>
337
337
  import { reactive } from '@abide/abide'
338
- import listContacts from '../listContacts.ts'
338
+
339
+ import listContacts from '../listContacts.handler.ts'
339
340
 
340
341
  const contacts = reactive(() => listContacts())
341
342
  </script>
@@ -347,8 +348,9 @@ const contacts = reactive(() => listContacts())
347
348
  </ul>
348
349
  ```
349
350
 
350
- ```ts src/contacts/listContacts.ts
351
+ ```ts src/contacts/listContacts.handler.ts
351
352
  import { GET } from '@abide/abide/server'
353
+
352
354
  import { CONTACTS } from './lib/CONTACTS.server.ts'
353
355
 
354
356
  export default GET(async () => {
@@ -360,7 +362,8 @@ export default GET(async () => {
360
362
 
361
363
  ```ts src/contacts/lib/ContactList.test.ts
362
364
  import { expect, test } from 'bun:test'
363
- import listContacts from '../listContacts.ts'
365
+
366
+ import listContacts from '../listContacts.handler.ts'
364
367
 
365
368
  test('the handler the list reads answers with every contact', async () => {
366
369
  const contacts = await listContacts()
@@ -372,7 +375,7 @@ test('the handler the list reads answers with every contact', async () => {
372
375
  })
373
376
  ```
374
377
 
375
- `listContacts.ts` runs on the server. The list imports it like any module, and the browser is sent a call under the same name in its place, so there is no `fetch`, no route to register, and no second copy of the contact type.
378
+ `listContacts.handler.ts` runs on the server. The list imports it like any module, and the browser is sent a call under the same name in its place, so there is no `fetch`, no route to register, and no second copy of the contact type.
376
379
 
377
380
  The test imports the same handler and calls it, in process. A handler only one page calls can live in that page's `<script server>` instead. See [Talk to the server](/guide/talk-to-the-server.md).
378
381
 
@@ -383,7 +386,8 @@ The test imports the same handler and calls it, in process. A handler only one p
383
386
  ```abide src/contacts/lib/PageTiming.abide
384
387
  <script>
385
388
  import { reactive } from '@abide/abide'
386
- import listContacts from '../listContacts.ts'
389
+
390
+ import listContacts from '../listContacts.handler.ts'
387
391
 
388
392
  let pageComplete = reactive('—')
389
393
  let contactsAnswered = reactive('—')
@@ -411,6 +415,7 @@ async function requestPage() {
411
415
 
412
416
  ```ts src/contacts/lib/PageTiming.test.ts
413
417
  import { expect, test } from 'bun:test'
418
+
414
419
  import { createApp } from '@abide/abide/server'
415
420
 
416
421
  test('the contacts page is complete before the contacts land', async () => {
@@ -450,8 +455,10 @@ let starred = reactive(false)
450
455
 
451
456
  ```ts src/contacts/lib/StarButton.test.ts
452
457
  import { expect, test } from 'bun:test'
458
+
453
459
  import { flushSync } from '@abide/abide'
454
460
  import { mount } from '@abide/harness/abide'
461
+
455
462
  import StarButton from './StarButton.abide'
456
463
 
457
464
  test('the click writes starred and the button follows', async () => {
@@ -473,7 +480,8 @@ Naming `starred` in the markup reads it and follows it from then on. Assigning t
473
480
  ```abide src/contacts/lib/ContactNote.abide
474
481
  <script>
475
482
  import { reactive } from '@abide/abide'
476
- import listContacts from '../listContacts.ts'
483
+
484
+ import listContacts from '../listContacts.handler.ts'
477
485
 
478
486
  // Owned, derived, loaded, and waiting for its first write.
479
487
  let draft = reactive('')
@@ -502,8 +510,10 @@ let saved = reactive<string>()
502
510
 
503
511
  ```ts src/contacts/lib/ContactNote.test.ts
504
512
  import { expect, test } from 'bun:test'
513
+
505
514
  import { flushSync } from '@abide/abide'
506
515
  import { mount } from '@abide/harness/abide'
516
+
507
517
  import ContactNote from './ContactNote.abide'
508
518
 
509
519
  const savedNote = () => document.querySelectorAll('strong')[3]?.textContent
@@ -574,8 +584,10 @@ let email = reactive('ada@example.com')
574
584
 
575
585
  ```ts src/profile/lib/ContactFields.test.ts
576
586
  import { expect, test } from 'bun:test'
587
+
577
588
  import { flushSync } from '@abide/abide'
578
589
  import { mount } from '@abide/harness/abide'
590
+
579
591
  import ContactFields from './ContactFields.abide'
580
592
 
581
593
  test('typing writes the value, and the value fills the field', async () => {
@@ -638,8 +650,10 @@ let phone = reactive('', {
638
650
 
639
651
  ```ts src/profile/lib/PhoneField.test.ts
640
652
  import { expect, test } from 'bun:test'
653
+
641
654
  import { flushSync } from '@abide/abide'
642
655
  import { mount } from '@abide/harness/abide'
656
+
643
657
  import PhoneField from './PhoneField.abide'
644
658
 
645
659
  test('the record keeps the digits, and the field shows them back', async () => {
@@ -687,8 +701,10 @@ const filled = reactive(() => [name, phone].filter(Boolean).length)
687
701
 
688
702
  ```ts src/profile/lib/ProfileSummary.test.ts
689
703
  import { expect, test } from 'bun:test'
704
+
690
705
  import { flushSync } from '@abide/abide'
691
706
  import { measure, mount } from '@abide/harness/abide'
707
+
692
708
  import ProfileSummary from './ProfileSummary.abide'
693
709
 
694
710
  const shown = () =>
@@ -747,8 +763,10 @@ let contact = reactive({
747
763
 
748
764
  ```ts src/profile/lib/AddressField.test.ts
749
765
  import { expect, test } from 'bun:test'
766
+
750
767
  import { flushSync } from '@abide/abide'
751
768
  import { mount } from '@abide/harness/abide'
769
+
752
770
  import AddressField from './AddressField.abide'
753
771
 
754
772
  test('a write to the city reaches the record it sits in', async () => {
@@ -809,8 +827,10 @@ let tags = reactive(['investor', 'speaker', 'mentor'])
809
827
 
810
828
  ```ts src/profile/lib/Tags.test.ts
811
829
  import { expect, test } from 'bun:test'
830
+
812
831
  import { flushSync } from '@abide/abide'
813
832
  import { measure, mount } from '@abide/harness/abide'
833
+
814
834
  import Tags from './Tags.abide'
815
835
 
816
836
  test('removing a tag removes its row and rewrites no other', async () => {
@@ -852,8 +872,10 @@ let followUp = reactive(false)
852
872
 
853
873
  ```ts src/profile/lib/FollowUp.test.ts
854
874
  import { expect, test } from 'bun:test'
875
+
855
876
  import { flushSync } from '@abide/abide'
856
877
  import { mount } from '@abide/harness/abide'
878
+
857
879
  import FollowUp from './FollowUp.abide'
858
880
 
859
881
  const dateField = () => document.querySelector('input[type="date"]')
@@ -882,6 +904,7 @@ test('the date field is in the page only while the box is ticked', async () => {
882
904
  ```abide src/profile/lib/ContactCard.abide
883
905
  <script>
884
906
  import { reactive } from '@abide/abide'
907
+
885
908
  import Avatar from './Avatar.abide'
886
909
 
887
910
  let name = reactive('Grace Hopper')
@@ -901,14 +924,19 @@ const { name } = props<{ name: string }>()
901
924
  </script>
902
925
 
903
926
  <span class="rounded-full border border-rule bg-raised px-2 py-1 font-semibold">
904
- {name.split(' ').map((word) => word.charAt(0)).join('')}
927
+ { name
928
+ .split(' ')
929
+ .map((word) => word.charAt(0))
930
+ .join('')}
905
931
  </span>
906
932
  ```
907
933
 
908
934
  ```ts src/profile/lib/ContactCard.test.ts
909
935
  import { expect, test } from 'bun:test'
936
+
910
937
  import { flushSync } from '@abide/abide'
911
938
  import { measure, mount } from '@abide/harness/abide'
939
+
912
940
  import ContactCard from './ContactCard.abide'
913
941
 
914
942
  test('the avatar follows its name prop, and is not rebuilt', async () => {
@@ -976,7 +1004,9 @@ import LeadBadge from './LeadBadge.abide'
976
1004
 
977
1005
  ```ts src/profile/lib/StageBadges.test.ts
978
1006
  import { expect, test } from 'bun:test'
1007
+
979
1008
  import { mount } from '@abide/harness/abide'
1009
+
980
1010
  import StageBadges from './StageBadges.abide'
981
1011
 
982
1012
  // The scope token: an attribute every rule in a `<style>` requires.
@@ -1045,8 +1075,10 @@ let calls = reactive(0)
1045
1075
 
1046
1076
  ```ts src/profile/lib/CallLog.test.ts
1047
1077
  import { expect, test } from 'bun:test'
1078
+
1048
1079
  import { flushSync } from '@abide/abide'
1049
1080
  import { mount } from '@abide/harness/abide'
1081
+
1050
1082
  import CallLog from './CallLog.abide'
1051
1083
 
1052
1084
  const rows = () =>
@@ -1085,15 +1117,15 @@ Every server call in this chapter is a handler: a function wrapped in `GET`, `PO
1085
1117
 
1086
1118
  ## A handler file answers at its own path
1087
1119
 
1088
- A `.ts` file beside the pages is a handler file. Its default export is the handler, and its path below `src` is the address it answers on.
1120
+ A file named `<name>.handler.ts` beside the pages is a handler file. Its default export is the handler, and its path below `src` is the address it answers on.
1089
1121
 
1090
1122
  | File below `src` | Answers on |
1091
1123
  | --- | --- |
1092
- | `**/<name>.ts` | its directory's route, then `<name>` |
1093
- | `deals/getDeal.ts` | `GET /deals/getDeal` |
1094
- | `users/getUser.ts` | `GET /users/getUser` |
1095
- | `health.ts` | `GET /health` |
1096
- | `media/[id]/save.ts` | `POST /media/42/save`, with `id` in its args |
1124
+ | `**/<name>.handler.ts` | its directory's route, then `<name>` |
1125
+ | `deals/getDeal.handler.ts` | `GET /deals/getDeal` |
1126
+ | `users/getUser.handler.ts` | `GET /users/getUser` |
1127
+ | `health.handler.ts` | `GET /health` |
1128
+ | `media/[id]/save.handler.ts` | `POST /media/42/save`, with `id` in its args |
1097
1129
  | `media/[id]/lib/Comments.abide` | `/__abide/handlers/media/id/lib/Comments/addComment`, for the `addComment` its `<script server>` declares |
1098
1130
 
1099
1131
  A handler file exports its handler and nothing else. A page imports it, a script calls its address, and the CLI and MCP offer it as a command and a tool.
@@ -1105,7 +1137,8 @@ A handler file exports its handler and nothing else. A page imports it, a script
1105
1137
  ```abide src/deals/lib/DealDetail.abide
1106
1138
  <script>
1107
1139
  import { reactive } from '@abide/abide'
1108
- import getDeal from '../getDeal.ts'
1140
+
1141
+ import getDeal from '../getDeal.handler.ts'
1109
1142
 
1110
1143
  let id = reactive('d1')
1111
1144
 
@@ -1113,10 +1146,12 @@ const deal = reactive(() => getDeal({ id }))
1113
1146
  </script>
1114
1147
 
1115
1148
  <div class="tabs">
1116
- {#for one of ['d1', 'd2']}
1117
- <button aria-selected={id === one}
1118
- onclick={() => { id = one }}>{one}</button>
1119
- {/for}
1149
+ {#for one of ['d1', 'd2']}
1150
+ <button aria-selected={id === one}
1151
+ onclick={() => {
1152
+ id = one
1153
+ }}>{one}</button>
1154
+ {/for}
1120
1155
  </div>
1121
1156
 
1122
1157
  <ul class="fields stacked">
@@ -1125,9 +1160,10 @@ const deal = reactive(() => getDeal({ id }))
1125
1160
  </ul>
1126
1161
  ```
1127
1162
 
1128
- ```ts src/deals/getDeal.ts
1163
+ ```ts src/deals/getDeal.handler.ts
1129
1164
  import { refuse } from '@abide/abide'
1130
1165
  import { GET } from '@abide/abide/server'
1166
+
1131
1167
  import { DEALS } from './lib/DEALS.server.ts'
1132
1168
 
1133
1169
  /** Answers at /deals/getDeal?id=…; the argument type is the schema. */
@@ -1143,7 +1179,8 @@ export default GET(
1143
1179
 
1144
1180
  ```ts src/deals/lib/DealDetail.test.ts
1145
1181
  import { expect, test } from 'bun:test'
1146
- import getDeal from '../getDeal.ts'
1182
+
1183
+ import getDeal from '../getDeal.handler.ts'
1147
1184
 
1148
1185
  test('the handler answers the deal its args name', async () => {
1149
1186
  const deal = await getDeal({ id: 'd2' })
@@ -1182,14 +1219,15 @@ const notes = reactive(() => getNotes({ id: 'd1' }))
1182
1219
  </script>
1183
1220
 
1184
1221
  <ul class="fields stacked">
1185
- {#for note of notes}
1186
- <li><span>note</span><strong>{note}</strong></li>
1187
- {/for}
1222
+ {#for note of notes}
1223
+ <li><span>note</span><strong>{note}</strong></li>
1224
+ {/for}
1188
1225
  </ul>
1189
1226
  ```
1190
1227
 
1191
1228
  ```ts src/deals/lib/DealNotes.test.ts
1192
1229
  import { expect, test } from 'bun:test'
1230
+
1193
1231
  import { createApp } from '@abide/abide/server'
1194
1232
 
1195
1233
  // The file's path, then the handler's name.
@@ -1226,7 +1264,8 @@ It still answers over http, below `/__abide/handlers` at its file's path and the
1226
1264
  ```abide src/deals/lib/NewDeal.abide
1227
1265
  <script>
1228
1266
  import { type Issues, reactive } from '@abide/abide'
1229
- import createDeal from '../createDeal.ts'
1267
+
1268
+ import createDeal from '../createDeal.handler.ts'
1230
1269
 
1231
1270
  type NewDeal = { name: string; company: string; amount: number }
1232
1271
 
@@ -1255,15 +1294,16 @@ async function add() {
1255
1294
  </div>
1256
1295
 
1257
1296
  <ul class="fields">
1258
- {#for [field, messages] of Object.entries(issues)}
1259
- <li><span>{field}</span><strong>{messages?.[0]}</strong></li>
1260
- {/for}
1297
+ {#for [field, messages] of Object.entries(issues)}
1298
+ <li><span>{field}</span><strong>{messages?.[0]}</strong></li>
1299
+ {/for}
1261
1300
  </ul>
1262
1301
  ```
1263
1302
 
1264
- ```ts src/deals/createDeal.ts
1303
+ ```ts src/deals/createDeal.handler.ts
1265
1304
  import { jsonSchema } from '@abide/abide'
1266
1305
  import { POST } from '@abide/abide/server'
1306
+
1267
1307
  import { DEALS } from './lib/DEALS.server.ts'
1268
1308
 
1269
1309
  type NewDeal = { name: string; company: string; amount: number }
@@ -1297,6 +1337,7 @@ export default POST(
1297
1337
 
1298
1338
  ```ts src/deals/lib/NewDeal.test.ts
1299
1339
  import { expect, test } from 'bun:test'
1340
+
1300
1341
  import { createApp } from '@abide/abide/server'
1301
1342
 
1302
1343
  test('a deal that does not fit is refused, field by field', async () => {
@@ -1341,8 +1382,9 @@ Two presses of Add inside one flight send one request and write one deal, and bo
1341
1382
  ```abide src/deals/lib/DealStage.abide
1342
1383
  <script>
1343
1384
  import { reactive } from '@abide/abide'
1344
- import getDeal from '../getDeal.ts'
1345
- import moveDeal from '../moveDeal.ts'
1385
+
1386
+ import getDeal from '../getDeal.handler.ts'
1387
+ import moveDeal from '../moveDeal.handler.ts'
1346
1388
 
1347
1389
  let id = reactive('d2')
1348
1390
  let refusal = reactive('')
@@ -1362,10 +1404,13 @@ async function move(stage: string) {
1362
1404
  </script>
1363
1405
 
1364
1406
  <div class="tabs">
1365
- {#for one of ['d2', 'd3']}
1366
- <button aria-selected={id === one}
1367
- onclick={() => { id = one; refusal = '' }}>{one}</button>
1368
- {/for}
1407
+ {#for one of ['d2', 'd3']}
1408
+ <button aria-selected={id === one}
1409
+ onclick={() => {
1410
+ id = one
1411
+ refusal = ''
1412
+ }}>{one}</button>
1413
+ {/for}
1369
1414
  </div>
1370
1415
 
1371
1416
  <ul class="fields">
@@ -1373,17 +1418,18 @@ async function move(stage: string) {
1373
1418
  </ul>
1374
1419
 
1375
1420
  <div class="row">
1376
- {#for stage of ['lead', 'qualified', 'proposal']}
1377
- <button onclick={() => move(stage)}>{stage}</button>
1378
- {/for}
1421
+ {#for stage of ['lead', 'qualified', 'proposal']}
1422
+ <button onclick={() => move(stage)}>{stage}</button>
1423
+ {/for}
1379
1424
  </div>
1380
1425
 
1381
1426
  <p class="status">{refusal}</p>
1382
1427
  ```
1383
1428
 
1384
- ```ts src/deals/moveDeal.ts
1429
+ ```ts src/deals/moveDeal.handler.ts
1385
1430
  import { jsonSchema, refuse } from '@abide/abide'
1386
1431
  import { PATCH } from '@abide/abide/server'
1432
+
1387
1433
  import { DEALS } from './lib/DEALS.server.ts'
1388
1434
 
1389
1435
  // A refusal with a name and typed data, which the page can match.
@@ -1409,7 +1455,8 @@ export default PATCH(
1409
1455
 
1410
1456
  ```ts src/deals/lib/DealStage.test.ts
1411
1457
  import { expect, test } from 'bun:test'
1412
- import moveDeal from '../moveDeal.ts'
1458
+
1459
+ import moveDeal from '../moveDeal.handler.ts'
1413
1460
 
1414
1461
  test('a closed deal is refused by name, with its stage as data', async () => {
1415
1462
  try {
@@ -1440,7 +1487,7 @@ Every refusal is a `Failed<Name, Data>`, in process and over the wire alike. The
1440
1487
 
1441
1488
  ```abide src/deals/lib/ExportDeals.abide
1442
1489
  <script>
1443
- import exportDeals from '../exportDeals.ts'
1490
+ import exportDeals from '../exportDeals.handler.ts'
1444
1491
  </script>
1445
1492
 
1446
1493
  <div class="row">
@@ -1448,8 +1495,9 @@ import exportDeals from '../exportDeals.ts'
1448
1495
  </div>
1449
1496
  ```
1450
1497
 
1451
- ```ts src/deals/exportDeals.ts
1498
+ ```ts src/deals/exportDeals.handler.ts
1452
1499
  import { GET } from '@abide/abide/server'
1500
+
1453
1501
  import { DEALS } from './lib/DEALS.server.ts'
1454
1502
 
1455
1503
  /** A Blob answers with its own type: this one is a CSV file. */
@@ -1466,7 +1514,8 @@ export default GET(
1466
1514
 
1467
1515
  ```ts src/deals/lib/ExportDeals.test.ts
1468
1516
  import { expect, test } from 'bun:test'
1469
- import exportDeals from '../exportDeals.ts'
1517
+
1518
+ import exportDeals from '../exportDeals.handler.ts'
1470
1519
 
1471
1520
  test('the export answers a CSV file', async () => {
1472
1521
  const file = await exportDeals()
@@ -1504,11 +1553,11 @@ Every answer carries headers you did not write:
1504
1553
  ```
1505
1554
  abide routes
1506
1555
  GET /deals deals/index.abide
1507
- POST /deals/createDeal deals/createDeal.ts
1508
- GET /deals/exportDeals deals/exportDeals.ts
1509
- GET /deals/getDeal deals/getDeal.ts
1510
- GET /deals/listDeals deals/listDeals.ts
1511
- PATCH /deals/moveDeal deals/moveDeal.ts
1556
+ POST /deals/createDeal deals/createDeal.handler.ts
1557
+ GET /deals/exportDeals deals/exportDeals.handler.ts
1558
+ GET /deals/getDeal deals/getDeal.handler.ts
1559
+ GET /deals/listDeals deals/listDeals.handler.ts
1560
+ PATCH /deals/moveDeal deals/moveDeal.handler.ts
1512
1561
  ```
1513
1562
 
1514
1563
  `abide routes [--json]` prints each address, its method and the file answering it, with every private handler marked `(private)`. With `--json` it prints the same as one document.
@@ -1520,7 +1569,8 @@ PATCH /deals/moveDeal deals/moveDeal.ts
1520
1569
  ```abide src/deals/lib/Pipeline.abide
1521
1570
  <script>
1522
1571
  import { reactive, refresh } from '@abide/abide'
1523
- import listDeals from '../listDeals.ts'
1572
+
1573
+ import listDeals from '../listDeals.handler.ts'
1524
1574
 
1525
1575
  const deals = reactive(() => listDeals())
1526
1576
 
@@ -1544,8 +1594,9 @@ const total = reactive(() => {
1544
1594
  </ul>
1545
1595
  ```
1546
1596
 
1547
- ```ts src/deals/listDeals.ts
1597
+ ```ts src/deals/listDeals.handler.ts
1548
1598
  import { GET } from '@abide/abide/server'
1599
+
1549
1600
  import { DEALS } from './lib/DEALS.server.ts'
1550
1601
 
1551
1602
  export default GET(
@@ -1560,8 +1611,10 @@ export default GET(
1560
1611
 
1561
1612
  ```ts src/deals/lib/Pipeline.test.ts
1562
1613
  import { expect, test } from 'bun:test'
1614
+
1563
1615
  import { reactive, refresh } from '@abide/abide'
1564
- import listDeals from '../listDeals.ts'
1616
+
1617
+ import listDeals from '../listDeals.handler.ts'
1565
1618
 
1566
1619
  test('a refresh is busy and still ready; a first load is not', async () => {
1567
1620
  const deals = reactive(() => listDeals())
@@ -1612,15 +1665,16 @@ import StageValue from './StageValue.abide'
1612
1665
  </script>
1613
1666
 
1614
1667
  <div class="row">
1615
- <StageCount/>
1616
- <StageValue/>
1668
+ <StageCount />
1669
+ <StageValue />
1617
1670
  </div>
1618
1671
  ```
1619
1672
 
1620
1673
  ```abide src/pipeline/lib/StageCount.abide
1621
1674
  <script>
1622
1675
  import { reactive } from '@abide/abide'
1623
- import getStage from '../getStage.ts'
1676
+
1677
+ import getStage from '../getStage.handler.ts'
1624
1678
  import { STAGES } from './STAGES.ts'
1625
1679
 
1626
1680
  let stage = reactive('lead')
@@ -1630,9 +1684,9 @@ const totals = reactive(() => getStage({ stage }))
1630
1684
  <ul class="fields stacked">
1631
1685
  <li><span>stage</span>
1632
1686
  <select bind:value={stage}>
1633
- {#for one of STAGES}
1634
- <option value={one}>{one}</option>
1635
- {/for}
1687
+ {#for one of STAGES}
1688
+ <option value={one}>{one}</option>
1689
+ {/for}
1636
1690
  </select></li>
1637
1691
  <li><span>deals</span>
1638
1692
  <strong>{totals.ready() ? totals.count : '—'}</strong></li>
@@ -1642,7 +1696,8 @@ const totals = reactive(() => getStage({ stage }))
1642
1696
  ```abide src/pipeline/lib/StageValue.abide
1643
1697
  <script>
1644
1698
  import { reactive } from '@abide/abide'
1645
- import getStage from '../getStage.ts'
1699
+
1700
+ import getStage from '../getStage.handler.ts'
1646
1701
  import { dollars } from './dollars.ts'
1647
1702
  import { STAGES } from './STAGES.ts'
1648
1703
 
@@ -1654,17 +1709,18 @@ const totals = reactive(() => getStage({ stage }))
1654
1709
  <ul class="fields stacked">
1655
1710
  <li><span>stage</span>
1656
1711
  <select bind:value={stage}>
1657
- {#for one of STAGES}
1658
- <option value={one}>{one}</option>
1659
- {/for}
1712
+ {#for one of STAGES}
1713
+ <option value={one}>{one}</option>
1714
+ {/for}
1660
1715
  </select></li>
1661
1716
  <li><span>value</span>
1662
1717
  <strong>{totals.ready() ? dollars(totals.value) : '—'}</strong></li>
1663
1718
  </ul>
1664
1719
  ```
1665
1720
 
1666
- ```ts src/pipeline/getStage.ts
1721
+ ```ts src/pipeline/getStage.handler.ts
1667
1722
  import { GET } from '@abide/abide/server'
1723
+
1668
1724
  import { DEALS } from './lib/DEALS.server.ts'
1669
1725
  import { LOADS } from './lib/LOADS.server.ts'
1670
1726
 
@@ -1689,10 +1745,12 @@ export default GET(
1689
1745
 
1690
1746
  ```ts src/pipeline/lib/StageTiles.test.ts
1691
1747
  import { expect, test } from 'bun:test'
1748
+
1692
1749
  import { mount } from '@abide/harness/abide'
1693
- import getStage from '../getStage.ts'
1694
- import StageTiles from './StageTiles.abide'
1750
+
1751
+ import getStage from '../getStage.handler.ts'
1695
1752
  import { LOADS } from './LOADS.server.ts'
1753
+ import StageTiles from './StageTiles.abide'
1696
1754
 
1697
1755
  function pick(index: number, stage: string): void {
1698
1756
  const select = document.querySelectorAll('select')[index]
@@ -1732,7 +1790,8 @@ A body with a parameter is a keyed value: one entry per set of arguments, held.
1732
1790
  ```abide src/pipeline/lib/Forecast.abide
1733
1791
  <script>
1734
1792
  import { reactive } from '@abide/abide'
1735
- import getForecast from '../getForecast.ts'
1793
+
1794
+ import getForecast from '../getForecast.handler.ts'
1736
1795
  import { dollars } from './dollars.ts'
1737
1796
 
1738
1797
  const forecast = reactive(() => getForecast())
@@ -1754,8 +1813,9 @@ const reloading = reactive(() => forecast.busy() && forecast.ready())
1754
1813
  </div>
1755
1814
  ```
1756
1815
 
1757
- ```ts src/pipeline/getForecast.ts
1816
+ ```ts src/pipeline/getForecast.handler.ts
1758
1817
  import { GET } from '@abide/abide/server'
1818
+
1759
1819
  import { DEALS } from './lib/DEALS.server.ts'
1760
1820
  import { LOADS } from './lib/LOADS.server.ts'
1761
1821
 
@@ -1784,8 +1844,10 @@ export default GET(
1784
1844
 
1785
1845
  ```ts src/pipeline/lib/Forecast.test.ts
1786
1846
  import { expect, test } from 'bun:test'
1847
+
1787
1848
  import { flushSync } from '@abide/abide'
1788
1849
  import { mount } from '@abide/harness/abide'
1850
+
1789
1851
  import Forecast from './Forecast.abide'
1790
1852
  import { LOADS } from './LOADS.server.ts'
1791
1853
 
@@ -1836,10 +1898,10 @@ Press Mark stale and then Reload now, and the forecast blanks between the two. T
1836
1898
 
1837
1899
  ```abide src/pipeline/lib/StageBoard.abide
1838
1900
  <script>
1839
- import getStage from '../getStage.ts'
1840
- import winDeal from '../winDeal.ts'
1841
- import StageTile from './StageTile.abide'
1901
+ import getStage from '../getStage.handler.ts'
1902
+ import winDeal from '../winDeal.handler.ts'
1842
1903
  import { STAGES } from './STAGES.ts'
1904
+ import StageTile from './StageTile.abide'
1843
1905
 
1844
1906
  async function win() {
1845
1907
  await winDeal()
@@ -1850,9 +1912,9 @@ async function win() {
1850
1912
  </script>
1851
1913
 
1852
1914
  <ul class="tiles">
1853
- {#for stage of STAGES}
1854
- <StageTile stage={stage}/>
1855
- {/for}
1915
+ {#for stage of STAGES}
1916
+ <StageTile stage={stage} />
1917
+ {/for}
1856
1918
  </ul>
1857
1919
 
1858
1920
  <div class="row">
@@ -1863,7 +1925,8 @@ async function win() {
1863
1925
  ```abide src/pipeline/lib/StageTile.abide
1864
1926
  <script>
1865
1927
  import { props, reactive } from '@abide/abide'
1866
- import getStage from '../getStage.ts'
1928
+
1929
+ import getStage from '../getStage.handler.ts'
1867
1930
 
1868
1931
  const { stage } = props<{ stage: string }>()
1869
1932
  const totals = reactive(() => getStage({ stage }))
@@ -1875,8 +1938,9 @@ const totals = reactive(() => getStage({ stage }))
1875
1938
  </li>
1876
1939
  ```
1877
1940
 
1878
- ```ts src/pipeline/winDeal.ts
1941
+ ```ts src/pipeline/winDeal.handler.ts
1879
1942
  import { POST } from '@abide/abide/server'
1943
+
1880
1944
  import { DEALS } from './lib/DEALS.server.ts'
1881
1945
 
1882
1946
  /** Moves the first open lead to won, and answers with its id. */
@@ -1892,10 +1956,12 @@ export default POST(
1892
1956
 
1893
1957
  ```ts src/pipeline/lib/StageBoard.test.ts
1894
1958
  import { expect, test } from 'bun:test'
1959
+
1895
1960
  import { mount } from '@abide/harness/abide'
1896
- import { STAGES } from './STAGES.ts'
1897
- import StageBoard from './StageBoard.abide'
1961
+
1898
1962
  import { LOADS } from './LOADS.server.ts'
1963
+ import StageBoard from './StageBoard.abide'
1964
+ import { STAGES } from './STAGES.ts'
1899
1965
 
1900
1966
  function counted(): number[] {
1901
1967
  return STAGES.map((stage) => LOADS[stage] ?? 0)
@@ -1934,7 +2000,8 @@ The triggers also come as free functions, and those take a `Selection`: the set
1934
2000
  ```abide src/pipeline/lib/DealSearch.abide
1935
2001
  <script>
1936
2002
  import { reactive } from '@abide/abide'
1937
- import searchDeals from '../searchDeals.ts'
2003
+
2004
+ import searchDeals from '../searchDeals.handler.ts'
1938
2005
 
1939
2006
  let q = reactive('')
1940
2007
  const found = reactive(() => searchDeals({ q }), { debounce: 300 })
@@ -1945,14 +2012,15 @@ const found = reactive(() => searchDeals({ q }), { debounce: 300 })
1945
2012
  </label>
1946
2013
 
1947
2014
  <ul class="fields">
1948
- {#for name of found.ready() ? found : []}
1949
- <li><strong>{name}</strong></li>
1950
- {/for}
2015
+ {#for name of found.ready() ? found : []}
2016
+ <li><strong>{name}</strong></li>
2017
+ {/for}
1951
2018
  </ul>
1952
2019
  ```
1953
2020
 
1954
- ```ts src/pipeline/searchDeals.ts
2021
+ ```ts src/pipeline/searchDeals.handler.ts
1955
2022
  import { GET } from '@abide/abide/server'
2023
+
1956
2024
  import { DEALS } from './lib/DEALS.server.ts'
1957
2025
  import { LOADS } from './lib/LOADS.server.ts'
1958
2026
 
@@ -1974,7 +2042,9 @@ export default GET(
1974
2042
 
1975
2043
  ```ts src/pipeline/lib/DealSearch.test.ts
1976
2044
  import { expect, test } from 'bun:test'
2045
+
1977
2046
  import { mount } from '@abide/harness/abide'
2047
+
1978
2048
  import DealSearch from './DealSearch.abide'
1979
2049
  import { LOADS } from './LOADS.server.ts'
1980
2050
 
@@ -2020,7 +2090,8 @@ Every keystroke is a new `q`, and so a new key. `debounce: 300` holds the search
2020
2090
  ```abide src/pipeline/lib/DealFeed.abide
2021
2091
  <script>
2022
2092
  import { reactive } from '@abide/abide'
2023
- import listDeals from '../listDeals.ts'
2093
+
2094
+ import listDeals from '../listDeals.handler.ts'
2024
2095
  import { dollars } from './dollars.ts'
2025
2096
 
2026
2097
  const deals = reactive(() => listDeals())
@@ -2044,10 +2115,10 @@ function applyNextUpdate() {
2044
2115
  </script>
2045
2116
 
2046
2117
  <ul class="fields">
2047
- {#for deal of deals.ready() ? deals : [] by deal.id}
2048
- <li><span>{deal.stage}</span><strong>{dollars(deal.amount)}</strong>
2049
- {deal.name}</li>
2050
- {/for}
2118
+ {#for deal of deals.ready() ? deals : [] by deal.id}
2119
+ <li><span>{deal.stage}</span><strong>{dollars(deal.amount)}</strong>
2120
+ {deal.name}</li>
2121
+ {/for}
2051
2122
  </ul>
2052
2123
 
2053
2124
  <div class="row">
@@ -2055,8 +2126,9 @@ function applyNextUpdate() {
2055
2126
  </div>
2056
2127
  ```
2057
2128
 
2058
- ```ts src/pipeline/listDeals.ts
2129
+ ```ts src/pipeline/listDeals.handler.ts
2059
2130
  import { GET } from '@abide/abide/server'
2131
+
2060
2132
  import { DEALS } from './lib/DEALS.server.ts'
2061
2133
 
2062
2134
  /** Every open deal, as a list. */
@@ -2072,8 +2144,10 @@ export default GET(
2072
2144
 
2073
2145
  ```ts src/pipeline/lib/DealFeed.test.ts
2074
2146
  import { expect, test } from 'bun:test'
2147
+
2075
2148
  import { flushSync } from '@abide/abide'
2076
2149
  import { measure, mount } from '@abide/harness/abide'
2150
+
2077
2151
  import DealFeed from './DealFeed.abide'
2078
2152
 
2079
2153
  test('an update rewrites one amount and builds nothing', async () => {
@@ -2109,7 +2183,8 @@ Change the held value without `patch` and nothing on the page moves, because no
2109
2183
  ```abide src/pipeline/lib/DealPicker.abide
2110
2184
  <script>
2111
2185
  import { reactive } from '@abide/abide'
2112
- import listDeals from '../listDeals.ts'
2186
+
2187
+ import listDeals from '../listDeals.handler.ts'
2113
2188
  import DealDetail from './DealDetail.abide'
2114
2189
  import DealRow from './DealRow.abide'
2115
2190
 
@@ -2119,12 +2194,12 @@ const deals = reactive(() => listDeals())
2119
2194
  reactive.share('selectedDeal', () => reactive('d1'))
2120
2195
  </script>
2121
2196
 
2122
- <DealDetail/>
2197
+ <DealDetail />
2123
2198
 
2124
2199
  <div class="row">
2125
- {#for deal of deals.ready() ? deals : [] by deal.id}
2126
- <DealRow id={deal.id} name={deal.name}/>
2127
- {/for}
2200
+ {#for deal of deals.ready() ? deals : [] by deal.id}
2201
+ <DealRow id={deal.id} name={deal.name} />
2202
+ {/for}
2128
2203
  </div>
2129
2204
  ```
2130
2205
 
@@ -2145,7 +2220,8 @@ let selected = reactive.share('selectedDeal', () => reactive(''))
2145
2220
  ```abide src/pipeline/lib/DealDetail.abide
2146
2221
  <script>
2147
2222
  import { reactive } from '@abide/abide'
2148
- import listDeals from '../listDeals.ts'
2223
+
2224
+ import listDeals from '../listDeals.handler.ts'
2149
2225
 
2150
2226
  const deals = reactive(() => listDeals())
2151
2227
  const selected = reactive.share('selectedDeal', () => reactive(''))
@@ -2162,8 +2238,10 @@ const deal = reactive(() =>
2162
2238
 
2163
2239
  ```ts src/pipeline/lib/DealPicker.test.ts
2164
2240
  import { expect, test } from 'bun:test'
2241
+
2165
2242
  import { flushSync } from '@abide/abide'
2166
2243
  import { mount } from '@abide/harness/abide'
2244
+
2167
2245
  import DealPicker from './DealPicker.abide'
2168
2246
 
2169
2247
  test('picking a deal in the list shows it in the detail panel', async () => {
@@ -2193,7 +2271,8 @@ Use the value `reactive.share` returns, and no other name. A value lives in the
2193
2271
  ```abide src/pipeline/lib/DealNote.abide
2194
2272
  <script>
2195
2273
  import { reactive, watch } from '@abide/abide'
2196
- import saveNote from '../saveNote.ts'
2274
+
2275
+ import saveNote from '../saveNote.handler.ts'
2197
2276
 
2198
2277
  let note = reactive('Wants a quote by Friday.')
2199
2278
  let saved = reactive('Wants a quote by Friday.')
@@ -2220,8 +2299,9 @@ watch(() => {
2220
2299
  </ul>
2221
2300
  ```
2222
2301
 
2223
- ```ts src/pipeline/saveNote.ts
2302
+ ```ts src/pipeline/saveNote.handler.ts
2224
2303
  import { POST } from '@abide/abide/server'
2304
+
2225
2305
  import { LOADS } from './lib/LOADS.server.ts'
2226
2306
  import { NOTES } from './lib/NOTES.server.ts'
2227
2307
 
@@ -2237,7 +2317,9 @@ export default POST(
2237
2317
 
2238
2318
  ```ts src/pipeline/lib/DealNote.test.ts
2239
2319
  import { expect, test } from 'bun:test'
2320
+
2240
2321
  import { mount } from '@abide/harness/abide'
2322
+
2241
2323
  import DealNote from './DealNote.abide'
2242
2324
  import { LOADS } from './LOADS.server.ts'
2243
2325
  import { NOTES } from './NOTES.server.ts'
@@ -2287,9 +2369,10 @@ Where a read subscribes depends on where it runs:
2287
2369
  ```abide src/pipeline/lib/PipelineView.abide
2288
2370
  <script>
2289
2371
  import { reactive } from '@abide/abide'
2372
+
2290
2373
  import { preferenceStore } from './preferenceStore.ts'
2291
- import StageTile from './StageTile.abide'
2292
2374
  import { STAGES } from './STAGES.ts'
2375
+ import StageTile from './StageTile.abide'
2293
2376
 
2294
2377
  const VIEWS = ['board', 'list']
2295
2378
 
@@ -2298,23 +2381,24 @@ let view = reactive('board', { store: preferenceStore('view') })
2298
2381
  </script>
2299
2382
 
2300
2383
  <div class="tabs">
2301
- {#for one of VIEWS}
2302
- <button aria-selected={view === one} onclick={() => (view = one)}>
2303
- {one}</button>
2304
- {/for}
2384
+ {#for one of VIEWS}
2385
+ <button aria-selected={view === one} onclick={() => (view = one)}>
2386
+ {one}</button>
2387
+ {/for}
2305
2388
  </div>
2306
2389
 
2307
2390
  <ul class={view === 'board' ? 'tiles' : 'fields'}>
2308
- {#for stage of STAGES}
2309
- <StageTile stage={stage}/>
2310
- {/for}
2391
+ {#for stage of STAGES}
2392
+ <StageTile stage={stage} />
2393
+ {/for}
2311
2394
  </ul>
2312
2395
  ```
2313
2396
 
2314
2397
  ```ts src/pipeline/lib/preferenceStore.ts
2315
2398
  import type { Store } from '@abide/abide'
2316
- import getPreference from '../getPreference.ts'
2317
- import setPreference from '../setPreference.ts'
2399
+
2400
+ import getPreference from '../getPreference.handler.ts'
2401
+ import setPreference from '../setPreference.handler.ts'
2318
2402
 
2319
2403
  /** Where a preference lives between visits: both halves, or neither. */
2320
2404
  export function preferenceStore(name: string): Store<string> {
@@ -2330,8 +2414,9 @@ export function preferenceStore(name: string): Store<string> {
2330
2414
  }
2331
2415
  ```
2332
2416
 
2333
- ```ts src/pipeline/getPreference.ts
2417
+ ```ts src/pipeline/getPreference.handler.ts
2334
2418
  import { GET } from '@abide/abide/server'
2419
+
2335
2420
  import { PREFERENCES } from './lib/PREFERENCES.server.ts'
2336
2421
 
2337
2422
  /** A saved preference, or `null` where none was saved. */
@@ -2345,8 +2430,9 @@ export default GET(
2345
2430
  )
2346
2431
  ```
2347
2432
 
2348
- ```ts src/pipeline/setPreference.ts
2433
+ ```ts src/pipeline/setPreference.handler.ts
2349
2434
  import { POST } from '@abide/abide/server'
2435
+
2350
2436
  import { PREFERENCES } from './lib/PREFERENCES.server.ts'
2351
2437
 
2352
2438
  /** Saves a preference. */
@@ -2360,8 +2446,10 @@ export default POST(
2360
2446
 
2361
2447
  ```ts src/pipeline/lib/PipelineView.test.ts
2362
2448
  import { expect, test } from 'bun:test'
2449
+
2363
2450
  import { flushSync } from '@abide/abide'
2364
2451
  import { mount } from '@abide/harness/abide'
2452
+
2365
2453
  import PipelineView from './PipelineView.abide'
2366
2454
  import { PREFERENCES } from './PREFERENCES.server.ts'
2367
2455
 
@@ -2454,6 +2542,7 @@ import LastActivity from './LastActivity.abide'
2454
2542
  ```abide src/activity/lib/LastActivity.abide
2455
2543
  <script>
2456
2544
  import { reactive } from '@abide/abide'
2545
+
2457
2546
  // The same import reaches the same channel: no prop, no provider.
2458
2547
  import { activity } from './activity.ts'
2459
2548
 
@@ -2471,8 +2560,10 @@ const last = reactive(() => activity.at(-1))
2471
2560
 
2472
2561
  ```ts src/activity/lib/QuickLog.test.ts
2473
2562
  import { expect, test } from 'bun:test'
2563
+
2474
2564
  import { flushSync } from '@abide/abide'
2475
2565
  import { mount } from '@abide/harness/abide'
2566
+
2476
2567
  import QuickLog from './QuickLog.abide'
2477
2568
 
2478
2569
  test('a reader the logger never names follows what it logs', async () => {
@@ -2537,8 +2628,10 @@ export function logActivity(contact: string, kind: string): void {
2537
2628
 
2538
2629
  ```ts src/activity/lib/RecentActivity.test.ts
2539
2630
  import { expect, test } from 'bun:test'
2631
+
2540
2632
  import { flushSync } from '@abide/abide'
2541
2633
  import { mount } from '@abide/harness/abide'
2634
+
2542
2635
  import RecentActivity from './RecentActivity.abide'
2543
2636
 
2544
2637
  test('logging past three adds one entry and drops the oldest', async () => {
@@ -2556,7 +2649,7 @@ test('logging past three adds one entry and drops the oldest', async () => {
2556
2649
  ```
2557
2650
 
2558
2651
  The trim is one line in the patch that appends: `if (entries.length > 3)
2559
- entries.shift()`. `patch` hands you the array the channel holds and changes it in place, so the push and the trim are one write. abide puts no cap on the array. The condition is yours: a count here, an age or a byte total where that is what bounds it. Keep the push and the trim in one function every writer calls, and the cap belongs to the channel rather than to whichever caller remembered it.
2652
+ entries.shift()`. `patch` hands you the array the channel holds and changes it in place, so the push and the trim are one write. abide puts no cap on the array. The condition is yours: a count here, an age or a byte total where that is what bounds it. Keep the push and the trim in one function every writer calls, and the cap belongs to the channel rather than to whichever caller remembered it.
2560
2653
 
2561
2654
  A value keeps no past of its own. Where you want what came before, make the value the array and append where you write, as here. An undo stack is the same: an array the app pushes into.
2562
2655
 
@@ -2567,8 +2660,9 @@ A value keeps no past of its own. Where you want what came before, make the valu
2567
2660
  ```abide src/activity/lib/AccountFeed.abide
2568
2661
  <script>
2569
2662
  import { reactive } from '@abide/abide'
2570
- import feed from '../feed.ts'
2571
- import logToFeed from '../logToFeed.ts'
2663
+
2664
+ import feed from '../feed.handler.ts'
2665
+ import logToFeed from '../logToFeed.handler.ts'
2572
2666
 
2573
2667
  const account = 'hopper-co'
2574
2668
  const entries = reactive(() => feed({ account }))
@@ -2600,16 +2694,18 @@ export const feeds = reactive((_account: { account: string }): Entry[] => [], {
2600
2694
  })
2601
2695
  ```
2602
2696
 
2603
- ```ts src/activity/feed.ts
2697
+ ```ts src/activity/feed.handler.ts
2604
2698
  import { GET } from '@abide/abide/server'
2699
+
2605
2700
  import { feeds } from './lib/feeds.ts'
2606
2701
 
2607
2702
  /** Answers at /activity/feed; every tab reading it follows it. */
2608
2703
  export default GET(feeds, { description: "An account's activity feed." })
2609
2704
  ```
2610
2705
 
2611
- ```ts src/activity/logToFeed.ts
2706
+ ```ts src/activity/logToFeed.handler.ts
2612
2707
  import { POST } from '@abide/abide/server'
2708
+
2613
2709
  import { feeds } from './lib/feeds.ts'
2614
2710
 
2615
2711
  const KINDS = ['Call', 'Email', 'Meeting']
@@ -2633,6 +2729,7 @@ export default POST(
2633
2729
 
2634
2730
  ```ts src/activity/lib/AccountFeed.test.ts
2635
2731
  import { expect, test } from 'bun:test'
2732
+
2636
2733
  import { createApp } from '@abide/abide/server'
2637
2734
 
2638
2735
  // Each request is a caller of its own, as each open tab is.
@@ -2674,7 +2771,8 @@ The feed lives in memory. A restart starts it empty, and each instance of the ap
2674
2771
  ```abide src/activity/lib/ActivityHistory.abide
2675
2772
  <script>
2676
2773
  import { reactive, refresh } from '@abide/abide'
2677
- import olderActivity from '../olderActivity.ts'
2774
+
2775
+ import olderActivity from '../olderActivity.handler.ts'
2678
2776
 
2679
2777
  const entries = reactive(() => olderActivity())
2680
2778
  </script>
@@ -2691,7 +2789,7 @@ const entries = reactive(() => olderActivity())
2691
2789
  </div>
2692
2790
  ```
2693
2791
 
2694
- ```ts src/activity/olderActivity.ts
2792
+ ```ts src/activity/olderActivity.handler.ts
2695
2793
  import { GET } from '@abide/abide/server'
2696
2794
 
2697
2795
  const OLDER = [
@@ -2715,7 +2813,8 @@ export default GET(
2715
2813
 
2716
2814
  ```ts src/activity/lib/ActivityHistory.test.ts
2717
2815
  import { expect, test } from 'bun:test'
2718
- import olderActivity from '../olderActivity.ts'
2816
+
2817
+ import olderActivity from '../olderActivity.handler.ts'
2719
2818
 
2720
2819
  test('the first entry is in hand while the rest are still read', async () => {
2721
2820
  const entries = olderActivity()
@@ -2746,7 +2845,8 @@ A page rendered on the server is not held for an unfinished stream. What the str
2746
2845
  ```abide src/activity/lib/CsvExport.abide
2747
2846
  <script>
2748
2847
  import { reactive } from '@abide/abide'
2749
- import exportCsv from '../exportCsv.ts'
2848
+
2849
+ import exportCsv from '../exportCsv.handler.ts'
2750
2850
 
2751
2851
  const csv = reactive(async () => (await exportCsv.raw()).text())
2752
2852
  </script>
@@ -2759,7 +2859,7 @@ const csv = reactive(async () => (await exportCsv.raw()).text())
2759
2859
  </div>
2760
2860
  ```
2761
2861
 
2762
- ```ts src/activity/exportCsv.ts
2862
+ ```ts src/activity/exportCsv.handler.ts
2763
2863
  import { bytes, GET } from '@abide/abide/server'
2764
2864
 
2765
2865
  const ENCODER = new TextEncoder()
@@ -2787,6 +2887,7 @@ export default GET(() => bytes(csvOf()), {
2787
2887
 
2788
2888
  ```ts src/activity/lib/CsvExport.test.ts
2789
2889
  import { expect, test } from 'bun:test'
2890
+
2790
2891
  import { createApp } from '@abide/abide/server'
2791
2892
 
2792
2893
  test('the export is a file, written a row at a time', async () => {
@@ -2819,7 +2920,8 @@ A stream whose first chunk is binary is sent as bytes without the helper. `bytes
2819
2920
  ```abide src/activity/lib/Wallboard.abide
2820
2921
  <script>
2821
2922
  import { reactive } from '@abide/abide'
2822
- import ticker from '../ticker.ts'
2923
+
2924
+ import ticker from '../ticker.handler.ts'
2823
2925
 
2824
2926
  // A plain request, as a wallboard sends it: no `Accept` asks for events.
2825
2927
  const answer = reactive(async () => {
@@ -2835,7 +2937,7 @@ const answer = reactive(async () => {
2835
2937
  <pre class="rounded-md border border-rule bg-raised p-3">{answer.body}</pre>
2836
2938
  ```
2837
2939
 
2838
- ```ts src/activity/ticker.ts
2940
+ ```ts src/activity/ticker.handler.ts
2839
2941
  import { GET, sse } from '@abide/abide/server'
2840
2942
 
2841
2943
  const MOVES = [
@@ -2852,6 +2954,7 @@ export default GET(() => sse(MOVES), {
2852
2954
 
2853
2955
  ```ts src/activity/lib/Wallboard.test.ts
2854
2956
  import { expect, test } from 'bun:test'
2957
+
2855
2958
  import { createApp } from '@abide/abide/server'
2856
2959
 
2857
2960
  test('a caller asking for nothing in particular is sent events', async () => {
@@ -2912,10 +3015,11 @@ The CRM outgrows one page. This chapter adds a page per company, the chrome they
2912
3015
  | File | Answers on |
2913
3016
  | --- | --- |
2914
3017
  | `src/companies/index.abide` | `/companies` |
2915
- | `src/companies/[id].abide` | `/companies/hopper-and-co`, and every other id |
3018
+ | `src/companies/[id=SLUG].abide` | `/companies/hopper-and-co`, and every other id |
3019
+ | `src/companies/SLUG.param.ts` | no address: it says what a segment naming `SLUG` takes |
2916
3020
  | `src/companies/layout.abide` | no address: it wraps every page at `/companies` and below |
2917
3021
  | `src/companies/error.abide` | no address: it answers when a page below it fails |
2918
- | `src/companies/getCompany.ts` | `GET /companies/getCompany` |
3022
+ | `src/companies/getCompany.handler.ts` | `GET /companies/getCompany` |
2919
3023
 
2920
3024
  An `**/index.abide` answers its directory's own address, and any other `**/<name>.abide` adds its name. A part of the path in brackets is a value the page reads:
2921
3025
 
@@ -2925,9 +3029,9 @@ An `**/index.abide` answers its directory's own address, and any other `**/<name
2925
3029
  | `[[name]]` | one segment or none | text, absent when nothing matched |
2926
3030
  | `[...name]` | every remaining segment | a list, empty when nothing matched |
2927
3031
 
2928
- ## `[id].abide` answers for every company
3032
+ ## `[id=SLUG].abide` answers for every company
2929
3033
 
2930
- **`[id].abide` — one file for every company** — Two companies, one page file. Pick one: the address changes, and the page loads the company the address names.
3034
+ **`[id=SLUG].abide` — one file for every company** — Two companies, one page file. Pick one: the address changes, and the page loads the company the address names.
2931
3035
 
2932
3036
  ```abide src/companies/lib/CompanyPages.abide
2933
3037
  <script>
@@ -2936,7 +3040,7 @@ import { reactive, url } from '@abide/abide'
2936
3040
  const IDS = ['analytical-engines', 'hopper-and-co']
2937
3041
 
2938
3042
  let open = reactive('hopper-and-co')
2939
- const address = reactive(() => url('/companies/[id]', { id: open }))
3043
+ const address = reactive(() => url('/companies/[id=SLUG]', { id: open }))
2940
3044
  </script>
2941
3045
 
2942
3046
  <div class="tabs">
@@ -2950,10 +3054,11 @@ const address = reactive(() => url('/companies/[id]', { id: open }))
2950
3054
  class="h-60 w-full rounded-md border border-rule"></iframe>
2951
3055
  ```
2952
3056
 
2953
- ```abide src/companies/[id].abide
3057
+ ```abide src/companies/[id=SLUG].abide
2954
3058
  <script>
2955
3059
  import { reactive, route } from '@abide/abide'
2956
- import getCompany from './getCompany.ts'
3060
+
3061
+ import getCompany from './getCompany.handler.ts'
2957
3062
  import CompanyTabs from './lib/CompanyTabs.abide'
2958
3063
 
2959
3064
  const company = reactive(() => getCompany({ id: route.params.id }))
@@ -2976,6 +3081,7 @@ const company = reactive(() => getCompany({ id: route.params.id }))
2976
3081
 
2977
3082
  ```ts src/companies/CompanyPage.test.ts
2978
3083
  import { expect, test } from 'bun:test'
3084
+
2979
3085
  import { createApp } from '@abide/abide/server'
2980
3086
 
2981
3087
  test('one file answers for every company, reading its id', async () => {
@@ -2995,10 +3101,100 @@ test('one file answers for every company, reading its id', async () => {
2995
3101
 
2996
3102
  | Name | What it does for the CRM |
2997
3103
  | --- | --- |
2998
- | `route.name` | the pattern that matched, `/companies/[id]` for every company, so a log line keys on the page rather than on each address |
3104
+ | `route.name` | the pattern that matched, `/companies/[id=SLUG]` for every company, so a log line keys on the page rather than on each address |
2999
3105
  | `Params` | what the segments matched; a query string is never in it |
3000
3106
  | `RouteParams<P>` | the type `route.params` has in a page or layout filed under `P`, so `id` is text and a key the route lacks is an error. A component under `lib/` has no route of its own and reads the loose `Params`: pass it the value as a prop, or narrow it with `RouteParams<'/…'>` |
3001
3107
 
3108
+ ## A param file says what an id looks like
3109
+
3110
+ **`SLUG.param.ts` — an address that cannot be an id never reaches the page** — A rep pastes a company's name where its id goes. No id has a capital or a space, so no page takes the address, and nothing asks the database for it.
3111
+
3112
+ ```abide src/companies/lib/PastedName.abide
3113
+ <script>
3114
+ import { reactive, url } from '@abide/abide'
3115
+
3116
+ const PASTED = ['hopper-and-co', 'Hopper & Co']
3117
+
3118
+ let open = reactive('Hopper & Co')
3119
+ const address = reactive(() => url('/companies/[id=SLUG]', { id: open }))
3120
+ </script>
3121
+
3122
+ <div class="tabs">
3123
+ {#for id of PASTED}
3124
+ <button aria-selected={open === id}
3125
+ onclick={() => (open = id)}>{id}</button>
3126
+ {/for}
3127
+ </div>
3128
+ <p class="status">{address}</p>
3129
+ <iframe src={address} title="An address the company page may not take"
3130
+ class="h-48 w-full rounded-md border border-rule"></iframe>
3131
+ ```
3132
+
3133
+ ```ts src/companies/SLUG.param.ts
3134
+ // A company's id: lowercase words and digits, joined by hyphens.
3135
+ export const SLUG = /[a-z0-9-]+/
3136
+ ```
3137
+
3138
+ ```abide src/companies/[id=SLUG].abide
3139
+ <script>
3140
+ import { reactive, route } from '@abide/abide'
3141
+
3142
+ import getCompany from './getCompany.handler.ts'
3143
+ import CompanyTabs from './lib/CompanyTabs.abide'
3144
+
3145
+ const company = reactive(() => getCompany({ id: route.params.id }))
3146
+ </script>
3147
+
3148
+ <head><title>{company.name} — CRM</title></head>
3149
+
3150
+ <h2 class="text-heading font-semibold"
3151
+ style:view-transition-name="company-{route.params.id}">{company.name}</h2>
3152
+ <ul class="fields">
3153
+ <li><span>contact</span><strong>{company.contact}</strong></li>
3154
+ <li><span>pipeline</span><strong>{company.pipeline}</strong></li>
3155
+ </ul>
3156
+ <CompanyTabs />
3157
+
3158
+ <style>
3159
+ ::view-transition-group(*) { animation-duration: 0.5s; }
3160
+ </style>
3161
+ ```
3162
+
3163
+ ```ts src/companies/Matcher.test.ts
3164
+ import { expect, test } from 'bun:test'
3165
+
3166
+ import { createApp } from '@abide/abide/server'
3167
+
3168
+ import { COMPANIES } from './lib/COMPANIES.server.ts'
3169
+
3170
+ test('the company page takes every id the CRM has, and no name', async () => {
3171
+ await using app = await createApp()
3172
+ for (const { id } of COMPANIES) {
3173
+ const answer = await app.fetch(
3174
+ new Request(`http://crm.invalid/companies/${id}`),
3175
+ )
3176
+ expect(answer.status).toBe(200)
3177
+ await answer.body?.cancel()
3178
+ }
3179
+ const pasted = await app.fetch(
3180
+ new Request('http://crm.invalid/companies/Hopper%20%26%20Co'),
3181
+ )
3182
+ expect(pasted.status).toBe(404)
3183
+ })
3184
+ ```
3185
+
3186
+ A segment names its matcher after an `=`. `[id=SLUG]` takes a part only where the whole of it, decoded, matches the `SLUG` in `SLUG.param.ts`, found beside the page or in the nearest directory above it. A part it refuses goes to the next route that could answer the address. The companies have none, so `Hopper & Co` is a 404, painted by `error.abide`.
3187
+
3188
+ | Named | Matches |
3189
+ | --- | --- |
3190
+ | `[name=NAME]` | one segment the matcher takes |
3191
+ | `[[name=NAME]]` | one segment the matcher takes, or none |
3192
+ | `[...name=NAME]` | every remaining segment, each one the matcher takes |
3193
+
3194
+ The build reads a param file without running it, so it holds one `export const` named as the file, a regular expression with no flags, and no import. A pattern that repeats a group that itself repeats, or that refers back to a group, is refused: either can make one crafted address slow to test.
3195
+
3196
+ A segment with no matcher never takes a part with a period in it, which is why `/favicon.ico` never reaches a company page. A matcher replaces that rule for its segment, so one that allows a period lets `url` write one too. `.` and `..` are never taken. Under `abide check`, a matcher that is a choice of words, `/deals|notes/`, types the param as those words in `route.params` and in `url`.
3197
+
3002
3198
  ## A layout holds what the companies pages share
3003
3199
 
3004
3200
  **`layout.abide` — the header the companies pages share, built once** — The companies header holds a note for the call you are on. Type one, then open a company and come back: the page changes, and the note stays.
@@ -3015,6 +3211,7 @@ import { url } from '@abide/abide'
3015
3211
  ```abide src/companies/layout.abide
3016
3212
  <script server>
3017
3213
  import { onError } from '@abide/abide/server'
3214
+
3018
3215
  import { tracker } from './lib/tracker.server.ts'
3019
3216
 
3020
3217
  onError((error) => tracker.capture(error))
@@ -3033,11 +3230,12 @@ let note = reactive('')
3033
3230
  <input bind:value={note} placeholder="Note for the call">
3034
3231
  </header>
3035
3232
 
3036
- <slot/>
3233
+ <slot />
3037
3234
  ```
3038
3235
 
3039
3236
  ```ts src/companies/Layout.test.ts
3040
3237
  import { expect, test } from 'bun:test'
3238
+
3041
3239
  import { createApp } from '@abide/abide/server'
3042
3240
 
3043
3241
  test('the list and a company render inside one layout', async () => {
@@ -3057,7 +3255,7 @@ A package can ship pages too. `import '…' with { type: 'routes' }` in a file o
3057
3255
 
3058
3256
  ## A link names a route, and `url` fills it in
3059
3257
 
3060
- The list links each company with `url('/companies/[id]', { id: company.id })`. You write the pattern a page is filed under rather than the address, and `url` fills in the segments. Leaving out a required one is a compile error, and a pattern no page is filed under is refused by `abide check`. A query goes last: `url('/companies', {}, { city: 'London' })`.
3258
+ The list links each company with `url('/companies/[id=SLUG]', { id: company.id })`. You write the pattern a page is filed under, matcher and all, rather than the address, and `url` fills in the segments. Leaving out a required one is a compile error, and a pattern no page is filed under is refused by `abide check`. A query goes last: `url('/companies', {}, { city: 'London' })`.
3061
3259
 
3062
3260
  | Name | What it does for the CRM |
3063
3261
  | --- | --- |
@@ -3106,6 +3304,7 @@ let draft = reactive('')
3106
3304
 
3107
3305
  ```ts src/companies/lib/CompanyTabs.test.ts
3108
3306
  import { expect, test } from 'bun:test'
3307
+
3109
3308
  import { createApp } from '@abide/abide/server'
3110
3309
 
3111
3310
  test('the open tab is read from the address', async () => {
@@ -3137,7 +3336,8 @@ import { url } from '@abide/abide'
3137
3336
  ```abide src/companies/index.abide
3138
3337
  <script>
3139
3338
  import { reactive, url } from '@abide/abide'
3140
- import listCompanies from './listCompanies.ts'
3339
+
3340
+ import listCompanies from './listCompanies.handler.ts'
3141
3341
 
3142
3342
  const companies = reactive(() => listCompanies())
3143
3343
  </script>
@@ -3145,7 +3345,7 @@ const companies = reactive(() => listCompanies())
3145
3345
  <ul class="fields">
3146
3346
  {#for company of companies by company.id}
3147
3347
  <li><span>{company.city}</span>
3148
- <a href={url('/companies/[id]', { id: company.id })}
3348
+ <a href={url('/companies/[id=SLUG]', { id: company.id })}
3149
3349
  style:view-transition-name="company-{company.id}"
3150
3350
  >{company.name}</a></li>
3151
3351
  {/for}
@@ -3156,10 +3356,11 @@ const companies = reactive(() => listCompanies())
3156
3356
  </style>
3157
3357
  ```
3158
3358
 
3159
- ```abide src/companies/[id].abide
3359
+ ```abide src/companies/[id=SLUG].abide
3160
3360
  <script>
3161
3361
  import { reactive, route } from '@abide/abide'
3162
- import getCompany from './getCompany.ts'
3362
+
3363
+ import getCompany from './getCompany.handler.ts'
3163
3364
  import CompanyTabs from './lib/CompanyTabs.abide'
3164
3365
 
3165
3366
  const company = reactive(() => getCompany({ id: route.params.id }))
@@ -3182,6 +3383,7 @@ const company = reactive(() => getCompany({ id: route.params.id }))
3182
3383
 
3183
3384
  ```ts src/companies/ViewTransition.test.ts
3184
3385
  import { expect, test } from 'bun:test'
3386
+
3185
3387
  import { createApp } from '@abide/abide/server'
3186
3388
 
3187
3389
  test("the list row and the company's heading share one name", async () => {
@@ -3209,7 +3411,7 @@ const PAGES = [
3209
3411
  { label: 'All companies', address: url('/companies') },
3210
3412
  {
3211
3413
  label: 'Hopper & Co',
3212
- address: url('/companies/[id]', { id: 'hopper-and-co' }),
3414
+ address: url('/companies/[id=SLUG]', { id: 'hopper-and-co' }),
3213
3415
  },
3214
3416
  ]
3215
3417
 
@@ -3254,6 +3456,7 @@ function followTitle(element: Element) {
3254
3456
  ```abide src/companies/layout.abide
3255
3457
  <script server>
3256
3458
  import { onError } from '@abide/abide/server'
3459
+
3257
3460
  import { tracker } from './lib/tracker.server.ts'
3258
3461
 
3259
3462
  onError((error) => tracker.capture(error))
@@ -3272,13 +3475,14 @@ let note = reactive('')
3272
3475
  <input bind:value={note} placeholder="Note for the call">
3273
3476
  </header>
3274
3477
 
3275
- <slot/>
3478
+ <slot />
3276
3479
  ```
3277
3480
 
3278
- ```abide src/companies/[id].abide
3481
+ ```abide src/companies/[id=SLUG].abide
3279
3482
  <script>
3280
3483
  import { reactive, route } from '@abide/abide'
3281
- import getCompany from './getCompany.ts'
3484
+
3485
+ import getCompany from './getCompany.handler.ts'
3282
3486
  import CompanyTabs from './lib/CompanyTabs.abide'
3283
3487
 
3284
3488
  const company = reactive(() => getCompany({ id: route.params.id }))
@@ -3301,6 +3505,7 @@ const company = reactive(() => getCompany({ id: route.params.id }))
3301
3505
 
3302
3506
  ```ts src/companies/Head.test.ts
3303
3507
  import { expect, test } from 'bun:test'
3508
+
3304
3509
  import { createApp } from '@abide/abide/server'
3305
3510
 
3306
3511
  const titlesOf = (html: string) => html.match(/<title>[^<]*<\/title>/g)
@@ -3329,7 +3534,7 @@ A `<head>` sits at the top level of a file, never inside a block. A `<title>`, a
3329
3534
  <script>
3330
3535
  import { reactive, url } from '@abide/abide'
3331
3536
 
3332
- const PAGE = url('/companies/[id]', { id: 'hopper-and-co' })
3537
+ const PAGE = url('/companies/[id=SLUG]', { id: 'hopper-and-co' })
3333
3538
 
3334
3539
  let loads = reactive(1)
3335
3540
  </script>
@@ -3341,10 +3546,11 @@ let loads = reactive(1)
3341
3546
  class="h-60 w-full rounded-md border border-rule"></iframe>
3342
3547
  ```
3343
3548
 
3344
- ```abide src/companies/[id].abide
3549
+ ```abide src/companies/[id=SLUG].abide
3345
3550
  <script>
3346
3551
  import { reactive, route } from '@abide/abide'
3347
- import getCompany from './getCompany.ts'
3552
+
3553
+ import getCompany from './getCompany.handler.ts'
3348
3554
  import CompanyTabs from './lib/CompanyTabs.abide'
3349
3555
 
3350
3556
  const company = reactive(() => getCompany({ id: route.params.id }))
@@ -3365,8 +3571,9 @@ const company = reactive(() => getCompany({ id: route.params.id }))
3365
3571
  </style>
3366
3572
  ```
3367
3573
 
3368
- ```ts src/companies/getCompany.ts
3574
+ ```ts src/companies/getCompany.handler.ts
3369
3575
  import { GET, notFound } from '@abide/abide/server'
3576
+
3370
3577
  import { COMPANIES } from './lib/COMPANIES.server.ts'
3371
3578
 
3372
3579
  /** Answers at /companies/getCompany; a company's page reads it. */
@@ -3385,6 +3592,7 @@ export default GET(
3385
3592
 
3386
3593
  ```ts src/companies/Streaming.test.ts
3387
3594
  import { expect, test } from 'bun:test'
3595
+
3388
3596
  import { createApp } from '@abide/abide/server'
3389
3597
 
3390
3598
  test('the page goes out without the company, and asks for it', async () => {
@@ -3419,7 +3627,7 @@ Nothing holds the document for a value. The page gets one turn to start its load
3419
3627
  import { url } from '@abide/abide'
3420
3628
  </script>
3421
3629
 
3422
- <iframe src={url('/companies/[id]', { id: 'initech' })}
3630
+ <iframe src={url('/companies/[id=SLUG]', { id: 'initech' })}
3423
3631
  title="A company that is not there"
3424
3632
  class="h-48 w-full rounded-md border border-rule"></iframe>
3425
3633
  ```
@@ -3440,8 +3648,9 @@ const { failure } = props<{ failure: Failed<string, unknown> }>()
3440
3648
  {/if}
3441
3649
  ```
3442
3650
 
3443
- ```ts src/companies/getCompany.ts
3651
+ ```ts src/companies/getCompany.handler.ts
3444
3652
  import { GET, notFound } from '@abide/abide/server'
3653
+
3445
3654
  import { COMPANIES } from './lib/COMPANIES.server.ts'
3446
3655
 
3447
3656
  /** Answers at /companies/getCompany; a company's page reads it. */
@@ -3460,6 +3669,7 @@ export default GET(
3460
3669
 
3461
3670
  ```ts src/companies/ErrorPage.test.ts
3462
3671
  import { expect, test } from 'bun:test'
3672
+
3463
3673
  import { createApp } from '@abide/abide/server'
3464
3674
 
3465
3675
  test('a missing company is a 404, painted by error.abide', async () => {
@@ -3483,8 +3693,9 @@ The refusal has to come in the page's first turn to set the status. That is why
3483
3693
  ```abide src/companies/lib/ExportButton.abide
3484
3694
  <script>
3485
3695
  import { isFailed, reactive, refresh } from '@abide/abide'
3486
- import exportCompanies from '../exportCompanies.ts'
3487
- import reportedErrors from '../reportedErrors.ts'
3696
+
3697
+ import exportCompanies from '../exportCompanies.handler.ts'
3698
+ import reportedErrors from '../reportedErrors.handler.ts'
3488
3699
 
3489
3700
  let told = reactive('—')
3490
3701
  const reported = reactive(() => reportedErrors())
@@ -3509,7 +3720,7 @@ async function runExport() {
3509
3720
  </ul>
3510
3721
  ```
3511
3722
 
3512
- ```ts src/companies/exportCompanies.ts
3723
+ ```ts src/companies/exportCompanies.handler.ts
3513
3724
  import { POST } from '@abide/abide/server'
3514
3725
 
3515
3726
  /** Answers at /companies/exportCompanies; the export button calls it. */
@@ -3525,6 +3736,7 @@ export default POST(
3525
3736
  ```abide src/companies/layout.abide
3526
3737
  <script server>
3527
3738
  import { onError } from '@abide/abide/server'
3739
+
3528
3740
  import { tracker } from './lib/tracker.server.ts'
3529
3741
 
3530
3742
  onError((error) => tracker.capture(error))
@@ -3543,11 +3755,12 @@ let note = reactive('')
3543
3755
  <input bind:value={note} placeholder="Note for the call">
3544
3756
  </header>
3545
3757
 
3546
- <slot/>
3758
+ <slot />
3547
3759
  ```
3548
3760
 
3549
3761
  ```ts src/companies/lib/ExportButton.test.ts
3550
3762
  import { expect, test } from 'bun:test'
3763
+
3551
3764
  import { createApp } from '@abide/abide/server'
3552
3765
 
3553
3766
  test('the caller gets a bare 500, and onError gets the reason', async () => {
@@ -3576,7 +3789,7 @@ test('the caller gets a bare 500, and onError gets the reason', async () => {
3576
3789
 
3577
3790
  ```abide src/companies/lib/SheetPreview.abide
3578
3791
  <script>
3579
- import previewCompany from '../previewCompany.ts'
3792
+ import previewCompany from '../previewCompany.handler.ts'
3580
3793
  </script>
3581
3794
 
3582
3795
  <iframe src={previewCompany.url({ id: 'hopper-and-co' })}
@@ -3584,10 +3797,11 @@ import previewCompany from '../previewCompany.ts'
3584
3797
  class="h-48 w-full rounded-md border border-rule"></iframe>
3585
3798
  ```
3586
3799
 
3587
- ```ts src/companies/previewCompany.ts
3800
+ ```ts src/companies/previewCompany.handler.ts
3588
3801
  import { GET, notFound, page, render } from '@abide/abide/server'
3589
- import CompanySheet from './lib/CompanySheet.abide'
3802
+
3590
3803
  import { COMPANIES } from './lib/COMPANIES.server.ts'
3804
+ import CompanySheet from './lib/CompanySheet.abide'
3591
3805
 
3592
3806
  /** Answers at /companies/previewCompany: an account sheet, as its own page. */
3593
3807
  export default GET(
@@ -3610,19 +3824,20 @@ const { company } = props<{
3610
3824
  </script>
3611
3825
 
3612
3826
  <html lang="en">
3613
- <body style="margin: 0; padding: 1.25rem; background: #fff; color: #1d2a28;
3827
+ <body style="margin: 0; padding: 1.25rem; background: #fff; color: #1d2a28;
3614
3828
  font: 14px/1.5 system-ui, sans-serif">
3615
- <h1 style="margin: 0 0 0.5rem; font-size: 1.25rem">{company.name}</h1>
3829
+ <h1 style="margin: 0 0 0.5rem; font-size: 1.25rem">{company.name}</h1>
3616
3830
  <table>
3617
- <tr><th align="left">Contact</th><td>{company.contact}</td></tr>
3618
- <tr><th align="left">Pipeline</th><td>{company.pipeline}</td></tr>
3619
- </table>
3620
- </body>
3831
+ <tr><th align="left">Contact</th><td>{company.contact}</td></tr>
3832
+ <tr><th align="left">Pipeline</th><td>{company.pipeline}</td></tr>
3833
+ </table>
3834
+ </body>
3621
3835
  </html>
3622
3836
  ```
3623
3837
 
3624
3838
  ```ts src/companies/lib/SheetPreview.test.ts
3625
3839
  import { expect, test } from 'bun:test'
3840
+
3626
3841
  import { createApp } from '@abide/abide/server'
3627
3842
 
3628
3843
  test('the sheet is a document of its own', async () => {
@@ -3677,8 +3892,9 @@ Everything in this chapter is in `src/account` of the chapter app, beside `src/c
3677
3892
  ```abide src/account/lib/SignIn.abide
3678
3893
  <script>
3679
3894
  import { principal, reactive } from '@abide/abide'
3680
- import signIn from '../signIn.ts'
3681
- import signOut from '../signOut.ts'
3895
+
3896
+ import signIn from '../signIn.handler.ts'
3897
+ import signOut from '../signOut.handler.ts'
3682
3898
 
3683
3899
  let email = reactive('maya@crm.example')
3684
3900
  </script>
@@ -3694,9 +3910,10 @@ let email = reactive('maya@crm.example')
3694
3910
  </div>
3695
3911
  ```
3696
3912
 
3697
- ```ts src/account/signIn.ts
3913
+ ```ts src/account/signIn.handler.ts
3698
3914
  import { principal, refuse } from '@abide/abide'
3699
3915
  import { onPrincipal, POST } from '@abide/abide/server'
3916
+
3700
3917
  import { TEAM } from './lib/TEAM.server.ts'
3701
3918
 
3702
3919
  /** Seals the member's id into the cookie, and nothing else. */
@@ -3717,7 +3934,7 @@ onPrincipal((claims) => {
3717
3934
  })
3718
3935
  ```
3719
3936
 
3720
- ```ts src/account/signOut.ts
3937
+ ```ts src/account/signOut.handler.ts
3721
3938
  import { principal } from '@abide/abide'
3722
3939
  import { POST } from '@abide/abide/server'
3723
3940
 
@@ -3726,6 +3943,7 @@ export default POST(() => principal.clear(), { description: 'Signs out.' })
3726
3943
 
3727
3944
  ```ts src/account/lib/SignIn.test.ts
3728
3945
  import { expect, test } from 'bun:test'
3946
+
3729
3947
  import { principal } from '@abide/abide'
3730
3948
  import { createApp } from '@abide/abide/server'
3731
3949
 
@@ -3765,7 +3983,7 @@ test('a request carrying the cookie is signed in', async () => {
3765
3983
  })
3766
3984
  ```
3767
3985
 
3768
- `principal.set(claims)` seals the claims into an `HttpOnly` cookie, and every request that carries it is that member's. `principal.clear()` signs them out. Both run in a handler, because the cookie is written on its answer. The `onPrincipal` at the foot of `signIn.ts` is the next card's.
3986
+ `principal.set(claims)` seals the claims into an `HttpOnly` cookie, and every request that carries it is that member's. `principal.clear()` signs them out. Both run in a handler, because the cookie is written on its answer. The `onPrincipal` at the foot of `signIn.handler.ts` is the next card's.
3769
3987
 
3770
3988
  The answer also tells the browser the caller changed. Every tab of the site reads `principal` again, so the form's own button flips with nothing passed to it.
3771
3989
 
@@ -3788,6 +4006,7 @@ Each is a reactive value. A template reading one follows it, and on the server e
3788
4006
  ```abide src/account/lib/Badge.abide
3789
4007
  <script>
3790
4008
  import { principal } from '@abide/abide'
4009
+
3791
4010
  import SignInAs from './SignInAs.abide'
3792
4011
  </script>
3793
4012
 
@@ -3801,9 +4020,10 @@ import SignInAs from './SignInAs.abide'
3801
4020
  </ul>
3802
4021
  ```
3803
4022
 
3804
- ```ts src/account/signIn.ts
4023
+ ```ts src/account/signIn.handler.ts
3805
4024
  import { principal, refuse } from '@abide/abide'
3806
4025
  import { onPrincipal, POST } from '@abide/abide/server'
4026
+
3807
4027
  import { TEAM } from './lib/TEAM.server.ts'
3808
4028
 
3809
4029
  /** Seals the member's id into the cookie, and nothing else. */
@@ -3826,6 +4046,7 @@ onPrincipal((claims) => {
3826
4046
 
3827
4047
  ```ts src/account/lib/Badge.test.ts
3828
4048
  import { expect, test } from 'bun:test'
4049
+
3829
4050
  import { principal } from '@abide/abide'
3830
4051
  import { createApp } from '@abide/abide/server'
3831
4052
 
@@ -3861,10 +4082,10 @@ const density = reactive(() => cookies().get('density') ?? 'comfortable')
3861
4082
  </script>
3862
4083
 
3863
4084
  <div class="tabs">
3864
- {#for one of DENSITIES}
3865
- <button aria-selected={density === one}
3866
- onclick={() => cookies().set('density', one)}>{one}</button>
3867
- {/for}
4085
+ {#for one of DENSITIES}
4086
+ <button aria-selected={density === one}
4087
+ onclick={() => cookies().set('density', one)}>{one}</button>
4088
+ {/for}
3868
4089
  </div>
3869
4090
 
3870
4091
  <ul class={density === 'compact' ? 'fields gap-0' : 'fields gap-flow'}>
@@ -3875,6 +4096,7 @@ const density = reactive(() => cookies().get('density') ?? 'comfortable')
3875
4096
 
3876
4097
  ```ts src/account/lib/Density.test.ts
3877
4098
  import { expect, test } from 'bun:test'
4099
+
3878
4100
  import { createApp } from '@abide/abide/server'
3879
4101
 
3880
4102
  test('the server paints the density the cookie holds', async () => {
@@ -3898,7 +4120,8 @@ test('the server paints the density the cookie holds', async () => {
3898
4120
  ```abide src/account/lib/ContactCard.abide
3899
4121
  <script>
3900
4122
  import { principal, reactive, watch } from '@abide/abide'
3901
- import getContact from '../getContact.ts'
4123
+
4124
+ import getContact from '../getContact.handler.ts'
3902
4125
  import SignInAs from './SignInAs.abide'
3903
4126
 
3904
4127
  const contact = reactive(() => getContact())
@@ -3919,7 +4142,7 @@ const dealValue = () => contact.dealValue ?? 'admins only'
3919
4142
  </ul>
3920
4143
  ```
3921
4144
 
3922
- ```ts src/account/getContact.ts
4145
+ ```ts src/account/getContact.handler.ts
3923
4146
  import { principal } from '@abide/abide'
3924
4147
  import { GET } from '@abide/abide/server'
3925
4148
 
@@ -3944,8 +4167,10 @@ export default GET(
3944
4167
 
3945
4168
  ```ts src/account/lib/ContactCard.test.ts
3946
4169
  import { expect, test } from 'bun:test'
4170
+
3947
4171
  import { createApp } from '@abide/abide/server'
3948
- import getContact from '../getContact.ts'
4172
+
4173
+ import getContact from '../getContact.handler.ts'
3949
4174
 
3950
4175
  test('an admin reads the deal value', async () => {
3951
4176
  await using app = await createApp()
@@ -3976,7 +4201,8 @@ An answer read under one member is not read again on its own when another signs
3976
4201
  ```abide src/account/lib/ExportContacts.abide
3977
4202
  <script>
3978
4203
  import { reactive } from '@abide/abide'
3979
- import exportContacts from '../exportContacts.ts'
4204
+
4205
+ import exportContacts from '../exportContacts.handler.ts'
3980
4206
  import SignInAs from './SignInAs.abide'
3981
4207
 
3982
4208
  let status = reactive('')
@@ -4000,9 +4226,10 @@ async function exportAll() {
4000
4226
  </div>
4001
4227
  ```
4002
4228
 
4003
- ```ts src/account/exportContacts.ts
4229
+ ```ts src/account/exportContacts.handler.ts
4004
4230
  import { principal, refuse } from '@abide/abide'
4005
4231
  import { POST } from '@abide/abide/server'
4232
+
4006
4233
  import { CONTACTS } from '../contacts/lib/CONTACTS.server.ts'
4007
4234
 
4008
4235
  export default POST(async () => CONTACTS, {
@@ -4020,8 +4247,10 @@ export default POST(async () => CONTACTS, {
4020
4247
 
4021
4248
  ```ts src/account/lib/ExportContacts.test.ts
4022
4249
  import { expect, test } from 'bun:test'
4250
+
4023
4251
  import { createApp } from '@abide/abide/server'
4024
- import exportContacts from '../exportContacts.ts'
4252
+
4253
+ import exportContacts from '../exportContacts.handler.ts'
4025
4254
 
4026
4255
  test('an admin exports every contact', async () => {
4027
4256
  await using app = await createApp()
@@ -4079,7 +4308,8 @@ Filed in the root `src/layout.abide`, a rung runs before routing, over every req
4079
4308
  ```abide src/account/lib/PartnerStatement.abide
4080
4309
  <script server>
4081
4310
  import { GET } from '@abide/abide/server'
4082
- import getStatement from '../getStatement.ts'
4311
+
4312
+ import getStatement from '../getStatement.handler.ts'
4083
4313
 
4084
4314
  // Another site's browser, asking for the statement under its own origin.
4085
4315
  const askAs = GET(
@@ -4114,7 +4344,7 @@ const allowed = reactive(() => askAs({ origin }))
4114
4344
  </ul>
4115
4345
  ```
4116
4346
 
4117
- ```ts src/account/getStatement.ts
4347
+ ```ts src/account/getStatement.handler.ts
4118
4348
  import { GET } from '@abide/abide/server'
4119
4349
 
4120
4350
  /** Read from the bookkeeping partner's own pages, and from no one else's. */
@@ -4126,6 +4356,7 @@ export default GET(() => ({ account: 'A-1042', balance: '$2,140.00' }), {
4126
4356
 
4127
4357
  ```ts src/account/lib/PartnerStatement.test.ts
4128
4358
  import { expect, test } from 'bun:test'
4359
+
4129
4360
  import { createApp } from '@abide/abide/server'
4130
4361
 
4131
4362
  function askAs(origin: string): Request {
@@ -4162,7 +4393,8 @@ abide answers the `OPTIONS` preflight a cross-origin `POST` sends, outside every
4162
4393
  ```abide src/account/lib/SalesReport.abide
4163
4394
  <script>
4164
4395
  import { reactive } from '@abide/abide'
4165
- import salesReport from '../salesReport.ts'
4396
+
4397
+ import salesReport from '../salesReport.handler.ts'
4166
4398
 
4167
4399
  const REGIONS = ['north', 'south']
4168
4400
 
@@ -4177,10 +4409,12 @@ function refusal() {
4177
4409
  </script>
4178
4410
 
4179
4411
  <div class="tabs">
4180
- {#for one of REGIONS}
4181
- <button aria-selected={region === one}
4182
- onclick={() => { region = one }}>{one}</button>
4183
- {/for}
4412
+ {#for one of REGIONS}
4413
+ <button aria-selected={region === one}
4414
+ onclick={() => {
4415
+ region = one
4416
+ }}>{one}</button>
4417
+ {/for}
4184
4418
  </div>
4185
4419
 
4186
4420
  <p class="status">{refusal()}</p>
@@ -4191,7 +4425,7 @@ function refusal() {
4191
4425
  </ul>
4192
4426
  ```
4193
4427
 
4194
- ```ts src/account/salesReport.ts
4428
+ ```ts src/account/salesReport.handler.ts
4195
4429
  import { GET } from '@abide/abide/server'
4196
4430
 
4197
4431
  const BY_REGION: Record<string, { week: string; total: string }[]> = {
@@ -4218,7 +4452,8 @@ export default GET(
4218
4452
 
4219
4453
  ```ts src/account/lib/SalesReport.test.ts
4220
4454
  import { expect, test } from 'bun:test'
4221
- import salesReport from '../salesReport.ts'
4455
+
4456
+ import salesReport from '../salesReport.handler.ts'
4222
4457
 
4223
4458
  test('a report slower than the timeout runs, a row at a time', async () => {
4224
4459
  const rows = []
@@ -4261,7 +4496,8 @@ None of these is a rate limit. A caller sending many small, fast requests passes
4261
4496
  ```abide src/account/lib/SyncContact.abide
4262
4497
  <script>
4263
4498
  import { reactive } from '@abide/abide'
4264
- import syncContact from '../syncContact.ts'
4499
+
4500
+ import syncContact from '../syncContact.handler.ts'
4265
4501
 
4266
4502
  let status = reactive('')
4267
4503
 
@@ -4283,9 +4519,10 @@ async function sync() {
4283
4519
  </div>
4284
4520
  ```
4285
4521
 
4286
- ```ts src/account/syncContact.ts
4522
+ ```ts src/account/syncContact.handler.ts
4287
4523
  import { refuse } from '@abide/abide'
4288
4524
  import { POST, reachable } from '@abide/abide/server'
4525
+
4289
4526
  import { billingOrigin } from './lib/billing.server.ts'
4290
4527
 
4291
4528
  /** Refuses at once while billing is down, rather than waiting on every call. */
@@ -4308,6 +4545,7 @@ export default POST(
4308
4545
 
4309
4546
  ```ts src/account/lib/SyncContact.test.ts
4310
4547
  import { expect, test } from 'bun:test'
4548
+
4311
4549
  import { reachable } from '@abide/abide/server'
4312
4550
 
4313
4551
  test('a host is asked once, however many calls ask after it', async () => {
@@ -4345,7 +4583,8 @@ The answer can be a minute old. The call that follows a `true` can still fail, a
4345
4583
  ```abide src/account/lib/AddContact.abide
4346
4584
  <script>
4347
4585
  import { reactive } from '@abide/abide'
4348
- import addContact from '../addContact.ts'
4586
+
4587
+ import addContact from '../addContact.handler.ts'
4349
4588
 
4350
4589
  let added = reactive<{ status: number; location: string | null }>()
4351
4590
 
@@ -4356,7 +4595,9 @@ async function add() {
4356
4595
  </script>
4357
4596
 
4358
4597
  <div class="row">
4359
- <button onclick={() => { added = add() }}>Add Katherine Johnson</button>
4598
+ <button onclick={() => {
4599
+ added = add()
4600
+ }}>Add Katherine Johnson</button>
4360
4601
  </div>
4361
4602
 
4362
4603
  <ul class="fields">
@@ -4367,7 +4608,7 @@ async function add() {
4367
4608
  </ul>
4368
4609
  ```
4369
4610
 
4370
- ```ts src/account/addContact.ts
4611
+ ```ts src/account/addContact.handler.ts
4371
4612
  import { POST, response } from '@abide/abide/server'
4372
4613
 
4373
4614
  let nextId = 4031
@@ -4387,6 +4628,7 @@ export default POST(
4387
4628
 
4388
4629
  ```ts src/account/lib/AddContact.test.ts
4389
4630
  import { expect, test } from 'bun:test'
4631
+
4390
4632
  import { createApp } from '@abide/abide/server'
4391
4633
 
4392
4634
  test('adding a contact answers 201, and where it lives', async () => {
@@ -4427,9 +4669,13 @@ let refused = reactive('—')
4427
4669
  function addWidget() {
4428
4670
  const widget = document.createElement('script')
4429
4671
  widget.src = 'https://widget.example/chat.js'
4430
- document.addEventListener('securitypolicyviolation', (event) => {
4431
- refused = `${event.blockedURI} by ${event.violatedDirective}`
4432
- }, { once: true })
4672
+ document.addEventListener(
4673
+ 'securitypolicyviolation',
4674
+ (event) => {
4675
+ refused = `${event.blockedURI} by ${event.violatedDirective}`
4676
+ },
4677
+ { once: true },
4678
+ )
4433
4679
  document.head.append(widget)
4434
4680
  }
4435
4681
  </script>
@@ -4452,18 +4698,19 @@ middleware(csp())
4452
4698
  </script>
4453
4699
 
4454
4700
  <html lang="en">
4455
- <head>
4456
- <meta charset="utf-8" />
4457
- <meta name="viewport" content="width=device-width, initial-scale=1" />
4458
- </head>
4701
+ <head>
4702
+ <meta charset="utf-8" />
4703
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4704
+ </head>
4459
4705
  <body>
4460
- <slot/>
4461
- </body>
4706
+ <slot />
4707
+ </body>
4462
4708
  </html>
4463
4709
  ```
4464
4710
 
4465
4711
  ```ts src/account/lib/Widget.test.ts
4466
4712
  import { expect, test } from 'bun:test'
4713
+
4467
4714
  import { createApp } from '@abide/abide/server'
4468
4715
 
4469
4716
  test("a page admits the app's own scripts, and no other origin's", async () => {
@@ -4516,7 +4763,8 @@ Everything in this chapter lands on one page of the CRM, `/ops`, under `src/ops`
4516
4763
  ```abide src/ops/lib/Retries.abide
4517
4764
  <script>
4518
4765
  import { reactive } from '@abide/abide'
4519
- import getRetries from '../getRetries.ts'
4766
+
4767
+ import getRetries from '../getRetries.handler.ts'
4520
4768
 
4521
4769
  const policy = reactive(() => getRetries())
4522
4770
  </script>
@@ -4527,7 +4775,7 @@ const policy = reactive(() => getRetries())
4527
4775
  </ul>
4528
4776
  ```
4529
4777
 
4530
- ```ts src/ops/getRetries.ts
4778
+ ```ts src/ops/getRetries.handler.ts
4531
4779
  import { config, GET, onConfig } from '@abide/abide/server'
4532
4780
 
4533
4781
  // The default; a deployment's RETRIES overrides it.
@@ -4545,6 +4793,7 @@ RETRIES=5
4545
4793
 
4546
4794
  ```ts src/ops/lib/Retries.test.ts
4547
4795
  import { expect, test } from 'bun:test'
4796
+
4548
4797
  import { createApp } from '@abide/abide/server'
4549
4798
 
4550
4799
  test('RETRIES=5 in .env overrides the default of 3, as a number', async () => {
@@ -4598,7 +4847,8 @@ onConfig(() => ({ RETRIES: 3 }), {
4598
4847
  ```abide src/ops/lib/Pool.abide
4599
4848
  <script>
4600
4849
  import { reactive } from '@abide/abide'
4601
- import getPool from '../getPool.ts'
4850
+
4851
+ import getPool from '../getPool.handler.ts'
4602
4852
 
4603
4853
  const times = reactive(() => getPool())
4604
4854
  </script>
@@ -4609,8 +4859,9 @@ const times = reactive(() => getPool())
4609
4859
  </ul>
4610
4860
  ```
4611
4861
 
4612
- ```ts src/ops/getPool.ts
4862
+ ```ts src/ops/getPool.handler.ts
4613
4863
  import { GET, onStart } from '@abide/abide/server'
4864
+
4614
4865
  import { pool } from './lib/pool.server.ts'
4615
4866
 
4616
4867
  // Connect before the app answers anything; note when it came up.
@@ -4628,6 +4879,7 @@ export default GET(() => ({ connectedAt: pool.connectedAt, upAt: pool.upAt }), {
4628
4879
 
4629
4880
  ```ts src/ops/lib/Pool.test.ts
4630
4881
  import { expect, test } from 'bun:test'
4882
+
4631
4883
  import { createApp } from '@abide/abide/server'
4632
4884
 
4633
4885
  test('the pool is connected before the first request is answered', async () => {
@@ -4667,21 +4919,23 @@ const sync = log.named('sync')
4667
4919
  </div>
4668
4920
 
4669
4921
  <ul class="fields stacked">
4670
- {#for record of log.records().slice(-20)}
4671
- {#if /:(billing|sync)$/.test(record.namespace)}
4672
- <li>
4673
- <span>{record.namespace}</span><strong>{record.message}</strong>
4674
- </li>
4675
- {/if}
4676
- {/for}
4922
+ {#for record of log.records().slice(-20)}
4923
+ {#if /:(billing|sync)$/.test(record.namespace)}
4924
+ <li>
4925
+ <span>{record.namespace}</span><strong>{record.message}</strong>
4926
+ </li>
4927
+ {/if}
4928
+ {/for}
4677
4929
  </ul>
4678
4930
  ```
4679
4931
 
4680
4932
  ```ts src/ops/lib/Console.test.ts
4681
4933
  import { expect, test } from 'bun:test'
4934
+
4682
4935
  import { flushSync } from '@abide/abide'
4683
4936
  import { createApp } from '@abide/abide/server'
4684
4937
  import { mount } from '@abide/harness/abide'
4938
+
4685
4939
  import manifest from '../../../package.json' with { type: 'json' }
4686
4940
  import Console from './Console.abide'
4687
4941
 
@@ -4742,7 +4996,8 @@ abide logs under `abide:`, so `DEBUG=abide:*` turns on the framework and nothing
4742
4996
  ```abide src/ops/lib/Sync.abide
4743
4997
  <script>
4744
4998
  import { reactive } from '@abide/abide'
4745
- import syncContact from '../syncContact.ts'
4999
+
5000
+ import syncContact from '../syncContact.handler.ts'
4746
5001
 
4747
5002
  let synced = reactive<{ trace: string; lines: string[] }>()
4748
5003
 
@@ -4765,8 +5020,9 @@ async function sync() {
4765
5020
  </ul>
4766
5021
  ```
4767
5022
 
4768
- ```ts src/ops/syncContact.ts
5023
+ ```ts src/ops/syncContact.handler.ts
4769
5024
  import { log, POST, trace } from '@abide/abide/server'
5025
+
4770
5026
  import { billing } from './lib/billing.ts'
4771
5027
 
4772
5028
  /** Answers at /ops/syncContact: syncs a contact, with its trace's lines. */
@@ -4797,6 +5053,7 @@ export const billing = {
4797
5053
 
4798
5054
  ```ts src/ops/lib/Sync.test.ts
4799
5055
  import { expect, test } from 'bun:test'
5056
+
4800
5057
  import { createApp } from '@abide/abide/server'
4801
5058
 
4802
5059
  const sync = () =>
@@ -4846,6 +5103,7 @@ With `timing`, a span joins `server-timing` under its name, so the name has to b
4846
5103
  ```abide src/ops/lib/Health.abide
4847
5104
  <script server>
4848
5105
  import { onHealth } from '@abide/abide/server'
5106
+
4849
5107
  import { pool } from './pool.server.ts'
4850
5108
 
4851
5109
  // Lets a page read `account.database` as a string.
@@ -4872,6 +5130,7 @@ const account = health()
4872
5130
 
4873
5131
  ```ts src/ops/lib/Health.test.ts
4874
5132
  import { expect, test } from 'bun:test'
5133
+
4875
5134
  import { createApp } from '@abide/abide/server'
4876
5135
 
4877
5136
  test('the health check carries the database beside the baseline', async () => {
@@ -4918,8 +5177,10 @@ import { online } from '@abide/abide'
4918
5177
 
4919
5178
  ```ts src/ops/lib/OfflineBanner.test.ts
4920
5179
  import { expect, test } from 'bun:test'
5180
+
4921
5181
  import { flushSync } from '@abide/abide'
4922
5182
  import { mount } from '@abide/harness/abide'
5183
+
4923
5184
  import OfflineBanner from './OfflineBanner.abide'
4924
5185
 
4925
5186
  test('the banner shows while the browser is offline, and goes', async () => {
@@ -4955,8 +5216,9 @@ watch(() => { if (online) refresh() })
4955
5216
  ```abide src/ops/lib/CallLog.abide
4956
5217
  <script>
4957
5218
  import { outbox, reactive } from '@abide/abide'
4958
- import listCalls from '../listCalls.ts'
4959
- import logCall from '../logCall.ts'
5219
+
5220
+ import listCalls from '../listCalls.handler.ts'
5221
+ import logCall from '../logCall.handler.ts'
4960
5222
 
4961
5223
  const calls = listCalls()
4962
5224
  let note = reactive('')
@@ -4990,8 +5252,9 @@ function log() {
4990
5252
  </ul>
4991
5253
  ```
4992
5254
 
4993
- ```ts src/ops/logCall.ts
5255
+ ```ts src/ops/logCall.handler.ts
4994
5256
  import { POST } from '@abide/abide/server'
5257
+
4995
5258
  import { type Call, calls } from './lib/calls.ts'
4996
5259
 
4997
5260
  /** Logs a call once, however often it is sent: the browser picks its id. */
@@ -5007,10 +5270,12 @@ export default POST(
5007
5270
 
5008
5271
  ```ts src/ops/lib/CallLog.test.ts
5009
5272
  import { expect, test } from 'bun:test'
5273
+
5010
5274
  import { flushSync } from '@abide/abide'
5011
5275
  import { mount } from '@abide/harness/abide'
5012
- import { calls } from './calls.ts'
5276
+
5013
5277
  import CallLog from './CallLog.abide'
5278
+ import { calls } from './calls.ts'
5014
5279
 
5015
5280
  test('a call logged with the network is saved, and nothing waits', async () => {
5016
5281
  using _log = await mount(CallLog({}))
@@ -5026,7 +5291,7 @@ test('a call logged with the network is saved, and nothing waits', async () => {
5026
5291
  })
5027
5292
 
5028
5293
  test('a call sent twice under one id is logged once', async () => {
5029
- const { default: logCall } = await import('../logCall.ts')
5294
+ const { default: logCall } = await import('../logCall.handler.ts')
5030
5295
  const call = { id: 'again', note: 'Asked twice' }
5031
5296
  await logCall(call)
5032
5297
  await logCall(call)
@@ -5067,7 +5332,7 @@ A handler file already carries an address, a method, an argument type and a desc
5067
5332
 
5068
5333
  | Surface | Who reads it | Where |
5069
5334
  | --- | --- | --- |
5070
- | `ui` | a page | `import searchContacts from './searchContacts.ts'` |
5335
+ | `ui` | a page | `import searchContacts from './searchContacts.handler.ts'` |
5071
5336
  | `openapi` | a generated client, an API gateway | `/__abide/openapi.json` |
5072
5337
  | `mcp` | a model | `/__abide/mcp` |
5073
5338
  | `cli` | a person or a script in a terminal | `abide call` |
@@ -5106,8 +5371,9 @@ async function readDocument() {
5106
5371
  {/if}
5107
5372
  ```
5108
5373
 
5109
- ```ts src/assistant/searchContacts.ts
5374
+ ```ts src/assistant/searchContacts.handler.ts
5110
5375
  import { GET } from '@abide/abide/server'
5376
+
5111
5377
  import { CONTACTS } from '../contacts/lib/CONTACTS.server.ts'
5112
5378
 
5113
5379
  /** One handler: an OpenAPI operation, an MCP tool and a command. */
@@ -5120,6 +5386,7 @@ export default GET(
5120
5386
 
5121
5387
  ```ts src/assistant/lib/OpenApiOperation.test.ts
5122
5388
  import { expect, test } from 'bun:test'
5389
+
5123
5390
  import { createApp } from '@abide/abide/server'
5124
5391
 
5125
5392
  test('the handler is an operation, with its sentence and type', async () => {
@@ -5205,8 +5472,9 @@ async function callAsAModel() {
5205
5472
  </ul>
5206
5473
  ```
5207
5474
 
5208
- ```ts src/assistant/searchContacts.ts
5475
+ ```ts src/assistant/searchContacts.handler.ts
5209
5476
  import { GET } from '@abide/abide/server'
5477
+
5210
5478
  import { CONTACTS } from '../contacts/lib/CONTACTS.server.ts'
5211
5479
 
5212
5480
  /** One handler: an OpenAPI operation, an MCP tool and a command. */
@@ -5219,6 +5487,7 @@ export default GET(
5219
5487
 
5220
5488
  ```ts src/assistant/lib/ModelToolCall.test.ts
5221
5489
  import { expect, test } from 'bun:test'
5490
+
5222
5491
  import { createApp } from '@abide/abide/server'
5223
5492
 
5224
5493
  const TOOL = 'get_assistant_searchContacts'
@@ -5265,7 +5534,8 @@ abide mcp --url https://crm.example
5265
5534
  ```abide src/assistant/lib/WithheldExport.abide
5266
5535
  <script>
5267
5536
  import { reactive, url } from '@abide/abide'
5268
- import exportContacts from '../exportContacts.ts'
5537
+
5538
+ import exportContacts from '../exportContacts.handler.ts'
5269
5539
 
5270
5540
  let listed = reactive('—')
5271
5541
  let answers = reactive('—')
@@ -5295,8 +5565,9 @@ async function check() {
5295
5565
  </ul>
5296
5566
  ```
5297
5567
 
5298
- ```ts src/assistant/exportContacts.ts
5568
+ ```ts src/assistant/exportContacts.handler.ts
5299
5569
  import { GET } from '@abide/abide/server'
5570
+
5300
5571
  import { CONTACTS } from '../contacts/lib/CONTACTS.server.ts'
5301
5572
 
5302
5573
  // Every contact at once: for the export button, not for a model.
@@ -5308,6 +5579,7 @@ export default GET(() => CONTACTS, {
5308
5579
 
5309
5580
  ```ts src/assistant/lib/WithheldExport.test.ts
5310
5581
  import { expect, test } from 'bun:test'
5582
+
5311
5583
  import { createApp } from '@abide/abide/server'
5312
5584
 
5313
5585
  test('off the tool list, and still answering on its address', async () => {
@@ -5340,7 +5612,7 @@ The export is not a tool, and its address still answers. `clients` decides what
5340
5612
 
5341
5613
  | Key | Set to `false` |
5342
5614
  | --- | --- |
5343
- | `ui` | No page can import the handler: the import is a compile error naming it. The stand-in in `v1/messages.ts` sets it. |
5615
+ | `ui` | No page can import the handler: the import is a compile error naming it. The stand-in in `v1/messages.handler.ts` sets it. |
5344
5616
  | `openapi` | No operation in `/__abide/openapi.json`. |
5345
5617
  | `mcp` | No tool on `/__abide/mcp`, and none for the assistant. |
5346
5618
  | `cli` | Not callable with `abide call`. |
@@ -5354,7 +5626,8 @@ A handler that returns a whole `Response` starts with `mcp` and `cli` off, havin
5354
5626
  ```abide src/assistant/lib/AssistantChat.abide
5355
5627
  <script>
5356
5628
  import { reactive } from '@abide/abide'
5357
- import askAssistant from '../askAssistant.ts'
5629
+
5630
+ import askAssistant from '../askAssistant.handler.ts'
5358
5631
 
5359
5632
  const QUESTION = 'Which company is Ada at?'
5360
5633
 
@@ -5369,32 +5642,34 @@ const events = reactive(() =>
5369
5642
  <ul class="chat">
5370
5643
  <li data-speaker="you"><span>you</span><p>{QUESTION}</p></li>
5371
5644
  {#for event of events}
5372
- {#if event.type === 'tool'}
5373
- <li><span>tool call</span>
5374
- <p class="font-mono text-chrome">{event.name}</p></li>
5375
- {:else if event.type === 'input'}
5376
- <li><span>with input</span>
5377
- <p class="font-mono text-chrome">
5378
- {JSON.stringify(event.input)}
5379
- </p></li>
5380
- {:else if event.type === 'result'}
5381
- <li><span>your handler answered</span>
5382
- <p class="font-mono text-chrome">{event.text}</p></li>
5383
- {:else if event.type === 'text'}
5384
- <li><span>assistant</span><p>{event.text}</p></li>
5385
- {/if}
5386
- {/for}
5645
+ {#if event.type === 'tool'}
5646
+ <li><span>tool call</span>
5647
+ <p class="font-mono text-chrome">{event.name}</p></li>
5648
+ {:else if event.type === 'input'}
5649
+ <li><span>with input</span>
5650
+ <p class="font-mono text-chrome">
5651
+ {JSON.stringify(event.input)}
5652
+ </p></li>
5653
+ {:else if event.type === 'result'}
5654
+ <li><span>your handler answered</span>
5655
+ <p class="font-mono text-chrome">{event.text}</p></li>
5656
+ {:else if event.type === 'text'}
5657
+ <li><span>assistant</span><p>{event.text}</p></li>
5658
+ {/if}
5659
+ {/for}
5387
5660
  </ul>
5388
5661
 
5389
5662
  <div class="row">
5390
- <button disabled={asked} onclick={() => { asked = true }}>Ask</button>
5663
+ <button disabled={asked} onclick={() => {
5664
+ asked = true
5665
+ }}>Ask</button>
5391
5666
  </div>
5392
5667
  ```
5393
5668
 
5394
- ```ts src/assistant/askAssistant.ts
5669
+ ```ts src/assistant/askAssistant.handler.ts
5395
5670
  import {
5396
- type AssistantMessage,
5397
5671
  assistant,
5672
+ type AssistantMessage,
5398
5673
  POST,
5399
5674
  type Provider,
5400
5675
  } from '@abide/abide/server'
@@ -5429,6 +5704,7 @@ export default POST(
5429
5704
 
5430
5705
  ```ts src/assistant/lib/AssistantChat.test.ts
5431
5706
  import { expect, test } from 'bun:test'
5707
+
5432
5708
  import { createApp } from '@abide/abide/server'
5433
5709
 
5434
5710
  test('the model calls the CRM search, through the app', async () => {
@@ -5483,20 +5759,21 @@ test('the model calls the CRM search, through the app', async () => {
5483
5759
  ```abide src/assistant/lib/ClaudeApiRequest.abide
5484
5760
  <script>
5485
5761
  import { reactive } from '@abide/abide'
5486
- import askClaude from '../askClaude.ts'
5762
+
5763
+ import askClaude from '../askClaude.handler.ts'
5487
5764
 
5488
5765
  const QUESTION = 'Which company is Ada at?'
5489
5766
 
5490
5767
  let asked = reactive(false)
5491
5768
  const events = reactive(() =>
5492
- asked
5493
- ? askClaude({ messages: [{ role: 'user', content: QUESTION }] })
5494
- : [],
5769
+ asked ? askClaude({ messages: [{ role: 'user', content: QUESTION }] }) : [],
5495
5770
  )
5496
5771
  </script>
5497
5772
 
5498
5773
  <div class="row">
5499
- <button disabled={asked} onclick={() => { asked = true }}>Ask</button>
5774
+ <button disabled={asked} onclick={() => {
5775
+ asked = true
5776
+ }}>Ask</button>
5500
5777
  <span class="status">{QUESTION}</span>
5501
5778
  </div>
5502
5779
 
@@ -5508,15 +5785,16 @@ const events = reactive(() =>
5508
5785
  {/for}
5509
5786
  ```
5510
5787
 
5511
- ```ts src/assistant/askClaude.ts
5788
+ ```ts src/assistant/askClaude.handler.ts
5512
5789
  import {
5513
- type AssistantMessage,
5514
5790
  assistant,
5791
+ type AssistantMessage,
5515
5792
  claudeApi,
5516
5793
  POST,
5517
5794
  request,
5518
5795
  } from '@abide/abide/server'
5519
- import standIn from './v1/messages.ts'
5796
+
5797
+ import standIn from './v1/messages.handler.ts'
5520
5798
 
5521
5799
  export default POST(
5522
5800
  async function* ({ messages }: { messages: AssistantMessage[] }) {
@@ -5537,7 +5815,7 @@ export default POST(
5537
5815
  )
5538
5816
  ```
5539
5817
 
5540
- ```ts src/assistant/v1/messages.ts
5818
+ ```ts src/assistant/v1/messages.handler.ts
5541
5819
  import { POST, sse } from '@abide/abide/server'
5542
5820
 
5543
5821
  // A stand-in for the Messages API, streaming back the body it was sent.
@@ -5571,6 +5849,7 @@ export default POST(
5571
5849
 
5572
5850
  ```ts src/assistant/lib/ClaudeApiRequest.test.ts
5573
5851
  import { expect, spyOn, test } from 'bun:test'
5852
+
5574
5853
  import { createApp } from '@abide/abide/server'
5575
5854
 
5576
5855
  test('the body is sent as written, with the transcript', async () => {
@@ -5626,8 +5905,9 @@ Leave `baseURL` out and the request goes to Anthropic. `claudeApi` reads no key
5626
5905
  ```abide src/assistant/lib/ClaudeCodeBridge.abide
5627
5906
  <script>
5628
5907
  import { reactive } from '@abide/abide'
5629
- import askClaudeCode from '../askClaudeCode.ts'
5630
- import bridge from '../bridge.ts'
5908
+
5909
+ import askClaudeCode from '../askClaudeCode.handler.ts'
5910
+ import bridge from '../bridge.handler.ts'
5631
5911
 
5632
5912
  const QUESTION = 'Which company is Grace at?'
5633
5913
 
@@ -5657,24 +5937,28 @@ const answer = reactive(() => {
5657
5937
  text-chrome">{pairing.at(-1)?.command}</pre>
5658
5938
  {:else}
5659
5939
  <div class="row">
5660
- <button onclick={() => { connecting = true }}>Connect</button>
5940
+ <button onclick={() => {
5941
+ connecting = true
5942
+ }}>Connect</button>
5661
5943
  </div>
5662
5944
  {/if}
5663
5945
 
5664
5946
  <ul class="chat">
5665
5947
  <li data-speaker="you"><span>you</span><p>{QUESTION}</p></li>
5666
5948
  {#if answer}
5667
- <li><span>claude code</span><p>{answer}</p></li>
5668
- {/if}
5949
+ <li><span>claude code</span><p>{answer}</p></li>
5950
+ {/if}
5669
5951
  </ul>
5670
5952
 
5671
5953
  <div class="row">
5672
- <button onclick={() => { asked += 1 }}>Ask</button>
5954
+ <button onclick={() => {
5955
+ asked += 1
5956
+ }}>Ask</button>
5673
5957
  <span class="status">{events.error()?.message ?? ''}</span>
5674
5958
  </div>
5675
5959
  ```
5676
5960
 
5677
- ```ts src/assistant/bridge.ts
5961
+ ```ts src/assistant/bridge.handler.ts
5678
5962
  import { claudeCode, GET } from '@abide/abide/server'
5679
5963
 
5680
5964
  // This browser's pairing, for as long as the page watches it.
@@ -5686,10 +5970,10 @@ export default GET(() => claudeCode.bridge(), {
5686
5970
  })
5687
5971
  ```
5688
5972
 
5689
- ```ts src/assistant/askClaudeCode.ts
5973
+ ```ts src/assistant/askClaudeCode.handler.ts
5690
5974
  import {
5691
- type AssistantMessage,
5692
5975
  assistant,
5976
+ type AssistantMessage,
5693
5977
  claudeCode,
5694
5978
  POST,
5695
5979
  } from '@abide/abide/server'
@@ -5708,6 +5992,7 @@ export default POST(
5708
5992
 
5709
5993
  ```ts src/assistant/lib/ClaudeCodeBridge.test.ts
5710
5994
  import { expect, test } from 'bun:test'
5995
+
5711
5996
  import { createApp } from '@abide/abide/server'
5712
5997
 
5713
5998
  // A request from one browser: the cookie a page is handed on its first visit.
@@ -5821,7 +6106,8 @@ Each card is a file of the reports page, beside the test or the bench that prove
5821
6106
  ```abide src/reports/lib/PipelineSummary.abide
5822
6107
  <script>
5823
6108
  import { reactive } from '@abide/abide'
5824
- import getSummary from '../getSummary.ts'
6109
+
6110
+ import getSummary from '../getSummary.handler.ts'
5825
6111
 
5826
6112
  const summary = reactive(() => getSummary())
5827
6113
  </script>
@@ -5832,8 +6118,9 @@ const summary = reactive(() => getSummary())
5832
6118
  </ul>
5833
6119
  ```
5834
6120
 
5835
- ```ts src/reports/getSummary.ts
6121
+ ```ts src/reports/getSummary.handler.ts
5836
6122
  import { GET } from '@abide/abide/server'
6123
+
5837
6124
  import { DEALS } from './lib/DEALS.server.ts'
5838
6125
 
5839
6126
  /** Answers at /reports/getSummary; the summary tiles and their test read it. */
@@ -5854,6 +6141,7 @@ export default GET(
5854
6141
 
5855
6142
  ```ts src/reports/lib/PipelineSummary.test.ts
5856
6143
  import { expect, test } from 'bun:test'
6144
+
5857
6145
  import { createApp } from '@abide/abide/server'
5858
6146
 
5859
6147
  // Run from the app's own directory: `createApp` finds the app there.
@@ -5889,6 +6177,7 @@ A `.test.ts` has no address, so a test sits beside the file it tests and is neve
5889
6177
  ```abide src/reports/lib/ClosedTiles.abide
5890
6178
  <script>
5891
6179
  import { reactive } from '@abide/abide'
6180
+
5892
6181
  import { closedTotals } from './closedTotals.ts'
5893
6182
 
5894
6183
  let days = reactive(7)
@@ -5931,8 +6220,10 @@ export function closedTotals(days: number) {
5931
6220
 
5932
6221
  ```ts src/reports/lib/ClosedTiles.test.ts
5933
6222
  import { expect, test } from 'bun:test'
6223
+
5934
6224
  import { flushSync } from '@abide/abide'
5935
6225
  import { measure, mount } from '@abide/harness/abide'
6226
+
5936
6227
  import ClosedTiles from './ClosedTiles.abide'
5937
6228
 
5938
6229
  test('a range switch rewrites two numbers and builds nothing', async () => {
@@ -5988,6 +6279,7 @@ The emulated DOM is JavaScript, so a count from it holds in a browser and a mill
5988
6279
  ```abide src/reports/lib/StageTiles.abide
5989
6280
  <script>
5990
6281
  import { reactive } from '@abide/abide'
6282
+
5991
6283
  import { summarizeStage } from './summarizeStage.ts'
5992
6284
 
5993
6285
  let stage = reactive('qualified')
@@ -6044,9 +6336,11 @@ export function summarizeStage(stage: string) {
6044
6336
 
6045
6337
  ```ts src/reports/lib/StageTiles.test.ts
6046
6338
  import { expect, test } from 'bun:test'
6339
+
6047
6340
  import { flushSync } from '@abide/abide'
6048
6341
  import { mount } from '@abide/harness/abide'
6049
6342
  import { measure, publishedBy } from '@abide/harness/measure'
6343
+
6050
6344
  import StageTiles from './StageTiles.abide'
6051
6345
 
6052
6346
  test('three tiles read one summary, worked out once per switch', async () => {
@@ -6072,6 +6366,7 @@ The three tiles read one derived value, so the summary runs once however many ti
6072
6366
  ```abide src/reports/lib/WonDeals.abide
6073
6367
  <script>
6074
6368
  import { reactive } from '@abide/abide'
6369
+
6075
6370
  import { money } from './money.ts'
6076
6371
 
6077
6372
  let won = reactive([{ company: 'Analytical Engines', amount: 8000 }])
@@ -6111,9 +6406,11 @@ export const money = {
6111
6406
 
6112
6407
  ```ts src/reports/lib/WonDeals.test.ts
6113
6408
  import { expect, spyOn } from 'bun:test'
6409
+
6114
6410
  import { flushSync } from '@abide/abide'
6115
6411
  import { mount } from '@abide/harness/abide'
6116
6412
  import { gate } from '@abide/harness/gate'
6413
+
6117
6414
  import { money } from './money.ts'
6118
6415
  import WonDeals from './WonDeals.abide'
6119
6416
 
@@ -6169,6 +6466,7 @@ With that set, every gate runs a second time with the revert installed, and that
6169
6466
  ```abide src/reports/lib/Forecast.abide
6170
6467
  <script>
6171
6468
  import { reactive } from '@abide/abide'
6469
+
6172
6470
  import { advance, type Deal, forecast } from './forecast.ts'
6173
6471
 
6174
6472
  let deals = reactive<Deal[]>([
@@ -6225,8 +6523,10 @@ export function advance(deals: Deal[], index: number): Deal[] {
6225
6523
 
6226
6524
  ```ts src/reports/lib/Forecast.test.ts
6227
6525
  import { expect, test } from 'bun:test'
6526
+
6228
6527
  import { flushSync } from '@abide/abide'
6229
6528
  import { mount } from '@abide/harness/abide'
6529
+
6230
6530
  import Forecast from './Forecast.abide'
6231
6531
 
6232
6532
  test('advancing a deal moves the forecast by its weighted amount', async () => {
@@ -6242,7 +6542,8 @@ test('advancing a deal moves the forecast by its weighted amount', async () => {
6242
6542
 
6243
6543
  ```ts src/reports/lib/Forecast.bench.ts
6244
6544
  import { reactive } from '@abide/abide'
6245
- import { type Batched, batch } from '@abide/harness/report'
6545
+ import { batch, type Batched } from '@abide/harness/report'
6546
+
6246
6547
  import { advance, type Deal, forecast } from './forecast.ts'
6247
6548
 
6248
6549
  // The forecast as the card keeps it, over a pipeline of `count` deals.
@@ -6304,6 +6605,7 @@ The `bench` script is `harness-bench`, which hands `runBenches` every `*.bench.t
6304
6605
  ```abide src/reports/lib/TopDeals.abide
6305
6606
  <script>
6306
6607
  import { reactive } from '@abide/abide'
6608
+
6307
6609
  import { topFive } from './rankDeals.ts'
6308
6610
 
6309
6611
  let by = reactive<'amount' | 'idle'>('amount')
@@ -6394,6 +6696,7 @@ A faster sort saves at most the part of the click the sort was. Find that part b
6394
6696
  ```abide src/reports/lib/DealsExport.abide
6395
6697
  <script>
6396
6698
  import { props } from '@abide/abide'
6699
+
6397
6700
  import { dealsOf, type ExportedDeal } from './dealsOf.ts'
6398
6701
 
6399
6702
  const { deals = dealsOf(4) } = props<{ deals?: ExportedDeal[] }>()
@@ -6429,8 +6732,10 @@ export function dealsOf(count: number): ExportedDeal[] {
6429
6732
 
6430
6733
  ```ts src/reports/lib/DealsExport.test.ts
6431
6734
  import { expect, test } from 'bun:test'
6735
+
6432
6736
  import { render } from '@abide/abide/server'
6433
6737
  import { bytes } from '@abide/harness/server'
6738
+
6434
6739
  import DealsExport from './DealsExport.abide'
6435
6740
  import { dealsOf } from './dealsOf.ts'
6436
6741
 
@@ -6486,6 +6791,8 @@ abide start --port 8080
6486
6791
 
6487
6792
  `abide check [dir…]` type-checks the `.abide` files as well as the TypeScript, and reports on the line you wrote. `abide lsp` is the same check in your editor. Run it first in CI, because it is the cheapest failure to report.
6488
6793
 
6794
+ `abide format [--check] [dir…]` formats every `.abide` the way `bun fmt` formats everything else: its scripts, styles and expressions under the `[fmt]` options in `bunfig.toml`, and its markup indented by its nesting. `--check` writes nothing and fails where a file would change, and `abide lsp` formats on save the same way.
6795
+
6489
6796
  `abide build` writes content-hashed chunks and a manifest naming them. `abide start [--port <n>]` serves what was built, and builds first where nothing was. Configuration is checked as it starts, so a misconfigured deployment fails to come up.
6490
6797
 
6491
6798
  | Name | What it does for the CRM |
@@ -7696,9 +8003,9 @@ Transports: what is written in place rather than imported.
7696
8003
 
7697
8004
  | File | Declares | Served at | Rules |
7698
8005
  | --- | --- | --- | --- |
7699
- | `users/getUser.ts` | `export default GET(…)` | `/users/getUser` | [16.2](/rulebook#16.2), [16.73](/rulebook#16.73), [19.1](/rulebook#19.1), [19.20](/rulebook#19.20), [23.14](/rulebook#23.14) |
7700
- | `health.ts` | `export default GET(…)` | `/health` | [16.2](/rulebook#16.2), [23.14](/rulebook#23.14) |
7701
- | `media/[id]/save.ts` | `export default POST(…)` | `/media/[id]/save`, `id` reaching its `Args` | [16.2](/rulebook#16.2), [16.65](/rulebook#16.65), [16.66](/rulebook#16.66), [16.67](/rulebook#16.67), [16.69](/rulebook#16.69) |
8006
+ | `users/getUser.handler.ts` | `export default GET(…)` | `/users/getUser` | [16.2](/rulebook#16.2), [16.73](/rulebook#16.73), [19.1](/rulebook#19.1), [19.20](/rulebook#19.20), [23.14](/rulebook#23.14) |
8007
+ | `health.handler.ts` | `export default GET(…)` | `/health` | [16.2](/rulebook#16.2), [23.14](/rulebook#23.14) |
8008
+ | `media/[id]/save.handler.ts` | `export default POST(…)` | `/media/[id]/save`, `id` reaching its `Args` | [16.2](/rulebook#16.2), [16.65](/rulebook#16.65), [16.66](/rulebook#16.66), [16.67](/rulebook#16.67), [16.69](/rulebook#16.69) |
7702
8009
  | `media/[id]/lib/Comments.abide` | `const addComment = POST(…)` in its `<script server>` | `/__abide/handlers/media/id/lib/Comments/addComment` | [16.70](/rulebook#16.70), [16.71](/rulebook#16.71), [16.72](/rulebook#16.72) |
7703
8010
 
7704
8011
  ## Headers abide generates
@@ -7872,14 +8179,14 @@ Generated surfaces: what is written in place rather than imported.
7872
8179
 
7873
8180
  | Name | Signature | Meaning | Rules |
7874
8181
  | --- | --- | --- | --- |
7875
- | `/__abide/openapi.json` | `GET` | The generated OpenAPI document. | [19.21](/rulebook#19.21), [19.22](/rulebook#19.22), [20.5](/rulebook#20.5), [20.6](/rulebook#20.6), [20.7](/rulebook#20.7), [20.8](/rulebook#20.8), [20.11](/rulebook#20.11), [20.13](/rulebook#20.13), [20.15](/rulebook#20.15), [36.7](/rulebook#36.7) |
8182
+ | `/__abide/openapi.json` | `GET` | The generated OpenAPI document. | [19.21](/rulebook#19.21), [19.22](/rulebook#19.22), [20.5](/rulebook#20.5), [20.6](/rulebook#20.6), [20.7](/rulebook#20.7), [20.8](/rulebook#20.8), [20.11](/rulebook#20.11), [20.13](/rulebook#20.13), [20.15](/rulebook#20.15), [20.23](/rulebook#20.23), [36.7](/rulebook#36.7) |
7876
8183
  | `OpenApiDocument` | `JsonValue` | OpenAPI 3.1, whose schema dialect is `JsonSchema`. | [20.6](/rulebook#20.6) |
7877
8184
 
7878
8185
  ## MCP
7879
8186
 
7880
8187
  | Name | Signature | Meaning | Rules |
7881
8188
  | --- | --- | --- | --- |
7882
- | `/__abide/mcp` | `POST` | The streamable-http MCP endpoint. | [19.21](/rulebook#19.21), [19.22](/rulebook#19.22), [20.5](/rulebook#20.5), [20.7](/rulebook#20.7), [20.8](/rulebook#20.8), [20.9](/rulebook#20.9), [20.10](/rulebook#20.10), [20.12](/rulebook#20.12), [20.13](/rulebook#20.13), [20.14](/rulebook#20.14), [20.16](/rulebook#20.16), [20.17](/rulebook#20.17), [20.19](/rulebook#20.19), [20.20](/rulebook#20.20), [20.21](/rulebook#20.21), [20.22](/rulebook#20.22), [36.7](/rulebook#36.7), [16.78](/rulebook#16.78) |
8189
+ | `/__abide/mcp` | `POST` | The streamable-http MCP endpoint. | [19.21](/rulebook#19.21), [19.22](/rulebook#19.22), [20.5](/rulebook#20.5), [20.7](/rulebook#20.7), [20.8](/rulebook#20.8), [20.9](/rulebook#20.9), [20.10](/rulebook#20.10), [20.12](/rulebook#20.12), [20.13](/rulebook#20.13), [20.14](/rulebook#20.14), [20.16](/rulebook#20.16), [20.17](/rulebook#20.17), [20.19](/rulebook#20.19), [20.20](/rulebook#20.20), [20.21](/rulebook#20.21), [20.22](/rulebook#20.22), [20.23](/rulebook#20.23), [36.7](/rulebook#36.7), [16.78](/rulebook#16.78) |
7883
8190
 
7884
8191
  ## Taught in
7885
8192
 
@@ -8799,7 +9106,7 @@ Generated surfaces: what is written in place rather than imported.
8799
9106
 
8800
9107
  | Name | Signature | Meaning | Rules |
8801
9108
  | --- | --- | --- | --- |
8802
- | `ParamsOf<P>` | `& { [K in RequiredNames<P>]: string \| number } & { [K in OptionalNames<P>]?: string \| number } & { [K in RestNames<P>]?: string \| number \| (string \| number)[] }` | What a route literal's segments accept. | [30.3](/rulebook#30.3), [30.4](/rulebook#30.4), [30.5](/rulebook#30.5) |
9109
+ | `ParamsOf<P>` | `& { [K in RequiredNames<P>]: string \| number } & { [K in OptionalNames<P>]?: string \| number } & { [K in RestNames<P>]?: string \| number \| (string \| number)[] }` | What a route literal's segments accept, narrowed where `abide check` declares a matcher's literals. | [30.3](/rulebook#30.3), [30.4](/rulebook#30.4), [30.5](/rulebook#30.5) |
8803
9110
 
8804
9111
  ## Taught in
8805
9112
 
@@ -8817,7 +9124,7 @@ Generated surfaces: what is written in place rather than imported.
8817
9124
 
8818
9125
  | Name | Signature | Meaning | Rules |
8819
9126
  | --- | --- | --- | --- |
8820
- | `RouteParams<P>` | `{ [K in RequiredNames<P>]: string } & { [K in OptionalNames<P>]?: string } & { [K in RestNames<P>]: string[] }` | What a page filed under `P` reads its params as. | [23.9](/rulebook#23.9) |
9127
+ | `RouteParams<P>` | `{ [K in RequiredNames<P>]: string } & { [K in OptionalNames<P>]?: string } & { [K in RestNames<P>]: string[] }` | What a page filed under `P` reads its params as, narrowed where `abide check` declares a matcher's literals. | [23.9](/rulebook#23.9) |
8821
9128
 
8822
9129
  ## Taught in
8823
9130
 
@@ -9046,9 +9353,10 @@ Pages and routing: what is written in place rather than imported.
9046
9353
  | --- | --- | --- |
9047
9354
  | `<head>` | A page's, layout's or component's contribution to the document head. | [39.1](/rulebook#39.1), [39.2](/rulebook#39.2), [39.3](/rulebook#39.3), [39.4](/rulebook#39.4), [39.5](/rulebook#39.5), [39.6](/rulebook#39.6), [39.11](/rulebook#39.11), [39.12](/rulebook#39.12), [39.13](/rulebook#39.13), [39.14](/rulebook#39.14), [39.15](/rulebook#39.15), [39.16](/rulebook#39.16), [39.17](/rulebook#39.17) |
9048
9355
  | `view-transition-name` | What turns view transitions on, in the app's own stylesheet. | [39.7](/rulebook#39.7), [39.8](/rulebook#39.8), [39.9](/rulebook#39.9), [39.10](/rulebook#39.10), [39.18](/rulebook#39.18), [39.19](/rulebook#39.19) |
9049
- | `[name]` | A required dynamic segment. | [16.65](/rulebook#16.65), [23.5](/rulebook#23.5), [30.3](/rulebook#30.3) |
9050
- | `[[name]]` | An optional segment. | [23.5](/rulebook#23.5), [30.4](/rulebook#30.4) |
9051
- | `[...name]` | A rest segment, terminal. | [23.5](/rulebook#23.5), [30.5](/rulebook#30.5) |
9356
+ | `[name]` | A required dynamic segment. | [16.65](/rulebook#16.65), [23.5](/rulebook#23.5), [30.3](/rulebook#30.3), [30.34](/rulebook#30.34), [30.36](/rulebook#30.36) |
9357
+ | `[[name]]` | An optional segment. | [23.5](/rulebook#23.5), [30.4](/rulebook#30.4), [30.34](/rulebook#30.34), [30.36](/rulebook#30.36) |
9358
+ | `[...name]` | A rest segment, terminal. | [23.5](/rulebook#23.5), [30.5](/rulebook#30.5), [30.34](/rulebook#30.34), [30.36](/rulebook#30.36) |
9359
+ | `[name=NAME]` · `[[name=NAME]]` · `[...name=NAME]` | A dynamic segment a param file's matcher decides. | [19.1](/rulebook#19.1), [23.26](/rulebook#23.26), [23.27](/rulebook#23.27), [30.19](/rulebook#30.19), [30.35](/rulebook#30.35), [30.36](/rulebook#30.36), [30.37](/rulebook#30.37) |
9052
9360
  | `/__abide/**` | Every endpoint abide controls. | [20.8](/rulebook#20.8) |
9053
9361
 
9054
9362
  ## Sinks
@@ -9150,9 +9458,10 @@ Configuration: what is written in place rather than imported.
9150
9458
  | `src/public/robots.txt` | The file answered at `/robots.txt`. | [36.11](/rulebook#36.11) |
9151
9459
  | `src/public/icon.png` · `src/public/icon.icns` | The files answered at `/icon.png` and `/icon.icns`, and the desktop app's icon. | [36.11](/rulebook#36.11), [38.79](/rulebook#38.79), [45.15](/rulebook#45.15) |
9152
9460
  | `src/**/` | A directory of the app's, a segment of the route of every file directly in it. | [23.14](/rulebook#23.14), [23.17](/rulebook#23.17), [23.22](/rulebook#23.22) |
9153
- | `**/index.abide` | The page at its directory's route. | [19.1](/rulebook#19.1), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [23.14](/rulebook#23.14), [23.18](/rulebook#23.18) |
9154
- | `**/<name>.abide` | The page at its directory's route and then `<name>`. | [19.1](/rulebook#19.1), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [23.14](/rulebook#23.14), [23.21](/rulebook#23.21) |
9155
- | `**/<name>.ts` | A handler file: the handler its default export declares, at its directory's route and then `<name>`. | [16.2](/rulebook#16.2), [16.28](/rulebook#16.28), [16.73](/rulebook#16.73), [16.74](/rulebook#16.74), [19.1](/rulebook#19.1), [23.14](/rulebook#23.14), [23.22](/rulebook#23.22) |
9461
+ | `**/index.abide` | The page at its directory's route. | [19.1](/rulebook#19.1), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [23.14](/rulebook#23.14), [23.18](/rulebook#23.18), [35.19](/rulebook#35.19) |
9462
+ | `**/<name>.abide` | The page at its directory's route and then `<name>`. | [19.1](/rulebook#19.1), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [23.14](/rulebook#23.14), [23.21](/rulebook#23.21), [35.19](/rulebook#35.19) |
9463
+ | `**/<NAME>.param.ts` | A param file: the matcher a segment naming `NAME` matches by. | [23.22](/rulebook#23.22), [23.24](/rulebook#23.24), [23.25](/rulebook#23.25), [23.26](/rulebook#23.26) |
9464
+ | `**/<name>.handler.ts` | A handler file: the handler its default export declares, at its directory's route and then `<name>`. | [16.2](/rulebook#16.2), [16.28](/rulebook#16.28), [16.73](/rulebook#16.73), [16.74](/rulebook#16.74), [19.1](/rulebook#19.1), [23.14](/rulebook#23.14), [23.22](/rulebook#23.22) |
9156
9465
  | `**/layout.abide` | A layout, rendering every page below its route through a slot, and the hooks over that route. | [35.1](/rulebook#35.1), [35.12](/rulebook#35.12), [35.13](/rulebook#35.13), [37.2](/rulebook#37.2), [41.11](/rulebook#41.11), [41.19](/rulebook#41.19), [41.23](/rulebook#41.23) |
9157
9466
  | `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. | [38.80](/rulebook#38.80), [38.82](/rulebook#38.82), [38.84](/rulebook#38.84), [38.85](/rulebook#38.85), [38.107](/rulebook#38.107) |
9158
9467
  | `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. | [35.15](/rulebook#35.15), [35.16](/rulebook#35.16), [35.17](/rulebook#35.17), [37.1](/rulebook#37.1), [37.3](/rulebook#37.3), [39.4](/rulebook#39.4) |
@@ -10414,7 +10723,8 @@ CLI: what is written in place rather than imported.
10414
10723
  | `abide scaffold <name>` | Writes a starter project. | [38.1](/rulebook#38.1), [38.42](/rulebook#38.42), [38.103](/rulebook#38.103), [38.104](/rulebook#38.104), [38.106](/rulebook#38.106), [38.112](/rulebook#38.112), [38.122](/rulebook#38.122), [38.124](/rulebook#38.124) |
10415
10724
  | `abide run <file> [args…]` | Runs a script under the abide runtime. | [38.2](/rulebook#38.2), [38.57](/rulebook#38.57) |
10416
10725
  | `abide check [dir…]` | The type-checker over a `.abide` tree. | [38.3](/rulebook#38.3), [38.31](/rulebook#38.31), [38.32](/rulebook#38.32), [38.33](/rulebook#38.33), [38.40](/rulebook#38.40), [38.105](/rulebook#38.105), [38.123](/rulebook#38.123) |
10417
- | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | [38.108](/rulebook#38.108), [38.123](/rulebook#38.123) |
10726
+ | `abide format [--check] [dir…]` | The formatter over a `.abide` tree. | [38.25](/rulebook#38.25), [38.125](/rulebook#38.125), [38.126](/rulebook#38.126), [38.127](/rulebook#38.127), [38.128](/rulebook#38.128), [38.129](/rulebook#38.129) |
10727
+ | `abide routes [--json]` | Every address the app answers, the file answering it, what each matcher takes, and where each hook is filed. | [38.108](/rulebook#38.108), [38.123](/rulebook#38.123) |
10418
10728
  | `abide dev [--port <n>]` | Watches the project and keeps the app up. | [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [37.8](/rulebook#37.8), [37.9](/rulebook#37.9), [38.4](/rulebook#38.4), [38.14](/rulebook#38.14), [38.15](/rulebook#38.15), [38.16](/rulebook#38.16), [38.17](/rulebook#38.17), [38.19](/rulebook#38.19), [38.24](/rulebook#38.24), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.28](/rulebook#38.28), [38.30](/rulebook#38.30), [38.35](/rulebook#38.35), [38.36](/rulebook#38.36), [38.37](/rulebook#38.37), [38.38](/rulebook#38.38), [38.41](/rulebook#38.41), [38.43](/rulebook#38.43), [38.58](/rulebook#38.58), [38.63](/rulebook#38.63), [38.64](/rulebook#38.64), [38.65](/rulebook#38.65), [38.94](/rulebook#38.94), [38.96](/rulebook#38.96), [38.97](/rulebook#38.97), [38.123](/rulebook#38.123) |
10419
10729
  | `abide build` | Builds the client into content-hashed chunks and a manifest. | [38.43](/rulebook#38.43), [38.5](/rulebook#38.5), [38.18](/rulebook#38.18), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.30](/rulebook#38.30), [38.66](/rulebook#38.66), [38.94](/rulebook#38.94), [38.123](/rulebook#38.123) |
10420
10730
  | `abide start [--port <n>]` | Boots the built app. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.100](/rulebook#38.100), [38.20](/rulebook#38.20), [38.21](/rulebook#38.21), [38.22](/rulebook#38.22), [38.23](/rulebook#38.23), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.28](/rulebook#38.28), [38.29](/rulebook#38.29), [38.58](/rulebook#38.58), [38.67](/rulebook#38.67), [38.68](/rulebook#38.68), [38.95](/rulebook#38.95), [38.97](/rulebook#38.97), [38.110](/rulebook#38.110), [38.123](/rulebook#38.123) |
@@ -10425,7 +10735,7 @@ CLI: what is written in place rather than imported.
10425
10735
  | `abide watch <address> [args]` | A live subscription to one of the app's followed `GET`s. | [38.69](/rulebook#38.69) |
10426
10736
  | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.44](/rulebook#38.44), [38.45](/rulebook#38.45), [38.46](/rulebook#38.46), [38.47](/rulebook#38.47), [38.48](/rulebook#38.48), [38.49](/rulebook#38.49), [38.50](/rulebook#38.50), [38.51](/rulebook#38.51), [38.52](/rulebook#38.52), [38.56](/rulebook#38.56), [38.93](/rulebook#38.93), [38.97](/rulebook#38.97), [38.123](/rulebook#38.123) |
10427
10737
  | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.70](/rulebook#38.70), [38.71](/rulebook#38.71), [38.72](/rulebook#38.72), [38.73](/rulebook#38.73), [38.74](/rulebook#38.74), [38.75](/rulebook#38.75), [38.76](/rulebook#38.76), [38.77](/rulebook#38.77), [38.78](/rulebook#38.78), [38.79](/rulebook#38.79), [38.80](/rulebook#38.80), [38.81](/rulebook#38.81), [38.82](/rulebook#38.82), [38.83](/rulebook#38.83), [38.84](/rulebook#38.84), [38.85](/rulebook#38.85), [38.86](/rulebook#38.86), [38.87](/rulebook#38.87), [38.88](/rulebook#38.88), [38.89](/rulebook#38.89), [38.90](/rulebook#38.90), [38.91](/rulebook#38.91), [38.92](/rulebook#38.92), [38.101](/rulebook#38.101), [38.102](/rulebook#38.102), [38.123](/rulebook#38.123) |
10428
- | `abide lsp` | The `.abide` language server, over stdio. | [38.11](/rulebook#38.11), [38.34](/rulebook#38.34), [38.39](/rulebook#38.39), [38.98](/rulebook#38.98), [38.116](/rulebook#38.116), [38.117](/rulebook#38.117), [38.118](/rulebook#38.118) |
10738
+ | `abide lsp` | The `.abide` language server, over stdio. | [38.11](/rulebook#38.11), [38.34](/rulebook#38.34), [38.39](/rulebook#38.39), [38.98](/rulebook#38.98), [38.116](/rulebook#38.116), [38.117](/rulebook#38.117), [38.118](/rulebook#38.118), [38.130](/rulebook#38.130) |
10429
10739
  | `--help` · `-h` · `abide help` | Usage, generated from the command list. | [38.12](/rulebook#38.12), [38.113](/rulebook#38.113), [38.114](/rulebook#38.114) |
10430
10740
  | `--version` · `-v` | The version of abide, or of the app a compiled executable holds. | [38.115](/rulebook#38.115) |
10431
10741