@abide/abide 0.51.0 → 0.53.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/AGENTS.md +518 -525
- package/CHANGELOG.md +54 -0
- package/README.md +130 -157
- package/package.json +5 -1
- package/src/abideResolverPlugin.ts +12 -0
- package/src/checkAbide.ts +60 -7
- package/src/lib/cli/completeCli.ts +38 -0
- package/src/lib/cli/printTopLevelHelp.ts +1 -0
- package/src/lib/cli/renderCliCompletions.ts +56 -0
- package/src/lib/cli/runCli.ts +27 -0
- package/src/lib/server/render.ts +70 -0
- package/src/lib/server/runtime/cacheStalenessBroadcaster.ts +33 -0
- package/src/lib/server/runtime/createServer.ts +43 -0
- package/src/lib/server/runtime/createUiPageRenderer.ts +37 -0
- package/src/lib/server/runtime/pageRenderSlot.ts +15 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +9 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +9 -0
- package/src/lib/server/sockets/registerSocket.ts +12 -0
- package/src/lib/shared/CACHE_STALENESS_SOCKET.ts +8 -0
- package/src/lib/shared/RESERVED_SOCKET_PREFIX.ts +8 -0
- package/src/lib/shared/applyCacheStalenessLocally.ts +18 -0
- package/src/lib/shared/attachRpcSelectorMethods.ts +3 -1
- package/src/lib/shared/cache.ts +32 -4
- package/src/lib/shared/cacheStalenessSlot.ts +21 -0
- package/src/lib/shared/createRpcServerProgram.ts +187 -0
- package/src/lib/shared/docSnapshotsSlot.ts +13 -0
- package/src/lib/shared/invalidate.ts +39 -0
- package/src/lib/shared/matcherFromEnvelope.ts +31 -0
- package/src/lib/shared/prepareRpcModule.ts +22 -3
- package/src/lib/shared/refresh.ts +9 -2
- package/src/lib/shared/selectorMatcher.ts +2 -15
- package/src/lib/shared/serializeSelector.ts +69 -0
- package/src/lib/shared/setsIntersect.ts +15 -0
- package/src/lib/shared/types/CacheStalenessApply.ts +13 -0
- package/src/lib/shared/types/CacheStalenessFrame.ts +24 -0
- package/src/lib/shared/types/DocSnapshots.ts +12 -0
- package/src/lib/shared/types/RemoteFunction.ts +11 -8
- package/src/lib/shared/types/RpcBuildStamps.ts +9 -0
- package/src/lib/shared/types/SsrPayload.ts +5 -0
- package/src/lib/test/createTestApp.ts +15 -0
- package/src/lib/ui/compile/COMPOUND_ASSIGNMENT_OPERATORS.ts +19 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +2 -0
- package/src/lib/ui/compile/analyzeComponent.ts +2 -0
- package/src/lib/ui/compile/compileComponent.ts +3 -1
- package/src/lib/ui/compile/compileSSR.ts +12 -0
- package/src/lib/ui/compile/compileShadow.ts +97 -16
- package/src/lib/ui/compile/desugarSignals.ts +60 -32
- package/src/lib/ui/compile/generateBuild.ts +5 -1
- package/src/lib/ui/compile/hasTopLevelAwait.ts +28 -0
- package/src/lib/ui/compile/isFunctionScopeBoundary.ts +20 -0
- package/src/lib/ui/compile/liftAsyncSubExpressions.ts +2 -2
- package/src/lib/ui/compile/lowerContext.ts +4 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +2 -14
- package/src/lib/ui/compile/lowerScript.ts +17 -2
- package/src/lib/ui/compile/renameSignalRefs.ts +83 -0
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
- package/src/lib/ui/compile/wrapReactionCellSources.ts +32 -0
- package/src/lib/ui/createScope.ts +42 -1
- package/src/lib/ui/dom/appendText.ts +29 -8
- package/src/lib/ui/dom/appendTextAt.ts +21 -4
- package/src/lib/ui/dom/assertClaimedText.ts +14 -4
- package/src/lib/ui/dom/attr.ts +41 -3
- package/src/lib/ui/dom/awaitBlock.ts +34 -13
- package/src/lib/ui/dom/each.ts +10 -2
- package/src/lib/ui/dom/eachAsync.ts +9 -1
- package/src/lib/ui/dom/readCellBlocking.ts +29 -0
- package/src/lib/ui/dom/readTextOrSuspend.ts +21 -0
- package/src/lib/ui/dom/spreadAttrs.ts +47 -1
- package/src/lib/ui/dom/switchBlock.ts +19 -2
- package/src/lib/ui/dom/tryBlock.ts +10 -0
- package/src/lib/ui/dom/when.ts +19 -1
- package/src/lib/ui/dom/withSuspense.ts +22 -0
- package/src/lib/ui/dom/writeCell.ts +22 -0
- package/src/lib/ui/runtime/DOC_SEED.ts +11 -0
- package/src/lib/ui/runtime/SuspenseSignal.ts +24 -0
- package/src/lib/ui/runtime/claimExpected.ts +6 -1
- package/src/lib/ui/runtime/flushEffects.ts +18 -10
- package/src/lib/ui/runtime/reportHydrationDivergence.ts +30 -0
- package/src/lib/ui/startClient.ts +22 -1
- package/src/lib/ui/subscribeCacheStaleness.ts +85 -0
- package/src/lib/ui/watch.ts +7 -2
- package/src/serverEntry.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.53.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- shadow narrows streaming cells to T|undefined + D7 barrier tripwire (ADR-0042 D5.2/D7) ([`034a384`](https://github.com/briancray/abide/commit/034a384bdfb5b482f818de757e10c2e2e88ac52c))
|
|
8
|
+
- suspend interpolations/attributes on a pending blocking read (ADR-0042) ([`88da27c`](https://github.com/briancray/abide/commit/88da27c998666b7cca9149effd5f171f3adca8d1))
|
|
9
|
+
- flip async-thunk blocking marker + emit $$readCellBlocking on the client (ADR-0042 D5/D6) ([`a3468fe`](https://github.com/briancray/abide/commit/a3468feacbb67ac9cd337ffa2e220b10af309324))
|
|
10
|
+
- dormant client-suspense foundation for await-means-resolved (ADR-0042) ([`b5f6104`](https://github.com/briancray/abide/commit/b5f6104c41ea1d57d3347ae007cdb1b316a09784))
|
|
11
|
+
- complete the client suspense withhold surface across all template effects (ADR-0042) ([`c5db314`](https://github.com/briancray/abide/commit/c5db314fa3e0ce18294f609be5fb43e5128970d0))
|
|
12
|
+
- fa7bfed: `await` now means "resolved" for a cell and its dependents, with the client suspending to mirror the server flush barrier (ADR-0042). A blocking `await` read (`{await foo()}`, `const x = state.computed(await …)`, `{#if await …}`) reads as a resolved value on both sides: the server already blocks the flush until it settles, and the client now **withholds the reading region** (interpolation, attribute, `{#if}`/`{#each}`/`{#switch}` block) until the value resolves rather than rendering against a pending `undefined`. So `{sources.length}` on an `await` binding no longer needs `?.` and no longer crashes while pending — the region simply shows nothing until it settles, then reveals. After the first resolution a refetch never re-suspends (stale-while-revalidate); on hydrate the warm-seed keeps the read `refreshing()`, never `pending()`, so there is no flash.
|
|
13
|
+
|
|
14
|
+
**Breaking:**
|
|
15
|
+
|
|
16
|
+
- **The async modifier alone is no longer a blocking marker — `await` is.** `computed(async () => getFoo())` (an `async` thunk with no top-level `await`) now creates a **streaming** cell (ships pending, resolves on the client) instead of a blocking one. Add `await` — `computed(async () => await getFoo())` — to keep the blocking/inline-SSR behavior. `computed(await …)` and `computed(async () => await …)` are unchanged (blocking).
|
|
17
|
+
- **A streaming (no-`await`) async `computed`/`linked` now types as `T | undefined`.** Its read is honestly pending-able, so guard it with `?.`/`??` (matching the runtime). A blocking `await` binding stays `T`.
|
|
18
|
+
|
|
19
|
+
No new public API — the change is in how `await` lowers and types.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- document await-means-resolved (ADR-0042) ([`16ec341`](https://github.com/briancray/abide/commit/16ec341a6ea7da509919b37a1cdcc2f237e04c1c))
|
|
24
|
+
- close await-suspense gaps and simplify the boundary (ADR-0042 review) ([`7f9fcbb`](https://github.com/briancray/abide/commit/7f9fcbbec862eba6da3103a6aca77d505ef6f58d))
|
|
25
|
+
- type {:catch} bindings as unknown, not any (ADR-0042) ([`9a91522`](https://github.com/briancray/abide/commit/9a915229caf92914b5bcd9e5625153831783049d))
|
|
26
|
+
- withhold pending blocking reads in {#await}/{#for await} subjects (ADR-0042) ([`e58b538`](https://github.com/briancray/abide/commit/e58b538f477c2bfcec654fd1a7aab65466cc956c))
|
|
27
|
+
|
|
28
|
+
## 0.52.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- 83f4e5e: warn on the inert watch(cell.member) source ([`554d3ff`](https://github.com/briancray/abide/commit/554d3ffbdef39a40ccb77c58017b759dbdcb17c2))
|
|
33
|
+
- e0c200b: `abide check` now type-checks the project's `.ts` files too, not only `.abide` templates. The `.ts` files were already loaded into the shadow program to resolve component imports, but their own diagnostics were never reported — so a mistyped `navigate('/p/[id]', {})`, `url()`, or `fn.patch()` call (or any type error) in an rpc handler, `app.ts`, or a `$shared` helper passed `abide check` silently. It now surfaces there, making `abide check` a complete type-check (templates + `.ts`) rather than templates only. Scoped to the project's own root files — a dependency or monorepo sibling resolved on demand is not reported. No new public API.
|
|
34
|
+
- 0858cd6: add `/completions <bash|zsh|fish>` to the generated CLI binary — prints a shell completion script that tab-completes command names and their `--flags` (with `--no-<flag>` for booleans). Candidates derive from the baked manifest via a live `/completions --query` callback, so completion always reflects the binary's real surface and can't drift from dispatch. No new public export.
|
|
35
|
+
- f42c33c: Add a path-addressed hydration-divergence signal on the DEBUG-gated `hydrate` channel (`DEBUG=hydrate`, or `localStorage['abide-debug'] = 'hydrate'` in the browser). With the channel off, behavior is unchanged — a claim divergence still throws the hard guard and the router recovers cold. With it on: a **text** divergence warns with the render-path of the enclosing component/branch/row and keeps hydrating, so one reload surfaces every mismatch instead of aborting at the first; a **structural** divergence names its path before throwing; and an **attribute** divergence — previously invisible, since binding always overwrote the server value — now warns too. The render-path is coarse (it locates the component, the detail names the value) and the compiled class/style/prop setters aren't covered yet. No new public API.
|
|
36
|
+
- fca2f95: add `abide/shared/invalidate` and broadcast both staleness verbs across clients (ADR-0041).
|
|
37
|
+
|
|
38
|
+
`invalidate(selector?, args?)` is reintroduced as the distinct DROP verb (next read reloads lazily; a mounted retained reader revalidates stale-in-place), paired with the existing `refresh` REFETCH verb. Instance sugar `fn.invalidate(args?)` sits beside `fn.refresh(args?)`.
|
|
39
|
+
|
|
40
|
+
Both verbs are now isomorphic: applied locally on the client, and on the SERVER they broadcast to every connected client over a reserved, server-publish-only `__abide/cache` socket (live-only, no replay — an offline client falls back to SWR staleness). **`refresh`'s server behaviour changed** from a local throwaway refetch that reached zero browsers to a cross-client broadcast — a semantic change to an existing public verb. Producer/closure and bare match-all selectors are not cross-client serializable and are rejected when broadcast from the server. The `__abide/*` socket namespace is now reserved (a user socket file may not declare it).
|
|
41
|
+
|
|
42
|
+
- 1c1d115: add `abide/server/render` — `render(path, params?, query?)` renders a page route to its HTML string in-process, through the same pipeline (app.html shell, layout chain, params, inline rpc reads) an HTTP GET of that URL runs, so a page stays directly linkable and its emailed form is one call away. Arg shape mirrors `url()`/`navigate`.
|
|
43
|
+
- ee96a2e: Plain `state(initial)` now warm-seeds from the server across hydration, so a nondeterministic initializer carries the server's value through instead of recomputing a divergent one on the client. `state(crypto.randomUUID())`, `state(Date.now())`, `state(Math.random())` — previously the client re-ran the initializer on hydrate and produced a different value than the SSR HTML (a divergence class the `hydrate` channel now reports). Each rendered scope's document snapshot is serialized into `__SSR__.docs` keyed by its render-path id (the same keying the async-cell warm-seed uses), and on hydration the first write to each slot adopts the server value (consume-once) while the throwaway fresh init is discarded. No new public API — same `state()` call, same authoring. Values that don't round-trip the wire codec (functions, class instances) or state read only client-side fall back to a cold init, unchanged.
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- 83f4e5e: generate .d.ts augmentations before type-checking .ts files ([`5da85e2`](https://github.com/briancray/abide/commit/5da85e2a2f411c221bb552b60b187ebee836d3ba))
|
|
48
|
+
- 83f4e5e: forward a 0-based completion index from the zsh wrapper ([`6d14ccd`](https://github.com/briancray/abide/commit/6d14ccd3aaa8467948fe78dbc4462b40c6ea6750))
|
|
49
|
+
- 83f4e5e: dedupe mirrored maps, skip the inert reaction pass ([`b938e10`](https://github.com/briancray/abide/commit/b938e1058b10d0fa7881d9cd4e3f774bb1dc35c2))
|
|
50
|
+
- 83f4e5e: reflect .ts checking, the hydrate channel, and state seeding ([`e97f0e0`](https://github.com/briancray/abide/commit/e97f0e0c3b4c5bafde99335663cb88d476ff64cc))
|
|
51
|
+
- 83f4e5e: make the bare seed the idiomatic computed/linked form ([`f889416`](https://github.com/briancray/abide/commit/f889416eedab23346c870d491c75b7e0f361bd8e))
|
|
52
|
+
- 83f4e5e: document invalidate, server-side broadcast, and CLI completions ([`fccc4b4`](https://github.com/briancray/abide/commit/fccc4b4e833ea49871c1a955743ce807d1902a28))
|
|
53
|
+
- 090d28e: `abide check` (and the editor shadow) now type-check a `computed`/`linked` seed written with a top-level `await` — `state.computed(await load())` — as its resolved value, instead of flagging a spurious "top-level await" error and mis-typing the binding (which forced `as unknown as Awaited<…>` casts downstream). The shadow now mirrors the compiler's `wrapSeed` lowering: a non-thunk seed is normalised to a thunk (async when it carries a top-level await) and read through a helper that unwraps a promise/stream to the value a bare cell read peeks. A bare `state(await …)` — not a `computed`/`linked` seed — is still correctly flagged, since it genuinely breaks the synchronous build.
|
|
54
|
+
- 129b404: Writing to a `linked` cell from a component `<script>` now works — `let draft = state.linked(() => source); draft = draft + 1` (and `+=`, `++`, `--`, `??=`). Previously the compiler lowered every `linked` reference through `$$readCell(name)` (a call), so an assignment target became `$$readCell(draft) = …` and the build failed with "Invalid assignment target" — only `bind:value={draft}` could write a linked cell, despite `linked` being documented as writable. Assignments to a `linked` binding now lower through a new `$$writeCell` helper (`abide/ui/dom/writeCell`) that dispatches `.value =` for a sync `State` seed and `.set(...)` for an async/stream `AsyncState` seed.
|
|
55
|
+
- 9154f36: The client build of an `$rpc` module now emits a minimal module — the `remoteProxy` fetch plus only the top-level statements the endpoint `opts` transitively reaches — instead of keeping the whole handler file and trusting tree-shaking to drop the server bits. Previously, server code reachable from module-level handler support (the common `const db = getDb()` shape, where `getDb` imports `bun:sqlite`) survived handler-elision and was _loaded_ on the browser target, failing the build with a raw `Browser build cannot import Bun builtin: "bun:sqlite"` before abide's own reachability guard could give a guided message. Reachability is resolved through the warm rpc `ts.Program` (binder/checker), so an `opts` value built from a module-level const — `schemas: { input: inputSchema }` where `const inputSchema = z.object(...)` — keeps that const and its imports, while anything only the handler touches is never emitted. No public API change; the reachability guard for every other client-reaches-server edge is unchanged, and builds with no warm program fall back to the previous keep-the-file transform.
|
|
56
|
+
|
|
3
57
|
## 0.51.0
|
|
4
58
|
|
|
5
59
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**A type-safe isomorphic framework where one typed declaration is an HTTP endpoint, a CLI subcommand, an MCP tool, and an OpenAPI operation at once.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You write a handler once; abide fans it out across every surface, and the bundler swaps the runtime per side — the same call reads in-process during SSR and becomes a typed `fetch` in the browser. Built for humans _and_ machines, on a single runtime.
|
|
6
6
|
|
|
7
7
|
- One direct dependency (TypeScript), one runtime (Bun ≥ 1.3.0).
|
|
8
|
-
- No barrels — every public name is its own module path (`@abide/abide/server/GET`, `@abide/abide/ui/state`, …). The namespace marks the side: `server/*` runs server-side, `ui/*` client-side, `shared/*` isomorphic.
|
|
9
8
|
|
|
10
9
|
## Quick start
|
|
11
10
|
|
|
12
11
|
```sh
|
|
13
|
-
|
|
14
|
-
bunx abide scaffold my-app
|
|
12
|
+
bunx abide scaffold my-app # scaffold, install, and start the dev server
|
|
15
13
|
```
|
|
16
14
|
|
|
17
|
-
Or clone the kitchen-sink example, which exercises the whole surface:
|
|
18
|
-
|
|
19
15
|
```sh
|
|
16
|
+
# or explore the full feature tour
|
|
20
17
|
git clone https://github.com/briancray/abide
|
|
21
18
|
cd abide/examples/kitchen-sink
|
|
22
19
|
bun install
|
|
@@ -25,59 +22,44 @@ bun run dev
|
|
|
25
22
|
|
|
26
23
|
## RPCs
|
|
27
24
|
|
|
28
|
-
An RPC is one export per file under `src/server/rpc/`. The file path is the URL; the
|
|
25
|
+
An RPC is one export per file under `src/server/rpc/`. The file path is the URL; the handler's typed input drives everything downstream. Any [Standard Schema](https://standardschema.dev) (zod, valibot, arktype — unadapted) validates the arguments and projects the MCP tool, CLI flags, and OpenAPI operation.
|
|
29
26
|
|
|
30
27
|
```ts
|
|
31
28
|
// src/server/rpc/getMessages.ts
|
|
32
29
|
import { GET } from '@abide/abide/server/GET'
|
|
33
30
|
import { json } from '@abide/abide/server/json'
|
|
34
31
|
import { z } from 'zod'
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}),
|
|
43
|
-
},
|
|
44
|
-
})
|
|
32
|
+
import { recentMessages } from '../db.ts'
|
|
33
|
+
|
|
34
|
+
// The success body's type flows back to every caller; `cache` retains the read.
|
|
35
|
+
export const getMessages = GET(
|
|
36
|
+
async ({ limit }: { limit: number }) => json(await recentMessages(limit)),
|
|
37
|
+
{ schemas: { input: z.object({ limit: z.number().int().max(100) }) }, cache: { ttl: 5_000 } },
|
|
38
|
+
)
|
|
45
39
|
```
|
|
46
40
|
|
|
47
|
-
One declaration
|
|
41
|
+
One declaration branches to every surface:
|
|
48
42
|
|
|
49
43
|
```text
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
SSR call browser
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
export const getMessages = GET(handler, { schemas })
|
|
45
|
+
│
|
|
46
|
+
┌──────────┬──────────────┼──────────────┬──────────────┐
|
|
47
|
+
▼ ▼ ▼ ▼ ▼
|
|
48
|
+
SSR call browser MCP tool CLI OpenAPI op
|
|
49
|
+
the bare fetch (read-only, subcommand in
|
|
50
|
+
call, typed proxy auto-exposed) abide cli /openapi.json
|
|
51
|
+
in-process fn(args) getMessages
|
|
57
52
|
```
|
|
58
53
|
|
|
59
|
-
A typed input
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
// src/server/rpc/sendMessage.ts
|
|
63
|
-
import { POST } from '@abide/abide/server/POST'
|
|
64
|
-
import { json } from '@abide/abide/server/json'
|
|
65
|
-
import { z } from 'zod'
|
|
66
|
-
import { append } from '$server/db.ts'
|
|
67
|
-
|
|
68
|
-
export const sendMessage = POST((args) => json(append(args.channel, args.body)), {
|
|
69
|
-
schemas: { input: z.object({ channel: z.string(), body: z.string() }) },
|
|
70
|
-
clients: { mcp: true }, // a mutating RPC must opt into MCP by hand
|
|
71
|
-
})
|
|
72
|
-
```
|
|
54
|
+
A **typed handler input** is what flips those surfaces on: the input type is projected to JSON Schema at build, so a plainly-typed handler auto-exposes to the CLI and — for read-only methods (`GET`/`HEAD`) — MCP with no hand-written `schemas.input` (a declared `schemas.input` adds runtime validation on top). A mutating method (`POST`/`PUT`/`PATCH`/`DELETE`) never auto-exposes to MCP; opt it in with `clients: { mcp: true }`.
|
|
73
55
|
|
|
74
|
-
Consume
|
|
56
|
+
Consume it by importing the export. The **bare call `getMessages(args)` is the smart read** — cached, coalesced, reactive, isomorphic (in-process on the server, `fetch` in the browser). Around it sit `getMessages.raw(args, opts?)` for the raw `Response`, the mutators/probes `.refresh()` / `.invalidate()` / `.patch(...)` / `.peek()` / `.pending()` / `.refreshing()` / `.error()`, and — when the handler streams (`jsonl`/`sse`) — a bare call that returns an `AsyncIterable`.
|
|
75
57
|
|
|
76
|
-
> Query and
|
|
58
|
+
> Query, path, and form args auto-coerce from the endpoint's typed shape at build, so a numeric/boolean/date field arrives already typed — no `z.coerce` (a value that won't parse stays a string, so the schema raises an honest 422). The per-RPC `timeout` (504 on every surface) is distinct from the client transport ceiling `ABIDE_CLIENT_TIMEOUT`.
|
|
77
59
|
|
|
78
60
|
## Sockets
|
|
79
61
|
|
|
80
|
-
A socket is one broadcast topic per file under `src/server/sockets/`. A `Socket<T>` is an isomorphic `AsyncIterable<T>` —
|
|
62
|
+
A socket is one broadcast topic per file under `src/server/sockets/`. A `Socket<T>` is an isomorphic `AsyncIterable<T>` — iterate it for the live stream — and every socket multiplexes onto one WebSocket at `/__abide/sockets`.
|
|
81
63
|
|
|
82
64
|
```ts
|
|
83
65
|
// src/server/sockets/chat.ts
|
|
@@ -85,171 +67,162 @@ import { socket } from '@abide/abide/server/socket'
|
|
|
85
67
|
import { z } from 'zod'
|
|
86
68
|
|
|
87
69
|
export const chat = socket({
|
|
88
|
-
schema: z.object({
|
|
89
|
-
tail:
|
|
90
|
-
ttl: 60_000, // evict retained frames older than 60s
|
|
70
|
+
schema: z.object({ user: z.string(), text: z.string() }),
|
|
71
|
+
tail: 50, // retain the last 50 frames so late joiners / reconnects seed from `.tail()`
|
|
72
|
+
ttl: 60_000, // evict retained frames older than 60s before replay
|
|
91
73
|
})
|
|
92
74
|
```
|
|
93
75
|
|
|
94
|
-
|
|
76
|
+
`chat.publish(frame)` is isomorphic — server code fans out in-process and to remote subscribers; client code sends a `pub` frame (gated by `clientPublish`, off by default). The HTTP face is `/__abide/sockets/<name>`: a `GET` returns the retained tail, a `POST` publishes.
|
|
95
77
|
|
|
96
78
|
## Components
|
|
97
79
|
|
|
98
|
-
A `.abide` component is HTML with a
|
|
99
|
-
|
|
100
|
-
Async data has no ceremony: **the bare call in an interpolation — `{getMessages({ limit })}` — is the way to read it.** It's a _peek_ that reads `undefined` while pending (and auto-streams on SSR), so it composes with `?? fallback`, `?.`, `{#if}`, and attributes with no wrapping block; pair it with the `.pending()` / `.error()` probes for loading and error affordances, and use `{await getMessages(...)}` to block SSR until the value is in the initial HTML. The `{#await}…{:then}…{:catch}…{:finally}…{/await}` block is the explicit opt-in — reach for it only when you want a distinct pending branch, a local `{:catch}`, or `{:then}` type-narrowing.
|
|
80
|
+
A `.abide` component is valid HTML with a `<script>`, reactive primitives imported by their own module paths, and mustache control-flow blocks. The page below imports the RPC and socket above and exercises the whole grammar:
|
|
101
81
|
|
|
102
82
|
```html
|
|
103
83
|
<script>
|
|
104
|
-
import { getMessages } from '$server/rpc/getMessages
|
|
105
|
-
import { sendMessage } from '$server/rpc/sendMessage
|
|
106
|
-
import {
|
|
107
|
-
import
|
|
84
|
+
import { getMessages } from '$server/rpc/getMessages'
|
|
85
|
+
import { sendMessage } from '$server/rpc/sendMessage'
|
|
86
|
+
import { countToday } from '$server/rpc/countToday'
|
|
87
|
+
import { chat } from '$server/sockets/chat'
|
|
88
|
+
import Card from '$ui/Card.abide'
|
|
89
|
+
import Avatar from '$ui/Avatar.abide'
|
|
90
|
+
import Message from '$ui/Message.abide'
|
|
108
91
|
import { state } from '@abide/abide/ui/state'
|
|
109
92
|
import { watch } from '@abide/abide/ui/watch'
|
|
110
93
|
import { html } from '@abide/abide/ui/html'
|
|
111
94
|
import { props } from '@abide/abide/ui/props'
|
|
112
95
|
|
|
113
|
-
const { title = 'Chat'
|
|
114
|
-
|
|
115
|
-
type Message = { id: string; author: string; body: string }
|
|
96
|
+
const { title = 'Chat' } = props()
|
|
116
97
|
|
|
117
|
-
let
|
|
98
|
+
let limit = state(20) // writable cell, read/reassigned as a plain variable
|
|
118
99
|
let draft = state('')
|
|
119
|
-
let
|
|
120
|
-
let
|
|
100
|
+
let notify = state(true)
|
|
101
|
+
let channel = state('general')
|
|
102
|
+
const trimmed = state.computed(() => draft.trim()) // read-only derived
|
|
103
|
+
const shown = state.linked(() => limit) // writable, reseeded when `limit` changes
|
|
121
104
|
|
|
122
|
-
|
|
123
|
-
|
|
105
|
+
const rootAttrs = { role: 'log' }
|
|
106
|
+
const extra = { compact: true }
|
|
107
|
+
const get = () => draft.toUpperCase()
|
|
108
|
+
const set = (next: string) => { draft = next }
|
|
124
109
|
|
|
125
|
-
|
|
110
|
+
// watch — the single reaction primitive: over a cell, then over a live socket.
|
|
111
|
+
watch(limit, (n) => console.log('limit is now', n))
|
|
112
|
+
watch(chat, (frame) => console.log('new message', frame.text))
|
|
126
113
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
})
|
|
114
|
+
function focus(node: HTMLElement) { node.focus() }
|
|
115
|
+
function connection() { return chat.pending() ? 'connecting' : 'open' }
|
|
116
|
+
function risky() { return draft.at(999)!.length }
|
|
131
117
|
|
|
132
|
-
async function
|
|
133
|
-
|
|
134
|
-
if (trimmed === '') return
|
|
135
|
-
await sendMessage({ channel, body: trimmed })
|
|
118
|
+
async function send() {
|
|
119
|
+
await sendMessage({ text: trimmed }) // a mutating RPC from an event handler
|
|
136
120
|
draft = ''
|
|
137
121
|
}
|
|
138
|
-
|
|
139
|
-
function autofocus(node: HTMLInputElement) {
|
|
140
|
-
node.focus()
|
|
141
|
-
return () => {}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const rowProps = { class: 'flex gap-2' }
|
|
145
|
-
|
|
146
|
-
// Derived two-way binding: read a string, coerce writes back into the numeric cell.
|
|
147
|
-
const get = () => String(limit)
|
|
148
|
-
const set = (next: string) => (limit = Number(next))
|
|
149
122
|
</script>
|
|
150
123
|
|
|
151
|
-
<section
|
|
152
|
-
<h1>{title}
|
|
153
|
-
|
|
154
|
-
<form onsubmit={submit}>
|
|
155
|
-
<input bind:value={draft} attach={autofocus} placeholder="Say something" />
|
|
156
|
-
<label><input type="checkbox" bind:checked={pinned} /> pin</label>
|
|
157
|
-
<label><input type="radio" bind:group={channel} value="general" /> general</label>
|
|
158
|
-
<label><input type="radio" bind:group={channel} value="random" /> random</label>
|
|
159
|
-
<input bind:value={{ get, set }} />
|
|
160
|
-
<button type="submit">Send</button>
|
|
161
|
-
</form>
|
|
162
|
-
|
|
163
|
-
{#snippet row(message: Message, index: number)}
|
|
164
|
-
<MessageCard {...rowProps} name={message.author} onclick={() => console.log(index)}>
|
|
165
|
-
<p>{message.body}</p>
|
|
166
|
-
</MessageCard>
|
|
167
|
-
{/snippet}
|
|
124
|
+
<section class="chat" class:empty={limit === 0} style:--rows={shown} {...rootAttrs}>
|
|
125
|
+
<h1>{title}</h1>
|
|
168
126
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
<p>{live} updates</p>
|
|
173
|
-
{:else}
|
|
174
|
-
<script>
|
|
175
|
-
let seenAt = state(Date.now())
|
|
176
|
-
let ageLabel = state.computed(() => `waiting since ${seenAt}`)
|
|
177
|
-
</script>
|
|
178
|
-
<style>
|
|
179
|
-
p { color: gray; }
|
|
180
|
-
</style>
|
|
181
|
-
<p>{ageLabel}</p>
|
|
182
|
-
{/if}
|
|
183
|
-
|
|
184
|
-
<!-- The default: a bare peek — `undefined` while pending, composes with `?.`/`??`; probes for affordances. -->
|
|
185
|
-
<p>
|
|
127
|
+
<!-- A bare read STREAMS: it peeks `undefined` while pending, so it composes
|
|
128
|
+
with `??` and the probes. `await` is the marker for the other mode. -->
|
|
129
|
+
<p class:loading={getMessages.pending({ limit })}>
|
|
186
130
|
{getMessages({ limit })?.length ?? 0} messages
|
|
187
|
-
{#if getMessages.
|
|
188
|
-
{#if getMessages.error({ limit })} · failed{/if}
|
|
131
|
+
{#if getMessages.error({ limit })}<span>failed to load</span>{/if}
|
|
189
132
|
</p>
|
|
190
133
|
|
|
191
|
-
<!-- {await
|
|
192
|
-
|
|
134
|
+
<!-- `{await}` means RESOLVED: the server blocks the flush and the client suspends
|
|
135
|
+
this region until it settles, so the value is never pending at the read (no `?.`)
|
|
136
|
+
— the region just shows nothing, then reveals. -->
|
|
137
|
+
<small>today: {await countToday()}</small>
|
|
193
138
|
|
|
194
|
-
<!--
|
|
139
|
+
<!-- `{#await}` is the explicit opt-in — a distinct pending branch and `{:then}` narrowing. -->
|
|
195
140
|
{#await getMessages({ limit })}
|
|
196
|
-
<p>
|
|
141
|
+
<p>loading…</p>
|
|
197
142
|
{:then messages}
|
|
198
|
-
{#
|
|
199
|
-
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
143
|
+
{#if messages.length}
|
|
144
|
+
{#for message, i of messages by message.id}
|
|
145
|
+
<Message message={message} ondelete={() => sendMessage({ text: '' })} {...extra}>
|
|
146
|
+
<Avatar alt={message.user} />
|
|
147
|
+
</Message>
|
|
148
|
+
{/for}
|
|
149
|
+
{:else if limit > 0}
|
|
150
|
+
<p>no messages yet</p>
|
|
151
|
+
{:else}
|
|
152
|
+
<p>raise the limit</p>
|
|
153
|
+
{/if}
|
|
154
|
+
{:catch err}
|
|
155
|
+
<p>{err instanceof Error ? err.message : String(err)}</p>
|
|
203
156
|
{:finally}
|
|
204
157
|
<hr />
|
|
205
158
|
{/await}
|
|
206
159
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{:
|
|
217
|
-
|
|
160
|
+
<!-- live socket frames over an AsyncIterable -->
|
|
161
|
+
<ul>
|
|
162
|
+
{#for await frame of chat}
|
|
163
|
+
<li>{frame.user}: {frame.text}</li>
|
|
164
|
+
{/for}
|
|
165
|
+
</ul>
|
|
166
|
+
|
|
167
|
+
{#switch connection()}
|
|
168
|
+
{:case 'open'}<span>live</span>
|
|
169
|
+
{:case 'connecting'}<span>connecting…</span>
|
|
170
|
+
{:default}<span>offline</span>
|
|
218
171
|
{/switch}
|
|
219
172
|
|
|
220
173
|
{#try}
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
</
|
|
224
|
-
{:catch}
|
|
225
|
-
<p>Card crashed</p>
|
|
174
|
+
<p>{risky()}</p>
|
|
175
|
+
{:catch e}
|
|
176
|
+
<p>widget crashed: {e instanceof Error ? e.message : String(e)}</p>
|
|
226
177
|
{:finally}
|
|
227
|
-
<span
|
|
178
|
+
<span>rendered</span>
|
|
228
179
|
{/try}
|
|
180
|
+
|
|
181
|
+
<form onsubmit={send}>
|
|
182
|
+
<input name="text" bind:value={draft} />
|
|
183
|
+
<label><input type="checkbox" bind:checked={notify} /> notify</label>
|
|
184
|
+
<input type="radio" bind:group={channel} value="general" />
|
|
185
|
+
<input aria-label="shout" bind:value={{ get, set }} />
|
|
186
|
+
<button attach={focus}>send</button>
|
|
187
|
+
</form>
|
|
188
|
+
|
|
189
|
+
{#snippet stat(label: string, value: number)}
|
|
190
|
+
<dd>{label}: {value}</dd>
|
|
191
|
+
{/snippet}
|
|
192
|
+
<dl>{stat('shown', limit)}</dl>
|
|
193
|
+
|
|
194
|
+
<Card>
|
|
195
|
+
<p>{html`<em>${trimmed}</em>`}</p>
|
|
196
|
+
</Card>
|
|
197
|
+
|
|
198
|
+
{#if limit > 10}
|
|
199
|
+
<script>
|
|
200
|
+
// a nested branch script: branch-local state, re-seeded per mount, no imports
|
|
201
|
+
let expanded = state(false)
|
|
202
|
+
const label = state.computed(() => (expanded ? 'less' : 'more'))
|
|
203
|
+
</script>
|
|
204
|
+
<button onclick={() => (expanded = !expanded)}>{label}</button>
|
|
205
|
+
<style>
|
|
206
|
+
button { font-weight: 600; }
|
|
207
|
+
</style>
|
|
208
|
+
{/if}
|
|
229
209
|
</section>
|
|
230
210
|
|
|
231
211
|
<style>
|
|
232
|
-
|
|
233
|
-
display: grid;
|
|
234
|
-
gap: 0.5rem;
|
|
235
|
-
}
|
|
212
|
+
.chat { display: grid; gap: 0.5rem; }
|
|
236
213
|
</style>
|
|
237
214
|
```
|
|
238
215
|
|
|
239
|
-
The
|
|
216
|
+
The child component fills the passed content at `{children()}`; `{#if children}…{:else}…{/if}` is the fallback (there is no `<slot>` element, no named slots):
|
|
240
217
|
|
|
241
218
|
```html
|
|
242
219
|
<script>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const { name, children, ...rest } = props<{ name: string; children?: Snippet }>()
|
|
220
|
+
import { props } from '@abide/abide/ui/props'
|
|
221
|
+
const { children } = props()
|
|
246
222
|
</script>
|
|
247
223
|
|
|
248
|
-
<article
|
|
249
|
-
<
|
|
250
|
-
{#if children} {children()} {:else}
|
|
251
|
-
<em>No content</em>
|
|
252
|
-
{/if}
|
|
224
|
+
<article class="card">
|
|
225
|
+
{#if children}{children()}{:else}<p>empty</p>{/if}
|
|
253
226
|
</article>
|
|
254
227
|
```
|
|
255
228
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"./server/sse": "./src/lib/server/sse.ts",
|
|
46
46
|
"./server/error": "./src/lib/server/error.ts",
|
|
47
47
|
"./server/redirect": "./src/lib/server/redirect.ts",
|
|
48
|
+
"./server/render": "./src/lib/server/render.ts",
|
|
48
49
|
"./server/request": "./src/lib/server/request.ts",
|
|
49
50
|
"./server/cookies": "./src/lib/server/cookies.ts",
|
|
50
51
|
"./server/env": "./src/lib/server/env.ts",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"./server/prompts/definePrompt": "./src/lib/server/prompts/definePrompt.ts",
|
|
59
60
|
"./ui/watch": "./src/lib/ui/watch.ts",
|
|
60
61
|
"./shared/pending": "./src/lib/shared/pending.ts",
|
|
62
|
+
"./shared/invalidate": "./src/lib/shared/invalidate.ts",
|
|
61
63
|
"./shared/patch": "./src/lib/shared/patch.ts",
|
|
62
64
|
"./shared/peek": "./src/lib/shared/peek.ts",
|
|
63
65
|
"./shared/refresh": "./src/lib/shared/refresh.ts",
|
|
@@ -93,6 +95,8 @@
|
|
|
93
95
|
"./ui/dom/spreadAttrs": "./src/lib/ui/dom/spreadAttrs.ts",
|
|
94
96
|
"./ui/dom/readCall": "./src/lib/ui/dom/readCall.ts",
|
|
95
97
|
"./ui/dom/readCell": "./src/lib/ui/dom/readCell.ts",
|
|
98
|
+
"./ui/dom/readCellBlocking": "./src/lib/ui/dom/readCellBlocking.ts",
|
|
99
|
+
"./ui/dom/writeCell": "./src/lib/ui/dom/writeCell.ts",
|
|
96
100
|
"./ui/dom/cellPending": "./src/lib/ui/dom/cellPending.ts",
|
|
97
101
|
"./ui/runtime/withPath": "./src/lib/ui/runtime/withPath.ts",
|
|
98
102
|
"./ui/settleAsyncCells": "./src/lib/ui/settleAsyncCells.ts",
|
|
@@ -396,6 +396,17 @@ export function abideResolverPlugin({
|
|
|
396
396
|
program / unresolvable body / no structured field) bakes nothing — a response array
|
|
397
397
|
stays an array. */
|
|
398
398
|
const outputWirePlan = rpcServerProgram?.outputWirePlanForModule(args.path)
|
|
399
|
+
/* The client-only reachability plan: the top-level statements the emitted client
|
|
400
|
+
module retains, rooted at the endpoint `opts` (imports + declarations opts
|
|
401
|
+
transitively needs). Present → rewriteForClient emits a MINIMAL module so the
|
|
402
|
+
handler and its server-only imports are never emitted — nothing server-side loads
|
|
403
|
+
or trips the reachability guard (ADR-0022 addendum). Only the client target needs
|
|
404
|
+
it; the server rewrite keeps the whole handler. undefined (no warm program /
|
|
405
|
+
unresolvable call) falls open to the keep-the-file rewrite. */
|
|
406
|
+
const clientKeep =
|
|
407
|
+
target === 'client'
|
|
408
|
+
? rpcServerProgram?.clientKeepForModule(args.path)
|
|
409
|
+
: undefined
|
|
399
410
|
/* The handler's input args projected to JSON Schema, stamped as `inputJsonSchema` so the
|
|
400
411
|
runtime OpenAPI parameters/body / MCP inputSchema / inspector input surface renders the
|
|
401
412
|
real input shape (and the endpoint becomes advertisable) without an author-declared
|
|
@@ -410,6 +421,7 @@ export function abideResolverPlugin({
|
|
|
410
421
|
outputSchema: returnBodySchema,
|
|
411
422
|
errorSchemas,
|
|
412
423
|
outputWirePlan,
|
|
424
|
+
clientKeep,
|
|
413
425
|
})
|
|
414
426
|
if (!prepared) {
|
|
415
427
|
throw new Error(
|
package/src/checkAbide.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import ts from 'typescript'
|
|
3
|
+
import { generateDeclarations } from './lib/shared/generateDeclarations.ts'
|
|
3
4
|
import { collectAbideDiagnostics } from './lib/ui/compile/collectAbideDiagnostics.ts'
|
|
4
5
|
import { createShadowProgram, type ShadowProgram } from './lib/ui/compile/createShadowProgram.ts'
|
|
5
6
|
import { interpolationClassifierForRoot } from './lib/ui/compile/interpolationClassifierForRoot.ts'
|
|
@@ -15,6 +16,13 @@ the same as each package checked on its own — and the same as the LSP. Returns
|
|
|
15
16
|
the error count so the CLI can set its exit code.
|
|
16
17
|
*/
|
|
17
18
|
export async function checkAbide({ cwd }: { cwd: string }): Promise<number> {
|
|
19
|
+
/* Materialize the project's generated augmentations (src/.abide/*.d.ts) first: since the
|
|
20
|
+
check pass now type-checks the project's `.ts` files too (rpc handlers, app.ts, tests),
|
|
21
|
+
those resolve `app.rpc.<name>` / typed `url('/rpc/…')` off the build-written RpcClient /
|
|
22
|
+
RpcRoutes augmentations. Without this, a cold `abide check` (CI, fresh clone — `.abide` is
|
|
23
|
+
gitignored, generated only by dev/build) reports every augmented member as missing. Fails
|
|
24
|
+
open, so a codegen hiccup degrades to the pre-`.ts`-check behaviour rather than aborting. */
|
|
25
|
+
await generateDeclarations({ cwd })
|
|
18
26
|
const { diagnostics, checked } = collectByProject(cwd)
|
|
19
27
|
const byFile = new Map<string, AbideDiagnostic[]>()
|
|
20
28
|
for (const diagnostic of diagnostics) {
|
|
@@ -64,17 +72,62 @@ function collectByProject(cwd: string): { diagnostics: AbideDiagnostic[]; checke
|
|
|
64
72
|
WHICH sub-expressions are async must read un-wrapped shadows — so it rides a separate verbatim
|
|
65
73
|
program, built (and reused) here per root. */
|
|
66
74
|
const classifierCache = new Map<string, ShadowProgram | undefined>()
|
|
67
|
-
const diagnostics = [...byProject].flatMap(([root, paths]) =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
const diagnostics = [...byProject].flatMap(([root, paths]) => {
|
|
76
|
+
const shadow = createShadowProgram(root, paths, (abidePath) =>
|
|
77
|
+
interpolationClassifierForRoot(classifierCache, root, abidePath),
|
|
78
|
+
)
|
|
79
|
+
/* The shadow program already holds the project's real `.ts` files (loaded so the
|
|
80
|
+
components' imports/types resolve), but `collectAbideDiagnostics` only reports the
|
|
81
|
+
`.abide` shadows. Report the `.ts` files too, so a mistyped `navigate`/`url`/`patch`
|
|
82
|
+
call — or any type error — in an rpc handler, `app.ts`, or a `$shared` helper fails
|
|
83
|
+
`abide check` instead of only surfacing under a separately-run `tsc`. */
|
|
84
|
+
return [...collectAbideDiagnostics(shadow), ...collectTsDiagnostics(shadow.program)]
|
|
85
|
+
})
|
|
74
86
|
const checked = [...byProject.values()].reduce((total, paths) => total + paths.length, 0)
|
|
75
87
|
return { diagnostics, checked }
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
/* Syntactic + semantic diagnostics for the project's own `.ts` files already in the shadow
|
|
91
|
+
program. Iterating the ROOT file names (the project's tsconfig inputs) — not every loaded
|
|
92
|
+
source — scopes this to the project itself: an on-demand-resolved import from node_modules or a
|
|
93
|
+
monorepo sibling is in the program but never a root, so its errors aren't attributed here.
|
|
94
|
+
`fileExists` drops the virtual `.abide.ts` shadows and the synthetic asset-modules file (neither
|
|
95
|
+
is on disk), `isDeclarationFile` drops the default libs and the generated `src/.abide/*.d.ts`,
|
|
96
|
+
and the `.ts`/`.tsx` guard drops raw `.abide` root entries. Real source coordinates — no remap. */
|
|
97
|
+
function collectTsDiagnostics(program: ts.Program): AbideDiagnostic[] {
|
|
98
|
+
const diagnostics: AbideDiagnostic[] = []
|
|
99
|
+
for (const rootName of program.getRootFileNames()) {
|
|
100
|
+
if (
|
|
101
|
+
(!rootName.endsWith('.ts') && !rootName.endsWith('.tsx')) ||
|
|
102
|
+
rootName.includes('/node_modules/') ||
|
|
103
|
+
!ts.sys.fileExists(rootName)
|
|
104
|
+
) {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
const sourceFile = program.getSourceFile(rootName)
|
|
108
|
+
if (sourceFile === undefined || sourceFile.isDeclarationFile) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
const raw = [
|
|
112
|
+
...program.getSyntacticDiagnostics(sourceFile),
|
|
113
|
+
...program.getSemanticDiagnostics(sourceFile),
|
|
114
|
+
]
|
|
115
|
+
for (const diagnostic of raw) {
|
|
116
|
+
if (diagnostic.start === undefined) {
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
diagnostics.push({
|
|
120
|
+
file: sourceFile.fileName,
|
|
121
|
+
start: diagnostic.start,
|
|
122
|
+
length: diagnostic.length ?? 0,
|
|
123
|
+
message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
|
|
124
|
+
category: diagnostic.category,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return diagnostics
|
|
129
|
+
}
|
|
130
|
+
|
|
78
131
|
/* Renders one diagnostic as `path:line:col severity message` plus the offending
|
|
79
132
|
line and a caret underline spanning the mapped range. */
|
|
80
133
|
function printDiagnostic(file: string, text: string, diagnostic: AbideDiagnostic): void {
|