@abide/abide 0.38.1 → 0.40.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 +5 -5
- package/CHANGELOG.md +63 -0
- package/package.json +9 -1
- package/src/build.ts +14 -0
- package/src/lib/bundle/exitWithParent.ts +4 -2
- package/src/lib/server/rpc/parseArgs.ts +10 -1
- package/src/lib/server/rpc/readBodyWithinLimit.ts +3 -0
- package/src/lib/server/rpc/runWithVerbTimeout.ts +7 -9
- package/src/lib/server/rpc/types/VerbHelper.ts +18 -21
- package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +8 -7
- package/src/lib/server/runtime/createAppAssetServer.ts +37 -11
- package/src/lib/server/runtime/createPublicAssetServer.ts +14 -5
- package/src/lib/server/runtime/createUiPageRenderer.ts +53 -42
- package/src/lib/server/runtime/internalErrorResponse.ts +5 -2
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +4 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +19 -3
- package/src/lib/server/sockets/defineSocket.ts +3 -1
- package/src/lib/shared/REF_JSON_HEADER.ts +9 -0
- package/src/lib/shared/REF_JSON_TAGS.ts +31 -0
- package/src/lib/shared/buildRpcRequest.ts +8 -1
- package/src/lib/shared/createRemoteFunction.ts +20 -11
- package/src/lib/shared/decodeRefJson.ts +110 -0
- package/src/lib/shared/encodeRefJson.ts +106 -0
- package/src/lib/shared/escapeHtml.ts +15 -0
- package/src/lib/shared/markFrameworkSourcesIgnored.ts +47 -0
- package/src/lib/shared/streamResponse.ts +8 -1
- package/src/lib/shared/types/RemoteCallable.ts +12 -3
- package/src/lib/shared/types/RpcOptions.ts +22 -0
- package/src/lib/shared/types/SourceMap.ts +14 -0
- package/src/lib/test/createTestSocketChannel.ts +6 -2
- package/src/lib/ui/compile/SSR_ESCAPE.ts +13 -3
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +5 -0
- package/src/lib/ui/compile/compileModule.ts +5 -2
- package/src/lib/ui/compile/compileSSR.ts +32 -9
- package/src/lib/ui/compile/compileShadow.ts +25 -10
- package/src/lib/ui/compile/composeProps.ts +53 -0
- package/src/lib/ui/compile/desugarSignals.ts +45 -17
- package/src/lib/ui/compile/generateBuild.ts +87 -76
- package/src/lib/ui/compile/generateSSR.ts +217 -71
- package/src/lib/ui/compile/isWhitespaceText.ts +11 -0
- package/src/lib/ui/compile/lowerDocAccess.ts +53 -16
- package/src/lib/ui/compile/parseTemplate.ts +118 -1
- package/src/lib/ui/compile/renameSignalRefs.ts +12 -0
- package/src/lib/ui/compile/resolveBranches.ts +21 -0
- package/src/lib/ui/compile/spreadExcludedNames.ts +27 -0
- package/src/lib/ui/compile/staticAttr.ts +1 -1
- package/src/lib/ui/compile/staticTextPart.ts +1 -1
- package/src/lib/ui/compile/types/TemplateAttr.ts +4 -1
- package/src/lib/ui/compile/types/TemplateNode.ts +15 -3
- package/src/lib/ui/createScope.ts +14 -0
- package/src/lib/ui/dom/appendText.ts +2 -3
- package/src/lib/ui/dom/appendTextAt.ts +2 -3
- package/src/lib/ui/dom/applyResolved.ts +5 -8
- package/src/lib/ui/dom/awaitBlock.ts +14 -1
- package/src/lib/ui/dom/mergeProps.ts +32 -0
- package/src/lib/ui/dom/on.ts +7 -0
- package/src/lib/ui/dom/parseRawNodes.ts +17 -0
- package/src/lib/ui/dom/readCall.ts +27 -0
- package/src/lib/ui/dom/restProps.ts +32 -0
- package/src/lib/ui/dom/spreadAttrs.ts +34 -0
- package/src/lib/ui/dom/spreadProps.ts +32 -0
- package/src/lib/ui/installHotBridge.ts +10 -0
- package/src/lib/ui/navigate.ts +28 -3
- package/src/lib/ui/remoteProxy.ts +68 -36
- package/src/lib/ui/renderChain.ts +39 -37
- package/src/lib/ui/renderToStream.ts +84 -68
- package/src/lib/ui/resumeSeedScript.ts +27 -0
- package/src/lib/ui/router.ts +81 -51
- package/src/lib/ui/runtime/RESUME.ts +13 -6
- package/src/lib/ui/runtime/createEffectNode.ts +5 -0
- package/src/lib/ui/runtime/localStoragePersistence.ts +21 -8
- package/src/lib/ui/runtime/toTeardown.ts +10 -5
- package/src/lib/ui/runtime/types/RenderContext.ts +8 -0
- package/src/lib/ui/runtime/types/SsrRender.ts +16 -11
- package/src/lib/ui/runtime/types/UiComponent.ts +7 -1
- package/src/lib/ui/socketChannel.ts +5 -2
- package/src/lib/ui/tryEncodeResume.ts +20 -0
- package/src/lib/ui/types/Scope.ts +9 -3
- package/src/lib/ui/compile/escapeHtml.ts +0 -15
- /package/src/lib/{server/runtime → shared}/safeJsonForScript.ts +0 -0
|
@@ -2,11 +2,15 @@ import { HOLE_ATTRIBUTE } from '../runtime/HOLE_ATTRIBUTE.ts'
|
|
|
2
2
|
import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
|
|
3
3
|
import { asOutlet } from './asOutlet.ts'
|
|
4
4
|
import { bindListenEvent } from './bindListenEvent.ts'
|
|
5
|
+
import { composeProps } from './composeProps.ts'
|
|
5
6
|
import { groupBindParts } from './groupBindParts.ts'
|
|
6
7
|
import { isControlFlow } from './isControlFlow.ts'
|
|
8
|
+
import { isWhitespaceText } from './isWhitespaceText.ts'
|
|
7
9
|
import { lowerContext } from './lowerContext.ts'
|
|
10
|
+
import { resolveBranches } from './resolveBranches.ts'
|
|
8
11
|
import { scopeAttr } from './scopeAttr.ts'
|
|
9
12
|
import { skeletonContext } from './skeletonContext.ts'
|
|
13
|
+
import { spreadExcludedNames } from './spreadExcludedNames.ts'
|
|
10
14
|
import { staticAttr } from './staticAttr.ts'
|
|
11
15
|
import { staticTextPart } from './staticTextPart.ts'
|
|
12
16
|
import type { TemplateNode } from './types/TemplateNode.ts'
|
|
@@ -21,6 +25,25 @@ surface (`count` → `model.count`) and then lowered to the doc patch/read API
|
|
|
21
25
|
`hostVar` and expects the dom bindings, `doc`, `effect`, and the component's
|
|
22
26
|
`model` in scope — the body the component compiler wraps and hoists cells into.
|
|
23
27
|
*/
|
|
28
|
+
|
|
29
|
+
/* A JS-identifier-safe frame name from an authored construct label (an attribute name
|
|
30
|
+
like `aria-label`, a bound property). Non-identifier chars → `_`; a leading digit gets
|
|
31
|
+
an `_` prefix; empty falls back to `thunk`. Callers prefix the label (`attr_`/`bind_`),
|
|
32
|
+
so the result is never a bare reserved word. */
|
|
33
|
+
function thunkName(label: string): string {
|
|
34
|
+
const safe = label.replace(/[^A-Za-z0-9_$]/g, '_').replace(/^(?=\d)/, '_')
|
|
35
|
+
return safe === '' ? 'thunk' : safe
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Names a reactive thunk so a stack frame reads `name@File.abide:line` instead of
|
|
39
|
+
`(anonymous)` — disambiguating which binding a frame is when several share a line. Emits
|
|
40
|
+
a named function expression (the only form whose name a debugger displays); the named
|
|
41
|
+
bodies never reference `this`/`arguments`, so the arrow→function swap is behaviour-safe,
|
|
42
|
+
and minify strips the name, so it costs nothing in production. */
|
|
43
|
+
function namedThunk(name: string, body: string): string {
|
|
44
|
+
return `function ${thunkName(name)}() { ${body} }`
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
export function generateBuild(
|
|
25
48
|
nodes: TemplateNode[],
|
|
26
49
|
hostVar: string,
|
|
@@ -76,7 +99,7 @@ export function generateBuild(
|
|
|
76
99
|
varName: string,
|
|
77
100
|
): string {
|
|
78
101
|
if (attr.kind === 'expression') {
|
|
79
|
-
return `attr(${varName}, ${JSON.stringify(attr.name)}, (
|
|
102
|
+
return `attr(${varName}, ${JSON.stringify(attr.name)}, ${namedThunk(`attr_${attr.name}`, `return (${lowerExpression(attr.code)})`)});\n`
|
|
80
103
|
}
|
|
81
104
|
if (attr.kind === 'event') {
|
|
82
105
|
return `on(${varName}, ${JSON.stringify(attr.event)}, (${lowerExpression(attr.code)}));\n`
|
|
@@ -95,12 +118,12 @@ export function generateBuild(
|
|
|
95
118
|
const value = lowerExpression(valueCode)
|
|
96
119
|
if (isRadio) {
|
|
97
120
|
return (
|
|
98
|
-
`effect((
|
|
121
|
+
`effect(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}) === (${value});`)});\n` +
|
|
99
122
|
`on(${varName}, "change", () => { if (${varName}.checked) { ${lowerStatement(`${attr.code} = ${valueCode}`)} } });\n`
|
|
100
123
|
)
|
|
101
124
|
}
|
|
102
125
|
return (
|
|
103
|
-
`effect((
|
|
126
|
+
`effect(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}).includes(${value});`)});\n` +
|
|
104
127
|
`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`
|
|
105
128
|
)
|
|
106
129
|
}
|
|
@@ -111,7 +134,7 @@ export function generateBuild(
|
|
|
111
134
|
`.get()` and writes via `.set(v)` — see `bindRead`/`bindWrite`. */
|
|
112
135
|
const event = bindListenEvent(attr.property, node.tag)
|
|
113
136
|
return (
|
|
114
|
-
`effect((
|
|
137
|
+
`effect(${namedThunk(`bind_${attr.property}`, `${varName}.${attr.property} = ${bindRead(attr.code)};`)});\n` +
|
|
115
138
|
`on(${varName}, ${JSON.stringify(event)}, () => { ${bindWrite(attr.code, `${varName}.${attr.property}`)} });\n`
|
|
116
139
|
)
|
|
117
140
|
}
|
|
@@ -146,7 +169,7 @@ export function generateBuild(
|
|
|
146
169
|
return staticTextPart(part.value)
|
|
147
170
|
}
|
|
148
171
|
binds.push(
|
|
149
|
-
`appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], (
|
|
172
|
+
`appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], ${namedThunk('text', `return (${lowerExpression(part.code)})`)});\n`,
|
|
150
173
|
)
|
|
151
174
|
return '<!--a-->'
|
|
152
175
|
})
|
|
@@ -219,7 +242,14 @@ export function generateBuild(
|
|
|
219
242
|
binds.push(`const ${elVar} = ${skVar}.el[${holeIndex(elIndex, node)}];\n`)
|
|
220
243
|
openTag += ` ${HOLE_ATTRIBUTE}`
|
|
221
244
|
for (const attr of node.attrs) {
|
|
222
|
-
if (attr.kind
|
|
245
|
+
if (attr.kind === 'spread') {
|
|
246
|
+
/* `{...expr}` onto the element: each key binds as a reactive attribute
|
|
247
|
+
(or an `on<event>` function as a listener) via `spreadAttrs`, skipping
|
|
248
|
+
any key explicitly named on the element (the explicit attr wins). */
|
|
249
|
+
binds.push(
|
|
250
|
+
`spreadAttrs(${elVar}, ${namedThunk('spread', `return (${lowerExpression(attr.code)})`)}, ${JSON.stringify(spreadExcludedNames(node.attrs))});\n`,
|
|
251
|
+
)
|
|
252
|
+
} else if (attr.kind !== 'static') {
|
|
223
253
|
binds.push(dynamicAttr(node, attr, elVar))
|
|
224
254
|
}
|
|
225
255
|
}
|
|
@@ -287,7 +317,7 @@ export function generateBuild(
|
|
|
287
317
|
const splitAlways = index < consumers.length - 1 ? ', true' : ''
|
|
288
318
|
return part.kind === 'static'
|
|
289
319
|
? `appendStatic(${parentVar}, ${JSON.stringify(part.value)}${splitAlways});\n`
|
|
290
|
-
: `appendText(${parentVar}, (
|
|
320
|
+
: `appendText(${parentVar}, ${namedThunk('text', `return (${lowerExpression(part.code)})`)}${splitAlways});\n`
|
|
291
321
|
})
|
|
292
322
|
.join('')
|
|
293
323
|
}
|
|
@@ -415,17 +445,17 @@ export function generateBuild(
|
|
|
415
445
|
return `if ($props && $props.$children) { ${invoke}; } else {\n${fallback}}\n`
|
|
416
446
|
}
|
|
417
447
|
|
|
418
|
-
/* The
|
|
419
|
-
|
|
420
|
-
function
|
|
421
|
-
const parts = node.props.map(
|
|
422
|
-
(prop) => `${JSON.stringify(prop.name)}: () => (${lowerExpression(prop.code)})`,
|
|
423
|
-
)
|
|
448
|
+
/* The child's slot content as a host-taking builder (`$children`), or undefined when
|
|
449
|
+
the component has no slotted children. */
|
|
450
|
+
function slotPart(node: Extract<TemplateNode, { kind: 'component' }>): string | undefined {
|
|
424
451
|
const slotCode = generateChildren(node.children, '$slot')
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
452
|
+
return slotCode.trim() === '' ? undefined : `"$children": ($slot) => {\n${slotCode}}`
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* The props bag a child mount receives — composed by the shared `composeProps` so the
|
|
456
|
+
build and SSR back-ends emit the same last-wins layering. */
|
|
457
|
+
function propsArg(node: Extract<TemplateNode, { kind: 'component' }>): string {
|
|
458
|
+
return composeProps(node.props, lowerExpression, slotPart(node))
|
|
429
459
|
}
|
|
430
460
|
|
|
431
461
|
/* Mounts a child component as a marker-bounded range on `parentVar`, positioned at
|
|
@@ -439,7 +469,7 @@ export function generateBuild(
|
|
|
439
469
|
parentVar: string,
|
|
440
470
|
before: string,
|
|
441
471
|
): string {
|
|
442
|
-
return `mountChild(${parentVar}, ${node.name},
|
|
472
|
+
return `mountChild(${parentVar}, ${node.name}, ${propsArg(node)}, ${before}, ${JSON.stringify(node.name)});\n`
|
|
443
473
|
}
|
|
444
474
|
|
|
445
475
|
/* An await block: pending → resolved(value) / error branches. Each branch is a
|
|
@@ -449,27 +479,25 @@ export function generateBuild(
|
|
|
449
479
|
parentVar: string,
|
|
450
480
|
before: string,
|
|
451
481
|
): string {
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
482
|
+
const [thenBranch, catchBranch, finallyBranch] = resolveBranches(
|
|
483
|
+
node,
|
|
484
|
+
'then',
|
|
485
|
+
'catch',
|
|
486
|
+
'finally',
|
|
487
|
+
)
|
|
488
|
+
const finallyChildren = finallyBranch?.children ?? []
|
|
456
489
|
/* Blocking: no pending, the children are the resolved branch bound to `node.as`.
|
|
457
490
|
Streaming: pending is the non-branch children, resolved is the `then` child. */
|
|
458
491
|
const pending = node.blocking
|
|
459
492
|
? []
|
|
460
493
|
: node.children.filter((child) => child.kind !== 'branch')
|
|
461
|
-
const thenBranch = node.children.find(isBranch('then'))
|
|
462
494
|
const thenThunk = node.blocking
|
|
463
495
|
? branchThunk(
|
|
464
496
|
node.children.filter((child) => child.kind !== 'branch'),
|
|
465
497
|
node.as ?? '_value',
|
|
466
498
|
finallyChildren,
|
|
467
499
|
)
|
|
468
|
-
: branchThunk(
|
|
469
|
-
branchChildren(thenBranch),
|
|
470
|
-
branchVar(thenBranch) ?? '_value',
|
|
471
|
-
finallyChildren,
|
|
472
|
-
)
|
|
500
|
+
: branchThunk(thenBranch?.children ?? [], thenBranch?.as ?? '_value', finallyChildren)
|
|
473
501
|
/* Neither catch nor finally → pass `undefined` so awaitBlock re-throws the
|
|
474
502
|
rejection (surfacing it) instead of rendering an empty branch. A finally-only
|
|
475
503
|
block keeps a catch thunk that renders just finally. */
|
|
@@ -477,8 +505,8 @@ export function generateBuild(
|
|
|
477
505
|
catchBranch === undefined && finallyChildren.length === 0
|
|
478
506
|
? 'undefined'
|
|
479
507
|
: branchThunk(
|
|
480
|
-
|
|
481
|
-
|
|
508
|
+
catchBranch?.children ?? [],
|
|
509
|
+
catchBranch?.as ?? '_error',
|
|
482
510
|
finallyChildren,
|
|
483
511
|
)
|
|
484
512
|
const pendingThunk = hasRenderableContent(pending) ? branchThunk(pending) : 'undefined'
|
|
@@ -490,16 +518,6 @@ export function generateBuild(
|
|
|
490
518
|
)
|
|
491
519
|
}
|
|
492
520
|
|
|
493
|
-
/* Children of a branch node (then/catch), or [] when the branch is absent. */
|
|
494
|
-
function branchChildren(branch: TemplateNode | undefined): TemplateNode[] {
|
|
495
|
-
return branch !== undefined && branch.kind === 'branch' ? branch.children : []
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/* The value/error variable name a branch binds, if any. */
|
|
499
|
-
function branchVar(branch: TemplateNode | undefined): string | undefined {
|
|
500
|
-
return branch !== undefined && branch.kind === 'branch' ? branch.as : undefined
|
|
501
|
-
}
|
|
502
|
-
|
|
503
521
|
/* A branch's content as a void render thunk `(parent[, value]) => void` that
|
|
504
522
|
builds its children — and an optional trailing `finally` branch — into
|
|
505
523
|
`parent`. The full-range model tracks the built content between markers, so a
|
|
@@ -538,17 +556,6 @@ export function generateBuild(
|
|
|
538
556
|
)
|
|
539
557
|
}
|
|
540
558
|
|
|
541
|
-
/* The branch child of a control block matching `which` (then/catch/finally). */
|
|
542
|
-
function findBranch(
|
|
543
|
-
children: TemplateNode[],
|
|
544
|
-
which: 'then' | 'catch' | 'finally',
|
|
545
|
-
): Extract<TemplateNode, { kind: 'branch' }> | undefined {
|
|
546
|
-
return children.find(
|
|
547
|
-
(child): child is Extract<TemplateNode, { kind: 'branch' }> =>
|
|
548
|
-
child.kind === 'branch' && child.branch === which,
|
|
549
|
-
)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
559
|
/* A sync error boundary: build the guarded subtree (++ finally); a throw while
|
|
553
560
|
building swaps to the catch branch (++ finally). No catch → `undefined`, which
|
|
554
561
|
makes the runtime re-throw to the nearest enclosing boundary. */
|
|
@@ -557,18 +564,14 @@ export function generateBuild(
|
|
|
557
564
|
parentVar: string,
|
|
558
565
|
before: string,
|
|
559
566
|
): string {
|
|
560
|
-
const catchBranch =
|
|
561
|
-
const finallyChildren =
|
|
567
|
+
const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
|
|
568
|
+
const finallyChildren = finallyBranch?.children ?? []
|
|
562
569
|
const guarded = node.children.filter((child) => child.kind !== 'branch')
|
|
563
570
|
const tryThunk = branchThunk(guarded, undefined, finallyChildren)
|
|
564
571
|
const catchThunk =
|
|
565
572
|
catchBranch === undefined
|
|
566
573
|
? 'undefined'
|
|
567
|
-
: branchThunk(
|
|
568
|
-
branchChildren(catchBranch),
|
|
569
|
-
branchVar(catchBranch) ?? '_error',
|
|
570
|
-
finallyChildren,
|
|
571
|
-
)
|
|
574
|
+
: branchThunk(catchBranch.children, catchBranch.as ?? '_error', finallyChildren)
|
|
572
575
|
return `tryBlock(${parentVar}, nextBlockId(), ${tryThunk}, ${catchThunk}, ${before});\n`
|
|
573
576
|
}
|
|
574
577
|
|
|
@@ -579,13 +582,33 @@ export function generateBuild(
|
|
|
579
582
|
parentVar: string,
|
|
580
583
|
before: string,
|
|
581
584
|
): string {
|
|
582
|
-
|
|
585
|
+
/* The `case` children are the chain's `elseif`/`else` branches in source order;
|
|
586
|
+
the rest are the `then` content. */
|
|
587
|
+
const branches = node.children.filter(
|
|
583
588
|
(child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
|
|
584
589
|
)
|
|
585
590
|
const thenChildren = node.children.filter((child) => child.kind !== 'case')
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
591
|
+
const hasElseif = branches.some((branch) => branch.condition !== undefined)
|
|
592
|
+
/* Fast path: a plain `if` (with optional `else`) is the binary `when` runtime. */
|
|
593
|
+
if (!hasElseif) {
|
|
594
|
+
const elseBranch = branches.find((branch) => branch.condition === undefined)
|
|
595
|
+
const thenThunk = branchThunk(thenChildren)
|
|
596
|
+
const elseThunk =
|
|
597
|
+
elseBranch === undefined ? 'undefined' : branchThunk(elseBranch.children)
|
|
598
|
+
return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
|
|
599
|
+
}
|
|
600
|
+
/* if/elseif/else is a cond-chain — reuse `switchBlock` over a constant `true`
|
|
601
|
+
subject with `Boolean`-coerced match thunks, so the first truthy branch wins
|
|
602
|
+
(`else` is the match-less default). */
|
|
603
|
+
const entries = [
|
|
604
|
+
`{ match: () => Boolean(${lowerExpression(node.condition)}), render: ${branchThunk(thenChildren)} }`,
|
|
605
|
+
...branches.map((branch) =>
|
|
606
|
+
branch.condition !== undefined
|
|
607
|
+
? `{ match: () => Boolean(${lowerExpression(branch.condition)}), render: ${branchThunk(branch.children)} }`
|
|
608
|
+
: `{ match: undefined, render: ${branchThunk(branch.children)} }`,
|
|
609
|
+
),
|
|
610
|
+
]
|
|
611
|
+
return `switchBlock(${parentVar}, () => true, [${entries.join(', ')}], ${before});\n`
|
|
589
612
|
}
|
|
590
613
|
|
|
591
614
|
/* A keyed each. Each row is a content RANGE (any content, tracked between the
|
|
@@ -607,11 +630,9 @@ export function generateBuild(
|
|
|
607
630
|
optional `<template catch>` branch rendered (after the streamed rows) when the
|
|
608
631
|
iterator rejects. Absent → `undefined`, so the rejection surfaces instead. */
|
|
609
632
|
const fn = node.async ? 'eachAsync' : 'each'
|
|
610
|
-
const catchBranch = node
|
|
611
|
-
(child) => child.kind === 'branch' && child.branch === 'catch',
|
|
612
|
-
)
|
|
633
|
+
const [catchBranch] = resolveBranches(node, 'catch')
|
|
613
634
|
const catchArg = node.async
|
|
614
|
-
? `, ${catchBranch === undefined ? 'undefined' : branchThunk(
|
|
635
|
+
? `, ${catchBranch === undefined ? 'undefined' : branchThunk(catchBranch.children, catchBranch.as ?? '_error')}`
|
|
615
636
|
: ''
|
|
616
637
|
return (
|
|
617
638
|
`${fn}(${parentVar}, () => (${lowerExpression(node.items)}), ` +
|
|
@@ -622,16 +643,6 @@ export function generateBuild(
|
|
|
622
643
|
return generateChildren(rootNodes, hostVar)
|
|
623
644
|
}
|
|
624
645
|
|
|
625
|
-
/* A text node that is purely whitespace (no interpolation, only blank static
|
|
626
|
-
parts). Both back-ends drop it, so it neither contributes markup nor breaks a
|
|
627
|
-
static clone run — it stays transparent so `<a/>\n<b/>` still coalesces. */
|
|
628
|
-
function isWhitespaceText(node: TemplateNode): boolean {
|
|
629
|
-
return (
|
|
630
|
-
node.kind === 'text' &&
|
|
631
|
-
node.parts.every((part) => part.kind === 'static' && part.value.trim() === '')
|
|
632
|
-
)
|
|
633
|
-
}
|
|
634
|
-
|
|
635
646
|
/*
|
|
636
647
|
Whether an element subtree is fully static — no reactive/event/bind attributes,
|
|
637
648
|
no nested `<script>`, and every descendant likewise static (static text, static
|