@abide/abide 0.37.0 → 0.38.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 +1 -1
- package/CHANGELOG.md +38 -0
- package/package.json +2 -1
- package/src/abideResolverPlugin.ts +3 -17
- package/src/appEntry.ts +18 -8
- package/src/controlServerWorker.ts +8 -1
- package/src/lib/cli/parseArgvForRpc.ts +8 -3
- package/src/lib/mcp/createMcpServer.ts +8 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +20 -2
- package/src/lib/mcp/toolResultFromResponse.ts +5 -0
- package/src/lib/server/rpc/parseArgs.ts +12 -1
- package/src/lib/server/runtime/acceptsGzip.ts +10 -1
- package/src/lib/server/runtime/gzipResponse.ts +2 -1
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +2 -1
- package/src/lib/server/runtime/streamFromIterator.ts +8 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +7 -1
- package/src/lib/server/sockets/defineSocket.ts +6 -1
- package/src/lib/shared/cache.ts +6 -0
- package/src/lib/shared/createPushIterator.ts +7 -2
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +5 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
- package/src/lib/ui/compile/asOutlet.ts +30 -0
- package/src/lib/ui/compile/compileModule.ts +13 -9
- package/src/lib/ui/compile/generateBuild.ts +83 -72
- package/src/lib/ui/compile/generateSSR.ts +56 -37
- package/src/lib/ui/compile/isAnchorPositioned.ts +19 -0
- package/src/lib/ui/compile/isControlFlow.ts +3 -1
- package/src/lib/ui/compile/isTextLeaf.ts +1 -1
- package/src/lib/ui/compile/parseTemplate.ts +10 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +25 -2
- package/src/lib/ui/compile/skeletonContext.ts +97 -35
- package/src/lib/ui/compile/skeletonable.ts +3 -2
- package/src/lib/ui/compile/types/SkeletonContext.ts +6 -0
- package/src/lib/ui/dom/appendSnippet.ts +60 -20
- package/src/lib/ui/dom/commentData.ts +14 -0
- package/src/lib/ui/dom/disposeRange.ts +21 -0
- package/src/lib/ui/dom/fillBefore.ts +10 -0
- package/src/lib/ui/dom/fillBoundary.ts +38 -0
- package/src/lib/ui/dom/fillRange.ts +30 -0
- package/src/lib/ui/dom/hydrate.ts +11 -21
- package/src/lib/ui/dom/mount.ts +16 -25
- package/src/lib/ui/dom/mountChild.ts +27 -14
- package/src/lib/ui/dom/mountRange.ts +45 -0
- package/src/lib/ui/dom/mountSlot.ts +7 -2
- package/src/lib/ui/dom/outlet.ts +62 -0
- package/src/lib/ui/dom/scopeLabel.ts +13 -7
- package/src/lib/ui/dom/skeleton.ts +11 -27
- package/src/lib/ui/dom/withScope.ts +33 -0
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/persist.ts +4 -1
- package/src/lib/ui/renderChain.ts +23 -15
- package/src/lib/ui/router.ts +78 -38
- package/src/lib/ui/runtime/OUTLET_MARKER.ts +10 -0
- package/src/lib/ui/runtime/OUTLET_TAG.ts +5 -6
- package/src/lib/ui/runtime/PENDING_OUTLET.ts +8 -0
- package/src/lib/ui/runtime/captureModelDoc.ts +12 -13
- package/src/lib/ui/runtime/createDoc.ts +20 -7
- package/src/lib/ui/runtime/hotReplace.ts +14 -10
- package/src/lib/ui/runtime/types/HotInstance.ts +11 -7
- package/src/lib/ui/runtime/types/Route.ts +6 -5
- package/src/lib/ui/runtime/types/UiComponent.ts +5 -0
- package/src/lib/ui/seedStreamedResolution.ts +10 -1
- package/src/lib/ui/compile/componentWrapperTag.ts +0 -13
- package/src/lib/ui/runtime/firstOutlet.ts +0 -22
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
2
2
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
3
|
+
import { asOutlet } from './asOutlet.ts'
|
|
3
4
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
4
|
-
import { componentWrapperTag } from './componentWrapperTag.ts'
|
|
5
5
|
import { groupBindParts } from './groupBindParts.ts'
|
|
6
6
|
import { isControlFlow } from './isControlFlow.ts'
|
|
7
7
|
import { lowerContext } from './lowerContext.ts'
|
|
@@ -32,12 +32,29 @@ export function generateBuild(
|
|
|
32
32
|
let counter = 0
|
|
33
33
|
const nextVar = (prefix: string): string => `${prefix}${counter++}`
|
|
34
34
|
|
|
35
|
+
/* In a layout, `<slot/>` outlets are rewritten to `OUTLET_TAG` elements up front
|
|
36
|
+
(`asOutlet`) so the static-clone path carries them as ordinary structure. `asOutlet`
|
|
37
|
+
CLONES every element it descends through and drops the slot's anchor hole, so the
|
|
38
|
+
shared skeleton context must walk THIS rewritten tree — the one the build traversal
|
|
39
|
+
below reads — or its node-keyed hole indices key the originals and never match. */
|
|
40
|
+
const rootNodes = isLayout ? nodes.map(asOutlet) : nodes
|
|
41
|
+
|
|
35
42
|
/* Per-node skeleton position from the SAME pass the SSR back-end reads — so the client's
|
|
36
43
|
anchor/text-leaf decisions consult one source of truth instead of re-deriving the
|
|
37
|
-
position structurally (the drift the shared context exists to prevent).
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
position structurally (the drift the shared context exists to prevent). */
|
|
45
|
+
const { markText, elIndex, anIndex } = skeletonContext(rootNodes)
|
|
46
|
+
|
|
47
|
+
/* The hole's index, assigned by the shared skeletonContext walk — the sole numberer. A
|
|
48
|
+
missing entry means this back-end reached a hole the shared walk didn't number: a
|
|
49
|
+
structural divergence between the two, surfaced loudly at compile time rather than as a
|
|
50
|
+
runtime hydration desync. */
|
|
51
|
+
function holeIndex(map: WeakMap<object, number>, key: object): number {
|
|
52
|
+
const index = map.get(key)
|
|
53
|
+
if (index === undefined) {
|
|
54
|
+
throw new Error('[abide] skeleton hole not numbered by the shared positional walk')
|
|
55
|
+
}
|
|
56
|
+
return index
|
|
57
|
+
}
|
|
41
58
|
|
|
42
59
|
/* The shared signal→`model` lowering + branch-scoped nested-script deref scope. */
|
|
43
60
|
const {
|
|
@@ -107,12 +124,17 @@ export function generateBuild(
|
|
|
107
124
|
block or slot drops an `<!--a-->` anchor at its position and mounts there (see
|
|
108
125
|
`anchorCursor`), so it can sit ANYWHERE among static siblings. Static descendants are
|
|
109
126
|
plain markup. */
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
/* The skeleton anchor var for an anchor-positioned node: declares `an<n> = sk.an[i]` as
|
|
128
|
+
a bind and returns the var name. The three anchored kinds (control-flow/component,
|
|
129
|
+
outlet, slot) all mount at this `<!--a-->` anchor, so they number through this ONE
|
|
130
|
+
site (`anIndex`) — no per-branch copy of the lookup to drift from the runtime scan. */
|
|
131
|
+
function anchorVarAt(node: TemplateNode, skVar: string, binds: string[]): string {
|
|
132
|
+
const anchorVar = nextVar('an')
|
|
133
|
+
binds.push(`const ${anchorVar} = ${skVar}.an[${holeIndex(anIndex, node)}];\n`)
|
|
134
|
+
return anchorVar
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function skeletonMarkup(node: TemplateNode, skVar: string, binds: string[]): string {
|
|
116
138
|
if (node.kind === 'text') {
|
|
117
139
|
/* Reactive text reached here is INTERLEAVED with element siblings (a text-leaf
|
|
118
140
|
is bound via `generateChildren` instead). It can't be element-positioned, so
|
|
@@ -124,31 +146,23 @@ export function generateBuild(
|
|
|
124
146
|
return staticTextPart(part.value)
|
|
125
147
|
}
|
|
126
148
|
binds.push(
|
|
127
|
-
`appendTextAt(${skVar}.an[${
|
|
149
|
+
`appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], () => (${lowerExpression(part.code)}));\n`,
|
|
128
150
|
)
|
|
129
151
|
return '<!--a-->'
|
|
130
152
|
})
|
|
131
153
|
.join('')
|
|
132
154
|
}
|
|
133
|
-
if (isControlFlow(node)) {
|
|
134
|
-
/* A control-flow block at its position: an `<!--a-->` anchor
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
element the anchor was cloned into
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
if (isControlFlow(node) || node.kind === 'component') {
|
|
156
|
+
/* A control-flow block OR a child component at its position: an `<!--a-->` anchor
|
|
157
|
+
in the clone, its content mounted as a marker-bounded range at it. `anchorCursor`
|
|
158
|
+
parks the hydrate cursor past the anchor and returns the create insertion
|
|
159
|
+
reference; the parent is the located element the anchor was cloned into
|
|
160
|
+
(`anchor.parentNode`). A component takes an anchor like a block — no wrapper
|
|
161
|
+
element — so its root lays out as a true direct child of `anchor.parentNode`. */
|
|
162
|
+
const anchorVar = anchorVarAt(node, skVar, binds)
|
|
140
163
|
binds.push(generateChild(node, `${anchorVar}.parentNode`, `anchorCursor(${anchorVar})`))
|
|
141
164
|
return '<!--a-->'
|
|
142
165
|
}
|
|
143
|
-
if (node.kind === 'component') {
|
|
144
|
-
/* The wrapper element is a positioned hole in the skeleton; the child mounts
|
|
145
|
-
into the located node. display:contents (idempotent, static so it lives in
|
|
146
|
-
the markup) keeps the wrapper out of layout — a pure mount host. */
|
|
147
|
-
const tag = componentWrapperTag(node.name)
|
|
148
|
-
const { code } = mountComponent(node, `${skVar}.el[${counter.el++}]`)
|
|
149
|
-
binds.push(code)
|
|
150
|
-
return `<${tag} ${HOLE_ATTRIBUTE} style="display:contents"></${tag}>`
|
|
151
|
-
}
|
|
152
166
|
if (node.kind === 'script') {
|
|
153
167
|
/* A nested `<script>` (scoped reactive block) emits no markup — its lowered body
|
|
154
168
|
runs as a bind, in document order, so its signals are declared before the later
|
|
@@ -166,11 +180,18 @@ export function generateBuild(
|
|
|
166
180
|
if (node.kind !== 'element') {
|
|
167
181
|
return '' // <style> emits no markup
|
|
168
182
|
}
|
|
183
|
+
if (node.tag === OUTLET_TAG) {
|
|
184
|
+
/* A layout's router fill point at its position: an `<!--a-->` anchor, an empty
|
|
185
|
+
`outlet` boundary the router fills with the next chain layer (`fillBoundary`).
|
|
186
|
+
No wrapper element — the filled child lays out as a direct child of the parent. */
|
|
187
|
+
const anchorVar = anchorVarAt(node, skVar, binds)
|
|
188
|
+
binds.push(`outlet(${anchorVar}.parentNode, anchorCursor(${anchorVar}));\n`)
|
|
189
|
+
return '<!--a-->'
|
|
190
|
+
}
|
|
169
191
|
if (node.tag === 'slot') {
|
|
170
192
|
/* A `<slot>` outlet at its position: an `<!--a-->` anchor, the slot's content
|
|
171
193
|
mounted as a marker-bounded range (`mountSlot`) so it positions like a block. */
|
|
172
|
-
const anchorVar =
|
|
173
|
-
binds.push(`const ${anchorVar} = ${skVar}.an[${counter.an++}];\n`)
|
|
194
|
+
const anchorVar = anchorVarAt(node, skVar, binds)
|
|
174
195
|
const hostVar = nextVar('host')
|
|
175
196
|
binds.push(
|
|
176
197
|
`mountSlot(${anchorVar}.parentNode, (${hostVar}) => {\n${generateSlot(node, hostVar)}}, anchorCursor(${anchorVar}));\n`,
|
|
@@ -195,7 +216,7 @@ export function generateBuild(
|
|
|
195
216
|
index BEFORE recursing, so holes number in pre-order — the order the runtime's
|
|
196
217
|
path walk produces them. */
|
|
197
218
|
elVar = nextVar('el')
|
|
198
|
-
binds.push(`const ${elVar} = ${skVar}.el[${
|
|
219
|
+
binds.push(`const ${elVar} = ${skVar}.el[${holeIndex(elIndex, node)}];\n`)
|
|
199
220
|
openTag += ` ${HOLE_ATTRIBUTE}`
|
|
200
221
|
for (const attr of node.attrs) {
|
|
201
222
|
if (attr.kind !== 'static') {
|
|
@@ -224,7 +245,7 @@ export function generateBuild(
|
|
|
224
245
|
/* A nested `<script>` among the children scopes its bindings to this subtree (its
|
|
225
246
|
later siblings auto-deref them); pop after. */
|
|
226
247
|
const inner = withNestedScripts(node.children, () =>
|
|
227
|
-
node.children.map((child) => skeletonMarkup(child, skVar,
|
|
248
|
+
node.children.map((child) => skeletonMarkup(child, skVar, binds)).join(''),
|
|
228
249
|
)
|
|
229
250
|
return `${openTag}${inner}</${node.tag}>`
|
|
230
251
|
}
|
|
@@ -233,12 +254,12 @@ export function generateBuild(
|
|
|
233
254
|
skeleton string (parsed once, cloned per mount) plus each hole's wiring against
|
|
234
255
|
its located node. */
|
|
235
256
|
function generateSkeleton(
|
|
236
|
-
node: Extract<TemplateNode, { kind: 'element'
|
|
257
|
+
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
237
258
|
parentVar: string,
|
|
238
259
|
): string {
|
|
239
260
|
const skVar = nextVar('sk')
|
|
240
261
|
const binds: string[] = []
|
|
241
|
-
const html = skeletonMarkup(node, skVar,
|
|
262
|
+
const html = skeletonMarkup(node, skVar, binds)
|
|
242
263
|
return `const ${skVar} = skeleton(${parentVar}, ${JSON.stringify(html)});\n${binds.join('')}`
|
|
243
264
|
}
|
|
244
265
|
|
|
@@ -270,14 +291,19 @@ export function generateBuild(
|
|
|
270
291
|
})
|
|
271
292
|
.join('')
|
|
272
293
|
}
|
|
294
|
+
if (node.kind === 'element' && node.tag === OUTLET_TAG) {
|
|
295
|
+
/* A standalone layout outlet (a top-level/element-nested `<slot/>` rewritten by
|
|
296
|
+
`asOutlet`, reached outside any skeleton): an empty `outlet` boundary at
|
|
297
|
+
`before`, no anchor — the router fills it with the next chain layer. */
|
|
298
|
+
return `outlet(${parentVar}, ${before});\n`
|
|
299
|
+
}
|
|
273
300
|
if (node.kind === 'element' && node.tag === 'slot') {
|
|
274
|
-
/* In a layout, `<slot/>` is the router's page outlet
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
reached inside a control-flow branch.) */
|
|
301
|
+
/* In a layout, `<slot/>` is the router's page outlet (`outlet` boundary the
|
|
302
|
+
router fills with the next chain layer). Top-level/element-nested layout slots
|
|
303
|
+
are rewritten to `OUTLET_TAG` up front by `asOutlet` and handled above; this
|
|
304
|
+
covers a layout slot reached inside a control-flow branch. */
|
|
279
305
|
if (isLayout) {
|
|
280
|
-
return `
|
|
306
|
+
return `outlet(${parentVar}, ${before});\n`
|
|
281
307
|
}
|
|
282
308
|
return generateSlot(node, parentVar)
|
|
283
309
|
}
|
|
@@ -301,10 +327,10 @@ export function generateBuild(
|
|
|
301
327
|
return '' // branches are consumed by their await block, never standalone
|
|
302
328
|
}
|
|
303
329
|
if (node.kind === 'component') {
|
|
304
|
-
/* A standalone component
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return
|
|
330
|
+
/* A standalone component (top-level, or a bare child of a branch/row/slot) mounts
|
|
331
|
+
directly as a marker range on `parentVar` at `before` — no anchor, no wrapper,
|
|
332
|
+
same as a standalone control-flow block routes through `generateIf`/etc. */
|
|
333
|
+
return generateChildComponent(node, parentVar, before)
|
|
308
334
|
}
|
|
309
335
|
if (node.kind === 'switch') {
|
|
310
336
|
return generateSwitch(node, parentVar, before)
|
|
@@ -389,8 +415,6 @@ export function generateBuild(
|
|
|
389
415
|
return `if ($props && $props.$children) { ${invoke}; } else {\n${fallback}}\n`
|
|
390
416
|
}
|
|
391
417
|
|
|
392
|
-
/* Mounts a child component into a wrapper element, passing each prop as a
|
|
393
|
-
reactive thunk so the child re-reads when the parent expression changes. */
|
|
394
418
|
/* The prop + slot thunks a child mount receives — its props as value thunks and
|
|
395
419
|
its slot content as a host-taking builder (`$children`). */
|
|
396
420
|
function componentParts(node: Extract<TemplateNode, { kind: 'component' }>): string[] {
|
|
@@ -404,18 +428,18 @@ export function generateBuild(
|
|
|
404
428
|
return parts
|
|
405
429
|
}
|
|
406
430
|
|
|
407
|
-
/* Mounts a child
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
431
|
+
/* Mounts a child component as a marker-bounded range on `parentVar`, positioned at
|
|
432
|
+
`before` (a skeleton anchor's `anchorCursor`, or `null` for a standalone child).
|
|
433
|
+
`mountRange` opens the `[`/`]` markers and builds the child between them — no
|
|
434
|
+
wrapper element — so the child's root is a true direct child of `parentVar`.
|
|
435
|
+
Hydration stays ambient, so the child claims its server range in place. The
|
|
436
|
+
component name passes as the scope label (the inspector's `<Counter>` name). */
|
|
437
|
+
function generateChildComponent(
|
|
411
438
|
node: Extract<TemplateNode, { kind: 'component' }>,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
`const ${wrapper} = ${varExpr};\n` +
|
|
417
|
-
`mountChild(${wrapper}, ${node.name}, { ${componentParts(node).join(', ')} });\n`
|
|
418
|
-
return { code, varName: wrapper }
|
|
439
|
+
parentVar: string,
|
|
440
|
+
before: string,
|
|
441
|
+
): string {
|
|
442
|
+
return `mountChild(${parentVar}, ${node.name}, { ${componentParts(node).join(', ')} }, ${before}, ${JSON.stringify(node.name)});\n`
|
|
419
443
|
}
|
|
420
444
|
|
|
421
445
|
/* An await block: pending → resolved(value) / error branches. Each branch is a
|
|
@@ -595,20 +619,7 @@ export function generateBuild(
|
|
|
595
619
|
)
|
|
596
620
|
}
|
|
597
621
|
|
|
598
|
-
|
|
599
|
-
router fills it later) — exactly the SSR placeholder. Rewriting it to an element
|
|
600
|
-
node up front lets the static-clone path carry it as ordinary structure. */
|
|
601
|
-
function asOutlet(node: TemplateNode): TemplateNode {
|
|
602
|
-
if (node.kind !== 'element') {
|
|
603
|
-
return node
|
|
604
|
-
}
|
|
605
|
-
if (node.tag === 'slot') {
|
|
606
|
-
return { ...node, tag: OUTLET_TAG, attrs: [], children: [] }
|
|
607
|
-
}
|
|
608
|
-
return { ...node, children: node.children.map(asOutlet) }
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
return generateChildren(isLayout ? nodes.map(asOutlet) : nodes, hostVar)
|
|
622
|
+
return generateChildren(rootNodes, hostVar)
|
|
612
623
|
}
|
|
613
624
|
|
|
614
625
|
/* A text node that is purely whitespace (no interpolation, only blank static
|
|
@@ -633,7 +644,7 @@ imperative. Static text and elements nested INSIDE a qualifying element are fine
|
|
|
633
644
|
enclosed by its tags.
|
|
634
645
|
*/
|
|
635
646
|
function isStaticCloneableElement(node: TemplateNode): boolean {
|
|
636
|
-
if (node.kind !== 'element' || node.tag === 'slot') {
|
|
647
|
+
if (node.kind !== 'element' || node.tag === 'slot' || node.tag === OUTLET_TAG) {
|
|
637
648
|
return false
|
|
638
649
|
}
|
|
639
650
|
if (node.attrs.some((attr) => attr.kind !== 'static')) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
|
|
1
2
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
2
|
-
import {
|
|
3
|
+
import { asOutlet } from './asOutlet.ts'
|
|
3
4
|
import { groupBindParts } from './groupBindParts.ts'
|
|
4
|
-
import {
|
|
5
|
+
import { isAnchorPositioned } from './isAnchorPositioned.ts'
|
|
5
6
|
import { lowerContext } from './lowerContext.ts'
|
|
6
7
|
import { scopeAttr } from './scopeAttr.ts'
|
|
7
8
|
import { skeletonContext } from './skeletonContext.ts'
|
|
@@ -18,6 +19,17 @@ import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
|
18
19
|
const RANGE_OPEN = '<!--[-->'
|
|
19
20
|
const RANGE_CLOSE = '<!--]-->'
|
|
20
21
|
|
|
22
|
+
/* The `then`/`catch`/`finally` branch child of an await/try block, or undefined. */
|
|
23
|
+
function branchNamed(
|
|
24
|
+
children: TemplateNode[],
|
|
25
|
+
which: 'then' | 'catch' | 'finally',
|
|
26
|
+
): Extract<TemplateNode, { kind: 'branch' }> | undefined {
|
|
27
|
+
return children.find(
|
|
28
|
+
(child): child is Extract<TemplateNode, { kind: 'branch' }> =>
|
|
29
|
+
child.kind === 'branch' && child.branch === which,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/*
|
|
22
34
|
Server code generator: turns the parsed template into statements that push HTML
|
|
23
35
|
fragments onto an output array, reading the document synchronously (no DOM, no
|
|
@@ -64,11 +76,17 @@ export function generateSSR(
|
|
|
64
76
|
return children.map((child) => generate(child, target)).join('')
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
/* In a layout, rewrite `<slot/>` outlets to `OUTLET_TAG` elements up front (the same shared
|
|
80
|
+
`asOutlet` the client back-end runs), then drive both the skeleton context and the
|
|
81
|
+
traversal from this tree — one decision site for the outlet, and the outlet emitted bare
|
|
82
|
+
through the generic element path exactly as the client clones it. */
|
|
83
|
+
const rootNodes = isLayout ? nodes.map(asOutlet) : nodes
|
|
84
|
+
|
|
67
85
|
/* Per-node skeleton position, computed once. Both back-ends read this single source of
|
|
68
86
|
truth so their `<!--a-->` anchor placement cannot drift — the fresh-context boundaries
|
|
69
87
|
(control-flow branches, component/slot/snippet content) are enumerated there, not
|
|
70
88
|
re-tracked here as mutable state that a forgotten reset could leak past. */
|
|
71
|
-
const { inSkeleton, markText } = skeletonContext(
|
|
89
|
+
const { inSkeleton, markText } = skeletonContext(rootNodes)
|
|
72
90
|
|
|
73
91
|
/* A control-flow branch's content, generated exactly like a normal child list so
|
|
74
92
|
a branch holds ANY content (components, text, nested blocks). `generate` emits
|
|
@@ -93,12 +111,12 @@ export function generateSSR(
|
|
|
93
111
|
inSkeleton.get(node) ? push(target, '<!--a-->') : ''
|
|
94
112
|
|
|
95
113
|
function generate(node: TemplateNode, target: string): string {
|
|
96
|
-
/*
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const anchor =
|
|
114
|
+
/* Every kind that mounts as a marker range is positioned by an `<!--a-->` anchor when
|
|
115
|
+
in a skeleton context: control-flow blocks, child components, and a layout's outlet /
|
|
116
|
+
a component's `<slot>` (both elements). `isAnchorPositioned` is the ONE decision site
|
|
117
|
+
(mirrored by the client's `skeletonMarkup`); `anchorMark` no-ops outside a skeleton,
|
|
118
|
+
so non-anchored nodes ignore the precomputed `anchor`. */
|
|
119
|
+
const anchor = isAnchorPositioned(node) ? anchorMark(node, target) : ''
|
|
102
120
|
if (node.kind === 'text') {
|
|
103
121
|
return node.parts
|
|
104
122
|
.map((part) => {
|
|
@@ -180,11 +198,11 @@ export function generateSSR(
|
|
|
180
198
|
return ''
|
|
181
199
|
}
|
|
182
200
|
if (node.kind === 'component') {
|
|
183
|
-
/* Server-render the child via its `render` and inline the HTML inside
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
`$children` the child
|
|
187
|
-
|
|
201
|
+
/* Server-render the child via its `render` and inline the HTML inside the same
|
|
202
|
+
`[ … ]` marker range the client mounts into (`mountRange`) — no wrapper element,
|
|
203
|
+
so SSR and client agree and the child's root lays out as a direct child. Props
|
|
204
|
+
pass as thunks; slot content passes as a string-returning `$children` the child
|
|
205
|
+
invokes from its <slot>. */
|
|
188
206
|
const parts = node.props.map(
|
|
189
207
|
(prop) => `${JSON.stringify(prop.name)}: () => (${lowerExpression(prop.code)})`,
|
|
190
208
|
)
|
|
@@ -206,20 +224,30 @@ export function generateSSR(
|
|
|
206
224
|
the enclosing render body, including from branch closures.) */
|
|
207
225
|
const result = nextVar('$child')
|
|
208
226
|
return (
|
|
209
|
-
|
|
227
|
+
anchor +
|
|
228
|
+
push(target, RANGE_OPEN) +
|
|
210
229
|
`const ${result} = ${node.name}.render({ ${parts.join(', ')} });\n` +
|
|
211
230
|
`${target}.push(${result}.html);\n` +
|
|
212
231
|
`for (const $a of ${result}.awaits) { $awaits.push($a); }\n` +
|
|
213
|
-
push(target,
|
|
232
|
+
push(target, RANGE_CLOSE)
|
|
214
233
|
)
|
|
215
234
|
}
|
|
235
|
+
if (node.kind === 'element' && node.tag === OUTLET_TAG) {
|
|
236
|
+
/* A layout's router fill point (`asOutlet` rewrote its `<slot/>`): an `<!--a-->`
|
|
237
|
+
anchor (in a skeleton) + an empty `<!--abide:outlet-->`…`<!--/abide:outlet-->`
|
|
238
|
+
boundary the chain composer folds the child layer into (`renderChain`) and the
|
|
239
|
+
client router fills/hydrates (`outlet`/`fillBoundary`) — no wrapper element. */
|
|
240
|
+
return anchor + push(target, `<!--${OUTLET_OPEN}--><!--${OUTLET_CLOSE}-->`)
|
|
241
|
+
}
|
|
216
242
|
if (node.kind === 'element' && node.tag === 'slot') {
|
|
217
|
-
/*
|
|
218
|
-
|
|
243
|
+
/* `asOutlet` already rewrote a layout's top-level/element-nested `<slot/>` to an
|
|
244
|
+
`OUTLET_TAG` element (handled above), so a `slot` node reaching here in a layout
|
|
245
|
+
is control-flow-nested — emit the same empty outlet boundary the client's
|
|
246
|
+
control-flow-nested path builds, which the chain composer folds the child into. */
|
|
219
247
|
if (isLayout) {
|
|
220
|
-
return push(target,
|
|
248
|
+
return push(target, `<!--${OUTLET_OPEN}--><!--${OUTLET_CLOSE}-->`)
|
|
221
249
|
}
|
|
222
|
-
return generateSlot(node, target)
|
|
250
|
+
return generateSlot(node, target, anchor)
|
|
223
251
|
}
|
|
224
252
|
let code = push(target, `<${node.tag}`)
|
|
225
253
|
/* Every `<style>` active at this element (own siblings + ancestors) — same set
|
|
@@ -272,6 +300,7 @@ export function generateSSR(
|
|
|
272
300
|
function generateSlot(
|
|
273
301
|
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
274
302
|
target: string,
|
|
303
|
+
anchor: string,
|
|
275
304
|
): string {
|
|
276
305
|
const wrap = inSkeleton.get(node)
|
|
277
306
|
const fallback = generateInto(node.children, target)
|
|
@@ -282,7 +311,7 @@ export function generateSSR(
|
|
|
282
311
|
if (!wrap) {
|
|
283
312
|
return body
|
|
284
313
|
}
|
|
285
|
-
return `${
|
|
314
|
+
return `${anchor}${openRange(target)}${body}${closeRange(target)}`
|
|
286
315
|
}
|
|
287
316
|
|
|
288
317
|
/* Boundary markers + a `$awaits` registration carrying the promise and
|
|
@@ -291,17 +320,12 @@ export function generateSSR(
|
|
|
291
320
|
an empty boundary — its resolved branch is the children bound to `node.as` — and
|
|
292
321
|
flags the entry so `renderToStream` settles it before the first flush. */
|
|
293
322
|
function generateAwait(node: Extract<TemplateNode, { kind: 'await' }>, target: string): string {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
(child): child is Extract<TemplateNode, { kind: 'branch' }> =>
|
|
297
|
-
child.kind === 'branch' && child.branch === which,
|
|
298
|
-
)
|
|
299
|
-
const catchBranch = branchOf('catch')
|
|
300
|
-
const finallyChildren = branchOf('finally')?.children ?? []
|
|
323
|
+
const catchBranch = branchNamed(node.children, 'catch')
|
|
324
|
+
const finallyChildren = branchNamed(node.children, 'finally')?.children ?? []
|
|
301
325
|
/* Resolved branch + its bound value: the children directly when blocking, the
|
|
302
326
|
`then` child when streaming. Pending (streaming only) is the non-branch
|
|
303
327
|
children. */
|
|
304
|
-
const thenBranch =
|
|
328
|
+
const thenBranch = branchNamed(node.children, 'then')
|
|
305
329
|
const resolvedChildren = node.blocking
|
|
306
330
|
? node.children.filter((child) => child.kind !== 'branch')
|
|
307
331
|
: (thenBranch?.children ?? [])
|
|
@@ -342,13 +366,8 @@ export function generateSSR(
|
|
|
342
366
|
an enclosing boundary / the 500 / the stream). Boundary comments let hydration
|
|
343
367
|
discard the server content if the client adoption fails. */
|
|
344
368
|
function generateTry(node: Extract<TemplateNode, { kind: 'try' }>, target: string): string {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
(child): child is Extract<TemplateNode, { kind: 'branch' }> =>
|
|
348
|
-
child.kind === 'branch' && child.branch === which,
|
|
349
|
-
)
|
|
350
|
-
const catchBranch = branchOf('catch')
|
|
351
|
-
const finallyChildren = branchOf('finally')?.children ?? []
|
|
369
|
+
const catchBranch = branchNamed(node.children, 'catch')
|
|
370
|
+
const finallyChildren = branchNamed(node.children, 'finally')?.children ?? []
|
|
352
371
|
const guarded = node.children.filter((child) => child.kind !== 'branch')
|
|
353
372
|
const errName = catchBranch?.as ?? '_error'
|
|
354
373
|
const id = nextVar('$tid')
|
|
@@ -371,5 +390,5 @@ export function generateSSR(
|
|
|
371
390
|
return code
|
|
372
391
|
}
|
|
373
392
|
|
|
374
|
-
return generateInto(
|
|
393
|
+
return generateInto(rootNodes, '$out')
|
|
375
394
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
2
|
+
import { isControlFlow } from './isControlFlow.ts'
|
|
3
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
The node kinds that mount as a marker range and so take an `<!--a-->` anchor when in a
|
|
7
|
+
skeleton — control-flow, child component, and the two outlet elements (a layout's
|
|
8
|
+
`OUTLET_TAG`, a component's `<slot>`). The single positioning predicate every back-end
|
|
9
|
+
shares — `generateSSR` gates its anchor emission on it, `skeletonContext` gates its anchor
|
|
10
|
+
indexing on it — so SSR, the client clone, and the shared numberer cannot disagree on which
|
|
11
|
+
nodes anchor.
|
|
12
|
+
*/
|
|
13
|
+
export function isAnchorPositioned(node: TemplateNode): boolean {
|
|
14
|
+
return (
|
|
15
|
+
isControlFlow(node) ||
|
|
16
|
+
node.kind === 'component' ||
|
|
17
|
+
(node.kind === 'element' && (node.tag === OUTLET_TAG || node.tag === 'slot'))
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -5,7 +5,9 @@ A control-flow block — `if`/`each`/`await`/`switch`/`try`. In a skeleton each
|
|
|
5
5
|
`<!--a-->` anchor cloned into its located parent at the block's position (see `anchorCursor`),
|
|
6
6
|
so a block can sit ANYWHERE among static siblings. The shared classification under block
|
|
7
7
|
anchor placement: both back-ends — and `skeletonable` — consult this so they agree on which
|
|
8
|
-
nodes are anchor-positioned
|
|
8
|
+
nodes are anchor-positioned. A child component is anchor-positioned too (it mounts as a
|
|
9
|
+
`[`…`]` range like a block, see `mountRange`); the back-ends OR this with `kind ===
|
|
10
|
+
'component'` rather than folding it in here, since a standalone component routes differently.
|
|
9
11
|
*/
|
|
10
12
|
export function isControlFlow(node: TemplateNode): boolean {
|
|
11
13
|
return (
|
|
@@ -8,7 +8,7 @@ located element (`generateBuild`) / emits it without an `<!--a-->` prefix (`gene
|
|
|
8
8
|
reactive text interleaved with element children is anchor-positioned instead. Computing it
|
|
9
9
|
once keeps the SSR string and the client skeleton from disagreeing about a `<!--a-->` — the
|
|
10
10
|
first slice of lifting the positional model out of the two parallel traversals, alongside
|
|
11
|
-
the already-shared `
|
|
11
|
+
the already-shared `isControlFlow` and `skeletonable`.
|
|
12
12
|
*/
|
|
13
13
|
export function isTextLeaf(node: Extract<TemplateNode, { kind: 'element' }>): boolean {
|
|
14
14
|
return node.children.every((child) => child.kind === 'text' || child.kind === 'style')
|
|
@@ -148,7 +148,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
148
148
|
} else {
|
|
149
149
|
attrs.push({ kind: 'expression', name, code, loc })
|
|
150
150
|
}
|
|
151
|
-
} else {
|
|
151
|
+
} else if (source.charAt(cursor) === '"' || source.charAt(cursor) === "'") {
|
|
152
152
|
const quote = source.charAt(cursor)
|
|
153
153
|
cursor += 1
|
|
154
154
|
let value = ''
|
|
@@ -158,6 +158,15 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
158
158
|
}
|
|
159
159
|
cursor += 1 // past closing quote
|
|
160
160
|
attrs.push({ kind: 'static', name, value })
|
|
161
|
+
} else {
|
|
162
|
+
/* Unquoted value (`<input type=text>`): runs to the next whitespace or
|
|
163
|
+
`>`, per the HTML unquoted-attribute rule. No delimiter to consume. */
|
|
164
|
+
let value = ''
|
|
165
|
+
while (cursor < source.length && !/[\s>]/.test(source.charAt(cursor))) {
|
|
166
|
+
value += source.charAt(cursor)
|
|
167
|
+
cursor += 1
|
|
168
|
+
}
|
|
169
|
+
attrs.push({ kind: 'static', name, value })
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
return attrs
|
|
@@ -35,8 +35,15 @@ export function renameSignalRefs(
|
|
|
35
35
|
if (ts.isPropertyAccessExpression(node)) {
|
|
36
36
|
skip.add(node.name)
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/* A declaration name is a binding, not a value read. A plain identifier is
|
|
39
|
+
skipped directly; a destructuring pattern's leaf names are collected so a
|
|
40
|
+
bound name shadowing a signal (`const { count } = …`) is never rewritten. */
|
|
41
|
+
if (ts.isVariableDeclaration(node)) {
|
|
42
|
+
if (ts.isIdentifier(node.name)) {
|
|
43
|
+
skip.add(node.name)
|
|
44
|
+
} else {
|
|
45
|
+
collectBindingIdentifiers(node.name, skip)
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
48
|
if (ts.isParameter(node) && ts.isIdentifier(node.name)) {
|
|
42
49
|
skip.add(node.name)
|
|
@@ -180,6 +187,22 @@ function collectStatementBindings(statement: ts.Statement, into: Set<string>): v
|
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
189
|
|
|
190
|
+
/* The identifier NODES a binding name binds — a plain identifier or the leaves of a
|
|
191
|
+
destructuring pattern (object/array, including nested patterns and rest elements).
|
|
192
|
+
For `{ a: b }` only the bound name `b` is a binding; `a` is the source property.
|
|
193
|
+
Feeds the skip set so a destructured name shadowing a signal is never rewritten. */
|
|
194
|
+
function collectBindingIdentifiers(name: ts.BindingName, into: Set<ts.Node>): void {
|
|
195
|
+
if (ts.isIdentifier(name)) {
|
|
196
|
+
into.add(name)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
for (const element of name.elements) {
|
|
200
|
+
if (ts.isBindingElement(element)) {
|
|
201
|
+
collectBindingIdentifiers(element.name, into)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
183
206
|
/* Every identifier bound by a binding name — a plain identifier or the leaves of a
|
|
184
207
|
destructuring pattern (object/array, including rest elements). */
|
|
185
208
|
function collectBindingNames(name: ts.BindingName, into: Set<string>): void {
|