@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
|
@@ -6,23 +6,25 @@ import type { CompiledShadow, ShadowMapping } from './types/CompiledShadow.ts'
|
|
|
6
6
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
|
-
Framework callables the `.abide` loader injects into a component's scope
|
|
10
|
-
|
|
11
|
-
type-check
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
Framework callables the `.abide` loader injects into a component's scope. `effect`,
|
|
10
|
+
`html`, `snippet`, and `scope` keep their real published types via imports so author
|
|
11
|
+
calls type-check — `scope()` is the authored reactive surface (`scope().state(...)` /
|
|
12
|
+
`.computed(...)` / `.undo()` …), so it must resolve like any import. `state`/`linked`/
|
|
13
|
+
`computed` are ALSO declared ambiently as a fallback for the rare bare/nested use the
|
|
14
|
+
top-level rewrite doesn't project (their top-level declarations become value types, so
|
|
15
|
+
these are normally unused — fine, the shadow disables noUnusedLocals). `prop` is sugar
|
|
16
|
+
too, but every `prop()` declaration is rewritten away, so it never appears here.
|
|
17
|
+
`$props` is the legacy untyped prop bag (pre-`prop()` sugar) made available raw.
|
|
16
18
|
*/
|
|
17
|
-
const SHADOW_PREAMBLE = `import {
|
|
18
|
-
import { linked } from '${ABIDE_PACKAGE_NAME}/ui/linked'
|
|
19
|
-
import { derived } from '${ABIDE_PACKAGE_NAME}/ui/derived'
|
|
20
|
-
import { effect } from '${ABIDE_PACKAGE_NAME}/ui/effect'
|
|
21
|
-
import { doc } from '${ABIDE_PACKAGE_NAME}/ui/doc'
|
|
19
|
+
const SHADOW_PREAMBLE = `import { effect } from '${ABIDE_PACKAGE_NAME}/ui/effect'
|
|
22
20
|
import { html } from '${ABIDE_PACKAGE_NAME}/shared/html'
|
|
23
21
|
import { snippet } from '${ABIDE_PACKAGE_NAME}/shared/snippet'
|
|
22
|
+
import { scope } from '${ABIDE_PACKAGE_NAME}/ui/scope'
|
|
23
|
+
declare function state<T>(initial?: T, transform?: (next: T, previous: T) => T): { value: T }
|
|
24
|
+
declare function linked<T>(seed: () => T, transform?: (next: T, previous: T) => T): { value: T }
|
|
25
|
+
declare function computed<T>(compute: () => T): { readonly value: T }
|
|
24
26
|
declare const $props: Record<string, (() => unknown) | undefined>
|
|
25
|
-
void [
|
|
27
|
+
void [effect, html, snippet, scope]
|
|
26
28
|
`
|
|
27
29
|
|
|
28
30
|
/*
|
|
@@ -35,10 +37,10 @@ returned segments.
|
|
|
35
37
|
|
|
36
38
|
The script's signal surface is rewritten to value types:
|
|
37
39
|
let count = state(0) → let count = (0)
|
|
38
|
-
const total =
|
|
40
|
+
const total = computed(() => …) → const total = (() => …)()
|
|
39
41
|
let title = prop<string>('t') → Props field + `let title = props['t']`
|
|
40
42
|
Everything else (functions, plain consts, imports) is emitted verbatim, so
|
|
41
|
-
expressions inside it (e.g. a
|
|
43
|
+
expressions inside it (e.g. a computed's compute body) are checked and mapped too.
|
|
42
44
|
*/
|
|
43
45
|
export function compileShadow(source: string): CompiledShadow {
|
|
44
46
|
const builder = createBuilder()
|
|
@@ -188,7 +190,7 @@ function analyzeScript(scriptBody: string, scriptStart: number): ScriptAnalysis
|
|
|
188
190
|
projects the leading script's scope: reactive declarations become their value
|
|
189
191
|
type, every other statement stays verbatim. Returns the projected source text
|
|
190
192
|
(unmapped — nested scripts carry no source offset yet), so a branch's markup
|
|
191
|
-
reads a nested signal as its value type instead of the raw `State`/`
|
|
193
|
+
reads a nested signal as its value type instead of the raw `State`/`Computed`. */
|
|
192
194
|
function projectNestedScript(code: string): string {
|
|
193
195
|
const file = ts.createSourceFile('nested.ts', code, ts.ScriptTarget.Latest, true)
|
|
194
196
|
const verbatim = (node: ts.Node): string => code.slice(node.getStart(file), node.getEnd())
|
|
@@ -205,7 +207,7 @@ function projectNestedScript(code: string): string {
|
|
|
205
207
|
.join('\n')
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
/* The `state`/`
|
|
210
|
+
/* The `state`/`computed`/`prop` declarations in a variable statement, or undefined
|
|
209
211
|
if it isn't one declaring them (so the caller emits it verbatim). A statement
|
|
210
212
|
mixing reactive and plain declarations is rare; treated as all-verbatim. */
|
|
211
213
|
function reactiveDeclarations(statement: ts.Statement): ts.VariableDeclaration[] | undefined {
|
|
@@ -217,19 +219,21 @@ function reactiveDeclarations(statement: ts.Statement): ts.VariableDeclaration[]
|
|
|
217
219
|
return reactive.length === declarations.length && reactive.length > 0 ? reactive : undefined
|
|
218
220
|
}
|
|
219
221
|
|
|
220
|
-
/* The callee name of a `NAME = state(...)` / `linked(...)` / `
|
|
221
|
-
`prop(...)` decl.
|
|
222
|
+
/* The callee name of a `NAME = state(...)` / `linked(...)` / `computed(...)` /
|
|
223
|
+
`prop(...)` decl — bare or the explicit scope form (`scope().state(...)` / `c.state(...)`),
|
|
224
|
+
receiver-agnostic (the method name marks it reactive). */
|
|
222
225
|
function signalCallee(declaration: ts.VariableDeclaration): string | undefined {
|
|
223
226
|
const initializer = declaration.initializer
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
ts.isCallExpression(initializer) &&
|
|
227
|
-
ts.isIdentifier(initializer.expression) &&
|
|
228
|
-
REACTIVE_CALLEES.has(initializer.expression.text)
|
|
229
|
-
) {
|
|
230
|
-
return initializer.expression.text
|
|
227
|
+
if (initializer === undefined || !ts.isCallExpression(initializer)) {
|
|
228
|
+
return undefined
|
|
231
229
|
}
|
|
232
|
-
|
|
230
|
+
const callee = initializer.expression
|
|
231
|
+
const name = ts.isIdentifier(callee)
|
|
232
|
+
? callee.text
|
|
233
|
+
: ts.isPropertyAccessExpression(callee)
|
|
234
|
+
? callee.name.text
|
|
235
|
+
: undefined
|
|
236
|
+
return name !== undefined && REACTIVE_CALLEES.has(name) ? name : undefined
|
|
233
237
|
}
|
|
234
238
|
|
|
235
239
|
/* Builds one scope line for a reactive declaration, projecting it to its value
|
|
@@ -242,7 +246,10 @@ function scopeLineFor(
|
|
|
242
246
|
): ScopeLine {
|
|
243
247
|
const name = ts.isIdentifier(declaration.name) ? declaration.name.text : '_'
|
|
244
248
|
const call = declaration.initializer as ts.CallExpression
|
|
245
|
-
|
|
249
|
+
/* Bare callee (`state`) or member callee (`scope().state` / `c.state`). */
|
|
250
|
+
const callee = ts.isPropertyAccessExpression(call.expression)
|
|
251
|
+
? call.expression.name.text
|
|
252
|
+
: (call.expression as ts.Identifier).text
|
|
246
253
|
if (callee === 'state') {
|
|
247
254
|
/* state<T>(initial): T is the value type — carry it onto the `let` so an
|
|
248
255
|
explicit annotation isn't lost to `any`/`any[]` inference of the initial. */
|
|
@@ -257,23 +264,38 @@ function scopeLineFor(
|
|
|
257
264
|
a guard like `x !== undefined` collapse to `never`). Unguarded access is
|
|
258
265
|
then correctly flagged possibly-undefined; a guard narrows cleanly. */
|
|
259
266
|
const valueType = annotation === '' ? ': unknown' : `${annotation} | undefined`
|
|
260
|
-
|
|
267
|
+
/* map the binding name (offset 4, past `let `) so hover/go-to resolve on it */
|
|
268
|
+
return { text: `let ${name}!${valueType};`, segments: [span(declaration.name, 4)] }
|
|
261
269
|
}
|
|
262
270
|
const prefix = `let ${name}${annotation} = (`
|
|
263
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
text: `${prefix}${verbatim(init)});`,
|
|
273
|
+
segments: [span(declaration.name, 4), span(init, prefix.length)],
|
|
274
|
+
}
|
|
264
275
|
}
|
|
265
|
-
if (callee === '
|
|
266
|
-
/*
|
|
276
|
+
if (callee === 'computed' || callee === 'linked') {
|
|
277
|
+
/* computed<T>(compute) / linked<T>(seed): T is the value type — the call's
|
|
267
278
|
first arg is a thunk, so invoking it yields the value. Annotate so an
|
|
268
279
|
explicit type argument isn't lost to inference of the thunk's return. */
|
|
269
280
|
const typeNode = call.typeArguments?.[0]
|
|
270
281
|
const annotation = typeNode === undefined ? '' : `: ${verbatim(typeNode)}`
|
|
271
282
|
const fn = call.arguments[0]
|
|
283
|
+
/* `linked` is a writable `State<T>` at runtime (it reseeds AND accepts `.value =`
|
|
284
|
+
writes), so project it as `let`; `computed` is genuinely read-only, so `const`. */
|
|
285
|
+
const keyword = callee === 'linked' ? 'let' : 'const'
|
|
286
|
+
/* binding-name map offset = past the keyword + space (`let ` = 4, `const ` = 6) */
|
|
287
|
+
const keywordOffset = keyword.length + 1
|
|
272
288
|
if (fn === undefined) {
|
|
273
|
-
return {
|
|
289
|
+
return {
|
|
290
|
+
text: `${keyword} ${name} = undefined;`,
|
|
291
|
+
segments: [span(declaration.name, keywordOffset)],
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const prefix = `${keyword} ${name}${annotation} = (`
|
|
295
|
+
return {
|
|
296
|
+
text: `${prefix}${verbatim(fn)})();`,
|
|
297
|
+
segments: [span(declaration.name, keywordOffset), span(fn, prefix.length)],
|
|
274
298
|
}
|
|
275
|
-
const prefix = `const ${name}${annotation} = (`
|
|
276
|
-
return { text: `${prefix}${verbatim(fn)})();`, segments: [span(fn, prefix.length)] }
|
|
277
299
|
}
|
|
278
300
|
/* prop<T>('key'): Props field `key[?]: T`, scope binding read from props. */
|
|
279
301
|
const key = call.arguments[0]
|
|
@@ -282,7 +304,10 @@ function scopeLineFor(
|
|
|
282
304
|
const typeText = typeNode === undefined ? 'unknown' : verbatim(typeNode)
|
|
283
305
|
const optional = typeNode === undefined || /\bundefined\b/.test(typeText)
|
|
284
306
|
props.push(` ${JSON.stringify(keyText)}${optional ? '?' : ''}: ${typeText}`)
|
|
285
|
-
return {
|
|
307
|
+
return {
|
|
308
|
+
text: `let ${name} = props[${JSON.stringify(keyText)}];`,
|
|
309
|
+
segments: [span(declaration.name, 4)],
|
|
310
|
+
}
|
|
286
311
|
}
|
|
287
312
|
|
|
288
313
|
/* Emits a sibling list — each node standalone via `emitNode`. */
|
|
@@ -464,7 +489,7 @@ function emitNode(node: TemplateNode, builder: Builder): void {
|
|
|
464
489
|
return
|
|
465
490
|
case 'script':
|
|
466
491
|
/* A scoped reactive `<script>`: value-project its reactive declarations to
|
|
467
|
-
their value types (`
|
|
492
|
+
their value types (`computed(…)` → the computed value, `state(…)` → the
|
|
468
493
|
initial) exactly as the leading script's scope lines are, so the branch's
|
|
469
494
|
markup type-checks a nested signal as its value — matching the runtime,
|
|
470
495
|
which derefs nested-script signals through the rest of the branch. Emitted
|
|
@@ -2,20 +2,46 @@ import ts from 'typescript'
|
|
|
2
2
|
import { REACTIVE_CALLEES } from './REACTIVE_CALLEES.ts'
|
|
3
3
|
import { renameSignalRefs } from './renameSignalRefs.ts'
|
|
4
4
|
|
|
5
|
+
/* The reactive primitives that must be reached through a scope (`prop` is excluded — it
|
|
6
|
+
reads parent-passed props, not scope data, so it stays a bare call). A bare call to one
|
|
7
|
+
of these is a compile error: reactive state is owned by a scope and the surface must show
|
|
8
|
+
it (`scope().state(...)`), so a reader always sees the scope interaction. */
|
|
9
|
+
const SCOPE_PRIMITIVES: ReadonlySet<string> = new Set(['state', 'linked', 'computed'])
|
|
10
|
+
|
|
11
|
+
/* Throws if the script calls a scope primitive bare (`state(0)`) instead of through a scope
|
|
12
|
+
(`scope().state(0)` / `c.state(0)`). Walks all calls, so a stray one nested in a function
|
|
13
|
+
is caught too, not just top-level declarations. */
|
|
14
|
+
function assertScopedPrimitives(source: ts.SourceFile): void {
|
|
15
|
+
const visit = (node: ts.Node): void => {
|
|
16
|
+
if (
|
|
17
|
+
ts.isCallExpression(node) &&
|
|
18
|
+
ts.isIdentifier(node.expression) &&
|
|
19
|
+
SCOPE_PRIMITIVES.has(node.expression.text)
|
|
20
|
+
) {
|
|
21
|
+
const name = node.expression.text
|
|
22
|
+
throw new Error(
|
|
23
|
+
`abide: bare \`${name}(...)\` is not allowed — reactive state lives on a scope. Use \`scope().${name}(...)\` (or a captured handle: \`const s = scope(); s.${name}(...)\`).`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
ts.forEachChild(node, visit)
|
|
27
|
+
}
|
|
28
|
+
visit(source)
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
/*
|
|
6
32
|
Desugars the signal surface into the document form. A component's `<script>`
|
|
7
33
|
declares reactive state as signals:
|
|
8
34
|
|
|
9
35
|
let count = state(0)
|
|
10
36
|
let items = state([])
|
|
11
|
-
const total =
|
|
37
|
+
const total = computed(() => count + items.length)
|
|
12
38
|
|
|
13
39
|
This collects the binding names, turns each plain `state(initial)` declaration
|
|
14
40
|
into an initialising assignment on a shared `model` document (in source order, so
|
|
15
41
|
a later state can read an earlier one), keeps the rest, then renames every
|
|
16
42
|
reference through `renameSignalRefs`. Plain `state` becomes `model.x` access that
|
|
17
43
|
`lowerDocAccess` lowers to patches/reads — the document substrate's deep,
|
|
18
|
-
fine-grained, serializable reactivity for free. `linked`, `
|
|
44
|
+
fine-grained, serializable reactivity for free. `linked`, `computed`, and
|
|
19
45
|
`state(initial, transform)` stay verbatim as runtime `.value` cells (referenced
|
|
20
46
|
as `name.value`): they own no doc slot, so they reseed/recompute on resume rather
|
|
21
47
|
than serialize. No reactive declarations → the script is returned untouched (the
|
|
@@ -25,10 +51,13 @@ export function desugarSignals(scriptBody: string): {
|
|
|
25
51
|
code: string
|
|
26
52
|
stateNames: Set<string>
|
|
27
53
|
derivedNames: Set<string>
|
|
54
|
+
computedNames: Set<string>
|
|
28
55
|
} {
|
|
29
56
|
const source = ts.createSourceFile('script.ts', scriptBody, ts.ScriptTarget.Latest, true)
|
|
57
|
+
assertScopedPrimitives(source)
|
|
30
58
|
const stateNames = new Set<string>()
|
|
31
59
|
const derivedNames = new Set<string>()
|
|
60
|
+
const computedNames = new Set<string>()
|
|
32
61
|
for (const statement of source.statements) {
|
|
33
62
|
if (!ts.isVariableStatement(statement)) {
|
|
34
63
|
continue
|
|
@@ -41,39 +70,129 @@ export function desugarSignals(scriptBody: string): {
|
|
|
41
70
|
if (isPlainStateSlot(declaration)) {
|
|
42
71
|
/* Plain `state(initial)` → a serializable `model` doc slot. */
|
|
43
72
|
stateNames.add(declaration.name.text)
|
|
73
|
+
} else if (isComputedSlot(declaration)) {
|
|
74
|
+
/* Read-only `computed(compute)` / `prop` → a computed `scope().derive`
|
|
75
|
+
doc slot, referenced as `name()` (its string-free reader): a function
|
|
76
|
+
of other paths, recomputed via the graph, never stored/serialized. */
|
|
77
|
+
computedNames.add(declaration.name.text)
|
|
44
78
|
} else if (callee !== undefined && REACTIVE_CALLEES.has(callee)) {
|
|
45
|
-
/* `.value` cells
|
|
46
|
-
`
|
|
47
|
-
|
|
79
|
+
/* `.value` cells: `linked` and `state(initial, transform)` — they own
|
|
80
|
+
a local store, so they stay cells (`computed` is always the read-only
|
|
81
|
+
slot above; there is no writable-computed cell). */
|
|
48
82
|
derivedNames.add(declaration.name.text)
|
|
49
83
|
}
|
|
50
84
|
}
|
|
51
85
|
}
|
|
52
|
-
if (stateNames.size === 0 && derivedNames.size === 0) {
|
|
53
|
-
return { code: scriptBody, stateNames, derivedNames }
|
|
86
|
+
if (stateNames.size === 0 && derivedNames.size === 0 && computedNames.size === 0) {
|
|
87
|
+
return { code: scriptBody, stateNames, derivedNames, computedNames }
|
|
54
88
|
}
|
|
55
89
|
|
|
56
90
|
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
|
|
57
91
|
const lines: string[] = []
|
|
58
92
|
if (stateNames.size > 0) {
|
|
59
|
-
lines.push('const model =
|
|
93
|
+
lines.push('const model = scope()')
|
|
60
94
|
}
|
|
61
95
|
for (const statement of source.statements) {
|
|
62
96
|
const stateAssignments = stateDeclarationAssignments(statement, printer, source)
|
|
97
|
+
const computedDeclarations = computedDeclarationLines(statement, printer, source)
|
|
63
98
|
const propDeclarations = propDeclarationLines(statement, printer, source)
|
|
99
|
+
const cellDeclarations = cellDeclarationLines(statement, printer, source)
|
|
64
100
|
if (stateAssignments !== undefined) {
|
|
65
101
|
lines.push(...stateAssignments)
|
|
102
|
+
} else if (computedDeclarations !== undefined) {
|
|
103
|
+
lines.push(...computedDeclarations)
|
|
66
104
|
} else if (propDeclarations !== undefined) {
|
|
67
105
|
lines.push(...propDeclarations)
|
|
106
|
+
} else if (cellDeclarations !== undefined) {
|
|
107
|
+
lines.push(...cellDeclarations)
|
|
68
108
|
} else {
|
|
69
109
|
lines.push(printer.printNode(ts.EmitHint.Unspecified, statement, source))
|
|
70
110
|
}
|
|
71
111
|
}
|
|
72
112
|
return {
|
|
73
|
-
code: renameSignalRefs(lines.join('\n'), stateNames, derivedNames),
|
|
113
|
+
code: renameSignalRefs(lines.join('\n'), stateNames, derivedNames, computedNames),
|
|
74
114
|
stateNames,
|
|
75
115
|
derivedNames,
|
|
116
|
+
computedNames,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* True for a read-only computed slot — `computed(compute)` with no write-through
|
|
121
|
+
`set`, or a `prop` (a read-only computed over the parent thunk). The writable
|
|
122
|
+
`computed(compute, set)` lens keeps a `.value` cell (handled by the caller). */
|
|
123
|
+
function isComputedSlot(declaration: ts.VariableDeclaration): boolean {
|
|
124
|
+
const callee = signalCallee(declaration)
|
|
125
|
+
if (callee === 'prop') {
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
const initializer = declaration.initializer
|
|
129
|
+
return (
|
|
130
|
+
callee === 'computed' &&
|
|
131
|
+
initializer !== undefined &&
|
|
132
|
+
ts.isCallExpression(initializer) &&
|
|
133
|
+
initializer.arguments.length === 1
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* A `.value`-cell signal declaration — a `linked` or a `state(initial, transform)` —
|
|
138
|
+
routed onto the scope: `linked(seed)` → `const x = scope().linked(seed)`. The cell
|
|
139
|
+
stays a standalone non-serializing cell (its refs stay `name.value`); the rewrite only
|
|
140
|
+
removes the bare runtime import so the sole reactive surface is `scope()`. */
|
|
141
|
+
function cellDeclarationLines(
|
|
142
|
+
statement: ts.Statement,
|
|
143
|
+
printer: ts.Printer,
|
|
144
|
+
source: ts.SourceFile,
|
|
145
|
+
): string[] | undefined {
|
|
146
|
+
if (!ts.isVariableStatement(statement)) {
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
const lines: string[] = []
|
|
150
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
151
|
+
const callee = signalCallee(declaration)
|
|
152
|
+
if (
|
|
153
|
+
!ts.isIdentifier(declaration.name) ||
|
|
154
|
+
(callee !== 'linked' && callee !== 'state') ||
|
|
155
|
+
isPlainStateSlot(declaration)
|
|
156
|
+
) {
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
const args = (declaration.initializer as ts.CallExpression).arguments
|
|
160
|
+
.map((argument) => printer.printNode(ts.EmitHint.Unspecified, argument, source))
|
|
161
|
+
.join(', ')
|
|
162
|
+
lines.push(`const ${declaration.name.text} = scope().${callee}(${args})`)
|
|
163
|
+
}
|
|
164
|
+
return lines
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* `let total = computed(compute)` → `const total = scope().derive("total", compute)`
|
|
168
|
+
— a computed doc slot whose reader the references lower to `total()`. */
|
|
169
|
+
function computedDeclarationLines(
|
|
170
|
+
statement: ts.Statement,
|
|
171
|
+
printer: ts.Printer,
|
|
172
|
+
source: ts.SourceFile,
|
|
173
|
+
): string[] | undefined {
|
|
174
|
+
if (!ts.isVariableStatement(statement)) {
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
const lines: string[] = []
|
|
178
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
179
|
+
if (
|
|
180
|
+
!ts.isIdentifier(declaration.name) ||
|
|
181
|
+
signalCallee(declaration) !== 'computed' ||
|
|
182
|
+
!isComputedSlot(declaration)
|
|
183
|
+
) {
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
const compute = (declaration.initializer as ts.CallExpression).arguments[0]
|
|
187
|
+
const computeText =
|
|
188
|
+
compute === undefined
|
|
189
|
+
? '() => undefined'
|
|
190
|
+
: printer.printNode(ts.EmitHint.Unspecified, compute, source)
|
|
191
|
+
lines.push(
|
|
192
|
+
`const ${declaration.name.text} = scope().derive(${JSON.stringify(declaration.name.text)}, ${computeText})`,
|
|
193
|
+
)
|
|
76
194
|
}
|
|
195
|
+
return lines
|
|
77
196
|
}
|
|
78
197
|
|
|
79
198
|
/* True for `state(initial, transform)` — the write-coercion transform forces a
|
|
@@ -94,20 +213,28 @@ function isPlainStateSlot(declaration: ts.VariableDeclaration): boolean {
|
|
|
94
213
|
return signalCallee(declaration) === 'state' && !hasTransform(declaration)
|
|
95
214
|
}
|
|
96
215
|
|
|
97
|
-
/* The callee name of a
|
|
216
|
+
/* The callee name of a reactive declaration, else undefined. Recognises both the bare
|
|
217
|
+
form (`NAME = state(...)`) and the explicit scope form (`NAME = scope().state(...)` or
|
|
218
|
+
`NAME = c.state(...)` for any scope handle `c`) — receiver-agnostic: the METHOD name is
|
|
219
|
+
what marks the binding reactive. Since `scope()` is the ambient scope (one object per
|
|
220
|
+
level), the receiver is irrelevant to lowering — the slot keys off the binding name and
|
|
221
|
+
lands on the same `model`, so the explicit form lowers exactly like the bare one. */
|
|
98
222
|
function signalCallee(declaration: ts.VariableDeclaration): string | undefined {
|
|
99
223
|
const initializer = declaration.initializer
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
) {
|
|
105
|
-
return
|
|
224
|
+
if (initializer === undefined || !ts.isCallExpression(initializer)) {
|
|
225
|
+
return undefined
|
|
226
|
+
}
|
|
227
|
+
const callee = initializer.expression
|
|
228
|
+
if (ts.isIdentifier(callee)) {
|
|
229
|
+
return callee.text
|
|
230
|
+
}
|
|
231
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
232
|
+
return callee.name.text
|
|
106
233
|
}
|
|
107
234
|
return undefined
|
|
108
235
|
}
|
|
109
236
|
|
|
110
|
-
/* If `statement` declares `prop(...)` bindings, returns a reactive
|
|
237
|
+
/* If `statement` declares `prop(...)` bindings, returns a reactive computed over
|
|
111
238
|
the parent-supplied `$props` thunk for each; otherwise undefined. The optional
|
|
112
239
|
call (`?.()`) tolerates an omitted prop. */
|
|
113
240
|
function propDeclarationLines(
|
|
@@ -126,7 +253,9 @@ function propDeclarationLines(
|
|
|
126
253
|
const key = (declaration.initializer as ts.CallExpression).arguments[0]
|
|
127
254
|
const keyText =
|
|
128
255
|
key === undefined ? "''" : printer.printNode(ts.EmitHint.Unspecified, key, source)
|
|
129
|
-
lines.push(
|
|
256
|
+
lines.push(
|
|
257
|
+
`const ${declaration.name.text} = scope().derive(${JSON.stringify(declaration.name.text)}, () => $props[${keyText}]?.())`,
|
|
258
|
+
)
|
|
130
259
|
}
|
|
131
260
|
return lines
|
|
132
261
|
}
|
|
@@ -3,6 +3,8 @@ import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
|
3
3
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
4
4
|
import { componentWrapperTag } from './componentWrapperTag.ts'
|
|
5
5
|
import { groupBindParts } from './groupBindParts.ts'
|
|
6
|
+
import { isControlFlow } from './isControlFlow.ts'
|
|
7
|
+
import { isTextLeaf } from './isTextLeaf.ts'
|
|
6
8
|
import { lowerContext } from './lowerContext.ts'
|
|
7
9
|
import { scopeAttr } from './scopeAttr.ts'
|
|
8
10
|
import { staticAttr } from './staticAttr.ts'
|
|
@@ -24,6 +26,7 @@ export function generateBuild(
|
|
|
24
26
|
hostVar: string,
|
|
25
27
|
stateNames: ReadonlySet<string>,
|
|
26
28
|
derivedNames: ReadonlySet<string>,
|
|
29
|
+
computedNames: ReadonlySet<string>,
|
|
27
30
|
isLayout = false,
|
|
28
31
|
): string {
|
|
29
32
|
let counter = 0
|
|
@@ -34,7 +37,9 @@ export function generateBuild(
|
|
|
34
37
|
expression: lowerExpression,
|
|
35
38
|
statement: lowerStatement,
|
|
36
39
|
withNestedScripts,
|
|
37
|
-
|
|
40
|
+
bindRead,
|
|
41
|
+
bindWrite,
|
|
42
|
+
} = lowerContext(stateNames, derivedNames, computedNames)
|
|
38
43
|
|
|
39
44
|
/* Emits the wiring for one non-static attribute against an already-obtained skeleton
|
|
40
45
|
element var — reactive `attr`, `on` listener, `attach`, or a two-way `bind`. */
|
|
@@ -75,14 +80,15 @@ export function generateBuild(
|
|
|
75
80
|
`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`
|
|
76
81
|
)
|
|
77
82
|
}
|
|
78
|
-
/* Two-way: drive the property from the
|
|
83
|
+
/* Two-way: drive the property from the bind target, and write it back on the
|
|
79
84
|
property's native event (`input` for most fields, but `toggle` for
|
|
80
|
-
`<details open>`, `change` for checked/select).
|
|
81
|
-
|
|
85
|
+
`<details open>`, `change` for checked/select). An lvalue target reads as
|
|
86
|
+
itself and writes by assignment; an accessor object (`{ get, set }`) reads via
|
|
87
|
+
`.get()` and writes via `.set(v)` — see `bindRead`/`bindWrite`. */
|
|
82
88
|
const event = bindListenEvent(attr.property, node.tag)
|
|
83
89
|
return (
|
|
84
|
-
`effect(() => { ${varName}.${attr.property} = ${
|
|
85
|
-
`on(${varName}, ${JSON.stringify(event)}, () => { ${
|
|
90
|
+
`effect(() => { ${varName}.${attr.property} = ${bindRead(attr.code)}; });\n` +
|
|
91
|
+
`on(${varName}, ${JSON.stringify(event)}, () => { ${bindWrite(attr.code, `${varName}.${attr.property}`)} });\n`
|
|
86
92
|
)
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -117,7 +123,7 @@ export function generateBuild(
|
|
|
117
123
|
})
|
|
118
124
|
.join('')
|
|
119
125
|
}
|
|
120
|
-
if (
|
|
126
|
+
if (isControlFlow(node)) {
|
|
121
127
|
/* A control-flow block at its position: an `<!--a-->` anchor in the clone, the
|
|
122
128
|
block mounted at it. `anchorCursor` parks the hydrate cursor past the anchor
|
|
123
129
|
and returns the create insertion reference; the block's parent is the located
|
|
@@ -172,10 +178,7 @@ export function generateBuild(
|
|
|
172
178
|
/* A text-leaf (only text/style children) with reactive text binds marker-free via
|
|
173
179
|
`generateChildren` on the located element; otherwise reactive text is interleaved
|
|
174
180
|
and uses `<!--a-->` anchors during the child recursion below. */
|
|
175
|
-
const
|
|
176
|
-
(child) => child.kind === 'text' || child.kind === 'style',
|
|
177
|
-
)
|
|
178
|
-
const textLeafBind = hasReactiveText && isTextLeaf
|
|
181
|
+
const textLeafBind = hasReactiveText && isTextLeaf(node)
|
|
179
182
|
let openTag = `<${node.tag}`
|
|
180
183
|
let elVar = ''
|
|
181
184
|
if (hasReactiveAttr || textLeafBind) {
|
|
@@ -496,11 +499,7 @@ export function generateBuild(
|
|
|
496
499
|
(child) =>
|
|
497
500
|
child.kind === 'element' ||
|
|
498
501
|
child.kind === 'component' ||
|
|
499
|
-
child
|
|
500
|
-
child.kind === 'each' ||
|
|
501
|
-
child.kind === 'await' ||
|
|
502
|
-
child.kind === 'try' ||
|
|
503
|
-
child.kind === 'switch' ||
|
|
502
|
+
isControlFlow(child) ||
|
|
504
503
|
child.kind === 'snippet' ||
|
|
505
504
|
(child.kind === 'text' && !isWhitespaceText(child)),
|
|
506
505
|
)
|
|
@@ -603,18 +602,6 @@ export function generateBuild(
|
|
|
603
602
|
return generateChildren(isLayout ? nodes.map(asOutlet) : nodes, hostVar)
|
|
604
603
|
}
|
|
605
604
|
|
|
606
|
-
/* A control-flow block — `if`/`each`/`await`/`switch`/`try`. In a skeleton each mounts at
|
|
607
|
-
an `<!--a-->` anchor cloned into its located parent at the block's position. */
|
|
608
|
-
function isControlFlowNode(node: TemplateNode): boolean {
|
|
609
|
-
return (
|
|
610
|
-
node.kind === 'if' ||
|
|
611
|
-
node.kind === 'each' ||
|
|
612
|
-
node.kind === 'await' ||
|
|
613
|
-
node.kind === 'switch' ||
|
|
614
|
-
node.kind === 'try'
|
|
615
|
-
)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
605
|
/* A text node that is purely whitespace (no interpolation, only blank static
|
|
619
606
|
parts). Both back-ends drop it, so it neither contributes markup nor breaks a
|
|
620
607
|
static clone run — it stays transparent so `<a/>\n<b/>` still coalesces. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
2
2
|
import { componentWrapperTag } from './componentWrapperTag.ts'
|
|
3
3
|
import { groupBindParts } from './groupBindParts.ts'
|
|
4
|
+
import { isControlFlow } from './isControlFlow.ts'
|
|
5
|
+
import { isTextLeaf } from './isTextLeaf.ts'
|
|
4
6
|
import { lowerContext } from './lowerContext.ts'
|
|
5
7
|
import { scopeAttr } from './scopeAttr.ts'
|
|
6
8
|
import { skeletonable } from './skeletonable.ts'
|
|
@@ -35,6 +37,7 @@ export function generateSSR(
|
|
|
35
37
|
nodes: TemplateNode[],
|
|
36
38
|
stateNames: ReadonlySet<string>,
|
|
37
39
|
derivedNames: ReadonlySet<string>,
|
|
40
|
+
computedNames: ReadonlySet<string>,
|
|
38
41
|
isLayout = false,
|
|
39
42
|
): string {
|
|
40
43
|
/* Compile-time counter for unique temp var names (runtime block ids, child render
|
|
@@ -47,7 +50,8 @@ export function generateSSR(
|
|
|
47
50
|
expression: lowerExpression,
|
|
48
51
|
statement,
|
|
49
52
|
withNestedScripts,
|
|
50
|
-
|
|
53
|
+
bindRead,
|
|
54
|
+
} = lowerContext(stateNames, derivedNames, computedNames)
|
|
51
55
|
|
|
52
56
|
/* A scoped-script body for SSR: the shared lowering, then strip effects
|
|
53
57
|
(client-only lifecycle that emits no HTML) — the one SSR-side asymmetry. */
|
|
@@ -98,6 +102,12 @@ export function generateSSR(
|
|
|
98
102
|
const anchorMark = (target: string): string => (inSkeleton ? push(target, '<!--a-->') : '')
|
|
99
103
|
|
|
100
104
|
function generate(node: TemplateNode, target: string): string {
|
|
105
|
+
/* A control-flow block is positioned by an `<!--a-->` anchor when in a skeleton
|
|
106
|
+
context. Emit it ONCE here — one decision site gated by the shared `isControlFlow`,
|
|
107
|
+
mirroring the client's single `skeletonMarkup` control-flow branch — rather than
|
|
108
|
+
prefixing each block kind's own `anchorMark` call (six sites that had to stay in
|
|
109
|
+
lock-step). `anchorMark` no-ops outside a skeleton, so non-block nodes ignore it. */
|
|
110
|
+
const anchor = isControlFlow(node) ? anchorMark(target) : ''
|
|
101
111
|
if (node.kind === 'text') {
|
|
102
112
|
return node.parts
|
|
103
113
|
.map((part) => {
|
|
@@ -119,7 +129,7 @@ export function generateSSR(
|
|
|
119
129
|
if (elseBranch !== undefined && elseBranch.kind === 'case') {
|
|
120
130
|
code += ` else {\n${branchContent(elseBranch.children, target)}}`
|
|
121
131
|
}
|
|
122
|
-
return `${
|
|
132
|
+
return `${anchor}${openRange(target)}${code}\n${closeRange(target)}`
|
|
123
133
|
}
|
|
124
134
|
if (node.kind === 'switch') {
|
|
125
135
|
const cases = node.children.filter(
|
|
@@ -137,7 +147,7 @@ export function generateSSR(
|
|
|
137
147
|
if (fallback !== undefined) {
|
|
138
148
|
code += `${started ? 'else ' : ''}{\n${branchContent(fallback.children, target)}}\n`
|
|
139
149
|
}
|
|
140
|
-
return `${
|
|
150
|
+
return `${anchor}${openRange(target)}${code}}\n${closeRange(target)}`
|
|
141
151
|
}
|
|
142
152
|
if (node.kind === 'case') {
|
|
143
153
|
return ''
|
|
@@ -165,15 +175,15 @@ export function generateSSR(
|
|
|
165
175
|
skeleton the `<!--a-->` anchor still marks its position (the client mounts
|
|
166
176
|
there); no range markers, since there are no server rows to claim. */
|
|
167
177
|
if (node.async) {
|
|
168
|
-
return
|
|
178
|
+
return anchor
|
|
169
179
|
}
|
|
170
|
-
return `${
|
|
180
|
+
return `${anchor}for (const ${node.as} of (${lowerExpression(node.items)})) {\n${openRange(target)}${branchContent(node.children, target)}${closeRange(target)}}\n`
|
|
171
181
|
}
|
|
172
182
|
if (node.kind === 'await') {
|
|
173
|
-
return `${
|
|
183
|
+
return `${anchor}${generateAwait(node, target)}`
|
|
174
184
|
}
|
|
175
185
|
if (node.kind === 'try') {
|
|
176
|
-
return `${
|
|
186
|
+
return `${anchor}${generateTry(node, target)}`
|
|
177
187
|
}
|
|
178
188
|
if (node.kind === 'branch') {
|
|
179
189
|
return ''
|
|
@@ -239,9 +249,9 @@ export function generateSSR(
|
|
|
239
249
|
} else if (attr.kind === 'bind' && attr.property === 'checked') {
|
|
240
250
|
/* A boolean property — its mere presence means checked, so emit the
|
|
241
251
|
attribute only when truthy (a string `checked="false"` still checks). */
|
|
242
|
-
code += `${target}.push((${
|
|
252
|
+
code += `${target}.push((${bindRead(attr.code)}) ? ' checked' : '');\n`
|
|
243
253
|
} else if (attr.kind === 'bind') {
|
|
244
|
-
code += `${target}.push(${JSON.stringify(` ${attr.property}="`)} + $esc(${
|
|
254
|
+
code += `${target}.push(${JSON.stringify(` ${attr.property}="`)} + $esc(${bindRead(attr.code)}) + '"');\n`
|
|
245
255
|
}
|
|
246
256
|
}
|
|
247
257
|
code += push(target, '>')
|
|
@@ -254,10 +264,7 @@ export function generateSSR(
|
|
|
254
264
|
inSkeleton = true
|
|
255
265
|
}
|
|
256
266
|
const previousMark = markText
|
|
257
|
-
|
|
258
|
-
(child) => child.kind === 'text' || child.kind === 'style',
|
|
259
|
-
)
|
|
260
|
-
markText = inSkeleton && !isTextLeaf
|
|
267
|
+
markText = inSkeleton && !isTextLeaf(node)
|
|
261
268
|
/* A `<script>` child scopes its bindings to this element's subtree. */
|
|
262
269
|
code += withNestedScripts(node.children, () => generateInto(node.children, target))
|
|
263
270
|
markText = previousMark
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
A control-flow block — `if`/`each`/`await`/`switch`/`try`. In a skeleton each mounts at an
|
|
5
|
+
`<!--a-->` anchor cloned into its located parent at the block's position (see `anchorCursor`),
|
|
6
|
+
so a block can sit ANYWHERE among static siblings. The shared classification under block
|
|
7
|
+
anchor placement: both back-ends — and `skeletonable` — consult this so they agree on which
|
|
8
|
+
nodes are anchor-positioned, the way `componentWrapperTag`/`isTextLeaf` already are.
|
|
9
|
+
*/
|
|
10
|
+
export function isControlFlow(node: TemplateNode): boolean {
|
|
11
|
+
return (
|
|
12
|
+
node.kind === 'if' ||
|
|
13
|
+
node.kind === 'each' ||
|
|
14
|
+
node.kind === 'await' ||
|
|
15
|
+
node.kind === 'switch' ||
|
|
16
|
+
node.kind === 'try'
|
|
17
|
+
)
|
|
18
|
+
}
|