@abide/abide 0.43.1 → 0.44.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/AGENTS.md +293 -219
  2. package/CHANGELOG.md +63 -0
  3. package/README.md +191 -67
  4. package/package.json +2 -2
  5. package/src/abideLsp.ts +8 -4
  6. package/src/abideResolverPlugin.ts +17 -2
  7. package/src/build.ts +9 -0
  8. package/src/buildDisconnected.ts +2 -0
  9. package/src/devEntry.ts +44 -9
  10. package/src/lib/bundle/probeAbideServer.ts +1 -1
  11. package/src/lib/cli/parseArgvForRpc.ts +25 -26
  12. package/src/lib/cli/runCli.ts +9 -24
  13. package/src/lib/cli/tokenizeArgvFlags.ts +84 -0
  14. package/src/lib/cli/types/CliManifest.ts +6 -4
  15. package/src/lib/mcp/annotationsForMethod.ts +1 -1
  16. package/src/lib/mcp/toolResultFromResponse.ts +1 -1
  17. package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
  18. package/src/lib/server/AppModule.ts +1 -1
  19. package/src/lib/server/cli/handleCliDownload.ts +7 -5
  20. package/src/lib/server/cli/installScript.ts +3 -3
  21. package/src/lib/server/prompts/promptRegistry.ts +1 -1
  22. package/src/lib/server/prompts/types/Prompt.ts +2 -1
  23. package/src/lib/server/rpc/defineRpc.ts +9 -1
  24. package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
  25. package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
  26. package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
  27. package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
  28. package/src/lib/server/runtime/createServer.ts +50 -12
  29. package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
  30. package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
  31. package/src/lib/server/runtime/crossOriginGate.ts +1 -1
  32. package/src/lib/server/runtime/devHotModuleResponse.ts +3 -3
  33. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
  34. package/src/lib/server/runtime/gzipResponse.ts +14 -16
  35. package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
  36. package/src/lib/server/runtime/runWithRequestScope.ts +4 -1
  37. package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
  38. package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
  39. package/src/lib/server/runtime/streamFromIterator.ts +2 -2
  40. package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
  41. package/src/lib/server/runtime/types/RequestStore.ts +16 -0
  42. package/src/lib/server/runtime/withResponseDefaults.ts +8 -3
  43. package/src/lib/server/socket.ts +1 -1
  44. package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
  45. package/src/lib/server/sockets/defineSocket.ts +5 -1
  46. package/src/lib/server/sockets/socketRegistry.ts +2 -2
  47. package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
  48. package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
  49. package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
  50. package/src/lib/shared/RPC_ARGS_TYPE.ts +7 -0
  51. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
  52. package/src/lib/shared/activeCacheStore.ts +5 -14
  53. package/src/lib/shared/activePage.ts +5 -19
  54. package/src/lib/shared/augmentModule.ts +18 -0
  55. package/src/lib/shared/basePath.ts +6 -5
  56. package/src/lib/shared/baseResolver.ts +10 -0
  57. package/src/lib/shared/baseSlot.ts +6 -12
  58. package/src/lib/shared/bodyValueForKind.ts +35 -0
  59. package/src/lib/shared/cache.ts +22 -2
  60. package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
  61. package/src/lib/shared/cacheStoreResolver.ts +12 -0
  62. package/src/lib/shared/cacheStoreSlot.ts +5 -13
  63. package/src/lib/shared/changeAffectsClient.ts +35 -0
  64. package/src/lib/shared/createPushIterator.ts +1 -1
  65. package/src/lib/shared/createResolverSlot.ts +37 -0
  66. package/src/lib/shared/debugGate.ts +90 -0
  67. package/src/lib/shared/decodeResponse.ts +11 -8
  68. package/src/lib/shared/emitLogRecord.ts +18 -4
  69. package/src/lib/shared/globalCacheStoreResolver.ts +12 -0
  70. package/src/lib/shared/globalCacheStoreSlot.ts +6 -11
  71. package/src/lib/shared/hasReplayableRequest.ts +17 -0
  72. package/src/lib/shared/html.ts +7 -26
  73. package/src/lib/shared/isDebugEnabled.ts +3 -10
  74. package/src/lib/shared/isDebugNegated.ts +4 -6
  75. package/src/lib/shared/manifestModule.ts +2 -2
  76. package/src/lib/shared/online.ts +1 -1
  77. package/src/lib/shared/outboxProbeSlot.ts +1 -1
  78. package/src/lib/shared/pageResolver.ts +16 -0
  79. package/src/lib/shared/pageSlot.ts +5 -14
  80. package/src/lib/shared/pageUrlForFile.ts +1 -1
  81. package/src/lib/shared/requestScopeResolver.ts +12 -0
  82. package/src/lib/shared/requestScopeSlot.ts +7 -12
  83. package/src/lib/shared/responseBodyKind.ts +35 -0
  84. package/src/lib/shared/routeParamsShape.ts +20 -0
  85. package/src/lib/shared/setBaseResolver.ts +2 -4
  86. package/src/lib/shared/setCacheStoreResolver.ts +3 -5
  87. package/src/lib/shared/setGlobalCacheStoreResolver.ts +3 -5
  88. package/src/lib/shared/setPageResolver.ts +2 -5
  89. package/src/lib/shared/setRequestScopeResolver.ts +3 -5
  90. package/src/lib/shared/snapshotShippable.ts +22 -0
  91. package/src/lib/shared/types/CacheEntry.ts +1 -1
  92. package/src/lib/shared/types/CacheInvalidation.ts +3 -3
  93. package/src/lib/shared/types/ClientFlags.ts +5 -3
  94. package/src/lib/shared/types/DebugGate.ts +10 -0
  95. package/src/lib/shared/types/LastConnection.ts +1 -1
  96. package/src/lib/shared/types/ResolverSlot.ts +10 -0
  97. package/src/lib/shared/url.ts +4 -1
  98. package/src/lib/shared/withBase.ts +1 -2
  99. package/src/lib/shared/writeDts.ts +10 -3
  100. package/src/lib/shared/writePublicAssetsDts.ts +5 -9
  101. package/src/lib/shared/writeRoutesDts.ts +14 -46
  102. package/src/lib/shared/writeRpcDts.ts +13 -12
  103. package/src/lib/shared/writeTestRpcDts.ts +11 -11
  104. package/src/lib/shared/writeTestSocketsDts.ts +9 -9
  105. package/src/lib/test/createTestSocketChannel.ts +3 -3
  106. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
  107. package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
  108. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
  109. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
  110. package/src/lib/ui/compile/abideUiPlugin.ts +10 -8
  111. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +27 -13
  112. package/src/lib/ui/compile/awaitPlan.ts +14 -2
  113. package/src/lib/ui/compile/catchBinding.ts +8 -0
  114. package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
  115. package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
  116. package/src/lib/ui/compile/compileComponent.ts +4 -4
  117. package/src/lib/ui/compile/compileModule.ts +56 -21
  118. package/src/lib/ui/compile/compileSSR.ts +12 -10
  119. package/src/lib/ui/compile/compileShadow.ts +163 -12
  120. package/src/lib/ui/compile/composeProps.ts +3 -3
  121. package/src/lib/ui/compile/createShadowLanguageService.ts +58 -4
  122. package/src/lib/ui/compile/createShadowProgram.ts +2 -1
  123. package/src/lib/ui/compile/createShadowScope.ts +42 -0
  124. package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
  125. package/src/lib/ui/compile/desugarSignals.ts +12 -7
  126. package/src/lib/ui/compile/eachPlan.ts +61 -0
  127. package/src/lib/ui/compile/elementPlan.ts +55 -0
  128. package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
  129. package/src/lib/ui/compile/generateBuild.ts +156 -127
  130. package/src/lib/ui/compile/generateSSR.ts +112 -73
  131. package/src/lib/ui/compile/ifPlan.ts +7 -1
  132. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
  133. package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
  134. package/src/lib/ui/compile/isTextLeaf.ts +8 -7
  135. package/src/lib/ui/compile/lowerContext.ts +36 -46
  136. package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
  137. package/src/lib/ui/compile/lowerScript.ts +1 -1
  138. package/src/lib/ui/compile/markupTokens.ts +313 -0
  139. package/src/lib/ui/compile/pagePropsType.ts +23 -0
  140. package/src/lib/ui/compile/parseTemplate.ts +135 -62
  141. package/src/lib/ui/compile/reactiveBinding.ts +47 -0
  142. package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
  143. package/src/lib/ui/compile/skeletonable.ts +5 -5
  144. package/src/lib/ui/compile/snippetPlan.ts +28 -0
  145. package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
  146. package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
  147. package/src/lib/ui/compile/switchPlan.ts +7 -1
  148. package/src/lib/ui/compile/tryPlan.ts +8 -1
  149. package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
  150. package/src/lib/ui/compile/types/Binding.ts +24 -0
  151. package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
  152. package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
  153. package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
  154. package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
  155. package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
  156. package/src/lib/ui/compile/types/TemplateNode.ts +9 -9
  157. package/src/lib/ui/compile/withBindings.ts +45 -0
  158. package/src/lib/ui/createScope.ts +2 -2
  159. package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
  160. package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
  161. package/src/lib/ui/dom/appendText.ts +1 -1
  162. package/src/lib/ui/dom/applyResolved.ts +1 -1
  163. package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
  164. package/src/lib/ui/dom/cloneStatic.ts +3 -2
  165. package/src/lib/ui/dom/each.ts +26 -7
  166. package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
  167. package/src/lib/ui/dom/mountChild.ts +8 -1
  168. package/src/lib/ui/dom/mountSwappableRange.ts +79 -0
  169. package/src/lib/ui/dom/readCall.ts +6 -4
  170. package/src/lib/ui/dom/scopeLabel.ts +3 -3
  171. package/src/lib/ui/dom/skeleton.ts +10 -1
  172. package/src/lib/ui/dom/switchBlock.ts +17 -63
  173. package/src/lib/ui/dom/types/EachRow.ts +4 -0
  174. package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
  175. package/src/lib/ui/dom/when.ts +11 -64
  176. package/src/lib/ui/history.ts +1 -2
  177. package/src/lib/ui/html.ts +28 -0
  178. package/src/lib/ui/installHotBridge.ts +0 -2
  179. package/src/lib/ui/renderChain.ts +16 -3
  180. package/src/lib/ui/router.ts +24 -3
  181. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
  182. package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
  183. package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
  184. package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
  185. package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
  186. package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
  187. package/src/lib/ui/runtime/createDoc.ts +107 -13
  188. package/src/lib/ui/runtime/createEffectNode.ts +5 -5
  189. package/src/lib/ui/runtime/flushEffects.ts +22 -9
  190. package/src/lib/ui/runtime/historyEntries.ts +1 -1
  191. package/src/lib/ui/runtime/track.ts +4 -2
  192. package/src/lib/ui/runtime/trigger.ts +34 -20
  193. package/src/lib/ui/runtime/types/Route.ts +2 -2
  194. package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
  195. package/src/lib/ui/runtime/types/UiProps.ts +9 -12
  196. package/src/lib/ui/runtime/walkPath.ts +15 -3
  197. package/src/lib/ui/startClient.ts +41 -14
  198. package/src/lib/ui/state.ts +3 -3
  199. package/src/lib/ui/types/Scope.ts +3 -3
  200. package/src/serverBuildPlugins.ts +11 -5
  201. package/src/zodCjsPlugin.ts +51 -0
  202. package/template/.zed/settings.json +7 -1
  203. package/template/package.json +3 -0
  204. package/template/src/ui/pages/layout.abide +3 -5
  205. package/src/lib/shared/matchesDebugPattern.ts +0 -16
  206. package/src/lib/shared/parseDebugPatterns.ts +0 -21
