@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,15 +1,20 @@
1
+ import type { Binding } from './types/Binding.ts'
1
2
  import type { TemplateNode } from './types/TemplateNode.ts'
2
3
 
3
4
  type Case = Extract<TemplateNode, { kind: 'case' }>
4
5
 
5
6
  /* The structural shape of a `switch`, resolved once so the build and SSR back-ends share one
6
7
  reading of its cases and only own emission. Both previously recomputed the `case` filter;
7
- SSR additionally located the match-less default. */
8
+ SSR additionally located the match-less default. A `switch` introduces no names, so
9
+ `bindings` is empty — present so every block plan answers the single binding source
10
+ uniformly. */
8
11
  export type SwitchPlan = {
9
12
  /* The `case` branches in source order (`match` set; the default leaves it unset). */
10
13
  cases: Case[]
11
14
  /* The match-less default branch, if present. */
12
15
  fallback: Case | undefined
16
+ /* A `switch` binds no names. */
17
+ bindings: Binding[]
13
18
  }
14
19
 
15
20
  /* Resolves a `switch` node's cases into the shared structural plan. */
@@ -18,5 +23,6 @@ export function switchPlan(node: Extract<TemplateNode, { kind: 'switch' }>): Swi
18
23
  return {
19
24
  cases,
20
25
  fallback: cases.find((branch) => branch.match === undefined),
26
+ bindings: [],
21
27
  }
22
28
  }
@@ -1,4 +1,6 @@
1
+ import { catchBinding } from './catchBinding.ts'
1
2
  import { resolveBranches } from './resolveBranches.ts'
3
+ import type { Binding } from './types/Binding.ts'
2
4
  import type { TemplateNode } from './types/TemplateNode.ts'
3
5
 
4
6
  /* The structural shape of a `try` error boundary, resolved once so the build and SSR
@@ -14,16 +16,21 @@ export type TryPlan = {
14
16
  finallyChildren: TemplateNode[]
15
17
  /* No catch → a throw propagates to the enclosing boundary (re-throw / `undefined` thunk). */
16
18
  hasCatch: boolean
19
+ /* The catch branch's binding (`catchAs`, `plain`); empty when no catch branch. The
20
+ guarded and `finally` branches bind nothing. */
21
+ catchBindings: Binding[]
17
22
  }
18
23
 
19
24
  /* Resolves a `try` node's branches into the shared structural plan. */
20
25
  export function tryPlan(node: Extract<TemplateNode, { kind: 'try' }>): TryPlan {
21
26
  const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
27
+ const catchAs = catchBranch?.as ?? '_error'
22
28
  return {
23
29
  guarded: node.children.filter((child) => child.kind !== 'branch'),
24
30
  catchChildren: catchBranch?.children ?? [],
25
- catchAs: catchBranch?.as ?? '_error',
31
+ catchAs,
26
32
  finallyChildren: finallyBranch?.children ?? [],
27
33
  hasCatch: catchBranch !== undefined,
34
+ catchBindings: catchBinding(catchAs, catchBranch !== undefined),
28
35
  }
29
36
  }
@@ -25,7 +25,7 @@ export type AnalyzedComponent = {
25
25
  CSS to bundle. A top-level `<style>` covers the whole component; a nested one
26
26
  covers only its sibling subtree. Empty for a component with no style. */
27
27
  styles: { attribute: string; css: string }[]
28
- /* False when the template contains an `await` block (not adoptable yet) — the
29
- router mounts (re-renders) rather than hydrates such a page. */
28
+ /* Always `true` hydration adopts every block in place, including `await`
29
+ (streamed value seeded through the resume manifest). */
30
30
  hydratable: boolean
31
31
  }
