@abide/abide 0.104.0 → 0.105.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +17 -17
  4. package/docs/RULEBOOK.md +50 -12
  5. package/docs/abide.md +9 -9
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +45 -86
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -20
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +11 -9
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +180 -512
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/readSources.ts +27 -42
  25. package/src/compiler/readsIn.ts +5 -16
  26. package/src/compiler/resolverIn.ts +77 -0
  27. package/src/server/abideModules.ts +92 -123
  28. package/src/server/answerRequest.ts +18 -4
  29. package/src/server/answerSocket.ts +136 -76
  30. package/src/server/answerValue.ts +28 -15
  31. package/src/server/appTree.ts +49 -71
  32. package/src/server/claudeApi.ts +2 -5
  33. package/src/server/clientBundle.ts +198 -0
  34. package/src/server/coerceArgs.ts +1 -1
  35. package/src/server/commands.ts +93 -57
  36. package/src/server/createApp.ts +90 -16
  37. package/src/server/deferredBuilds.ts +175 -0
  38. package/src/server/devWorker.ts +107 -319
  39. package/src/server/faceOf.ts +120 -0
  40. package/src/server/fenceServer.ts +10 -18
  41. package/src/server/index.ts +3 -0
  42. package/src/server/placement.ts +167 -0
  43. package/src/server/remote.ts +14 -42
  44. package/src/server/render.ts +25 -17
  45. package/src/server/renderComponent.ts +9 -5
  46. package/src/server/requestScope.ts +10 -4
  47. package/src/server/responseRelay.ts +389 -0
  48. package/src/server/runCompiled.ts +9 -12
  49. package/src/server/runDesktop.ts +6 -9
  50. package/src/server/serveBuild.ts +26 -73
  51. package/src/server/serveDev.ts +19 -176
  52. package/src/server/streamEvent.ts +29 -0
  53. package/src/server/withAbideSources.ts +2 -1
  54. package/src/shared/HandlerRoute.ts +68 -0
  55. package/src/shared/REFUSAL_KEY.ts +5 -0
  56. package/src/shared/answerOf.ts +105 -78
  57. package/src/shared/filledPath.ts +1 -9
  58. package/src/shared/framing.ts +127 -128
  59. package/src/shared/queryText.ts +15 -7
  60. package/src/shared/reactive/Cursor.ts +4 -2
  61. package/src/shared/reactive/Reactive.ts +285 -197
  62. package/src/shared/reactive/index.ts +11 -8
  63. package/src/shared/reactive/pairReactive.ts +1 -1
  64. package/src/shared/reactive/scoped.ts +4 -3
  65. package/src/shared/reactive/share.ts +11 -1
  66. package/src/shared/routePattern.ts +0 -123
  67. package/src/shared/rpc.ts +17 -24
  68. package/src/shared/rpcRequest.ts +18 -47
  69. package/src/shared/takeEarlyCall.ts +12 -37
  70. package/src/shared/url.ts +1 -1
  71. package/src/ui/Level.ts +276 -0
  72. package/src/ui/List.ts +17 -17
  73. package/src/ui/Router.ts +20 -87
  74. package/src/ui/applyHead.ts +92 -79
  75. package/src/ui/index.ts +4 -2
  76. package/src/ui/leave.ts +5 -0
  77. package/src/ui/reconcile.ts +23 -104
  78. package/src/ui/regions.ts +27 -2
  79. package/src/ui/rows.ts +123 -2
  80. package/src/ui/wire.ts +12 -27
  81. package/src/compiler/hooksCalled.ts +0 -35
  82. package/src/server/clientBuildOptions.ts +0 -54
  83. package/src/server/refuseAddressCollision.ts +0 -37
  84. package/src/shared/reactive/chunksOfStream.ts +0 -33
  85. 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`, `no-cache` and `X-Accel-Buffering: no`.
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 and no public
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 its page
2428
- or layout is mounted.
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.14 On a client navigation, a keyed `<head>` contribution that a leaving page or layout made and
2431
- no arriving one makes MUST return to the nearest kept layout's value for its key, and MUST be removed
2432
- where no kept layout has that key.
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-cache` and `X-Accel-Buffering: no`. 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.
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 --out dist/openapi.json
10264
+ abide openapi > dist/openapi.json
10265
10265
  ```
10266
10266
 
10267
- The same document, produced 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.
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 `openapi` and `compile` write, rather than standard output.
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 [--out <file>] [--url <origin>]` | The OpenAPI document written rather than served. |
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 --out dist/openapi.json
13103
+ abide openapi > dist/openapi.json
13104
13104
  ```
13105
13105
 
13106
13106
  ## See also
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.104.0",
3
+ "version": "0.105.1",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -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.checking && read.routeParams !== undefined)
445
- return `(${spelled} as unknown as () => __RouteParams<${JSON.stringify(read.routeParams[0])}, ${JSON.stringify(read.routeParams[1])}>)`
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 { appFiles } from '#server/appTree.ts'
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 — makes `path`, a dependency's file, a root of the opened config, telling
451
- * `changed` the config moved where it was not one already.
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
- // The app's own is a root already: telling the config it moved reloads the project.
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
- if (placeOf(this.belowRoot(source.path)) === 'dependency')
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 `within` is checked as it stands, where `scripts` asks for them — an editor
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' | 'position'> & {
825
- quiet?: boolean
826
- position?: string | undefined
827
- })[],
828
- within: string,
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(source.file, source.answer)
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 [path, text] of await this.walked.configModules) {
868
- const shadow = moduleConfigShadow(text)
869
- const at = `${this.canonical(path)}${CONFIG_SUFFIX}`
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 under the
876
- // root, declared into abide's own `Routes`. A module rather than a script, since only a
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
- this.walked.routes,
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.placeOf(
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 placeOf(
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
- * 30.26 — every page under `root`, and every public file's address, as a key of abide's `Routes`, or
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 [...routes].sort())
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
- }