@@ -6,23 +6,30 @@ import { asOutlet } from './asOutlet.ts'
6
6
  import { awaitPlan } from './awaitPlan.ts'
7
7
  import { bindListenEvent } from './bindListenEvent.ts'
8
8
  import { composeProps } from './composeProps.ts'
9
- import { destructureBindingNames } from './destructureBindingNames.ts'
9
+ import { eachPlan } from './eachPlan.ts'
10
+ import { elementPlan } from './elementPlan.ts'
10
11
  import { groupBindParts } from './groupBindParts.ts'
11
12
  import { ifPlan } from './ifPlan.ts'
13
+ import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
12
14
  import { isControlFlow } from './isControlFlow.ts'
15
+ import { isPlainIdentifier } from './isPlainIdentifier.ts'
13
16
  import { isWhitespaceText } from './isWhitespaceText.ts'
14
17
  import { lowerContext } from './lowerContext.ts'
15
18
  import { makeVarNamer } from './makeVarNamer.ts'
16
- import { resolveBranches } from './resolveBranches.ts'
19
+ import { reactiveBinding } from './reactiveBinding.ts'
17
20
  import { scopeAttr } from './scopeAttr.ts'
18
21
  import { skeletonContext } from './skeletonContext.ts'
22
+ import { snippetPlan } from './snippetPlan.ts'
19
23
  import { spreadExcludedNames } from './spreadExcludedNames.ts'
