@abide/abide 0.41.0 → 0.41.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 (38) hide show
  1. package/AGENTS.md +327 -326
  2. package/CHANGELOG.md +44 -0
  3. package/README.md +87 -78
  4. package/package.json +1 -1
  5. package/src/lib/ui/compile/SSR_ESCAPE.ts +3 -1
  6. package/src/lib/ui/compile/compileShadow.ts +5 -0
  7. package/src/lib/ui/compile/generateBuild.ts +11 -4
  8. package/src/lib/ui/compile/generateSSR.ts +17 -8
  9. package/src/lib/ui/compile/skeletonContext.ts +67 -79
  10. package/src/lib/ui/compile/templateAnchorAdapter.ts +61 -0
  11. package/src/lib/ui/compile/templateElementAdapter.ts +44 -0
  12. package/src/lib/ui/compile/walkAnchorOrder.ts +57 -0
  13. package/src/lib/ui/compile/walkElementOrder.ts +60 -0
  14. package/src/lib/ui/dom/appendSnippet.ts +9 -8
  15. package/src/lib/ui/dom/applyResolved.ts +3 -4
  16. package/src/lib/ui/dom/awaitBlock.ts +37 -31
  17. package/src/lib/ui/dom/buildDetachedRange.ts +30 -0
  18. package/src/lib/ui/dom/depthZeroNodes.ts +34 -0
  19. package/src/lib/ui/dom/domAnchorAdapter.ts +29 -0
  20. package/src/lib/ui/dom/domElementAdapter.ts +20 -0
  21. package/src/lib/ui/dom/each.ts +7 -11
  22. package/src/lib/ui/dom/eachAsync.ts +12 -17
  23. package/src/lib/ui/dom/isElement.ts +6 -0
  24. package/src/lib/ui/dom/markerDepthDelta.ts +19 -0
  25. package/src/lib/ui/dom/mountRange.ts +4 -3
  26. package/src/lib/ui/dom/mountSlot.ts +4 -3
  27. package/src/lib/ui/dom/on.ts +6 -1
  28. package/src/lib/ui/dom/replaceRange.ts +24 -0
  29. package/src/lib/ui/dom/skeleton.ts +35 -92
  30. package/src/lib/ui/dom/switchBlock.ts +13 -10
  31. package/src/lib/ui/dom/when.ts +13 -10
  32. package/src/lib/ui/runtime/RANGE_MARKER.ts +16 -0
  33. package/src/lib/ui/runtime/batch.ts +22 -0
  34. package/src/lib/ui/runtime/clientPage.ts +3 -8
  35. package/src/lib/ui/runtime/createDoc.ts +9 -13
  36. package/src/lib/ui/seedResolved.ts +28 -0
  37. package/src/lib/ui/startClient.ts +6 -4
  38. package/src/lib/ui/types/ResolvedFrame.ts +15 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # abide
2
2
 
