@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
@@ -1,14 +1,25 @@
1
1
  import { decodeHtmlEntities } from './decodeHtmlEntities.ts'
2
+ import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
2
3
  import { isWhitespaceText } from './isWhitespaceText.ts'
3
4
  import type { TemplateAttr } from './types/TemplateAttr.ts'
4
5
  import type { TemplateNode } from './types/TemplateNode.ts'
5
6
  import type { TextPart } from './types/TextPart.ts'
6
7
  import { VOID_TAGS } from './VOID_TAGS.ts'
7
8
 
9
+ /* Compiled once — the parser tests these per source character, so a per-call literal
10
+ would recompile them across the whole template scan. */
11
+ const WHITESPACE = /\s/
12
+ const WHITESPACE_OR_GT = /[\s>]/
13
+ const ATTR_NAME_END = /[\s=>/]/
14
+ const TAG_NAME_END = /[\s>/]/
15
+ const UPPERCASE_START = /^[A-Z]/
16
+ const AWAIT_PREFIX = /^await\b/
17
+ const LEADING_KEYWORD = /^\s*(\S+)/
18
+
8
19
  /*
9
20
  A minimal compile-time parser for the abide template subset: elements, text with
10
21
  `{expr}` interpolation, static/`{expr}`/`on<event>={expr}` attributes, and
11
- `<template each as key>` control flow. Not a full HTML parser — it covers what
22
+ `{#if}/{#for}/{#await}/{#switch}/{#try}` block control flow. Not a full HTML parser — it covers what
12
23
  components need and reads brace expressions with quote/nesting awareness so an
13
24
  expression containing `<`, `>`, or `}` parses intact. Void elements self-close.
14
25
  `<!-- … -->` comments are dropped (no node emitted) so they leave no trace in the
@@ -73,7 +84,9 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
73
84
  /* True when `cursor` is on a `<style>` open tag — read raw (`readStyle`) so its
74
85
  CSS braces never misparse as `{expr}` interpolations. */
75
86
  function atStyleTag(): boolean {
76
- return source.startsWith('<style', cursor) && /[\s>]/.test(source.charAt(cursor + 6))
87
+ return (
88
+ source.startsWith('<style', cursor) && WHITESPACE_OR_GT.test(source.charAt(cursor + 6))
89
+ )
77
90
  }
78
91
 
79
92
  /* Reads a `<style>…</style>` block into a `style` node carrying its CSS body; an
@@ -97,6 +110,19 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
97
110
  return source.charAt(cursor) === '{' && source.charAt(cursor + 1) === '#'
98
111
  }
99
112
 
113
+ /* `{children()}` is the slot fill point — the content a parent passed (a component)
114
+ or the route chain below (a layout). It parses to the SAME node the retired `<slot>`
115
+ element produced, so every downstream helper that branches on `tag === 'slot'` is
116
+ unchanged. */
117
+ const CHILDREN_CALL = /^\{\s*children\s*\(\s*\)\s*\}/
118
+ function atChildrenCall(): boolean {
119
+ return source.charAt(cursor) === '{' && CHILDREN_CALL.test(source.slice(cursor))
120
+ }
121
+ function readChildrenCall(): TemplateNode {
122
+ cursor = source.indexOf('}', cursor) + 1 // consume `{children()}`
123
+ return { kind: 'element', tag: 'slot', attrs: [], children: [] }
124
+ }
125
+
100
126
  /* Reads a `{#…}` / `{:…}` / `{/…}` token starting on `{`. Tracks string literals
101
127
  and nested braces (same scan as readBracedExpression) so a brace/quote inside an
102
128
  expression (`{#if obj.has('}')}`) doesn't end the token early. Returns the sigil,
@@ -132,13 +158,14 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
132
158
  /* The leading keyword of a directive body (`if`, `for`, `await`, `switch`, `try`,
133
159
  `else`, `then`, `catch`, `finally`, `case`, `default`). */
134
160
  function headKeyword(body: string): string {
135
- const match = body.match(/^\s*(\S+)/)
161
+ const match = body.match(LEADING_KEYWORD)
136
162
  return match?.[1] ?? ''
137
163
  }
138
164
 