20
24
  import { staticAttr } from './staticAttr.ts'
21
25
  import { staticTextPart } from './staticTextPart.ts'
22
26
  import { switchPlan } from './switchPlan.ts'
23
27
  import { tryPlan } from './tryPlan.ts'
28
+ import type { Binding } from './types/Binding.ts'
29
+ import type { ShadowKind } from './types/ShadowKind.ts'
24
30
  import type { TemplateNode } from './types/TemplateNode.ts'
25
31
  import { VOID_TAGS } from './VOID_TAGS.ts'
32
+ import { withBindings } from './withBindings.ts'
26
33
 
27
34
  /* The skeleton positioning anchor a control-flow block / component / slot / outlet
28
35
  stamps into its skeleton markup, sourced from the same `ANCHOR` wire-alphabet constant
@@ -36,8 +43,9 @@ attributes, reactive `attr`/`text` bindings, `on` listeners, keyed `each`, and
36
43
  conditional `when`. Every embedded expression is first rewritten from the signal
37
44
  surface (`count` → `model.count`) and then lowered to the doc patch/read API
38
45
  (cell-hoisting runs over the whole result afterwards). The output operates on
39
- `hostVar` and expects the dom bindings, `doc`, `effect`, and the component's
40
- `model` in scope — the body the component compiler wraps and hoists cells into.
46
+ `hostVar` and expects the `$$`-aliased dom bindings (`$$effect`, `$$each`, …)
47
+ and the component's `$$model` (emitted by `desugarSignals`) in scope — the body
48
+ the component compiler wraps and hoists cells into.
41
49
  */
42
50
 
43
51
  /* A JS-identifier-safe frame name from an authored construct label (an attribute name
@@ -97,49 +105,17 @@ export function generateBuild(
97
105
  expression: lowerExpression,
98
106
  statement: lowerStatement,
99
107
  withNestedScripts,
100
- withLocalDerived,
108
+ withShadow,
101
109
  bindRead,
102
110
  bindWrite,
103
111
  } = lowerContext(stateNames, derivedNames, computedNames)
104
112
 
105
- /* A value binding the runtime can update in place (an `await` `then` value, a keyed `each`
106
- item) is passed as a reactive `.value` cell; the consuming branch/row reads its NAME(s)
107
- as a deref so it re-runs in place when a re-settle/re-key sets the cell. */
108
- const isPlainIdentifier = (name: string | undefined): name is string =>
109
- name !== undefined && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)
110
-
111
- /* How a reactive value param binds: `param` is the thunk's value parameter (the cell),
112
- `prefix` declares any per-leaf readers, `localNames` enter the deref scope for the body.
113
- A plain identifier reads the cell directly (`item` → `item.value`); a destructure
114
- re-applies over the cell per read so each leaf stays reactive (JS handles
115
- defaults/rest/rename/nesting). The caller lowers the body with `localNames` derived. */
116
- function reactiveBinding(authorParam: string): {
117
- param: string
118
- prefix: string
119
- localNames: string[]
120
- } {
121
- if (isPlainIdentifier(authorParam)) {
122
- return { param: authorParam, prefix: '', localNames: [authorParam] }
123
- }
124
- const cellParam = nextVar('aw')
125
- const deriveVar = nextVar('ad')
126
- const leaves = destructureBindingNames(authorParam)
127
- /* Lower the destructure declaration so a default/computed-key initializer that
128
- references a component signal (`{ label = fallback }`, `{ [key]: v }`) is rewritten
129
- to its `model`/cell form — the bound leaf names are name-slots and stay untouched.
130
- A pattern with no such initializer lowers to itself, so the common case is unchanged
131
- and SSR (which lowers the same declaration) stays congruent. */
132
- const declaration = lowerStatement(`const ${authorParam} = ${cellParam}.value`)
133
- const prefix =
134
- `const ${deriveVar} = { get value() { ${declaration} return { ${leaves.join(', ')} }; } };\n` +
135
- leaves
136
- .map(
137
- (leaf) =>
138
- `const ${leaf} = { get value() { return ${deriveVar}.value.${leaf}; } };\n`,
139
- )
140
- .join('')
141
- return { param: cellParam, prefix, localNames: leaves }
142
- }
113
+ /* Maps a plan `Binding`'s classification to the client `ShadowKind`: a `reactive` value
114
+ derefs as a `.value` cell (`derived`), a `plain` value as the bare local (`plain`). The
115
+ injected mapping `withBindings` registers every binding through the one place a name's
116
+ kind is chosen on this back-end. */
117
+ const buildBindingKind = (binding: Binding): ShadowKind =>
118
+ binding.classification === 'reactive' ? 'derived' : 'plain'
143
119
 
144
120
  /* Emits the wiring for one non-static attribute against an already-obtained skeleton
145
121
  element var — reactive `attr`, `on` listener, `attach`, or a two-way `bind`. */