3
+ ## 0.41.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - shared replaceRange region-update seam ([`184cf41`](https://github.com/briancray/abide/commit/184cf41157ef0a5340a4a916d6cfc4f4e09b6c71))
8
+
9
+ - [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - single-source the marker-depth and isElement helpers ([`188c6ff`](https://github.com/briancray/abide/commit/188c6ff29fcaff2c9c206b971a2f04daf9a18cb3))
10
+
11
+ - [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - guard against double-dispose on reentrant branch swap ([`40600c7`](https://github.com/briancray/abide/commit/40600c7921c9fee9b6be947472c770af795f60c6))
12
+
13
+ - [`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))
14
+
15
+ - [`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))
16
+
17
+ - [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - single client warm-seed intake (seedResolved) ([`8c3c19e`](https://github.com/briancray/abide/commit/8c3c19ed0a4d4244cb2634c025ef9ff88b7c9670))
18
+
19
+ - [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - centralize comment-marker sentinels and detached-range builder ([`99d4499`](https://github.com/briancray/abide/commit/99d44991c499b96fc016c3d204cb4dafa340cbe1))
20
+
21
+ - [`74f5312`](https://github.com/briancray/abide/commit/74f531260e17f48162397142fb19b3df9e081b2c) - unify skeleton positional model on shared walks ([`cbb9daa`](https://github.com/briancray/abide/commit/cbb9daaa6f31532331df44136dd029f3e32cdfd7))
22
+
23
+ - [`f3e0a13`](https://github.com/briancray/abide/commit/f3e0a132f9f9fbb020a8c783a0b54d3b37457e39) - Coalesce reactive writes inside event handlers
24
+
25
+ Event handlers now batch their writes: a handler that sets several signals re-runs
26
+ each dependent effect/computed/DOM-binding once on the handler's exit instead of once
27
+ per write (the previous default flushed eagerly per write). The change stays fully
28
+ synchronous — the flush runs at handler-end before it returns — so the causal stack
29
+ (`dispatch → handler → effects`) is intact and server/client scheduling stays identical.
30
+
31
+ Factored the batch idiom `createDoc`/`clientPage` inlined into a shared, nesting-safe
32
+ `batch()` (flushes only on the depth-0 exit) and migrated both onto it, so a handler
33
+ that triggers a doc patch now coalesces end-to-end rather than flushing mid-handler.
34
+
35
+ A handler writing N fields cuts dependent re-runs N× (bench: an 8-field form handler
36
+ drops aggregate re-runs 8× and runs ~4.8× faster). Single-write handlers and navigation
37
+ (already batched) are unchanged. New contract, pinned by tests: a handler that writes
38
+ then synchronously reads the bound DOM sees the pre-write value until it returns; signal
39
+ reads stay current.
40
+
41
+ - [`f26cba2`](https://github.com/briancray/abide/commit/f26cba22b92683f7316cc7212c5fe10500bdad13) - `abide check`: declare the `each` `index` binding in the type-check shadow
42
+
43
+ `index="i"` (0.41.0) was bound in the build and SSR passes but not in the shadow the
44
+ type-checker reads, so `{i}` in a row body false-positived "Cannot find name 'i'". The
45
+ shadow now declares the index as a `number` inside the loop body, matching the runtime.
46
+
3
47
  ## 0.41.0
4
48
 
5
49
  ### 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 OpenAPI
4
- spec — and the bundler swaps the runtime per side.**
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 and web standards: you declare a verb,
7
- a socket, or a component once, and the same callable runs server-side, in the
8
- browser, from the terminal, and from an agent the bundler decides which
9
- runtime each side gets. Built for humans *and* machines.
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 the TypeScript compiler, used by `abide check`.
12
- Tailwind is an optional peer. Everything runs on a single Bun runtime
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 # copies the template, installs, starts dev
18
+ bunx abide scaffold my-app # scaffolds, installs deps, and starts dev
19
19
  ```
20
20
 
21
- Or clone the kitchen-sink and run it:
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,143 @@ bun run dev
29
29
 
30
30
  ## RPCs
31
31
 
32
- An RPC is one export per file under `src/server/rpc/`. **The file path is the
33
- URL.** The schema validates arguments and projects the MCP tool, the CLI flags,
34
- and the OpenAPI operation. The contract is Standard Schema zod, valibot, or
35
- arktype, unadapted.
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 — file path is the route: GET /getMessages
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(board(room)), { inputSchema })
46
+ export const getMessages = GET(({ room, limit }) => json(recent(room).slice(-limit)), {
47
+ inputSchema,
48
+ })
46
49
  ```
47
50
 
48
- One declaration, five surfaces:
51
+ One declaration, every surface:
49
52
 
50
53
  ```text
51
54
  getMessages = GET(fn, { inputSchema })
52
55
 
53
- ┌───────────┬─────────────┼────────────┬──────────────┐
54
- SSR call browser fetch MCP tool CLI command OpenAPI op
55
- cache(fn)() typed proxy() (read-only) abide ... cli /openapi.json
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 everywhere and MCP for read-only verbs; a mutating
59
- verb never auto-exposes to MCP — it needs explicit `clients: { mcp: true }`.
60
- Consume the verb four ways: `cache(getMessages)({ room })` resolves in-process
61
- during SSR, the same call hits a swapped `fetch` in the browser,
62
- `getMessages.raw(args)` returns the undecoded `Response`, and
63
- `getMessages.stream(args)` is an iterable view of the body.
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 travel as strings — validate with `z.coerce.*`. The per-verb
66
- > `timeout` (504 on every surface) is distinct from `ABIDE_CLIENT_TIMEOUT`.
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>`, and every socket multiplexes
72
- onto one websocket at `/__abide/sockets`.
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/messages.ts — topic name = file name: `messages`
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(), room: z.string(), from: z.string(), text: 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 50 frames; evict any older than an hour
82
- export const messages = socket({ schema, tail: 50, ttl: 3_600_000 })
83
- export type Message = z.infer<typeof schema>
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
- The socket also has an HTTP face at `/__abide/sockets/messages`, for clients
87
- that can't speak the ws multiplex: `GET` returns the retained tail, `POST`
88
- publishes — gated by `clientPublish` (off by default, so the POST 403s).
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 is the payoff: it imports the verb and the socket above
93
- and ties them together with the native `<template>` grammar. Reactive state is
94
- reached only through `scope()` `scope().state()` is writable,
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 ambientno 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 { postMessage } from '$server/rpc/postMessage.ts'
104
- import { messages } from '$server/sockets/messages.ts'
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 = 'lobby' } = props<{ room?: string }>()
113
+ const { room } = props()
108
114
 
109
- // SSR-warm history; live frames then stream over the ws
110
- const seed = cache(getMessages)({ room })
111
- const live = scope().computed(() => tail(messages, { last: 50 }))
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 sort = scope().state('newest')
122
+ let view = scope().state('all')
117
123
 
118
124
  async function send() {
119
- await postMessage({ room, from, text })
125
+ await publishChat({ from, text })
120
126
  text = ''
121
127
  }
122
128
  </script>
123
129
 
124
- <template name="bubble" args={msg}>
125
- <li class="flex gap-2"><Avatar name={msg.from} /> <b>{msg.from}</b> {msg.text}</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} class="flex gap-2">
129
- <input bind:value={from} class="border px-2" />
130
- <input bind:value={text} placeholder="message" class="flex-1 border px-2" />
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={sort} value="newest" /> newest</label>
133
- <button disabled={!text} class="border px-3">send</button>
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
- <template if={pinned}>
137
- <p class="text-xs text-amber-700">room pinned</p>
138
- <template else><p class="text-xs text-slate-400">not pinned</p></template>
143
+ <template if={latest}>
144
+ <p>latest from {latest.from}</p>
145
+ <template else>
146
+ <p>no messages yet</p>
139
147
  </template>
140
148
 
141
- <template switch={sort}>
142
- <template case={'newest'}><p class="text-xs">newest first</p></template>
143
- <template default><p class="text-xs">oldest first</p></template>
149
+ <template switch={view}>
150
+ <template case="mine"><p>showing your messages</p></template>
151
+ <template default><p>showing every message</p></template>
144
152
  </template>
145
153
 
146
- <template await={seed}>
147
- <p class="text-xs text-slate-500">loading…</p>
148
- <template then="history">
149
- <ul class="mt-3 space-y-1">
150
- <template each={live ?? history} as="msg" key="msg.id">
151
- {bubble(msg)}
152
- </template>
153
- </ul>
154
- </template>
155
- <template catch="err"><p class="text-rose-700">{err.message}</p></template>
154
+ <template await={history}>
155
+ <p>loading…</p>
156
+ <template then="data">
157
+ <ul>
158
+ <template each={data} as="message" key="message.id" index="i">
159
+ {i}. {line(message)}
160
+ </template>
161
+ </ul>
162
+ <template catch="reason">
163
+ <p>failed: {reason.message}</p>
164
+ </template>
156
165
  </template>
157
166
 
158
167
  <style>
159
- li { font-size: 0.875rem; }
168
+ form { display: flex; gap: 0.5rem; }
160
169
  </style>
161
170
  ```
162
171
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
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",
@@ -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
- "? ('<!--abide:snippet-->' + v[$SNIP] + '<!--/abide:snippet-->') " +
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);"
@@ -417,6 +417,11 @@ function emitNode(node: TemplateNode, builder: Builder): void {
417
417
  if (node.key !== undefined) {
418
418
  builder.raw(`void (${node.key});\n`)
419
419
  }
420
+ /* `index="i"` binds the row's position — always a number (the row ordinal,
421
+ or an async stream's arrival count). Declare it so body references check. */
422
+ if (node.index !== undefined) {
423
+ builder.raw(`const ${node.index}: number = 0;\n`)
424
+ }
420
425
  emitNodes(node.children, builder)
421
426
  builder.raw('}\n')
422
427
  return
@@ -1,6 +1,7 @@
1
1
  import { assertExhaustive } from '../../shared/assertExhaustive.ts'
2
2
  import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
3
3
  import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
4
+ import { ANCHOR } from '../runtime/RANGE_MARKER.ts'
4
5
  import { asOutlet } from './asOutlet.ts'
5
6
  import { bindListenEvent } from './bindListenEvent.ts'
6
7
  import { composeProps } from './composeProps.ts'
@@ -18,6 +19,12 @@ import { staticTextPart } from './staticTextPart.ts'
18
19
  import type { TemplateNode } from './types/TemplateNode.ts'
19
20
  import { VOID_TAGS } from './VOID_TAGS.ts'
20
21
 
22
+ /* The skeleton positioning anchor a control-flow block / component / slot / outlet
23
+ stamps into its skeleton markup, sourced from the same `ANCHOR` wire-alphabet constant
24
+ the client's anchor scan (`skeleton`) matches — so the markup the build clones and the
25
+ markup the scan reads can never drift on a literal. */
26
+ const ANCHOR_COMMENT = `<!--${ANCHOR}-->`
27
+
21
28
  /*
22
29
  Generates the build statements for a parsed template: element creation, static
23
30
  attributes, reactive `attr`/`text` bindings, `on` listeners, keyed `each`, and
@@ -213,7 +220,7 @@ export function generateBuild(
213
220
  binds.push(
214
221
  `appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], ${namedThunk('text', `return (${lowerExpression(part.code)})`)});\n`,
215
222
  )
216
- return '<!--a-->'
223
+ return ANCHOR_COMMENT
217
224
  })
218
225
  .join('')
219
226
  }
@@ -226,7 +233,7 @@ export function generateBuild(
226
233
  element — so its root lays out as a true direct child of `anchor.parentNode`. */
227
234
  const anchorVar = anchorVarAt(node, skVar, binds)
228
235
  binds.push(generateChild(node, `${anchorVar}.parentNode`, `anchorCursor(${anchorVar})`))
229
- return '<!--a-->'
236
+ return ANCHOR_COMMENT
230
237
  }
231
238
  if (node.kind === 'script') {
232
239
  /* A nested `<script>` (scoped reactive block) emits no markup — its lowered body
@@ -251,7 +258,7 @@ export function generateBuild(
251
258
  No wrapper element — the filled child lays out as a direct child of the parent. */
252
259
  const anchorVar = anchorVarAt(node, skVar, binds)
253
260
  binds.push(`outlet(${anchorVar}.parentNode, anchorCursor(${anchorVar}));\n`)
254
- return '<!--a-->'
261
+ return ANCHOR_COMMENT
255
262
  }
256
263
  if (node.tag === 'slot') {
257
264
  /* A `<slot>` outlet at its position: an `<!--a-->` anchor, the slot's content
@@ -261,7 +268,7 @@ export function generateBuild(
261
268
  binds.push(
262
269
  `mountSlot(${anchorVar}.parentNode, (${hostVar}) => {\n${generateSlot(node, hostVar)}}, anchorCursor(${anchorVar}));\n`,
263
270
  )
264
- return '<!--a-->'
271
+ return ANCHOR_COMMENT
265
272
  }
266
273
  const hasReactiveAttr = node.attrs.some((attr) => attr.kind !== 'static')
267
274
  const reactiveTextChild = node.children.find(
@@ -1,6 +1,11 @@
1
1
  import { assertExhaustive } from '../../shared/assertExhaustive.ts'
2
2
  import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
3
3
  import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
4
+ import {
5
+ ANCHOR,
6
+ RANGE_CLOSE as RANGE_CLOSE_DATA,
7
+ RANGE_OPEN as RANGE_OPEN_DATA,
8
+ } from '../runtime/RANGE_MARKER.ts'
4
9
  import { asOutlet } from './asOutlet.ts'
5
10
  import { composeProps } from './composeProps.ts'
6
11
  import { groupBindParts } from './groupBindParts.ts'
@@ -16,12 +21,16 @@ import { stripEffects } from './stripEffects.ts'
16
21
  import type { TemplateNode } from './types/TemplateNode.ts'
17
22
  import { VOID_TAGS } from './VOID_TAGS.ts'
18
23
 
19
- /* The range boundary comments a control-flow block emits around its content. They
20
- serialize exactly as the client's `document.createComment('[' | ']')` markers, so
21
- the client claims the same `[ ]` boundary it builds the comment-marked range
22
- that lets a branch hold any content. */
23
- const RANGE_OPEN = '<!--[-->'
24
- const RANGE_CLOSE = '<!--]-->'
24
+ /* The range boundary comments a control-flow block emits around its content. Sourced
25
+ from the SAME wire-alphabet constants the client's `document.createComment` markers
26
+ use (`RANGE_MARKER`), wrapped in comment syntax so a server-emitted boundary and the
27
+ client `[ ]` boundary it claims can never drift on a literal. */
28
+ const RANGE_OPEN = `<!--${RANGE_OPEN_DATA}-->`
29
+ const RANGE_CLOSE = `<!--${RANGE_CLOSE_DATA}-->`
30
+
31
+ /* The skeleton positioning anchor a control-flow block / slot / outlet emits, sourced
32
+ from the same `ANCHOR` constant the client's anchor scan matches. */
33
+ const ANCHOR_COMMENT = `<!--${ANCHOR}-->`
25
34
 
26
35
  /*
27
36
  Server code generator: turns the parsed template into statements that push HTML
@@ -172,7 +181,7 @@ export function generateSSR(
172
181
  Outside a skeleton (top-level / inside a branch) blocks mount on the host directly,
173
182
  so no anchor. */
174
183
  const anchorMark = (node: TemplateNode, target: string): string =>
175
- inSkeleton.get(node) ? push(target, '<!--a-->') : ''
184
+ inSkeleton.get(node) ? push(target, ANCHOR_COMMENT) : ''
176
185
 
177
186
  function generate(node: TemplateNode, target: string): string {
178
187
  /* Every kind that mounts as a marker range is positioned by an `<!--a-->` anchor when
@@ -197,7 +206,7 @@ export function generateSSR(
197
206
  ? `$text(await (${lowered}))`
198
207
  : `$text(${lowered})`
199
208
  return markText.get(node)
200
- ? `${target}.push('<!--a-->' + ${value});\n`
209
+ ? `${target}.push('${ANCHOR_COMMENT}' + ${value});\n`
201
210
  : `${target}.push(${value});\n`
202
211
  })
203
212
  .join('')