@abide/abide 0.104.0 → 0.105.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 +16 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +17 -17
- package/docs/RULEBOOK.md +50 -12
- package/docs/abide.md +9 -9
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +45 -86
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -20
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +11 -9
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +180 -512
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +92 -123
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +49 -71
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +4 -2
- package/src/shared/reactive/Reactive.ts +285 -197
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -123
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/reconcile.ts +4 -110
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -33
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
package/docs/RULEBOOK.md
CHANGED
|
@@ -582,6 +582,9 @@ start again when the load ends.
|
|
|
582
582
|
|
|
583
583
|
11.70 `s.set` MUST take only a `Reactive` whose `Failures` are this value's.
|
|
584
584
|
|
|
585
|
+
11.71 An unkeyed `reactive` with a `store`, built on a server outside every request, MUST NOT run
|
|
586
|
+
the `store`'s `get` before it is reached.
|
|
587
|
+
|
|
585
588
|
# 12. Effects
|
|
586
589
|
|
|
587
590
|
12.1 A `Disposer` MUST run before each rerun of its `Effect`, and once more at teardown.
|
|
@@ -899,7 +902,7 @@ such a call MUST NOT reach it.
|
|
|
899
902
|
the reader asks for that type and `application/jsonl` otherwise.
|
|
900
903
|
|
|
901
904
|
16.45 An sse answer MUST frame each value as `data: <json>` followed by a blank line, with
|
|
902
|
-
`text/event-stream
|
|
905
|
+
`text/event-stream` and `cache-control: private, no-store`.
|
|
903
906
|
|
|
904
907
|
16.46 A stream of value chunks MUST admit jsonl and then sse, and a stream of binary chunks MUST
|
|
905
908
|
admit `application/octet-stream` alone.
|
|
@@ -984,6 +987,15 @@ naming it.
|
|
|
984
987
|
16.74 A module of the client graph importing a handler file MUST receive the wrapper 16.28 states as
|
|
985
988
|
that file's default.
|
|
986
989
|
|
|
990
|
+
16.75 A streamed answer — jsonl, sse, bytes, or a document `page` hands a stream — MUST carry
|
|
991
|
+
`X-Accel-Buffering: no`.
|
|
992
|
+
|
|
993
|
+
16.76 An `Rpc` streaming under 16.32 MUST end its answer where the run it sends ends: at that run's
|
|
994
|
+
close, where a new run replaces it, and with the failure that run ended on.
|
|
995
|
+
|
|
996
|
+
16.77 A `timeout` MUST NOT end a `throttle` or `debounce` window holding a production back, and the
|
|
997
|
+
call MUST wait on that window as 5.15 has it.
|
|
998
|
+
|
|
987
999
|
# 17. Response helpers
|
|
988
1000
|
|
|
989
1001
|
17.1 Every body helper MUST take `Values<T>`.
|
|
@@ -1086,6 +1098,10 @@ value again rather than apply what a patch wrote, as 18.12 has it for a version
|
|
|
1086
1098
|
18.26 A subscription to a `GET` MUST pass the `middleware` 37.2 runs at the route of the file that
|
|
1087
1099
|
declared it, as it opens, and MUST NOT be fed where a rung answered in the handler's place.
|
|
1088
1100
|
|
|
1101
|
+
18.27 A reader of a `text/event-stream` answer — an `Rpc`'s copy and `abide watch` alike — MUST read
|
|
1102
|
+
it by the format's fields: a comment skipped, the space after a field's colon optional, `data` lines
|
|
1103
|
+
joined, and an event ended by a blank line.
|
|
1104
|
+
|
|
1089
1105
|
# 19. Mount paths and schemas
|
|
1090
1106
|
|
|
1091
1107
|
19.1 A route MUST be answered for each method by one file at most, a page answering `GET` and `HEAD`
|
|
@@ -1566,12 +1582,16 @@ the page shown, and MUST NOT where only its query or fragment does.
|
|
|
1566
1582
|
30.25 A `navigate` that does not remount the page MUST NOT move the scroll, except to the element a
|
|
1567
1583
|
fragment it carries names.
|
|
1568
1584
|
|
|
1569
|
-
30.26 `abide check` MUST refuse a `url` literal that no page of the app is filed under
|
|
1570
|
-
file of the app is served at.
|
|
1585
|
+
30.26 `abide check` MUST refuse a `url` literal that no page route of the app (23.3) is filed under
|
|
1586
|
+
and no public file of the app is served at.
|
|
1571
1587
|
|
|
1572
1588
|
30.27 A `navigate` whose request fails, or whose route's chunks fail to load, MUST leave the address
|
|
1573
1589
|
to the document.
|
|
1574
1590
|
|
|
1591
|
+
30.28 A `Query` record and the args an `Rpc` carries in its URL MUST be written in one spelling: a
|
|
1592
|
+
member left `undefined` omitted, a list its member repeated, an empty list its name with no `=`, and
|
|
1593
|
+
anything else its text.
|
|
1594
|
+
|
|
1575
1595
|
# 31. Reading and writing by name
|
|
1576
1596
|
|
|
1577
1597
|
31.1 Inside a `.abide` file a name bound to a `Reactive` MUST be read wherever 31.3 does not hold it.
|
|
@@ -2045,7 +2065,8 @@ answered per 41.12.
|
|
|
2045
2065
|
|
|
2046
2066
|
37.1 A lifecycle hook MUST be filed at the route of the `.abide` or the handler file that called it,
|
|
2047
2067
|
one under a `lib` or a `tests` directory at the route of the directory holding that one, and at the
|
|
2048
|
-
root route where neither did
|
|
2068
|
+
root route where neither did; a call MUST be read as a hook only where it names a hook the module
|
|
2069
|
+
imports from abide, under any name, or a member of a namespace it imports from abide.
|
|
2049
2070
|
|
|
2050
2071
|
37.2 `middleware` filed below the root route MUST run after routing, over a request its route or a
|
|
2051
2072
|
route below it answers, outermost route first.
|
|
@@ -2145,7 +2166,8 @@ link none where the build bundled none.
|
|
|
2145
2166
|
by the replacement.
|
|
2146
2167
|
|
|
2147
2168
|
38.25 `--root` MUST default to `src` where the working directory holds one, and to the working
|
|
2148
|
-
directory otherwise
|
|
2169
|
+
directory otherwise; and the app it finds MUST be the one the whole command means — what it builds,
|
|
2170
|
+
boots, serves and announces.
|
|
2149
2171
|
|
|
2150
2172
|
38.26 A command that finds no app at its `--root` MUST fail, naming where it looked.
|
|
2151
2173
|
|
|
@@ -2169,7 +2191,7 @@ warnings alone.
|
|
|
2169
2191
|
every file that refusal does not stop.
|
|
2170
2192
|
|
|
2171
2193
|
38.34 `abide lsp` MUST report what 38.3 reports, over the text the editor holds rather than the
|
|
2172
|
-
text on disk.
|
|
2194
|
+
text on disk, and every refusal the app's tree makes of the file, as the tree was last saved.
|
|
2173
2195
|
|
|
2174
2196
|
38.35 `abide dev` MUST send a response body as the app produces it.
|
|
2175
2197
|
|
|
@@ -2391,6 +2413,11 @@ with `--json` it MUST print the same as one JSON document.
|
|
|
2391
2413
|
38.109 Under `bun test` with `@abide/abide/test` preloaded, a test MUST import a `.abide` of the app as
|
|
2392
2414
|
the module it compiles to, whether or not `createApp` has run.
|
|
2393
2415
|
|
|
2416
|
+
38.110 A build `abide start`, `abide call`, `abide openapi` or `abide mcp` makes first (38.100) MUST
|
|
2417
|
+
be reported on stderr, leaving the command's stdout to its own output.
|
|
2418
|
+
|
|
2419
|
+
38.111 `abide openapi` MUST print the OpenAPI document on stdout.
|
|
2420
|
+
|
|
2394
2421
|
# 39. Head and view transitions
|
|
2395
2422
|
|
|
2396
2423
|
39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
|
|
@@ -2402,7 +2429,8 @@ the module it compiles to, whether or not `createApp` has run.
|
|
|
2402
2429
|
39.4 `<head>` contributions MUST be merged by key, and the deepest contributor MUST win. The shell's
|
|
2403
2430
|
`<head>` MUST be the outermost contributor.
|
|
2404
2431
|
|
|
2405
|
-
39.5 A `<head>` contribution with no merge key MUST append in tree order
|
|
2432
|
+
39.5 A `<head>` contribution with no merge key MUST append — in tree order among the contributions
|
|
2433
|
+
its level mounts with, and when it mounts for a component mounted after its level.
|
|
2406
2434
|
|
|
2407
2435
|
39.6 On a client navigation the incoming page's keyed `<head>` contributions MUST replace the
|
|
2408
2436
|
outgoing page's by key, and a layout's MUST NOT move.
|
|
@@ -2424,12 +2452,19 @@ app MUST NOT be required to feature-detect.
|
|
|
2424
2452
|
|
|
2425
2453
|
39.12 The last `<head>` contribution for a key within one file MUST win.
|
|
2426
2454
|
|
|
2427
|
-
39.13 In a browser, a `<head>` contribution that reads a value MUST follow it for as long as
|
|
2428
|
-
|
|
2455
|
+
39.13 In a browser, a `<head>` contribution that reads a value MUST follow it for as long as the
|
|
2456
|
+
page, layout or component that made it is mounted.
|
|
2457
|
+
|
|
2458
|
+
39.14 When a page, layout or component leaves — on a client navigation, or with its region — a keyed
|
|
2459
|
+
`<head>` contribution it made and no deeper contributor still makes MUST return to the nearest
|
|
2460
|
+
remaining outer contributor's value for its key, and MUST be removed where no remaining outer
|
|
2461
|
+
contributor has that key.
|
|
2429
2462
|
|
|
2430
|
-
39.
|
|
2431
|
-
|
|
2432
|
-
|
|
2463
|
+
39.15 A `<head>` contribution with no merge key that its page, layout or component writes again
|
|
2464
|
+
MUST replace, in place, the one that same contributor wrote before.
|
|
2465
|
+
|
|
2466
|
+
39.16 A `<head>` contribution with no merge key MUST be removed when the page, layout or component
|
|
2467
|
+
that made it leaves.
|
|
2433
2468
|
|
|
2434
2469
|
# 40. Documentation
|
|
2435
2470
|
|
|
@@ -2601,6 +2636,9 @@ has answered or failed.
|
|
|
2601
2636
|
41.27 An `Rpc` call `render` built the page's markup from in flight MUST be in flight as hydration
|
|
2602
2637
|
adopts the page, however early its answer arrived.
|
|
2603
2638
|
|
|
2639
|
+
41.28 A `render` MUST let the turn's queued microtasks run between setting up a `<Name/>` whose setup
|
|
2640
|
+
made an `Rpc` call and building that component's markup.
|
|
2641
|
+
|
|
2604
2642
|
# 42. The app object
|
|
2605
2643
|
|
|
2606
2644
|
42.1 `createApp` MUST build the route table and compose every `Middleware` rung, per 16.14.
|
package/docs/abide.md
CHANGED
|
@@ -573,7 +573,7 @@ Read on: [Head & metadata](/pages/set-the-title-and-social-preview.md) · [Layou
|
|
|
573
573
|
|
|
574
574
|
Rendering a page runs its setup: the `<script>` blocks, the `reactive` calls, the bodies the markup reads. Each read that needs the server starts its work immediately, so the loads on a page are in flight together rather than in the order the markup happens to mention them.
|
|
575
575
|
|
|
576
|
-
The total time is the slowest one, not the sum — and the first byte does not wait for any of them. The markup waits one turn, so a load that answers without waiting on anything is painted rather than left as a hole.
|
|
576
|
+
The total time is the slowest one, not the sum — and the first byte does not wait for any of them. The markup waits one turn, so a load that answers without waiting on anything is painted rather than left as a hole. A component whose setup starts a load gets the same turn before its own markup is built, however deep it sits.
|
|
577
577
|
|
|
578
578
|
## 4. The first bytes leave
|
|
579
579
|
|
|
@@ -4930,7 +4930,7 @@ Each takes `Values<T>`, and sse is reachable from jsonl without declaring it: se
|
|
|
4930
4930
|
|
|
4931
4931
|
## An async generator streams as JSON Lines
|
|
4932
4932
|
|
|
4933
|
-
A handler returning an `AsyncGenerator` is framed as **jsonl** — one JSON value per line, `application/jsonl`, or `application/x-ndjson` for a reader that asks for NDJSON by name (16.44). The body is written per `pull`, so back-pressure reaches your database cursor rather than piling rows up in memory.
|
|
4933
|
+
A handler returning an `AsyncGenerator` is framed as **jsonl** — one JSON value per line, `application/jsonl`, or `application/x-ndjson` for a reader that asks for NDJSON by name (16.44). The body is written per `pull`, so back-pressure reaches your database cursor rather than piling rows up in memory, and it carries `X-Accel-Buffering: no`, as every streamed answer does, so a proxy in front passes each line on as it arrives (16.75).
|
|
4934
4934
|
|
|
4935
4935
|
That is the default framing and not the only one.
|
|
4936
4936
|
|
|
@@ -4986,7 +4986,7 @@ const feed = reactive(async () => {
|
|
|
4986
4986
|
*1 helper, 0 lines of framing* — the arm writes `data: `, the blank line after it and the two headers a proxy needs to leave the stream alone, by hand, per event.
|
|
4987
4987
|
|
|
4988
4988
|
The card's request carries no `Accept`, which is what a wallboard script or a `curl` sends, and without `sse` that caller is answered jsonl. A caller that does ask — an `EventSource` sends `Accept:
|
|
4989
|
-
text/event-stream` — gets the same events from a handler that never declared them: the jsonl transcript reframed as `data: <json>` per line with a blank line after it, `no-
|
|
4989
|
+
text/event-stream` — gets the same events from a handler that never declared them: the jsonl transcript reframed as `data: <json>` per line with a blank line after it, under `private, no-store` like any rpc answer. That is one URL with two bodies, so it carries `vary: accept`. Declaring `sse` is for the caller who cannot say what it wants. The `:` the card's answer opens on is a comment, which sends the head before the first event exists.
|
|
4990
4990
|
|
|
4991
4991
|
Read on: [Response types](/data/answer-with-something-other-than-json.md)
|
|
4992
4992
|
|
|
@@ -7220,7 +7220,7 @@ Read on: [Streaming HTML](/pages/send-the-page-before-the-data-lands.md) · [Req
|
|
|
7220
7220
|
|
|
7221
7221
|
The incoming page's keyed contributions replace the outgoing page's **by key**. A layout's contributions do not move at all, the layout not having been rebuilt — so the app's icon and its font preloads are declared once and never touched again.
|
|
7222
7222
|
|
|
7223
|
-
A key only the outgoing page set goes back to the layout's value, and is removed where no layout has one. The tab ends on the title a fresh load of that address shows.
|
|
7223
|
+
A key only the outgoing page set goes back to the layout's value, and is removed where no layout has one, and what it appended with no key leaves with it. The tab ends on the title a fresh load of that address shows.
|
|
7224
7224
|
|
|
7225
7225
|
Read on: [Links & navigation](/pages/link-to-another-page.md)
|
|
7226
7226
|
|
|
@@ -10261,10 +10261,10 @@ The address runs inside the app's own middleware like every `/__abide/**` addres
|
|
|
10261
10261
|
## Written rather than served
|
|
10262
10262
|
|
|
10263
10263
|
```
|
|
10264
|
-
abide openapi
|
|
10264
|
+
abide openapi > dist/openapi.json
|
|
10265
10265
|
```
|
|
10266
10266
|
|
|
10267
|
-
The same document,
|
|
10267
|
+
The same document, printed from the source rather than asked of a server. That is what a build step wants — a spec committed beside the code, or handed to a client generator — and it does not require the app to be running or the address to be on.
|
|
10268
10268
|
|
|
10269
10269
|
`--url` points it at a running app instead, which is what you want when the question is what *that deployment* is serving.
|
|
10270
10270
|
|
|
@@ -13032,7 +13032,7 @@ abide --help
|
|
|
13032
13032
|
- `--port <n>` — the listen port for `dev` and `start`. Defaults to `PORT`, then to 3000 — where `dev` moves on to the first free port through 3099.
|
|
13033
13033
|
- `--root <dir>` — the app's source directory. Defaults to `src` where you are standing, and to where you are standing when there is none — so `cd ledger && abide dev` and `abide dev --root
|
|
13034
13034
|
./src` are the same command. A directory holding no `.abide` under a `package.json` is not an app, and the command says so rather than building nothing.
|
|
13035
|
-
- `--out <dir>` — where `build` writes, beside the app rather than beside you: building `packages/ledger/src` from anywhere leaves `packages/ledger/.abide`, which is where `start` looks. It is also the file `
|
|
13035
|
+
- `--out <dir>` — where `build` writes, beside the app rather than beside you: building `packages/ledger/src` from anywhere leaves `packages/ledger/.abide`, which is where `start` looks. It is also the file `compile` and `bundle` write.
|
|
13036
13036
|
- `--url <origin>` — the running app the shell, `openapi`, `mcp`, `call` and `logs` talk to. Defaults to `ABIDE_APP_URL`, then to the local app.
|
|
13037
13037
|
- `--target`, `--platforms` — what `compile` builds for.
|
|
13038
13038
|
|
|
@@ -13056,7 +13056,7 @@ abide --help
|
|
|
13056
13056
|
| `abide start [--port <n>]` | Boots the built app. |
|
|
13057
13057
|
| `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. |
|
|
13058
13058
|
| `abide call <address> [args]` | One call to one handler. |
|
|
13059
|
-
| `abide openapi [--
|
|
13059
|
+
| `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. |
|
|
13060
13060
|
| `abide mcp [--url <origin>]` | The app's MCP server over stdio. |
|
|
13061
13061
|
| `abide watch <address> [args]` | A live subscription to one of the app's followed `GET`s. |
|
|
13062
13062
|
| `abide compile [--target] [--out] [--platforms]` | One standalone executable. |
|
|
@@ -13100,7 +13100,7 @@ abide call invoices/getInvoice id=4310
|
|
|
13100
13100
|
### Write the OpenAPI document at build time
|
|
13101
13101
|
|
|
13102
13102
|
```
|
|
13103
|
-
abide openapi
|
|
13103
|
+
abide openapi > dist/openapi.json
|
|
13104
13104
|
```
|
|
13105
13105
|
|
|
13106
13106
|
## See also
|
package/package.json
CHANGED
package/src/compiler/Lowering.ts
CHANGED
|
@@ -33,6 +33,12 @@ import { nesting, type Token, tokenize } from './tokenize.ts'
|
|
|
33
33
|
// no parentheses to survive one.
|
|
34
34
|
const SHORT_CIRCUIT: ReadonlySet<string> = new Set(['||', '&&', '??'])
|
|
35
35
|
|
|
36
|
+
/** 23.9 — a page's or a layout's route, which its `route.params` promises. */
|
|
37
|
+
export type Routed = { route: string; kind: 'page' | 'layout' }
|
|
38
|
+
|
|
39
|
+
/** A run of emitted code a `Lowering` wrote, and where each of its runs stood in the projection. */
|
|
40
|
+
export type LoweredChunk = { text: string; origins: Origin[] }
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* One file's lowering: each piece the emitters ask for, with its reactive reads rewritten.
|
|
38
44
|
*
|
|
@@ -42,9 +48,6 @@ const SHORT_CIRCUIT: ReadonlySet<string> = new Set(['||', '&&', '??'])
|
|
|
42
48
|
* so the import goes with the call; left to the callers it is one `used.add` per call site and a
|
|
43
49
|
* missed one emits a module calling a name it never imported.
|
|
44
50
|
*/
|
|
45
|
-
/** A run of emitted code a `Lowering` wrote, and where each of its runs stood in the projection. */
|
|
46
|
-
export type LoweredChunk = { text: string; origins: Origin[] }
|
|
47
|
-
|
|
48
51
|
export class Lowering {
|
|
49
52
|
readonly projection: Projection
|
|
50
53
|
readonly used = new Set<string>()
|
|
@@ -85,18 +88,25 @@ export class Lowering {
|
|
|
85
88
|
* the projection: what `sourceMapOf` finds in the module the emitter writes.
|
|
86
89
|
*/
|
|
87
90
|
private readonly chunks: LoweredChunk[] | undefined
|
|
91
|
+
/**
|
|
92
|
+
* 23.9 — for a checked lowering of a page or a layout, its route and which of the two it is:
|
|
93
|
+
* what its `route.params` is typed by.
|
|
94
|
+
*/
|
|
95
|
+
private readonly routed: Routed | undefined
|
|
88
96
|
|
|
89
97
|
constructor(
|
|
90
98
|
file: AbideFile,
|
|
91
99
|
answer: CheckerAnswer,
|
|
92
100
|
checking = false,
|
|
93
101
|
chunks?: LoweredChunk[],
|
|
102
|
+
routed?: Routed,
|
|
94
103
|
) {
|
|
95
104
|
this.projection = projectShadow(file)
|
|
96
105
|
this.answer = answer
|
|
97
106
|
this.holdsSlot = holdsSlot(file.template)
|
|
98
107
|
this.checking = checking
|
|
99
108
|
this.chunks = chunks
|
|
109
|
+
this.routed = routed
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
/**
|
|
@@ -441,8 +451,8 @@ export class Lowering {
|
|
|
441
451
|
const spelled = this.projection.text.slice(read.start, read.end)
|
|
442
452
|
// 23.9 — WHAT THIS PAGE'S OWN ROUTE PROMISES, for the checker alone: the running module
|
|
443
453
|
// reads the one `route` every page shares.
|
|
444
|
-
if (this.
|
|
445
|
-
return `(${spelled} as unknown as () => __RouteParams<${JSON.stringify(
|
|
454
|
+
if (this.routed !== undefined && read.routeParams)
|
|
455
|
+
return `(${spelled} as unknown as () => __RouteParams<${JSON.stringify(this.routed.route)}, ${JSON.stringify(this.routed.kind)}>)`
|
|
446
456
|
return read.through ? `${spelled}()` : spelled
|
|
447
457
|
}
|
|
448
458
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
2
|
+
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
3
|
+
import { diagnosisAt } from './lineOf.ts'
|
|
4
|
+
import type { Diagnosis } from './TypeSession.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where everything the compile refuses goes, each at the file it was refused in.
|
|
8
|
+
*
|
|
9
|
+
* TWO MODES AND ONE CALL: a build stops at a refusal, so a throwing sink throws it; `abide
|
|
10
|
+
* check` reports every one at its file and goes on (38.33), so a collecting sink keeps each as a
|
|
11
|
+
* finding at the file's first line. Every stage takes this and nothing else, so no stage decides for
|
|
12
|
+
* itself what a refusal does, and none spells a path its own way: a file is named relative to `root`
|
|
13
|
+
* or absolutely, and kept absolute.
|
|
14
|
+
*/
|
|
15
|
+
export class Refusals {
|
|
16
|
+
/** Every refusal kept, in the order met; empty in a throwing sink. */
|
|
17
|
+
readonly found: Diagnosis[] = []
|
|
18
|
+
/** What a file below the root is named relative to. */
|
|
19
|
+
readonly root: string
|
|
20
|
+
/** Whether a refusal is kept and the compile goes on, rather than thrown. */
|
|
21
|
+
readonly collecting: boolean
|
|
22
|
+
|
|
23
|
+
constructor(root: string, collecting: boolean) {
|
|
24
|
+
this.root = root
|
|
25
|
+
this.collecting = collecting
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Refuses at `file`, below the root or absolute: `thrown` is thrown, or kept. */
|
|
29
|
+
at(file: string, thrown: unknown): void {
|
|
30
|
+
if (!this.collecting) throw thrown
|
|
31
|
+
this.found.push(
|
|
32
|
+
diagnosisAt(
|
|
33
|
+
resolve(this.root, file),
|
|
34
|
+
undefined,
|
|
35
|
+
0,
|
|
36
|
+
'error',
|
|
37
|
+
thrown instanceof Error ? thrown.message : String(thrown),
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What another stage kept, as this one takes it: every one kept, or every one thrown as one
|
|
44
|
+
* error, each at its file — a build that stops names all it found rather than a first one and a
|
|
45
|
+
* second run (11.37).
|
|
46
|
+
*/
|
|
47
|
+
adopt(found: readonly Diagnosis[]): void {
|
|
48
|
+
if (found.length === 0) return
|
|
49
|
+
if (this.collecting) {
|
|
50
|
+
this.found.push(...found)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
const lines: string[] = []
|
|
54
|
+
for (const one of found) lines.push(formatDiagnosis(one, process.cwd()))
|
|
55
|
+
throw new Error(lines.join('\n'))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
SignatureKind,
|
|
12
12
|
SymbolFlags,
|
|
13
13
|
} from 'typescript/unstable/async'
|
|
14
|
-
import {
|
|
15
|
-
import { fileKind, placeOf, routeOf } from '#shared/routePattern.ts'
|
|
14
|
+
import { placeOf } from '#server/placement.ts'
|
|
16
15
|
import { aliasedSymbol } from './aliasedSymbol.ts'
|
|
17
16
|
import { AMBIENT_SOURCES, ambientReadsIn } from './ambientReadsIn.ts'
|
|
17
|
+
import type { AppLayout, PlacedSource } from './appLayout.ts'
|
|
18
18
|
import { argsDefaultsIn } from './argsDefaultsIn.ts'
|
|
19
19
|
import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
|
|
20
20
|
import {
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
import type { Finding } from './Finding.ts'
|
|
27
27
|
import { CHECKED_VALUES } from './Lowering.ts'
|
|
28
28
|
import { diagnosisAt, lineOf } from './lineOf.ts'
|
|
29
|
-
import { mountedUnder } from './mountsOf.ts'
|
|
30
29
|
import type { AbideFile } from './parseAbide.ts'
|
|
31
30
|
import { projectShadow } from './projectShadow.ts'
|
|
32
31
|
import { abideIn } from './reactiveTypes.ts'
|
|
@@ -65,18 +64,6 @@ export class TypeSession {
|
|
|
65
64
|
* text's offsets.
|
|
66
65
|
*/
|
|
67
66
|
private shadows = new Map<string, { path: string; text: string }>()
|
|
68
|
-
/**
|
|
69
|
-
* What `diagnose` last read off one walk of the root: the route table it declared (30.26) and the
|
|
70
|
-
* modules it read for their `onConfig` (27.13). Kept until `edited` hears anything move, so a
|
|
71
|
-
* check asked again and again while an editor types walks and reads nothing on disk.
|
|
72
|
-
*/
|
|
73
|
-
private walked:
|
|
74
|
-
| {
|
|
75
|
-
within: string
|
|
76
|
-
routes: string
|
|
77
|
-
configModules: Promise<[string, string][]>
|
|
78
|
-
}
|
|
79
|
-
| undefined = undefined
|
|
80
67
|
/** The checker's own key for a path: resolved, and case-folded where its host folds case. */
|
|
81
68
|
private keyOf: (path: string) => string
|
|
82
69
|
/**
|
|
@@ -358,7 +345,6 @@ export class TypeSession {
|
|
|
358
345
|
* checker is not told what was under it.
|
|
359
346
|
*/
|
|
360
347
|
edited(paths: string[] | undefined): void {
|
|
361
|
-
this.walked = undefined
|
|
362
348
|
if (paths === undefined) {
|
|
363
349
|
this.everything = true
|
|
364
350
|
return
|
|
@@ -447,10 +433,12 @@ export class TypeSession {
|
|
|
447
433
|
}
|
|
448
434
|
|
|
449
435
|
/**
|
|
450
|
-
* 34.25, 23.19 —
|
|
451
|
-
* `changed` the config moved where it was not one
|
|
436
|
+
* 34.25, 23.19 — where `source` is a dependency's file, makes `path` — it, or what it lowers to
|
|
437
|
+
* — a root of the opened config, telling `changed` the config moved where it was not one
|
|
438
|
+
* already. The app's own is a root already: telling the config it moved reloads the project.
|
|
452
439
|
*/
|
|
453
|
-
private openOutside(path: string, changed: string[]): void {
|
|
440
|
+
private openOutside(source: string, path: string, changed: string[]): void {
|
|
441
|
+
if (placeOf(this.belowRoot(source)) !== 'dependency') return
|
|
454
442
|
if (this.outside.has(path)) return
|
|
455
443
|
this.outside.add(path)
|
|
456
444
|
if (!changed.includes(this.opened)) changed.push(this.opened)
|
|
@@ -472,9 +460,7 @@ export class TypeSession {
|
|
|
472
460
|
const changed: string[] = []
|
|
473
461
|
for (const path of modules) {
|
|
474
462
|
const canonical = this.canonical(path)
|
|
475
|
-
|
|
476
|
-
if (placeOf(this.belowRoot(canonical)) === 'dependency')
|
|
477
|
-
this.openOutside(canonical, changed)
|
|
463
|
+
this.openOutside(canonical, canonical, changed)
|
|
478
464
|
}
|
|
479
465
|
const project = await this.currentProject([], changed)
|
|
480
466
|
if (project === undefined) return project
|
|
@@ -574,7 +560,6 @@ export class TypeSession {
|
|
|
574
560
|
async readsFor(
|
|
575
561
|
sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
|
|
576
562
|
text?: string
|
|
577
|
-
position?: string | undefined
|
|
578
563
|
})[],
|
|
579
564
|
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
580
565
|
const answered = await this.classify(sources)
|
|
@@ -597,7 +582,6 @@ export class TypeSession {
|
|
|
597
582
|
private async classify(
|
|
598
583
|
sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
|
|
599
584
|
text?: string
|
|
600
|
-
position?: string | undefined
|
|
601
585
|
})[],
|
|
602
586
|
): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
|
|
603
587
|
const warnings: Diagnosis[] = []
|
|
@@ -646,8 +630,7 @@ export class TypeSession {
|
|
|
646
630
|
// AN UNCHANGED SHADOW IS NOT REPORTED, which is what a kept session saves on a save:
|
|
647
631
|
// the one page that moved is re-read rather than all of them.
|
|
648
632
|
this.register(map.shadowPath, projection.text, created, changed)
|
|
649
|
-
|
|
650
|
-
this.openOutside(map.shadowPath, changed)
|
|
633
|
+
this.openOutside(source.path, map.shadowPath, changed)
|
|
651
634
|
queries.push({
|
|
652
635
|
map,
|
|
653
636
|
spans: {
|
|
@@ -690,8 +673,6 @@ export class TypeSession {
|
|
|
690
673
|
sourceFile,
|
|
691
674
|
query.map.shadowPath,
|
|
692
675
|
query.spans,
|
|
693
|
-
// 23.19 — a mounted page's route is its position's.
|
|
694
|
-
source.position ?? this.belowRoot(source.path),
|
|
695
676
|
source.file,
|
|
696
677
|
)
|
|
697
678
|
answers[at] = read.answer
|
|
@@ -812,22 +793,27 @@ export class TypeSession {
|
|
|
812
793
|
* 38.3 — WHAT THE CHECKER SAYS ABOUT EACH FILE AS IT RUNS, placed on the `.abide` line. Each
|
|
813
794
|
* source is checked as its lowered program (`checkedShadow`), which is registered for the life
|
|
814
795
|
* of the session beside its shadow for the reason a shadow is; and every `.ts` root of the
|
|
815
|
-
* project under
|
|
796
|
+
* project under the app's root is checked as it stands, where `scripts` asks for them — an editor
|
|
816
797
|
* asking about one page does not want the whole app. `sources` carry their text, which is what
|
|
817
798
|
* a position is a line of. A `quiet` source is lowered and not reported on: a component an
|
|
818
799
|
* editor's page imports, which is there for the props the page is held to (33.4).
|
|
819
800
|
*
|
|
801
|
+
* `app` is the app the sources are checked in (`appLayout`): its route table, which `url`'s
|
|
802
|
+
* literal is held to (30.26), and its handler files, which type `Config` (27.13). A source placed
|
|
803
|
+
* as a page or a layout carries its `kind` and `route`, which its `route.params` is typed by (23.9).
|
|
804
|
+
*
|
|
820
805
|
* `refused` is the path of every source whose checked lowering refused, which `diagnoses` has
|
|
821
806
|
* reported at its line — `abide check` emits the rest of the app without them.
|
|
822
807
|
*/
|
|
823
808
|
async diagnose(
|
|
824
|
-
sources: (Omit<ReadSource, 'quiet'
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
809
|
+
sources: (Omit<ReadSource, 'quiet'> &
|
|
810
|
+
Partial<Pick<PlacedSource, 'kind' | 'route'>> & {
|
|
811
|
+
quiet?: boolean
|
|
812
|
+
})[],
|
|
813
|
+
app: Pick<AppLayout, 'root' | 'addresses' | 'handlers'>,
|
|
829
814
|
scripts = true,
|
|
830
815
|
): Promise<{ diagnoses: Diagnosis[]; refused: Set<string> }> {
|
|
816
|
+
const within = app.root
|
|
831
817
|
const created: string[] = []
|
|
832
818
|
const changed: string[] = []
|
|
833
819
|
const checked: {
|
|
@@ -836,13 +822,24 @@ export class TypeSession {
|
|
|
836
822
|
refusals: readonly Finding[]
|
|
837
823
|
}[] = []
|
|
838
824
|
for (const source of sources) {
|
|
839
|
-
const shadow = checkedShadow(
|
|
825
|
+
const shadow = checkedShadow(
|
|
826
|
+
source.file,
|
|
827
|
+
source.answer,
|
|
828
|
+
(source.kind === 'page' || source.kind === 'layout') &&
|
|
829
|
+
source.route !== undefined
|
|
830
|
+
? { route: source.route, kind: source.kind }
|
|
831
|
+
: undefined,
|
|
832
|
+
)
|
|
840
833
|
const map = new ShadowMap(
|
|
841
834
|
this.canonical(source.path),
|
|
842
835
|
projectShadow(source.file).origins,
|
|
843
836
|
shadow.origins,
|
|
844
837
|
)
|
|
845
838
|
this.register(map.checkedPath, shadow.text, created, changed)
|
|
839
|
+
// 23.19 — a package's page an editor opens is reported on, so its checked module is a
|
|
840
|
+
// root of the project as its shadow is (`classify`).
|
|
841
|
+
if (!source.quiet)
|
|
842
|
+
this.openOutside(source.path, map.checkedPath, changed)
|
|
846
843
|
checked.push({ source, map, refusals: shadow.refusals })
|
|
847
844
|
}
|
|
848
845
|
// 34.6 — WHAT THE BUNDLER RESOLVES AND THE CHECKER CANNOT: a stylesheet imported for its
|
|
@@ -854,30 +851,23 @@ export class TypeSession {
|
|
|
854
851
|
created,
|
|
855
852
|
changed,
|
|
856
853
|
)
|
|
857
|
-
if (this.walked?.within !== within) {
|
|
858
|
-
// A refused tree is `checkApp`'s finding (38.33), never the checker's crash.
|
|
859
|
-
const files = appFiles(within, () => {})
|
|
860
|
-
this.walked = {
|
|
861
|
-
within,
|
|
862
|
-
routes: routesDeclaration(within, files),
|
|
863
|
-
configModules: configModulesUnder(within, files),
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
854
|
// 27.13 — what a handler file defaults, beside it.
|
|
867
|
-
for (const
|
|
868
|
-
const
|
|
869
|
-
const
|
|
855
|
+
for (const handler of app.handlers) {
|
|
856
|
+
const at = `${this.canonical(handler.path)}${CONFIG_SUFFIX}`
|
|
857
|
+
const shadow = handler.hooks.includes('onConfig')
|
|
858
|
+
? moduleConfigShadow(handler.text)
|
|
859
|
+
: undefined
|
|
870
860
|
// One that stops filing keeps its place and declares nothing, so the next snapshot is
|
|
871
861
|
// told it moved.
|
|
872
862
|
if (shadow !== undefined || this.shadows.has(this.keyOf(at)))
|
|
873
863
|
this.register(at, shadow ?? 'export {}\n', created, changed)
|
|
874
864
|
}
|
|
875
|
-
// 30.26 — THE APP'S ROUTE TABLE, which `url`'s literal is held to: every page
|
|
876
|
-
//
|
|
865
|
+
// 30.26 — THE APP'S ROUTE TABLE, which `url`'s literal is held to: every page the build
|
|
866
|
+
// routes, declared into abide's own `Routes`. A module rather than a script, since only a
|
|
877
867
|
// module augments one.
|
|
878
868
|
this.register(
|
|
879
869
|
`${this.canonical(within)}/${ROUTES_DECLARATION}`,
|
|
880
|
-
|
|
870
|
+
routesDeclaration(app.addresses),
|
|
881
871
|
created,
|
|
882
872
|
changed,
|
|
883
873
|
)
|
|
@@ -1056,7 +1046,7 @@ export class TypeSession {
|
|
|
1056
1046
|
const named = (node as { name?: Node }).name ?? node
|
|
1057
1047
|
const file = node.getSourceFile()
|
|
1058
1048
|
definitions.push(
|
|
1059
|
-
this.
|
|
1049
|
+
this.spanInSource(
|
|
1060
1050
|
file.fileName,
|
|
1061
1051
|
file.text,
|
|
1062
1052
|
named.getStart(file),
|
|
@@ -1078,7 +1068,7 @@ export class TypeSession {
|
|
|
1078
1068
|
* back through both of its maps, and one this session has not lowered — or a span of the
|
|
1079
1069
|
* scaffolding a lowering adds, which the author never wrote — placed at its top.
|
|
1080
1070
|
*/
|
|
1081
|
-
private
|
|
1071
|
+
private spanInSource(
|
|
1082
1072
|
fileName: string,
|
|
1083
1073
|
text: string,
|
|
1084
1074
|
start: number,
|
|
@@ -1153,23 +1143,10 @@ function isFile(path: string): boolean {
|
|
|
1153
1143
|
const ROUTES_DECLARATION = 'abide-routes.d.ts'
|
|
1154
1144
|
const URL_SOURCE = Bun.fileURLToPath(new URL('../shared/url', import.meta.url))
|
|
1155
1145
|
|
|
1156
|
-
/**
|
|
1157
|
-
|
|
1158
|
-
* nothing where it has neither.
|
|
1159
|
-
*/
|
|
1160
|
-
function routesDeclaration(
|
|
1161
|
-
root: string,
|
|
1162
|
-
{ abide, published }: ReturnType<typeof appFiles>,
|
|
1163
|
-
): string {
|
|
1164
|
-
const routes = new Set<string>()
|
|
1165
|
-
for (const path of abide)
|
|
1166
|
-
if (fileKind(path) === 'page') routes.add(routeOf(path))
|
|
1167
|
-
// 23.19 — and every page a mount places in the app, at its position.
|
|
1168
|
-
for (const { position } of mountedUnder(root, abide))
|
|
1169
|
-
if (fileKind(position) === 'page') routes.add(routeOf(position))
|
|
1170
|
-
for (const address of published.values()) routes.add(address)
|
|
1146
|
+
/** 30.26 — each of `addresses` as a key of abide's `Routes`. */
|
|
1147
|
+
function routesDeclaration(addresses: readonly string[]): string {
|
|
1171
1148
|
let keys = ''
|
|
1172
|
-
for (const route of
|
|
1149
|
+
for (const route of addresses)
|
|
1173
1150
|
keys += ` ${JSON.stringify(route)}: true\n`
|
|
1174
1151
|
return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
|
|
1175
1152
|
}
|
|
@@ -1327,21 +1304,3 @@ function flatten(diagnostic: {
|
|
|
1327
1304
|
|
|
1328
1305
|
/** What a module that is no `.abide` is checked beside as, for the `Config` it files (27.13). */
|
|
1329
1306
|
const CONFIG_SUFFIX = '.config-shadow.ts'
|
|
1330
|
-
|
|
1331
|
-
/**
|
|
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
|
-
*/
|
|
1335
|
-
async function configModulesUnder(
|
|
1336
|
-
within: string,
|
|
1337
|
-
{ handlers }: ReturnType<typeof appFiles>,
|
|
1338
|
-
): Promise<[string, string][]> {
|
|
1339
|
-
const paths: string[] = []
|
|
1340
|
-
for (const name of handlers) paths.push(`${within}/${name}`)
|
|
1341
|
-
const texts = await Promise.all(paths.map((path) => Bun.file(path).text()))
|
|
1342
|
-
const found: [string, string][] = []
|
|
1343
|
-
for (let at = 0; at < paths.length; at += 1)
|
|
1344
|
-
if ((texts[at] as string).includes('onConfig'))
|
|
1345
|
-
found.push([paths[at] as string, texts[at] as string])
|
|
1346
|
-
return found
|
|
1347
|
-
}
|