@@ -147,27 +123,42 @@ export function generateBuild(
147
123
  node: Extract<TemplateNode, { kind: 'element' }>,
148
124
  attr: Extract<
149
125
  (typeof node.attrs)[number],
150
- { kind: 'expression' | 'event' | 'attach' | 'bind' | 'class' | 'style' }
126
+ {
127
+ kind:
128
+ | 'expression'
129
+ | 'interpolated'
130
+ | 'event'
131
+ | 'attach'
132
+ | 'bind'
133
+ | 'class'
134
+ | 'style'
135
+ }
151
136
  >,
152
137
  varName: string,
153
138
  ): string {
154
139
  if (attr.kind === 'expression') {
155
- return `attr(${varName}, ${JSON.stringify(attr.name)}, ${namedThunk(`attr_${attr.name}`, `return (${lowerExpression(attr.code)})`)});\n`
140
+ return `$$attr(${varName}, ${JSON.stringify(attr.name)}, ${namedThunk(`attr_${attr.name}`, `return (${lowerExpression(attr.code)})`)});\n`
141
+ }
142
+ /* `name="literal {expr}"` — the template-literal concatenation bound as a reactive
143
+ string attribute (always present). A class/style with directives is merged
144
+ upstream of this dispatch, so it never reaches here. */
145
+ if (attr.kind === 'interpolated') {
146
+ return `$$attr(${varName}, ${JSON.stringify(attr.name)}, ${namedThunk(`attr_${attr.name}`, `return (${lowerExpression(interpolatedTemplateLiteral(attr.parts))})`)});\n`
156
147
  }
157
148
  if (attr.kind === 'event') {
158
- return `on(${varName}, ${JSON.stringify(attr.event)}, (${lowerExpression(attr.code)}));\n`
149
+ return `$$on(${varName}, ${JSON.stringify(attr.event)}, (${lowerExpression(attr.code)}));\n`
159
150
  }
160
151
  if (attr.kind === 'attach') {
161
- return `attach(${varName}, (${lowerExpression(attr.code)}));\n`
152
+ return `$$attach(${varName}, (${lowerExpression(attr.code)}));\n`
162
153
  }
163
154
  /* `class:<name>` — toggle the class by truthiness; surgical, no element re-render.
164
155
  Layers on top of any static `class="…"` in the skeleton (classList is additive). */
165
156
  if (attr.kind === 'class') {
166
- return `effect(${namedThunk(`class_${attr.name}`, `${varName}.classList.toggle(${JSON.stringify(attr.name)}, !!(${lowerExpression(attr.code)}));`)});\n`
157
+ return `$$effect(${namedThunk(`class_${attr.name}`, `${varName}.classList.toggle(${JSON.stringify(attr.name)}, !!(${lowerExpression(attr.code)}));`)});\n`
167
158
  }
168
159
  /* `style:<property>` — write one inline style / custom property reactively. */
169
160
  if (attr.kind === 'style') {
170
- return `effect(${namedThunk(`style_${attr.property}`, `${varName}.style.setProperty(${JSON.stringify(attr.property)}, String(${lowerExpression(attr.code)}));`)});\n`
161
+ return `$$effect(${namedThunk(`style_${attr.property}`, `${varName}.style.setProperty(${JSON.stringify(attr.property)}, String(${lowerExpression(attr.code)}));`)});\n`
171
162
  }
172
163
  if (attr.property === 'group') {
173
164
  /* Grouped two-way: radio binds the path to the single checked `value`;
@@ -180,13 +171,13 @@ export function generateBuild(
180
171
  const value = lowerExpression(valueCode)
181
172
  if (isRadio) {
182
173
  return (
183
- `effect(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}) === (${value});`)});\n` +
184
- `on(${varName}, "change", () => { if (${varName}.checked) { ${lowerStatement(`${attr.code} = ${valueCode}`)} } });\n`
174
+ `$$effect(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}) === (${value});`)});\n` +
175
+ `$$on(${varName}, "change", () => { if (${varName}.checked) { ${lowerStatement(`${attr.code} = ${valueCode}`)} } });\n`
185
176
  )
186
177
  }
187
178
  return (
188
- `effect(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}).includes(${value});`)});\n` +
189
- `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`
179
+ `$$effect(${namedThunk('bind_group', `${varName}.checked = (${lowerExpression(attr.code)}).includes(${value});`)});\n` +
180
+ `$$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`
190
181
  )
191
182
  }
192
183
  /* Two-way: drive the property from the bind target, and write it back on the
@@ -196,8 +187,8 @@ export function generateBuild(
196
187
  `.get()` and writes via `.set(v)` — see `bindRead`/`bindWrite`. */
197
188
  const event = bindListenEvent(attr.property, node.tag)
198
189
  return (
199
- `effect(${namedThunk(`bind_${attr.property}`, `${varName}.${attr.property} = ${bindRead(attr.code)};`)});\n` +
200
- `on(${varName}, ${JSON.stringify(event)}, () => { ${bindWrite(attr.code, `${varName}.${attr.property}`)} });\n`
190
+ `$$effect(${namedThunk(`bind_${attr.property}`, `${varName}.${attr.property} = ${bindRead(attr.code)};`)});\n` +
191
+ `$$on(${varName}, ${JSON.stringify(event)}, () => { ${bindWrite(attr.code, `${varName}.${attr.property}`)} });\n`
201
192
  )
202
193
  }
203
194
 
@@ -231,7 +222,7 @@ export function generateBuild(
231
222
  return staticTextPart(part.value)
232
223
  }
233
224
  binds.push(
234
- `appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], ${namedThunk('text', `return (${lowerExpression(part.code)})`)});\n`,
225
+ `$$appendTextAt(${skVar}.an[${holeIndex(anIndex, part)}], ${namedThunk('text', `return (${lowerExpression(part.code)})`)});\n`,
235
226
  )
236
227
  return ANCHOR_COMMENT
237
228
  })
