@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 +13 -0
- package/docs/REGISTRY.md +10 -10
- package/docs/RULEBOOK.md +18 -12
- package/docs/abide.md +22 -37
- package/package.json +1 -1
- package/src/compiler/TypeSession.ts +4 -6
- package/src/compiler/appMap.ts +1 -6
- package/src/compiler/checkApp.ts +9 -9
- package/src/compiler/checkedShadow.ts +1 -1
- package/src/compiler/compileApp.ts +44 -7
- package/src/compiler/projectShadow.ts +1 -1
- package/src/server/abideModules.ts +80 -43
- package/src/server/appTree.ts +10 -2
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/versionOf.ts +1 -2
- package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
- package/src/shared/reactive/Cursor.ts +66 -123
- package/src/shared/reactive/Reactive.ts +74 -28
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/chunksOfStream.ts +10 -34
- package/src/shared/routePattern.ts +21 -8
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.
|
|
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>` |
|
|
115
|
-
| `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The
|
|
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
|
|
110
|
-
|
|
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
|
|
113
|
-
|
|
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
|
|
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
|
|
1168
|
-
|
|
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
|
|
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
|
-
`
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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 `
|
|
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/
|
|
9235
|
-
|
|
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/
|
|
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
|
-
##
|
|
10295
|
+
## Every handler is a tool
|
|
10311
10296
|
|
|
10312
|
-
|
|
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
|
|
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>` |
|
|
11814
|
-
| `s[Symbol.asyncIterator]` | `() => AsyncIterator<Produced>` | The
|
|
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
|
|
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
|
|
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
|
@@ -863,7 +863,7 @@ export class TypeSession {
|
|
|
863
863
|
configModules: configModulesUnder(within, files),
|
|
864
864
|
}
|
|
865
865
|
}
|
|
866
|
-
// 27.13 — what a handler file
|
|
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
|
|
1333
|
-
*
|
|
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
|
|
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)
|
package/src/compiler/appMap.ts
CHANGED
|
@@ -53,7 +53,7 @@ export async function appMap(
|
|
|
53
53
|
: 1,
|
|
54
54
|
)
|
|
55
55
|
const hooks: MappedHook[] = []
|
|
56
|
-
const { abide, handlers
|
|
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
|
|
package/src/compiler/checkApp.ts
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|