@abide/abide 0.41.0 → 0.42.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 +333 -326
- package/CHANGELOG.md +92 -0
- package/README.md +91 -83
- package/package.json +1 -1
- package/src/abideLsp.ts +46 -8
- package/src/abideResolverPlugin.ts +3 -5
- package/src/lib/server/runtime/devClientFingerprint.ts +2 -1
- package/src/lib/shared/escapeRegex.ts +9 -0
- package/src/lib/shared/fileName.ts +9 -0
- package/src/lib/shared/fileStem.ts +3 -1
- package/src/lib/shared/programNameForPackage.ts +3 -1
- package/src/lib/shared/stripImport.ts +3 -1
- package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
- package/src/lib/ui/compile/SSR_ESCAPE.ts +3 -1
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/compileShadow.ts +46 -9
- package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
- package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
- package/src/lib/ui/compile/generateBuild.ts +13 -6
- package/src/lib/ui/compile/generateSSR.ts +23 -14
- package/src/lib/ui/compile/makeVarNamer.ts +10 -0
- package/src/lib/ui/compile/offsetToPosition.ts +9 -0
- package/src/lib/ui/compile/parseTemplate.ts +524 -104
- package/src/lib/ui/compile/skeletonContext.ts +67 -79
- package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
- package/src/lib/ui/compile/templateAnchorAdapter.ts +61 -0
- package/src/lib/ui/compile/templateElementAdapter.ts +44 -0
- package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +9 -0
- package/src/lib/ui/compile/walkAnchorOrder.ts +57 -0
- package/src/lib/ui/compile/walkElementOrder.ts +60 -0
- package/src/lib/ui/dom/appendSnippet.ts +9 -8
- package/src/lib/ui/dom/appendText.ts +1 -5
- package/src/lib/ui/dom/applyResolved.ts +4 -9
- package/src/lib/ui/dom/awaitBlock.ts +37 -31
- package/src/lib/ui/dom/buildDetachedRange.ts +30 -0
- package/src/lib/ui/dom/depthZeroNodes.ts +34 -0
- package/src/lib/ui/dom/domAnchorAdapter.ts +29 -0
- package/src/lib/ui/dom/domElementAdapter.ts +20 -0
- package/src/lib/ui/dom/each.ts +7 -11
- package/src/lib/ui/dom/eachAsync.ts +12 -17
- package/src/lib/ui/dom/isComment.ts +6 -0
- package/src/lib/ui/dom/isElement.ts +6 -0
- package/src/lib/ui/dom/markerDepthDelta.ts +19 -0
- package/src/lib/ui/dom/mountRange.ts +4 -3
- package/src/lib/ui/dom/mountSlot.ts +4 -3
- package/src/lib/ui/dom/on.ts +6 -1
- package/src/lib/ui/dom/replaceRange.ts +24 -0
- package/src/lib/ui/dom/skeleton.ts +35 -92
- package/src/lib/ui/dom/switchBlock.ts +13 -10
- package/src/lib/ui/dom/when.ts +13 -10
- package/src/lib/ui/runtime/RANGE_MARKER.ts +16 -0
- package/src/lib/ui/runtime/batch.ts +22 -0
- package/src/lib/ui/runtime/clientPage.ts +3 -8
- package/src/lib/ui/runtime/createDoc.ts +12 -16
- package/src/lib/ui/runtime/pathSegments.ts +10 -0
- package/src/lib/ui/seedResolved.ts +28 -0
- package/src/lib/ui/startClient.ts +6 -4
- package/src/lib/ui/types/ResolvedFrame.ts +15 -0
- package/template/.zed/settings.json +4 -0
- package/template/src/ui/pages/page.abide +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,97 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.42.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - reject stray {:…}/{/…} tokens outside a block ([`1b5d54e`](https://github.com/briancray/abide/commit/1b5d54e7798acfa7c9b15ce6b65b0c2b6a8c2c07))
|
|
8
|
+
|
|
9
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#switch}/{:case}/{:default} blocks to the switch AST ([`448da38`](https://github.com/briancray/abide/commit/448da3840c120ac3b15d239c090f123c25527fec))
|
|
10
|
+
|
|
11
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#for … of … by …} blocks to the each AST ([`7bb2ac8`](https://github.com/briancray/abide/commit/7bb2ac8f3012e3d40cfcbe8ddf277d6a8732ee93))
|
|
12
|
+
|
|
13
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - LSP semantic-token encoder + extract offsetToPosition ([`7daeb92`](https://github.com/briancray/abide/commit/7daeb926f045058aa452b5f539ddd75b0d8efa67))
|
|
14
|
+
|
|
15
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - semantic-token legend + TS-classification mapping for abide lsp ([`892f97b`](https://github.com/briancray/abide/commit/892f97bbc6dfd8af9301297276995f7060fe3f9a))
|
|
16
|
+
|
|
17
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - type-aware semanticClassifications on the shadow language service ([`9377dd2`](https://github.com/briancray/abide/commit/9377dd24d855cde790d960fceef00255904ee926))
|
|
18
|
+
|
|
19
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - block control-flow grammar formatting + shared helpers ([`b39c0a7`](https://github.com/briancray/abide/commit/b39c0a70696b031b63ef157c67360c2da0fd60b0))
|
|
20
|
+
|
|
21
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - zed onboarding — auto-install extension + enable semantic tokens ([`b47acc7`](https://github.com/briancray/abide/commit/b47acc7cc106956f115c70b110c29ce017a71cd0))
|
|
22
|
+
|
|
23
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#if}/{:else if}/{:else} blocks to the if AST ([`b662397`](https://github.com/briancray/abide/commit/b66239765bae2ec7a24d70bc7b2edbcbe400614c))
|
|
24
|
+
|
|
25
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#try}/{:catch}/{:finally} blocks to the try AST ([`b82c3dd`](https://github.com/briancray/abide/commit/b82c3ddaaec3db3941639df952e568f017d6c767))
|
|
26
|
+
|
|
27
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - Breaking: remove <template> control-flow directives in favor of {#…} blocks ([`bf5cf89`](https://github.com/briancray/abide/commit/bf5cf89813a8dcbc967e1303bd2509e18e701761))
|
|
28
|
+
|
|
29
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - color of/by connectors in {#for} block heads ([`c958614`](https://github.com/briancray/abide/commit/c9586147f8196e9e6301b14cdb08986b36da2751))
|
|
30
|
+
|
|
31
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - parse {#await} streaming + blocking blocks to the await AST ([`cfd7ff0`](https://github.com/briancray/abide/commit/cfd7ff0102b2be2ac241a6c0b1965949aa6621bf))
|
|
32
|
+
|
|
33
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - serve textDocument/semanticTokens/full for .abide ([`e152663`](https://github.com/briancray/abide/commit/e152663dae9e60e13d16f0edf4b0a140a99b1bfb))
|
|
34
|
+
|
|
35
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - structural {#…} block framing tokenizer ([`e1e85e8`](https://github.com/briancray/abide/commit/e1e85e847e9442f8c6140df5346b1d37f8cfad54))
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - point block-construct loc at its primary expression (shadow source-map fidelity) ([`0fb0efa`](https://github.com/briancray/abide/commit/0fb0efab43639331c856969fdb6a7b83bd7ab604))
|
|
40
|
+
|
|
41
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - unify block/branch child-scan loops behind a shared helper ([`4683098`](https://github.com/briancray/abide/commit/4683098df96edd0321dc66d200372e0fbedce2e2))
|
|
42
|
+
|
|
43
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - document {#…} control-flow blocks in the AGENTS surface map ([`68fe96f`](https://github.com/briancray/abide/commit/68fe96f3294a228b91eef5cde37c0300673eb88a))
|
|
44
|
+
|
|
45
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - map {#for}/{:then}/{:catch} bindings + by-key to source so hover & semantic tokens land on them ([`e8447e8`](https://github.com/briancray/abide/commit/e8447e8d54934737b80c2f49d4f4d1b134786531))
|
|
46
|
+
|
|
47
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - map <template args> snippet param + await/try {:catch} bindings (+ index/catch/snippet tests) ([`f724d10`](https://github.com/briancray/abide/commit/f724d101f5baed79de2d39a1ab9598eb9d1eb818))
|
|
48
|
+
|
|
49
|
+
- [`568956c`](https://github.com/briancray/abide/commit/568956c88ff1d32ca79a2aa8a1a0ffffeb25afe3) - satisfy noUncheckedIndexedAccess in block-token + shadow paths ([`fbb6e44`](https://github.com/briancray/abide/commit/fbb6e4443932e92277fb332b639c746663c7759b))
|
|
50
|
+
|
|
51
|
+
## 0.41.1
|
|
52
|
+
|
|
53
|
+
### Patch Changes
|
|
54
|
+
|
|
55
|
+
- [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - shared replaceRange region-update seam ([`184cf41`](https://github.com/briancray/abide/commit/184cf41157ef0a5340a4a916d6cfc4f4e09b6c71))
|
|
56
|
+
|
|
57
|
+
- [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - single-source the marker-depth and isElement helpers ([`188c6ff`](https://github.com/briancray/abide/commit/188c6ff29fcaff2c9c206b971a2f04daf9a18cb3))
|
|
58
|
+
|
|
59
|
+
- [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - guard against double-dispose on reentrant branch swap ([`40600c7`](https://github.com/briancray/abide/commit/40600c7921c9fee9b6be947472c770af795f60c6))
|
|
60
|
+
|
|
61
|
+
- [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - await block on marker ranges, not a node array ([`5d12e83`](https://github.com/briancray/abide/commit/5d12e83b7313a04a925df3464ef5c7669bcbc1e6))
|
|
62
|
+
|
|
63
|
+
- [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - refresh AGENTS surface map, README, and kitchen-sink example ([`6cd7a3f`](https://github.com/briancray/abide/commit/6cd7a3f5686a69f4dd5faf87026d7f1ab0362bdb))
|
|
64
|
+
|
|
65
|
+
- [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - single client warm-seed intake (seedResolved) ([`8c3c19e`](https://github.com/briancray/abide/commit/8c3c19ed0a4d4244cb2634c025ef9ff88b7c9670))
|
|
66
|
+
|
|
67
|
+
- [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - centralize comment-marker sentinels and detached-range builder ([`99d4499`](https://github.com/briancray/abide/commit/99d44991c499b96fc016c3d204cb4dafa340cbe1))
|
|
68
|
+
|
|
69
|
+
- [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - unify skeleton positional model on shared walks ([`cbb9daa`](https://github.com/briancray/abide/commit/cbb9daaa6f31532331df44136dd029f3e32cdfd7))
|
|
70
|
+
|
|
71
|
+
- [`f3e0a13`](https://github.com/briancray/abide/commit/f3e0a132f9f9fbb020a8c783a0b54d3b37457e39) - Coalesce reactive writes inside event handlers
|
|
72
|
+
|
|
73
|
+
Event handlers now batch their writes: a handler that sets several signals re-runs
|
|
74
|
+
each dependent effect/computed/DOM-binding once on the handler's exit instead of once
|
|
75
|
+
per write (the previous default flushed eagerly per write). The change stays fully
|
|
76
|
+
synchronous — the flush runs at handler-end before it returns — so the causal stack
|
|
77
|
+
(`dispatch → handler → effects`) is intact and server/client scheduling stays identical.
|
|
78
|
+
|
|
79
|
+
Factored the batch idiom `createDoc`/`clientPage` inlined into a shared, nesting-safe
|
|
80
|
+
`batch()` (flushes only on the depth-0 exit) and migrated both onto it, so a handler
|
|
81
|
+
that triggers a doc patch now coalesces end-to-end rather than flushing mid-handler.
|
|
82
|
+
|
|
83
|
+
A handler writing N fields cuts dependent re-runs N× (bench: an 8-field form handler
|
|
84
|
+
drops aggregate re-runs 8× and runs ~4.8× faster). Single-write handlers and navigation
|
|
85
|
+
(already batched) are unchanged. New contract, pinned by tests: a handler that writes
|
|
86
|
+
then synchronously reads the bound DOM sees the pre-write value until it returns; signal
|
|
87
|
+
reads stay current.
|
|
88
|
+
|
|
89
|
+
- [`f26cba2`](https://github.com/briancray/abide/commit/f26cba22b92683f7316cc7212c5fe10500bdad13) - `abide check`: declare the `each` `index` binding in the type-check shadow
|
|
90
|
+
|
|
91
|
+
`index="i"` (0.41.0) was bound in the build and SSR passes but not in the shadow the
|
|
92
|
+
type-checker reads, so `{i}` in a row body false-positived "Cannot find name 'i'". The
|
|
93
|
+
shadow now declares the index as a `number` inside the loop body, matching the runtime.
|
|
94
|
+
|
|
3
95
|
## 0.41.0
|
|
4
96
|
|
|
5
97
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
-
**One typed declaration fans out to HTTP, a CLI, an MCP tool, and an
|
|
4
|
-
spec —
|
|
3
|
+
**One typed declaration fans out to HTTP, a CLI, an MCP tool, and an
|
|
4
|
+
OpenAPI spec — the bundler swaps the runtime per side.**
|
|
5
5
|
|
|
6
|
-
abide is an isomorphic framework on Bun
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
abide is an isomorphic framework on Bun where you write a function once and
|
|
7
|
+
it serves every consumer: a browser fetch, an in-process SSR call, a CLI
|
|
8
|
+
subcommand, an MCP tool, an OpenAPI operation. The same callable keeps its
|
|
9
|
+
name and behaviour on both sides — the bundler decides whether it runs the
|
|
10
|
+
real handler or a network proxy. Built for humans _and_ machines.
|
|
10
11
|
|
|
11
|
-
- One direct dependency
|
|
12
|
-
|
|
13
|
-
(≥ 1.3.0); no second toolchain.
|
|
12
|
+
- One direct dependency (`typescript`); `tailwindcss` + `bun-plugin-tailwind`
|
|
13
|
+
are optional peers. Single runtime: Bun ≥ 1.3.0.
|
|
14
14
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
bunx abide scaffold my-app #
|
|
18
|
+
bunx abide scaffold my-app # scaffolds, installs deps, and starts dev
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
Or
|
|
21
|
+
Or read the full feature tour in the kitchen-sink example:
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
24
|
git clone https://github.com/briancray/abide
|
|
@@ -29,134 +29,142 @@ bun run dev
|
|
|
29
29
|
|
|
30
30
|
## RPCs
|
|
31
31
|
|
|
32
|
-
An RPC is one export per file under `src/server/rpc/`.
|
|
33
|
-
URL
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
An RPC is one export per file under `src/server/rpc/`. The file path is the
|
|
33
|
+
URL; the export name is the verb. A Standard Schema (zod / valibot / arktype,
|
|
34
|
+
unadapted) validates the args and projects the same shape into the MCP tool,
|
|
35
|
+
the CLI flags, and the OpenAPI operation.
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
|
-
// src/server/rpc/getMessages.ts
|
|
38
|
+
// src/server/rpc/getMessages.ts
|
|
39
39
|
import { GET } from '@abide/abide/server/GET'
|
|
40
40
|
import { json } from '@abide/abide/server/json'
|
|
41
41
|
import { z } from 'zod'
|
|
42
|
+
import { recent } from '../../chatState.ts'
|
|
42
43
|
|
|
43
|
-
const inputSchema = z.object({ room: z.string() })
|
|
44
|
+
const inputSchema = z.object({ room: z.string(), limit: z.coerce.number().default(20) })
|
|
44
45
|
|
|
45
|
-
export const getMessages = GET(({ room }) => json(
|
|
46
|
+
export const getMessages = GET(({ room, limit }) => json(recent(room).slice(-limit)), {
|
|
47
|
+
inputSchema,
|
|
48
|
+
})
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
One declaration,
|
|
51
|
+
One declaration, every surface:
|
|
49
52
|
|
|
50
53
|
```text
|
|
51
54
|
getMessages = GET(fn, { inputSchema })
|
|
52
55
|
│
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
┌─────────────┬─────────┼──────────┬──────────────┐
|
|
57
|
+
▼ ▼ ▼ ▼ ▼
|
|
58
|
+
SSR call browser MCP tool CLI sub- OpenAPI
|
|
59
|
+
cache(fn)() fetch (read) command operation
|
|
60
|
+
proxy
|
|
56
61
|
```
|
|
57
62
|
|
|
58
|
-
A schema unlocks the CLI
|
|
59
|
-
verb never auto-exposes to MCP — it needs explicit
|
|
60
|
-
Consume the verb four ways: `cache(getMessages)(
|
|
61
|
-
|
|
62
|
-
`getMessages.raw(args)`
|
|
63
|
-
`getMessages.stream(args)`
|
|
63
|
+
A schema unlocks the CLI for every verb and MCP for read-only verbs (`GET` /
|
|
64
|
+
`HEAD`); a mutating verb never auto-exposes to MCP — it needs an explicit
|
|
65
|
+
`clients: { mcp: true }`. Consume the verb four ways: `cache(getMessages)(args)`
|
|
66
|
+
in-process (warm SSR hydration), the swapped `fetch` proxy in the browser,
|
|
67
|
+
`getMessages.raw(args)` for the untouched `Response`, and
|
|
68
|
+
`getMessages.stream(args)` to iterate a `jsonl`/`sse` body.
|
|
64
69
|
|
|
65
|
-
> Query args
|
|
66
|
-
> `timeout` (504 on every surface) is distinct from
|
|
70
|
+
> Query args arrive as strings — wrap numeric/boolean fields in `z.coerce.*`.
|
|
71
|
+
> The per-verb `timeout` (504 on every surface) is distinct from the
|
|
72
|
+
> client-side `ABIDE_CLIENT_TIMEOUT`.
|
|
67
73
|
|
|
68
74
|
## Sockets
|
|
69
75
|
|
|
70
76
|
A socket is one broadcast topic per file under `src/server/sockets/`. A
|
|
71
|
-
`Socket<T>` is an isomorphic `AsyncIterable<T
|
|
72
|
-
|
|
77
|
+
`Socket<T>` is an isomorphic `AsyncIterable<T>`; every socket multiplexes onto
|
|
78
|
+
one WebSocket at `/__abide/sockets`.
|
|
73
79
|
|
|
74
80
|
```ts
|
|
75
|
-
// src/server/sockets/
|
|
81
|
+
// src/server/sockets/chat.ts
|
|
76
82
|
import { socket } from '@abide/abide/server/socket'
|
|
77
83
|
import { z } from 'zod'
|
|
78
84
|
|
|
79
|
-
const schema = z.object({ id: z.string(),
|
|
85
|
+
const schema = z.object({ id: z.string(), from: z.string(), text: z.string(), at: z.number() })
|
|
80
86
|
|
|
81
|
-
// retain the last
|
|
82
|
-
export const
|
|
83
|
-
export type
|
|
87
|
+
// retain the last 100 frames; evict any older than an hour
|
|
88
|
+
export const chat = socket({ schema, tail: 100, ttl: 3_600_000 })
|
|
89
|
+
export type ChatMessage = z.infer<typeof schema>
|
|
84
90
|
```
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
publishes — gated by `clientPublish` (off
|
|
92
|
+
It also has an HTTP face for clients that can't speak the multiplex (the CLI
|
|
93
|
+
and MCP): `GET /__abide/sockets/chat` returns the retained tail, and
|
|
94
|
+
`POST /__abide/sockets/chat` publishes — gated by `clientPublish` (default off,
|
|
95
|
+
so browsers publish through a validating verb instead).
|
|
89
96
|
|
|
90
|
-
## Components
|
|
97
|
+
## Components — the full template
|
|
91
98
|
|
|
92
|
-
A `.abide` component
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`scope().computed()` is read-only. `props()` and `effect()` are in-scope, no
|
|
96
|
-
import.
|
|
99
|
+
A `.abide` component pulls the verb and the socket above into one page and
|
|
100
|
+
exercises the template grammar. `scope`, `props`, `effect`, `html`, and
|
|
101
|
+
`snippet` are ambient — no import needed.
|
|
97
102
|
|
|
103
|
+
<!-- prettier-ignore -->
|
|
98
104
|
```html
|
|
99
105
|
<script>
|
|
100
106
|
import { cache } from '@abide/abide/shared/cache'
|
|
101
107
|
import { tail } from '@abide/abide/ui/tail'
|
|
102
108
|
import { getMessages } from '$server/rpc/getMessages.ts'
|
|
103
|
-
import {
|
|
104
|
-
import {
|
|
109
|
+
import { publishChat } from '$server/rpc/publishChat.ts'
|
|
110
|
+
import { chat } from '$server/sockets/chat.ts'
|
|
105
111
|
import Avatar from '$ui/Avatar.abide'
|
|
106
112
|
|
|
107
|
-
const { room
|
|
113
|
+
const { room } = props()
|
|
108
114
|
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
const
|
|
115
|
+
// warm on the server, live on the client
|
|
116
|
+
const history = scope().computed(() => cache(getMessages)({ room }))
|
|
117
|
+
const latest = scope().computed(() => tail(chat))
|
|
112
118
|
|
|
113
119
|
let from = scope().state('alice')
|
|
114
120
|
let text = scope().state('')
|
|
115
121
|
let pinned = scope().state(false)
|
|
116
|
-
let
|
|
122
|
+
let view = scope().state('all')
|
|
117
123
|
|
|
118
124
|
async function send() {
|
|
119
|
-
await
|
|
125
|
+
await publishChat({ from, text })
|
|
120
126
|
text = ''
|
|
121
127
|
}
|
|
122
128
|
</script>
|
|
123
129
|
|
|
124
|
-
<template name="
|
|
125
|
-
<li
|
|
130
|
+
<template name="line" args={message}>
|
|
131
|
+
<li><Avatar name={message.from} /> <b>{message.from}</b>: {message.text}</li>
|
|
126
132
|
</template>
|
|
127
133
|
|
|
128
|
-
<form onsubmit={send}
|
|
129
|
-
<input bind:value={from}
|
|
130
|
-
<input bind:value={text} placeholder="message"
|
|
134
|
+
<form onsubmit={send}>
|
|
135
|
+
<input bind:value={from} placeholder="name" />
|
|
136
|
+
<input bind:value={text} placeholder="message" />
|
|
131
137
|
<label><input type="checkbox" bind:checked={pinned} /> pin</label>
|
|
132
|
-
<label><input type="radio" bind:group={
|
|
133
|
-
<
|
|
138
|
+
<label><input type="radio" bind:group={view} value="all" /> all</label>
|
|
139
|
+
<label><input type="radio" bind:group={view} value="mine" /> mine</label>
|
|
140
|
+
<button disabled={!text}>send</button>
|
|
134
141
|
</form>
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
<p
|
|
138
|
-
|
|
139
|
-
</
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
</
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
</
|
|
155
|
-
|
|
156
|
-
</
|
|
143
|
+
{#if latest}
|
|
144
|
+
<p>latest from {latest.from}</p>
|
|
145
|
+
{:else}
|
|
146
|
+
<p>no messages yet</p>
|
|
147
|
+
{/if}
|
|
148
|
+
|
|
149
|
+
{#switch view}
|
|
150
|
+
{:case 'mine'}<p>showing your messages</p>
|
|
151
|
+
{:default}<p>showing every message</p>
|
|
152
|
+
{/switch}
|
|
153
|
+
|
|
154
|
+
{#await history}
|
|
155
|
+
<p>loading…</p>
|
|
156
|
+
{:then data}
|
|
157
|
+
<ul>
|
|
158
|
+
{#for message, i of data by message.id}
|
|
159
|
+
{i}. {line(message)}
|
|
160
|
+
{/for}
|
|
161
|
+
</ul>
|
|
162
|
+
{:catch reason}
|
|
163
|
+
<p>failed: {reason.message}</p>
|
|
164
|
+
{/await}
|
|
157
165
|
|
|
158
166
|
<style>
|
|
159
|
-
|
|
167
|
+
form { display: flex; gap: 0.5rem; }
|
|
160
168
|
</style>
|
|
161
169
|
```
|
|
162
170
|
|
package/package.json
CHANGED
package/src/abideLsp.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
2
2
|
import ts from 'typescript'
|
|
3
|
+
import { ABIDE_SEMANTIC_TOKENS_LEGEND } from './lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
|
|
3
4
|
import type {
|
|
4
5
|
ShadowLanguageService,
|
|
5
6
|
ShadowQuickInfo,
|
|
6
7
|
} from './lib/ui/compile/createShadowLanguageService.ts'
|
|
7
8
|
import { createShadowLanguageService } from './lib/ui/compile/createShadowLanguageService.ts'
|
|
9
|
+
import { encodeSemanticTokens } from './lib/ui/compile/encodeSemanticTokens.ts'
|
|
8
10
|
import { nearestProjectRoot } from './lib/ui/compile/nearestProjectRoot.ts'
|
|
11
|
+
import { offsetToPosition } from './lib/ui/compile/offsetToPosition.ts'
|
|
12
|
+
import { structuralBlockTokens } from './lib/ui/compile/structuralBlockTokens.ts'
|
|
9
13
|
import type { AbideDiagnostic } from './lib/ui/compile/types/AbideDiagnostic.ts'
|
|
10
14
|
|
|
11
15
|
/*
|
|
@@ -19,6 +23,28 @@ unsaved text as overlays. Each document routes to a shadow service for its
|
|
|
19
23
|
nearest tsconfig, so files in a monorepo opened at its root are checked against
|
|
20
24
|
their own project — matching `abide check` run from that package.
|
|
21
25
|
*/
|
|
26
|
+
/*
|
|
27
|
+
The semantic-tokens `data` array for one component: the structural `{#…}` framing
|
|
28
|
+
merged with the shadow's type-aware expression tokens, encoded to the LSP wire
|
|
29
|
+
format. Never throws — on any internal failure it yields an empty stream so the
|
|
30
|
+
editor falls back to tree-sitter highlighting.
|
|
31
|
+
*/
|
|
32
|
+
export function componentSemanticTokens(
|
|
33
|
+
service: ShadowLanguageService,
|
|
34
|
+
abidePath: string,
|
|
35
|
+
text: string,
|
|
36
|
+
): number[] {
|
|
37
|
+
try {
|
|
38
|
+
const tokens = [
|
|
39
|
+
...structuralBlockTokens(text),
|
|
40
|
+
...service.semanticClassifications(abidePath),
|
|
41
|
+
]
|
|
42
|
+
return encodeSemanticTokens(text, tokens)
|
|
43
|
+
} catch {
|
|
44
|
+
return []
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
|
|
23
49
|
const documentText = new Map<string, string>()
|
|
24
50
|
|
|
@@ -64,7 +90,14 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
|
|
|
64
90
|
jsonrpc: '2.0',
|
|
65
91
|
id: message.id,
|
|
66
92
|
result: {
|
|
67
|
-
capabilities: {
|
|
93
|
+
capabilities: {
|
|
94
|
+
textDocumentSync: 1,
|
|
95
|
+
hoverProvider: true,
|
|
96
|
+
semanticTokensProvider: {
|
|
97
|
+
legend: ABIDE_SEMANTIC_TOKENS_LEGEND,
|
|
98
|
+
full: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
68
101
|
serverInfo: { name: 'abide-lsp' },
|
|
69
102
|
},
|
|
70
103
|
})
|
|
@@ -115,6 +148,18 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
|
|
|
115
148
|
})
|
|
116
149
|
return
|
|
117
150
|
}
|
|
151
|
+
case 'textDocument/semanticTokens/full': {
|
|
152
|
+
const { uri } = message.params.textDocument
|
|
153
|
+
const data = isAbide(uri)
|
|
154
|
+
? componentSemanticTokens(
|
|
155
|
+
serviceFor(fileURLToPath(uri)),
|
|
156
|
+
fileURLToPath(uri),
|
|
157
|
+
documentText.get(fileURLToPath(uri)) ?? '',
|
|
158
|
+
)
|
|
159
|
+
: []
|
|
160
|
+
send({ jsonrpc: '2.0', id: message.id, result: { data } })
|
|
161
|
+
return
|
|
162
|
+
}
|
|
118
163
|
case 'textDocument/didClose': {
|
|
119
164
|
const { uri } = message.params.textDocument
|
|
120
165
|
if (isAbide(uri)) {
|
|
@@ -194,13 +239,6 @@ function toLspHover(text: string, info: ShadowQuickInfo): object {
|
|
|
194
239
|
}
|
|
195
240
|
}
|
|
196
241
|
|
|
197
|
-
/* An absolute offset → LSP `{ line, character }` (0-based, UTF-16 code units). */
|
|
198
|
-
function offsetToPosition(text: string, offset: number): { line: number; character: number } {
|
|
199
|
-
const before = text.slice(0, offset)
|
|
200
|
-
const line = before.split('\n').length - 1
|
|
201
|
-
return { line, character: offset - (before.lastIndexOf('\n') + 1) }
|
|
202
|
-
}
|
|
203
|
-
|
|
204
242
|
/* An LSP `{ line, character }` (0-based) → absolute offset in `text`. */
|
|
205
243
|
function positionToOffset(text: string, position: { line: number; character: number }): number {
|
|
206
244
|
const lineStart = text
|
|
@@ -4,6 +4,8 @@ import type { BunPlugin } from 'bun'
|
|
|
4
4
|
import { Glob } from 'bun'
|
|
5
5
|
import { abideImportName } from './lib/shared/abideImportName.ts'
|
|
6
6
|
import { abideLog } from './lib/shared/abideLog.ts'
|
|
7
|
+
import { escapeRegex } from './lib/shared/escapeRegex.ts'
|
|
8
|
+
import { fileName } from './lib/shared/fileName.ts'
|
|
7
9
|
import { fileStem } from './lib/shared/fileStem.ts'
|
|
8
10
|
import { jsonSchemaForPromptArguments } from './lib/shared/jsonSchemaForPromptArguments.ts'
|
|
9
11
|
import { manifestModule } from './lib/shared/manifestModule.ts'
|
|
@@ -65,10 +67,6 @@ function resolveExtensionUncached(path: string): string {
|
|
|
65
67
|
|
|
66
68
|
const NS = 'abide-virtual'
|
|
67
69
|
|
|
68
|
-
function escapeRegex(value: string): string {
|
|
69
|
-
return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
70
|
/* Memoises a zero-arg async producer so repeat calls reuse the first in-flight promise. */
|
|
73
71
|
function once<T>(produce: () => Promise<T>): () => Promise<T> {
|
|
74
72
|
let promise: Promise<T> | undefined
|
|
@@ -791,7 +789,7 @@ async function scanPages(pagesDir: string): Promise<PagesScan> {
|
|
|
791
789
|
return { pageFiles: [], layoutFiles: [] }
|
|
792
790
|
}
|
|
793
791
|
const allFiles = await Array.fromAsync(new Glob('**/*.abide').scan({ cwd: pagesDir }))
|
|
794
|
-
const leafIs = (name: string) => (file: string) => (file
|
|
792
|
+
const leafIs = (name: string) => (file: string) => fileName(file) === name
|
|
795
793
|
return {
|
|
796
794
|
pageFiles: allFiles.filter(leafIs('page.abide')),
|
|
797
795
|
layoutFiles: allFiles.filter(leafIs('layout.abide')),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { relative } from 'node:path'
|
|
2
|
+
import { fileName } from '../../shared/fileName.ts'
|
|
2
3
|
import { analyzeComponent } from '../../ui/compile/analyzeComponent.ts'
|
|
3
4
|
import { compileComponent } from '../../ui/compile/compileComponent.ts'
|
|
4
5
|
import { nearestProjectRoot } from '../../ui/compile/nearestProjectRoot.ts'
|
|
@@ -19,7 +20,7 @@ const GENERATED = /(^|\/)\.abide\//
|
|
|
19
20
|
// page.abide / layout.abide are router-mounted, not `mountChild`-tracked, so they
|
|
20
21
|
// can't hot-swap — they fold into `structure` (a reload) instead.
|
|
21
22
|
function isPageOrLayout(moduleId: string): boolean {
|
|
22
|
-
const file = moduleId
|
|
23
|
+
const file = fileName(moduleId)
|
|
23
24
|
return file === 'page.abide' || file === 'layout.abide'
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Escapes the regex metacharacters in `value` so it can be embedded literally
|
|
3
|
+
inside a `new RegExp(...)` pattern. Shared by the $rpc/$sockets import stripper,
|
|
4
|
+
the resolver plugin's virtual-namespace matcher, and the SSR snippet-call
|
|
5
|
+
rewriter so the same escaping is applied one way everywhere.
|
|
6
|
+
*/
|
|
7
|
+
export function escapeRegex(value: string): string {
|
|
8
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The bare leaf filename of a path — directory stripped, extension kept —
|
|
3
|
+
e.g. `users/list.ts` → `list.ts`. The extension-stripping `fileStem` builds on
|
|
4
|
+
this; call sites that match a full filename (`layout.abide`, `page.abide`) use
|
|
5
|
+
it directly so the leaf-grab is written one way.
|
|
6
|
+
*/
|
|
7
|
+
export function fileName(path: string): string {
|
|
8
|
+
return path.split('/').pop() ?? ''
|
|
9
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fileName } from './fileName.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
The bare filename of a path, with directory and trailing extension stripped —
|
|
3
5
|
e.g. `users/list.ts` → `list`, `/_virtual/mcp-resources.ts` → `mcp-resources`.
|
|
@@ -5,5 +7,5 @@ Used to derive a virtual-module name from its path and to check an $rpc /
|
|
|
5
7
|
$sockets module's single export name against its file stem.
|
|
6
8
|
*/
|
|
7
9
|
export function fileStem(path: string): string {
|
|
8
|
-
return (path
|
|
10
|
+
return fileName(path).replace(/\.[^.]+$/, '')
|
|
9
11
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fileName } from './fileName.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Derives the CLI program/binary name from a package.json `name` field.
|
|
3
5
|
Scoped names (`@scope/tool`) keep only the final segment so the value is
|
|
@@ -10,5 +12,5 @@ export function programNameForPackage(name: string | undefined): string {
|
|
|
10
12
|
if (name === undefined || name === '') {
|
|
11
13
|
return 'app'
|
|
12
14
|
}
|
|
13
|
-
return name
|
|
15
|
+
return fileName(name) || 'app'
|
|
14
16
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeRegex } from './escapeRegex.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Strips the user's `import { … } from '<moduleName>'` declaration from a
|
|
3
5
|
module source. Used by the $rpc / $sockets rewriters to remove the
|
|
@@ -18,7 +20,7 @@ includes newlines, so multi-line braced imports like
|
|
|
18
20
|
still match — the body just can't contain another `}` to bound it.
|
|
19
21
|
*/
|
|
20
22
|
export function stripImport(source: string, moduleName: string): string {
|
|
21
|
-
const escaped = moduleName
|
|
23
|
+
const escaped = escapeRegex(moduleName)
|
|
22
24
|
const pattern = new RegExp(
|
|
23
25
|
`^\\s*import\\s*\\{[^}]*\\}\\s*from\\s*['"]${escaped}['"]\\s*;?\\s*$`,
|
|
24
26
|
'gm',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The LSP semantic-tokens legend abide lsp advertises, and the decoder from
|
|
3
|
+
TypeScript's encoded classifications to legend names. TypeScript encodes a
|
|
4
|
+
classification as `((tokenType + 1) << 8) + modifierBitset`; its TokenType and
|
|
5
|
+
TokenModifier enums fix the orders below. `keyword`/`operator` carry the `{#…}`
|
|
6
|
+
block framing the structural tokenizer emits.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* TS TokenType order (class=0 … member=11) → LSP token-type name. */
|
|
10
|
+
const TS_TYPE_TO_LSP = [
|
|
11
|
+
'class',
|
|
12
|
+
'enum',
|
|
13
|
+
'interface',
|
|
14
|
+
'namespace',
|
|
15
|
+
'typeParameter',
|
|
16
|
+
'type',
|
|
17
|
+
'parameter',
|
|
18
|
+
'variable',
|
|
19
|
+
'enumMember',
|
|
20
|
+
'property',
|
|
21
|
+
'function',
|
|
22
|
+
'method',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
/* TS TokenModifier order (declaration=bit 0 … local=bit 5). */
|
|
26
|
+
const TS_MODIFIERS = ['declaration', 'static', 'async', 'readonly', 'defaultLibrary', 'local']
|
|
27
|
+
|
|
28
|
+
export const ABIDE_SEMANTIC_TOKENS_LEGEND = {
|
|
29
|
+
tokenTypes: [...TS_TYPE_TO_LSP, 'keyword', 'operator'],
|
|
30
|
+
tokenModifiers: TS_MODIFIERS,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Decodes one TypeScript encoded classification into legend names. */
|
|
34
|
+
export function mapTsClassification(
|
|
35
|
+
classification: number,
|
|
36
|
+
): { type: string; modifiers: string[] } | undefined {
|
|
37
|
+
const tokenType = (classification >> 8) - 1
|
|
38
|
+
const type = TS_TYPE_TO_LSP[tokenType]
|
|
39
|
+
if (type === undefined) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
const modifierSet = classification & 255
|
|
43
|
+
const modifiers = TS_MODIFIERS.filter((_, bit) => (modifierSet & (1 << bit)) !== 0)
|
|
44
|
+
return { type, modifiers }
|
|
45
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SNIPPET_CLOSE, SNIPPET_OPEN } from '../runtime/RANGE_MARKER.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Source text for the `$esc` / `$attr` / `$spread` / `$text` / `$snip` helpers injected
|
|
3
5
|
into every SSR render body. `$esc` escapes the five HTML-significant characters. `$attr`
|
|
@@ -30,6 +32,6 @@ export const SSR_ESCAPE =
|
|
|
30
32
|
"const $SNIP = Symbol.for('abide.snippet');\n" +
|
|
31
33
|
'const $snip = (s) => ({ [$SNIP]: s });\n' +
|
|
32
34
|
'const $text = (v) => (v !== null && typeof v === "object" && $SNIP in v) ' +
|
|
33
|
-
|
|
35
|
+
`? ('<!--${SNIPPET_OPEN}-->' + v[$SNIP] + '<!--${SNIPPET_CLOSE}-->') ` +
|
|
34
36
|
': (v !== null && typeof v === "object" && $RAW in v) ' +
|
|
35
37
|
"? ('<!--abide:html-->' + v[$RAW] + '<!--/abide:html-->') : $esc(v);"
|