@@ -245,7 +236,9 @@ export function generateBuild(
245
236
  (`anchor.parentNode`). A component takes an anchor like a block — no wrapper
246
237
  element — so its root lays out as a true direct child of `anchor.parentNode`. */
247
238
  const anchorVar = anchorVarAt(node, skVar, binds)
248
- binds.push(generateChild(node, `${anchorVar}.parentNode`, `anchorCursor(${anchorVar})`))
239
+ binds.push(
240
+ generateChild(node, `${anchorVar}.parentNode`, `$$anchorCursor(${anchorVar})`),
241
+ )
249
242
  return ANCHOR_COMMENT
250
243
  }
251
244
  if (node.kind === 'script') {
@@ -270,7 +263,7 @@ export function generateBuild(
270
263
  `outlet` boundary the router fills with the next chain layer (`fillBoundary`).
271
264
  No wrapper element — the filled child lays out as a direct child of the parent. */
272
265
  const anchorVar = anchorVarAt(node, skVar, binds)
273
- binds.push(`outlet(${anchorVar}.parentNode, anchorCursor(${anchorVar}));\n`)
266
+ binds.push(`$$outlet(${anchorVar}.parentNode, $$anchorCursor(${anchorVar}));\n`)
274
267
  return ANCHOR_COMMENT
275
268
  }
276
269
  if (node.tag === 'slot') {
@@ -279,7 +272,7 @@ export function generateBuild(
279
272
  const anchorVar = anchorVarAt(node, skVar, binds)
280
273
  const hostVar = nextVar('host')
281
274
  binds.push(
282
- `mountSlot(${anchorVar}.parentNode, (${hostVar}) => {\n${generateSlot(node, hostVar)}}, anchorCursor(${anchorVar}));\n`,
275
+ `$$mountSlot(${anchorVar}.parentNode, (${hostVar}) => {\n${generateSlot(node, hostVar)}}, $$anchorCursor(${anchorVar}));\n`,
283
276
  )
284
277
  return ANCHOR_COMMENT
285
278
  }
@@ -294,6 +287,12 @@ export function generateBuild(
294
287
  SSR back-end also reads, rather than re-deriving leaf-ness via `isTextLeaf` here. */
295
288
  const textLeafBind =
296
289
  reactiveTextChild !== undefined && markText.get(reactiveTextChild) === false
290
+ /* The shared per-element emission DECISION (one site both back-ends consult): each
291
+ attribute classified + tagged with its merge status, the class/style/directive merge
292
+ folded in, and void-tag status. The composed `classParts`/`styleParts` and the
293
+ merge triggers come from the same plan SSR renders, so neither the attribute set nor
294
+ the merged value can drift. Build still RENDERS each kind below as live wiring. */
295
+ const plan = elementPlan(node, lowerExpression)
297
296
  let openTag = `<${node.tag}`
298
297
  let elVar = ''
299
298
  if (hasReactiveAttr || textLeafBind) {
@@ -303,13 +302,33 @@ export function generateBuild(
303
302
  elVar = nextVar('el')
304
303
  binds.push(`const ${elVar} = ${skVar}.el[${holeIndex(elIndex, node)}];\n`)
305
304
  openTag += ` ${HOLE_ATTRIBUTE}`
306
- for (const attr of node.attrs) {
305
+ /* An interpolated (reactive) class/style base can't layer additive directive toggles
306
+ on top — re-setting the base would wipe them — so base + its directives collapse
307
+ into ONE effect computing the whole value (`mergeClassBuild`/`mergeStyleBuild`,
308
+ the build trigger); their attrs are then skipped (`mergedBuild`) in the dispatch
309
+ below. A STATIC base keeps the surgical-toggle model (the base sits in the cloned
310
+ skeleton). */
311
+ if (plan.merge.mergeClassBuild) {
312
+ binds.push(
313
+ `$$effect(${namedThunk('class_merge', `${elVar}.setAttribute("class", [${plan.merge.classParts.join(', ')}].filter(Boolean).join(' '));`)});\n`,
314
+ )
315
+ }
316
+ if (plan.merge.mergeStyleBuild) {
317
+ binds.push(
318
+ `$$effect(${namedThunk('style_merge', `${elVar}.setAttribute("style", [${plan.merge.styleParts.join(', ')}].filter(Boolean).join(';'));`)});\n`,
319
+ )
320
+ }
321
+ for (const { attr, mergedBuild } of plan.attrs) {
322
+ /* Skip the attrs already folded into a merged class/style effect. */
323
+ if (mergedBuild) {
324
+ continue
325
+ }
307
326
  if (attr.kind === 'spread') {
308
327
  /* `{...expr}` onto the element: each key binds as a reactive attribute
309
328
  (or an `on<event>` function as a listener) via `spreadAttrs`, skipping
310
329
  any key explicitly named on the element (the explicit attr wins). */
311
330
  binds.push(
312
- `spreadAttrs(${elVar}, ${namedThunk('spread', `return (${lowerExpression(attr.code)})`)}, ${JSON.stringify(spreadExcludedNames(node.attrs))});\n`,
331
+ `$$spreadAttrs(${elVar}, ${namedThunk('spread', `return (${lowerExpression(attr.code)})`)}, ${JSON.stringify(spreadExcludedNames(node.attrs))});\n`,
313
332
  )
314
333
  } else if (attr.kind !== 'static') {
315
334
  binds.push(dynamicAttr(node, attr, elVar))
@@ -319,13 +338,16 @@ export function generateBuild(
319
338
  for (const scope of node.scopes ?? []) {
320
339
  openTag += scopeAttr(scope)
321
340
  }
322
- for (const attr of node.attrs) {
323
- if (attr.kind === 'static') {
341
+ /* Static attrs sit in the cloned skeleton markup — emitted here regardless of whether
342
+ the element is a hole. A static class/style folded into a merged attribute (build
343
+ trigger) is skipped, since the merge effect re-sets the whole value. */
344
+ for (const { attr, mergedBuild } of plan.attrs) {
345
+ if (attr.kind === 'static' && !mergedBuild) {
324
346
  openTag += staticAttr(attr.name, attr.value)
325
347
  }
326
348
  }
327
349
  openTag += '>'
328
- if (VOID_TAGS.has(node.tag)) {
350
+ if (plan.isVoid) {
329
351
  return openTag
330
352
  }
331
353
  if (textLeafBind) {
@@ -352,7 +374,7 @@ export function generateBuild(
352
374
  const skVar = nextVar('sk')
353
375
  const binds: string[] = []
354
376
  const html = skeletonMarkup(node, skVar, binds)
355
- return `const ${skVar} = skeleton(${parentVar}, ${JSON.stringify(html)});\n${binds.join('')}`
377
+ return `const ${skVar} = $$skeleton(${parentVar}, ${JSON.stringify(html)});\n${binds.join('')}`
356
378
  }
357
379
 
358
380
  /* Emits code appending `node` to `parentVar`. */
@@ -378,8 +400,8 @@ export function generateBuild(
378
400
  .map((part, index) => {
379
401
  const splitAlways = index < consumers.length - 1 ? ', true' : ''
380
402
  return part.kind === 'static'
381
- ? `appendStatic(${parentVar}, ${JSON.stringify(part.value)}${splitAlways});\n`
382
- : `appendText(${parentVar}, ${namedThunk('text', `return (${lowerExpression(part.code)})`)}${splitAlways});\n`
403
+ ? `$$appendStatic(${parentVar}, ${JSON.stringify(part.value)}${splitAlways});\n`
404
+ : `$$appendText(${parentVar}, ${namedThunk('text', `return (${lowerExpression(part.code)})`)}${splitAlways});\n`
383
405
  })
384
406
  .join('')
385
407
  }
@@ -387,7 +409,7 @@ export function generateBuild(
387
409
  /* A standalone layout outlet (a top-level/element-nested `<slot/>` rewritten by
388
410
  `asOutlet`, reached outside any skeleton): an empty `outlet` boundary at
389
411
  `before`, no anchor — the router fills it with the next chain layer. */
390
- return `outlet(${parentVar}, ${before});\n`
412
+ return `$$outlet(${parentVar}, ${before});\n`
391
413
  }
392
414
  if (node.kind === 'element' && node.tag === 'slot') {
393
415
  /* In a layout, `<slot/>` is the router's page outlet (`outlet` boundary the
@@ -395,7 +417,7 @@ export function generateBuild(
395
417
  are rewritten to `OUTLET_TAG` up front by `asOutlet` and handled above; this
396
418
  covers a layout slot reached inside a control-flow branch. */
397
419
  if (isLayout) {
398
- return `outlet(${parentVar}, ${before});\n`
420
+ return `$$outlet(${parentVar}, ${before});\n`
399
421
  }
400
422
  return generateSlot(node, parentVar)
401
423
  }
@@ -452,7 +474,7 @@ export function generateBuild(
452
474
  let runHtml = ''
453
475
  const flush = (): void => {
454
476
  if (runHtml !== '') {
455
- code += `cloneStatic(${parentVar}, ${JSON.stringify(runHtml)});\n`
477
+ code += `$$cloneStatic(${parentVar}, ${JSON.stringify(runHtml)});\n`
456
478
  runHtml = ''
457
479
  }
458
480
  }
@@ -473,8 +495,14 @@ export function generateBuild(
473
495
  the component scope (its `model`/cells); `args` are plain parameters bound by
474
496
  the call. Appends nothing at the declaration site — `{name(args)}` mounts it. */
475
497
  function generateSnippet(node: Extract<TemplateNode, { kind: 'snippet' }>): string {
476
- const body = node.children.map((child) => generateChild(child, '$host')).join('')
477
- return `function ${node.name}(${node.params ?? ''}) {\nreturn snippet(($host) => {\n${body}});\n}\n`
498
+ const plan = snippetPlan(node)
499
+ /* `args` are plain call parameters, not component cells — `withBindings` registers the
500
+ plan's `plain` bindings so the body reads the bare local, shadowing a same-named
501
+ component signal rather than reading it. */
502
+ const body = withBindings(withShadow, plan.bindings, buildBindingKind, () =>
503
+ plan.children.map((child) => generateChild(child, '$host')).join(''),
504
+ )
505
+ return `function ${plan.name}(${plan.params ?? ''}) {\nreturn $$snippet(($host) => {\n${body}});\n}\n`
478
506
  }
479
507
 
480
508
  /* A switch: each `case` is `{ match: () => value, render }`, the default is
@@ -493,21 +521,17 @@ export function generateBuild(
493
521
  return `{ match: ${match}, render: ${branchThunk(branch.children)} }`
494
522
  })
495
523
  .join(', ')
496
- return `switchBlock(${parentVar}, () => (${lowerExpression(node.subject)}), [${cases}], ${before});\n`
524
+ return `$$switchBlock(${parentVar}, () => (${lowerExpression(node.subject)}), [${cases}], ${before});\n`
497
525
  }
498
526
 
499
- /* A `<slot>` outlet: render the parent-provided content (`$children`), falling
500
- back to the slot's own children when the parent supplied none. */
527
+ /* A `{children()}` slot fill point: render the parent-provided content (`$children`).
528
+ A fallback is now an authored `{#if children}…{:else}…{/if}`, so the slot node never
529
+ carries children — the invoke is unconditional but guarded against a propless mount. */
501
530
  function generateSlot(
502
- node: Extract<TemplateNode, { kind: 'element' }>,
531
+ _node: Extract<TemplateNode, { kind: 'element' }>,
503
532
  parentVar: string,
504
533
  ): string {
505
- const fallback = generateChildren(node.children, parentVar)
506
- const invoke = `$props.$children(${parentVar})`
507
- if (fallback.trim() === '') {
508
- return `if ($props && $props.$children) { ${invoke}; }\n`
509
- }
510
- return `if ($props && $props.$children) { ${invoke}; } else {\n${fallback}}\n`
534
+ return `if ($props && $props.$children) { $props.$children(${parentVar}); }\n`
511
535
  }
512
536
 
513
537
  /* The child's slot content as a host-taking builder (`$children`), or undefined when
@@ -534,11 +558,11 @@ export function generateBuild(
534
558
  parentVar: string,
535
559
  before: string,
536
560
  ): string {
537
- return `mountChild(${parentVar}, ${node.name}, ${propsArg(node)}, ${before}, ${JSON.stringify(node.name)});\n`
561
+ return `$$mountChild(${parentVar}, ${node.name}, ${propsArg(node)}, ${before}, ${JSON.stringify(node.name)});\n`
538
562
  }
539
563
 
540
564
  /* An await block: pending → resolved(value) / error branches. Each branch is a
541
- single-element root; a render thunk returns its node. */
565
+ void render thunk `(parent[, value]) => void` that builds its content into `parent`. */
542
566
  function generateAwait(
543
567
  node: Extract<TemplateNode, { kind: 'await' }>,
544
568
  parentVar: string,
@@ -551,21 +575,20 @@ export function generateBuild(
551
575
  resolved content/binding feeds one thunk regardless of mode. */
552
576
  const thenThunk = branchThunk(
553
577
  plan.resolvedChildren,
554
- plan.resolvedAs,
578
+ plan.resolvedBindings,
555
579
  plan.finallyChildren,
556
- true,
557
580
  )
558
581
  /* Neither catch nor finally → pass `undefined` so awaitBlock re-throws the
559
582
  rejection (surfacing it) instead of rendering an empty branch. A finally-only
560
583
  block keeps a catch thunk that renders just finally. */
561
584
  const catchThunk = plan.surfaceRejection
562
585
  ? 'undefined'
563
- : branchThunk(plan.catchChildren, plan.catchAs, plan.finallyChildren)
586
+ : branchThunk(plan.catchChildren, plan.catchBindings, plan.finallyChildren)
564
587
  const pendingThunk = hasRenderableContent(plan.pending)
565
588
  ? branchThunk(plan.pending)
566
589
  : 'undefined'
567
590
  return (
568
- `awaitBlock(${parentVar}, nextBlockId(), () => (${lowerExpression(node.promise)}), ` +
591
+ `$$awaitBlock(${parentVar}, $$nextBlockId(), () => (${lowerExpression(node.promise)}), ` +
569
592
  `${pendingThunk}, ` +
570
593
  `${thenThunk}, ` +
571
594
  `${catchThunk}, ${before});\n`
@@ -576,28 +599,33 @@ export function generateBuild(
576
599
  builds its children — and an optional trailing `finally` branch — into
577
600
  `parent`. The full-range model tracks the built content between markers, so a
578
601
  branch holds ANY content (components, text, nested control-flow, snippets) and
579
- is generated exactly like a normal child list. `valueParam` binds a resolved /
580
- error / item value into scope. Nested `<script>`s are emitted in document order
581
- by `generateChildren`; `withNestedScripts` puts their bindings in deref scope. */
602
+ is generated exactly like a normal child list. `bindings` are the value param(s)
603
+ the plan declared (an `await` `then` value `reactive`; a `catch` error → `plain`)
604
+ at most one. Names flow to the deref scope ONLY through `withBindings` over the
605
+ plan's bindings; the cell wiring of a `reactive` binding is arranged by
606
+ `reactiveBinding`. Nested `<script>`s are emitted in document order by
607
+ `generateChildren`; `withNestedScripts` puts their bindings in deref scope. */
582
608
  function branchThunk(
583
609
  children: TemplateNode[],
584
- valueParam?: string,
610
+ bindings: Binding[] = [],
585
611
  finallyChildren: TemplateNode[] = [],
586
- reactiveValue = false,
587
612
  ): string {
588
613
  const parentParam = nextVar('p')
589
- /* A reactive value (an `await` `then`) arrives as a `.value` cell the runtime can set
590
- in place, so the branch re-runs in place on a re-settle instead of being rebuilt. */
591
- const binding =
592
- reactiveValue && valueParam !== undefined ? reactiveBinding(valueParam) : undefined
593
- const param = binding?.param ?? valueParam
594
- const prefix = binding?.prefix ?? ''
595
- const localNames = binding?.localNames ?? []
614
+ /* A `reactive` binding arrives as a `.value` cell the runtime can set in place (the
615
+ branch re-runs in place on a re-settle); `reactiveBinding` renders its cell param +
616
+ per-leaf reader prefix. A `plain` binding is the author param verbatim — a real arrow
617
+ parameter `withBindings` registers under `plain`, read as the bare local. */
618
+ const reactive = bindings.find((binding) => binding.classification === 'reactive')
619
+ const wiring =
620
+ reactive === undefined ? undefined : reactiveBinding(reactive, nextVar, lowerStatement)
621
+ const plainBinding = bindings.find((binding) => binding.classification === 'plain')
622
+ const param = wiring?.param ?? plainBinding?.name
623
+ const prefix = wiring?.prefix ?? ''
596
624
  const head = param === undefined ? `(${parentParam})` : `(${parentParam}, ${param})`
597
625
  const body = withNestedScripts(children, () =>
598
- localNames.length === 0
599
- ? generateChildren(children, parentParam)
600
- : withLocalDerived(localNames, () => generateChildren(children, parentParam)),
626
+ withBindings(withShadow, bindings, buildBindingKind, () =>
627
+ generateChildren(children, parentParam),
628
+ ),
601
629
  )
602
630
  const finallyBody =
603
631
  finallyChildren.length > 0
@@ -630,11 +658,11 @@ export function generateBuild(
630
658
  before: string,
631
659
  ): string {
632
660
  const plan = tryPlan(node)
633
- const tryThunk = branchThunk(plan.guarded, undefined, plan.finallyChildren)
661
+ const tryThunk = branchThunk(plan.guarded, [], plan.finallyChildren)
634
662
  const catchThunk = plan.hasCatch
635
- ? branchThunk(plan.catchChildren, plan.catchAs, plan.finallyChildren)
663
+ ? branchThunk(plan.catchChildren, plan.catchBindings, plan.finallyChildren)
636
664
  : 'undefined'
637
- return `tryBlock(${parentVar}, nextBlockId(), ${tryThunk}, ${catchThunk}, ${before});\n`
665
+ return `$$tryBlock(${parentVar}, $$nextBlockId(), ${tryThunk}, ${catchThunk}, ${before});\n`
638
666
  }
639
667
 
640
668
  /* A conditional with an optional nested `<template else>` (a `case` child). Each
@@ -652,7 +680,7 @@ export function generateBuild(
652
680
  const thenThunk = branchThunk(plan.thenChildren)
653
681
  const elseThunk =
654
682
  plan.elseBranch === undefined ? 'undefined' : branchThunk(plan.elseBranch.children)
655
- return `when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
683
+ return `$$when(${parentVar}, () => (${lowerExpression(node.condition)}), ${thenThunk}, ${elseThunk}, ${before});\n`
656
684
  }
657
685
  /* if/elseif/else is a cond-chain — reuse `switchBlock` over a constant `true`
658
686
  subject with `Boolean`-coerced match thunks, so the first truthy branch wins
@@ -665,7 +693,7 @@ export function generateBuild(
665
693
  : `{ match: undefined, render: ${branchThunk(branch.children)} }`,
666
694
  ),
667
695
  ]
668
- return `switchBlock(${parentVar}, () => true, [${entries.join(', ')}], ${before});\n`
696
+ return `$$switchBlock(${parentVar}, () => true, [${entries.join(', ')}], ${before});\n`
669
697
  }
670
698
 
671
699
  /* A keyed each. Each row is a content RANGE (any content, tracked between the
@@ -675,6 +703,7 @@ export function generateBuild(
675
703
  parentVar: string,
676
704
  before: string,
677
705
  ): string {
706
+ const plan = eachPlan(node)
678
707
  const rowParam = nextVar('p')
679
708
  /* The item is a reactive `.value` cell so a re-key with a changed value updates the row
680
709
  in place (no rebuild). `keyOf` receives the RAW item; the key expression is lowered
@@ -683,34 +712,34 @@ export function generateBuild(
683
712
  plain `as` returns its name; a destructuring `as` binds a fresh param and returns
684
713
  THAT, not the pattern re-wrapped (`[i,crumb]` → `[i,crumb]` would allocate a fresh
685
714
  array per reconcile, so keys never match and every row rebuilds). An explicit `key`
686
- destructures the item via `node.as` to read its leaves. */
687
- const rawItemParam = isPlainIdentifier(node.as) ? node.as : nextVar('k')
688
- const keyParam = node.key === undefined ? rawItemParam : node.as
689
- const keyExpression = node.key === undefined ? rawItemParam : lowerExpression(node.key)
690
- const binding = reactiveBinding(node.as)
691
- /* `index="i"` binds the row's position as a third reactive cell param (the runtime
692
- always passes it). It is a plain identifier read as `i.value` so it enters the
693
- body's deref scope alongside the item's leaf names; an unnamed param when absent. */
694
- const indexParam = node.index === undefined ? '' : `, ${node.index}`
695
- const bodyLocalNames =
696
- node.index === undefined ? binding.localNames : [...binding.localNames, node.index]
715
+ destructures the item via `plan.as` to read its leaves. */
716
+ const rawItemParam = isPlainIdentifier(plan.as) ? plan.as : nextVar('k')
717
+ const keyParam = plan.key === undefined ? rawItemParam : plan.as
718
+ const keyExpression = plan.key === undefined ? rawItemParam : lowerExpression(plan.key)
719
+ /* The item is the row's `reactive` binding (its `.value` cell + per-leaf reader prefix
720
+ rendered by `reactiveBinding`); `index="i"` is a second reactive cell param, a plain
721
+ identifier read as `i.value` (no prefix). The names of BOTH enter the deref scope via
722
+ `withBindings` over `plan.bindings`, not a hand-built list. */
723
+ const itemWiring = reactiveBinding(plan.bindings[0] as Binding, nextVar, lowerStatement)
724
+ const indexParam = plan.index === undefined ? '' : `, ${plan.index}`
697
725
  /* The row body builds its children (a `<script>` declares per-row local signals,
698
726
  emitted in document order) into the row parent. A `<template catch>` child is
699
727
  consumed by the async-each, not the row — `generateChildren` skips it. */
700
- const rowBody = withNestedScripts(node.children, () =>
701
- withLocalDerived(bodyLocalNames, () => generateChildren(node.children, rowParam)),
728
+ const rowBody = withNestedScripts(plan.children, () =>
729
+ withBindings(withShadow, plan.bindings, buildBindingKind, () =>
730
+ generateChildren(plan.children, rowParam),
731
+ ),
702
732
  )
703
733
  /* `await` → the AsyncIterable runtime, drained row-by-row on the client, with an
704
734
  optional `<template catch>` branch rendered (after the streamed rows) when the
705
735
  iterator rejects. Absent → `undefined`, so the rejection surfaces instead. */
706
- const fn = node.async ? 'eachAsync' : 'each'
707
- const [catchBranch] = resolveBranches(node, 'catch')
708
- const catchArg = node.async
709
- ? `, ${catchBranch === undefined ? 'undefined' : branchThunk(catchBranch.children, catchBranch.as ?? '_error')}`
736
+ const fn = plan.async ? '$$eachAsync' : '$$each'
737
+ const catchArg = plan.async
738
+ ? `, ${plan.hasCatch ? branchThunk(plan.catchChildren, plan.catchBindings) : 'undefined'}`
710
739
  : ''
711
740
  return (
712
- `${fn}(${parentVar}, () => (${lowerExpression(node.items)}), ` +
713
- `(${keyParam}) => (${keyExpression}), (${rowParam}, ${binding.param}${indexParam}) => {\n${binding.prefix}${rowBody}}${catchArg}, ${before});\n`
741
+ `${fn}(${parentVar}, () => (${lowerExpression(plan.items)}), ` +
742
+ `(${keyParam}) => (${keyExpression}), (${rowParam}, ${itemWiring.param}${indexParam}) => {\n${itemWiring.prefix}${rowBody}}${catchArg}, ${before});\n`
714
743
  )
715
744
  }
716
745