@abide/abide 0.30.0 → 0.31.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.
Files changed (44) hide show
  1. package/AGENTS.md +4 -3
  2. package/CHANGELOG.md +26 -0
  3. package/package.json +2 -1
  4. package/src/lib/bundle/disconnected.abide +82 -82
  5. package/src/lib/cli/dispatchCommand.ts +3 -2
  6. package/src/lib/cli/resolveCliTarget.ts +2 -3
  7. package/src/lib/cli/runCli.ts +2 -3
  8. package/src/lib/cli/runSession.ts +2 -3
  9. package/src/lib/mcp/dispatchMcpRequest.ts +3 -2
  10. package/src/lib/mcp/mcpSurface.ts +2 -1
  11. package/src/lib/mcp/toolResultFromResponse.ts +2 -1
  12. package/src/lib/server/rpc/parseArgs.ts +1 -3
  13. package/src/lib/server/runtime/streamFromIterator.ts +3 -1
  14. package/src/lib/server/runtime/warnUnguardedMcp.ts +4 -3
  15. package/src/lib/server/sockets/createSocketDispatcher.ts +5 -7
  16. package/src/lib/shared/cacheEntryFromSnapshot.ts +2 -1
  17. package/src/lib/shared/contentTypeOf.ts +6 -0
  18. package/src/lib/shared/decodeResponse.ts +2 -1
  19. package/src/lib/shared/isCompileTarget.ts +7 -1
  20. package/src/lib/shared/isModuleNotFound.ts +3 -1
  21. package/src/lib/shared/isStreamingResponse.ts +2 -1
  22. package/src/lib/shared/messageFromError.ts +6 -0
  23. package/src/lib/shared/streamResponse.ts +2 -1
  24. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +7 -0
  25. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +1 -0
  26. package/src/lib/ui/compile/VOID_TAGS.ts +4 -3
  27. package/src/lib/ui/compile/compileComponent.ts +12 -2
  28. package/src/lib/ui/compile/compileModule.ts +7 -4
  29. package/src/lib/ui/compile/compileSSR.ts +11 -2
  30. package/src/lib/ui/compile/compileShadow.ts +146 -49
  31. package/src/lib/ui/compile/createShadowLanguageService.ts +2 -1
  32. package/src/lib/ui/compile/createShadowProgram.ts +2 -1
  33. package/src/lib/ui/compile/desugarSignals.ts +41 -14
  34. package/src/lib/ui/compile/parseTemplate.ts +21 -26
  35. package/src/lib/ui/compile/prepareNestedScript.ts +4 -2
  36. package/src/lib/ui/derived.ts +25 -4
  37. package/src/lib/ui/dom/awaitBlock.ts +1 -24
  38. package/src/lib/ui/dom/discardBoundary.ts +27 -0
  39. package/src/lib/ui/dom/tryBlock.ts +7 -26
  40. package/src/lib/ui/installHotBridge.ts +2 -0
  41. package/src/lib/ui/linked.ts +34 -0
  42. package/src/lib/ui/router.ts +1 -1
  43. package/src/lib/ui/state.ts +9 -2
  44. package/template/src/ui/pages/page.abide +1 -1
@@ -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 read-only reactive cell computed from other cells — the abide replacement for
9
- `$derived`. Lazy: it recomputes on read only when a dependency has changed, and
10
- never serializes (it is re-derived from its inputs on resume). Read via `.value`.
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
+ }
@@ -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: matched?.route ?? pathname,
243
+ route: chainRoute,
244
244
  params,
245
245
  url:
246
246
  typeof location === 'undefined'
@@ -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>