@abide/abide 0.29.0 → 0.31.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 +6 -4
- package/CHANGELOG.md +32 -0
- package/package.json +2 -1
- package/src/lib/bundle/disconnected.abide +82 -82
- package/src/lib/cli/dispatchCommand.ts +3 -2
- package/src/lib/cli/resolveCliTarget.ts +2 -3
- package/src/lib/cli/runCli.ts +2 -3
- package/src/lib/cli/runSession.ts +2 -3
- package/src/lib/mcp/dispatchMcpRequest.ts +3 -2
- package/src/lib/mcp/mcpSurface.ts +2 -1
- package/src/lib/mcp/toolResultFromResponse.ts +2 -1
- package/src/lib/server/rpc/parseArgs.ts +1 -3
- package/src/lib/server/runtime/streamFromIterator.ts +3 -1
- package/src/lib/server/runtime/warnUnguardedMcp.ts +4 -3
- package/src/lib/server/sockets/createSocketDispatcher.ts +5 -7
- package/src/lib/shared/cacheEntryFromSnapshot.ts +2 -1
- package/src/lib/shared/contentTypeOf.ts +6 -0
- package/src/lib/shared/decodeResponse.ts +2 -1
- package/src/lib/shared/isCompileTarget.ts +7 -1
- package/src/lib/shared/isModuleNotFound.ts +3 -1
- package/src/lib/shared/isStreamingResponse.ts +2 -1
- package/src/lib/shared/messageFromError.ts +6 -0
- package/src/lib/shared/streamResponse.ts +2 -1
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +7 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
- package/src/lib/ui/compile/VOID_TAGS.ts +4 -3
- package/src/lib/ui/compile/compileComponent.ts +12 -2
- package/src/lib/ui/compile/compileModule.ts +7 -4
- package/src/lib/ui/compile/compileSSR.ts +11 -2
- package/src/lib/ui/compile/compileShadow.ts +165 -54
- package/src/lib/ui/compile/createShadowLanguageService.ts +2 -1
- package/src/lib/ui/compile/createShadowProgram.ts +2 -1
- package/src/lib/ui/compile/desugarSignals.ts +41 -14
- package/src/lib/ui/compile/parseTemplate.ts +21 -26
- package/src/lib/ui/compile/prepareNestedScript.ts +4 -2
- package/src/lib/ui/derived.ts +25 -4
- package/src/lib/ui/dom/awaitBlock.ts +1 -24
- package/src/lib/ui/dom/discardBoundary.ts +27 -0
- package/src/lib/ui/dom/tryBlock.ts +7 -26
- package/src/lib/ui/installHotBridge.ts +2 -0
- package/src/lib/ui/linked.ts +34 -0
- package/src/lib/ui/router.ts +23 -6
- package/src/lib/ui/state.ts +9 -2
- package/template/src/ui/pages/page.abide +1 -1
|
@@ -2,6 +2,7 @@ import { decodeHtmlEntities } from './decodeHtmlEntities.ts'
|
|
|
2
2
|
import type { TemplateAttr } from './types/TemplateAttr.ts'
|
|
3
3
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
4
4
|
import type { TextPart } from './types/TextPart.ts'
|
|
5
|
+
import { VOID_TAGS } from './VOID_TAGS.ts'
|
|
5
6
|
|
|
6
7
|
/*
|
|
7
8
|
A minimal compile-time parser for the abide template subset: elements, text with
|
|
@@ -19,23 +20,6 @@ text, never mistaken for a real style. Keeping it in the tree lets the front-end
|
|
|
19
20
|
scope it to its sibling subtree (`analyzeComponent`); the node emits no DOM/markup.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
|
-
const VOID_TAGS = new Set([
|
|
23
|
-
'area',
|
|
24
|
-
'base',
|
|
25
|
-
'br',
|
|
26
|
-
'col',
|
|
27
|
-
'embed',
|
|
28
|
-
'hr',
|
|
29
|
-
'img',
|
|
30
|
-
'input',
|
|
31
|
-
'link',
|
|
32
|
-
'meta',
|
|
33
|
-
'param',
|
|
34
|
-
'source',
|
|
35
|
-
'track',
|
|
36
|
-
'wbr',
|
|
37
|
-
])
|
|
38
|
-
|
|
39
23
|
/* A braced template expression with the absolute source offset of its first
|
|
40
24
|
(post-trim) character, so the type-checking shadow can map a diagnostic back. */
|
|
41
25
|
type Braced = { code: string; loc: number }
|
|
@@ -245,18 +229,29 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
|
|
|
245
229
|
return { nodes: roots }
|
|
246
230
|
}
|
|
247
231
|
|
|
248
|
-
/* Turns a component's attributes into props
|
|
249
|
-
|
|
232
|
+
/* Turns a component's attributes into props. A component has no directives —
|
|
233
|
+
every attribute is a prop under its written name, so `on*`/`bind:`/`attach`
|
|
234
|
+
round-trip to their original names (the kinds the tag-blind attribute parser
|
|
235
|
+
assigned) instead of being dropped. A static value becomes a string literal;
|
|
236
|
+
every other kind keeps its `code`, letting a prop hold any value, functions
|
|
237
|
+
included (e.g. an `onclick` callback). */
|
|
250
238
|
function toProps(attrs: TemplateAttr[]): { name: string; code: string; loc?: number }[] {
|
|
251
|
-
|
|
252
|
-
for (const attr of attrs) {
|
|
239
|
+
return attrs.map((attr) => {
|
|
253
240
|
if (attr.kind === 'static') {
|
|
254
|
-
|
|
255
|
-
} else if (attr.kind === 'expression') {
|
|
256
|
-
props.push({ name: attr.name, code: attr.code, loc: attr.loc })
|
|
241
|
+
return { name: attr.name, code: JSON.stringify(attr.value) }
|
|
257
242
|
}
|
|
258
|
-
|
|
259
|
-
|
|
243
|
+
/* Every non-static kind keeps its `code`/`loc`; only the prop name differs —
|
|
244
|
+
a directive (`event`/`bind`/`attach`) round-trips to its written name. */
|
|
245
|
+
const name =
|
|
246
|
+
attr.kind === 'event'
|
|
247
|
+
? `on${attr.event}`
|
|
248
|
+
: attr.kind === 'bind'
|
|
249
|
+
? `bind:${attr.property}`
|
|
250
|
+
: attr.kind === 'attach'
|
|
251
|
+
? 'attach'
|
|
252
|
+
: attr.name
|
|
253
|
+
return { name, code: attr.code, loc: attr.loc }
|
|
254
|
+
})
|
|
260
255
|
}
|
|
261
256
|
|
|
262
257
|
/* The literal text of an attribute (a static value or an expression's code);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
+
import { REACTIVE_CALLEES } from './REACTIVE_CALLEES.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
The signal binding names a `<script>` nested in a control-flow branch declares
|
|
5
|
-
(`state`/`derived`/`prop`). The back-end adds them to the deref scope so both the
|
|
6
|
+
(`state`/`linked`/`derived`/`prop`). The back-end adds them to the deref scope so both the
|
|
6
7
|
script body and the branch's markup rewrite `{a}` → `a.value` — these stay PLAIN
|
|
7
8
|
signals (local to the branch's render, owned by its scope, re-seeded from the
|
|
8
9
|
in-scope data each mount), unlike the top-level component script which desugars to
|
|
@@ -18,7 +19,8 @@ export function nestedBindingNames(code: string): Set<string> {
|
|
|
18
19
|
for (const declaration of statement.declarationList.declarations) {
|
|
19
20
|
const callee = signalCallee(declaration)
|
|
20
21
|
if (
|
|
21
|
-
|
|
22
|
+
callee !== undefined &&
|
|
23
|
+
REACTIVE_CALLEES.has(callee) &&
|
|
22
24
|
ts.isIdentifier(declaration.name)
|
|
23
25
|
) {
|
|
24
26
|
names.add(declaration.name.text)
|
package/src/lib/ui/derived.ts
CHANGED
|
@@ -2,15 +2,26 @@ import { createComputedNode } from './runtime/createComputedNode.ts'
|
|
|
2
2
|
import { OWNER } from './runtime/OWNER.ts'
|
|
3
3
|
import { readNode } from './runtime/readNode.ts'
|
|
4
4
|
import type { Derived } from './runtime/types/Derived.ts'
|
|
5
|
+
import type { State } from './runtime/types/State.ts'
|
|
5
6
|
import { unlinkDeps } from './runtime/unlinkDeps.ts'
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
|
-
A
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
A reactive cell computed from other cells — the abide replacement for `$derived`.
|
|
10
|
+
Lazy: it recomputes on read only when a dependency has changed, and never
|
|
11
|
+
serializes (it is re-derived from its inputs on resume). Read via `.value`.
|
|
12
|
+
|
|
13
|
+
With a `set`, it becomes a writable lens (Vue/Svelte's writable computed): the
|
|
14
|
+
value still derives from upstream, but assigning `.value` runs `set`, whose job
|
|
15
|
+
is to write *through* to the upstream sources. The write retriggers those
|
|
16
|
+
sources, marking this computed dirty, so the next read recomputes — there is no
|
|
17
|
+
local store, upstream stays the single source of truth. `set` is imperative
|
|
18
|
+
(void): it writes an external target, unlike `state`/`linked`'s `transform`,
|
|
19
|
+
which returns a value into their own store.
|
|
11
20
|
*/
|
|
12
21
|
// @readme plumbing
|
|
13
|
-
export function derived<T>(compute: () => T): Derived<T>
|
|
22
|
+
export function derived<T>(compute: () => T): Derived<T>
|
|
23
|
+
export function derived<T>(compute: () => T, set: (next: T) => void): State<T>
|
|
24
|
+
export function derived<T>(compute: () => T, set?: (next: T) => void): Derived<T> | State<T> {
|
|
14
25
|
const node = createComputedNode(compute as () => unknown)
|
|
15
26
|
/* Tear down with the enclosing scope, the way an effect does. A computed only
|
|
16
27
|
unlinks from its sources when it re-runs (`runNode` re-tracking); one read
|
|
@@ -20,9 +31,19 @@ export function derived<T>(compute: () => T): Derived<T> {
|
|
|
20
31
|
if (OWNER.current !== undefined) {
|
|
21
32
|
OWNER.current.push(() => unlinkDeps(node))
|
|
22
33
|
}
|
|
34
|
+
if (set === undefined) {
|
|
35
|
+
return {
|
|
36
|
+
get value(): T {
|
|
37
|
+
return readNode(node) as T
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
}
|
|
23
41
|
return {
|
|
24
42
|
get value(): T {
|
|
25
43
|
return readNode(node) as T
|
|
26
44
|
},
|
|
45
|
+
set value(next: T) {
|
|
46
|
+
set(next)
|
|
47
|
+
},
|
|
27
48
|
}
|
|
28
49
|
}
|
|
@@ -3,6 +3,7 @@ import { claimChild } from '../runtime/claimChild.ts'
|
|
|
3
3
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
4
|
import { RESUME } from '../runtime/RESUME.ts'
|
|
5
5
|
import { scope } from '../runtime/scope.ts'
|
|
6
|
+
import { discardBoundary } from './discardBoundary.ts'
|
|
6
7
|
|
|
7
8
|
/*
|
|
8
9
|
Async binding — the runtime for `<template await>`. Renders the pending branch,
|
|
@@ -198,27 +199,3 @@ export function awaitBlock(
|
|
|
198
199
|
function isThenable(value: unknown): value is Promise<unknown> {
|
|
199
200
|
return value !== null && typeof (value as { then?: unknown })?.then === 'function'
|
|
200
201
|
}
|
|
201
|
-
|
|
202
|
-
/* Remove the SSR boundary — open marker through close marker (inclusive) — and
|
|
203
|
-
park the hydration cursor on the node after it, so a fresh run replaces it
|
|
204
|
-
without duplicating the server's pending shell. */
|
|
205
|
-
function discardBoundary(
|
|
206
|
-
parent: Node,
|
|
207
|
-
open: Node | null,
|
|
208
|
-
closeData: string,
|
|
209
|
-
hydration: NonNullable<(typeof RENDER)['hydration']>,
|
|
210
|
-
): void {
|
|
211
|
-
let node = open
|
|
212
|
-
let after: Node | null = null
|
|
213
|
-
while (node !== null) {
|
|
214
|
-
const next = node.nextSibling
|
|
215
|
-
const isClose = (node as { data?: string }).data === closeData
|
|
216
|
-
parent.removeChild(node)
|
|
217
|
-
if (isClose) {
|
|
218
|
-
after = next
|
|
219
|
-
break
|
|
220
|
-
}
|
|
221
|
-
node = next
|
|
222
|
-
}
|
|
223
|
-
hydration.next.set(parent, after)
|
|
224
|
-
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RENDER } from '../runtime/RENDER.ts'
|
|
2
|
+
|
|
3
|
+
/* Remove an SSR boundary — open marker through close marker (inclusive) — and park
|
|
4
|
+
the hydration cursor on the node after it, returning that node. A fresh run then
|
|
5
|
+
replaces the boundary in place without duplicating the server's pending shell.
|
|
6
|
+
Shared by the await and try blocks (await ignores the return). */
|
|
7
|
+
export function discardBoundary(
|
|
8
|
+
parent: Node,
|
|
9
|
+
open: Node | null,
|
|
10
|
+
closeData: string,
|
|
11
|
+
hydration: NonNullable<(typeof RENDER)['hydration']>,
|
|
12
|
+
): Node | null {
|
|
13
|
+
let node = open
|
|
14
|
+
let after: Node | null = null
|
|
15
|
+
while (node !== null) {
|
|
16
|
+
const next = node.nextSibling
|
|
17
|
+
const isClose = (node as { data?: string }).data === closeData
|
|
18
|
+
parent.removeChild(node)
|
|
19
|
+
if (isClose) {
|
|
20
|
+
after = next
|
|
21
|
+
break
|
|
22
|
+
}
|
|
23
|
+
node = next
|
|
24
|
+
}
|
|
25
|
+
hydration.next.set(parent, after)
|
|
26
|
+
return after
|
|
27
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
2
2
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
3
3
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
4
|
+
import { discardBoundary } from './discardBoundary.ts'
|
|
4
5
|
|
|
5
6
|
/*
|
|
6
7
|
Synchronous error boundary — the runtime for `<template try>`. Builds the guarded
|
|
@@ -26,7 +27,12 @@ export function tryBlock(
|
|
|
26
27
|
renderCatch?: (parent: Node, error: unknown) => Node[],
|
|
27
28
|
): void {
|
|
28
29
|
/* Run a build under a fresh ownership scope; on throw, tear down the partial
|
|
29
|
-
effects/listeners it registered and rethrow so the caller can fall back.
|
|
30
|
+
effects/listeners it registered and rethrow so the caller can fall back.
|
|
31
|
+
Deliberately not `scope()`: that returns a deferred disposer and leaks the
|
|
32
|
+
partial scope on throw (it only restores the owner), whereas an error boundary
|
|
33
|
+
must dispose eagerly when the guarded build throws and hand back the built
|
|
34
|
+
`Node[]` (not a disposer) on success — those nodes belong to the enclosing
|
|
35
|
+
scope. Different return type, different throw semantics; merging would be wrong. */
|
|
30
36
|
const buildScoped = (build: () => Node[]): Node[] => {
|
|
31
37
|
const previous = OWNER.current
|
|
32
38
|
const disposers: Array<() => void> = []
|
|
@@ -85,28 +91,3 @@ export function tryBlock(
|
|
|
85
91
|
parent.appendChild(node)
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
|
-
|
|
89
|
-
/* Remove the SSR boundary — open marker through close marker (inclusive) — and
|
|
90
|
-
park the hydration cursor on the node after it, returning that node so a fresh
|
|
91
|
-
catch can be inserted in the boundary's place. */
|
|
92
|
-
function discardBoundary(
|
|
93
|
-
parent: Node,
|
|
94
|
-
open: Node | null,
|
|
95
|
-
closeData: string,
|
|
96
|
-
hydration: NonNullable<(typeof RENDER)['hydration']>,
|
|
97
|
-
): Node | null {
|
|
98
|
-
let node = open
|
|
99
|
-
let after: Node | null = null
|
|
100
|
-
while (node !== null) {
|
|
101
|
-
const next = node.nextSibling
|
|
102
|
-
const isClose = (node as { data?: string }).data === closeData
|
|
103
|
-
parent.removeChild(node)
|
|
104
|
-
if (isClose) {
|
|
105
|
-
after = next
|
|
106
|
-
break
|
|
107
|
-
}
|
|
108
|
-
node = next
|
|
109
|
-
}
|
|
110
|
-
hydration.next.set(parent, after)
|
|
111
|
-
return after
|
|
112
|
-
}
|
|
@@ -21,6 +21,7 @@ import { switchBlock } from './dom/switchBlock.ts'
|
|
|
21
21
|
import { tryBlock } from './dom/tryBlock.ts'
|
|
22
22
|
import { when } from './dom/when.ts'
|
|
23
23
|
import { effect } from './effect.ts'
|
|
24
|
+
import { linked } from './linked.ts'
|
|
24
25
|
import { enterRenderPass } from './runtime/enterRenderPass.ts'
|
|
25
26
|
import { exitRenderPass } from './runtime/exitRenderPass.ts'
|
|
26
27
|
import { hotReloadEnabled } from './runtime/hotReloadEnabled.ts'
|
|
@@ -44,6 +45,7 @@ export function installHotBridge(): void {
|
|
|
44
45
|
snippet,
|
|
45
46
|
doc,
|
|
46
47
|
state,
|
|
48
|
+
linked,
|
|
47
49
|
derived,
|
|
48
50
|
effect,
|
|
49
51
|
mount,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createEffectNode } from './runtime/createEffectNode.ts'
|
|
2
|
+
import type { State } from './runtime/types/State.ts'
|
|
3
|
+
import { state } from './state.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
A writable cell seeded reactively from upstream — the abide form of Angular's
|
|
7
|
+
`linkedSignal`. Like `state` it owns a local value and can diverge from its
|
|
8
|
+
source (an edit-form draft, a local working copy); unlike `state` the seed is a
|
|
9
|
+
reactive thunk, so the cell reseeds whenever the thunk's dependencies change.
|
|
10
|
+
Between reseeds it holds whatever was written to it, and edits never flow upstream
|
|
11
|
+
(the seed reads, it does not write). The thunk is mandatory: it *is* the
|
|
12
|
+
reactivity — a bare value can never reseed, which would just make this `state`.
|
|
13
|
+
|
|
14
|
+
`transform` is the same coercion gate as on `state`, and it runs on *every* value
|
|
15
|
+
entering the store — explicit `.value =` writes and reseeds alike — so the store
|
|
16
|
+
never holds an un-coerced value (`return previous` rejects via the `Object.is`
|
|
17
|
+
no-op). The seed is captured by reference: callers clone in the thunk
|
|
18
|
+
(`linked(() => structuredClone(x))`) when they want isolation.
|
|
19
|
+
*/
|
|
20
|
+
// @readme plumbing
|
|
21
|
+
export function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T): State<T> {
|
|
22
|
+
/* The cell is a plain `state` — same store, same write path, so `transform` gates
|
|
23
|
+
reseeds and explicit writes identically. */
|
|
24
|
+
const cell = state<T>(undefined as T, transform)
|
|
25
|
+
/* Reactive reseed: the effect tracks the seed thunk and writes the cell when its
|
|
26
|
+
sources change. The cell is only written (its setter reads the store as a plain
|
|
27
|
+
field, never `readNode`), so it stays off the effect's dependency list — only
|
|
28
|
+
what `seed` reads can retrigger it. `createEffectNode` registers the disposer
|
|
29
|
+
with the enclosing scope. */
|
|
30
|
+
createEffectNode(() => {
|
|
31
|
+
cell.value = seed()
|
|
32
|
+
})
|
|
33
|
+
return cell
|
|
34
|
+
}
|
package/src/lib/ui/router.ts
CHANGED
|
@@ -240,7 +240,7 @@ export function router(
|
|
|
240
240
|
}
|
|
241
241
|
/* Publish the active page so the `page` proxy resolves route/params/url. */
|
|
242
242
|
clientPage.value = {
|
|
243
|
-
route:
|
|
243
|
+
route: chainRoute,
|
|
244
244
|
params,
|
|
245
245
|
url:
|
|
246
246
|
typeof location === 'undefined'
|
|
@@ -264,12 +264,29 @@ export function router(
|
|
|
264
264
|
const hydrating =
|
|
265
265
|
first && pageView?.hydratable === true && pageView.hydrate !== undefined
|
|
266
266
|
first = false
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
base
|
|
267
|
+
/* The DOM mutation a navigation makes: tear the divergent chain down,
|
|
268
|
+
clear its DOM (a fresh mount; hydration adopts in place), rebuild. */
|
|
269
|
+
const swap = (): void => {
|
|
270
|
+
const base = disposeFrom(divergence)
|
|
271
|
+
if (!hydrating) {
|
|
272
|
+
base.textContent = ''
|
|
273
|
+
}
|
|
274
|
+
buildFrom(base, divergence, chainKeys, layoutViews, pageView, params, hydrating)
|
|
275
|
+
}
|
|
276
|
+
/* Wrap the swap in a View Transition where the browser supports it, so
|
|
277
|
+
the page change cross-fades (and shared `view-transition-name` elements
|
|
278
|
+
morph) — the synchronous swap is exactly the mutation the API snapshots
|
|
279
|
+
around. Skipped while hydrating: the first paint adopts SSR DOM in place,
|
|
280
|
+
not animate. CSS owns opting out (e.g. prefers-reduced-motion). */
|
|
281
|
+
if (
|
|
282
|
+
!hydrating &&
|
|
283
|
+
typeof document !== 'undefined' &&
|
|
284
|
+
'startViewTransition' in document
|
|
285
|
+
) {
|
|
286
|
+
document.startViewTransition(swap)
|
|
287
|
+
} else {
|
|
288
|
+
swap()
|
|
271
289
|
}
|
|
272
|
-
buildFrom(base, divergence, chainKeys, layoutViews, pageView, params, hydrating)
|
|
273
290
|
})
|
|
274
291
|
})
|
|
275
292
|
})
|
package/src/lib/ui/state.ts
CHANGED
|
@@ -10,16 +10,23 @@ plain getter/setter over a signal node, so a read/write shows up as exactly that
|
|
|
10
10
|
in a stack trace. The compiler's job (later) is only to auto-deref `{cell}` in
|
|
11
11
|
templates and tag this declaration as a serializable manifest slot; the runtime
|
|
12
12
|
needs no magic.
|
|
13
|
+
|
|
14
|
+
`transform` is an optional coercion gate on the write path: every `.value =`
|
|
15
|
+
runs it and stores what it returns, with `previous` for clamp-relative writes or
|
|
16
|
+
rejection (`return previous` is an `Object.is` no-op). It is the local-truth
|
|
17
|
+
mirror of `derived`'s write-through `set` — here the value lives in this cell, so
|
|
18
|
+
the gate *returns* what to store rather than writing an external target. The
|
|
19
|
+
construction `initial` is taken verbatim; the gate runs on writes only.
|
|
13
20
|
*/
|
|
14
21
|
// @readme plumbing
|
|
15
|
-
export function state<T>(initial: T): State<T> {
|
|
22
|
+
export function state<T>(initial: T, transform?: (next: T, previous: T) => T): State<T> {
|
|
16
23
|
const node = createSignalNode(initial)
|
|
17
24
|
return {
|
|
18
25
|
get value(): T {
|
|
19
26
|
return readNode(node) as T
|
|
20
27
|
},
|
|
21
28
|
set value(next: T) {
|
|
22
|
-
writeNode(node, next)
|
|
29
|
+
writeNode(node, transform === undefined ? next : transform(next, node.value as T))
|
|
23
30
|
},
|
|
24
31
|
}
|
|
25
32
|
}
|
|
@@ -10,8 +10,8 @@ replayed on the client during hydration with no second fetch. A `then` *child*
|
|
|
10
10
|
instead would stream the resolution in out of order.
|
|
11
11
|
*/
|
|
12
12
|
import { cache } from '@abide/abide/shared/cache'
|
|
13
|
-
import Layout from '../Layout.abide'
|
|
14
13
|
import { getHello } from '$server/rpc/getHello.ts'
|
|
14
|
+
import Layout from '../Layout.abide'
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<Layout>
|