@@ -0,0 +1,24 @@
1
+ /*
2
+ A name a block introduces into a body's scope, carried on its Plan and classified
3
+ ONCE as the single source of truth both back-ends read. `name` is the author param
4
+ as written (a plain identifier or a destructuring pattern — `item`, `i`, `_error`,
5
+ `{ a, b }`); its leaf names are derived where it is registered (`withBindings`),
6
+ never re-derived in a back-end. `classification` decides how each back-end RENDERS
7
+ the binding — the only per-back-end choice left:
8
+
9
+ - `reactive` — an `await` `then` value, an `each` item / index. The client binds a
10
+ `.value` cell the runtime can update in place (read as a deref via `reactiveBinding`);
11
+ SSR has no cells, so it renders it as a plain shadow.
12
+ - `plain` — a `catch` error, `snippet` args. A real JS local on both back-ends, read
13
+ as the bare identifier.
14
+
15
+ A binding that mis-lowers to the enclosing component signal is the `block-binding-shadow`
16
+ bug this model designs out: there is one name set and one classification per block, and
17
+ one registration loop (`withBindings`).
18
+ */
19
+ export type Binding = {
20
+ /* The author param as written — a plain identifier or a destructuring pattern. */
21
+ name: string
22
+ /* How each back-end renders it: a reactive `.value` cell (client) vs a plain local. */
23
+ classification: 'reactive' | 'plain'
24
+ }
@@ -12,4 +12,16 @@ an offset in no mapping is synthesised scaffolding and its diagnostic is dropped
12
12
  */
13
13
  export type ShadowMapping = { shadowStart: number; sourceStart: number; length: number }
14
14
 
