@abide/abide 0.31.1 → 0.32.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 +2 -1
- package/CHANGELOG.md +26 -0
- package/package.json +1 -2
- package/src/checkAbide.ts +11 -6
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +4 -3
- package/src/lib/server/runtime/createServer.ts +28 -23
- package/src/lib/server/runtime/createUiPageRenderer.ts +1 -1
- package/src/lib/ui/compile/AbideCompileError.ts +16 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +0 -1
- package/src/lib/ui/compile/abideUiPlugin.ts +25 -2
- package/src/lib/ui/compile/bindListenEvent.ts +19 -0
- package/src/lib/ui/compile/compileShadow.ts +65 -52
- package/src/lib/ui/compile/componentWrapperTag.ts +20 -0
- package/src/lib/ui/compile/generateBuild.ts +114 -212
- package/src/lib/ui/compile/generateSSR.ts +54 -88
- package/src/lib/ui/compile/lowerContext.ts +64 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +6 -1
- package/src/lib/ui/compile/offsetToLineColumn.ts +16 -0
- package/src/lib/ui/compile/scopeAttr.ts +9 -0
- package/src/lib/ui/compile/staticAttr.ts +11 -0
- package/src/lib/ui/compile/staticTextPart.ts +12 -0
- package/src/lib/ui/compile/unwrapParens.ts +10 -0
- package/src/lib/ui/dom/applyResolved.ts +9 -6
- package/src/lib/ui/dom/awaitBlock.ts +27 -21
- package/src/lib/ui/dom/clearBetween.ts +16 -0
- package/src/lib/ui/dom/each.ts +64 -38
- package/src/lib/ui/dom/eachAsync.ts +41 -54
- package/src/lib/ui/dom/fillBefore.ts +16 -0
- package/src/lib/ui/dom/moveRange.ts +19 -0
- package/src/lib/ui/dom/openMarker.ts +22 -0
- package/src/lib/ui/dom/removeRange.ts +18 -0
- package/src/lib/ui/dom/switchBlock.ts +32 -40
- package/src/lib/ui/dom/tryBlock.ts +31 -35
- package/src/lib/ui/dom/types/EachRow.ts +10 -3
- package/src/lib/ui/dom/types/SwitchCase.ts +3 -2
- package/src/lib/ui/dom/when.ts +34 -43
- package/src/lib/ui/installHotBridge.ts +0 -2
- package/src/lib/ui/renderToStream.ts +14 -11
- package/src/lib/ui/state.ts +14 -5
- package/src/lib/ui/compile/branchElements.ts +0 -50
- package/src/lib/ui/dom/openRoot.ts +0 -20
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
/* A live row in a keyed list:
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/* A live row in a keyed list: a content RANGE bounded by two comment markers (so a
|
|
2
|
+
row holds any content, not just one node), plus the disposer for the bindings
|
|
3
|
+
created in its ownership scope. `pending` holds a freshly built row's nodes in a
|
|
4
|
+
fragment until first placement inserts them. */
|
|
5
|
+
export type EachRow = {
|
|
6
|
+
start: Node
|
|
7
|
+
end: Node
|
|
8
|
+
dispose: () => void
|
|
9
|
+
pending?: DocumentFragment
|
|
10
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* One branch of a `switch`: `match` returns the value this case selects on
|
|
2
|
-
(undefined = the default branch); `render` builds the branch's
|
|
2
|
+
(undefined = the default branch); `render` builds the branch's content into the
|
|
3
|
+
parent (the block tracks it as a range between markers). */
|
|
3
4
|
export type SwitchCase = {
|
|
4
5
|
match: (() => unknown) | undefined
|
|
5
|
-
render: (parent: Node) =>
|
|
6
|
+
render: (parent: Node) => void
|
|
6
7
|
}
|
package/src/lib/ui/dom/when.ts
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
|
-
import { claimChild } from '../runtime/claimChild.ts'
|
|
3
2
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
3
|
import { scope } from '../runtime/scope.ts'
|
|
4
|
+
import { clearBetween } from './clearBetween.ts'
|
|
5
|
+
import { fillBefore } from './fillBefore.ts'
|
|
6
|
+
import { openMarker } from './openMarker.ts'
|
|
5
7
|
|
|
6
8
|
/*
|
|
7
|
-
Conditional binding — the runtime for `<template if>` (with optional `else`).
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Conditional binding — the runtime for `<template if>` (with optional `else`). The
|
|
10
|
+
branch's content lives in a RANGE bounded by two comment markers, so a branch may
|
|
11
|
+
hold anything — elements, components, text, nested control-flow, snippets — not
|
|
12
|
+
just element roots. An effect tracks `condition()` and swaps the range's content
|
|
13
|
+
on a truthy↔falsy flip (`render` truthy, `renderElse` falsy); an unchanged
|
|
14
|
+
condition is a no-op.
|
|
12
15
|
|
|
13
|
-
On hydrate it adopts the
|
|
14
|
-
in place (its
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
On hydrate it adopts the server-rendered range: claim the start marker, run the
|
|
17
|
+
matching render in place (its content claims the existing nodes), then claim the
|
|
18
|
+
end marker. The effect's first run sees the same branch and is a no-op; later
|
|
19
|
+
toggles clear the range and build fresh into a fragment.
|
|
17
20
|
*/
|
|
18
21
|
// @readme plumbing
|
|
19
22
|
export function when(
|
|
20
23
|
parent: Node,
|
|
21
24
|
condition: () => unknown,
|
|
22
|
-
render: (parent: Node) =>
|
|
23
|
-
renderElse?: (parent: Node) =>
|
|
25
|
+
render: (parent: Node) => void,
|
|
26
|
+
renderElse?: (parent: Node) => void,
|
|
24
27
|
): void {
|
|
25
28
|
const hydration = RENDER.hydration
|
|
26
|
-
|
|
27
|
-
let
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
const build = (chosen: (parent: Node) => Node[]): { nodes: Node[]; dispose: () => void } => {
|
|
31
|
-
let nodes: Node[] = []
|
|
32
|
-
const dispose = scope(() => {
|
|
33
|
-
nodes = chosen(parent)
|
|
34
|
-
})
|
|
35
|
-
return { nodes, dispose }
|
|
36
|
-
}
|
|
29
|
+
const chosenFor = (branch: 'then' | 'else') => (branch === 'then' ? render : renderElse)
|
|
30
|
+
let dispose: (() => void) | undefined
|
|
31
|
+
let activeBranch: 'then' | 'else'
|
|
32
|
+
let end: Comment
|
|
37
33
|
|
|
34
|
+
const start = openMarker(parent, '[')
|
|
38
35
|
if (hydration !== undefined) {
|
|
39
36
|
activeBranch = condition() ? 'then' : 'else'
|
|
40
|
-
const chosen = activeBranch
|
|
37
|
+
const chosen = chosenFor(activeBranch)
|
|
41
38
|
if (chosen !== undefined) {
|
|
42
|
-
|
|
39
|
+
dispose = scope(() => chosen(parent)) // content claims the SSR nodes in place
|
|
43
40
|
}
|
|
44
|
-
|
|
45
|
-
parent.insertBefore(anchor, claimChild(hydration, parent))
|
|
41
|
+
end = openMarker(parent, ']')
|
|
46
42
|
} else {
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
end = openMarker(parent, ']')
|
|
44
|
+
activeBranch = condition() ? 'then' : 'else'
|
|
45
|
+
const chosen = chosenFor(activeBranch)
|
|
46
|
+
if (chosen !== undefined) {
|
|
47
|
+
dispose = fillBefore(end, chosen)
|
|
48
|
+
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
effect(() => {
|
|
@@ -53,21 +53,12 @@ export function when(
|
|
|
53
53
|
if (branch === activeBranch) {
|
|
54
54
|
return
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for (const node of active.nodes) {
|
|
59
|
-
parent.removeChild(node)
|
|
60
|
-
}
|
|
61
|
-
active = undefined
|
|
62
|
-
}
|
|
56
|
+
clearBetween(start, end, dispose)
|
|
57
|
+
dispose = undefined
|
|
63
58
|
activeBranch = branch
|
|
64
|
-
const chosen = branch
|
|
65
|
-
if (chosen
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
active = build(chosen)
|
|
69
|
-
for (const node of active.nodes) {
|
|
70
|
-
parent.insertBefore(node, anchor)
|
|
59
|
+
const chosen = chosenFor(branch)
|
|
60
|
+
if (chosen !== undefined) {
|
|
61
|
+
dispose = fillBefore(end, chosen)
|
|
71
62
|
}
|
|
72
63
|
})
|
|
73
64
|
}
|
|
@@ -16,7 +16,6 @@ import { mount } from './dom/mount.ts'
|
|
|
16
16
|
import { mountChild } from './dom/mountChild.ts'
|
|
17
17
|
import { on } from './dom/on.ts'
|
|
18
18
|
import { openChild } from './dom/openChild.ts'
|
|
19
|
-
import { openRoot } from './dom/openRoot.ts'
|
|
20
19
|
import { switchBlock } from './dom/switchBlock.ts'
|
|
21
20
|
import { tryBlock } from './dom/tryBlock.ts'
|
|
22
21
|
import { when } from './dom/when.ts'
|
|
@@ -50,7 +49,6 @@ export function installHotBridge(): void {
|
|
|
50
49
|
effect,
|
|
51
50
|
mount,
|
|
52
51
|
openChild,
|
|
53
|
-
openRoot,
|
|
54
52
|
appendText,
|
|
55
53
|
appendSnippet,
|
|
56
54
|
appendStatic,
|
|
@@ -5,10 +5,11 @@ import type { SsrAwait, SsrRender } from './runtime/types/SsrRender.ts'
|
|
|
5
5
|
Out-of-order SSR streaming. Yields the pending shell first (so the browser paints
|
|
6
6
|
immediately), then one resolved fragment per await block as its promise settles —
|
|
7
7
|
in completion order, not source order, so a slow read never blocks a fast one.
|
|
8
|
-
Each resolved fragment is a `<abide-resolve data-id="ID"
|
|
9
|
-
that `applyResolved` swaps into the matching
|
|
10
|
-
|
|
11
|
-
`await` block adopts the resolved branch on resume
|
|
8
|
+
Each resolved fragment is a `<abide-resolve data-id="ID"><script type="application/json">
|
|
9
|
+
…</script>…</abide-resolve>` that `applyResolved` swaps into the matching
|
|
10
|
+
`<!--abide:await:ID-->` boundary; the leading script holds the JSON-serialized value,
|
|
11
|
+
registered for hydration so an `await` block adopts the resolved branch on resume
|
|
12
|
+
instead of re-running.
|
|
12
13
|
|
|
13
14
|
This is the await-block-streams half of the cache rule: a top-level `await` in the
|
|
14
15
|
script would have blocked the shell (inlined), but an await *block* flushes its
|
|
@@ -46,7 +47,9 @@ export async function* renderToStream(render: () => SsrRender): AsyncGenerator<s
|
|
|
46
47
|
const resolved = await Promise.race(inflight.values())
|
|
47
48
|
inflight.delete(resolved.id)
|
|
48
49
|
const resume = encodeResume(resolved.resume)
|
|
49
|
-
yield `<abide-resolve data-id="${resolved.id}"
|
|
50
|
+
yield `<abide-resolve data-id="${resolved.id}">` +
|
|
51
|
+
`<script type="application/json">${resume}</script>` +
|
|
52
|
+
`${resolved.html}</abide-resolve>`
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -94,11 +97,11 @@ function settle(block: SsrAwait): Promise<Settled> {
|
|
|
94
97
|
)
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
/* JSON for
|
|
98
|
-
|
|
100
|
+
/* JSON for a `<script type="application/json">` data block: script content is raw
|
|
101
|
+
text, so only `<` needs neutralizing (emitted as a unicode escape) to keep a
|
|
102
|
+
literal `</script>` from closing the block early — quotes stay raw. Far cheaper
|
|
103
|
+
than attribute escaping (no full-string `"`/`&` passes) and JSON.parse decodes it
|
|
104
|
+
back. `applyResolved`/the inline swap script read it via `.textContent`. */
|
|
99
105
|
function encodeResume(resume: ResumeEntry): string {
|
|
100
|
-
return JSON.stringify(resume)
|
|
101
|
-
.replace(/&/g, '&')
|
|
102
|
-
.replace(/"/g, '"')
|
|
103
|
-
.replace(/</g, '<')
|
|
106
|
+
return JSON.stringify(resume).replace(/</g, '\\u003c')
|
|
104
107
|
}
|
package/src/lib/ui/state.ts
CHANGED
|
@@ -18,15 +18,24 @@ mirror of `derived`'s write-through `set` — here the value lives in this cell,
|
|
|
18
18
|
the gate *returns* what to store rather than writing an external target. The
|
|
19
19
|
construction `initial` is taken verbatim; the gate runs on writes only.
|
|
20
20
|
*/
|
|
21
|
+
/* No-arg form for an undefined initial with a declared type: `state<Foo>()` is
|
|
22
|
+
`State<Foo | undefined>`. Without it `state<Foo>(undefined)` is an arity/assign
|
|
23
|
+
error and `state(undefined)` infers `T = undefined` (every `.value` access then
|
|
24
|
+
narrows to `never`). */
|
|
21
25
|
// @readme plumbing
|
|
22
|
-
export function state<T>(
|
|
26
|
+
export function state<T>(): State<T | undefined>
|
|
27
|
+
export function state<T>(initial: T, transform?: (next: T, previous: T) => T): State<T>
|
|
28
|
+
export function state<T>(
|
|
29
|
+
initial?: T,
|
|
30
|
+
transform?: (next: T, previous: T) => T,
|
|
31
|
+
): State<T | undefined> {
|
|
23
32
|
const node = createSignalNode(initial)
|
|
24
33
|
return {
|
|
25
|
-
get value(): T {
|
|
26
|
-
return readNode(node) as T
|
|
34
|
+
get value(): T | undefined {
|
|
35
|
+
return readNode(node) as T | undefined
|
|
27
36
|
},
|
|
28
|
-
set value(next: T) {
|
|
29
|
-
writeNode(node, transform === undefined ? next : transform(next, node.value as T))
|
|
37
|
+
set value(next: T | undefined) {
|
|
38
|
+
writeNode(node, transform === undefined ? next : transform(next as T, node.value as T))
|
|
30
39
|
},
|
|
31
40
|
}
|
|
32
41
|
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
The element roots of a control-flow branch (`if`/`else`/`switch case`/`await
|
|
5
|
-
then|catch`). A branch may hold one or MORE top-level elements — each becomes a
|
|
6
|
-
root the block tracks as a range. Whitespace-only text between/around them is
|
|
7
|
-
dropped (so SSR and the client build agree on the node set, keeping hydration
|
|
8
|
-
aligned). Any other top-level content — meaningful text, a component, or a nested
|
|
9
|
-
control-flow `<template>` — must be wrapped in an element; it throws a clear error
|
|
10
|
-
rather than silently dropping (full fragment roots are a separate feature).
|
|
11
|
-
|
|
12
|
-
Both back-ends call this, so the server HTML and the client build contain exactly
|
|
13
|
-
the same roots in the same order.
|
|
14
|
-
*/
|
|
15
|
-
type ElementNode = Extract<TemplateNode, { kind: 'element' }>
|
|
16
|
-
|
|
17
|
-
export function branchElements(
|
|
18
|
-
children: TemplateNode[],
|
|
19
|
-
context: string,
|
|
20
|
-
allowEmpty = false,
|
|
21
|
-
): ElementNode[] {
|
|
22
|
-
const elements: ElementNode[] = []
|
|
23
|
-
for (const child of children) {
|
|
24
|
-
if (child.kind === 'element') {
|
|
25
|
-
elements.push(child)
|
|
26
|
-
continue
|
|
27
|
-
}
|
|
28
|
-
/* Whitespace-only text is layout noise between roots — drop it. */
|
|
29
|
-
if (child.kind === 'text' && isWhitespaceOnly(child)) {
|
|
30
|
-
continue
|
|
31
|
-
}
|
|
32
|
-
/* A scoped `<script>` is emitted as code by the back-end, not a root; a
|
|
33
|
-
`<style>` is bundled CSS (its scope already stamped on the roots). */
|
|
34
|
-
if (child.kind === 'script' || child.kind === 'style') {
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
throw new Error(
|
|
38
|
-
`[abide] ${context} content must be element(s); wrap text / components / nested <template> in an element`,
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
if (elements.length === 0 && !allowEmpty) {
|
|
42
|
-
throw new Error(`[abide] ${context} must contain at least one element`)
|
|
43
|
-
}
|
|
44
|
-
return elements
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/* A text node whose parts are all whitespace literals (no interpolation). */
|
|
48
|
-
function isWhitespaceOnly(node: Extract<TemplateNode, { kind: 'text' }>): boolean {
|
|
49
|
-
return node.parts.every((part) => part.kind === 'static' && part.value.trim() === '')
|
|
50
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { claimChild } from '../runtime/claimChild.ts'
|
|
2
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Opens the root element of a control-flow branch/row, which the block inserts
|
|
6
|
-
itself. In create mode it returns a detached element (the block does the
|
|
7
|
-
insert); in hydrate mode it claims the existing server-rendered root from the
|
|
8
|
-
parent's claim pointer (in place — the block adopts it). The compiler emits this
|
|
9
|
-
for `each`/`if`/`switch`/`await` branch roots so adoption and creation share code.
|
|
10
|
-
*/
|
|
11
|
-
// @readme plumbing
|
|
12
|
-
export function openRoot(parent: Node, tag: string): Element {
|
|
13
|
-
const hydration = RENDER.hydration
|
|
14
|
-
if (hydration !== undefined) {
|
|
15
|
-
const current = claimChild(hydration, parent)
|
|
16
|
-
hydration.next.set(parent, current === null ? null : current.nextSibling)
|
|
17
|
-
return current as unknown as Element
|
|
18
|
-
}
|
|
19
|
-
return document.createElement(tag)
|
|
20
|
-
}
|