139
165
  /* Reads a `{#…}` control block: the open token, its children up to a continuation
140
- `{:…}` (a branch) or close `{/…}`, recursing. Emits the same nodes toControlFlow
141
- does today (if/each/await/switch/try + case/branch children). */
166
+ `{:…}` (a branch) or close `{/…}`, recursing. Emits `if`/`each`/`await`/`switch`/`try`
167
+ nodes with their `case`/`branch` children — the same TemplateNode kinds the back-ends
168
+ and `skeletonContext` consume. */
142
169
  function readBlock(): TemplateNode {
143
170
  const open = readBlockToken() // sigil is '#'
144
171
  const keyword = headKeyword(open.body)
@@ -207,8 +234,32 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
207
234
  const children = readBlockChildren('try')
208
235
  return { kind: 'try', children }
209
236
  }
237
+ if (keyword === 'snippet') {
238
+ const head = open.body.slice(open.body.indexOf('snippet') + 'snippet'.length).trim()
239
+ const parenAt = head.indexOf('(')
240
+ const name = (parenAt === -1 ? head : head.slice(0, parenAt)).trim()
241
+ if (name === '') {
242
+ throw new Error('[abide] {#snippet} requires a name, e.g. {#snippet row(item)}')
243
+ }
244
+ /* Params ride the parens: `{#snippet row({ item })}` → `{ item }`. */
245
+ const params =
246
+ parenAt === -1
247
+ ? undefined
248
+ : head.slice(parenAt + 1, head.lastIndexOf(')')).trim() || undefined
249
+ const children = readBlockChildren('snippet')
250
+ return {
251
+ kind: 'snippet',
252
+ name,
253
+ params,
254
+ children,
255
+ loc:
256
+ parenAt === -1
257
+ ? undefined
258
+ : exprLoc(open.loc, open.body, open.body.indexOf('(') + 1),
259
+ }
260
+ }
210
261
  throw new Error(
211
- `[abide] unknown control block {#${keyword}} — expected if/for/await/switch/try`,
262
+ `[abide] unknown control block {#${keyword}} — expected if/for/await/switch/try/snippet`,
212
263
  )
213
264
  }
214
265
 
@@ -227,6 +278,8 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
227
278
  }
