@abide/abide 0.33.1 → 0.34.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 +39 -34
- package/CHANGELOG.md +30 -0
- package/README.md +21 -21
- package/package.json +5 -5
- package/src/abideResolverPlugin.ts +1 -1
- package/src/build.ts +1 -1
- package/src/compile.ts +1 -1
- package/src/lib/bundle/BundleMenu.ts +1 -1
- package/src/lib/bundle/BundleMenuItem.ts +1 -1
- package/src/lib/bundle/BundleWindow.ts +1 -1
- package/src/lib/bundle/bundled.ts +1 -1
- package/src/lib/bundle/disconnected.abide +105 -75
- package/src/lib/bundle/onMenu.ts +1 -1
- package/src/lib/mcp/createMcpServer.ts +1 -1
- package/src/lib/server/AppModule.ts +1 -1
- package/src/lib/server/DELETE.ts +1 -1
- package/src/lib/server/GET.ts +1 -1
- package/src/lib/server/HEAD.ts +1 -1
- package/src/lib/server/PATCH.ts +1 -1
- package/src/lib/server/POST.ts +1 -1
- package/src/lib/server/PUT.ts +1 -1
- package/src/lib/server/agent.ts +1 -1
- package/src/lib/server/appDataDir.ts +1 -1
- package/src/lib/server/cookies.ts +1 -1
- package/src/lib/server/env.ts +1 -1
- package/src/lib/server/error.ts +1 -1
- package/src/lib/server/json.ts +1 -1
- package/src/lib/server/jsonl.ts +1 -1
- package/src/lib/server/prompts/definePrompt.ts +1 -1
- package/src/lib/server/prompts/renderPromptTemplate.ts +1 -1
- package/src/lib/server/reachable.ts +1 -1
- package/src/lib/server/redirect.ts +1 -1
- package/src/lib/server/request.ts +1 -1
- package/src/lib/server/rpc/defineVerb.ts +1 -1
- package/src/lib/server/runtime/buildCacheSnapshot.ts +1 -1
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +2 -2
- package/src/lib/server/runtime/types/InspectorContext.ts +2 -2
- package/src/lib/server/server.ts +1 -1
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/defineSocket.ts +1 -1
- package/src/lib/server/sse.ts +1 -1
- package/src/lib/shared/HttpError.ts +1 -1
- package/src/lib/shared/cache.ts +18 -18
- package/src/lib/shared/createSubscriber.ts +1 -1
- package/src/lib/shared/health.ts +1 -1
- package/src/lib/shared/html.ts +1 -1
- package/src/lib/shared/isSubscribable.ts +1 -1
- package/src/lib/shared/log.ts +1 -1
- package/src/lib/shared/online.ts +1 -1
- package/src/lib/shared/page.ts +1 -1
- package/src/lib/shared/pending.ts +2 -2
- package/src/lib/shared/probeRegistries.ts +2 -2
- package/src/lib/shared/refreshing.ts +2 -2
- package/src/lib/shared/selectorMatcher.ts +8 -8
- package/src/lib/shared/selectorPrefix.ts +1 -1
- package/src/lib/shared/snippet.ts +1 -1
- package/src/lib/shared/toTagSet.ts +4 -0
- package/src/lib/shared/trace.ts +1 -1
- package/src/lib/shared/types/CacheEntry.ts +3 -3
- package/src/lib/shared/types/CacheOptions.ts +3 -3
- package/src/lib/shared/types/CacheSelector.ts +2 -2
- package/src/lib/shared/url.ts +1 -1
- package/src/lib/shared/withJsonSchema.ts +1 -1
- package/src/lib/test/assertAgentFrameConformance.ts +1 -1
- package/src/lib/test/createScriptedSurface.ts +1 -1
- package/src/lib/test/createTestApp.ts +1 -1
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +7 -2
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +3 -4
- package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
- package/src/lib/ui/compile/analyzeComponent.ts +2 -1
- package/src/lib/ui/compile/compileComponent.ts +3 -3
- package/src/lib/ui/compile/compileModule.ts +30 -2
- package/src/lib/ui/compile/compileSSR.ts +5 -5
- package/src/lib/ui/compile/compileShadow.ts +62 -37
- package/src/lib/ui/compile/desugarSignals.ts +147 -18
- package/src/lib/ui/compile/generateBuild.ts +15 -28
- package/src/lib/ui/compile/generateSSR.ts +20 -13
- package/src/lib/ui/compile/isControlFlow.ts +18 -0
- package/src/lib/ui/compile/isTextLeaf.ts +15 -0
- package/src/lib/ui/compile/lowerContext.ts +45 -5
- package/src/lib/ui/compile/prepareNestedScript.ts +12 -8
- package/src/lib/ui/compile/renameSignalRefs.ts +21 -4
- package/src/lib/ui/compile/skeletonable.ts +1 -14
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +1 -0
- package/src/lib/ui/computed.ts +32 -0
- package/src/lib/ui/createScope.ts +91 -0
- package/src/lib/ui/dom/anchorCursor.ts +1 -1
- package/src/lib/ui/dom/appendSnippet.ts +1 -1
- package/src/lib/ui/dom/appendStatic.ts +1 -1
- package/src/lib/ui/dom/appendText.ts +1 -1
- package/src/lib/ui/dom/appendTextAt.ts +1 -1
- package/src/lib/ui/dom/applyResolved.ts +1 -1
- package/src/lib/ui/dom/attach.ts +7 -2
- package/src/lib/ui/dom/attr.ts +1 -1
- package/src/lib/ui/dom/awaitBlock.ts +1 -1
- package/src/lib/ui/dom/cloneStatic.ts +1 -1
- package/src/lib/ui/dom/each.ts +4 -3
- package/src/lib/ui/dom/eachAsync.ts +1 -1
- package/src/lib/ui/dom/hydrate.ts +20 -6
- package/src/lib/ui/dom/mount.ts +12 -1
- package/src/lib/ui/dom/mountChild.ts +6 -2
- package/src/lib/ui/dom/mountSlot.ts +1 -1
- package/src/lib/ui/dom/on.ts +10 -4
- package/src/lib/ui/dom/openMarker.ts +3 -3
- package/src/lib/ui/dom/skeleton.ts +13 -5
- package/src/lib/ui/dom/switchBlock.ts +1 -1
- package/src/lib/ui/dom/text.ts +1 -1
- package/src/lib/ui/dom/tryBlock.ts +1 -1
- package/src/lib/ui/dom/when.ts +1 -1
- package/src/lib/ui/effect.ts +7 -2
- package/src/lib/ui/enterScope.ts +18 -0
- package/src/lib/ui/exitScope.ts +8 -0
- package/src/lib/ui/history.ts +102 -0
- package/src/lib/ui/installHotBridge.ts +6 -8
- package/src/lib/ui/linked.ts +0 -1
- package/src/lib/ui/navigate.ts +1 -1
- package/src/lib/ui/outbox.ts +115 -0
- package/src/lib/ui/persist.ts +112 -0
- package/src/lib/ui/probeNavigation.ts +1 -1
- package/src/lib/ui/remoteProxy.ts +1 -1
- package/src/lib/ui/renderToStream.ts +1 -1
- package/src/lib/ui/router.ts +1 -1
- package/src/lib/ui/runtime/CURRENT_SCOPE.ts +10 -0
- package/src/lib/ui/runtime/OUTLET_TAG.ts +1 -1
- package/src/lib/ui/runtime/PATCH_BUS.ts +28 -0
- package/src/lib/ui/runtime/RESUME.ts +1 -1
- package/src/lib/ui/runtime/captureModelDoc.ts +29 -0
- package/src/lib/ui/runtime/claimExpected.ts +26 -0
- package/src/lib/ui/runtime/createComputedNode.ts +1 -1
- package/src/lib/ui/runtime/createDoc.ts +73 -3
- package/src/lib/ui/runtime/enterRenderPass.ts +1 -1
- package/src/lib/ui/runtime/escapeKey.ts +13 -0
- package/src/lib/ui/runtime/exitRenderPass.ts +1 -1
- package/src/lib/ui/runtime/hotReplace.ts +17 -8
- package/src/lib/ui/runtime/inScope.ts +19 -0
- package/src/lib/ui/runtime/localStoragePersistence.ts +34 -0
- package/src/lib/ui/runtime/nextBlockId.ts +1 -1
- package/src/lib/ui/runtime/seedModelDoc.ts +26 -0
- package/src/lib/ui/runtime/types/{Derived.ts → Computed.ts} +1 -1
- package/src/lib/ui/runtime/types/Doc.ts +3 -0
- package/src/lib/ui/runtime/types/HotInstance.ts +6 -2
- package/src/lib/ui/runtime/types/PatchEvent.ts +16 -0
- package/src/lib/ui/runtime/unescapeKey.ts +12 -0
- package/src/lib/ui/runtime/walkPath.ts +3 -1
- package/src/lib/ui/scope.ts +19 -0
- package/src/lib/ui/socketProxy.ts +1 -1
- package/src/lib/ui/startClient.ts +1 -1
- package/src/lib/ui/state.ts +1 -2
- package/src/lib/ui/sync.ts +42 -0
- package/src/lib/ui/tail.ts +1 -1
- package/src/lib/ui/types/History.ts +14 -0
- package/src/lib/ui/types/Outbox.ts +14 -0
- package/src/lib/ui/types/PersistHandle.ts +11 -0
- package/src/lib/ui/types/PersistenceStore.ts +12 -0
- package/src/lib/ui/types/Scope.ts +57 -0
- package/src/lib/ui/types/SyncTransport.ts +13 -0
- package/src/preload.ts +1 -1
- package/src/lib/shared/toScopeSet.ts +0 -4
- package/src/lib/ui/derived.ts +0 -49
- package/src/lib/ui/doc.ts +0 -15
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
An element whose children are ONLY text and `<style>` — the common `<h1>{x}</h1>` shape.
|
|
5
|
+
The shared anchor-placement decision for reactive text: BOTH back-ends consult this so they
|
|
6
|
+
agree on where a text anchor goes. A text-leaf binds its reactive text marker-free on the
|
|
7
|
+
located element (`generateBuild`) / emits it without an `<!--a-->` prefix (`generateSSR`);
|
|
8
|
+
reactive text interleaved with element children is anchor-positioned instead. Computing it
|
|
9
|
+
once keeps the SSR string and the client skeleton from disagreeing about a `<!--a-->` — the
|
|
10
|
+
first slice of lifting the positional model out of the two parallel traversals, alongside
|
|
11
|
+
the already-shared `componentWrapperTag` and `skeletonable`.
|
|
12
|
+
*/
|
|
13
|
+
export function isTextLeaf(node: Extract<TemplateNode, { kind: 'element' }>): boolean {
|
|
14
|
+
return node.children.every((child) => child.kind === 'text' || child.kind === 'style')
|
|
15
|
+
}
|
|
@@ -15,9 +15,13 @@ fix lands here once instead of in lockstep across `generateBuild`/`generateSSR`.
|
|
|
15
15
|
SSR's effect-stripping stays a caller-side wrap (`stripEffects`), the one real
|
|
16
16
|
asymmetry; the node-walk skeletons stay in each back-end.
|
|
17
17
|
*/
|
|
18
|
-
export function lowerContext(
|
|
18
|
+
export function lowerContext(
|
|
19
|
+
stateNames: ReadonlySet<string>,
|
|
20
|
+
derivedNames: ReadonlySet<string>,
|
|
21
|
+
computedNames: ReadonlySet<string> = new Set(),
|
|
22
|
+
) {
|
|
19
23
|
/* Branch-scoped signal bindings (from nested `<script>`s) — they deref to
|
|
20
|
-
`.value` like a `
|
|
24
|
+
`.value` like a `computed`. Pushed while a branch's script + markup compile,
|
|
21
25
|
popped after, so they shadow only within that subtree. */
|
|
22
26
|
const localDerived = new Set<string>()
|
|
23
27
|
const derefScope = (): ReadonlySet<string> =>
|
|
@@ -28,16 +32,52 @@ export function lowerContext(stateNames: ReadonlySet<string>, derivedNames: Read
|
|
|
28
32
|
expression, not a block of labeled statements, through both rewrite passes;
|
|
29
33
|
the wrapper is then peeled back off. */
|
|
30
34
|
function expression(code: string): string {
|
|
31
|
-
const renamed = renameSignalRefs(`(${code})`, stateNames, derefScope())
|
|
35
|
+
const renamed = renameSignalRefs(`(${code})`, stateNames, derefScope(), computedNames)
|
|
32
36
|
return unwrapParens(lowerDocAccess(renamed, 'model').trim().replace(/;$/, ''))
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/* As above but keeps the trailing `;` for a statement/handler body. */
|
|
36
40
|
function statement(code: string): string {
|
|
37
|
-
const renamed = renameSignalRefs(code, stateNames, derefScope())
|
|
41
|
+
const renamed = renameSignalRefs(code, stateNames, derefScope(), computedNames)
|
|
38
42
|
return lowerDocAccess(renamed, 'model').trim()
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
/* A two-way bind target is either an LVALUE (`count`, `model.lines[i]`) — reads as
|
|
46
|
+
itself, writes by assignment — or an ACCESSOR object from `bind:value={{ get, set }}`
|
|
47
|
+
— reads via `.get()`, writes via `.set(v)`. The accessor is the only way to bind a
|
|
48
|
+
value whose write goes somewhere other than the read target (the replacement for the
|
|
49
|
+
old writable `computed(compute, set)` lens). A read-only `computed` is NOT an lvalue,
|
|
50
|
+
so binding one bare is a compile error pointing at the accessor form. */
|
|
51
|
+
const isAccessorBind = (code: string): boolean => code.trim().startsWith('{')
|
|
52
|
+
|
|
53
|
+
function guardWritableBind(code: string): void {
|
|
54
|
+
const name = code.trim()
|
|
55
|
+
if (computedNames.has(name)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`bind: \`${name}\` is a read-only computed — bind a writable target, or an accessor: bind:value={{ get: () => ${name}, set: (next) => { /* write the source */ } }}`,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* The expression a two-way bind reads its current value from. */
|
|
63
|
+
function bindRead(code: string): string {
|
|
64
|
+
if (isAccessorBind(code)) {
|
|
65
|
+
return `(${expression(code)}).get()`
|
|
66
|
+
}
|
|
67
|
+
guardWritableBind(code)
|
|
68
|
+
return expression(code)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* The statement a two-way bind runs to write `valueExpr` (a raw runtime expression,
|
|
72
|
+
e.g. `el.value`) back into its target. */
|
|
73
|
+
function bindWrite(code: string, valueExpr: string): string {
|
|
74
|
+
if (isAccessorBind(code)) {
|
|
75
|
+
return `(${expression(code)}).set(${valueExpr});`
|
|
76
|
+
}
|
|
77
|
+
guardWritableBind(code)
|
|
78
|
+
return statement(`${code} = ${valueExpr}`)
|
|
79
|
+
}
|
|
80
|
+
|
|
41
81
|
/* Adds any `<script>` children's binding names to the deref scope (so the script
|
|
42
82
|
bodies and the branch's markup auto-deref them), runs `body` within that scope,
|
|
43
83
|
then pops the names it added. Returns whatever `body` produces. */
|
|
@@ -60,5 +100,5 @@ export function lowerContext(stateNames: ReadonlySet<string>, derivedNames: Read
|
|
|
60
100
|
return result
|
|
61
101
|
}
|
|
62
102
|
|
|
63
|
-
return { expression, statement, withNestedScripts }
|
|
103
|
+
return { expression, statement, withNestedScripts, bindRead, bindWrite }
|
|
64
104
|
}
|
|
@@ -3,7 +3,7 @@ import { REACTIVE_CALLEES } from './REACTIVE_CALLEES.ts'
|
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
The signal binding names a `<script>` nested in a control-flow branch declares
|
|
6
|
-
(`state`/`linked`/`
|
|
6
|
+
(`state`/`linked`/`computed`/`prop`). The back-end adds them to the deref scope so both the
|
|
7
7
|
script body and the branch's markup rewrite `{a}` → `a.value` — these stay PLAIN
|
|
8
8
|
signals (local to the branch's render, owned by its scope, re-seeded from the
|
|
9
9
|
in-scope data each mount), unlike the top-level component script which desugars to
|
|
@@ -30,15 +30,19 @@ export function nestedBindingNames(code: string): Set<string> {
|
|
|
30
30
|
return names
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/* The callee name of a `NAME = state(...)` / `
|
|
33
|
+
/* The callee name of a `NAME = state(...)` / `computed(...)` / `prop(...)` declaration —
|
|
34
|
+
bare or the explicit scope form (`scope().state(...)` / `c.state(...)`), receiver-agnostic. */
|
|
34
35
|
function signalCallee(declaration: ts.VariableDeclaration): string | undefined {
|
|
35
36
|
const initializer = declaration.initializer
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
) {
|
|
41
|
-
return
|
|
37
|
+
if (initializer === undefined || !ts.isCallExpression(initializer)) {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
const callee = initializer.expression
|
|
41
|
+
if (ts.isIdentifier(callee)) {
|
|
42
|
+
return callee.text
|
|
43
|
+
}
|
|
44
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
45
|
+
return callee.name.text
|
|
42
46
|
}
|
|
43
47
|
return undefined
|
|
44
48
|
}
|
|
@@ -3,7 +3,7 @@ import ts from 'typescript'
|
|
|
3
3
|
/*
|
|
4
4
|
Rewrites references to a component's signal bindings into the document form the
|
|
5
5
|
rest of the pipeline understands: a `state` binding `count` becomes `model.count`
|
|
6
|
-
(data access `lowerDocAccess` then lowers to a patch/read), and a `
|
|
6
|
+
(data access `lowerDocAccess` then lowers to a patch/read), and a `computed`
|
|
7
7
|
binding `total` becomes `total.value`. Only value-position identifiers are
|
|
8
8
|
touched — declaration names, parameter names, and property names are collected
|
|
9
9
|
into a skip set first, and object shorthand (`{ count }`) is expanded to
|
|
@@ -14,6 +14,7 @@ export function renameSignalRefs(
|
|
|
14
14
|
code: string,
|
|
15
15
|
stateNames: ReadonlySet<string>,
|
|
16
16
|
derivedNames: ReadonlySet<string>,
|
|
17
|
+
computedNames: ReadonlySet<string> = new Set(),
|
|
17
18
|
): string {
|
|
18
19
|
const source = ts.createSourceFile('component.ts', code, ts.ScriptTarget.Latest, true)
|
|
19
20
|
|
|
@@ -47,13 +48,23 @@ export function renameSignalRefs(
|
|
|
47
48
|
const visit = (node: ts.Node): ts.Node => {
|
|
48
49
|
/* Shorthand `{ count }` → `{ count: model.count }` / `{ total: total.value }`. */
|
|
49
50
|
if (ts.isShorthandPropertyAssignment(node)) {
|
|
50
|
-
const replacement = referenceFor(
|
|
51
|
+
const replacement = referenceFor(
|
|
52
|
+
node.name.text,
|
|
53
|
+
stateNames,
|
|
54
|
+
derivedNames,
|
|
55
|
+
computedNames,
|
|
56
|
+
)
|
|
51
57
|
if (replacement !== undefined) {
|
|
52
58
|
return ts.factory.createPropertyAssignment(node.name.text, replacement)
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
if (ts.isIdentifier(node) && !skip.has(node)) {
|
|
56
|
-
const replacement = referenceFor(
|
|
62
|
+
const replacement = referenceFor(
|
|
63
|
+
node.text,
|
|
64
|
+
stateNames,
|
|
65
|
+
derivedNames,
|
|
66
|
+
computedNames,
|
|
67
|
+
)
|
|
57
68
|
if (replacement !== undefined) {
|
|
58
69
|
return replacement
|
|
59
70
|
}
|
|
@@ -69,15 +80,21 @@ export function renameSignalRefs(
|
|
|
69
80
|
return output
|
|
70
81
|
}
|
|
71
82
|
|
|
72
|
-
/* `model.<name>` for a state binding, `<name
|
|
83
|
+
/* `model.<name>` for a state binding, `<name>()` for a computed doc-slot (the
|
|
84
|
+
string-free reader `scope().derive` returns), `<name>.value` for a runtime cell
|
|
85
|
+
(linked / lens / transform-state), else undefined. */
|
|
73
86
|
function referenceFor(
|
|
74
87
|
name: string,
|
|
75
88
|
stateNames: ReadonlySet<string>,
|
|
76
89
|
derivedNames: ReadonlySet<string>,
|
|
90
|
+
computedNames: ReadonlySet<string>,
|
|
77
91
|
): ts.Expression | undefined {
|
|
78
92
|
if (stateNames.has(name)) {
|
|
79
93
|
return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('model'), name)
|
|
80
94
|
}
|
|
95
|
+
if (computedNames.has(name)) {
|
|
96
|
+
return ts.factory.createCallExpression(ts.factory.createIdentifier(name), undefined, [])
|
|
97
|
+
}
|
|
81
98
|
if (derivedNames.has(name)) {
|
|
82
99
|
return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), 'value')
|
|
83
100
|
}
|
|
@@ -1,19 +1,6 @@
|
|
|
1
|
+
import { isControlFlow } from './isControlFlow.ts'
|
|
1
2
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
3
|
|
|
3
|
-
/* A control-flow block — `if`/`each`/`await`/`switch`/`try`. In a skeleton it mounts at an
|
|
4
|
-
`<!--a-->` anchor cloned into the located parent at the block's position (see
|
|
5
|
-
`anchorCursor`), so a block can sit ANYWHERE among static siblings — no contiguity or
|
|
6
|
-
prefix-shape constraint. */
|
|
7
|
-
function isControlFlow(node: TemplateNode): boolean {
|
|
8
|
-
return (
|
|
9
|
-
node.kind === 'if' ||
|
|
10
|
-
node.kind === 'each' ||
|
|
11
|
-
node.kind === 'await' ||
|
|
12
|
-
node.kind === 'switch' ||
|
|
13
|
-
node.kind === 'try'
|
|
14
|
-
)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
4
|
/* Whether a subtree is skeleton STRUCTURE — anything the parser-backed clone can carry with
|
|
18
5
|
its holes mounted in place: elements, `<style>`, text (static OR reactive), child
|
|
19
6
|
components (their wrapper element is positioned in the skeleton), control-flow blocks and
|
|
@@ -13,6 +13,7 @@ export type AnalyzedComponent = {
|
|
|
13
13
|
imports: string
|
|
14
14
|
stateNames: Set<string>
|
|
15
15
|
derivedNames: Set<string>
|
|
16
|
+
computedNames: Set<string>
|
|
16
17
|
nodes: TemplateNode[]
|
|
17
18
|
/* One entry per non-empty `<style>` in the template (in source order): the scope
|
|
18
19
|
attribute its covered elements carry (annotated onto `nodes`) and the scoped
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createComputedNode } from './runtime/createComputedNode.ts'
|
|
2
|
+
import { OWNER } from './runtime/OWNER.ts'
|
|
3
|
+
import { readNode } from './runtime/readNode.ts'
|
|
4
|
+
import type { Computed } from './runtime/types/Computed.ts'
|
|
5
|
+
import { unlinkDeps } from './runtime/unlinkDeps.ts'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
A reactive cell computed from other cells — the abide replacement for `$derived`.
|
|
9
|
+
Named `computed` (not `derived`) and ALWAYS read-only: unlike Svelte's reassignable
|
|
10
|
+
`$derived`, a computed is purely a function of its inputs, the way Vue/Angular
|
|
11
|
+
`computed` is. There is no local store and nothing to assign — a write to a computed
|
|
12
|
+
would be a write to its *sources*, which is expressed at the binding site instead
|
|
13
|
+
(`bind:value={{ get, set }}`), where the write actually originates. Lazy: it
|
|
14
|
+
recomputes on read only when a dependency changed, and never serializes (it is
|
|
15
|
+
re-derived from its inputs on resume). Read via `.value`.
|
|
16
|
+
*/
|
|
17
|
+
export function computed<T>(compute: () => T): Computed<T> {
|
|
18
|
+
const node = createComputedNode(compute as () => unknown)
|
|
19
|
+
/* Tear down with the enclosing scope, the way an effect does. A computed only
|
|
20
|
+
unlinks from its sources when it re-runs (`runNode` re-tracking); one read
|
|
21
|
+
once and then abandoned never re-runs, so absent this it would sit in its
|
|
22
|
+
source signals' subscriber lists forever — re-marked dirty on every write to
|
|
23
|
+
them — for the source's lifetime. Outside a scope it owns its own life (no-op). */
|
|
24
|
+
if (OWNER.current !== undefined) {
|
|
25
|
+
OWNER.current.push(() => unlinkDeps(node))
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
get value(): T {
|
|
29
|
+
return readNode(node) as T
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { computed } from './computed.ts'
|
|
2
|
+
import { history } from './history.ts'
|
|
3
|
+
import { linked } from './linked.ts'
|
|
4
|
+
import { persist as persistDoc } from './persist.ts'
|
|
5
|
+
import { createDoc } from './runtime/createDoc.ts'
|
|
6
|
+
import type { Doc } from './runtime/types/Doc.ts'
|
|
7
|
+
import { state } from './state.ts'
|
|
8
|
+
import { sync } from './sync.ts'
|
|
9
|
+
import type { History } from './types/History.ts'
|
|
10
|
+
import type { PersistHandle } from './types/PersistHandle.ts'
|
|
11
|
+
import type { Scope } from './types/Scope.ts'
|
|
12
|
+
|
|
13
|
+
/* A process-stable counter id. The serialization-stable LEXICAL id (route +
|
|
14
|
+
component + tree position) that `persistent`/`sync` want across reloads/peers
|
|
15
|
+
is stamped by the compiler later; until then a scope's id is unique within a
|
|
16
|
+
run — enough for in-session undo and the bus, and as an explicit key's fallback. */
|
|
17
|
+
let nextId = 0
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
Builds a lexical scope. Its data is a document — created eagerly from `initial`,
|
|
21
|
+
or (when `awaiting`) ADOPTED from the first `doc()` a component body creates under
|
|
22
|
+
it, so a scope can wrap the component's own model without changing the data
|
|
23
|
+
lowering. Data methods mirror `Doc` and delegate to that document; capabilities
|
|
24
|
+
are lazy (`undoable`/`persistent` attach `history`/`persist` to it on first call);
|
|
25
|
+
`child` nests; `dispose` tears the subtree down children-first.
|
|
26
|
+
*/
|
|
27
|
+
export function createScope(
|
|
28
|
+
initial: unknown = {},
|
|
29
|
+
parent: Scope | undefined = undefined,
|
|
30
|
+
awaiting = false,
|
|
31
|
+
): Scope {
|
|
32
|
+
/* Eager unless awaiting adoption; `data()` lazily mints an empty doc if a body
|
|
33
|
+
never created one (a stateless component that still reaches for its scope). */
|
|
34
|
+
let document: Doc | undefined = awaiting ? undefined : createDoc(initial)
|
|
35
|
+
const data = (): Doc => (document ??= createDoc({}))
|
|
36
|
+
const id = parent === undefined ? `scope-${nextId++}` : `${parent.id}.${nextId++}`
|
|
37
|
+
const children: Scope[] = []
|
|
38
|
+
let past: History | undefined
|
|
39
|
+
let persistence: PersistHandle | undefined
|
|
40
|
+
let unsync: (() => void) | undefined
|
|
41
|
+
|
|
42
|
+
const self: Scope = {
|
|
43
|
+
id,
|
|
44
|
+
parent,
|
|
45
|
+
read: (path) => data().read(path),
|
|
46
|
+
replace: (path, value) => data().replace(path, value),
|
|
47
|
+
add: (path, value) => data().add(path, value),
|
|
48
|
+
remove: (path) => data().remove(path),
|
|
49
|
+
apply: (patch) => data().apply(patch),
|
|
50
|
+
cell: (path) => data().cell(path),
|
|
51
|
+
derive: (path, compute) => data().derive(path, compute),
|
|
52
|
+
snapshot: () => data().snapshot(),
|
|
53
|
+
/* The `.value`-cell signal forms, namespaced under the scope — standalone
|
|
54
|
+
non-serializing cells (owned by the render scope), reached only here. */
|
|
55
|
+
state,
|
|
56
|
+
linked,
|
|
57
|
+
computed,
|
|
58
|
+
child: (childInitial = {}) => {
|
|
59
|
+
const created = createScope(childInitial, self)
|
|
60
|
+
children.push(created)
|
|
61
|
+
return created
|
|
62
|
+
},
|
|
63
|
+
root: () => (parent === undefined ? self : parent.root()),
|
|
64
|
+
record: (options) => {
|
|
65
|
+
past ??= history(data(), options)
|
|
66
|
+
},
|
|
67
|
+
persist: (key) => {
|
|
68
|
+
persistence ??= persistDoc(data(), key ?? id)
|
|
69
|
+
},
|
|
70
|
+
broadcast: (transport) => {
|
|
71
|
+
unsync ??= sync(data(), transport)
|
|
72
|
+
},
|
|
73
|
+
undo: () => past?.undo(),
|
|
74
|
+
redo: () => past?.redo(),
|
|
75
|
+
canUndo: () => past?.canUndo() ?? false,
|
|
76
|
+
canRedo: () => past?.canRedo() ?? false,
|
|
77
|
+
dispose: () => {
|
|
78
|
+
for (const created of children) {
|
|
79
|
+
created.dispose()
|
|
80
|
+
}
|
|
81
|
+
children.length = 0
|
|
82
|
+
past?.dispose()
|
|
83
|
+
past = undefined
|
|
84
|
+
persistence?.dispose()
|
|
85
|
+
persistence = undefined
|
|
86
|
+
unsync?.()
|
|
87
|
+
unsync = undefined
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
return self
|
|
91
|
+
}
|
|
@@ -13,7 +13,7 @@ the parent's claim cursor there, so the block claims its own server range in pla
|
|
|
13
13
|
block ignores the returned reference on hydrate). `parentNode` is the located element the
|
|
14
14
|
anchor was cloned into.
|
|
15
15
|
*/
|
|
16
|
-
// @
|
|
16
|
+
// @documentation plumbing
|
|
17
17
|
export function anchorCursor(anchor: Node): Node | null {
|
|
18
18
|
const reference = anchor.nextSibling
|
|
19
19
|
const hydration = RENDER.hydration
|
|
@@ -14,7 +14,7 @@ On hydrate the builder runs against the server-rendered nodes between the
|
|
|
14
14
|
claim them in place. The cursor is advanced past the open marker before, and past
|
|
15
15
|
the close marker after, so the markers themselves are skipped.
|
|
16
16
|
*/
|
|
17
|
-
// @
|
|
17
|
+
// @documentation plumbing
|
|
18
18
|
export function appendSnippet(parent: Node, read: () => unknown): void {
|
|
19
19
|
const builder = snippetPayload(read())
|
|
20
20
|
if (typeof builder !== 'function') {
|
|
@@ -7,7 +7,7 @@ mode), or claimed from the server-rendered text (hydrate mode). As with reactive
|
|
|
7
7
|
text, a merged SSR text node is split at this literal's length so the next claim
|
|
8
8
|
lines up; nothing is bound since the text never changes.
|
|
9
9
|
*/
|
|
10
|
-
// @
|
|
10
|
+
// @documentation plumbing
|
|
11
11
|
export function appendStatic(parent: Node, value: string, splitAlways = false): void {
|
|
12
12
|
const hydration = RENDER.hydration
|
|
13
13
|
if (hydration !== undefined) {
|
|
@@ -20,7 +20,7 @@ between `<!--abide:html-->`/`<!--/abide:html-->` markers are adopted (hydrate),
|
|
|
20
20
|
a change re-parses and swaps. A binding is text or raw for its lifetime (decided by
|
|
21
21
|
its first value), so plain text — the common case — stays a cheap single node.
|
|
22
22
|
*/
|
|
23
|
-
// @
|
|
23
|
+
// @documentation plumbing
|
|
24
24
|
export function appendText(parent: Node, read: () => unknown, splitAlways = false): void {
|
|
25
25
|
/* A snippet call (`{row(args)}`) mounts its builder; a `html\`\`` value inserts
|
|
26
26
|
raw markup; everything else is escaped text — decided by the first value. */
|
|
@@ -21,7 +21,7 @@ Hydrate delegates to `appendText` with the cursor temporarily pointed at the anc
|
|
|
21
21
|
content (the server rendered `<!--a-->value`, so the value is the anchor's next sibling),
|
|
22
22
|
reusing its text-split / snippet / raw-html claiming.
|
|
23
23
|
*/
|
|
24
|
-
// @
|
|
24
|
+
// @documentation plumbing
|
|
25
25
|
export function appendTextAt(anchor: Node, read: () => unknown): void {
|
|
26
26
|
const parent = anchor.parentNode as Node
|
|
27
27
|
if (RENDER.hydration !== undefined) {
|
|
@@ -9,7 +9,7 @@ the pending nodes between the markers, and inserts the resolved content in their
|
|
|
9
9
|
place. The pending shell painted instantly; this swaps in each value as it
|
|
10
10
|
arrives — completing the out-of-order streaming loop on the client.
|
|
11
11
|
*/
|
|
12
|
-
// @
|
|
12
|
+
// @documentation plumbing
|
|
13
13
|
export function applyResolved(root: Element, frame: string): void {
|
|
14
14
|
const holder = document.createElement('div')
|
|
15
15
|
holder.innerHTML = frame
|
package/src/lib/ui/dom/attach.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CURRENT_SCOPE } from '../runtime/CURRENT_SCOPE.ts'
|
|
2
|
+
import { inScope } from '../runtime/inScope.ts'
|
|
1
3
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
2
4
|
import { toTeardown } from '../runtime/toTeardown.ts'
|
|
3
5
|
import type { EffectResult } from '../runtime/types/EffectResult.ts'
|
|
@@ -11,10 +13,13 @@ the attachment sets up. Node-lifetime, not reactive — for param reactivity put
|
|
|
11
13
|
async; its teardown then runs once the promise settles. Runtime target for an
|
|
12
14
|
`attach={…}` binding.
|
|
13
15
|
*/
|
|
14
|
-
// @
|
|
16
|
+
// @documentation plumbing
|
|
15
17
|
export function attach(element: Element, attachment: (node: Element) => EffectResult): void {
|
|
18
|
+
/* The attachment body runs now (scope already current); pin the teardown, which
|
|
19
|
+
fires on dispose, so an ambient `scope()` in it resolves the component. */
|
|
20
|
+
const captured = CURRENT_SCOPE.current
|
|
16
21
|
const teardown = toTeardown(attachment(element))
|
|
17
22
|
if (teardown !== undefined && OWNER.current !== undefined) {
|
|
18
|
-
OWNER.current.push(teardown)
|
|
23
|
+
OWNER.current.push(() => inScope(captured, teardown))
|
|
19
24
|
}
|
|
20
25
|
}
|
package/src/lib/ui/dom/attr.ts
CHANGED
|
@@ -6,7 +6,7 @@ and false/null/undefined removes it (the standard present/absent semantics);
|
|
|
6
6
|
anything else is stringified. One effect per bound attribute, so only the
|
|
7
7
|
changed attribute touches the DOM.
|
|
8
8
|
*/
|
|
9
|
-
// @
|
|
9
|
+
// @documentation plumbing
|
|
10
10
|
export function attr(element: Element, name: string, read: () => unknown): void {
|
|
11
11
|
effect(() => {
|
|
12
12
|
const value = read()
|
|
@@ -25,7 +25,7 @@ Hydration adopts in place, by precedence:
|
|
|
25
25
|
After the first (adopting) run, later invalidations swap content before an anchor
|
|
26
26
|
parked just before the close marker.
|
|
27
27
|
*/
|
|
28
|
-
// @
|
|
28
|
+
// @documentation plumbing
|
|
29
29
|
export function awaitBlock(
|
|
30
30
|
parent: Node,
|
|
31
31
|
id: number,
|
|
@@ -17,7 +17,7 @@ claim cursor past the run's top-level nodes (nothing inside binds, so none need
|
|
|
17
17
|
claiming individually). The node count comes from the parsed template, which has
|
|
18
18
|
the same top-level node count as the server markup.
|
|
19
19
|
*/
|
|
20
|
-
// @
|
|
20
|
+
// @documentation plumbing
|
|
21
21
|
export function cloneStatic(parent: Node, html: string): void {
|
|
22
22
|
/* When `parent` is a foreign element built imperatively, the run's markup has no
|
|
23
23
|
foreign ancestor of its own, so a bare `<path>` would parse into the HTML
|
package/src/lib/ui/dom/each.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { effect } from '../effect.ts'
|
|
2
2
|
import { claimChild } from '../runtime/claimChild.ts'
|
|
3
|
+
import { claimExpected } from '../runtime/claimExpected.ts'
|
|
3
4
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
4
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
5
6
|
import { scope } from '../runtime/scope.ts'
|
|
@@ -26,7 +27,7 @@ On hydrate the SSR rows are already in place and in order: each row claims its
|
|
|
26
27
|
markers and content where they sit (no reordering), the anchor parks after them,
|
|
27
28
|
and the first reconcile is skipped.
|
|
28
29
|
*/
|
|
29
|
-
// @
|
|
30
|
+
// @documentation plumbing
|
|
30
31
|
export function each<T>(
|
|
31
32
|
parent: Node,
|
|
32
33
|
items: () => Iterable<T>,
|
|
@@ -44,10 +45,10 @@ export function each<T>(
|
|
|
44
45
|
const buildRow = (item: T): EachRow => {
|
|
45
46
|
const hydration = RENDER.hydration
|
|
46
47
|
if (hydration !== undefined) {
|
|
47
|
-
const start =
|
|
48
|
+
const start = claimExpected(hydration, parent, 'each row start marker')
|
|
48
49
|
hydration.next.set(parent, start.nextSibling)
|
|
49
50
|
const dispose = scope(() => render(parent, item))
|
|
50
|
-
const end =
|
|
51
|
+
const end = claimExpected(hydration, parent, 'each row end marker')
|
|
51
52
|
hydration.next.set(parent, end.nextSibling)
|
|
52
53
|
return { start, end, dispose }
|
|
53
54
|
}
|
|
@@ -21,7 +21,7 @@ On a mid-stream rejection the already-streamed rows stay and the `<template catc
|
|
|
21
21
|
branch (`renderCatch`) renders after them; absent a catch branch the rejection
|
|
22
22
|
surfaces (re-throws), mirroring `<template await>`.
|
|
23
23
|
*/
|
|
24
|
-
// @
|
|
24
|
+
// @documentation plumbing
|
|
25
25
|
export function eachAsync<T>(
|
|
26
26
|
parent: Node,
|
|
27
27
|
items: () => AsyncIterable<T>,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createScope } from '../createScope.ts'
|
|
2
|
+
import { CURRENT_SCOPE } from '../runtime/CURRENT_SCOPE.ts'
|
|
1
3
|
import { enterRenderPass } from '../runtime/enterRenderPass.ts'
|
|
2
4
|
import { exitRenderPass } from '../runtime/exitRenderPass.ts'
|
|
3
5
|
import { RENDER } from '../runtime/RENDER.ts'
|
|
@@ -11,24 +13,36 @@ and reactive effects to the server's markup in place (no re-render, preserved
|
|
|
11
13
|
focus/scroll). Returns a disposer.
|
|
12
14
|
|
|
13
15
|
Adopts the server DOM in place across the framework: static structure (elements
|
|
14
|
-
+ text + bindings), `if`/`else`, keyed `each`, `switch`, and child
|
|
15
|
-
(with slots) — they hydrate automatically because the wrapper is
|
|
16
|
-
hydration is still active.
|
|
17
|
-
|
|
16
|
+
+ text + bindings), `if`/`else`, keyed `each`, `switch`, `try`, and child
|
|
17
|
+
components (with slots) — they hydrate automatically because the wrapper is
|
|
18
|
+
claimed while hydration is still active. `await` adopts too when it can resume
|
|
19
|
+
the value (a streamed `RESUME[id]` or a warm-sync/cache read); only a genuinely-
|
|
20
|
+
pending `await` — no resume, not cache-warm — discards its boundary and builds
|
|
21
|
+
the pending branch fresh (see `awaitBlock`).
|
|
18
22
|
*/
|
|
19
|
-
// @
|
|
23
|
+
// @documentation plumbing
|
|
20
24
|
export function hydrate(host: Element, build: (host: Element) => void): () => void {
|
|
21
25
|
const previous = RENDER.hydration
|
|
22
26
|
RENDER.hydration = { next: new Map() }
|
|
23
27
|
enterRenderPass()
|
|
28
|
+
/* Same lexical scope establishment as `mount` — a hydrated component owns a scope
|
|
29
|
+
too, adopting the model its build adopts. */
|
|
30
|
+
const parentScope = CURRENT_SCOPE.current
|
|
31
|
+
const lexical = createScope({}, parentScope, true)
|
|
32
|
+
CURRENT_SCOPE.current = lexical
|
|
24
33
|
try {
|
|
25
|
-
|
|
34
|
+
const stop = scope(() => {
|
|
26
35
|
try {
|
|
27
36
|
build(host)
|
|
28
37
|
} finally {
|
|
29
38
|
exitRenderPass()
|
|
39
|
+
CURRENT_SCOPE.current = parentScope
|
|
30
40
|
}
|
|
31
41
|
})
|
|
42
|
+
return () => {
|
|
43
|
+
stop()
|
|
44
|
+
lexical.dispose()
|
|
45
|
+
}
|
|
32
46
|
} finally {
|
|
33
47
|
RENDER.hydration = previous
|
|
34
48
|
}
|
package/src/lib/ui/dom/mount.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createScope } from '../createScope.ts'
|
|
2
|
+
import { CURRENT_SCOPE } from '../runtime/CURRENT_SCOPE.ts'
|
|
1
3
|
import { enterRenderPass } from '../runtime/enterRenderPass.ts'
|
|
2
4
|
import { exitRenderPass } from '../runtime/exitRenderPass.ts'
|
|
3
5
|
import { scope } from '../runtime/scope.ts'
|
|
@@ -12,18 +14,27 @@ Brackets a render pass so the outermost mount resets the block-id counter and an
|
|
|
12
14
|
inlined child component's mount continues it — keeping await/try ids aligned with
|
|
13
15
|
the SSR stream (see `enterRenderPass`).
|
|
14
16
|
*/
|
|
15
|
-
// @
|
|
17
|
+
// @documentation plumbing
|
|
16
18
|
export function mount(host: Element, build: (host: Element) => void): () => void {
|
|
17
19
|
enterRenderPass()
|
|
20
|
+
/* Establish this component's lexical scope, nested under the enclosing one, in
|
|
21
|
+
`awaiting` mode so it adopts the model doc the build's first `doc()` creates.
|
|
22
|
+
`scope()` and its capabilities resolve to it during the build; the previous
|
|
23
|
+
scope is restored after (synchronous build, so the restore is exact). */
|
|
24
|
+
const parentScope = CURRENT_SCOPE.current
|
|
25
|
+
const lexical = createScope({}, parentScope, true)
|
|
26
|
+
CURRENT_SCOPE.current = lexical
|
|
18
27
|
const stop = scope(() => {
|
|
19
28
|
try {
|
|
20
29
|
build(host)
|
|
21
30
|
} finally {
|
|
22
31
|
exitRenderPass()
|
|
32
|
+
CURRENT_SCOPE.current = parentScope
|
|
23
33
|
}
|
|
24
34
|
})
|
|
25
35
|
return () => {
|
|
26
36
|
stop()
|
|
37
|
+
lexical.dispose()
|
|
27
38
|
host.textContent = ''
|
|
28
39
|
}
|
|
29
40
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureModelDoc } from '../runtime/captureModelDoc.ts'
|
|
1
2
|
import { hotReloadEnabled } from '../runtime/hotReloadEnabled.ts'
|
|
2
3
|
import { OWNER } from '../runtime/OWNER.ts'
|
|
3
4
|
import { registerHotInstance } from '../runtime/registerHotInstance.ts'
|
|
@@ -13,7 +14,7 @@ together when the parent (or branch/row) tears down. The factory already mounts
|
|
|
13
14
|
under its own scope (see `mount`), so the recorded disposer tears down just this
|
|
14
15
|
instance.
|
|
15
16
|
*/
|
|
16
|
-
// @
|
|
17
|
+
// @documentation plumbing
|
|
17
18
|
export function mountChild(
|
|
18
19
|
host: Element,
|
|
19
20
|
factory: UiComponent,
|
|
@@ -24,7 +25,10 @@ export function mountChild(
|
|
|
24
25
|
factory(host, props)
|
|
25
26
|
return
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
/* Capture the component's model alongside its disposer, so a later swap can carry
|
|
29
|
+
its state across (see `hotReplace`). */
|
|
30
|
+
const { dispose, model } = captureModelDoc(() => factory(host, props))
|
|
31
|
+
const instance = { host, factory, props, dispose, model }
|
|
28
32
|
const remove = registerHotInstance(moduleId, instance)
|
|
29
33
|
OWNER.current?.push(() => {
|
|
30
34
|
instance.dispose()
|
|
@@ -14,7 +14,7 @@ claiming.
|
|
|
14
14
|
Create fills the range before the end marker; hydrate adopts the server range in place
|
|
15
15
|
(claiming from the parked cursor). Mirrors `when` without the conditional swap.
|
|
16
16
|
*/
|
|
17
|
-
// @
|
|
17
|
+
// @documentation plumbing
|
|
18
18
|
export function mountSlot(
|
|
19
19
|
parent: Node,
|
|
20
20
|
render: (host: Node) => void,
|