@abide/abide 0.46.0 → 0.47.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 +365 -320
- package/CHANGELOG.md +80 -0
- package/README.md +203 -168
- package/package.json +12 -8
- package/src/abideLsp.ts +11 -12
- package/src/abideResolverPlugin.ts +9 -4
- package/src/lib/bundle/disconnected.abide +3 -0
- package/src/lib/cli/printCommandHelp.ts +43 -0
- package/src/lib/cli/printSessionHelp.ts +2 -1
- package/src/lib/cli/{printHelp.ts → printTopLevelHelp.ts} +3 -40
- package/src/lib/cli/runCli.ts +2 -1
- package/src/lib/mcp/buildPrompts.ts +25 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +8 -6
- package/src/lib/mcp/mcpResourceServerSlot.ts +5 -10
- package/src/lib/mcp/mcpSurface.ts +13 -252
- package/src/lib/mcp/mcpTools.ts +187 -0
- package/src/lib/mcp/renderPrompt.ts +25 -0
- package/src/lib/mcp/types/McpSurface.ts +15 -0
- package/src/lib/mcp/types/PromptDescriptor.ts +5 -0
- package/src/lib/mcp/types/PromptMessage.ts +2 -0
- package/src/lib/mcp/types/ToolDescriptor.ts +7 -0
- package/src/lib/mcp/types/ToolResult.ts +2 -0
- package/src/lib/server/agent.ts +1 -1
- package/src/lib/server/jsonl.ts +2 -2
- package/src/lib/server/rpc/defineRpc.ts +5 -0
- package/src/lib/server/runtime/containsTraversal.ts +21 -17
- package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
- package/src/lib/server/runtime/createServer.ts +101 -71
- package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
- package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
- package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
- package/src/lib/server/runtime/finalizeResponse.ts +32 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
- package/src/lib/server/sockets/defineSocket.ts +26 -7
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
- package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
- package/src/lib/server/sse.ts +2 -2
- package/src/lib/shared/DEFER.ts +8 -0
- package/src/lib/shared/activeCacheStore.ts +2 -2
- package/src/lib/shared/activePage.ts +2 -2
- package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
- package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
- package/src/lib/shared/basePath.ts +2 -2
- package/src/lib/shared/baseSlot.ts +6 -5
- package/src/lib/shared/bodyValueForKind.ts +1 -2
- package/src/lib/shared/buildSocketOverChannel.ts +40 -4
- package/src/lib/shared/cache.ts +477 -110
- package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
- package/src/lib/shared/cacheStoreSlot.ts +9 -5
- package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
- package/src/lib/shared/createRemoteFunction.ts +58 -9
- package/src/lib/shared/createResolverSlot.ts +24 -31
- package/src/lib/shared/createSocketSubRegistry.ts +2 -2
- package/src/lib/shared/decodeRefJson.ts +4 -2
- package/src/lib/shared/decodeResponse.ts +8 -6
- package/src/lib/shared/done.ts +14 -0
- package/src/lib/shared/encodeRefJson.ts +4 -2
- package/src/lib/shared/globalCacheStoreSlot.ts +9 -6
- package/src/lib/shared/hydratingSlot.ts +12 -0
- package/src/lib/shared/isLayoutFile.ts +12 -0
- package/src/lib/shared/keyForRemoteCall.ts +2 -1
- package/src/lib/shared/keyPrefixForRemote.ts +12 -0
- package/src/lib/shared/matchRoute.ts +175 -0
- package/src/lib/shared/normalizePathname.ts +11 -0
- package/src/lib/shared/pageSlot.ts +14 -5
- package/src/lib/shared/pageUrlForFile.ts +3 -3
- package/src/lib/shared/parseRouteSegments.ts +16 -7
- package/src/lib/shared/patch.ts +41 -0
- package/src/lib/shared/peek.ts +35 -0
- package/src/lib/shared/prepareRemoteExport.ts +40 -0
- package/src/lib/shared/prepareRpcModule.ts +98 -16
- package/src/lib/shared/prepareSocketModule.ts +11 -15
- package/src/lib/shared/queryStringFromArgs.ts +11 -0
- package/src/lib/shared/reachable.ts +102 -0
- package/src/lib/shared/refresh.ts +22 -0
- package/src/lib/shared/requestScopeSlot.ts +10 -7
- package/src/lib/shared/routeParamsShape.ts +9 -9
- package/src/lib/shared/rpcErrorRegistry.ts +69 -0
- package/src/lib/shared/selectorPrefix.ts +3 -10
- package/src/lib/shared/setOwnProperty.ts +19 -0
- package/src/lib/shared/snippet.ts +1 -1
- package/src/lib/shared/subscribableProbes.ts +111 -0
- package/src/lib/shared/tailProbeSlot.ts +8 -1
- package/src/lib/shared/types/CacheEntry.ts +9 -15
- package/src/lib/shared/types/CacheOptions.ts +8 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
- package/src/lib/shared/types/RemoteCallable.ts +25 -7
- package/src/lib/shared/types/RemoteFunction.ts +48 -13
- package/src/lib/shared/types/ResolverSlot.ts +7 -4
- package/src/lib/shared/types/RpcError.ts +26 -0
- package/src/lib/shared/types/SmartReadOptions.ts +32 -0
- package/src/lib/shared/types/Socket.ts +54 -0
- package/src/lib/shared/types/SsrBootState.ts +27 -0
- package/src/lib/shared/types/SsrPayload.ts +21 -0
- package/src/lib/shared/types/Subscribable.ts +13 -13
- package/src/lib/shared/types/TailHooks.ts +2 -1
- package/src/lib/shared/url.ts +29 -14
- package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
- package/src/lib/shared/writeTestSocketsDts.ts +1 -1
- package/src/lib/test/createScriptedSurface.ts +1 -1
- package/src/lib/test/createTestApp.ts +2 -2
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
- package/src/lib/ui/compile/analyzeComponent.ts +28 -6
- package/src/lib/ui/compile/compileModule.ts +137 -11
- package/src/lib/ui/compile/compileShadow.ts +101 -84
- package/src/lib/ui/compile/desugarSignals.ts +120 -107
- package/src/lib/ui/compile/generateBuild.ts +48 -16
- package/src/lib/ui/compile/generateSSR.ts +117 -14
- package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
- package/src/lib/ui/compile/lowerContext.ts +10 -4
- package/src/lib/ui/compile/lowerScript.ts +72 -5
- package/src/lib/ui/compile/parseTemplate.ts +1 -14
- package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
- package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
- package/src/lib/ui/compile/signalCallee.ts +24 -0
- package/src/lib/ui/compile/stripEffects.ts +14 -11
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +0 -5
- package/src/lib/ui/dom/appendStatic.ts +9 -1
- package/src/lib/ui/dom/appendText.ts +15 -3
- package/src/lib/ui/dom/assertClaimedText.ts +20 -0
- package/src/lib/ui/dom/awaitBlock.ts +19 -83
- package/src/lib/ui/dom/bindSelectValue.ts +48 -0
- package/src/lib/ui/dom/each.ts +12 -1
- package/src/lib/ui/dom/hydrate.ts +10 -0
- package/src/lib/ui/dom/mountChild.ts +2 -5
- package/src/lib/ui/dom/mountRange.ts +0 -75
- package/src/lib/ui/effect.ts +4 -3
- package/src/lib/ui/installHotBridge.ts +4 -2
- package/src/lib/ui/remoteProxy.ts +18 -2
- package/src/lib/ui/renderToStream.ts +9 -13
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/router.ts +21 -2
- package/src/lib/ui/runtime/RESUME.ts +0 -6
- package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
- package/src/lib/ui/scope.ts +6 -3
- package/src/lib/ui/seedBootState.ts +53 -0
- package/src/lib/ui/socketChannel.ts +2 -2
- package/src/lib/ui/socketProxy.ts +9 -3
- package/src/lib/ui/startClient.ts +16 -30
- package/src/lib/ui/state.ts +44 -13
- package/src/lib/ui/sync.ts +11 -5
- package/src/lib/ui/tryEncodeResume.ts +2 -5
- package/src/lib/ui/types/Scope.ts +14 -10
- package/src/lib/ui/watch.ts +140 -0
- package/src/serverEntry.ts +6 -6
- package/template/CLAUDE.md +1 -1
- package/template/package.json +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +2 -2
- package/template/test/app.test.ts +1 -1
- package/src/lib/server/reachable.ts +0 -45
- package/src/lib/server/sockets/types/Socket.ts +0 -23
- package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
- package/src/lib/shared/DEFER_MIN_ARRAY_LENGTH.ts +0 -14
- package/src/lib/shared/baseResolver.ts +0 -10
- package/src/lib/shared/cacheKeyOf.ts +0 -7
- package/src/lib/shared/cacheKeyStore.ts +0 -8
- package/src/lib/shared/cacheStoreResolver.ts +0 -12
- package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
- package/src/lib/shared/pageResolver.ts +0 -16
- package/src/lib/shared/recordCacheKey.ts +0 -6
- package/src/lib/shared/requestScopeResolver.ts +0 -12
- package/src/lib/shared/setBaseResolver.ts +0 -4
- package/src/lib/shared/setCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setPageResolver.ts +0 -4
- package/src/lib/shared/setRequestScopeResolver.ts +0 -4
- package/src/lib/shared/toBunRoutePattern.ts +0 -28
- package/src/lib/shared/types/TailOptions.ts +0 -10
- package/src/lib/ui/deferResume.ts +0 -36
- package/src/lib/ui/dom/firstElementBetween.ts +0 -14
- package/src/lib/ui/matchRoute.ts +0 -106
- package/src/lib/ui/runtime/scheduleWake.ts +0 -28
- package/src/lib/ui/runtime/whenIdle.ts +0 -21
- package/src/lib/ui/runtime/whenVisible.ts +0 -105
- package/src/lib/ui/tail.ts +0 -324
- /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
- /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
|
@@ -1,66 +1,23 @@
|
|
|
1
1
|
import ts from 'typescript'
|
|
2
|
-
import {
|
|
2
|
+
import { type ReactiveImportBindings, reactiveImportBindings } from './resolveReactiveExport.ts'
|
|
3
|
+
import { signalCallee } from './signalCallee.ts'
|
|
3
4
|
|
|
4
5
|
const factory = ts.factory
|
|
5
6
|
|
|
6
|
-
/*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
through to the `effect` helper), not a doc slot. */
|
|
12
|
-
const SCOPE_PRIMITIVES: ReadonlySet<string> = new Set(['state', 'linked', 'computed', 'effect'])
|
|
13
|
-
|
|
14
|
-
/* The primitive names a top-level `const { state, computed } = scope()` destructure binds.
|
|
15
|
-
Such a name is scope-bound — its bare call below is the destructured method, not a stray
|
|
16
|
-
global — so it is exempt from the bare-primitive error. Only a destructure of a `scope()`
|
|
17
|
-
call counts (receiver-agnostic on the callee name, matching signalCallee); an aliased
|
|
18
|
-
binding (`{ state: s }`) is not recognised, so the canonical name must be kept. */
|
|
19
|
-
function scopeDestructuredPrimitives(source: ts.SourceFile): Set<string> {
|
|
20
|
-
const bound = new Set<string>()
|
|
21
|
-
for (const statement of source.statements) {
|
|
22
|
-
if (!ts.isVariableStatement(statement)) {
|
|
23
|
-
continue
|
|
24
|
-
}
|
|
25
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
26
|
-
if (
|
|
27
|
-
signalCallee(declaration) === 'scope' &&
|
|
28
|
-
ts.isObjectBindingPattern(declaration.name)
|
|
29
|
-
) {
|
|
30
|
-
for (const element of declaration.name.elements) {
|
|
31
|
-
if (
|
|
32
|
-
element.propertyName === undefined &&
|
|
33
|
-
ts.isIdentifier(element.name) &&
|
|
34
|
-
SCOPE_PRIMITIVES.has(element.name.text)
|
|
35
|
-
) {
|
|
36
|
-
bound.add(element.name.text)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return bound
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/* Throws on a bare scope primitive (`state(0)` instead of `scope().state(0)`) or on the
|
|
46
|
-
removed `prop(...)` reader — props are now read by destructuring `props()`. Walks all
|
|
47
|
-
calls, so a stray one nested in a function is caught too, not just top-level declarations.
|
|
48
|
-
A primitive destructured from `scope()` at the top is scope-bound and exempt. */
|
|
49
|
-
function assertScopedPrimitives(source: ts.SourceFile): void {
|
|
50
|
-
const scopeBound = scopeDestructuredPrimitives(source)
|
|
7
|
+
/* Throws on the removed `prop(...)` reader — props are now read by destructuring `props()`.
|
|
8
|
+
Walks all calls, so a stray one nested in a function is caught too. Bare reactive
|
|
9
|
+
primitives (`state(0)`) are the SURFACE now (recognised by import binding + lowered), so
|
|
10
|
+
they no longer throw — only the withdrawn `prop` reader does. */
|
|
11
|
+
function assertNoRemovedReaders(source: ts.SourceFile): void {
|
|
51
12
|
const visit = (node: ts.Node): void => {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
throw new Error(
|
|
61
|
-
'abide: `prop(...)` has been removed — read props by destructuring `props()`, e.g. `const { name } = props()` (with a default: `const { name = fallback } = props()`).',
|
|
62
|
-
)
|
|
63
|
-
}
|
|
13
|
+
if (
|
|
14
|
+
ts.isCallExpression(node) &&
|
|
15
|
+
ts.isIdentifier(node.expression) &&
|
|
16
|
+
node.expression.text === 'prop'
|
|
17
|
+
) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'abide: `prop(...)` has been removed — read props by destructuring `props()`, e.g. `const { name } = props()` (with a default: `const { name = fallback } = props()`).',
|
|
20
|
+
)
|
|
64
21
|
}
|
|
65
22
|
ts.forEachChild(node, visit)
|
|
66
23
|
}
|
|
@@ -93,7 +50,11 @@ export function desugarSignals(source: ts.SourceFile): {
|
|
|
93
50
|
derivedNames: Set<string>
|
|
94
51
|
computedNames: Set<string>
|
|
95
52
|
} {
|
|
96
|
-
|
|
53
|
+
assertNoRemovedReaders(source)
|
|
54
|
+
/* The file's reactive import bindings — each local (alias-safe) mapped to its
|
|
55
|
+
canonical primitive. The single recognition authority: every callee below resolves
|
|
56
|
+
against these import bindings and nothing else. */
|
|
57
|
+
const bindings = reactiveImportBindings(source)
|
|
97
58
|
const stateNames = new Set<string>()
|
|
98
59
|
const derivedNames = new Set<string>()
|
|
99
60
|
const computedNames = new Set<string>()
|
|
@@ -105,7 +66,7 @@ export function desugarSignals(source: ts.SourceFile): {
|
|
|
105
66
|
continue
|
|
106
67
|
}
|
|
107
68
|
for (const declaration of statement.declarationList.declarations) {
|
|
108
|
-
const callee = signalCallee(declaration)
|
|
69
|
+
const callee = signalCallee(declaration, bindings)
|
|
109
70
|
if (callee === 'props') {
|
|
110
71
|
/* `const {…, ...rest} = props()` — each named binding is a read-only computed
|
|
111
72
|
over the parent thunk (read as `name()`); a `...rest` binding stays a plain
|
|
@@ -124,15 +85,15 @@ export function desugarSignals(source: ts.SourceFile): {
|
|
|
124
85
|
if (!ts.isIdentifier(declaration.name)) {
|
|
125
86
|
continue
|
|
126
87
|
}
|
|
127
|
-
if (isPlainStateSlot(declaration)) {
|
|
88
|
+
if (isPlainStateSlot(declaration, bindings)) {
|
|
128
89
|
/* Plain `state(initial)` → a serializable `model` doc slot. */
|
|
129
90
|
stateNames.add(declaration.name.text)
|
|
130
|
-
} else if (isComputedSlot(declaration)) {
|
|
91
|
+
} else if (isComputedSlot(declaration, bindings)) {
|
|
131
92
|
/* Read-only `computed(compute)` → a computed `scope().derive` doc slot,
|
|
132
93
|
referenced as `name()` (its string-free reader): a function of other
|
|
133
94
|
paths, recomputed via the graph, never stored/serialized. */
|
|
134
95
|
computedNames.add(declaration.name.text)
|
|
135
|
-
} else if (callee
|
|
96
|
+
} else if (callee === 'linked' || callee === 'state') {
|
|
136
97
|
/* `.value` cells: `linked` and `state(initial, transform)` — they own
|
|
137
98
|
a local store, so they stay cells (`computed` is always the read-only
|
|
138
99
|
slot above; there is no writable-computed cell). */
|
|
@@ -163,7 +124,7 @@ export function desugarSignals(source: ts.SourceFile): {
|
|
|
163
124
|
)
|
|
164
125
|
}
|
|
165
126
|
for (const statement of root.statements) {
|
|
166
|
-
statements.push(...loweredStatement(statement))
|
|
127
|
+
statements.push(...loweredStatement(statement, bindings))
|
|
167
128
|
}
|
|
168
129
|
return factory.updateSourceFile(root, statements)
|
|
169
130
|
}
|
|
@@ -175,13 +136,65 @@ export function desugarSignals(source: ts.SourceFile): {
|
|
|
175
136
|
assignments, computed → `scope().derive` consts, props → derive consts (+ restProps),
|
|
176
137
|
cells → `scope().<callee>(...)` consts; anything else passes through verbatim. The
|
|
177
138
|
per-statement dispatch mirrors the name-collection pass. */
|
|
178
|
-
function loweredStatement(
|
|
139
|
+
function loweredStatement(
|
|
140
|
+
statement: ts.Statement,
|
|
141
|
+
bindings: ReactiveImportBindings,
|
|
142
|
+
): ts.Statement[] {
|
|
143
|
+
rejectMixedDeclaration(statement, bindings)
|
|
179
144
|
return (
|
|
180
|
-
stateAssignmentStatements(statement) ??
|
|
181
|
-
computedStatements(statement) ??
|
|
182
|
-
propsStatements(statement) ??
|
|
183
|
-
cellStatements(statement) ?? [statement]
|
|
145
|
+
stateAssignmentStatements(statement, bindings) ??
|
|
146
|
+
computedStatements(statement, bindings) ??
|
|
147
|
+
propsStatements(statement, bindings) ??
|
|
148
|
+
cellStatements(statement, bindings) ?? [statement]
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Each lowering function above is all-or-nothing per VariableStatement: it returns
|
|
153
|
+
undefined the moment one declaration doesn't match its kind, so the whole statement
|
|
154
|
+
passes through verbatim. A statement mixing a reactive declaration with a differently-
|
|
155
|
+
lowered one (`let count = state(0), step = 5`) therefore ships literal — but the
|
|
156
|
+
collection pass already registered `count`, so references lower to `$$model.read("count")`
|
|
157
|
+
with no `$$model.count = 0` seed (silent undefined). Reject it with a legible error so
|
|
158
|
+
the author splits the declarations; a checker-free compiler must fail loud, not
|
|
159
|
+
mis-lower silently. Same-kind lists (`let a = state(0), b = state(1)`) are fine. */
|
|
160
|
+
function rejectMixedDeclaration(statement: ts.Statement, bindings: ReactiveImportBindings): void {
|
|
161
|
+
if (!ts.isVariableStatement(statement)) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
const kinds = new Set(
|
|
165
|
+
statement.declarationList.declarations.map((declaration) =>
|
|
166
|
+
loweringKind(declaration, bindings),
|
|
167
|
+
),
|
|
184
168
|
)
|
|
169
|
+
/* Any two distinct kinds break: each lowering function bails on the non-matching
|
|
170
|
+
declaration, so the statement ships verbatim. A uniform list (size 1) is fine. */
|
|
171
|
+
if (kinds.size > 1) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'abide: declare each reactive signal in its own statement — a `let`/`const` that mixes a reactive declaration (state/linked/computed/props) with another kind cannot be lowered as one statement.',
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* The lowering bucket a single declaration falls into — mirrors the name-collection
|
|
179
|
+
dispatch so `rejectMixedDeclaration` can detect a statement spanning more than one. */
|
|
180
|
+
function loweringKind(
|
|
181
|
+
declaration: ts.VariableDeclaration,
|
|
182
|
+
bindings: ReactiveImportBindings,
|
|
183
|
+
): 'state' | 'computed' | 'props' | 'cell' | 'plain' {
|
|
184
|
+
const callee = signalCallee(declaration, bindings)
|
|
185
|
+
if (callee === 'props') {
|
|
186
|
+
return 'props'
|
|
187
|
+
}
|
|
188
|
+
if (isPlainStateSlot(declaration, bindings)) {
|
|
189
|
+
return 'state'
|
|
190
|
+
}
|
|
191
|
+
if (isComputedSlot(declaration, bindings)) {
|
|
192
|
+
return 'computed'
|
|
193
|
+
}
|
|
194
|
+
if (callee === 'linked' || callee === 'state') {
|
|
195
|
+
return 'cell'
|
|
196
|
+
}
|
|
197
|
+
return 'plain'
|
|
185
198
|
}
|
|
186
199
|
|
|
187
200
|
/* `const NAME = <init>` — a fresh const declaration reusing the original initializer node. */
|
|
@@ -210,10 +223,13 @@ function scopeMethodCall(method: string, args: readonly ts.Expression[]): ts.Cal
|
|
|
210
223
|
/* True for a read-only computed slot — `computed(compute)` with no write-through
|
|
211
224
|
`set`. The writable `computed(compute, set)` lens keeps a `.value` cell (handled by
|
|
212
225
|
the caller). */
|
|
213
|
-
function isComputedSlot(
|
|
226
|
+
function isComputedSlot(
|
|
227
|
+
declaration: ts.VariableDeclaration,
|
|
228
|
+
bindings: ReactiveImportBindings,
|
|
229
|
+
): boolean {
|
|
214
230
|
const initializer = declaration.initializer
|
|
215
231
|
return (
|
|
216
|
-
signalCallee(declaration) === 'computed' &&
|
|
232
|
+
signalCallee(declaration, bindings) === 'computed' &&
|
|
217
233
|
initializer !== undefined &&
|
|
218
234
|
ts.isCallExpression(initializer) &&
|
|
219
235
|
initializer.arguments.length === 1
|
|
@@ -224,17 +240,20 @@ function isComputedSlot(declaration: ts.VariableDeclaration): boolean {
|
|
|
224
240
|
routed onto the scope: `linked(seed)` → `const x = scope().linked(seed)`. The cell
|
|
225
241
|
stays a standalone non-serializing cell (its refs stay `name.value`); the rewrite only
|
|
226
242
|
removes the bare runtime import so the sole reactive surface is `scope()`. */
|
|
227
|
-
function cellStatements(
|
|
243
|
+
function cellStatements(
|
|
244
|
+
statement: ts.Statement,
|
|
245
|
+
bindings: ReactiveImportBindings,
|
|
246
|
+
): ts.Statement[] | undefined {
|
|
228
247
|
if (!ts.isVariableStatement(statement)) {
|
|
229
248
|
return undefined
|
|
230
249
|
}
|
|
231
250
|
const statements: ts.Statement[] = []
|
|
232
251
|
for (const declaration of statement.declarationList.declarations) {
|
|
233
|
-
const callee = signalCallee(declaration)
|
|
252
|
+
const callee = signalCallee(declaration, bindings)
|
|
234
253
|
if (
|
|
235
254
|
!ts.isIdentifier(declaration.name) ||
|
|
236
255
|
(callee !== 'linked' && callee !== 'state') ||
|
|
237
|
-
isPlainStateSlot(declaration)
|
|
256
|
+
isPlainStateSlot(declaration, bindings)
|
|
238
257
|
) {
|
|
239
258
|
return undefined
|
|
240
259
|
}
|
|
@@ -246,7 +265,10 @@ function cellStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
|
246
265
|
|
|
247
266
|
/* `let total = computed(compute)` → `const total = scope().derive("total", compute)`
|
|
248
267
|
— a computed doc slot whose reader the references lower to `total()`. */
|
|
249
|
-
function computedStatements(
|
|
268
|
+
function computedStatements(
|
|
269
|
+
statement: ts.Statement,
|
|
270
|
+
bindings: ReactiveImportBindings,
|
|
271
|
+
): ts.Statement[] | undefined {
|
|
250
272
|
if (!ts.isVariableStatement(statement)) {
|
|
251
273
|
return undefined
|
|
252
274
|
}
|
|
@@ -254,8 +276,8 @@ function computedStatements(statement: ts.Statement): ts.Statement[] | undefined
|
|
|
254
276
|
for (const declaration of statement.declarationList.declarations) {
|
|
255
277
|
if (
|
|
256
278
|
!ts.isIdentifier(declaration.name) ||
|
|
257
|
-
signalCallee(declaration) !== 'computed' ||
|
|
258
|
-
!isComputedSlot(declaration)
|
|
279
|
+
signalCallee(declaration, bindings) !== 'computed' ||
|
|
280
|
+
!isComputedSlot(declaration, bindings)
|
|
259
281
|
) {
|
|
260
282
|
return undefined
|
|
261
283
|
}
|
|
@@ -294,29 +316,11 @@ function hasTransform(declaration: ts.VariableDeclaration): boolean {
|
|
|
294
316
|
/* A plain `state(initial)` with no transform → a serializable `model` doc slot;
|
|
295
317
|
every other reactive declaration is a `.value` cell. The one rule shared by the
|
|
296
318
|
name-collection pass and the slot lowering. */
|
|
297
|
-
function isPlainStateSlot(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
form (`NAME = state(...)`) and the explicit scope form (`NAME = scope().state(...)` or
|
|
303
|
-
`NAME = c.state(...)` for any scope handle `c`) — receiver-agnostic: the METHOD name is
|
|
304
|
-
what marks the binding reactive. Since `scope()` is the ambient scope (one object per
|
|
305
|
-
level), the receiver is irrelevant to lowering — the slot keys off the binding name and
|
|
306
|
-
lands on the same `model`, so the explicit form lowers exactly like the bare one. */
|
|
307
|
-
function signalCallee(declaration: ts.VariableDeclaration): string | undefined {
|
|
308
|
-
const initializer = declaration.initializer
|
|
309
|
-
if (initializer === undefined || !ts.isCallExpression(initializer)) {
|
|
310
|
-
return undefined
|
|
311
|
-
}
|
|
312
|
-
const callee = initializer.expression
|
|
313
|
-
if (ts.isIdentifier(callee)) {
|
|
314
|
-
return callee.text
|
|
315
|
-
}
|
|
316
|
-
if (ts.isPropertyAccessExpression(callee)) {
|
|
317
|
-
return callee.name.text
|
|
318
|
-
}
|
|
319
|
-
return undefined
|
|
319
|
+
function isPlainStateSlot(
|
|
320
|
+
declaration: ts.VariableDeclaration,
|
|
321
|
+
bindings: ReactiveImportBindings,
|
|
322
|
+
): boolean {
|
|
323
|
+
return signalCallee(declaration, bindings) === 'state' && !hasTransform(declaration)
|
|
320
324
|
}
|
|
321
325
|
|
|
322
326
|
/* One destructured prop: the local binding name, the parent prop key it reads, and
|
|
@@ -375,17 +379,23 @@ function propsBindingKey(element: ts.BindingElement): string {
|
|
|
375
379
|
read as `name()` — plus a `const rest = restProps($props, [consumed])` for a rest
|
|
376
380
|
binding; otherwise undefined. The `?? default` applies the binding's `= default`
|
|
377
381
|
fallback when the prop is absent. */
|
|
378
|
-
function propsStatements(
|
|
382
|
+
function propsStatements(
|
|
383
|
+
statement: ts.Statement,
|
|
384
|
+
bindings: ReactiveImportBindings,
|
|
385
|
+
): ts.Statement[] | undefined {
|
|
379
386
|
if (!ts.isVariableStatement(statement)) {
|
|
380
387
|
return undefined
|
|
381
388
|
}
|
|
382
389
|
const statements: ts.Statement[] = []
|
|
383
390
|
for (const declaration of statement.declarationList.declarations) {
|
|
384
|
-
if (
|
|
391
|
+
if (
|
|
392
|
+
signalCallee(declaration, bindings) !== 'props' ||
|
|
393
|
+
!ts.isObjectBindingPattern(declaration.name)
|
|
394
|
+
) {
|
|
385
395
|
return undefined
|
|
386
396
|
}
|
|
387
|
-
const { bindings, rest } = propsDestructure(declaration)
|
|
388
|
-
for (const { local, key, initializer } of
|
|
397
|
+
const { bindings: propBindings, rest } = propsDestructure(declaration)
|
|
398
|
+
for (const { local, key, initializer } of propBindings) {
|
|
389
399
|
/* `$props["key"]?.()` — the parent thunk, optionally called. */
|
|
390
400
|
const read = factory.createCallChain(
|
|
391
401
|
factory.createElementAccessExpression(
|
|
@@ -422,7 +432,7 @@ function propsStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
|
422
432
|
}
|
|
423
433
|
/* The rest bag gathers every prop not named above (and not `$children`). */
|
|
424
434
|
if (rest !== undefined) {
|
|
425
|
-
const consumed =
|
|
435
|
+
const consumed = propBindings.map((binding) => factory.createStringLiteral(binding.key))
|
|
426
436
|
statements.push(
|
|
427
437
|
constDeclaration(
|
|
428
438
|
rest,
|
|
@@ -443,13 +453,16 @@ function propsStatements(statement: ts.Statement): ts.Statement[] | undefined {
|
|
|
443
453
|
|
|
444
454
|
/* If `statement` declares `state(...)` bindings, returns `model.<name> = <init>`
|
|
445
455
|
assignment statements (one per declaration); otherwise undefined. */
|
|
446
|
-
function stateAssignmentStatements(
|
|
456
|
+
function stateAssignmentStatements(
|
|
457
|
+
statement: ts.Statement,
|
|
458
|
+
bindings: ReactiveImportBindings,
|
|
459
|
+
): ts.Statement[] | undefined {
|
|
447
460
|
if (!ts.isVariableStatement(statement)) {
|
|
448
461
|
return undefined
|
|
449
462
|
}
|
|
450
463
|
const statements: ts.Statement[] = []
|
|
451
464
|
for (const declaration of statement.declarationList.declarations) {
|
|
452
|
-
if (!ts.isIdentifier(declaration.name) || !isPlainStateSlot(declaration)) {
|
|
465
|
+
if (!ts.isIdentifier(declaration.name) || !isPlainStateSlot(declaration, bindings)) {
|
|
453
466
|
/* Only a plain `state(initial)` becomes a slot; `state(initial, transform)`
|
|
454
467
|
(and everything else) is a `.value` cell — pass it through so the
|
|
455
468
|
runtime call (and its transform) survives. */
|
|
@@ -22,6 +22,7 @@ import { skeletonContext } from './skeletonContext.ts'
|
|
|
22
22
|
import { snippetPlan } from './snippetPlan.ts'
|
|
23
23
|
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
24
24
|
import { staticAttr } from './staticAttr.ts'
|
|
25
|
+
import { staticAttrValue } from './staticAttrValue.ts'
|
|
25
26
|
import { staticTextPart } from './staticTextPart.ts'
|
|
26
27
|
import { switchPlan } from './switchPlan.ts'
|
|
27
28
|
import { tryPlan } from './tryPlan.ts'
|
|
@@ -43,7 +44,7 @@ attributes, reactive `attr`/`text` bindings, `on` listeners, keyed `each`, and
|
|
|
43
44
|
conditional `when`. Every embedded expression is first rewritten from the signal
|
|
44
45
|
surface (`count` → `model.count`) and then lowered to the doc patch/read API
|
|
45
46
|
(cell-hoisting runs over the whole result afterwards). The output operates on
|
|
46
|
-
`hostVar` and expects the `$$`-aliased dom bindings (`$$
|
|
47
|
+
`hostVar` and expects the `$$`-aliased dom bindings (`$$watch`, `$$each`, …)
|
|
47
48
|
and the component's `$$model` (emitted by `desugarSignals`) in scope — the body
|
|
48
49
|
the component compiler wraps and hoists cells into.
|
|
49
50
|
*/
|
|
@@ -154,11 +155,11 @@ export function generateBuild(
|
|
|
154
155
|
/* `class:<name>` — toggle the class by truthiness; surgical, no element re-render.
|
|
155
156
|
Layers on top of any static `class="…"` in the skeleton (classList is additive). */
|
|
156
157
|
if (attr.kind === 'class') {
|
|
157
|
-
return `$$
|
|
158
|
+
return `$$watch(${namedThunk(`class_${attr.name}`, `${varName}.classList.toggle(${JSON.stringify(attr.name)}, !!(${lowerExpression(attr.code)}));`)});\n`
|
|
158
159
|
}
|
|
159
160
|
/* `style:<property>` — write one inline style / custom property reactively. */
|
|
160
161
|
if (attr.kind === 'style') {
|
|
161
|
-
return `$$
|
|
162
|
+
return `$$watch(${namedThunk(`style_${attr.property}`, `${varName}.style.setProperty(${JSON.stringify(attr.property)}, String(${lowerExpression(attr.code)}));`)});\n`
|
|
162
163
|
}
|
|
163
164
|
if (attr.property === 'group') {
|
|
164
165
|
/* Grouped two-way: radio binds the path to the single checked `value`;
|
|
@@ -171,24 +172,46 @@ export function generateBuild(
|
|
|
171
172
|
const value = lowerExpression(valueCode)
|
|
172
173
|
if (isRadio) {
|
|
173
174
|
return (
|
|
174
|
-
`$$
|
|
175
|
+
`$$watch(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}) === (${value});`)});\n` +
|
|
175
176
|
`$$on(${varName}, "change", () => { if (${varName}.checked) { ${lowerStatement(`${attr.code} = ${valueCode}`)} } });\n`
|
|
176
177
|
)
|
|
177
178
|
}
|
|
178
179
|
return (
|
|
179
|
-
`$$
|
|
180
|
+
`$$watch(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}).includes(${value});`)});\n` +
|
|
180
181
|
`$$on(${varName}, "change", () => { const $groupValue = ${value}; if (${varName}.checked) { if (!(${lowerExpression(attr.code)}).includes($groupValue)) { ${lowerStatement(`${attr.code}.push($groupValue)`)} } } else { const $groupIndex = (${lowerExpression(attr.code)}).indexOf($groupValue); if ($groupIndex !== -1) { ${lowerStatement(`delete ${attr.code}[$groupIndex]`)} } } });\n`
|
|
181
182
|
)
|
|
182
183
|
}
|
|
184
|
+
/* `<select bind:value>` — options frequently mount after this binding runs (a
|
|
185
|
+
`{#for}` child, an async list) and the browser drops a `value` set naming a
|
|
186
|
+
not-yet-present option, so route to `bindSelectValue`: it re-applies on option
|
|
187
|
+
changes via a MutationObserver and switches single/array semantics on `multiple`
|
|
188
|
+
(`<select multiple>` binds an array of the selected option values). The read/write
|
|
189
|
+
are the same lvalue/accessor forms every bind uses; the helper decides how to
|
|
190
|
+
apply and collect them. */
|
|
191
|
+
if (attr.property === 'value' && node.tag === 'select') {
|
|
192
|
+
const multiple = staticAttrValue(node, 'multiple') !== undefined
|
|
193
|
+
return `$$bindSelectValue(${varName}, () => (${bindRead(attr.code)}), ($selectValue) => { ${bindWrite(attr.code, '$selectValue')} }, ${multiple});\n`
|
|
194
|
+
}
|
|
183
195
|
/* Two-way: drive the property from the bind target, and write it back on the
|
|
184
196
|
property's native event (`input` for most fields, but `toggle` for
|
|
185
197
|
`<details open>`, `change` for checked/select). An lvalue target reads as
|
|
186
198
|
itself and writes by assignment; an accessor object (`{ get, set }`) reads via
|
|
187
|
-
`.get()` and writes via `.set(v)` — see `bindRead`/`bindWrite`.
|
|
199
|
+
`.get()` and writes via `.set(v)` — see `bindRead`/`bindWrite`. A numeric input
|
|
200
|
+
(`type="number"`/`"range"`) reports its edit as a string on `el.value`, which
|
|
201
|
+
would corrupt number-typed state; source the write-back from `valueAsNumber`
|
|
202
|
+
instead (empty field → `undefined`), gated on a statically-known type. */
|
|
188
203
|
const event = bindListenEvent(attr.property, node.tag)
|
|
204
|
+
const staticType = staticAttrValue(node, 'type')
|
|
205
|
+
const isNumericInput =
|
|
206
|
+
attr.property === 'value' &&
|
|
207
|
+
node.tag === 'input' &&
|
|
208
|
+
(staticType === 'number' || staticType === 'range')
|
|
209
|
+
const writeSource = isNumericInput
|
|
210
|
+
? `(${varName}.value === '' ? undefined : ${varName}.valueAsNumber)`
|
|
211
|
+
: `${varName}.${attr.property}`
|
|
189
212
|
return (
|
|
190
|
-
`$$
|
|
191
|
-
`$$on(${varName}, ${JSON.stringify(event)}, () => { ${bindWrite(attr.code,
|
|
213
|
+
`$$watch(${namedThunk(`bind_${attr.property}`, `${varName}.${attr.property} = ${bindRead(attr.code)};`)});\n` +
|
|
214
|
+
`$$on(${varName}, ${JSON.stringify(event)}, () => { ${bindWrite(attr.code, writeSource)} });\n`
|
|
192
215
|
)
|
|
193
216
|
}
|
|
194
217
|
|
|
@@ -310,12 +333,12 @@ export function generateBuild(
|
|
|
310
333
|
skeleton). */
|
|
311
334
|
if (plan.merge.mergeClassBuild) {
|
|
312
335
|
binds.push(
|
|
313
|
-
`$$
|
|
336
|
+
`$$watch(${namedThunk('class_merge', `${elVar}.setAttribute("class", [${plan.merge.classParts.join(', ')}].filter(Boolean).join(' '));`)});\n`,
|
|
314
337
|
)
|
|
315
338
|
}
|
|
316
339
|
if (plan.merge.mergeStyleBuild) {
|
|
317
340
|
binds.push(
|
|
318
|
-
`$$
|
|
341
|
+
`$$watch(${namedThunk('style_merge', `${elVar}.setAttribute("style", [${plan.merge.styleParts.join(', ')}].filter(Boolean).join(';'));`)});\n`,
|
|
319
342
|
)
|
|
320
343
|
}
|
|
321
344
|
for (const { attr, mergedBuild } of plan.attrs) {
|
|
@@ -558,11 +581,7 @@ export function generateBuild(
|
|
|
558
581
|
parentVar: string,
|
|
559
582
|
before: string,
|
|
560
583
|
): string {
|
|
561
|
-
|
|
562
|
-
(the common case) leaves the call byte-identical to before — eager hydration. */
|
|
563
|
-
const trigger =
|
|
564
|
-
node.clientTrigger !== undefined ? `, ${JSON.stringify(node.clientTrigger)}` : ''
|
|
565
|
-
return `$$mountChild(${parentVar}, ${node.name}, ${propsArg(node)}, ${before}, ${JSON.stringify(node.name)}${trigger});\n`
|
|
584
|
+
return `$$mountChild(${parentVar}, ${node.name}, ${propsArg(node)}, ${before}, ${JSON.stringify(node.name)});\n`
|
|
566
585
|
}
|
|
567
586
|
|
|
568
587
|
/* An await block: pending → resolved(value) / error branches. Each branch is a
|
|
@@ -719,7 +738,20 @@ export function generateBuild(
|
|
|
719
738
|
destructures the item via `plan.as` to read its leaves. */
|
|
720
739
|
const rawItemParam = isPlainIdentifier(plan.as) ? plan.as : nextVar('k')
|
|
721
740
|
const keyParam = plan.key === undefined ? rawItemParam : plan.as
|
|
722
|
-
|
|
741
|
+
/* The key callback binds the RAW item via `plan.as`, so lower the key expression with
|
|
742
|
+
the item's names shadowed as PLAIN locals — otherwise a `by item.id` whose `item`
|
|
743
|
+
collides with a component signal rewrites to `$$model.read("item/id")` (every row
|
|
744
|
+
keys off the same signal → keyed reconciliation degenerates). The plain kind (not
|
|
745
|
+
the row body's reactive `.value` kind) keeps it the bare param the callback binds. */
|
|
746
|
+
const keyExpression =
|
|
747
|
+
plan.key === undefined
|
|
748
|
+
? rawItemParam
|
|
749
|
+
: withBindings(
|
|
750
|
+
withShadow,
|
|
751
|
+
[plan.bindings[0] as Binding],
|
|
752
|
+
() => 'plain',
|
|
753
|
+
() => lowerExpression(plan.key as string),
|
|
754
|
+
)
|
|
723
755
|
/* The item is the row's `reactive` binding (its `.value` cell + per-leaf reader prefix
|
|
724
756
|
rendered by `reactiveBinding`); `index="i"` is a second reactive cell param, a plain
|
|
725
757
|
identifier read as `i.value` (no prefix). The names of BOTH enter the deref scope via
|