228
279
  if (atBlock()) {
229
280
  nodes.push(readBlock())
281
+ } else if (atChildrenCall()) {
282
+ nodes.push(readChildrenCall())
230
283
  } else if (source.startsWith('<!--', cursor)) {
231
284
  skipComment()
232
285
  } else if (atStyleTag()) {
@@ -243,7 +296,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
243
296
  `{:…}` ends the current branch's children and starts a new `case`/`branch` node
244
297
  (per construct). The leading children (before the first `{:…}`) are the block's
245
298
  own children (the `if`/`await`/`try` then-content). Returns the full children list
246
- INCLUDING the case/branch nodes, matching toControlFlow's output. */
299
+ INCLUDING the case/branch nodes, producing the same TemplateNode tree the back-ends consume. */
247
300
  function readBlockChildren(keyword: string): TemplateNode[] {
248
301
  const nodes: TemplateNode[] = []
249
302
  while (cursor < source.length) {
@@ -403,6 +456,9 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
403
456
  let literal = ''
404
457
  while (cursor < source.length && source.charAt(cursor) !== '<') {
405
458
  if (source.charAt(cursor) === '{') {
459
+ if (atChildrenCall()) {
460
+ break // a slot fill point — handled by the enclosing scan loop
461
+ }
406
462
  const next = source.charAt(cursor + 1)
407
463
  if (next === '#' || next === ':' || next === '/') {
408
464
  break // a block/continuation/close token — not interpolation
@@ -427,7 +483,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
427
483
  function readAttributes(): TemplateAttr[] {
428
484
  const attrs: TemplateAttr[] = []
429
485
  while (cursor < source.length) {
430
- while (/\s/.test(source.charAt(cursor))) {
486
+ while (WHITESPACE.test(source.charAt(cursor))) {
431
487
  cursor += 1
432
488
  }
433
489
  const char = source.charAt(cursor)
@@ -454,11 +510,11 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
454
510
  }
455
511
  let name = ''
456
512
  const nameLoc = baseOffset + cursor // absolute offset of the attribute name
457
- while (cursor < source.length && !/[\s=>/]/.test(source.charAt(cursor))) {
513
+ while (cursor < source.length && !ATTR_NAME_END.test(source.charAt(cursor))) {
458
514
  name += source.charAt(cursor)
459
515
  cursor += 1
460
516
  }
461
- while (/\s/.test(source.charAt(cursor))) {
517
+ while (WHITESPACE.test(source.charAt(cursor))) {
462
518
  cursor += 1
463
519
  }
464
520
  if (source.charAt(cursor) !== '=') {
@@ -466,7 +522,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
466
522
  continue
467
523
  }
468
524
  cursor += 1 // past '='
469
- while (/\s/.test(source.charAt(cursor))) {
525
+ while (WHITESPACE.test(source.charAt(cursor))) {
470
526
  cursor += 1
471
527
  }
472
528
  if (source.charAt(cursor) === '{') {
@@ -487,18 +543,44 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
487
543
  } else if (source.charAt(cursor) === '"' || source.charAt(cursor) === "'") {
488
544
  const quote = source.charAt(cursor)
489
545
  cursor += 1
490
- let value = ''
546
+ /* Scan the quoted span into static/`{expr}` parts (the text-node model),
547
+ reusing the brace/quote-aware expression reader so a `}` inside an
548
+ expression doesn't close the value early. */
549
+ const parts: TextPart[] = []
550
+ let literal = ''
491
551
  while (cursor < source.length && source.charAt(cursor) !== quote) {
492
- value += source.charAt(cursor)
493
- cursor += 1
552
+ if (source.charAt(cursor) === '{') {
553
+ if (literal !== '') {
554
+ parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
555
+ literal = ''
556
+ }
557
+ const { code, loc } = readBracedExpression()
558
+ parts.push({ kind: 'expression', code, loc })
559
+ } else {
560
+ literal += source.charAt(cursor)
561
+ cursor += 1
562
+ }
494
563
  }
495
564
  cursor += 1 // past closing quote
496
- attrs.push({ kind: 'static', name, value, nameLoc })
565
+ if (literal !== '') {
566
+ parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
567
+ }
568
+ /* No `{expr}` → a plain static attribute; its literal is entity-decoded
569
+ like a text node (SSR/clone re-escape it), so a value such as `&amp;`
570
+ round-trips instead of double-escaping. Otherwise an interpolated value. */
571
+ if (parts.every((part) => part.kind === 'static')) {
572
+ const value = parts
573
+ .map((part) => (part.kind === 'static' ? part.value : ''))
574
+ .join('')
575
+ attrs.push({ kind: 'static', name, value, nameLoc })
576
+ } else {
577
+ attrs.push({ kind: 'interpolated', name, parts, nameLoc })
578
+ }
497
579
  } else {
498
580
  /* Unquoted value (`<input type=text>`): runs to the next whitespace or
499
581
  `>`, per the HTML unquoted-attribute rule. No delimiter to consume. */
500
582
  let value = ''
501
- while (cursor < source.length && !/[\s>]/.test(source.charAt(cursor))) {
583
+ while (cursor < source.length && !WHITESPACE_OR_GT.test(source.charAt(cursor))) {
502
584
  /* Stop before a `/` that closes the tag (`<Comp x=y/>`) so the value
503
585
  doesn't swallow the self-closing slash and defeat detection; a `/`
504
586
  elsewhere (e.g. a URL `href=/a/b`) stays part of the value. */
@@ -518,7 +600,7 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
518
600
  cursor += 1 // past '<'
519
601
  let tag = ''
520
602
  const tagStart = cursor // absolute (baseOffset-relative) offset of the tag name
521
- while (cursor < source.length && !/[\s>/]/.test(source.charAt(cursor))) {
603
+ while (cursor < source.length && !TAG_NAME_END.test(source.charAt(cursor))) {
522
604
  tag += source.charAt(cursor)
523
605
  cursor += 1
524
606
  }
@@ -533,6 +615,9 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
533
615
  verbatim to its `</script>` (not parsed as markup), scoped by the
534
616
  containing branch when compiled. */
535
617
  if (tag === 'script' && !selfClosing) {
618
+ /* Body starts at the current cursor; its absolute source offset lets the shadow
619
+ map a diagnostic raised inside it (e.g. a top-level await) back to source. */
620
+ const bodyLoc = baseOffset + cursor
536
621
  const close = source.indexOf('</script>', cursor)
537
622
  const end = close === -1 ? source.length : close
538
623
  const code = source.slice(cursor, end)
@@ -549,11 +634,11 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
549
634
  "import statements must live in the component's leading <script>, not a nested <template> script — they hoist to module scope for the whole template. For lazy loading, use a dynamic import() inside an effect.",
550
635
  )
551
636
  }
552
- return { kind: 'script', code }
637
+ return { kind: 'script', code, loc: bodyLoc }
553
638
  }
554
639
  /* A capitalised tag is a child component; its attributes become props and
555
640
  its children become slot content (rendered where the child puts <slot>). */
556
- if (/^[A-Z]/.test(tag)) {
641
+ if (UPPERCASE_START.test(tag)) {
557
642
  const slotted = selfClosing ? [] : readChildren(tag)
558
643
  return {
559
644
  kind: 'component',
@@ -569,6 +654,11 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
569
654
  throw new Error('[abide] {...expr} spread is not supported on a <template> directive')
570
655
  }
571
656
  const children = selfClosing || VOID_TAGS.has(tag) ? [] : readChildren(tag)
657
+ if (tag === 'slot') {
658
+ throw new Error(
659
+ '[abide] the <slot> element was removed — render passed content with {children()} (with {#if children}{children()}{:else}…{/if} for a fallback)',
660
+ )
661
+ }
572
662
  if (tag === 'template') {
573
663
  return toSnippetOrTemplate(attrs, children)
574
664
  }
@@ -601,6 +691,8 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
601
691
  nodes.push(readStyle())
602
692
  } else if (atBlock()) {
603
693
  nodes.push(readBlock())
694
+ } else if (atChildrenCall()) {
695
+ nodes.push(readChildrenCall())
604
696
  } else if (source.charAt(cursor) === '<') {
605
697
  nodes.push(readElement())
606
698
  } else {
@@ -619,6 +711,8 @@ export function parseTemplate(source: string, baseOffset = 0): { nodes: Template
619
711
  roots.push(readStyle())
620
712
  } else if (atBlock()) {
621
713
  roots.push(readBlock())
714
+ } else if (atChildrenCall()) {
715
+ roots.push(readChildrenCall())
622
716
  } else if (source.charAt(cursor) === '<') {
623
717
  roots.push(readElement())
624
718
  } else {
@@ -682,8 +776,8 @@ function keywordAtDepthZero(text: string, word: string): number {
682
776
  depth === 0 &&
683
777
  text.startsWith(word, i) &&
684
778
  i > 0 &&
685
- /\s/.test(text.charAt(i - 1)) &&
686
- (i + word.length === text.length || /\s/.test(text.charAt(i + word.length)))
779
+ WHITESPACE.test(text.charAt(i - 1)) &&
780
+ (i + word.length === text.length || WHITESPACE.test(text.charAt(i + word.length)))
687
781
  ) {
688
782
  return i
689
783
  }
@@ -740,13 +834,13 @@ function parseForHead(
740
834
  const leadingSpace = (text: string): number => text.length - text.trimStart().length
741
835
  const skipSpace = (i: number): number => {
742
836
  let at = i
743
- while (at < body.length && /\s/.test(body.charAt(at))) {
837
+ while (at < body.length && WHITESPACE.test(body.charAt(at))) {
744
838
  at += 1
745
839
  }
746
840
  return at
747
841
  }
748
842
  let bindingStart = skipSpace(body.indexOf('for') + 3)
749
- const isAsync = /^await\b/.test(body.slice(bindingStart))
843
+ const isAsync = AWAIT_PREFIX.test(body.slice(bindingStart))
750
844
  if (isAsync) {
751
845
  bindingStart = skipSpace(bindingStart + 'await'.length)
752
846
  }
@@ -903,6 +997,15 @@ function toProps(
903
997
  nameLoc: attr.nameLoc,
904
998
  }
905
999
  }
1000
+ /* `label="hi {name}"` — a string-valued prop, the template-literal concatenation
1001
+ of its parts (lowered downstream like any other prop value). */
1002
+ if (attr.kind === 'interpolated') {
1003
+ return {
1004
+ name: attr.name,
1005
+ code: interpolatedTemplateLiteral(attr.parts),
1006
+ nameLoc: attr.nameLoc,
1007
+ }
1008
+ }
906
1009
  /* Every non-static kind keeps its `code`/`loc`; only the prop name differs —
907
1010
  a directive (`event`/`bind`/`class`/`style`/`attach`) round-trips to its written
908
1011
  name as a passthrough prop. */
@@ -925,24 +1028,6 @@ function toProps(
925
1028
  })
926
1029
  }
927
1030
 
928
- /* The literal text of an attribute (a static value or an expression's code);
929
- undefined for event/bind attributes, which a directive never is. */
930
- function attrText(attr: TemplateAttr): string | undefined {
931
- if (attr.kind === 'static') {
932
- return attr.value
933
- }
934
- if (attr.kind === 'expression') {
935
- return attr.code
936
- }
937
- return undefined
938
- }
939
-
940
- /* The source offset of an attribute expression's code (see TextPart.loc); only
941
- expression attributes carry one — a static value isn't a checkable expression. */
942
- function attrLoc(attr: TemplateAttr | undefined): number | undefined {
943
- return attr !== undefined && attr.kind === 'expression' ? attr.loc : undefined
944
- }
945
-
946
1031
  /* The attribute's source name (`on<event>`/`bind:<property>` reconstructed). */
947
1032
  function attrName(attr: TemplateAttr): string {
948
1033
  if (attr.kind === 'event') {
@@ -970,7 +1055,6 @@ function attrName(attr: TemplateAttr): string {
970
1055
  return attr.name
971
1056
  }
972
1057
 
973
- /* Turns a `<template>` directive into a control node (if/each/await + then/catch). */
974
1058
  /* The control-flow attribute names that used to drive `<template>` directives — now
975
1059
  moved to `{#…}` blocks. A `<template>` carrying one is a migration error. */
976
1060
  const CONTROL_DIRECTIVES = new Set([
@@ -988,10 +1072,9 @@ const CONTROL_DIRECTIVES = new Set([
988
1072
  'try',
989
1073
  ])
990
1074
 
991
- /* A `<template>` is now ONLY a snippet declaration (`name`) or a plain inert
992
- `<template>` element. Control flow moved to `{#…}` blocks; a directive attribute
993
- (`if`/`each`/`await`/…) is a migration error pointing at the block form. `name`
994
- makes the element callable; without it, it stays an inert reusable fragment. */
1075
+ /* A `<template>` is now ONLY a plain inert element (e.g. client-side cloning).
1076
+ Snippet declarations moved to `{#snippet name(args)}…{/snippet}` blocks; control
1077
+ flow moved to `{#…}` blocks both throw migration errors here. */
995
1078
  function toSnippetOrTemplate(attrs: TemplateAttr[], children: TemplateNode[]): TemplateNode {
996
1079
  const find = (name: string) => attrs.find((attr) => attrName(attr) === name)
997
1080
  const directive = attrs.find((attr) => CONTROL_DIRECTIVES.has(attrName(attr)))
@@ -1002,22 +1085,12 @@ function toSnippetOrTemplate(attrs: TemplateAttr[], children: TemplateNode[]): T
1002
1085
  `[abide] <template ${name}> control flow was removed — use the {#${block}…} block instead`,
1003
1086
  )
1004
1087
  }
1005
- /* `<template name="row" args={item}>` declares a snippet a named builder. `args`
1006
- (its parameter list) rides the `{…}` expression slot. */
1007
- const snippet = find('name')
1008
- if (snippet !== undefined) {
1009
- const name = attrText(snippet)
1010
- if (name === undefined || name === '') {
1011
- throw new Error('[abide] <template name> requires a snippet name')
1012
- }
1013
- const params = find('args')
1014
- return {
1015
- kind: 'snippet',
1016
- name,
1017
- params: params === undefined ? undefined : attrText(params),
1018
- children,
1019
- loc: attrLoc(params),
1020
- }
1088
+ /* `<template name>` snippet declarations were retired for the `{#snippet name(args)}`
1089
+ block reject with a migration error. */
1090
+ if (find('name') !== undefined) {
1091
+ throw new Error(
1092
+ '[abide] <template name> snippet declarations were removed — use a {#snippet name(args)}…{/snippet} block',
1093
+ )
1021
1094
  }
1022
1095
  /* A plain inert `<template>` element (e.g. client-side cloning) — keep as an element. */
1023
1096
  return { kind: 'element', tag: 'template', attrs, children }
@@ -0,0 +1,47 @@
1
+ import { destructureBindingNames } from './destructureBindingNames.ts'
2
+ import { isPlainIdentifier } from './isPlainIdentifier.ts'
3
+ import type { Binding } from './types/Binding.ts'
4
+
5
+ /* How the client back-end binds a `reactive` Binding (an `await` `then` value, a keyed
6
+ `each` item / index): the value arrives as a `.value` cell the runtime can update in
7
+ place, so the consuming branch/row re-runs in place on a re-settle/re-key rather than
8
+ being rebuilt. THE RENDERER of a `reactive` classification — it takes the Binding the
9
+ plan declared, never a raw author param, so a back-end can only render what the plan
10
+ classified. `param` is the thunk's value parameter (the cell), `prefix` declares any
11
+ per-leaf readers, `localNames` (the Binding's leaf names) enter the deref scope for the
12
+ body. A plain identifier reads the cell directly (`item` → `item.value`); a destructure
13
+ re-applies over the cell per read so each leaf stays reactive (JS handles
14
+ defaults/rest/rename/nesting). */
15
+ export type ReactiveBindingWiring = {
16
+ param: string
17
+ prefix: string
18
+ localNames: string[]
19
+ }
20
+
21
+ /* Renders a `reactive` Binding's cell wiring for the client back-end. `nextVar` mints the
22
+ synthetic cell/derive vars; `lowerStatement` lowers the destructure declaration so a
23
+ default/computed-key initializer referencing a component signal is rewritten to its
24
+ `model`/cell form (the bound leaf names are name-slots, untouched). */
25
+ export function reactiveBinding(
26
+ binding: Binding,
27
+ nextVar: (hint: string) => string,
28
+ lowerStatement: (code: string) => string,
29
+ ): ReactiveBindingWiring {
30
+ const authorParam = binding.name
31
+ if (isPlainIdentifier(authorParam)) {
32
+ return { param: authorParam, prefix: '', localNames: [authorParam] }
33
+ }
34
+ const cellParam = nextVar('aw')
35
+ const deriveVar = nextVar('ad')
36
+ const leaves = destructureBindingNames(authorParam)
37
+ const declaration = lowerStatement(`const ${authorParam} = ${cellParam}.value`)
38
+ const prefix =
39
+ `const ${deriveVar} = { get value() { ${declaration} return { ${leaves.join(', ')} }; } };\n` +
40
+ leaves
41
+ .map(
42
+ (leaf) =>
43
+ `const ${leaf} = { get value() { return ${deriveVar}.value.${leaf}; } };\n`,
44
+ )
45
+ .join('')
46
+ return { param: cellParam, prefix, localNames: leaves }
47
+ }
@@ -3,12 +3,12 @@ import { TS_PRINTER } from './TS_PRINTER.ts'
3
3
 
4
4
  /*
5
5
  Rewrites references to a component's signal bindings into the document form the
6
- rest of the pipeline understands: a `state` binding `count` becomes `model.count`
6
+ rest of the pipeline understands: a `state` binding `count` becomes `$$model.count`
7
7
  (data access `lowerDocAccess` then lowers to a patch/read), and a `computed`
8
8
  binding `total` becomes `total.value`. Only value-position identifiers are
9
9
  touched — declaration names, parameter names, and property names are collected
10
10
  into a skip set first, and object shorthand (`{ count }`) is expanded to
11
- `{ count: model.count }`. This is the bridge from the signal surface the author
11
+ `{ count: $$model.count }`. This is the bridge from the signal surface the author
12
12
  writes to the patch substrate underneath.
13
13
 
14
14
  The rewrite is lexically scope-aware: a reference whose name is re-bound by an
@@ -37,7 +37,7 @@ export function renameSignalRefs(
37
37
  The signal-rewrite as a `ts.TransformerFactory`, so the script pipeline can chain it
38
38
  with `docAccessTransformer` over a SINGLE parsed tree (see `lowerScript`) instead of
39
39
  print-then-reparse between passes. `renameSignalRefs` is the standalone string wrapper
40
- kept for per-expression callers (`lowerContext`).
40
+ kept for test and one-off callers.
41
41
  */
42
42
  export function signalRefsTransformer(
43
43
  stateNames: ReadonlySet<string>,
@@ -47,6 +47,12 @@ export function signalRefsTransformer(
47
47
  `<script>` declaration) — a nearer lexical scope than the component signals, so a
48
48
  name here shadows a same-named `state`/`computed`/`derived` and derefs as a cell. */
49
49
  blockLocal: ReadonlySet<string> = new Set(),
50
+ /* Block-local PLAIN bindings — an SSR `await` `then` value bound as a real JS local
51
+ holding the plain resolved value, not a reactive cell. Like `blockLocal` it shadows
52
+ a same-named component signal (it is a nearer lexical scope), but it derefs as the
53
+ bare identifier, not `.value`: SSR declares `const foo = <resolved>`, so a reference
54
+ reads the local directly. Seeded into the root shadow set so it is left untouched. */
55
+ blockLocalPlain: ReadonlySet<string> = new Set(),
50
56
  ): ts.TransformerFactory<ts.SourceFile> {
51
57
  /* The signal names that a nested binding can shadow — only these matter for
52
58
  scope tracking, so we ignore every other local binding. */
@@ -55,6 +61,16 @@ export function signalRefsTransformer(
55
61
  ...derivedNames,
56
62
  ...computedNames,
57
63
  ...blockLocal,
64
+ /* The reserved slot reader (`{#if children}` → `$props?.$children`) is rewritten
65
+ through `referenceFor` like a signal, so it inherits the same lexical-scope
66
+ tracking: a `{#snippet row(children)}` arg, a `{#for children of …}` item, a
67
+ script param named `children` re-binds it for that subtree and is left untouched. */
68
+ 'children',
69
+ /* `scope` is the author-facing reactive entry (`scope().state(...)`), lowered to the
70
+ reserved `$$scope` import so a user variable named `scope` can never collide — but
71
+ shadowably: a `const scope`/param `scope` re-binds it for that subtree and is left
72
+ untouched, exactly like a signal name. */
73
+ 'scope',
58
74
  ])
59
75
  return (context) => (root) => {
60
76
  /* The identifier nodes that are names, not value reads — collected by walking
@@ -89,7 +105,7 @@ export function signalRefsTransformer(
89
105
  if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
90
106
  return node
91
107
  }
92
- /* Shorthand `{ count }` → `{ count: model.count }` / `{ total: total.value }`,
108
+ /* Shorthand `{ count }` → `{ count: $$model.count }` / `{ total: total.value }`,
93
109
  unless a nearer scope shadows the name. */
94
110
  if (ts.isShorthandPropertyAssignment(node) && !shadowed.has(node.name.text)) {
95
111
  const replacement = referenceFor(
@@ -126,7 +142,7 @@ export function signalRefsTransformer(
126
142
  }
127
143
  return visit
128
144
  }
129
- return ts.visitNode(root, makeVisitor(new Set())) as ts.SourceFile
145
+ return ts.visitNode(root, makeVisitor(new Set(blockLocalPlain))) as ts.SourceFile
130
146
  }
131
147
  }
132
148
 
@@ -314,11 +330,12 @@ function functionParameters(node: ts.Node): ts.NodeArray<ts.ParameterDeclaration
314
330
  return undefined
315
331
  }
316
332
 
317
- /* `model.<name>` for a state binding, `<name>()` for a computed doc-slot (the
333
+ /* `$$model.<name>` for a state binding, `<name>()` for a computed doc-slot (the
318
334
  string-free reader `scope().derive` returns), `<name>.value` for a runtime cell
319
- (linked / lens / transform-state), else undefined. A `blockLocal` binding shadows
320
- any same-named component signal — it is a nearer lexical scope — so it derefs as a
321
- cell (`<name>.value`) regardless of a colliding `state`/`computed`/`derived`. */
335
+ (linked / lens / transform-state), `$props?.$children` for the reserved slot reader,
336
+ else undefined. A `blockLocal` binding shadows any same-named component signal — it is
337
+ a nearer lexical scope — so it derefs as a cell (`<name>.value`) regardless of a
338
+ colliding `state`/`computed`/`derived`, and ahead of the `children` slot reader. */
322
339
  function referenceFor(
323
340
  name: string,
324
341
  stateNames: ReadonlySet<string>,
@@ -329,8 +346,25 @@ function referenceFor(
329
346
  if (blockLocal.has(name)) {
330
347
  return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), 'value')
331
348
  }
349
+ /* `{children()}` is a slot NODE the parser handles; any other bare `children` read
350
+ (notably `{#if children}`) is the reserved reader. `?.` guards a propless mount. */
351
+ if (name === 'children') {
352
+ return ts.factory.createPropertyAccessChain(
353
+ ts.factory.createIdentifier('$props'),
354
+ ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
355
+ ts.factory.createIdentifier('$children'),
356
+ )
357
+ }
358
+ /* The author-facing reactive entry `scope` lowers to its reserved `$$scope` import (the
359
+ value read only — a `.scope` member or a shadowing local is never reached here). */
360
+ if (name === 'scope') {
361
+ return ts.factory.createIdentifier('$$scope')
362
+ }
332
363
  if (stateNames.has(name)) {
333
- return ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('model'), name)
364
+ return ts.factory.createPropertyAccessExpression(
365
+ ts.factory.createIdentifier('$$model'),
366
+ name,
367
+ )
334
368
  }
335
369
  if (computedNames.has(name)) {
336
370
  return ts.factory.createCallExpression(ts.factory.createIdentifier(name), undefined, [])
@@ -4,11 +4,11 @@ import type { TemplateNode } from './types/TemplateNode.ts'
4
4
 
5
5
  /* Whether a subtree is skeleton STRUCTURE — anything the parser-backed clone can carry with
6
6
  its holes mounted in place: elements, `<style>`, text (static OR reactive), child
7
- components (their wrapper element is positioned in the skeleton), control-flow blocks and
8
- `<slot>` outlets (each anchor-mounted at its position), and emit-nothing nodes — a nested
9
- `<script>` (a scoped reactive block) and a `<template name>` snippet (a hoisted builder),
10
- both of which the skeleton's bind list runs in document order, scoped like
11
- `generateElement` does. Only a standalone `branch`/`case` (consumed by its block, never a
7
+ components (anchor-positioned via an `<!--a-->` marker in the skeleton), control-flow
8
+ blocks and `<slot>` outlets (each anchor-mounted at its position), and emit-nothing nodes
9
+ — a nested `<script>` (a scoped reactive block) and a `{#snippet}` block (a hoisted
10
+ builder), both of which the skeleton's bind list runs in document order, scoped like
11
+ `generateChild` does. Only a standalone `branch`/`case` (consumed by its block, never a
12
12
  loose child) disqualifies. */
13
13
  function skeletonStructure(node: TemplateNode): boolean {
14
14
  if (isControlFlow(node) || node.kind === 'component') {
@@ -0,0 +1,28 @@
1
+ import type { Binding } from './types/Binding.ts'
2
+ import type { TemplateNode } from './types/TemplateNode.ts'
3
+
4
+ /* The structural shape of a `snippet` declaration (`<template name="row" args={item}>`),
5
+ resolved once so the build and SSR back-ends share one reading of it and only own emission.
6
+ Both previously read the name, the raw `args` source, and the body straight off the node.
7
+ The `args` params are now classified ONCE here as `bindings` (`plain` — real call
8
+ parameters, not cells), the single source both back-ends register through `withBindings`. */
9
+ export type SnippetPlan = {
10
+ /* The snippet's name — the hoisted builder function's identifier and call name. */
11
+ name: string
12
+ /* The raw `args` source spliced into the builder's parameter list, or undefined. */
13
+ params: string | undefined
14
+ /* The snippet body. */
15
+ children: TemplateNode[]
16
+ /* The body's bindings: the `args` params (`plain`); empty when the snippet takes none. */
17
+ bindings: Binding[]
18
+ }
19
+
20
+ /* Reads a `snippet` node's structure into the shared structural plan. */
21
+ export function snippetPlan(node: Extract<TemplateNode, { kind: 'snippet' }>): SnippetPlan {
22
+ return {
23
+ name: node.name,
24
+ params: node.params,
25
+ children: node.children,
26
+ bindings: node.params === undefined ? [] : [{ name: node.params, classification: 'plain' }],
27
+ }
28
+ }
@@ -7,13 +7,13 @@ both back-ends pass this set to skip those keys from the spread — keeping the
7
7
  server string and the client DOM congruent (no duplicate attribute, and no
8
8
  SSR-first-wins vs client-spread-wins divergence). Excluding by NAME (not by a
9
9
  runtime present/absent check) means the two sides agree regardless of a dynamic
10
- attribute's runtime value. Static and dynamic (`{expr}`) attrs contribute their
11
- name, a `bind:` its property, an `on<event>` its `on…` form; `attach`/`spread`
12
- carry no attribute name.
10
+ attribute's runtime value. Static, dynamic (`{expr}`), and interpolated
11
+ (`"… {expr} …"`) attrs contribute their name, a `bind:` its property, an
12
+ `on<event>` its `on…` form; `attach`/`spread` carry no attribute name.
13
13
  */
14
14
  export function spreadExcludedNames(attrs: TemplateAttr[]): string[] {
15
15
  return attrs.flatMap((attr) => {
16
- if (attr.kind === 'static' || attr.kind === 'expression') {
16
+ if (attr.kind === 'static' || attr.kind === 'expression' || attr.kind === 'interpolated') {
17
17
  return [attr.name]
18
18
  }
19
19
  if (attr.kind === 'bind') {
@@ -6,10 +6,9 @@ HTML grammar sees only as text and the shadow program lowers away. A pure scan o
6
6
  raw source (independent of a successful parse, so it survives mid-edit), it emits
7
7
  an `operator` token for the `{`+sigil opener and a `keyword` token for the block
8
8
  word, plus the `of`/`by` connectors inside a `{#for …}` head. Expression interiors
9
- (and the `{@const}`/`{@html}` tags and bare `{expr}` interpolations) are NOT touched
10
- here — those are the shadow's job. A keyword allowlist after the sigil prevents
11
- matching arbitrary `{:foo}` text. Longest phrases first so `for await` beats `for`
12
- and `else if` beats `else`.
9
+ and bare `{expr}` interpolations are NOT touched here — those are the shadow's job.
10
+ A keyword allowlist after the sigil prevents matching arbitrary `{:foo}` text.
11
+ Longest phrases first so `for await` beats `for` and `else if` beats `else`.
13
12
  */
14
13
  const BLOCK_KEYWORDS = [
15
14
  'for await',