@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.
- package/AGENTS.md +327 -326
- package/CHANGELOG.md +44 -0
- package/README.md +87 -78
- package/package.json +1 -1
- package/src/lib/ui/compile/SSR_ESCAPE.ts +3 -1
- package/src/lib/ui/compile/compileShadow.ts +5 -0
- package/src/lib/ui/compile/generateBuild.ts +11 -4
- package/src/lib/ui/compile/generateSSR.ts +17 -8
- package/src/lib/ui/compile/skeletonContext.ts +67 -79
- package/src/lib/ui/compile/templateAnchorAdapter.ts +61 -0
- package/src/lib/ui/compile/templateElementAdapter.ts +44 -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/applyResolved.ts +3 -4
- 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/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 +9 -13
- 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/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
|
|
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,143 @@ 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
|
-
<template if={
|
|
137
|
-
<p
|
|
138
|
-
|
|
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={
|
|
142
|
-
|
|
143
|
-
|
|
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={
|
|
147
|
-
<p
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<
|
|
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
|
-
|
|
168
|
+
form { display: flex; gap: 0.5rem; }
|
|
160
169
|
</style>
|
|
161
170
|
```
|
|
162
171
|
|
package/package.json
CHANGED
|
@@ -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);"
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,
|
|
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('
|
|
209
|
+
? `${target}.push('${ANCHOR_COMMENT}' + ${value});\n`
|
|
201
210
|
: `${target}.push(${value});\n`
|
|
202
211
|
})
|
|
203
212
|
.join('')
|