@abide/abide 0.38.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 +8 -0
- package/package.json +2 -1
- package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +4 -3
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/asOutlet.ts +6 -5
- package/src/lib/ui/compile/compileModule.ts +13 -9
- package/src/lib/ui/compile/generateBuild.ts +55 -44
- package/src/lib/ui/compile/generateSSR.ts +30 -21
- 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/skeletonContext.ts +19 -20
- package/src/lib/ui/compile/skeletonable.ts +3 -2
- package/src/lib/ui/dom/commentData.ts +14 -0
- package/src/lib/ui/dom/disposeRange.ts +21 -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/outlet.ts +62 -0
- package/src/lib/ui/dom/scopeLabel.ts +8 -6
- package/src/lib/ui/dom/skeleton.ts +11 -30
- package/src/lib/ui/dom/withScope.ts +33 -0
- package/src/lib/ui/installHotBridge.ts +2 -0
- 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/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/compile/componentWrapperTag.ts +0 -15
- package/src/lib/ui/runtime/firstOutlet.ts +0 -22
package/AGENTS.md
CHANGED
|
@@ -162,7 +162,7 @@ Test suites compile `.abide` and rewrite verbs under `bun test` via `preload = [
|
|
|
162
162
|
- `abide/ui/enterScope`, `abide/ui/exitScope` — push/pop the lexical scope around an SSR render.
|
|
163
163
|
- `abide/ui/router`, `abide/ui/startClient`, `abide/ui/renderToStream` — the client router, the official client entry (reads `window.__SSR__`, seeds cache, starts the router), and the out-of-order SSR streamer.
|
|
164
164
|
- `abide/ui/remoteProxy`, `abide/ui/socketProxy` — the bundler-emitted client substitutes that swap a server verb/socket import for a `fetch` proxy / ws-multiplexed `Socket`.
|
|
165
|
-
- `abide/ui/dom/*` — the compiled-template DOM runtime (one node-builder per construct): `mount`, `mountChild`, `mountSlot`, `hydrate`, `skeleton`, `cloneStatic`, `anchorCursor`, `text`, `appendText`, `appendTextAt`, `appendSnippet`, `appendStatic`, `attr`, `on`, `attach`, `each`, `eachAsync`, `when`, `awaitBlock`, `tryBlock`, `switchBlock`, `applyResolved`.
|
|
165
|
+
- `abide/ui/dom/*` — the compiled-template DOM runtime (one node-builder per construct): `mount`, `mountChild`, `mountSlot`, `outlet`, `hydrate`, `skeleton`, `cloneStatic`, `anchorCursor`, `text`, `appendText`, `appendTextAt`, `appendSnippet`, `appendStatic`, `attr`, `on`, `attach`, `each`, `eachAsync`, `when`, `awaitBlock`, `tryBlock`, `switchBlock`, `applyResolved`. `outlet` is the marker-range a layout `<slot/>` compiles to (no wrapper element — the router fills it with the next chain layer as a direct child).
|
|
166
166
|
- `abide/ui/runtime/*` — render-pass helpers: `escapeKey` (JSON-Pointer key escaping), `nextBlockId`, `enterRenderPass`, `exitRenderPass`.
|
|
167
167
|
|
|
168
168
|
## Build / tooling — @documentation building
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.38.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`67ac25f`](https://github.com/briancray/abide/commit/67ac25faf78b029d0b14659df1e4ec1a05ae7a10) - document outlet marker-range in AGENTS.md surface map ([`ab23673`](https://github.com/briancray/abide/commit/ab236733ee1f81e19ce09ee35fe44624e955a6e4))
|
|
8
|
+
|
|
9
|
+
- [`67ac25f`](https://github.com/briancray/abide/commit/67ac25faf78b029d0b14659df1e4ec1a05ae7a10) - unify pages/layouts/outlets on marker-range model ([`df1c95f`](https://github.com/briancray/abide/commit/df1c95f89ff8d6d6806606dbece531c6be7aabdf))
|
|
10
|
+
|
|
3
11
|
## 0.38.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.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",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"./ui/dom/skeleton": "./src/lib/ui/dom/skeleton.ts",
|
|
90
90
|
"./ui/dom/anchorCursor": "./src/lib/ui/dom/anchorCursor.ts",
|
|
91
91
|
"./ui/dom/mountSlot": "./src/lib/ui/dom/mountSlot.ts",
|
|
92
|
+
"./ui/dom/outlet": "./src/lib/ui/dom/outlet.ts",
|
|
92
93
|
"./ui/dom/attr": "./src/lib/ui/dom/attr.ts",
|
|
93
94
|
"./ui/dom/on": "./src/lib/ui/dom/on.ts",
|
|
94
95
|
"./ui/dom/attach": "./src/lib/ui/dom/attach.ts",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/* The tag prefix
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/* The `abide-` tag prefix on framework-owned custom elements (the router's
|
|
2
|
+
`abide-outlet`, the streaming `abide-resolve`/`abide-cache` fragments). `scopeLabel`
|
|
3
|
+
strips it to read the outlet host's bare name. Child components no longer mount into
|
|
4
|
+
an `abide-<name>` wrapper — they build as marker ranges (see `mountRange`). */
|
|
4
5
|
export const COMPONENT_WRAPPER_PREFIX = 'abide-'
|
|
@@ -30,6 +30,7 @@ export const UI_RUNTIME_IMPORTS: { name: string; specifier: string }[] = [
|
|
|
30
30
|
{ name: 'tryBlock', specifier: 'ui/dom/tryBlock' },
|
|
31
31
|
{ name: 'switchBlock', specifier: 'ui/dom/switchBlock' },
|
|
32
32
|
{ name: 'mountSlot', specifier: 'ui/dom/mountSlot' },
|
|
33
|
+
{ name: 'outlet', specifier: 'ui/dom/outlet' },
|
|
33
34
|
{ name: 'mountChild', specifier: 'ui/dom/mountChild' },
|
|
34
35
|
{ name: 'hydrate', specifier: 'ui/dom/hydrate' },
|
|
35
36
|
{ name: 'escapeKey', specifier: 'ui/runtime/escapeKey' },
|
|
@@ -8,11 +8,12 @@ structure AND lets BOTH back-ends feed the same tree to `skeletonContext` — on
|
|
|
8
8
|
for "a layout slot is an outlet, not an anchor", instead of the client running `asOutlet` while
|
|
9
9
|
SSR mirrors it with an inline special-case.
|
|
10
10
|
|
|
11
|
-
The outlet is a structural mount
|
|
12
|
-
|
|
13
|
-
into (
|
|
14
|
-
the two back-ends agree:
|
|
15
|
-
the slot's annotated `scopes` and
|
|
11
|
+
The outlet is a structural mount point, not styled content, so it carries NO attrs and NO style
|
|
12
|
+
scope — both back-ends lower it to the bare `<!--abide:outlet-->`…`<!--/abide:outlet-->` boundary
|
|
13
|
+
`renderChain` folds the child layer into (matching its `OUTLET_PLACEHOLDER`). Stripping `scopes`
|
|
14
|
+
is what makes the two back-ends agree: SSR emits the boundary bare, but without this the client
|
|
15
|
+
clone would read the slot's annotated `scopes` and stamp them — a hydration mismatch for any
|
|
16
|
+
scoped layout.
|
|
16
17
|
|
|
17
18
|
Control-flow children are fresh build contexts (their own runtime mounts a nested slot), so they
|
|
18
19
|
are not descended into — a `<slot>` inside an `{#if}` stays a slot node, handled at its own
|
|
@@ -42,11 +42,13 @@ export function compileModule(
|
|
|
42
42
|
const id = JSON.stringify(options.moduleId)
|
|
43
43
|
return `const { ${names}, hotReplace } = window.__abide
|
|
44
44
|
${userImports}
|
|
45
|
-
function
|
|
46
|
-
return mount(host, (host) => {
|
|
45
|
+
function build(host, $props) {
|
|
47
46
|
${body}
|
|
48
|
-
})
|
|
49
47
|
}
|
|
48
|
+
function component(host, $props) {
|
|
49
|
+
return mount(host, build, $props)
|
|
50
|
+
}
|
|
51
|
+
component.build = build
|
|
50
52
|
component.__abideId = ${id}
|
|
51
53
|
if (!hotReplace(${id}, component)) location.reload()
|
|
52
54
|
`
|
|
@@ -61,17 +63,17 @@ if (!hotReplace(${id}, component)) location.reload()
|
|
|
61
63
|
exactly which names it emitted, so it filters here. A name absent from the body
|
|
62
64
|
is unreferenced; erring toward inclusion (a stray match in user script) only
|
|
63
65
|
keeps a harmless unused import, never drops a needed one. */
|
|
64
|
-
const moduleBody = `
|
|
65
|
-
return mount(host, (host) => {
|
|
66
|
+
const moduleBody = `function build(host, $props) {
|
|
66
67
|
${body}
|
|
67
|
-
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default function component(host, $props) {
|
|
71
|
+
return mount(host, build, $props)
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
/* Adopt the server-rendered DOM in place instead of rebuilding it. */
|
|
71
75
|
export function hydrateInto(host, $props) {
|
|
72
|
-
return hydrate(host,
|
|
73
|
-
${body}
|
|
74
|
-
})
|
|
76
|
+
return hydrate(host, build, $props)
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
export function render($props) {
|
|
@@ -80,6 +82,8 @@ ${ssrBody}
|
|
|
80
82
|
|
|
81
83
|
component.render = render
|
|
82
84
|
component.hydrate = hydrateInto
|
|
85
|
+
/* The bare build, so a parent can range-mount this as a nested child (no wrapper). */
|
|
86
|
+
component.build = build
|
|
83
87
|
component.hydratable = ${analyzed.hydratable}
|
|
84
88
|
${options.moduleId === undefined ? '' : `component.__abideId = ${JSON.stringify(options.moduleId)}\n`}`
|
|
85
89
|
/* Scope each name to the surface that genuinely references it. The SSR body
|
|
@@ -2,7 +2,6 @@ import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
|
2
2
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
3
3
|
import { asOutlet } from './asOutlet.ts'
|
|
4
4
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
5
|
-
import { componentWrapperTag } from './componentWrapperTag.ts'
|
|
6
5
|
import { groupBindParts } from './groupBindParts.ts'
|
|
7
6
|
import { isControlFlow } from './isControlFlow.ts'
|
|
8
7
|
import { lowerContext } from './lowerContext.ts'
|
|
@@ -125,6 +124,16 @@ export function generateBuild(
|
|
|
125
124
|
block or slot drops an `<!--a-->` anchor at its position and mounts there (see
|
|
126
125
|
`anchorCursor`), so it can sit ANYWHERE among static siblings. Static descendants are
|
|
127
126
|
plain markup. */
|
|
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
|
+
|
|
128
137
|
function skeletonMarkup(node: TemplateNode, skVar: string, binds: string[]): string {
|
|
129
138
|
if (node.kind === 'text') {
|
|
130
139
|
/* Reactive text reached here is INTERLEAVED with element siblings (a text-leaf
|
|
@@ -143,25 +152,17 @@ export function generateBuild(
|
|
|
143
152
|
})
|
|
144
153
|
.join('')
|
|
145
154
|
}
|
|
146
|
-
if (isControlFlow(node)) {
|
|
147
|
-
/* A control-flow block at its position: an `<!--a-->` anchor
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
element the anchor was cloned into
|
|
151
|
-
|
|
152
|
-
|
|
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)
|
|
153
163
|
binds.push(generateChild(node, `${anchorVar}.parentNode`, `anchorCursor(${anchorVar})`))
|
|
154
164
|
return '<!--a-->'
|
|
155
165
|
}
|
|
156
|
-
if (node.kind === 'component') {
|
|
157
|
-
/* The wrapper element is a positioned hole in the skeleton; the child mounts
|
|
158
|
-
into the located node. display:contents (idempotent, static so it lives in
|
|
159
|
-
the markup) keeps the wrapper out of layout — a pure mount host. */
|
|
160
|
-
const tag = componentWrapperTag(node.name)
|
|
161
|
-
const { code } = mountComponent(node, `${skVar}.el[${holeIndex(elIndex, node)}]`)
|
|
162
|
-
binds.push(code)
|
|
163
|
-
return `<${tag} ${HOLE_ATTRIBUTE} style="display:contents"></${tag}>`
|
|
164
|
-
}
|
|
165
166
|
if (node.kind === 'script') {
|
|
166
167
|
/* A nested `<script>` (scoped reactive block) emits no markup — its lowered body
|
|
167
168
|
runs as a bind, in document order, so its signals are declared before the later
|
|
@@ -179,11 +180,18 @@ export function generateBuild(
|
|
|
179
180
|
if (node.kind !== 'element') {
|
|
180
181
|
return '' // <style> emits no markup
|
|
181
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
|
+
}
|
|
182
191
|
if (node.tag === 'slot') {
|
|
183
192
|
/* A `<slot>` outlet at its position: an `<!--a-->` anchor, the slot's content
|
|
184
193
|
mounted as a marker-bounded range (`mountSlot`) so it positions like a block. */
|
|
185
|
-
const anchorVar =
|
|
186
|
-
binds.push(`const ${anchorVar} = ${skVar}.an[${holeIndex(anIndex, node)}];\n`)
|
|
194
|
+
const anchorVar = anchorVarAt(node, skVar, binds)
|
|
187
195
|
const hostVar = nextVar('host')
|
|
188
196
|
binds.push(
|
|
189
197
|
`mountSlot(${anchorVar}.parentNode, (${hostVar}) => {\n${generateSlot(node, hostVar)}}, anchorCursor(${anchorVar}));\n`,
|
|
@@ -246,7 +254,7 @@ export function generateBuild(
|
|
|
246
254
|
skeleton string (parsed once, cloned per mount) plus each hole's wiring against
|
|
247
255
|
its located node. */
|
|
248
256
|
function generateSkeleton(
|
|
249
|
-
node: Extract<TemplateNode, { kind: 'element'
|
|
257
|
+
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
250
258
|
parentVar: string,
|
|
251
259
|
): string {
|
|
252
260
|
const skVar = nextVar('sk')
|
|
@@ -283,14 +291,19 @@ export function generateBuild(
|
|
|
283
291
|
})
|
|
284
292
|
.join('')
|
|
285
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
|
+
}
|
|
286
300
|
if (node.kind === 'element' && node.tag === 'slot') {
|
|
287
|
-
/* In a layout, `<slot/>` is the router's page outlet
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
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. */
|
|
292
305
|
if (isLayout) {
|
|
293
|
-
return `
|
|
306
|
+
return `outlet(${parentVar}, ${before});\n`
|
|
294
307
|
}
|
|
295
308
|
return generateSlot(node, parentVar)
|
|
296
309
|
}
|
|
@@ -314,10 +327,10 @@ export function generateBuild(
|
|
|
314
327
|
return '' // branches are consumed by their await block, never standalone
|
|
315
328
|
}
|
|
316
329
|
if (node.kind === 'component') {
|
|
317
|
-
/* A standalone component
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
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)
|
|
321
334
|
}
|
|
322
335
|
if (node.kind === 'switch') {
|
|
323
336
|
return generateSwitch(node, parentVar, before)
|
|
@@ -402,8 +415,6 @@ export function generateBuild(
|
|
|
402
415
|
return `if ($props && $props.$children) { ${invoke}; } else {\n${fallback}}\n`
|
|
403
416
|
}
|
|
404
417
|
|
|
405
|
-
/* Mounts a child component into a wrapper element, passing each prop as a
|
|
406
|
-
reactive thunk so the child re-reads when the parent expression changes. */
|
|
407
418
|
/* The prop + slot thunks a child mount receives — its props as value thunks and
|
|
408
419
|
its slot content as a host-taking builder (`$children`). */
|
|
409
420
|
function componentParts(node: Extract<TemplateNode, { kind: 'component' }>): string[] {
|
|
@@ -417,18 +428,18 @@ export function generateBuild(
|
|
|
417
428
|
return parts
|
|
418
429
|
}
|
|
419
430
|
|
|
420
|
-
/* Mounts a child
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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(
|
|
424
438
|
node: Extract<TemplateNode, { kind: 'component' }>,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
`const ${wrapper} = ${varExpr};\n` +
|
|
430
|
-
`mountChild(${wrapper}, ${node.name}, { ${componentParts(node).join(', ')} });\n`
|
|
431
|
-
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`
|
|
432
443
|
}
|
|
433
444
|
|
|
434
445
|
/* An await block: pending → resolved(value) / error branches. Each branch is a
|
|
@@ -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,8 +1,8 @@
|
|
|
1
|
+
import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
|
|
1
2
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
2
3
|
import { asOutlet } from './asOutlet.ts'
|
|
3
|
-
import { componentWrapperTag } from './componentWrapperTag.ts'
|
|
4
4
|
import { groupBindParts } from './groupBindParts.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { isAnchorPositioned } from './isAnchorPositioned.ts'
|
|
6
6
|
import { lowerContext } from './lowerContext.ts'
|
|
7
7
|
import { scopeAttr } from './scopeAttr.ts'
|
|
8
8
|
import { skeletonContext } from './skeletonContext.ts'
|
|
@@ -111,12 +111,12 @@ export function generateSSR(
|
|
|
111
111
|
inSkeleton.get(node) ? push(target, '<!--a-->') : ''
|
|
112
112
|
|
|
113
113
|
function generate(node: TemplateNode, target: string): string {
|
|
114
|
-
/*
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
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) : ''
|
|
120
120
|
if (node.kind === 'text') {
|
|
121
121
|
return node.parts
|
|
122
122
|
.map((part) => {
|
|
@@ -198,11 +198,11 @@ export function generateSSR(
|
|
|
198
198
|
return ''
|
|
199
199
|
}
|
|
200
200
|
if (node.kind === 'component') {
|
|
201
|
-
/* Server-render the child via its `render` and inline the HTML inside
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`$children` the child
|
|
205
|
-
|
|
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>. */
|
|
206
206
|
const parts = node.props.map(
|
|
207
207
|
(prop) => `${JSON.stringify(prop.name)}: () => (${lowerExpression(prop.code)})`,
|
|
208
208
|
)
|
|
@@ -224,22 +224,30 @@ export function generateSSR(
|
|
|
224
224
|
the enclosing render body, including from branch closures.) */
|
|
225
225
|
const result = nextVar('$child')
|
|
226
226
|
return (
|
|
227
|
-
|
|
227
|
+
anchor +
|
|
228
|
+
push(target, RANGE_OPEN) +
|
|
228
229
|
`const ${result} = ${node.name}.render({ ${parts.join(', ')} });\n` +
|
|
229
230
|
`${target}.push(${result}.html);\n` +
|
|
230
231
|
`for (const $a of ${result}.awaits) { $awaits.push($a); }\n` +
|
|
231
|
-
push(target,
|
|
232
|
+
push(target, RANGE_CLOSE)
|
|
232
233
|
)
|
|
233
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
|
+
}
|
|
234
242
|
if (node.kind === 'element' && node.tag === 'slot') {
|
|
235
243
|
/* `asOutlet` already rewrote a layout's top-level/element-nested `<slot/>` to an
|
|
236
|
-
`OUTLET_TAG` element (handled
|
|
237
|
-
|
|
238
|
-
|
|
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. */
|
|
239
247
|
if (isLayout) {
|
|
240
|
-
return push(target,
|
|
248
|
+
return push(target, `<!--${OUTLET_OPEN}--><!--${OUTLET_CLOSE}-->`)
|
|
241
249
|
}
|
|
242
|
-
return generateSlot(node, target)
|
|
250
|
+
return generateSlot(node, target, anchor)
|
|
243
251
|
}
|
|
244
252
|
let code = push(target, `<${node.tag}`)
|
|
245
253
|
/* Every `<style>` active at this element (own siblings + ancestors) — same set
|
|
@@ -292,6 +300,7 @@ export function generateSSR(
|
|
|
292
300
|
function generateSlot(
|
|
293
301
|
node: Extract<TemplateNode, { kind: 'element' }>,
|
|
294
302
|
target: string,
|
|
303
|
+
anchor: string,
|
|
295
304
|
): string {
|
|
296
305
|
const wrap = inSkeleton.get(node)
|
|
297
306
|
const fallback = generateInto(node.children, target)
|
|
@@ -302,7 +311,7 @@ export function generateSSR(
|
|
|
302
311
|
if (!wrap) {
|
|
303
312
|
return body
|
|
304
313
|
}
|
|
305
|
-
return `${
|
|
314
|
+
return `${anchor}${openRange(target)}${body}${closeRange(target)}`
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
/* Boundary markers + a `$awaits` registration carrying the promise and
|
|
@@ -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')
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
2
|
+
import { isAnchorPositioned } from './isAnchorPositioned.ts'
|
|
1
3
|
import { isControlFlow } from './isControlFlow.ts'
|
|
2
4
|
import { isTextLeaf } from './isTextLeaf.ts'
|
|
3
5
|
import { skeletonable } from './skeletonable.ts'
|
|
@@ -13,9 +15,9 @@ that decide `<!--a-->` anchor placement — and assigns each hole its `el`/`an`
|
|
|
13
15
|
document-order walk, so the numbering cannot drift from the decisions: one walk owns both.
|
|
14
16
|
|
|
15
17
|
The index assignment is scoped per skeleton root (a `skeletonable` element not already in a
|
|
16
|
-
skeleton — the unit `generateSkeleton` instantiates with `{ el: 0, an: 0 }
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
skeleton — the unit `generateSkeleton` instantiates with `{ el: 0, an: 0 }`). `el` numbers
|
|
19
|
+
element holes in pre-order; `an` numbers anchor holes (interleaved reactive text PARTS,
|
|
20
|
+
control-flow blocks, child components, `<slot>` outlets) in
|
|
19
21
|
document order — the orders the runtime's `indexElementHoles`/`scanAnchors` re-derive from
|
|
20
22
|
the realized DOM, so the compile-time numbers and the runtime positions line up.
|
|
21
23
|
|
|
@@ -26,8 +28,8 @@ content, a `<slot>`'s fallback, a snippet's body) — never cloned by the enclos
|
|
|
26
28
|
export function skeletonContext(nodes: TemplateNode[]): SkeletonContext {
|
|
27
29
|
const inSkeleton = new WeakMap<TemplateNode, boolean>()
|
|
28
30
|
const markText = new WeakMap<TemplateNode, boolean>()
|
|
29
|
-
/* Element holes keyed by node; anchor holes keyed by node (control-flow/slot)
|
|
30
|
-
reactive text PART object (a text node carries one anchor per reactive part). */
|
|
31
|
+
/* Element holes keyed by node; anchor holes keyed by node (control-flow/component/slot)
|
|
32
|
+
OR by the reactive text PART object (a text node carries one anchor per reactive part). */
|
|
31
33
|
const elIndex = new WeakMap<TemplateNode, number>()
|
|
32
34
|
const anIndex = new WeakMap<object, number>()
|
|
33
35
|
|
|
@@ -45,21 +47,16 @@ export function skeletonContext(nodes: TemplateNode[]): SkeletonContext {
|
|
|
45
47
|
markText.set(node, nodeMarkText)
|
|
46
48
|
|
|
47
49
|
/* Control-flow blocks, components, and snippets are fresh build contexts. The node
|
|
48
|
-
ITSELF is a hole in the enclosing skeleton (an anchor for a block
|
|
49
|
-
|
|
50
|
+
ITSELF is a hole in the enclosing skeleton (an `<!--a-->` anchor for a block OR a
|
|
51
|
+
component — both mount as a marker-bounded range at that anchor); its children
|
|
52
|
+
re-enter the skeleton only via their own roots. */
|
|
50
53
|
if (isControlFlow(node) || node.kind === 'component' || node.kind === 'snippet') {
|
|
51
|
-
/* A
|
|
52
|
-
standalone
|
|
53
|
-
|
|
54
|
+
/* A block or a component takes an anchor only inside an enclosing skeleton (a
|
|
55
|
+
standalone one routes through `generateIf`/`mountChild`, not the skeleton path);
|
|
56
|
+
a snippet declares a builder and is never anchor-positioned (`isAnchorPositioned`). */
|
|
57
|
+
if (counter !== undefined && isAnchorPositioned(node)) {
|
|
54
58
|
anIndex.set(node, counter.an++)
|
|
55
59
|
}
|
|
56
|
-
/* A component is its OWN element hole either way: in the enclosing skeleton's
|
|
57
|
-
counter when nested, else as the root (index 0) of its own standalone skeleton
|
|
58
|
-
(`generateChild` routes a lone component through `generateSkeleton` too). */
|
|
59
|
-
if (node.kind === 'component') {
|
|
60
|
-
const componentCounter = counter ?? { el: 0, an: 0 }
|
|
61
|
-
elIndex.set(node, componentCounter.el++)
|
|
62
|
-
}
|
|
63
60
|
for (const child of childrenOf(node)) {
|
|
64
61
|
visit(child, false, false, undefined)
|
|
65
62
|
}
|
|
@@ -90,9 +87,11 @@ export function skeletonContext(nodes: TemplateNode[]): SkeletonContext {
|
|
|
90
87
|
if (node.kind !== 'element') {
|
|
91
88
|
return // script / style carry no skeleton children and no hole
|
|
92
89
|
}
|
|
93
|
-
if (node.tag === 'slot') {
|
|
94
|
-
/*
|
|
95
|
-
|
|
90
|
+
if (node.tag === 'slot' || node.tag === OUTLET_TAG) {
|
|
91
|
+
/* A component `<slot>` content fill OR a layout's `OUTLET_TAG` router fill point
|
|
92
|
+
(`asOutlet`) is an anchor hole in the enclosing skeleton — both mount a marker
|
|
93
|
+
range at the anchor (`mountSlot` / `outlet`). A `<slot>`'s children are its
|
|
94
|
+
fallback (a fresh context); an outlet has none. */
|
|
96
95
|
if (counter !== undefined) {
|
|
97
96
|
anIndex.set(node, counter.an++)
|
|
98
97
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
1
2
|
import { isControlFlow } from './isControlFlow.ts'
|
|
2
3
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
3
4
|
|
|
@@ -22,7 +23,7 @@ function skeletonStructure(node: TemplateNode): boolean {
|
|
|
22
23
|
if (node.kind !== 'element') {
|
|
23
24
|
return false // standalone branch|case
|
|
24
25
|
}
|
|
25
|
-
if (node.tag === 'slot') {
|
|
26
|
+
if (node.tag === 'slot' || node.tag === OUTLET_TAG) {
|
|
26
27
|
return true
|
|
27
28
|
}
|
|
28
29
|
return node.children.every(skeletonStructure)
|
|
@@ -43,7 +44,7 @@ function hasHole(node: TemplateNode): boolean {
|
|
|
43
44
|
return node.parts.some((part) => part.kind !== 'static')
|
|
44
45
|
}
|
|
45
46
|
if (node.kind === 'element') {
|
|
46
|
-
if (node.tag === 'slot') {
|
|
47
|
+
if (node.tag === 'slot' || node.tag === OUTLET_TAG) {
|
|
47
48
|
return true
|
|
48
49
|
}
|
|
49
50
|
return node.attrs.some((attr) => attr.kind !== 'static') || node.children.some(hasHole)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* A comment node's data, or undefined for elements/text. A comment is a node that is
|
|
2
|
+
neither an element (`hasAttribute`) nor a text node (`splitText`); the mini-dom
|
|
3
|
+
exposes no `nodeType`, so detect by method. Shared by every marker-range scan
|
|
4
|
+
(`skeleton`'s anchor walk, `outlet`'s close-marker skip) so the convention is probed
|
|
5
|
+
one way everywhere. */
|
|
6
|
+
export function commentData(node: Node): string | undefined {
|
|
7
|
+
if (
|
|
8
|
+
typeof (node as Element).hasAttribute === 'function' ||
|
|
9
|
+
typeof (node as Text).splitText === 'function'
|
|
10
|
+
) {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
return (node as Comment).data
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Scope } from '../types/Scope.ts'
|
|
2
|
+
import { clearBetween } from './clearBetween.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The standard teardown for a marker-bounded range (component, layout/page boundary,
|
|
6
|
+
slot): stop the content's reactivity, dispose its lexical scope, and clear the nodes
|
|
7
|
+
between the markers — leaving the markers in place so a hot swap rebuilds the range.
|
|
8
|
+
Shared by every range mount (`fillRange`, `mountRange`, `fillBoundary`) so the one
|
|
9
|
+
disposer contract lives in a single place.
|
|
10
|
+
*/
|
|
11
|
+
export function disposeRange(
|
|
12
|
+
scoped: { stop: () => void; lexical: Scope },
|
|
13
|
+
start: Comment,
|
|
14
|
+
end: Comment,
|
|
15
|
+
): () => void {
|
|
16
|
+
return () => {
|
|
17
|
+
scoped.stop()
|
|
18
|
+
scoped.lexical.dispose()
|
|
19
|
+
clearBetween(start, end)
|
|
20
|
+
}
|
|
21
|
+
}
|