@abide/abide 0.32.1 → 0.33.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 +3 -3
- package/CHANGELOG.md +93 -63
- package/package.json +6 -2
- package/src/lib/server/runtime/buildCacheSnapshot.ts +5 -4
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/shared/cache.ts +43 -29
- package/src/lib/shared/types/CacheEntry.ts +12 -12
- package/src/lib/shared/types/CacheOptions.ts +17 -13
- package/src/lib/ui/README.md +3 -3
- package/src/lib/ui/compile/HTML_TAGS.ts +132 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +4 -1
- package/src/lib/ui/compile/componentWrapperTag.ts +13 -10
- package/src/lib/ui/compile/generateBuild.ts +265 -121
- package/src/lib/ui/compile/generateSSR.ts +78 -37
- package/src/lib/ui/compile/parseTemplate.ts +52 -0
- package/src/lib/ui/compile/skeletonable.ts +80 -0
- package/src/lib/ui/dom/MATHML_NAMESPACE.ts +6 -0
- package/src/lib/ui/dom/SVG_NAMESPACE.ts +7 -0
- package/src/lib/ui/dom/anchorCursor.ts +24 -0
- package/src/lib/ui/dom/appendSnippet.ts +1 -1
- package/src/lib/ui/dom/appendTextAt.ts +70 -0
- package/src/lib/ui/dom/awaitBlock.ts +27 -7
- package/src/lib/ui/dom/cloneStatic.ts +15 -24
- package/src/lib/ui/dom/each.ts +44 -25
- package/src/lib/ui/dom/eachAsync.ts +6 -2
- package/src/lib/ui/dom/effectiveChildNamespace.ts +13 -0
- package/src/lib/ui/dom/enterNamespace.ts +20 -0
- package/src/lib/ui/dom/fillBefore.ts +20 -3
- package/src/lib/ui/dom/foreignWrapperTag.ts +22 -0
- package/src/lib/ui/dom/hydrate.ts +1 -1
- package/src/lib/ui/dom/inheritedNamespace.ts +19 -0
- package/src/lib/ui/dom/mountSlot.ts +32 -0
- package/src/lib/ui/dom/openMarker.ts +4 -2
- package/src/lib/ui/dom/skeleton.ts +202 -0
- package/src/lib/ui/dom/switchBlock.ts +10 -3
- package/src/lib/ui/dom/templateFor.ts +28 -0
- package/src/lib/ui/dom/tryBlock.ts +7 -5
- package/src/lib/ui/dom/types/SkeletonHoles.ts +8 -0
- package/src/lib/ui/dom/when.ts +6 -2
- package/src/lib/ui/installHotBridge.ts +8 -2
- package/src/lib/ui/runtime/HOLE_ATTRIBUTE.ts +9 -0
- package/src/lib/ui/runtime/RENDER.ts +7 -0
- package/src/lib/ui/runtime/createDoc.ts +11 -8
- package/src/lib/ui/runtime/types/PathWalk.ts +10 -0
- package/src/lib/ui/runtime/types/UiProps.ts +3 -4
- package/src/lib/ui/runtime/walkPath.ts +27 -0
- package/template/src/ui/pages/about/page.abide +4 -6
- package/template/src/ui/pages/layout.abide +21 -0
- package/template/src/ui/pages/page.abide +5 -8
- package/src/lib/ui/compile/partitionSlots.ts +0 -36
- package/src/lib/ui/dom/openChild.ts +0 -22
- package/src/lib/ui/runtime/pathExists.ts +0 -23
- package/src/lib/ui/runtime/valueAtPath.ts +0 -18
- package/template/src/ui/Layout.abide +0 -19
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
1
2
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
2
3
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
3
4
|
import { componentWrapperTag } from './componentWrapperTag.ts'
|
|
4
5
|
import { groupBindParts } from './groupBindParts.ts'
|
|
5
6
|
import { lowerContext } from './lowerContext.ts'
|
|
6
|
-
import { partitionSlots } from './partitionSlots.ts'
|
|
7
7
|
import { scopeAttr } from './scopeAttr.ts'
|
|
8
8
|
import { staticAttr } from './staticAttr.ts'
|
|
9
|
-
import { staticAttrValue } from './staticAttrValue.ts'
|
|
10
9
|
import { staticTextPart } from './staticTextPart.ts'
|
|
11
10
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
12
11
|
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
@@ -37,69 +36,207 @@ export function generateBuild(
|
|
|
37
36
|
withNestedScripts,
|
|
38
37
|
} = lowerContext(stateNames, derivedNames)
|
|
39
38
|
|
|
40
|
-
/*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
root (rows/branches, which are create-only). */
|
|
44
|
-
function generateElement(
|
|
39
|
+
/* Emits the wiring for one non-static attribute against an already-obtained skeleton
|
|
40
|
+
element var — reactive `attr`, `on` listener, `attach`, or a two-way `bind`. */
|
|
41
|
+
function dynamicAttr(
|
|
45
42
|
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
attr: Extract<
|
|
44
|
+
(typeof node.attrs)[number],
|
|
45
|
+
{ kind: 'expression' | 'event' | 'attach' | 'bind' }
|
|
46
|
+
>,
|
|
47
|
+
varName: string,
|
|
48
|
+
): string {
|
|
49
|
+
if (attr.kind === 'expression') {
|
|
50
|
+
return `attr(${varName}, ${JSON.stringify(attr.name)}, () => (${lowerExpression(attr.code)}));\n`
|
|
51
|
+
}
|
|
52
|
+
if (attr.kind === 'event') {
|
|
53
|
+
return `on(${varName}, ${JSON.stringify(attr.event)}, (${lowerExpression(attr.code)}));\n`
|
|
54
|
+
}
|
|
55
|
+
if (attr.kind === 'attach') {
|
|
56
|
+
return `attach(${varName}, (${lowerExpression(attr.code)}));\n`
|
|
57
|
+
}
|
|
58
|
+
if (attr.property === 'group') {
|
|
59
|
+
/* Grouped two-way: radio binds the path to the single checked `value`;
|
|
60
|
+
checkbox treats the path as an array, adding/removing `value` on toggle.
|
|
61
|
+
Membership reads the array via the lowered path and calls native
|
|
62
|
+
`.includes`/`.indexOf` (the doc API has no array search); mutations go
|
|
63
|
+
through `push`/`delete`, which lower to `add`/`remove` patches that the
|
|
64
|
+
doc reindexes. */
|
|
65
|
+
const { valueCode, isRadio } = groupBindParts(node)
|
|
66
|
+
const value = lowerExpression(valueCode)
|
|
67
|
+
if (isRadio) {
|
|
68
|
+
return (
|
|
69
|
+
`effect(() => { ${varName}.checked = (${lowerExpression(attr.code)}) === (${value}); });\n` +
|
|
70
|
+
`on(${varName}, "change", () => { if (${varName}.checked) { ${lowerStatement(`${attr.code} = ${valueCode}`)} } });\n`
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
`effect(() => { ${varName}.checked = (${lowerExpression(attr.code)}).includes(${value}); });\n` +
|
|
75
|
+
`on(${varName}, "change", () => { const $groupValue = ${value}; if (${varName}.checked) { if (!(${lowerExpression(attr.code)}).includes($groupValue)) { ${lowerStatement(`${attr.code}.push($groupValue)`)} } } else { const $groupIndex = (${lowerExpression(attr.code)}).indexOf($groupValue); if ($groupIndex !== -1) { ${lowerStatement(`delete ${attr.code}[$groupIndex]`)} } } });\n`
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
/* Two-way: drive the property from the path, and write the path back on the
|
|
79
|
+
property's native event (`input` for most fields, but `toggle` for
|
|
80
|
+
`<details open>`, `change` for checked/select). The path is an lvalue, so
|
|
81
|
+
the write lowers to an assignment. */
|
|
82
|
+
const event = bindListenEvent(attr.property, node.tag)
|
|
83
|
+
return (
|
|
84
|
+
`effect(() => { ${varName}.${attr.property} = ${lowerExpression(attr.code)}; });\n` +
|
|
85
|
+
`on(${varName}, ${JSON.stringify(event)}, () => { ${lowerStatement(`${attr.code} = ${varName}.${attr.property}`)} });\n`
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Renders a skeletonable node to its marker-stamped skeleton markup, appending each
|
|
90
|
+
hole's wiring to `binds`. Children are walked in document order, so the holes number
|
|
91
|
+
in the order the runtime produces them: element holes (reactive attr / text-leaf
|
|
92
|
+
text) by element-only path (`sk.el`, pre-order); anchor holes (interleaved reactive
|
|
93
|
+
text, control-flow blocks, slots) by document-order scan (`sk.an`). A control-flow
|
|
94
|
+
block or slot drops an `<!--a-->` anchor at its position and mounts there (see
|
|
95
|
+
`anchorCursor`), so it can sit ANYWHERE among static siblings. Static descendants are
|
|
96
|
+
plain markup. */
|
|
97
|
+
function skeletonMarkup(
|
|
98
|
+
node: TemplateNode,
|
|
99
|
+
skVar: string,
|
|
100
|
+
counter: { el: number; an: number },
|
|
101
|
+
binds: string[],
|
|
102
|
+
): string {
|
|
103
|
+
if (node.kind === 'text') {
|
|
104
|
+
/* Reactive text reached here is INTERLEAVED with element siblings (a text-leaf
|
|
105
|
+
is bound via `generateChildren` instead). It can't be element-positioned, so
|
|
106
|
+
it gets an `<!--a-->` anchor — kept in both SSR and client (like a control-flow
|
|
107
|
+
range marker), located by document-order scan (`sk.an`). */
|
|
108
|
+
return node.parts
|
|
109
|
+
.map((part) => {
|
|
110
|
+
if (part.kind === 'static') {
|
|
111
|
+
return staticTextPart(part.value)
|
|
112
|
+
}
|
|
113
|
+
binds.push(
|
|
114
|
+
`appendTextAt(${skVar}.an[${counter.an++}], () => (${lowerExpression(part.code)}));\n`,
|
|
115
|
+
)
|
|
116
|
+
return '<!--a-->'
|
|
117
|
+
})
|
|
118
|
+
.join('')
|
|
119
|
+
}
|
|
120
|
+
if (isControlFlowNode(node)) {
|
|
121
|
+
/* A control-flow block at its position: an `<!--a-->` anchor in the clone, the
|
|
122
|
+
block mounted at it. `anchorCursor` parks the hydrate cursor past the anchor
|
|
123
|
+
and returns the create insertion reference; the block's parent is the located
|
|
124
|
+
element the anchor was cloned into (`anchor.parentNode`). */
|
|
125
|
+
const anchorVar = nextVar('an')
|
|
126
|
+
binds.push(`const ${anchorVar} = ${skVar}.an[${counter.an++}];\n`)
|
|
127
|
+
binds.push(generateChild(node, `${anchorVar}.parentNode`, `anchorCursor(${anchorVar})`))
|
|
128
|
+
return '<!--a-->'
|
|
129
|
+
}
|
|
130
|
+
if (node.kind === 'component') {
|
|
131
|
+
/* The wrapper element is a positioned hole in the skeleton; the child mounts
|
|
132
|
+
into the located node (idempotent display:contents for a transparent wrap,
|
|
133
|
+
static so it lives in the markup). */
|
|
134
|
+
const { tag, transparent } = componentWrapperTag(node.name)
|
|
135
|
+
const { code } = mountComponent(node, `${skVar}.el[${counter.el++}]`)
|
|
136
|
+
binds.push(code)
|
|
137
|
+
const style = transparent ? ' style="display:contents"' : ''
|
|
138
|
+
return `<${tag} ${HOLE_ATTRIBUTE}${style}></${tag}>`
|
|
139
|
+
}
|
|
140
|
+
if (node.kind === 'script') {
|
|
141
|
+
/* A nested `<script>` (scoped reactive block) emits no markup — its lowered body
|
|
142
|
+
runs as a bind, in document order, so its signals are declared before the later
|
|
143
|
+
siblings that deref them (the enclosing `withNestedScripts` puts those names in
|
|
144
|
+
scope). */
|
|
145
|
+
binds.push(`${lowerStatement(node.code)}\n`)
|
|
146
|
+
return ''
|
|
147
|
+
}
|
|
148
|
+
if (node.kind === 'snippet') {
|
|
149
|
+
/* A `<template name>` snippet declares a hoisted builder, appending nothing here —
|
|
150
|
+
`{name(args)}` mounts it. Emit the declaration as a bind. */
|
|
151
|
+
binds.push(generateSnippet(node))
|
|
152
|
+
return ''
|
|
153
|
+
}
|
|
154
|
+
if (node.kind !== 'element') {
|
|
155
|
+
return '' // <style> emits no markup
|
|
156
|
+
}
|
|
157
|
+
if (node.tag === 'slot') {
|
|
158
|
+
/* A `<slot>` outlet at its position: an `<!--a-->` anchor, the slot's content
|
|
159
|
+
mounted as a marker-bounded range (`mountSlot`) so it positions like a block. */
|
|
160
|
+
const anchorVar = nextVar('an')
|
|
161
|
+
binds.push(`const ${anchorVar} = ${skVar}.an[${counter.an++}];\n`)
|
|
162
|
+
const hostVar = nextVar('host')
|
|
163
|
+
binds.push(
|
|
164
|
+
`mountSlot(${anchorVar}.parentNode, (${hostVar}) => {\n${generateSlot(node, hostVar)}}, anchorCursor(${anchorVar}));\n`,
|
|
165
|
+
)
|
|
166
|
+
return '<!--a-->'
|
|
167
|
+
}
|
|
168
|
+
const hasReactiveAttr = node.attrs.some((attr) => attr.kind !== 'static')
|
|
169
|
+
const hasReactiveText = node.children.some(
|
|
170
|
+
(child) => child.kind === 'text' && child.parts.some((part) => part.kind !== 'static'),
|
|
171
|
+
)
|
|
172
|
+
/* A text-leaf (only text/style children) with reactive text binds marker-free via
|
|
173
|
+
`generateChildren` on the located element; otherwise reactive text is interleaved
|
|
174
|
+
and uses `<!--a-->` anchors during the child recursion below. */
|
|
175
|
+
const isTextLeaf = node.children.every(
|
|
176
|
+
(child) => child.kind === 'text' || child.kind === 'style',
|
|
177
|
+
)
|
|
178
|
+
const textLeafBind = hasReactiveText && isTextLeaf
|
|
179
|
+
let openTag = `<${node.tag}`
|
|
180
|
+
let elVar = ''
|
|
181
|
+
if (hasReactiveAttr || textLeafBind) {
|
|
182
|
+
/* The element is a located hole (for attr binds or text-leaf text). Take its
|
|
183
|
+
index BEFORE recursing, so holes number in pre-order — the order the runtime's
|
|
184
|
+
path walk produces them. */
|
|
185
|
+
elVar = nextVar('el')
|
|
186
|
+
binds.push(`const ${elVar} = ${skVar}.el[${counter.el++}];\n`)
|
|
187
|
+
openTag += ` ${HOLE_ATTRIBUTE}`
|
|
188
|
+
for (const attr of node.attrs) {
|
|
189
|
+
if (attr.kind !== 'static') {
|
|
190
|
+
binds.push(dynamicAttr(node, attr, elVar))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
52
194
|
for (const scope of node.scopes ?? []) {
|
|
53
|
-
|
|
195
|
+
openTag += scopeAttr(scope)
|
|
54
196
|
}
|
|
55
197
|
for (const attr of node.attrs) {
|
|
56
198
|
if (attr.kind === 'static') {
|
|
57
|
-
|
|
58
|
-
} else if (attr.kind === 'expression') {
|
|
59
|
-
code += `attr(${varName}, ${JSON.stringify(attr.name)}, () => (${lowerExpression(attr.code)}));\n`
|
|
60
|
-
} else if (attr.kind === 'event') {
|
|
61
|
-
code += `on(${varName}, ${JSON.stringify(attr.event)}, (${lowerExpression(attr.code)}));\n`
|
|
62
|
-
} else if (attr.kind === 'attach') {
|
|
63
|
-
code += `attach(${varName}, (${lowerExpression(attr.code)}));\n`
|
|
64
|
-
} else if (attr.kind === 'bind' && attr.property === 'group') {
|
|
65
|
-
/* Grouped two-way: radio binds the path to the single checked
|
|
66
|
-
`value`; checkbox treats the path as an array, adding/removing
|
|
67
|
-
`value` on toggle. Membership reads the array via the lowered
|
|
68
|
-
path and calls native `.includes`/`.indexOf` (the doc API has no
|
|
69
|
-
array search); mutations go through `push`/`delete`, which lower
|
|
70
|
-
to `add`/`remove` patches that the doc reindexes. */
|
|
71
|
-
const { valueCode, isRadio } = groupBindParts(node)
|
|
72
|
-
const value = lowerExpression(valueCode)
|
|
73
|
-
if (isRadio) {
|
|
74
|
-
code += `effect(() => { ${varName}.checked = (${lowerExpression(attr.code)}) === (${value}); });\n`
|
|
75
|
-
code += `on(${varName}, "change", () => { if (${varName}.checked) { ${lowerStatement(`${attr.code} = ${valueCode}`)} } });\n`
|
|
76
|
-
} else {
|
|
77
|
-
code += `effect(() => { ${varName}.checked = (${lowerExpression(attr.code)}).includes(${value}); });\n`
|
|
78
|
-
code += `on(${varName}, "change", () => { const $groupValue = ${value}; if (${varName}.checked) { if (!(${lowerExpression(attr.code)}).includes($groupValue)) { ${lowerStatement(`${attr.code}.push($groupValue)`)} } } else { const $groupIndex = (${lowerExpression(attr.code)}).indexOf($groupValue); if ($groupIndex !== -1) { ${lowerStatement(`delete ${attr.code}[$groupIndex]`)} } } });\n`
|
|
79
|
-
}
|
|
80
|
-
} else {
|
|
81
|
-
/* Two-way: drive the property from the path, and write the path
|
|
82
|
-
back on the property's native event (`input` for most fields,
|
|
83
|
-
but `toggle` for `<details open>`, `change` for checked/select).
|
|
84
|
-
The path is an lvalue, so the write lowers to an assignment. */
|
|
85
|
-
const event = bindListenEvent(attr.property, node.tag)
|
|
86
|
-
code += `effect(() => { ${varName}.${attr.property} = ${lowerExpression(attr.code)}; });\n`
|
|
87
|
-
code += `on(${varName}, ${JSON.stringify(event)}, () => { ${lowerStatement(`${attr.code} = ${varName}.${attr.property}`)} });\n`
|
|
199
|
+
openTag += staticAttr(attr.name, attr.value)
|
|
88
200
|
}
|
|
89
201
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
202
|
+
openTag += '>'
|
|
203
|
+
if (VOID_TAGS.has(node.tag)) {
|
|
204
|
+
return openTag
|
|
205
|
+
}
|
|
206
|
+
if (textLeafBind) {
|
|
207
|
+
/* Clone the element empty, build its text on the located node with the
|
|
208
|
+
imperative path — handles static/reactive/snippet/raw-html text. */
|
|
209
|
+
binds.push(generateChildren(node.children, elVar))
|
|
210
|
+
return `${openTag}</${node.tag}>`
|
|
211
|
+
}
|
|
212
|
+
/* A nested `<script>` among the children scopes its bindings to this subtree (its
|
|
213
|
+
later siblings auto-deref them); pop after. */
|
|
214
|
+
const inner = withNestedScripts(node.children, () =>
|
|
215
|
+
node.children.map((child) => skeletonMarkup(child, skVar, counter, binds)).join(''),
|
|
216
|
+
)
|
|
217
|
+
return `${openTag}${inner}</${node.tag}>`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Emits a skeletonable subtree via the skeleton path: a marker-stamped static
|
|
221
|
+
skeleton string (parsed once, cloned per mount) plus each hole's wiring against
|
|
222
|
+
its located node. */
|
|
223
|
+
function generateSkeleton(
|
|
224
|
+
node: Extract<TemplateNode, { kind: 'element' | 'component' }>,
|
|
225
|
+
parentVar: string,
|
|
226
|
+
): string {
|
|
227
|
+
const skVar = nextVar('sk')
|
|
228
|
+
const binds: string[] = []
|
|
229
|
+
const html = skeletonMarkup(node, skVar, { el: 0, an: 0 }, binds)
|
|
230
|
+
return `const ${skVar} = skeleton(${parentVar}, ${JSON.stringify(html)});\n${binds.join('')}`
|
|
94
231
|
}
|
|
95
232
|
|
|
96
233
|
/* Emits code appending `node` to `parentVar`. */
|
|
97
|
-
function generateChild(node: TemplateNode, parentVar: string): string {
|
|
234
|
+
function generateChild(node: TemplateNode, parentVar: string, before = 'null'): string {
|
|
98
235
|
if (node.kind === 'script') {
|
|
99
236
|
return `${lowerStatement(node.code)}\n`
|
|
100
237
|
}
|
|
101
238
|
/* A `<style>` emits no DOM — its CSS is bundled and its scope attribute is
|
|
102
|
-
already stamped onto the elements it covers (see `
|
|
239
|
+
already stamped onto the elements it covers (see `staticHtml`/`skeletonMarkup`). */
|
|
103
240
|
if (node.kind === 'style') {
|
|
104
241
|
return ''
|
|
105
242
|
}
|
|
@@ -122,36 +259,43 @@ export function generateBuild(
|
|
|
122
259
|
.join('')
|
|
123
260
|
}
|
|
124
261
|
if (node.kind === 'element' && node.tag === 'slot') {
|
|
125
|
-
/* In a layout,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
262
|
+
/* In a layout, `<slot/>` is the router's page outlet: a bare empty `OUTLET_TAG`
|
|
263
|
+
element the router mounts the next chain layer into, cloned (create) / claimed
|
|
264
|
+
(hydrate) so it matches the SSR placeholder. (Top-level/nested-in-element layout
|
|
265
|
+
slots are rewritten to `OUTLET_TAG` up front by `asOutlet`; this covers a slot
|
|
266
|
+
reached inside a control-flow branch.) */
|
|
267
|
+
if (isLayout) {
|
|
268
|
+
return `cloneStatic(${parentVar}, ${JSON.stringify(`<${OUTLET_TAG}></${OUTLET_TAG}>`)});\n`
|
|
130
269
|
}
|
|
131
270
|
return generateSlot(node, parentVar)
|
|
132
271
|
}
|
|
133
272
|
if (node.kind === 'element') {
|
|
134
|
-
/*
|
|
135
|
-
|
|
136
|
-
|
|
273
|
+
/* Every bound element builds through the parser-backed skeleton (one clone +
|
|
274
|
+
located holes / anchors, correct foreign namespaces). A fully-static element
|
|
275
|
+
never reaches here — `generateChildren` coalesces it into a `cloneStatic` run —
|
|
276
|
+
so a non-slot element here always carries a hole and is skeletonable. */
|
|
277
|
+
return generateSkeleton(node, parentVar)
|
|
137
278
|
}
|
|
138
279
|
if (node.kind === 'if') {
|
|
139
|
-
return generateIf(node, parentVar)
|
|
280
|
+
return generateIf(node, parentVar, before)
|
|
140
281
|
}
|
|
141
282
|
if (node.kind === 'await') {
|
|
142
|
-
return generateAwait(node, parentVar)
|
|
283
|
+
return generateAwait(node, parentVar, before)
|
|
143
284
|
}
|
|
144
285
|
if (node.kind === 'try') {
|
|
145
|
-
return generateTry(node, parentVar)
|
|
286
|
+
return generateTry(node, parentVar, before)
|
|
146
287
|
}
|
|
147
288
|
if (node.kind === 'branch') {
|
|
148
289
|
return '' // branches are consumed by their await block, never standalone
|
|
149
290
|
}
|
|
150
291
|
if (node.kind === 'component') {
|
|
151
|
-
|
|
292
|
+
/* A standalone component builds through the skeleton too — its wrapper element
|
|
293
|
+
is a located hole, the child mounts into it (same as a component nested in a
|
|
294
|
+
skeletonable element). */
|
|
295
|
+
return generateSkeleton(node, parentVar)
|
|
152
296
|
}
|
|
153
297
|
if (node.kind === 'switch') {
|
|
154
|
-
return generateSwitch(node, parentVar)
|
|
298
|
+
return generateSwitch(node, parentVar, before)
|
|
155
299
|
}
|
|
156
300
|
if (node.kind === 'case') {
|
|
157
301
|
return '' // cases are consumed by their switch/if, never standalone
|
|
@@ -159,7 +303,7 @@ export function generateBuild(
|
|
|
159
303
|
if (node.kind === 'snippet') {
|
|
160
304
|
return generateSnippet(node)
|
|
161
305
|
}
|
|
162
|
-
return generateEach(node, parentVar)
|
|
306
|
+
return generateEach(node, parentVar, before)
|
|
163
307
|
}
|
|
164
308
|
|
|
165
309
|
/* Builds a sibling list, coalescing maximal runs of fully-static element subtrees
|
|
@@ -202,6 +346,7 @@ export function generateBuild(
|
|
|
202
346
|
function generateSwitch(
|
|
203
347
|
node: Extract<TemplateNode, { kind: 'switch' }>,
|
|
204
348
|
parentVar: string,
|
|
349
|
+
before: string,
|
|
205
350
|
): string {
|
|
206
351
|
const cases = node.children
|
|
207
352
|
.filter(
|
|
@@ -215,60 +360,41 @@ export function generateBuild(
|
|
|
215
360
|
return `{ match: ${match}, render: ${branchThunk(branch.children)} }`
|
|
216
361
|
})
|
|
217
362
|
.join(', ')
|
|
218
|
-
return `switchBlock(${parentVar}, () => (${lowerExpression(node.subject)}), [${cases}]);\n`
|
|
363
|
+
return `switchBlock(${parentVar}, () => (${lowerExpression(node.subject)}), [${cases}], ${before});\n`
|
|
219
364
|
}
|
|
220
365
|
|
|
221
|
-
/* A `<slot>` outlet: render the parent-provided content
|
|
222
|
-
|
|
223
|
-
children when the parent supplied none. */
|
|
366
|
+
/* A `<slot>` outlet: render the parent-provided content (`$children`), falling
|
|
367
|
+
back to the slot's own children when the parent supplied none. */
|
|
224
368
|
function generateSlot(
|
|
225
369
|
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
226
370
|
parentVar: string,
|
|
227
371
|
): string {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
name === undefined
|
|
231
|
-
? '$props && $props.$children'
|
|
232
|
-
: `$props && $props.$slots && $props.$slots[${JSON.stringify(name)}]`
|
|
233
|
-
const invoke =
|
|
234
|
-
name === undefined
|
|
235
|
-
? `$props.$children(${parentVar})`
|
|
236
|
-
: `$props.$slots[${JSON.stringify(name)}](${parentVar})`
|
|
237
|
-
const fallback = node.children.map((child) => generateChild(child, parentVar)).join('')
|
|
372
|
+
const fallback = generateChildren(node.children, parentVar)
|
|
373
|
+
const invoke = `$props.$children(${parentVar})`
|
|
238
374
|
if (fallback.trim() === '') {
|
|
239
|
-
return `if ($
|
|
375
|
+
return `if ($props && $props.$children) { ${invoke}; }\n`
|
|
240
376
|
}
|
|
241
|
-
return `if ($
|
|
377
|
+
return `if ($props && $props.$children) { ${invoke}; } else {\n${fallback}}\n`
|
|
242
378
|
}
|
|
243
379
|
|
|
244
380
|
/* Mounts a child component into a wrapper element, passing each prop as a
|
|
245
381
|
reactive thunk so the child re-reads when the parent expression changes. */
|
|
246
382
|
/* The prop + slot thunks a child mount receives — its props as value thunks and
|
|
247
|
-
its slot content as host-taking
|
|
383
|
+
its slot content as a host-taking builder (`$children`). */
|
|
248
384
|
function componentParts(node: Extract<TemplateNode, { kind: 'component' }>): string[] {
|
|
249
385
|
const parts = node.props.map(
|
|
250
386
|
(prop) => `${JSON.stringify(prop.name)}: () => (${lowerExpression(prop.code)})`,
|
|
251
387
|
)
|
|
252
|
-
const
|
|
253
|
-
const slotCode = groups.default.map((child) => generateChild(child, '$slot')).join('')
|
|
388
|
+
const slotCode = generateChildren(node.children, '$slot')
|
|
254
389
|
if (slotCode.trim() !== '') {
|
|
255
390
|
parts.push(`"$children": ($slot) => {\n${slotCode}}`)
|
|
256
391
|
}
|
|
257
|
-
if (groups.named.length > 0) {
|
|
258
|
-
const entries = groups.named
|
|
259
|
-
.map((group) => {
|
|
260
|
-
const code = group.nodes.map((child) => generateChild(child, '$slot')).join('')
|
|
261
|
-
return `${JSON.stringify(group.name)}: ($slot) => {\n${code}}`
|
|
262
|
-
})
|
|
263
|
-
.join(', ')
|
|
264
|
-
parts.push(`"$slots": { ${entries} }`)
|
|
265
|
-
}
|
|
266
392
|
return parts
|
|
267
393
|
}
|
|
268
394
|
|
|
269
|
-
/* Mounts a child into a wrapper obtained via `varExpr` (
|
|
270
|
-
|
|
271
|
-
|
|
395
|
+
/* Mounts a child into a wrapper obtained via `varExpr` (a skeleton-located node).
|
|
396
|
+
Hydration stays active, so the child adopts its server markup inside the wrapper.
|
|
397
|
+
Returns the wrapper var. */
|
|
272
398
|
function mountComponent(
|
|
273
399
|
node: Extract<TemplateNode, { kind: 'component' }>,
|
|
274
400
|
varExpr: string,
|
|
@@ -280,25 +406,12 @@ export function generateBuild(
|
|
|
280
406
|
return { code, varName: wrapper }
|
|
281
407
|
}
|
|
282
408
|
|
|
283
|
-
function generateComponent(
|
|
284
|
-
node: Extract<TemplateNode, { kind: 'component' }>,
|
|
285
|
-
parentVar: string,
|
|
286
|
-
): string {
|
|
287
|
-
const { tag, transparent } = componentWrapperTag(node.name)
|
|
288
|
-
const { code, varName } = mountComponent(
|
|
289
|
-
node,
|
|
290
|
-
`openChild(${parentVar}, ${JSON.stringify(tag)})`,
|
|
291
|
-
)
|
|
292
|
-
/* A void-name remap is layout-transparent so the child's root stays a direct
|
|
293
|
-
child of the parent (idempotent on a claimed SSR node that already has it). */
|
|
294
|
-
return transparent ? `${code}${varName}.setAttribute("style", "display:contents");\n` : code
|
|
295
|
-
}
|
|
296
|
-
|
|
297
409
|
/* An await block: pending → resolved(value) / error branches. Each branch is a
|
|
298
410
|
single-element root; a render thunk returns its node. */
|
|
299
411
|
function generateAwait(
|
|
300
412
|
node: Extract<TemplateNode, { kind: 'await' }>,
|
|
301
413
|
parentVar: string,
|
|
414
|
+
before: string,
|
|
302
415
|
): string {
|
|
303
416
|
const isBranch = (which: 'then' | 'catch' | 'finally') => (child: TemplateNode) =>
|
|
304
417
|
child.kind === 'branch' && child.branch === which
|
|
@@ -337,7 +450,7 @@ export function generateBuild(
|
|
|
337
450
|
`awaitBlock(${parentVar}, nextBlockId(), () => (${lowerExpression(node.promise)}), ` +
|
|
338
451
|
`${pendingThunk}, ` +
|
|
339
452
|
`${thenThunk}, ` +
|
|
340
|
-
`${catchThunk});\n`
|
|
453
|
+
`${catchThunk}, ${before});\n`
|
|
341
454
|
)
|
|
342
455
|
}
|
|
343
456
|
|
|
@@ -407,7 +520,11 @@ export function generateBuild(
|
|
|
407
520
|
/* A sync error boundary: build the guarded subtree (++ finally); a throw while
|
|
408
521
|
building swaps to the catch branch (++ finally). No catch → `undefined`, which
|
|
409
522
|
makes the runtime re-throw to the nearest enclosing boundary. */
|
|
410
|
-
function generateTry(
|
|
523
|
+
function generateTry(
|
|
524
|
+
node: Extract<TemplateNode, { kind: 'try' }>,
|
|
525
|
+
parentVar: string,
|
|
526
|
+
before: string,
|
|
527
|
+
): string {
|
|
411
528
|
const catchBranch = findBranch(node.children, 'catch')
|
|
412
529
|
const finallyChildren = branchChildren(findBranch(node.children, 'finally'))
|
|
413
530
|
const guarded = node.children.filter((child) => child.kind !== 'branch')
|
|
@@ -420,22 +537,23 @@ export function generateBuild(
|
|
|
420
537
|
branchVar(catchBranch) ?? '_error',
|
|
421
538
|
finallyChildren,
|
|
422
539
|
)
|
|
423
|
-
return `tryBlock(${parentVar}, nextBlockId(), ${tryThunk}, ${catchThunk});\n`
|
|
540
|
+
return `tryBlock(${parentVar}, nextBlockId(), ${tryThunk}, ${catchThunk}, ${before});\n`
|
|
424
541
|
}
|
|
425
542
|
|
|
426
543
|
/* A conditional with an optional nested `<template else>` (a `case` child). Each
|
|
427
544
|
branch is a content range the runtime tracks between markers. */
|
|
428
|
-
function generateIf(
|
|
545
|
+
function generateIf(
|
|
546
|
+
node: Extract<TemplateNode, { kind: 'if' }>,
|
|
547
|
+
parentVar: string,
|
|
548
|
+
before: string,
|
|
549
|
+
): string {
|
|
429
550
|
const elseBranch = node.children.find(
|
|
430
551
|
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
431
552
|
)
|
|
432
553
|
const thenChildren = node.children.filter((child) => child.kind !== 'case')
|
|
433
554
|
const thenThunk = branchThunk(thenChildren)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
const elseThunk = branchThunk(elseBranch.children)
|
|
438
|
-
return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk});\n`
|
|
555
|
+
const elseThunk = elseBranch === undefined ? 'undefined' : branchThunk(elseBranch.children)
|
|
556
|
+
return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
|
|
439
557
|
}
|
|
440
558
|
|
|
441
559
|
/* A keyed each. Each row is a content RANGE (any content, tracked between the
|
|
@@ -443,6 +561,7 @@ export function generateBuild(
|
|
|
443
561
|
function generateEach(
|
|
444
562
|
node: Extract<TemplateNode, { kind: 'each' }>,
|
|
445
563
|
parentVar: string,
|
|
564
|
+
before: string,
|
|
446
565
|
): string {
|
|
447
566
|
const rowParam = nextVar('p')
|
|
448
567
|
/* The row body builds its children (a `<script>` declares per-row local signals,
|
|
@@ -464,11 +583,36 @@ export function generateBuild(
|
|
|
464
583
|
: ''
|
|
465
584
|
return (
|
|
466
585
|
`${fn}(${parentVar}, () => (${lowerExpression(node.items)}), ` +
|
|
467
|
-
`(${node.as}) => (${keyExpression}), (${rowParam}, ${node.as}) => {\n${rowBody}}${catchArg});\n`
|
|
586
|
+
`(${node.as}) => (${keyExpression}), (${rowParam}, ${node.as}) => {\n${rowBody}}${catchArg}, ${before});\n`
|
|
468
587
|
)
|
|
469
588
|
}
|
|
470
589
|
|
|
471
|
-
|
|
590
|
+
/* In a layout the `<slot/>` page outlet is a bare empty `OUTLET_TAG` element (the
|
|
591
|
+
router fills it later) — exactly the SSR placeholder. Rewriting it to an element
|
|
592
|
+
node up front lets the static-clone path carry it as ordinary structure. */
|
|
593
|
+
function asOutlet(node: TemplateNode): TemplateNode {
|
|
594
|
+
if (node.kind !== 'element') {
|
|
595
|
+
return node
|
|
596
|
+
}
|
|
597
|
+
if (node.tag === 'slot') {
|
|
598
|
+
return { ...node, tag: OUTLET_TAG, attrs: [], children: [] }
|
|
599
|
+
}
|
|
600
|
+
return { ...node, children: node.children.map(asOutlet) }
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return generateChildren(isLayout ? nodes.map(asOutlet) : nodes, hostVar)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* A control-flow block — `if`/`each`/`await`/`switch`/`try`. In a skeleton each mounts at
|
|
607
|
+
an `<!--a-->` anchor cloned into its located parent at the block's position. */
|
|
608
|
+
function isControlFlowNode(node: TemplateNode): boolean {
|
|
609
|
+
return (
|
|
610
|
+
node.kind === 'if' ||
|
|
611
|
+
node.kind === 'each' ||
|
|
612
|
+
node.kind === 'await' ||
|
|
613
|
+
node.kind === 'switch' ||
|
|
614
|
+
node.kind === 'try'
|
|
615
|
+
)
|
|
472
616
|
}
|
|
473
617
|
|
|
474
618
|
/* A text node that is purely whitespace (no interpolation, only blank static
|