@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
package/src/lib/ui/dom/when.ts
CHANGED
|
@@ -24,6 +24,7 @@ export function when(
|
|
|
24
24
|
condition: () => unknown,
|
|
25
25
|
render: (parent: Node) => void,
|
|
26
26
|
renderElse?: (parent: Node) => void,
|
|
27
|
+
before: Node | null = null,
|
|
27
28
|
): void {
|
|
28
29
|
const hydration = RENDER.hydration
|
|
29
30
|
const chosenFor = (branch: 'then' | 'else') => (branch === 'then' ? render : renderElse)
|
|
@@ -31,7 +32,10 @@ export function when(
|
|
|
31
32
|
let activeBranch: 'then' | 'else'
|
|
32
33
|
let end: Comment
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
/* `before` (a static node located by the skeleton) places the range among siblings on
|
|
36
|
+
create, so the block sits before a static suffix rather than at the parent's end.
|
|
37
|
+
Hydrate ignores it — the claim cursor (positioned past the prefix) drives placement. */
|
|
38
|
+
const start = openMarker(parent, '[', before)
|
|
35
39
|
if (hydration !== undefined) {
|
|
36
40
|
activeBranch = condition() ? 'then' : 'else'
|
|
37
41
|
const chosen = chosenFor(activeBranch)
|
|
@@ -40,7 +44,7 @@ export function when(
|
|
|
40
44
|
}
|
|
41
45
|
end = openMarker(parent, ']')
|
|
42
46
|
} else {
|
|
43
|
-
end = openMarker(parent, ']')
|
|
47
|
+
end = openMarker(parent, ']', before)
|
|
44
48
|
activeBranch = condition() ? 'then' : 'else'
|
|
45
49
|
const chosen = chosenFor(activeBranch)
|
|
46
50
|
if (chosen !== undefined) {
|
|
@@ -2,9 +2,11 @@ import { html } from '../shared/html.ts'
|
|
|
2
2
|
import { snippet } from '../shared/snippet.ts'
|
|
3
3
|
import { derived } from './derived.ts'
|
|
4
4
|
import { doc } from './doc.ts'
|
|
5
|
+
import { anchorCursor } from './dom/anchorCursor.ts'
|
|
5
6
|
import { appendSnippet } from './dom/appendSnippet.ts'
|
|
6
7
|
import { appendStatic } from './dom/appendStatic.ts'
|
|
7
8
|
import { appendText } from './dom/appendText.ts'
|
|
9
|
+
import { appendTextAt } from './dom/appendTextAt.ts'
|
|
8
10
|
import { attach } from './dom/attach.ts'
|
|
9
11
|
import { attr } from './dom/attr.ts'
|
|
10
12
|
import { awaitBlock } from './dom/awaitBlock.ts'
|
|
@@ -14,8 +16,9 @@ import { eachAsync } from './dom/eachAsync.ts'
|
|
|
14
16
|
import { hydrate } from './dom/hydrate.ts'
|
|
15
17
|
import { mount } from './dom/mount.ts'
|
|
16
18
|
import { mountChild } from './dom/mountChild.ts'
|
|
19
|
+
import { mountSlot } from './dom/mountSlot.ts'
|
|
17
20
|
import { on } from './dom/on.ts'
|
|
18
|
-
import {
|
|
21
|
+
import { skeleton } from './dom/skeleton.ts'
|
|
19
22
|
import { switchBlock } from './dom/switchBlock.ts'
|
|
20
23
|
import { tryBlock } from './dom/tryBlock.ts'
|
|
21
24
|
import { when } from './dom/when.ts'
|
|
@@ -48,11 +51,13 @@ export function installHotBridge(): void {
|
|
|
48
51
|
derived,
|
|
49
52
|
effect,
|
|
50
53
|
mount,
|
|
51
|
-
openChild,
|
|
52
54
|
appendText,
|
|
55
|
+
appendTextAt,
|
|
53
56
|
appendSnippet,
|
|
54
57
|
appendStatic,
|
|
55
58
|
cloneStatic,
|
|
59
|
+
skeleton,
|
|
60
|
+
anchorCursor,
|
|
56
61
|
attr,
|
|
57
62
|
on,
|
|
58
63
|
attach,
|
|
@@ -62,6 +67,7 @@ export function installHotBridge(): void {
|
|
|
62
67
|
awaitBlock,
|
|
63
68
|
tryBlock,
|
|
64
69
|
switchBlock,
|
|
70
|
+
mountSlot,
|
|
65
71
|
mountChild,
|
|
66
72
|
hydrate,
|
|
67
73
|
nextBlockId,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The marker attribute the client back-end stamps on each hole element in a skeleton
|
|
3
|
+
string — an element carrying reactive attributes/listeners/binds whose live node the
|
|
4
|
+
build must wire up. Its value is the hole's index, so attach code addresses holes by
|
|
5
|
+
number regardless of document order. `skeleton` records each marked node's path,
|
|
6
|
+
strips the attribute, and returns the holes indexed by it. One source of truth so the
|
|
7
|
+
compiler's emit and the runtime's read can't drift.
|
|
8
|
+
*/
|
|
9
|
+
export const HOLE_ATTRIBUTE = 'data-abide-hole'
|
|
@@ -11,13 +11,20 @@ child components it inlines, so ids are globally unique within one render pass (
|
|
|
11
11
|
SSR stream and client hydration agree on them — `RESUME` is keyed by id). `depth`
|
|
12
12
|
tracks nesting so the OUTERMOST render/mount resets the counter and a child render/
|
|
13
13
|
mount continues it. See `enterRenderPass`/`nextBlockId`.
|
|
14
|
+
|
|
15
|
+
`namespace` is the ambient foreign-content namespace (SVG/MathML) a control-flow block
|
|
16
|
+
sets from its insertion parent while building into a detached fragment, so foreign
|
|
17
|
+
elements built there get the right namespace — the fragment itself carries none. It is
|
|
18
|
+
undefined outside foreign content. See `enterNamespace`/`effectiveChildNamespace`.
|
|
14
19
|
*/
|
|
15
20
|
export const RENDER: {
|
|
16
21
|
hydration: { next: Map<Node, Node | null> } | undefined
|
|
17
22
|
blockId: number
|
|
18
23
|
depth: number
|
|
24
|
+
namespace: string | undefined
|
|
19
25
|
} = {
|
|
20
26
|
hydration: undefined,
|
|
21
27
|
blockId: 0,
|
|
22
28
|
depth: 0,
|
|
29
|
+
namespace: undefined,
|
|
23
30
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { applyPatchToTree } from './applyPatchToTree.ts'
|
|
2
2
|
import { createSignalNode } from './createSignalNode.ts'
|
|
3
3
|
import { flushEffects } from './flushEffects.ts'
|
|
4
|
-
import { pathExists } from './pathExists.ts'
|
|
5
4
|
import { REACTIVE_CONTEXT } from './REACTIVE_CONTEXT.ts'
|
|
6
5
|
import { readNode } from './readNode.ts'
|
|
7
6
|
import { trigger } from './trigger.ts'
|
|
@@ -9,7 +8,7 @@ import type { Cell } from './types/Cell.ts'
|
|
|
9
8
|
import type { Doc } from './types/Doc.ts'
|
|
10
9
|
import type { Patch } from './types/Patch.ts'
|
|
11
10
|
import type { ReactiveNode } from './types/ReactiveNode.ts'
|
|
12
|
-
import {
|
|
11
|
+
import { walkPath } from './walkPath.ts'
|
|
13
12
|
import { writeNode } from './writeNode.ts'
|
|
14
13
|
|
|
15
14
|
/*
|
|
@@ -39,7 +38,7 @@ export function createDoc(initial: unknown): Doc {
|
|
|
39
38
|
function nodeFor(path: string): ReactiveNode {
|
|
40
39
|
let node = nodes.get(path)
|
|
41
40
|
if (node === undefined) {
|
|
42
|
-
node = createSignalNode(
|
|
41
|
+
node = createSignalNode(walkPath(tree, path).value)
|
|
43
42
|
nodes.set(path, node)
|
|
44
43
|
}
|
|
45
44
|
return node
|
|
@@ -60,7 +59,7 @@ export function createDoc(initial: unknown): Doc {
|
|
|
60
59
|
paying a scan over every minted node.
|
|
61
60
|
*/
|
|
62
61
|
function wakeSubtree(rootPath: string, force: boolean, descend: boolean): void {
|
|
63
|
-
const rootValue =
|
|
62
|
+
const rootValue = walkPath(tree, rootPath).value
|
|
64
63
|
const rootNode = nodes.get(rootPath)
|
|
65
64
|
if (rootNode !== undefined) {
|
|
66
65
|
if (force) {
|
|
@@ -82,8 +81,9 @@ export function createDoc(initial: unknown): Doc {
|
|
|
82
81
|
and this very descend scan degrades linearly with it. The woken
|
|
83
82
|
reader re-mints a fresh node on its flush if the path ever returns.
|
|
84
83
|
Deleting the current entry mid-iteration is safe on a Map. */
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const walk = walkPath(tree, candidate)
|
|
85
|
+
if (walk.exists) {
|
|
86
|
+
writeNode(node, walk.value)
|
|
87
87
|
} else {
|
|
88
88
|
writeNode(node, undefined)
|
|
89
89
|
nodes.delete(candidate)
|
|
@@ -95,8 +95,11 @@ export function createDoc(initial: unknown): Doc {
|
|
|
95
95
|
function apply(patch: Patch): void {
|
|
96
96
|
const segments = patch.path === '' ? [] : patch.path.split('/')
|
|
97
97
|
tree = applyPatchToTree(tree, patch, segments)
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
/* parentPath is patch.path minus its last segment — the same string
|
|
99
|
+
`segments.slice(0, -1).join('/')` rebuilds, taken by one slice instead. */
|
|
100
|
+
const lastSlash = patch.path.lastIndexOf('/')
|
|
101
|
+
const parentPath = lastSlash === -1 ? '' : patch.path.slice(0, lastSlash)
|
|
102
|
+
const parentValue = walkPath(tree, parentPath).value
|
|
100
103
|
const leafKey = segments[segments.length - 1] as string | undefined
|
|
101
104
|
/* A structural change (add/remove, or an array element replaced by index)
|
|
102
105
|
reshapes the parent; a plain value replace reshapes only its own path. */
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The result of walking a `/`-joined path through a tree: whether the path still
|
|
3
|
+
resolves to an own slot at every segment, and the value it holds. `exists` is
|
|
4
|
+
false when a segment is missing (a deleted key, an out-of-range index); `value`
|
|
5
|
+
is then `undefined`. Separates a missing path from one holding a real `undefined`.
|
|
6
|
+
*/
|
|
7
|
+
export type PathWalk = {
|
|
8
|
+
exists: boolean
|
|
9
|
+
value: unknown
|
|
10
|
+
}
|
|
@@ -3,13 +3,12 @@ What a component is invoked with. Two real shapes flow through the one parameter
|
|
|
3
3
|
A top-level page/layout is called by the router (client) and `renderChain` (SSR)
|
|
4
4
|
with its decoded route params — a plain string map. A nested child is called by
|
|
5
5
|
the compiler-emitted `mountChild` with a map of reactive thunks (each authored
|
|
6
|
-
prop, read in the body as `$props[name]?.()`) plus optional slot
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
prop, read in the body as `$props[name]?.()`) plus an optional `$children` slot
|
|
7
|
+
builder carrying the component's `<slot>` markup, mounting into the host element
|
|
8
|
+
it is handed.
|
|
9
9
|
*/
|
|
10
10
|
export type UiProps =
|
|
11
11
|
| Record<string, string>
|
|
12
12
|
| (Record<string, () => unknown> & {
|
|
13
13
|
$children?: (host: Element) => void
|
|
14
|
-
$slots?: Record<string, (host: Element) => void>
|
|
15
14
|
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PathWalk } from './types/PathWalk.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Walks a `/`-joined path through a plain tree in one pass, returning both whether
|
|
5
|
+
the path still resolves (`exists`) and the value it holds (`value`). `''` is the
|
|
6
|
+
root. Arrays index by their numeric segment as a string — JS array access coerces
|
|
7
|
+
the key, and `in` covers an own index in range or `length`.
|
|
8
|
+
|
|
9
|
+
The two answers are inseparable on the eviction path (`createDoc`'s descend),
|
|
10
|
+
which must distinguish a path the tree no longer has (a deleted key, an
|
|
11
|
+
out-of-range index after a shrink) from one holding a genuine `undefined` — a
|
|
12
|
+
distinction the value alone can't make. Returning both walks the path once where
|
|
13
|
+
a separate value-read + existence-check would walk it twice.
|
|
14
|
+
*/
|
|
15
|
+
export function walkPath(tree: unknown, path: string): PathWalk {
|
|
16
|
+
if (path === '') {
|
|
17
|
+
return { exists: tree !== undefined, value: tree }
|
|
18
|
+
}
|
|
19
|
+
let current: unknown = tree
|
|
20
|
+
for (const segment of path.split('/')) {
|
|
21
|
+
if (current === null || typeof current !== 'object' || !(segment in current)) {
|
|
22
|
+
return { exists: false, value: undefined }
|
|
23
|
+
}
|
|
24
|
+
current = (current as Record<string, unknown>)[segment]
|
|
25
|
+
}
|
|
26
|
+
return { exists: true, value: current }
|
|
27
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
/* A second page — served at GET /about. Folder name becomes the URL segment
|
|
3
|
-
|
|
2
|
+
/* A second page — served at GET /about. Folder name becomes the URL segment; the
|
|
3
|
+
root layout.abide wraps it, same as the home page. */
|
|
4
4
|
</script>
|
|
5
5
|
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
<p>This is a barebones abide app.</p>
|
|
9
|
-
</Layout>
|
|
6
|
+
<h1>About</h1>
|
|
7
|
+
<p>This is a barebones abide app.</p>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/*
|
|
3
|
+
Root layout. A layout.abide wraps every page at or below its folder — here
|
|
4
|
+
src/ui/pages/, so it wraps the whole app. The page lands in the <slot/> outlet;
|
|
5
|
+
the resolver finds it by filename (no per-page import), and the client router
|
|
6
|
+
keeps it mounted across navigation, so this chrome never re-mounts. A nested
|
|
7
|
+
layout.abide (in a subfolder) wraps inside this one. Runs on the server during
|
|
8
|
+
SSR and on the client after hydration.
|
|
9
|
+
*/
|
|
10
|
+
import '../app.css'
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<header>
|
|
14
|
+
<nav>
|
|
15
|
+
<a href="/">Home</a>
|
|
16
|
+
<a href="/about">About</a>
|
|
17
|
+
</nav>
|
|
18
|
+
</header>
|
|
19
|
+
<main>
|
|
20
|
+
<slot></slot>
|
|
21
|
+
</main>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
/*
|
|
3
3
|
Root page — served at GET /. Every folder under src/ui/pages/ that contains a
|
|
4
|
-
page.abide mounts at that folder's URL.
|
|
4
|
+
page.abide mounts at that folder's URL; the root layout.abide wraps it.
|
|
5
5
|
|
|
6
6
|
The blocking await-block below (the `then` attribute sits ON the <template await>)
|
|
7
7
|
resolves on the server during SSR and renders inline — no pending placeholder. The
|
|
@@ -11,12 +11,9 @@ instead would stream the resolution in out of order.
|
|
|
11
11
|
*/
|
|
12
12
|
import { cache } from '@abide/abide/shared/cache'
|
|
13
13
|
import { getHello } from '$server/rpc/getHello.ts'
|
|
14
|
-
import Layout from '../Layout.abide'
|
|
15
14
|
</script>
|
|
16
15
|
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<p>Edit <code>src/ui/pages/page.abide</code> and the page hot-reloads.</p>
|
|
22
|
-
</Layout>
|
|
16
|
+
<template await={cache(getHello)()} then="hello">
|
|
17
|
+
<h1>{hello.message}</h1>
|
|
18
|
+
</template>
|
|
19
|
+
<p>Edit <code>src/ui/pages/page.abide</code> and the page hot-reloads.</p>
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { staticAttrValue } from './staticAttrValue.ts'
|
|
2
|
-
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
3
|
-
|
|
4
|
-
/* A component's slotted children split by destination slot. */
|
|
5
|
-
export type SlotGroups = {
|
|
6
|
-
default: TemplateNode[]
|
|
7
|
-
named: { name: string; nodes: TemplateNode[] }[]
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/*
|
|
11
|
-
Partitions a component's children by their `slot="name"` attribute: an element
|
|
12
|
-
carrying one goes to that named group (with the directive attr stripped so it
|
|
13
|
-
never renders as a real attribute), everything else forms the default slot. Both
|
|
14
|
-
back-ends partition identically, so SSR and client agree on which markup lands in
|
|
15
|
-
which `<slot>`.
|
|
16
|
-
*/
|
|
17
|
-
export function partitionSlots(children: TemplateNode[]): SlotGroups {
|
|
18
|
-
const defaults: TemplateNode[] = []
|
|
19
|
-
const named = new Map<string, TemplateNode[]>()
|
|
20
|
-
for (const child of children) {
|
|
21
|
-
const name = child.kind === 'element' ? staticAttrValue(child, 'slot') : undefined
|
|
22
|
-
if (child.kind !== 'element' || name === undefined) {
|
|
23
|
-
defaults.push(child)
|
|
24
|
-
continue
|
|
25
|
-
}
|
|
26
|
-
const stripped = {
|
|
27
|
-
...child,
|
|
28
|
-
attrs: child.attrs.filter((attr) => !(attr.kind === 'static' && attr.name === 'slot')),
|
|
29
|
-
}
|
|
30
|
-
named.set(name, [...(named.get(name) ?? []), stripped])
|
|
31
|
-
}
|
|
32
|
-
return {
|
|
33
|
-
default: defaults,
|
|
34
|
-
named: [...named].map(([name, nodes]) => ({ name, nodes })),
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { claimChild } from '../runtime/claimChild.ts'
|
|
2
|
-
import { RENDER } from '../runtime/RENDER.ts'
|
|
3
|
-
|
|
4
|
-
/*
|
|
5
|
-
Opens a child element of `parent`: creates and appends it (create mode), or claims
|
|
6
|
-
the existing server-rendered node at the parent's current build position (hydrate
|
|
7
|
-
mode), advancing the claim pointer. Returns the element so bindings and children
|
|
8
|
-
attach to it. The compiler emits this for every element so the same build code
|
|
9
|
-
serves both modes.
|
|
10
|
-
*/
|
|
11
|
-
// @readme plumbing
|
|
12
|
-
export function openChild(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
|
-
const element = document.createElement(tag)
|
|
20
|
-
parent.appendChild(element)
|
|
21
|
-
return element
|
|
22
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Whether a `/`-joined path still resolves through `tree` — every segment present
|
|
3
|
-
in its container. The `in` operator covers both shapes: an object key, an array's
|
|
4
|
-
own index (in range) or its `length`. Distinguishes a path the tree no longer has
|
|
5
|
-
(a deleted key, an out-of-range index after a shrink) from one holding a genuine
|
|
6
|
-
`undefined`, which `valueAtPath` alone can't — used to evict dead reactive nodes.
|
|
7
|
-
*/
|
|
8
|
-
export function pathExists(tree: unknown, path: string): boolean {
|
|
9
|
-
if (path === '') {
|
|
10
|
-
return tree !== undefined
|
|
11
|
-
}
|
|
12
|
-
let current: unknown = tree
|
|
13
|
-
for (const segment of path.split('/')) {
|
|
14
|
-
if (current === null || typeof current !== 'object') {
|
|
15
|
-
return false
|
|
16
|
-
}
|
|
17
|
-
if (!(segment in current)) {
|
|
18
|
-
return false
|
|
19
|
-
}
|
|
20
|
-
current = (current as Record<string, unknown>)[segment]
|
|
21
|
-
}
|
|
22
|
-
return true
|
|
23
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Reads the value at a `/`-joined path in a plain tree. `''` is the root. Returns
|
|
3
|
-
undefined if any segment is missing — arrays index by their numeric segment as a
|
|
4
|
-
string, which works because JS array access coerces the key.
|
|
5
|
-
*/
|
|
6
|
-
export function valueAtPath(tree: unknown, path: string): unknown {
|
|
7
|
-
if (path === '') {
|
|
8
|
-
return tree
|
|
9
|
-
}
|
|
10
|
-
let current: unknown = tree
|
|
11
|
-
for (const segment of path.split('/')) {
|
|
12
|
-
if (current === null || typeof current !== 'object') {
|
|
13
|
-
return undefined
|
|
14
|
-
}
|
|
15
|
-
current = (current as Record<string, unknown>)[segment]
|
|
16
|
-
}
|
|
17
|
-
return current
|
|
18
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
/*
|
|
3
|
-
Userland root layout. abide has no framework layout resolution — a layout is just
|
|
4
|
-
a component a page wraps its body in (<Layout>…</Layout>), and the body lands in
|
|
5
|
-
the <slot>. Import and wrap this from each page.abide to share chrome. Runs on the
|
|
6
|
-
server during SSR and on the client after hydration.
|
|
7
|
-
*/
|
|
8
|
-
import './app.css'
|
|
9
|
-
</script>
|
|
10
|
-
|
|
11
|
-
<header>
|
|
12
|
-
<nav>
|
|
13
|
-
<a href="/">Home</a>
|
|
14
|
-
<a href="/about">About</a>
|
|
15
|
-
</nav>
|
|
16
|
-
</header>
|
|
17
|
-
<main>
|
|
18
|
-
<slot></slot>
|
|
19
|
-
</main>
|