15
- export type CompiledShadow = { code: string; mappings: ShadowMapping[] }
15
+ /*
16
+ A diagnostic the shadow compiler raises itself (not from `tsc`), already located
17
+ in `.abide` source coordinates — `start`/`length` are an offset range in the
18
+ original file, so it needs no segment remap. Used for author rules the type
19
+ system can't express, e.g. importing a compiler-internal runtime helper.
20
+ */
21
+ export type ShadowDiagnostic = { start: number; length: number; message: string }
22
+
23
+ export type CompiledShadow = {
24
+ code: string
25
+ mappings: ShadowMapping[]
26
+ diagnostics?: ShadowDiagnostic[]
27
+ }
@@ -0,0 +1,43 @@
1
+ import type { ClassStyleMergePlan } from '../classStyleMergePlan.ts'
2
+ import type { TemplateAttr } from './TemplateAttr.ts'
3
+
4
+ /*
5
+ The per-element shared compile decision both back-ends render from — the element-level
6
+ sibling of `skeletonContext`'s tree-level positional model, layered over it (ADR-0013).
7
+ It centralizes the DECISIONS the two code-gen back-ends (`generateBuild`, `generateSSR`)
8
+ previously made twice in hand-mirrored per-attribute dispatch: each attribute's kind, the
9
+ class/style/directive merge, which attrs are folded into that merge (so both skip them),
10
+ and whether the tag is void. Each back-end still RENDERS per kind — build wires live
11
+ effects/thunks, SSR escapes strings — but consults ONE classification, so the markup the
12
+ client clones and the server emits can never drift on the attribute set or merge logic.
13
+
14
+ The positional holes/anchors are NOT recomputed here — they stay in `skeletonContext` and
15
+ each back-end reads `elIndex`/`anIndex`/`markText` from it. `elementPlan` is the decision
16
+ on TOP of that positional pass, not a second positional model.
17
+ */
18
+
19
+ /* One classified attribute on an element. `attr` is the original `TemplateAttr`; `merged*`
20
+ flags it as folded into the class/style merge for that back-end (so the back-end skips it
21
+ in its per-attr dispatch). The merge TRIGGERS differ — build merges only when the base is
22
+ interpolated (a static base stays in the cloned skeleton with surgical toggles), SSR merges
23
+ whenever a directive exists (it must emit one attribute string) — so each side reads its own
24
+ flag. */
25
+ export type ElementPlanAttr = {
26
+ attr: TemplateAttr
27
+ /* Folded into the class/style merge on the build back-end (skip in build's dispatch). */
28
+ mergedBuild: boolean
29
+ /* Folded into the class/style merge on the SSR back-end (skip in SSR's dispatch). */
30
+ mergedSSR: boolean
31
+ }
32
+
33
+ export type ElementPlan = {
34
+ /* Every attribute classified in author order, each tagged with its per-back-end merge
35
+ status. Both back-ends iterate this one list and render per `attr.kind`. */
36
+ attrs: ElementPlanAttr[]
37
+ /* The class/style/directive merge decision (`classStyleMergePlan`) folded in as this
38
+ plan's class/style branch — both back-ends render the merged value from its parts. */
39
+ merge: ClassStyleMergePlan
40
+ /* A void tag (`<img>`, `<input>`) has no closing tag and no children — both back-ends
41
+ emit the open tag only. From the shared `VOID_TAGS` set. */
42
+ isVoid: boolean
43
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ Which deref a branch-local shadow name lowers to. A block value param either binds
3
+ as a reactive `.value` cell (`derived` — read as `name.value`, the client back-end's
4
+ keyed-each item / `await then` value) or as a plain JS local holding the resolved
5
+ value (`plain` — read as the bare identifier, the SSR back-end's inline `await` value
6
+ and both back-ends' snippet/catch params). A name MUST be registered under the kind
7
+ its emitted code reads it as, or it mis-lowers to the component signal it shadows.
8
+ */
9
+ export type ShadowKind = 'derived' | 'plain'
@@ -0,0 +1,19 @@
1
+ import type { ShadowKind } from './ShadowKind.ts'
2
+
3
+ /*
4
+ The typed branch-local shadow stack both lowering back-ends thread through. Replaces
5
+ the two loose `Set`s reached via closure with one value owning both kinds, and the
6
+ manual push-then-pop with a single structured `withShadow` that pushes on entry and
7
+ pops in a `finally` — so a branch's shadows cannot outlive the branch even if the body
8
+ throws (the SSR `await then` TDZ path threw mid-body and leaked under the old hand pop).
9
+
10
+ `names` snapshots a kind for the expression transformer: a binding pushed mid-compile
11
+ must be honoured AND shadow a same-named component signal.
12
+ */
13
+ export type ShadowScope = {
14
+ /* Push the names not already present (under `kind`) for the duration of `body`, then
15
+ pop exactly what was added — in a `finally`, so a throw cannot leak the shadow. */
16
+ withShadow: <T>(names: Iterable<string>, kind: ShadowKind, body: () => T) => T
17
+ /* A snapshot of the names currently shadowing under `kind`, for the transformer. */
18
+ names: (kind: ShadowKind) => ReadonlySet<string>
19
+ }
@@ -1,3 +1,5 @@
1
+ import type { TextPart } from './TextPart.ts'
2
+
1
3
  /*
2
4
  An element attribute. `static` is a literal — `bare` marks a valueless attribute
3
5
  (`disabled`, not `disabled=""`), letting a component coerce it to `true` while a
@@ -15,6 +17,11 @@ optional, set only when the parser tracks positions for the type-checking shadow
15
17
  export type TemplateAttr =
16
18
  | { kind: 'static'; name: string; value: string; bare?: true; nameLoc?: number }
17
19
  | { kind: 'expression'; name: string; code: string; loc?: number; nameLoc?: number }
20
+ /* `name="literal {expr}"` — a quoted value mixing literal text with `{expr}`
21
+ interpolations (the text-node `parts` model on an attribute). Bound reactively
22
+ as the template-literal concatenation of its parts; always yields a string, so
23
+ the attribute is always present. */
24
+ | { kind: 'interpolated'; name: string; parts: TextPart[]; nameLoc?: number }
18
25
  | { kind: 'event'; event: string; code: string; loc?: number }
19
26
  | { kind: 'bind'; property: string; code: string; loc?: number }
20
27
  /* `class:<name>={code}` — toggles the class on/off by `code`'s truthiness, written
@@ -3,8 +3,8 @@ import type { TextPart } from './TextPart.ts'
3
3
 
4
4
  /*
5
5
  A parsed template node. `text` carries interpolation parts; `element` carries
6
- attributes and children; `each` is the `<template each as key>` control flow over
7
- a list; `if`/`await`/`switch`/`try` are its control-flow siblings.
6
+ attributes and children; `each` is the `{#for items as item}` control flow over a
7
+ list; `if`/`await`/`switch`/`try` are its control-flow siblings.
8
8
 
9
9
  `loc` (where present) is the absolute offset of the node's primary expression in
10
10
  the original `.abide` source — additive, set only when the parser tracks
@@ -12,7 +12,7 @@ positions for the type-checking shadow, ignored by the runtime back-ends.
12
12
  */
13
13
  export type TemplateNode =
14
14
  | { kind: 'text'; parts: TextPart[] }
15
- | { kind: 'script'; code: string }
15
+ | { kind: 'script'; code: string; loc?: number }
16
16
  /* A `<style>` declared in the template. `css` is its raw body, read structurally
17
17
  so a `<style>` inside an expression isn't mistaken for one. The node stays in
18
18
  place: it scopes its sibling subtree, so the front-end derives each scope
@@ -50,9 +50,9 @@ export type TemplateNode =
50
50
  | {
51
51
  kind: 'await'
52
52
  promise: string
53
- /* `then` riding the `await` tag (`<template await={p} then={v}>`) makes the
54
- block BLOCKING: no pending branch, children are the resolved content bound
55
- to `as`, SSR settles before the first flush. Absent → streaming. */
53
+ /* `then` in the await head (`{#await p then v}`) makes the block BLOCKING:
54
+ no pending branch, children are the resolved content bound to `as`, SSR
55
+ settles before the first flush. Absent → streaming. */
56
56
  blocking: boolean
57
57
  as: string | undefined
58
58
  children: TemplateNode[]
@@ -93,9 +93,9 @@ export type TemplateNode =
93
93
  children: TemplateNode[]
94
94
  loc?: number
95
95
  }
96
- /* A `<template name="row" args={item}>` snippet: a named, scope-capturing
97
- builder declared once and called like a function (`{row(item)}`). `params`
98
- is the raw `args` source spliced into the builder's parameter list. */
96
+ /* A `{#snippet row(item)}` snippet: a named, scope-capturing builder declared
97
+ once and called like a function (`{row(item)}`). `params` is the raw
98
+ parameter list from the parens (absent when there are no parameters). */
99
99
  | {
100
100
  kind: 'snippet'
101
101
  name: string
@@ -0,0 +1,45 @@
1
+ import { destructureBindingNames } from './destructureBindingNames.ts'
2
+ import type { Binding } from './types/Binding.ts'
3
+ import type { ShadowKind } from './types/ShadowKind.ts'
4
+ import type { ShadowScope } from './types/ShadowScope.ts'
5
+
6
+ /*
7
+ The ONE shared registration path for a block's bindings, built on `withShadow`. Iterates
8
+ a plan's `Binding[]` once, derives each binding's leaf names (`destructureBindingNames` —
9
+ the single name derivation), and registers them under the `ShadowKind` the injected
10
+ `kindMapping` returns, for the duration of `body`. The back-ends differ ONLY in the
11
+ mapping they inject: the client maps `reactive → derived` (a `.value` cell, its wiring
12
+ arranged separately via `reactiveBinding`) and `plain → plain`; SSR has no cells, so it
13
+ maps every binding → `plain`. A name a block introduces therefore flows to exactly one
14
+ shadow kind, decided by the plan's classification and one mapping — `block-binding-shadow`
15
+ (a name registered on one back-end but not the other, or under the wrong kind) is designed
16
+ out, not test-caught.
17
+ */
18
+ export function withBindings<T>(
19
+ withShadow: ShadowScope['withShadow'],
20
+ bindings: Binding[],
21
+ kindMapping: (binding: Binding) => ShadowKind,
22
+ body: () => T,
23
+ ): T {
24
+ /* Group each binding's leaf names by the kind the mapping assigns, so one `withShadow`
25
+ per kind registers them all (nested, so both kinds shadow simultaneously). */
26
+ const byKind = new Map<ShadowKind, string[]>()
27
+ for (const binding of bindings) {
28
+ const kind = kindMapping(binding)
29
+ const names = byKind.get(kind) ?? []
30
+ names.push(...destructureBindingNames(binding.name))
31
+ byKind.set(kind, names)
32
+ }
33
+ /* Fold the per-kind `withShadow` calls around `body` — each pushes its names on entry
34
+ and pops them in a `finally`, so no branch's shadows outlive the branch. */
35
+ const entries = [...byKind]
36
+ const run = (index: number): T => {
37
+ const entry = entries[index]
38
+ if (entry === undefined) {
39
+ return body()
40
+ }
41
+ const [kind, names] = entry
42
+ return withShadow(names, kind, () => run(index + 1))
43
+ }
44
+ return run(0)
45
+ }
@@ -13,7 +13,7 @@ import type { PersistHandle } from './types/PersistHandle.ts'
13
13
  import type { Scope } from './types/Scope.ts'
14
14
 
15
15
  /* A process-stable counter id. The serialization-stable LEXICAL id (route +
16
- component + tree position) that `persistent`/`sync` want across reloads/peers
16
+ component + tree position) that `persist`/`broadcast` want across reloads/peers
17
17
  is stamped by the compiler later; until then a scope's id is unique within a
18
18
  run — enough for in-session undo and the bus, and as an explicit key's fallback. */
19
19
  let nextId = 0
@@ -23,7 +23,7 @@ Builds a lexical scope. Its data is a document — created eagerly from `initial
23
23
  or (when `awaiting`) ADOPTED from the first `doc()` a component body creates under
24
24
  it, so a scope can wrap the component's own model without changing the data
25
25
  lowering. Data methods mirror `Doc` and delegate to that document; capabilities
26
- are lazy (`undoable`/`persistent` attach `history`/`persist` to it on first call);
26
+ are lazy (`record`/`persist` attach `history`/`persist` to it on first call);
27
27
  `child` nests; `dispose` tears the subtree down children-first.
28
28
  */
29
29
  export function createScope(
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  The MathML foreign-content namespace — the `<math>` counterpart to `SVG_NAMESPACE`.
3
- The imperative build reads it to namespace MathML elements the static `skeleton` path
4
- doesn't cover.
3
+ The runtime reads it to namespace MathML children mounted into a `<math>` parent
4
+ dynamically, where no parser-built ancestor namespaces them for free.
5
5
  */
6
6
  export const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'
@@ -1,7 +1,8 @@
1
1
  /*
2
2
  The SVG foreign-content namespace. An element created in it (or cloned from a
3
3
  parser-built `<svg>` subtree) renders as SVG; the same tag in the HTML namespace
4
- renders as nothing. The imperative build reads it to namespace foreign elements that
5
- the static `skeleton` path doesn't cover (a foreign parent with dynamic children).
4
+ renders as nothing. The runtime reads it (via `effectiveChildNamespace`) to namespace
5
+ foreign children mounted into a foreign parent dynamically at a skeleton anchor or in a
6
+ control-flow fragment — where no parser-built `<svg>` ancestor namespaces them for free.
6
7
  */
7
8
  export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
@@ -16,7 +16,7 @@ node: created and appended (create), or the server-rendered text node claimed
16
16
  claimed node is split at the current value's length — deterministic, because
17
17
  `read()` returns the same value the server rendered.
18
18
 
19
- A value branded by `html\`…\`` (see abide/shared/html) inserts raw markup instead:
19
+ A value branded by `html\`…\`` (see abide/ui/html) inserts raw markup instead:
20
20
  its parsed nodes go between an anchor (create), or the server-rendered nodes
21
21
  between `<!--abide:html-->`/`<!--/abide:html-->` markers are adopted (hydrate), and
22
22
  a change re-parses and swaps. A binding is text or raw for its lifetime (decided by
@@ -3,7 +3,7 @@ import { isComment } from './isComment.ts'
3
3
 
4
4
  /*
5
5
  Bundle-side consumer of an SSR stream chunk, the counterpart of the doc stream's inline
6
- vanilla scripts (SSR_SWAP_SCRIPT's `__abideSwap` + `__abideResolve`) for a stream the
6
+ vanilla scripts (`SSR_SWAP_SCRIPT`'s `__abideSwap` and `CACHE_RESOLVE_SCRIPT`'s `__abideResolve`) for a stream the
7
7
  running bundle consumes itself — streaming SPA navigation, socket-delivered SSR. It routes
8
8
  the two chunk kinds the stream interleaves:
9
9
 
@@ -6,9 +6,10 @@ import { enterNamespace } from './enterNamespace.ts'
6
6
  Builds a `[ … ]`-bounded content range into a DETACHED fragment under a fresh reactive
7
7
  scope, then returns the markers, the fragment, and the scope disposer — leaving
8
8
  INSERTION to the caller. The create-path range builder for the keyed list runtimes
9
- (`each` / `eachAsync`): `each` holds the fragment in `pending` for deferred placement
10
- (its reconcile reorders rows), `eachAsync` inserts it immediately at the stream anchor
11
- (arrival order). Both need the same bracketed-fragment build, so it lives here once.
9
+ (`each` / `eachAsync`) and `awaitBlock` (branch swaps): `each` holds the fragment in
10
+ `pending` for deferred placement (its reconcile reorders rows), `eachAsync` inserts it
11
+ immediately at the stream anchor (arrival order). All need the same bracketed-fragment
12
+ build, so it lives here once.
12
13
 
13
14
  Unlike `openMarker`, this never touches a live parent or the hydrate claim cursor — the
14
15
  markers are created fresh and parked in a fragment — so it is a pure CREATE primitive;
@@ -19,8 +19,9 @@ the same top-level node count as the server markup.
19
19
  */
20
20
  // @documentation plumbing
21
21
  export function cloneStatic(parent: Node, html: string): void {
22
- /* When `parent` is a foreign element built imperatively, the run's markup has no
23
- foreign ancestor of its own, so a bare `<path>` would parse into the HTML
22
+ /* When `parent` is a foreign element built dynamically (not part of this static
23
+ run's own parser-built subtree), the run's markup has no foreign ancestor of
24
+ its own, so a bare `<path>` would parse into the HTML
24
25
  namespace. Parse it inside a matching `<svg>`/`<math>` wrapper and clone the
25
26
  wrapper's children — the parser namespaces them correctly. */
26
27
  const wrapper = foreignWrapperTag(parent)
@@ -42,6 +42,10 @@ export function each<T>(
42
42
  before: Node | null = null,
43
43
  ): void {
44
44
  const rows = new Map<string, EachRow>()
45
+ /* Monotonic reconcile-pass id. Each reconcile stamps every surviving row with the
46
+ current pass, so prune is a `row.gen !== pass` int compare instead of building a
47
+ `Set` of present keys every reconcile. */
48
+ let generation = 0
45
49
  /* Each row's scope, registered with the owner so every live row disposes on owner
46
50
  teardown (the effect's own disposer only unsubscribes it from `items()`). */
47
51
  const group = scopeGroup()
@@ -136,11 +140,26 @@ export function each<T>(
136
140
  try {
137
141
  const list = Array.isArray(source) ? source : [...source]
138
142
  const keys = list.map(keyOf)
139
- const present = new Set(keys)
140
- /* Prune departed rows first so their ranges don't sit between survivors and
141
- throw off the in-place sibling checks below. */
143
+ generation += 1
144
+ const pass = generation
145
+ /* Resolve each desired key to its surviving row once, here, stamping it with
146
+ this pass — so prune below is an int compare (no `Set`) and the placement
147
+ walk reuses the resolved row instead of a second `rows.get`. Halving the map
148
+ lookups per reconcile is a real win once the harness's O(n) `nextSibling` no
149
+ longer swamps it. `undefined` marks a key with no surviving row — a build. */
150
+ const resolved: (EachRow | undefined)[] = new Array(keys.length)
151
+ for (let index = 0; index < keys.length; index += 1) {
152
+ const row = rows.get(keys[index] as string)
153
+ if (row !== undefined) {
154
+ row.gen = pass
155
+ }
156
+ resolved[index] = row
157
+ }
158
+ /* Prune departed rows first (those the resolve loop didn't reach this pass) so
159
+ their ranges don't sit between survivors and throw off the in-place sibling
160
+ checks below. */
142
161
  for (const [key, row] of rows) {
143
- if (!present.has(key)) {
162
+ if (row.gen !== pass) {
144
163
  row.dispose()
145
164
  removeRange(row.start, row.end)
146
165
  rows.delete(key)
@@ -151,11 +170,11 @@ export function each<T>(
151
170
  freshly built) rows move. */
152
171
  let cursor: Node = anchor
153
172
  for (let index = list.length - 1; index >= 0; index -= 1) {
154
- const key = keys[index] as string
155
- let row = rows.get(key)
173
+ let row = resolved[index]
156
174
  if (row === undefined) {
157
175
  row = buildRow(list[index] as T, index)
158
- rows.set(key, row)
176
+ row.gen = pass
177
+ rows.set(keys[index] as string, row)
159
178
  } else {
160
179
  /* Surviving key: push the (possibly new) item and position into the row's
161
180
  cells. Both write through `Object.is`, so an unchanged item/position is a
@@ -6,8 +6,8 @@ import { SVG_NAMESPACE } from './SVG_NAMESPACE.ts'
6
6
  The wrapper tag a static run must be parsed inside so its children land in `parent`'s
7
7
  foreign namespace — `svg`/`math`, or undefined for HTML. A bare `<path>` fragment
8
8
  parses into the HTML namespace; wrapping it in `<svg>` lets the parser namespace it.
9
- `cloneStatic` uses this for a static run coalesced under a foreign parent that was
10
- built imperatively, or under a control-flow block's fragment inside foreign content
9
+ `cloneStatic` uses this for a static run coalesced under a foreign parent built
10
+ dynamically, or under a control-flow block's fragment inside foreign content
11
11
  (where `parent`'s effective namespace comes from the ambient context).
12
12
  */
13
13
  export function foreignWrapperTag(parent: Node): string | undefined {
@@ -5,6 +5,13 @@ import { registerHotInstance } from '../runtime/registerHotInstance.ts'
5
5
  import type { UiComponent } from '../runtime/types/UiComponent.ts'
6
6
  import { mountRange } from './mountRange.ts'
7
7
 
8
+ /* Build-time flag the production client defines false (see build.ts `define`) so the hot
9
+ path below — and its captureModelDoc/registerHotInstance imports — dead-code-eliminate
10
+ (`!false` folds to a constant the minifier can prove) instead of shipping behind a runtime
11
+ flag it can't. Dev defines it true; the test preload sets it on globalThis so the bare
12
+ reference resolves to true there, leaving `hotReloadEnabled` alone in charge as before. */
13
+ declare const __ABIDE_DEV__: boolean
14
+
8
15
  /*
9
16
  Mounts a child component as a marker-bounded range at `before` in `parent` — no
10
17
  wrapper element, so the child's root is a true direct child of the parent (see
@@ -24,7 +31,7 @@ export function mountChild(
24
31
  label?: string,
25
32
  ): void {
26
33
  const moduleId = factory.__abideId
27
- if (!hotReloadEnabled.current || moduleId === undefined) {
34
+ if (!__ABIDE_DEV__ || !hotReloadEnabled.current || moduleId === undefined) {
28
35
  mountRange(parent, factory.build, props, before, label)
29
36
  return
30
37
  }
@@ -0,0 +1,79 @@
1
+ import { effect } from '../effect.ts'
2
+ import { RANGE_CLOSE, RANGE_OPEN } from '../runtime/RANGE_MARKER.ts'
3
+ import { RENDER } from '../runtime/RENDER.ts'
4
+ import { scope } from '../runtime/scope.ts'
5
+ import { scopeGroup } from '../runtime/scopeGroup.ts'
6
+ import { fillBefore } from './fillBefore.ts'
7
+ import { openMarker } from './openMarker.ts'
8
+ import { replaceRange } from './replaceRange.ts'
9
+
10
+ /*
11
+ The shared lifecycle of every single-slot control-flow block (`when`, `switchBlock`):
12
+ a marker-bounded RANGE that holds at most one piece of content at a time, swapped as a
13
+ unit when a `key` changes. The block supplies a `key()` thunk (the identity of the
14
+ chosen branch — `'then'|'else'` for `when`, the case index for `switchBlock`) and a
15
+ `contentFor(key)` resolver returning that branch's builder, or `undefined` for an
16
+ empty branch. This module owns the marker setup, the hydrate-vs-create fork, and the
17
+ swap effect with its reentrancy/teardown dance — the structure both blocks copy-pasted.
18
+
19
+ The chosen branch builds through `scope` (directly on hydrate, via `fillBefore` on a
20
+ swap), which builds untracked — so a raw reactive read in the branch content doesn't
21
+ subscribe the swap effect; only `key()` (which reads the block's source) drives the
22
+ toggle. The branch's own interpolations still track, each through its own effect.
23
+
24
+ On hydrate it adopts the server-rendered range: claim the start marker, run the chosen
25
+ branch in place (its content claims the existing nodes), then claim the end marker. The
26
+ effect's first run sees the same key and is a no-op; later changes clear the range and
27
+ build fresh into a fragment.
28
+
29
+ `before` (a static node located by the skeleton) places the range among siblings on
30
+ create, so the block sits before a static suffix rather than at the parent's end.
31
+ Hydrate ignores it — the claim cursor (positioned past the prefix) drives placement.
32
+ */
33
+ export function mountSwappableRange<Key>(
34
+ parent: Node,
35
+ key: () => Key,
36
+ contentFor: (key: Key) => ((parent: Node) => void) | undefined,
37
+ before: Node | null = null,
38
+ ): void {
39
+ const hydration = RENDER.hydration
40
+ /* The live branch's scope, registered with the owner so it disposes on owner
41
+ teardown — not only on a branch swap via replaceRange. */
42
+ const group = scopeGroup()
43
+ let dispose: (() => void) | undefined
44
+ let activeKey: Key
45
+ let end: Comment
46
+
47
+ const start = openMarker(parent, RANGE_OPEN, before)
48
+ if (hydration !== undefined) {
49
+ activeKey = key()
50
+ const chosen = contentFor(activeKey)
51
+ if (chosen !== undefined) {
52
+ dispose = group.track(scope(() => chosen(parent))) // content claims the SSR nodes in place
53
+ }
54
+ end = openMarker(parent, RANGE_CLOSE)
55
+ } else {
56
+ end = openMarker(parent, RANGE_CLOSE, before)
57
+ activeKey = key()
58
+ const chosen = contentFor(activeKey)
59
+ if (chosen !== undefined) {
60
+ dispose = group.track(fillBefore(end, chosen))
61
+ }
62
+ }
63
+
64
+ effect(() => {
65
+ const next = key()
66
+ if (next === activeKey) {
67
+ return
68
+ }
69
+ activeKey = next
70
+ const chosen = contentFor(next)
71
+ /* Null `dispose` before `replaceRange` builds the new branch: a reentrant swap
72
+ during that build (an effect in the new content writing the source) would
73
+ otherwise re-enter with the already-disposed disposer and clear it twice. */
74
+ const prior = dispose
75
+ dispose = undefined
76
+ const built = replaceRange(start, end, prior, chosen)
77
+ dispose = built !== undefined ? group.track(built) : undefined
78
+ })
79
+ }
@@ -1,9 +1,11 @@
1
1
  /*
2
2
  Guarded method call on a reactive-document read. The doc-access lowering rewrites
3
- `model.draft.trim()` into `model.read("draft").trim()`; when that read is nullish
4
- the bare form throws the engine's opaque `undefined is not an object (evaluating
5
- '…read("draft").trim()')`, naming only the desugared call. This wraps the
6
- non-optional call so the throw names the authored scope path and member instead —
3
+ a non-optional method call on a reactive-document read — e.g. `model.draft.trim()`
4
+ directly to `$$readCall(model.read("draft"), "draft", "trim", [...])` rather than
5
+ the naive `model.read("draft").trim()`, which would throw the engine's opaque
6
+ `undefined is not an object (evaluating '…read("draft").trim()')`, naming only the
7
+ desugared call. This wraps the non-optional call so the throw names the authored
8
+ scope path and member instead —
7
9
  the source-mapped stack frame still resolves to the `.abide` line, so message and
8
10
  location together read in authored terms. Both opaque engine errors are covered: a
9
11
  nullish receiver (`undefined is not an object`) AND a present receiver whose member
@@ -4,9 +4,9 @@ establishes, derived from its host element's tag. The router instead fills layer
4
4
  marker boundaries with no host element, so it passes each layer's route key as an
5
5
  explicit label (`fillBoundary`); a nested child likewise passes its component name (see
6
6
  `mountRange`). The `abide-` prefix strip turns a framework host tag like `abide-resolve`
7
- into `resolve`; any other host yields its lowercased tag. Dev-only feeds the
8
- inspector's Reactive tab a readable scope name. Returns undefined when there's no element
9
- to name from.
7
+ into `resolve`; any other host yields its lowercased tag. Feeds the inspector's
8
+ Reactive tab a readable scope name; computed and stored in all builds (only the
9
+ inspector surface is dev-only). Returns undefined when there's no element to name from.
10
10
  */
11
11
  import { COMPONENT_WRAPPER_PREFIX } from '../COMPONENT_WRAPPER_PREFIX.ts'
12
12
 
@@ -129,10 +129,19 @@ export function skeleton(parent: Node, html: string): SkeletonHoles {
129
129
  hydration.next.set(parent, node)
130
130
  } else {
131
131
  const children = source.childNodes
132
+ /* Stage clones in a fragment ONLY for a live (connected) parent, where one
133
+ append reflows once instead of per clone. A detached parent triggers no
134
+ reflow on append, so the fragment is pure overhead there — skip it and
135
+ append direct. `topLevel` collects each clone either way for anchor/
136
+ element-hole resolution. */
137
+ const target: Node = parent.isConnected ? document.createDocumentFragment() : parent
132
138
  for (let index = 0; index < children.length; index += 1) {
133
139
  const clone = (children[index] as Node).cloneNode(true)
134
140
  topLevel.push(clone)
135
- parent.appendChild(clone)
141
+ target.appendChild(clone)
142
+ }
143
+ if (target !== parent) {
144
+ parent.appendChild(target)
136
145
  }
137
146
  }
138
147
  /* Anchor holes via the ONE shared ordering rule (`walkAnchorOrder`) — the same traversal