@abide/abide 0.103.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.104.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c95d974: Breaking: for await over a reactive is one run ([`0013bd0`](https://github.com/briancray/abide/commit/0013bd0a279bc73cbd5d5db3f4424bd2a2418fac))
8
+ - c95d974: Breaking: mcp publishes every handler as a tool ([`a983075`](https://github.com/briancray/abide/commit/a98307582cc388f38c3d2377e176f74fe41b63da))
9
+ - c95d974: Breaking: a hook called outside a .abide or a handler file is a build error (37.13) ([`c03e97c`](https://github.com/briancray/abide/commit/c03e97c05d27db709ecb02ad06a53574b1c98ee5))
10
+ - c95d974: a server directory nests under lib or tests (23.16) ([`f928540`](https://github.com/briancray/abide/commit/f928540ecbe57af94c780a2b73c7109ef87faa52))
11
+
12
+ ### Patch Changes
13
+
14
+ - c95d974: every spelling of a handler file import is the face the server mounts (16.2) ([`1e2e721`](https://github.com/briancray/abide/commit/1e2e72171950f7b3d0144fbe12b4e6d35c226fbd))
15
+
3
16
  ## 0.103.0
4
17
 
5
18
  ### Minor Changes
package/docs/REGISTRY.md CHANGED
@@ -93,7 +93,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
93
93
  | `identity` | `(next: Stored, previous: Stored) => unknown` | What makes it the same value. | 5.1, 5.2, 5.3, 5.4, 5.5, 9.3, 11.17, 18.4 |
94
94
  | `structural` | `(next: unknown, previous: unknown) => boolean` | The by-value comparator an app declares on `identity`. | 5.4, 5.5, 18.4 |
95
95
  | `by` | `(keyof Element & string) \| ((element: Element) => unknown)`, over the `Element` a `Stored` array holds | What tells one element of the value from another. | 5.22, 18.4, 18.15, 32.32 |
96
- | `ttl` | `number` | The life of a production, or of a keyed entry. | 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 6.11, 11.6, 11.35, 11.45, 11.48, 11.49, 11.69 |
96
+ | `ttl` | `number` | The life of a production, or of a keyed entry. | 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 6.10, 11.6, 11.35, 11.45, 11.48, 11.49, 11.69 |
97
97
  | `args` | `Schema<Args>` | The gate on a keyed value's args. | 9.4, 11.28, 11.30, 11.40, 11.41, 11.56, 11.60, 11.68 |
98
98
  | `global` | `boolean` | Opts out of the default scope into one cache shared by every caller in the process. | 11.34, 11.35, 11.36, 11.37, 11.38, 11.39, 11.53, 11.67 |
99
99
  | `tags` | `string[] \| ((args: Args) => string[])` | What a `Selection` matches on. | 11.43, 11.44, 18.4, 18.15 |
@@ -111,8 +111,8 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
111
111
  | `s.then` | `<Result>(onSettled?: (value: Stored) => Result \| PromiseLike<Result>, onFailed?: (error: unknown) => Result \| PromiseLike<Result>) => Promise<Result>` | The settling, as a promise. | 1.4, 2.7, 2.8 |
112
112
  | `s.catch` | `<Result>(onFailed: (error: unknown) => Result \| PromiseLike<Result>) => Promise<Stored \| Result>` | The settling's refusal path. | 1.4, 1.5 |
113
113
  | `s.finally` | `(onSettled: () => void) => Promise<Stored>` | The settling's end, whichever way it went. | 1.4, 1.5 |
114
- | `for await (… of s)` | `AsyncIterable<Produced>` | The live cursor face of a read. | 2.5, 2.6, 5.10, 6.10, 9.12 |
115
- | `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The production in flight, and then what follows it. | 2.5, 6.10 |
114
+ | `for await (… of s)` | `AsyncIterable<Produced>` | What one run produced: the settled value once, or a stream's chunks. | 2.5, 2.6, 5.10, 9.12, 9.15, 9.16 |
115
+ | `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The run being served, from its start to its end. | 2.5, 2.6 |
116
116
 
117
117
  ### Writes
118
118
 
@@ -671,13 +671,13 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
671
671
 
672
672
  | Name | Signature | Meaning | Rules |
673
673
  | --- | --- | --- | --- |
674
- | `middleware` | `(...rungs: Middleware<{ request: Request }, Response>[]) => () => void` | The per-request lane at the calling file's route. | 18.26, 37.2, 37.3 |
675
- | `onStart` | `(fn: (start: () => Promise<void>) => void \| Promise<void>) => () => void` | Wraps the boot. | 37.4, 37.6 |
676
- | `onStop` | `(fn: (stop: () => Promise<void>) => void \| Promise<void>) => () => void` | Mirrors it for teardown. | 37.4, 37.6 |
677
- | `onError` | `(fn: (error: unknown) => unknown) => () => void` | Runs on an unexpected error at the calling file's route. | 37.5, 37.8 |
678
- | `onConfig` | `(fn: ConfigDefaults \| null, options?: ConfigOptions) => () => void` | The app's config defaults. | 27.6, 37.7 |
679
- | `onHealth` | `(report: () => unknown \| Promise<unknown>) => () => void` | The app's health reporter. | 25.2, 37.7 |
680
- | `onPrincipal` | `(resolve: (claims: unknown) => unknown \| Promise<unknown>) => () => void` | Turns claims into the app's half of a principal. | 26.4, 37.7 |
674
+ | `middleware` | `(...rungs: Middleware<{ request: Request }, Response>[]) => () => void` | The per-request lane at the calling file's route. | 18.26, 37.2, 37.3, 37.13 |
675
+ | `onStart` | `(fn: (start: () => Promise<void>) => void \| Promise<void>) => () => void` | Wraps the boot. | 37.4, 37.6, 37.13 |
676
+ | `onStop` | `(fn: (stop: () => Promise<void>) => void \| Promise<void>) => () => void` | Mirrors it for teardown. | 37.4, 37.6, 37.13 |
677
+ | `onError` | `(fn: (error: unknown) => unknown) => () => void` | Runs on an unexpected error at the calling file's route. | 37.5, 37.8, 37.13 |
678
+ | `onConfig` | `(fn: ConfigDefaults \| null, options?: ConfigOptions) => () => void` | The app's config defaults. | 27.6, 37.7, 37.13 |
679
+ | `onHealth` | `(report: () => unknown \| Promise<unknown>) => () => void` | The app's health reporter. | 25.2, 37.7, 37.13 |
680
+ | `onPrincipal` | `(resolve: (claims: unknown) => unknown \| Promise<unknown>) => () => void` | Turns claims into the app's half of a principal. | 26.4, 37.7, 37.13 |
681
681
  | `onLauncher` | `(options: () => LauncherOptions) => () => void` | The window the desktop app opens and the forms its launcher offers. | 38.80, 38.81, 38.83, 38.86, 38.102, 38.107 |
682
682
 
683
683
  ## The desktop app
package/docs/RULEBOOK.md CHANGED
@@ -106,11 +106,11 @@ second.
106
106
 
107
107
  2.4 A failed revalidation MUST NOT cause a read to throw.
108
108
 
109
- 2.5 `s[Symbol.asyncIterator]` MUST yield the production in flight from its start, and MUST then
110
- continue live.
109
+ 2.5 On a value that is not a streaming producer, `s[Symbol.asyncIterator]` MUST settle as `s.then`
110
+ does, yielding once what it resolves with.
111
111
 
112
- 2.6 On a value that is not a streaming producer, `s[Symbol.asyncIterator]` MUST hand back the
113
- standing value once per turn, however many productions or patches landed since the last.
112
+ 2.6 On a streaming producer, `s[Symbol.asyncIterator]` MUST yield the chunks of the run being served
113
+ from its first, and MUST end when that run closes.
114
114
 
115
115
  2.7 `s.then` MUST resolve with the value where `s.busy` becomes false and 2.2 would not throw.
116
116
 
@@ -270,9 +270,7 @@ the production it serves.
270
270
 
271
271
  6.9 An expiry MUST NOT report `s.busy` true.
272
272
 
273
- 6.10 A cursor MUST NOT be handed an `undefined` production.
274
-
275
- 6.11 A streaming producer's `ttl` MUST run from its run's close, and MUST NOT reach the run in
273
+ 6.10 A streaming producer's `ttl` MUST run from its run's close, and MUST NOT reach the run in
276
274
  flight.
277
275
 
278
276
  # 7. Triggers
@@ -396,6 +394,11 @@ made by one (11.61) MUST end with it.
396
394
  9.14 A subscriber's copy of a wired `Reactive` MUST report `s.busy` true until the first production
397
395
  arrives.
398
396
 
397
+ 9.15 A cursor over a streaming producer MUST throw, once it has yielded every chunk, the failure its
398
+ run ended on.
399
+
400
+ 9.16 A cursor over a streaming producer MUST end where a new run replaces the one it was reading.
401
+
399
402
  # 10. Sharing
400
403
 
401
404
  10.1 `reactive.share` MUST get or create in one call.
@@ -1164,8 +1167,8 @@ lists a handler for it. What a closed one answers is 36.7.
1164
1167
 
1165
1168
  20.8 Every `/__abide/**` address MUST run inside the app's own middleware.
1166
1169
 
1167
- 20.9 `/__abide/mcp` MUST publish a handler that reads as a tool and a resource according to which
1168
- arm `GET` took, and MUST NOT require a flag to tell them apart.
1170
+ 20.9 `/__abide/mcp` MUST publish every handler it lists as a tool, a `GET` taking no args included,
1171
+ and MUST NOT publish one as a resource.
1169
1172
 
1170
1173
  20.10 A handler whose own `Middleware` rung would refuse a caller MUST still be listed, and MUST
1171
1174
  still be refused on the call.
@@ -1294,7 +1297,8 @@ segment a page below it adds.
1294
1297
 
1295
1298
  23.15 A file under a fence MUST NOT have a route.
1296
1299
 
1297
- 23.16 A fence under a fence MUST be a build error naming both.
1300
+ 23.16 A fence under a fence MUST be a build error naming both, save a `server` directory under a
1301
+ `lib` or a `tests` directory.
1298
1302
 
1299
1303
  23.17 A route whose first segment is `__abide` MUST be a build error.
1300
1304
 
@@ -1432,8 +1436,7 @@ module ran is not the one served after it.
1432
1436
  27.12 `Config` MUST admit a field an app declares on it, of any type.
1433
1437
 
1434
1438
  27.13 `abide check` and `abide lsp` MUST type `Config` with what the defaults of every `onConfig`
1435
- called as a statement at the top of a `<script server>`, a handler file or a module under a `server`
1436
- directory return, and a union return with the keys
1439
+ called as a statement at the top of a `<script server>` or a handler file return, and a union return with the keys
1437
1440
  every member of it has.
1438
1441
 
1439
1442
  # 28. Logging
@@ -2079,6 +2082,9 @@ through `App.stop`, and the process MUST then exit with 0.
2079
2082
 
2080
2083
  37.12 A second `SIGTERM` or `SIGINT` while the app is stopping MUST end the process at once.
2081
2084
 
2085
+ 37.13 A lifecycle hook called in a module of the app's own that is neither a `.abide` nor a handler
2086
+ file MUST be a build error naming the module and the hook.
2087
+
2082
2088
  # 38. Commands
2083
2089
 
2084
2090
  38.1 `abide scaffold` MUST write a starter project, and MUST skip a step already done.
package/docs/abide.md CHANGED
@@ -1597,11 +1597,11 @@ Read on: [Throttle & debounce](/values/slow-down-a-value-that-changes-too-fast.m
1597
1597
 
1598
1598
  ## Following a channel sees turns, not every message
1599
1599
 
1600
- `for await (const turns of onboarding)` walks the channel's cursor. On a value with no streaming producer the cursor hands back the standing value once per turn, however many writes landed since the last, and a patch in place is one of them (2.6). A burst of three messages in one turn is one step, holding all three.
1600
+ `onboarding.watch((turns) => …)` follows the channel. The effect runs once at the end of a turn, however many writes landed in it, and a patch in place is one of them (12.10). A burst of three messages in one turn is one run, holding all three.
1601
1601
 
1602
- So there is no exactly-once tail of a channel. Code that has to act on every message acts where it writes it — in `say`, beside the push. The cursor never hands back an `undefined` value (6.10).
1602
+ So there is no exactly-once tail of a channel. Code that has to act on every message acts where it writes it — in `say`, beside the push.
1603
1603
 
1604
- A stream is the other case: its cursor yields from the start of the run in flight and then live, one step per chunk (2.5), and a capped window still delivers every chunk (5.10).
1604
+ `for await` is not a follow. Over a value it yields the settled value once and ends, as `await` does (2.5). Over a stream it yields each chunk of the run and ends at the close (2.6), and a capped window still delivers every chunk (5.10).
1605
1605
 
1606
1606
  Read on: [Watching values](/values/do-something-when-a-value-changes.md) · [Streaming data](/data/send-data-as-it-arrives.md)
1607
1607
 
@@ -2506,7 +2506,7 @@ Two costs, and both are yours rather than abide's.
2506
2506
 
2507
2507
  And a global value is bounded by its own `ttl` and the app's `invalidate` and by nothing else. There is no byte ceiling and no eviction count: measuring an arbitrary value costs an O(size) walk, and an LRU would evict an entry a reader is mid-flight on. `global: true` with `ttl: Infinity` is an unbounded cache — unbounded because the app asked for it, the bound being a number the app knows and abide does not.
2508
2508
 
2509
- A global value over a stream fans one producer out to many readers, and it needs no retention to do it. A reader arriving mid-stream gets the run in flight from its start and then goes live (2.5), and a reader leaving does not cancel the producer for the ones that remain (11.39).
2509
+ A global value over a stream fans one producer out to many readers, and it needs no retention to do it. A reader arriving mid-stream gets the run in flight from its first chunk to its close (2.6), and a reader leaving does not cancel the producer for the ones that remain (11.39).
2510
2510
 
2511
2511
  Read on: [Recent values](/values/keep-the-last-few-values.md) · [Auth & principal](/app/know-who-is-calling.md) · [Authorization](/data/decide-who-may-call-what.md) · [Response types](/data/answer-with-something-other-than-json.md)
2512
2512
 
@@ -3652,7 +3652,7 @@ Read on: [Patching a list](/values/patch-a-list-from-a-live-feed.md) · [Lists](
3652
3652
 
3653
3653
  Every `Reactive` holds its standing value and nothing before it. Where you want the ones before it, hold them: make the value the array, and append where you write.
3654
3654
 
3655
- A reader cannot build the window for you. `for await` over a value hands back the standing value once per turn, and a burst of writes in one turn is one step (2.6), so a window a reader assembles misses whatever landed in the same turn. The writer sees every value, which is why the window is built where the write is.
3655
+ A reader cannot build the window for you. A `watch` runs once per turn, and a burst of writes in one turn is one run (12.10), so a window a reader assembles misses whatever landed in the same turn. The writer sees every value, which is why the window is built where the write is.
3656
3656
 
3657
3657
  A channel is this same shape with many writers — a `reactive` over an array that everyone connected reads, trimmed the same way.
3658
3658
 
@@ -5176,7 +5176,7 @@ Read on: [Loading states](/values/show-a-value-that-isnt-there-yet.md) · [Lists
5176
5176
 
5177
5177
  ## A reader arriving mid-run replays the run
5178
5178
 
5179
- There is no retention option on the transport and none is owed. What a late reader needs is the answer being built, and a cursor over the stream hands it the run in flight from its start before it goes live (2.5). So a reader arriving halfway through still paints every row.
5179
+ There is no retention option on the transport and none is owed. What a late reader needs is the answer being built, and a `for await` over the stream hands it the run in flight from its first chunk to its close (2.6). So a reader arriving halfway through still paints every row.
5180
5180
 
5181
5181
  ## A stream is not a channel
5182
5182
 
@@ -5386,7 +5386,7 @@ The call hands back the same `Reactive` the handler had, `Produced` included (16
5386
5386
 
5387
5387
  Writing to that copy is not how to send. A `set` on it stands only until the next production replaces it (9.4), so a turn goes through `say`.
5388
5388
 
5389
- `room.invalidate({ id })` and `room.refresh({ id })` inside a handler reach every browser's copy of that room (7.10, 7.18). A `for await` over the room hands back the standing array once per turn, a burst collapsing to the latest (2.6), so code that has to act on every message acts in `say`.
5389
+ `room.invalidate({ id })` and `room.refresh({ id })` inside a handler reach every browser's copy of that room (7.10, 7.18). A `watch` on the room runs once per turn, a burst collapsing to the latest (12.10), so code that has to act on every message acts in `say`.
5390
5390
 
5391
5391
  ## Each instance holds its own room
5392
5392
 
@@ -9229,27 +9229,10 @@ Read on: [Config](/app/configure-the-app.md) · [Authorization](/data/decide-who
9229
9229
 
9230
9230
  ## A root rung answers what no route does
9231
9231
 
9232
- `middleware` filed at the root route runs before routing, so a rung there can answer an address no page or handler claims, and hand everything else on with `next()`. These docs serve every page as markdown that way:
9233
-
9234
- ```ts src/lib/downloads.ts — excerpt
9235
- middleware(async (next, { request }) => {
9236
- const { pathname, origin } = new URL(request.url)
9237
- if (!pathname.endsWith('.md') && !pathname.endsWith('/llms.txt'))
9238
- return next()
9239
- if (pathname === '/llms.txt')
9240
- return new Response('# abide\n\nEvery page, as markdown: abide.md\n')
9241
- if (pathname === '/abide.md') {
9242
- bundled ??= bundle(origin)
9243
- return new Response(await bundled, { headers: MARKDOWN })
9244
- }
9245
- for (const group of SITE)
9246
- for (const page of group.pages)
9247
- if (pathname === markdownAddressOf(page.route))
9248
- return new Response(await pageMarkdown(page.route, origin), {
9249
- headers: MARKDOWN,
9250
- })
9251
- return next()
9252
- })
9232
+ `middleware` filed at the root route runs before routing, so a rung there can answer an address no page or handler claims, and hand everything else on with `next()`. These docs serve every page as markdown that way — the rung is a module's export, and the root layout files it with `middleware(serveDownloads)`, since a hook is only called in a page, a layout or a handler file (37.13):
9233
+
9234
+ ```ts src/lib/serveDownloads.ts — excerpt
9235
+ export const serveDownloads: Middleware<
9253
9236
  ```
9254
9237
 
9255
9238
  It uses the same response helpers everything else does, there being no second set.
@@ -10006,13 +9989,15 @@ let refused = reactive('—')
10006
9989
  <script server>
10007
9990
  // The docs app's own hooks; each example files its own in its own file.
10008
9991
  import { csp, middleware, onConfig } from '@abide/abide/server'
10009
- import '#src/lib/downloads.ts'
9992
+ import { serveDownloads } from '#src/lib/serveDownloads.ts'
10010
9993
 
10011
9994
  // The sign-in examples need a secret: one per process, or the deployment's.
10012
9995
  const PRINCIPAL_SECRET = crypto.randomUUID()
10013
9996
 
10014
9997
  onConfig(() => ({ ABIDE_PRINCIPAL_SECRET: PRINCIPAL_SECRET }))
10015
9998
 
9999
+ middleware(serveDownloads)
10000
+
10016
10001
  // Every docs page's policy; 'self' so the docs can frame invoice previews.
10017
10002
  middleware(csp({ 'frame-ancestors': ["'self'"] }))
10018
10003
  </script>
@@ -10307,11 +10292,11 @@ Read on: [Withholding a surface](/machines/keep-a-handler-off-a-surface.md)
10307
10292
 
10308
10293
  An MCP server is a list of tools with typed arguments and descriptions. A declared handler is a typed argument shape with a description. `/__abide/mcp` is the second read as the first, over streamable http.
10309
10294
 
10310
- ## The method decides tool or resource
10295
+ ## Every handler is a tool
10311
10296
 
10312
- A handler is published as a **tool** or as a **resource** according to which arm `GET` took, and no flag tells them apart. A read is a resource, a mutation is a tool — which is the distinction MCP already draws, arrived at from the declaration rather than from an annotation somebody has to keep true.
10297
+ Every handler the route table publishes is a **tool** — a read that takes no arguments as much as a mutation. None is published as an MCP resource, because a client that only calls tools, Claude Code under `abide-bridge` among them, would never reach a handler listed as one.
10313
10298
 
10314
- That is the whole of the mapping, and it is worth being able to state in one line: the model sees your app's verbs because your app already declared which of its handlers change something.
10299
+ That is the whole of the mapping: the model sees every handler your app publishes, and whether one changes something is the handler's own method, not a second listing.
10315
10300
 
10316
10301
  ## The description is the handler's
10317
10302
 
@@ -11810,8 +11795,8 @@ Every `Reactive` takes these, whichever overload made it.
11810
11795
  | `s.then` | `<Result>(onSettled?: (value: Stored) => Result \| PromiseLike<Result>, onFailed?: (error: unknown) => Result \| PromiseLike<Result>) => Promise<Result>` | The settling, as a promise. Resolves when `busy()` goes false, and rejects with what a read would throw. |
11811
11796
  | `s.catch` | `<Result>(onFailed: (error: unknown) => Result \| PromiseLike<Result>) => Promise<Stored \| Result>` | The settling's refusal path, derived from `then`. Fires for a producer that failed with nothing landed, and for nothing else. |
11812
11797
  | `s.finally` | `(onSettled: () => void) => Promise<Stored>` | The settling's end, whichever way it went. What ends is the settling, not the value — a subscriber's copy of a channel settles at its first production and goes on. |
11813
- | `for await (… of s)` | `AsyncIterable<Produced>` | The live cursor face of a read. On a stream, one turn per chunk. On any other value, the standing value once per turn — a burst collapses to the latest, and an in-place `patch` is a turn of its own. |
11814
- | `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The production in flight from its start, and then what follows it. A chunk is a piece of one value, so a stream's cursor sees every chunk; any other value's cursor sees where the value stands. |
11798
+ | `for await (… of s)` | `AsyncIterable<Produced>` | What one run produced. On a stream, one turn per chunk, ending at the close and throwing what failed the run. On any other value, the settled value once, as `await` gives it. To follow changes, `watch`. |
11799
+ | `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The run being served, from its start to its end. A chunk is a piece of one value, so a stream's cursor sees every chunk; any other value's cursor sees the value it settles on. |
11815
11800
 
11816
11801
  ## Writes
11817
11802
 
@@ -12843,7 +12828,7 @@ src/
12843
12828
 
12844
12829
  A route is its file's path without the extension, and an `index.abide`, a `layout.abide` and an `error.abide` take their directory's (23.14). Every other `.abide` directly in a directory is a page (23.21), so a component lives under a `lib`. A `.ts` there is a handler file.
12845
12830
 
12846
- `lib`, `tests`, `server` and `public` are the fences, and nothing under one has a route (23.15). One inside another is a build error, and so is an `index.abide`, a `layout.abide` or an `error.abide` under one (23.16, 23.18). A directory's top level holds pages, layouts, error pages, handler files, directories and fences alone — any other file there is a build error naming the `lib` to move it into (23.22). So listing a directory lists its routes, and `abide routes` lists the app's.
12831
+ `lib`, `tests`, `server` and `public` are the fences, and nothing under one has a route (23.15). One inside another is a build error, save a `server` inside a `lib` or a `tests`, which keeps code only the server loads beside the components that call it — and so is an `index.abide`, a `layout.abide` or an `error.abide` under one (23.16, 23.18). A directory's top level holds pages, layouts, error pages, handler files, directories and fences alone — any other file there is a build error naming the `lib` to move it into (23.22). So listing a directory lists its routes, and `abide routes` lists the app's.
12847
12832
 
12848
12833
  ### Resolution order
12849
12834
 
@@ -12967,7 +12952,7 @@ A page and a handler file are **addresses rather than names**, and so are `index
12967
12952
  | `onPrincipal` | `(resolve: (claims: unknown) => unknown \| Promise<unknown>) => () => void` | Turns claims into the app's half of a principal. |
12968
12953
  | `onLauncher` | `(options: () => LauncherOptions) => () => void` | The window the desktop app opens and the forms its launcher offers. |
12969
12954
 
12970
- Each is a hook you **call** rather than an export the framework looks for, a `.abide` exporting nothing. It is filed at the route of the `.abide` or the handler file that called it, and at the root where a module called it (37.1). A `.abide` under a `lib` or a `tests` directory files its hooks at the route of the directory holding that one. `onLauncher` is called in `src/launcher.abide` alone.
12955
+ Each is a hook you **call** rather than an export the framework looks for, a `.abide` exporting nothing. It is filed at the route of the `.abide` or the handler file that called it (37.1), and any other module calling one is a build error (37.13) — export the function and call the hook where it serves. A `.abide` under a `lib` or a `tests` directory files its hooks at the route of the directory holding that one. `onLauncher` is called in `src/launcher.abide` alone.
12971
12956
 
12972
12957
  `middleware` runs over that route and the ones below it — before routing at the root, after it anywhere else — and `onError` hears an error there, nearest route first. The rest are the process's wherever they are written, and every one of a kind composes: `onConfig` and `onPrincipal` merge what each answers, the deeper route's over the root's. `abide routes` lists every call with the route it is filed at.
12973
12958
 
@@ -12985,7 +12970,7 @@ Every one hands back the way off again, so a module that is torn down can remove
12985
12970
 
12986
12971
  ### `Config` is typed from the `onConfig` calls
12987
12972
 
12988
- `abide check` and `abide lsp` type `Config` from the defaults every `onConfig` returns, read where it is called as a statement at the top of a `<script server>`, of a handler file or of a module under a `server` directory (27.13). A default that returns a union types the keys every member of it has. So a config read is typed without a declaration of its own, wherever in the app the default is written.
12973
+ `abide check` and `abide lsp` type `Config` from the defaults every `onConfig` returns, read where it is called as a statement at the top of a `<script server>` or of a handler file (27.13). A default that returns a union types the keys every member of it has. So a config read is typed without a declaration of its own, wherever in the app the default is written.
12989
12974
 
12990
12975
  ### `NODE_ENV` is verbatim
12991
12976
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.103.0",
3
+ "version": "0.104.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -863,7 +863,7 @@ export class TypeSession {
863
863
  configModules: configModulesUnder(within, files),
864
864
  }
865
865
  }
866
- // 27.13 — what a handler file or a server module defaults, beside it.
866
+ // 27.13 — what a handler file defaults, beside it.
867
867
  for (const [path, text] of await this.walked.configModules) {
868
868
  const shadow = moduleConfigShadow(text)
869
869
  const at = `${this.canonical(path)}${CONFIG_SUFFIX}`
@@ -1329,17 +1329,15 @@ function flatten(diagnostic: {
1329
1329
  const CONFIG_SUFFIX = '.config-shadow.ts'
1330
1330
 
1331
1331
  /**
1332
- * Every handler file and every module under a `server` directory below `within` that names
1333
- * `onConfig`, with its text — the modules a server runs that 27.13 types `Config` off.
1332
+ * Every handler file below `within` that names `onConfig`, with its text — the modules a server
1333
+ * runs that 27.13 types `Config` off, since 37.13 refuses a hook anywhere else that is no `.abide`.
1334
1334
  */
1335
1335
  async function configModulesUnder(
1336
1336
  within: string,
1337
- { handlers, modules }: ReturnType<typeof appFiles>,
1337
+ { handlers }: ReturnType<typeof appFiles>,
1338
1338
  ): Promise<[string, string][]> {
1339
1339
  const paths: string[] = []
1340
1340
  for (const name of handlers) paths.push(`${within}/${name}`)
1341
- for (const name of modules)
1342
- if (placeOf(name) === 'server') paths.push(`${within}/${name}`)
1343
1341
  const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
1344
1342
  const found: [string, string][] = []
1345
1343
  for (let at = 0; at < paths.length; at += 1)
@@ -53,7 +53,7 @@ export async function appMap(
53
53
  : 1,
54
54
  )
55
55
  const hooks: MappedHook[] = []
56
- const { abide, handlers, modules } = appFiles(root, () => {})
56
+ const { abide, handlers } = appFiles(root, () => {})
57
57
  for (const file of abide) {
58
58
  const parsed = parseAbide(readFileSync(`${root}/${file}`, 'utf8'))
59
59
  // Block by block: one block's last token would decide how the next one's first reads.
@@ -64,11 +64,6 @@ export async function appMap(
64
64
  for (const file of handlers)
65
65
  for (const hook of hooksCalled(readFileSync(`${root}/${file}`, 'utf8')))
66
66
  hooks.push({ hook, route: scopeOf(file), file })
67
- // 37.1 — A MODULE THAT IS NEITHER files at the root: it runs as it is imported, before the
68
- // importing file's route is set.
69
- for (const file of modules)
70
- for (const hook of hooksCalled(readFileSync(`${root}/${file}`, 'utf8')))
71
- hooks.push({ hook, route: '/', file })
72
67
  return { routes, hooks }
73
68
  }
74
69
 
@@ -4,6 +4,7 @@ import { placeOf } from '#shared/routePattern.ts'
4
4
  import {
5
5
  type AppChecker,
6
6
  emitApp,
7
+ refuseModuleHooks,
7
8
  resolverIn,
8
9
  sourcesUnder,
9
10
  withChecker,
@@ -40,10 +41,13 @@ export async function checkApp(
40
41
  return withChecker(root, kept, async (session) => {
41
42
  const refused: Diagnosis[] = []
42
43
  const own = parseSources(sources, refused)
43
- // 23.16-23.18 — what the build's walk would refuse of the tree, at each file it names.
44
- const { handlers } = appFiles(root, (path, message) => {
45
- refused.push(refusal(`${root}/${path}`, message))
46
- })
44
+ // 23.16-23.18 and 37.13 — what the build's walk would refuse of the tree, at each file it names.
45
+ // 38.33 — each at its file, and every one rather than the first.
46
+ const atFile = (relative: string, thrown: unknown): void => {
47
+ refused.push(refusal(`${root}/${relative}`, thrown))
48
+ }
49
+ const { handlers, modules } = appFiles(root, atFile)
50
+ refuseModuleHooks(root, modules, atFile)
47
51
  const resolve = resolverIn()
48
52
  const { sources: everyParsed, handlers: handlerFiles } =
49
53
  await withDependencies(root, own, handlers, resolve, refused)
@@ -83,11 +87,7 @@ export async function checkApp(
83
87
  emittable,
84
88
  resolve,
85
89
  handlerFiles,
86
- // 38.33 — each at its file, and every one rather than the first.
87
- (relative, thrown) =>
88
- refused.push(
89
- refusal(`${root}/${relative}`, thrown),
90
- ),
90
+ atFile,
91
91
  )
92
92
  ).warnings,
93
93
  )
@@ -169,7 +169,7 @@ ${aliases}declare module '${SERVER_SPECIFIER}' {
169
169
  const TOP_LEVEL_CONFIG = /^onConfig\s*\(/gm
170
170
 
171
171
  /**
172
- * 27.13 FOR A MODULE THAT IS NO `.abide` — a handler file, or one under a `server` directory: its
172
+ * 27.13 FOR A HANDLER FILE, the one module that is no `.abide` and may call a hook (37.13): its
173
173
  * text with each `onConfig(…)` at its top level bound as the lowering binds a server block's, and
174
174
  * `Config` extended by each, or `undefined` where it files none.
175
175
  *
@@ -45,7 +45,7 @@ import {
45
45
  readSources,
46
46
  refusal,
47
47
  } from './readSources.ts'
48
- import { respecified } from './splitImports.ts'
48
+ import { importStatements, respecified } from './splitImports.ts'
49
49
  import { type Diagnosis, TypeSession } from './TypeSession.ts'
50
50
  import { writeDefaultProject } from './writeDefaultProject.ts'
51
51
 
@@ -350,7 +350,8 @@ export async function compileApp(
350
350
  const root = realpathSync(appRoot)
351
351
  // ONE WALK for the sources, the handler files and the public files, refusing a tree 23.16-23.18
352
352
  // forbids before anything is read.
353
- const { abide, handlers, published } = appFiles(root)
353
+ const { abide, handlers, modules, published } = appFiles(root)
354
+ refuseModuleHooks(root, modules)
354
355
  const sources = await sourcesAt(root, abide)
355
356
  // 16.2 — an app of handler files alone is still an app.
356
357
  if (sources.length === 0 && handlers.length === 0)
@@ -584,11 +585,7 @@ export async function emitApp(
584
585
  function onAbideOf(root: string, path: string, text: string): string {
585
586
  const from = dirname(realpathSync(path))
586
587
  return respecified(text, (specifier) => {
587
- if (
588
- specifier !== SHARED_SPECIFIER &&
589
- !specifier.startsWith(`${SHARED_SPECIFIER}/`)
590
- )
591
- return undefined
588
+ if (!isAbideSpecifier(specifier)) return undefined
592
589
  let target: string
593
590
  try {
594
591
  target = resolvedFile(specifier, root)
@@ -708,6 +705,46 @@ function refuseStrayLauncher(
708
705
  if (callsLauncher(readFileSync(file.path, 'utf8'))) stray(file.relative)
709
706
  }
710
707
 
708
+ /**
709
+ * 37.13 — a hook called in a module that is neither a `.abide` nor a handler file runs as the module
710
+ * is imported, before any file's route is set, and not at all where nothing imports it. A hook is a
711
+ * name the module imports from abide, or a member of a namespace it does, so `emitter.onError(…)` in
712
+ * a module that imports abide for `reactive` is no hook.
713
+ */
714
+ export function refuseModuleHooks(
715
+ root: string,
716
+ modules: readonly string[],
717
+ refuse: Refuse = THROW,
718
+ ): void {
719
+ for (const module of modules) {
720
+ const source = readFileSync(`${root}/${module}`, 'utf8')
721
+ if (!source.includes(SHARED_SPECIFIER)) continue
722
+ const imported = new Set<string>()
723
+ for (const statement of importStatements(source))
724
+ if (isAbideSpecifier(statement.specifier))
725
+ for (const binding of statement.bindings)
726
+ if (!binding.typeOnly) imported.add(binding.imported)
727
+ const hook = hooksCalled(source).find(
728
+ (name) => imported.has(name) || imported.has('*'),
729
+ )
730
+ if (hook !== undefined)
731
+ refuse(
732
+ module,
733
+ new Error(
734
+ `abide: ${module} calls ${hook}, and it is neither a .abide nor a handler file, so the call has no route. Export what it is handed and call ${hook} in the page, layout or handler file it serves. RULEBOOK 37.13.`,
735
+ ),
736
+ )
737
+ }
738
+ }
739
+
740
+ /** `@abide/abide` or one of its subpaths. */
741
+ function isAbideSpecifier(specifier: string): boolean {
742
+ return (
743
+ specifier === SHARED_SPECIFIER ||
744
+ specifier.startsWith(`${SHARED_SPECIFIER}/`)
745
+ )
746
+ }
747
+
711
748
  function callsLauncher(source: string): boolean {
712
749
  return (
713
750
  source.includes('onLauncher') &&
@@ -683,7 +683,7 @@ const ROWS_OF = '__rowsOf'
683
683
  *
684
684
  * The item is read off the subject as `listOf` iterates it: what a `Reactive` read by
685
685
  * name hands back (31.1 lowers `rows` to `rows()`), or the subject itself. THE CALL IS THE FIRST
686
- * OVERLOAD because a `Reactive` is ALSO an `AsyncIterable` of its productions (2.5), and inferred
686
+ * OVERLOAD because a `Reactive` is ALSO an `AsyncIterable` of what it produced (2.5, 2.6), and inferred
687
687
  * over one union the item came out as the row OR the whole list. Declared AFTER everything, so no
688
688
  * offset above it moves, and ambient, so there is nothing to emit. Where inference fails the item
689
689
  * is `unknown`, and a member of that is unprovable — the safe side.