@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,20 +1,33 @@
1
1
  import { REACTIVE_CONTEXT } from './REACTIVE_CONTEXT.ts'
2
+ import type { ReactiveNode } from './types/ReactiveNode.ts'
2
3
  import { updateIfNecessary } from './updateIfNecessary.ts'
3
4
 
4
5
  /*
5
6
  Drains queued effects synchronously. Each is queued when it first leaves CLEAN, but
6
7
  runs only if `updateIfNecessary` finds a dependency that truly changed — a CHECK
7
8
  effect whose deps all memoised back to equal values settles to CLEAN without
8
- running its body. Snapshots and clears the queue each pass so an effect that dirties
9
- further effects re-queues them for the next pass rather than mutating the set
10
- mid-iteration; loops until the graph settles.
9
+ running its body. Double-buffers the queue each pass swaps in a fresh array and
10
+ drains the captured one so an effect that dirties further effects re-queues them
11
+ for the next pass rather than mutating the array mid-iteration; loops until the
12
+ graph settles. The swap reuses the drained array as the next spare, so a steady
13
+ flush allocates nothing.
11
14
  */
12
15
  export function flushEffects(): void {
13
- while (REACTIVE_CONTEXT.pendingEffects.size > 0) {
14
- const batch = [...REACTIVE_CONTEXT.pendingEffects]
15
- REACTIVE_CONTEXT.pendingEffects.clear()
16
- for (const node of batch) {
17
- updateIfNecessary(node)
18
- }
16
+ /* Empty-queue fast path allocates nothing — only the length check, matching the
17
+ old `Set.size` guard. Hot: a write with no subscribed effect (e.g. a reconcile's
18
+ index-cell write nobody reads) still calls here at batch-exit, thousands of times
19
+ per list op; the spare array below must not be born on that path. */
20
+ if (REACTIVE_CONTEXT.pendingEffects.length === 0) {
21
+ return
19
22
  }
23
+ let spare: ReactiveNode[] = []
24
+ do {
25
+ const batch = REACTIVE_CONTEXT.pendingEffects
26
+ REACTIVE_CONTEXT.pendingEffects = spare
27
+ for (let index = 0; index < batch.length; index += 1) {
28
+ updateIfNecessary(batch[index] as ReactiveNode)
29
+ }
30
+ batch.length = 0
31
+ spare = batch
32
+ } while (REACTIVE_CONTEXT.pendingEffects.length > 0)
20
33
  }
@@ -2,7 +2,7 @@ import type { AbideHistoryState } from './types/AbideHistoryState.ts'
2
2
 
3
3
  /*
4
4
  Per-history-entry scroll buckets for manual scroll restoration. The browser's own
5
- restoration is disabled (`history.scrollRestoration = 'manual'` at router boot)
5
+ restoration is disabled (`history.scrollRestoration = 'manual'` after the first build/hydration)
6
6
  because the router tears the page down and rebuilds it AFTER the browser would have
7
7
  restored scroll — so the offset is lost against a node that no longer exists. Instead
8
8
  each history entry carries a monotonic id (stamped into `history.state.abideEntry` by
@@ -15,8 +15,10 @@ observer is reading the same source in the same order as before — advance the
15
15
  cursor and reuse the edge, allocating nothing. Otherwise splice a fresh edge in at
16
16
  the cursor; `runNode` trims whatever stale edges trail the cursor when compute ends.
17
17
 
18
- A consecutive re-read of the same source (`dep` already the edge at the cursor) is
19
- absorbed by the reuse check, so `{x} {x}` in one computation links `x` once.
18
+ On re-runs, an edge whose `dep` matches the next slot in the established list is
19
+ reused in-place (cursor advances, no allocation). New edges are still appended for
20
+ reads that extend the list or find a mismatched slot — including a second consecutive
21
+ read of the same source when no matching edge yet follows the cursor.
20
22
  */
21
23
  export function track(dep: ReactiveNode): void {
22
24
  const sub = REACTIVE_CONTEXT.observer
@@ -15,27 +15,41 @@ reaches). `nextSub` is read before recursing so the walk holds no edge a deeper
15
15
  pass could detach.
16
16
  */
17
17
  function mark(node: ReactiveNode, status: number): void {
18
- if (node.status >= status) {
19
- return
20
- }
21
- const wasClean = node.status === NODE_STATE.CLEAN
22
- node.status = status
23
- if (node.isEffect) {
24
- if (wasClean) {
25
- REACTIVE_CONTEXT.pendingEffects.add(node)
18
+ /* Tail-iterative down the subscriber cone: a node with a single subscriber (a
19
+ linear computed chain) advances by reassigning `node` instead of recursing, so a
20
+ deep chain costs a loop, not a 500-frame call stack. Only a true fan-out (>1
21
+ subscriber) recurses, and only for the extra branches. */
22
+ let nextStatus = status
23
+ for (;;) {
24
+ if (node.status >= nextStatus) {
25
+ return
26
26
  }
27
- return
28
- }
29
- /* A computed propagates CHECK to its subscribers only on its first move out of
30
- CLEAN they are already CHECK on any later upgrade, so re-walking is wasted. */
31
- if (!wasClean) {
32
- return
33
- }
34
- let link = node.subsHead
35
- while (link !== undefined) {
36
- const next = link.nextSub
37
- mark(link.sub, NODE_STATE.CHECK)
38
- link = next
27
+ const wasClean = node.status === NODE_STATE.CLEAN
28
+ node.status = nextStatus
29
+ if (node.isEffect) {
30
+ if (wasClean) {
31
+ REACTIVE_CONTEXT.pendingEffects.push(node)
32
+ }
33
+ return
34
+ }
35
+ /* A computed propagates CHECK to its subscribers only on its first move out of
36
+ CLEAN they are already CHECK on any later upgrade, so re-walking is wasted. */
37
+ if (!wasClean) {
38
+ return
39
+ }
40
+ const head = node.subsHead
41
+ if (head === undefined) {
42
+ return
43
+ }
44
+ /* Recurse into every branch but the last; continue the loop on the last, so the
45
+ common single-subscriber chain never recurses. */
46
+ let link = head
47
+ while (link.nextSub !== undefined) {
48
+ mark(link.sub, NODE_STATE.CHECK)
49
+ link = link.nextSub
50
+ }
51
+ node = link.sub
52
+ nextStatus = NODE_STATE.CHECK
39
53
  }
40
54
  }
41
55
 
@@ -1,8 +1,8 @@
1
1
  /* A routable page/layout. Callable to mount directly into a host (the direct-mount
2
2
  API), but the router instead uses `build` — the bare client build — to fill the
3
3
  layer into its outlet boundary as a marker range (no `<abide-outlet>` wrapper; see
4
- `fillBoundary`/`outlet`). `hydratable` (false when the page has an `await` block)
5
- tells the router whether the first paint adopts the SSR DOM in place. */
4
+ `fillBoundary`/`outlet`). `hydratable` (always true for compiled components; undefined on manually-constructed
5
+ Route objects) tells the router whether the first paint adopts the SSR DOM in place. */
6
6
  export type Route = ((host: Element, props?: unknown) => (() => void) | undefined) & {
7
7
  build: (host: Node, props?: unknown) => void
8
8
  hydratable?: boolean
@@ -10,8 +10,9 @@ page/route registries carry — abide-ui's compiled-component shape.
10
10
  export type UiComponent = ((host: Element, props?: UiProps) => () => void) & {
11
11
  /* `ctx` is the request-local block-id counter, threaded so a child's ids share the
12
12
  page's depth-first numbering; omitted at the top level (a fresh counter defaults
13
- in). Returns a Promise when the component has an inline `await` (a blocking `{#await
14
- … then}` block or a child render); otherwise renders synchronously. Callers `await`
13
+ in). Returns a Promise when the component contains an inline `await` a blocking
14
+ `{#await … then}` block, a child-component render, a `<slot>` read, or a
15
+ top-level `await` in the author script; otherwise renders synchronously. Callers `await`
15
16
  it either way (awaiting a sync value just returns it). */
16
17
  render: (props?: UiProps, ctx?: RenderContext) => SsrRender | Promise<SsrRender>
17
18
  hydrate?: (host: Element, props?: UiProps) => () => void
@@ -1,14 +1,11 @@
1
1
  /*
2
- What a component is invoked with. Two real shapes flow through the one parameter.
3
- A top-level page/layout is called by the router (client) and `renderChain` (SSR)
4
- with its decoded route params a plain string map. A nested child is called by
5
- the compiler-emitted `mountChild` with a map of reactive thunks (each authored
6
- prop, read in the body as `$props[name]?.()`) plus an optional `$children` slot
7
- builder carrying the component's `<slot>` markup, mounting into the host element
8
- it is handed.
2
+ What a component is invoked with. A top-level page/layout is called by the router
3
+ (client) and `renderChain` (SSR) with its route params as reactive thunks, shaped
4
+ identically to the thunk map `mountChild` passes a nested child so `props()` reads
5
+ (`$props[name]?.()`) work uniformly on both. `$children` is the slot builder a parent
6
+ component passes (carrying the component's `{children()}` content), or `CHILD_PRESENT`
7
+ the router/SSR set on a layout that has a child layer below it.
9
8
  */
10
- export type UiProps =
11
- | Record<string, string>
12
- | (Record<string, () => unknown> & {
13
- $children?: (host: Element) => void
14
- })
9
+ export type UiProps = Record<string, (() => unknown) | ((host: Element) => void)> & {
10
+ $children?: (host: Element) => void
11
+ }
@@ -17,13 +17,25 @@ export function walkPath(tree: unknown, path: string): PathWalk {
17
17
  if (path === '') {
18
18
  return { exists: tree !== undefined, value: tree }
19
19
  }
20
+ /* A path with no `~` carries no escape in any segment, so the per-segment
21
+ unescape is skipped wholesale — the case for every plain-identifier path. */
22
+ const escaped = path.includes('~')
20
23
  let current: unknown = tree
21
- for (const rawSegment of path.split('/')) {
22
- const segment = unescapeKey(rawSegment)
24
+ /* Scan `/`-separated segments by index, no `split` array allocated — this runs
25
+ once per candidate on the structural descend scan, the hot path's hot loop. */
26
+ let start = 0
27
+ for (;;) {
28
+ const slash = path.indexOf('/', start)
29
+ const end = slash === -1 ? path.length : slash
30
+ const raw = path.slice(start, end)
31
+ const segment = escaped ? unescapeKey(raw) : raw
23
32
  if (current === null || typeof current !== 'object' || !(segment in current)) {
24
33
  return { exists: false, value: undefined }
25
34
  }
26
35
  current = (current as Record<string, unknown>)[segment]
36
+ if (slash === -1) {
37
+ return { exists: true, value: current }
38
+ }
39
+ start = slash + 1
27
40
  }
28
- return { exists: true, value: current }
29
41
  }
@@ -1,26 +1,41 @@
1
1
  import { createCacheStore } from '../shared/createCacheStore.ts'
2
+ import { healthSeedSlot } from '../shared/healthSeedSlot.ts'
3
+ import { rpcTimeoutSlot } from '../shared/rpcTimeoutSlot.ts'
4
+ import { setAppName } from '../shared/setAppName.ts'
2
5
  import { setBaseResolver } from '../shared/setBaseResolver.ts'
3
6
  import { setCacheStoreResolver } from '../shared/setCacheStoreResolver.ts'
4
7
  import { setGlobalCacheStoreResolver } from '../shared/setGlobalCacheStoreResolver.ts'
5
8
  import { setPageResolver } from '../shared/setPageResolver.ts'
6
9
  import type { CacheSnapshotEntry } from '../shared/types/CacheSnapshotEntry.ts'
7
10
  import type { StreamedResolution } from '../shared/types/StreamedResolution.ts'
8
- import { installHotBridge } from './installHotBridge.ts'
9
- import { installInspectorBridge } from './installInspectorBridge.ts'
10
11
  import { probeNavigation } from './probeNavigation.ts'
12
+
13
+ /* Build-time flag the production client defines false (see build.ts `define`) so the dev-only
14
+ hot bridge — and the entire DOM runtime it statically pulls in for re-builds — is dead-code-
15
+ eliminated, not even emitted as a chunk. Dev defines it true; the test preload sets it on
16
+ globalThis so the bare reference resolves there. */
17
+ declare const __ABIDE_DEV__: boolean
18
+
11
19
  import { router } from './router.ts'
12
20
  import { clientPage } from './runtime/clientPage.ts'
13
21
  import type { RouteLoader } from './runtime/types/RouteLoader.ts'
14
22
  import { seedResolved } from './seedResolved.ts'
15
23
 
16
24
  /* The server's __SSR__ payload this entry consumes. */
17
- type SsrPayload = { cache?: CacheSnapshotEntry[]; base?: string }
25
+ type SsrPayload = {
26
+ cache?: CacheSnapshotEntry[]
27
+ base?: string
28
+ app?: string
29
+ health?: Record<string, unknown>
30
+ clientTimeout?: number
31
+ }
18
32
 
19
33
  /*
20
34
  The official abide-ui client entry. Reads the server's `window.__SSR__` payload,
21
35
  seeds a tab-scoped cache store from the inline snapshot (so a warm `cache()` read
22
- resolves synchronously and the matching `<template await>` adopts the SSR DOM with
23
- no re-fetch), installs the mount base, and starts the router which imports the
36
+ resolves synchronously and the matching `{#await}` adopts the SSR DOM with
37
+ no re-fetch), installs the mount base, seeds the per-page __SSR__ stamps (app
38
+ name, health, RPC timeout), and starts the router — which imports the
24
39
  current route's chunk, adopts the server-rendered `#app`, then drives SPA
25
40
  navigation — importing each further page's chunk on first visit and probing the
26
41
  destination through the server's app.handle so auth/redirect gating still applies.
@@ -37,19 +52,31 @@ export function startClient(
37
52
  if (target === null) {
38
53
  throw new Error('[abide] startClient: missing #app target')
39
54
  }
40
- /* Dev only: the live-reload script sets `__abideDev` before this module runs,
41
- so the runtime bridge is in place before any component mounts and records
42
- its instances (mountChild) for hot replacement. */
43
- if ((globalThis as { __abideDev?: boolean }).__abideDev) {
44
- installHotBridge()
55
+ /* Dev only: the live-reload script sets `__abideDev` before this module runs, so the
56
+ runtime bridge is in place before any component mounts and records its instances
57
+ (mountChild) for hot replacement. Lazy-imported and `__ABIDE_DEV__`-gated so the
58
+ bridge (and the DOM runtime it drags in) is fully dead-code-eliminated in production
59
+ rather than shipped behind a runtime flag the minifier can't prove false. */
60
+ if (__ABIDE_DEV__ && (globalThis as { __abideDev?: boolean }).__abideDev) {
61
+ import('./installHotBridge.ts').then((module) => module.installHotBridge())
45
62
  }
46
- /* Inspector only: the server injects `__abideInspect` when ABIDE_ENABLE_INSPECTOR
47
- is on, so the scope/router bridge arms before the router builds any scope. */
63
+ /* Inspector only: the server injects `__abideInspect` when ABIDE_ENABLE_INSPECTOR is on,
64
+ so the scope/router bridge arms before the router builds any scope. Inspector can be
65
+ enabled in production, so this stays a lazy chunk (not `__ABIDE_DEV__`-gated) — emitted
66
+ but fetched only when the flag is set, never weighing down the default client load. */
48
67
  if ((globalThis as { __abideInspect?: boolean }).__abideInspect) {
49
- installInspectorBridge()
68
+ import('./installInspectorBridge.ts').then((module) => module.installInspectorBridge())
50
69
  }
51
70
  const ssr = (globalThis as { __SSR__?: SsrPayload }).__SSR__ ?? {}
52
71
  setBaseResolver(() => ssr.base ?? '')
72
+ /* Seed the per-page __SSR__ stamps into their shared slots before mount: the app
73
+ name (default log channel), the health payload (so health()'s first probe is warm),
74
+ and the env-configured RPC timeout (ABIDE_CLIENT_TIMEOUT, shipped per request).
75
+ Without this the browser falls back to channel 'app', a cold first health probe,
76
+ and unbounded RPC fetches. */
77
+ setAppName(ssr.app)
78
+ healthSeedSlot.payload = ssr.health
79
+ rpcTimeoutSlot.ms = ssr.clientTimeout
53
80
  /* The `page` proxy reads route/params/url off the router-updated snapshot. */
54
81
  setPageResolver(() => clientPage.value)
55
82
 
@@ -61,7 +88,7 @@ export function startClient(
61
88
  (inline — reads settled at render-return, in __SSR__) and `__abideResumeCache` (pending
62
89
  {#await} reads whose `__abideResolve(...)` chunks the stream pushed during parse, before
63
90
  this deferred bundle ran). A warm entry lets a `cache()` read resolve synchronously so
64
- `<template await>` adopts without a refetch; a miss marker re-fetches live. */
91
+ `{#await}` adopts without a refetch; a miss marker re-fetches live. */
65
92
  const streamed =
66
93
  (globalThis as { __abideResumeCache?: StreamedResolution[] }).__abideResumeCache ?? []
67
94
  for (const resolution of [...(ssr.cache ?? []), ...streamed]) {
@@ -7,9 +7,9 @@ import { writeNode } from './runtime/writeNode.ts'
7
7
  A writable reactive cell — abide's from-scratch reactive primitive, with no
8
8
  compiler sigil and no external reactivity-library import. `.value` is a
9
9
  plain getter/setter over a signal node, so a read/write shows up as exactly that
10
- in a stack trace. The compiler's job (later) is only to auto-deref `{cell}` in
11
- templates and tag this declaration as a serializable manifest slot; the runtime
12
- needs no magic.
10
+ in a stack trace. The compiler enforces scope attachment (bare `state()` is a compile
11
+ error use `scope().state()`), desugars plain `state(initial)` to a serializable `model`
12
+ doc slot, and keeps `state(initial, transform)` as a `.value` cell; the runtime needs no magic.
13
13
 
14
14
  `transform` is an optional coercion gate on the write path: every `.value =`
15
15
  runs it and stores what it returns, with `previous` for clamp-relative writes or
@@ -17,9 +17,9 @@ methods so a scope is a passable value: `<Child parentScope={scope} />`.
17
17
  Capabilities route where the scope's changes go: `record()` to an undo journal,
18
18
  `persist()` to durable storage, `broadcast()` to peers — declared once, then
19
19
  `undo`/`redo` act on a recorded scope. `id` is the scope's identity for the
20
- boundary-crossing capabilities — `persist()` defaults its key to it. `scope()` is
21
- the ONLY public entry; everything else is a method reached through it (the
22
- `history`/`persist`/`sync` helpers it composes are internal).
20
+ boundary-crossing capabilities — `persist()` defaults its key to it. `scope()` is the only user-facing public entry; `effect` is also exported
21
+ but only for generated binding code and the SSR strip (tagged plumbing). The
22
+ `history`/`persist`/`sync` helpers it composes are internal.
23
23
  */
24
24
  export type Scope = {
25
25
  readonly id: string
@@ -1,12 +1,14 @@
1
1
  import type { BunPlugin } from 'bun'
2
2
  import { abideResolverPlugin } from './abideResolverPlugin.ts'
3
3
  import { abideUiPlugin } from './lib/ui/compile/abideUiPlugin.ts'
4
+ import { zodCjsPlugin } from './zodCjsPlugin.ts'
4
5
 
5
6
  /*
6
- The server-target Bun.build plugin pair shared by compile / buildCli / bundleApp:
7
- the abide-ui `.abide` loader (so SSR `render()` resolves) plus abide's virtual-
8
- module resolver. `embedAssets` flips on the gzip asset embed used by the
9
- standalone server binary; the CLI + launcher builds leave it off.
7
+ The server-target Bun.build plugin set shared by compile / buildCli / bundleApp:
8
+ the abide-ui `.abide` loader (so SSR `render()` resolves), abide's virtual-module
9
+ resolver, and the zod→CommonJS shim that works around bun's broken ESM-cycle
10
+ bundling (see zodCjsPlugin). `embedAssets` flips on the gzip asset embed used by
11
+ the standalone server binary; the CLI + launcher builds leave it off.
10
12
  */
11
13
  export function serverBuildPlugins({
12
14
  cwd,
@@ -15,5 +17,9 @@ export function serverBuildPlugins({
15
17
  cwd: string
16
18
  embedAssets?: boolean
17
19
  }): BunPlugin[] {
18
- return [abideUiPlugin, abideResolverPlugin({ cwd, embedAssets, target: 'server' })]
20
+ return [
21
+ zodCjsPlugin(cwd),
22
+ abideUiPlugin,
23
+ abideResolverPlugin({ cwd, embedAssets, target: 'server' }),
24
+ ]
19
25
  }
@@ -0,0 +1,51 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { dirname } from 'node:path'
3
+ import type { BunPlugin } from 'bun'
4
+
5
+ /*
6
+ Routes every `zod` import in a server bundle to zod's CommonJS build.
7
+
8
+ zod v4's ESM is reached through two subpaths (`zod` and `zod/v4/core`) whose
9
+ modules form an initialization cycle. Bun's bundler emits a broken init order
10
+ for that cycle (oven-sh/bun#31586, closed as not-planned), so a bundled binary
11
+ crashes (`<helper> is not defined`) or — for a lazily imported module — silently
12
+ fails to evaluate, which drops every rpc whose module imports zod from the
13
+ registry. It only bites bundled output; `abide dev` runs unbundled and is fine.
14
+
15
+ zod ships a parallel CommonJS build (`index.cjs`, `v4/core/index.cjs`, …), and
16
+ CommonJS resolves circular requires through a mutating `module.exports` with no
17
+ temporal-dead-zone, so the cycle initializes correctly. Resolving each zod
18
+ specifier to its `.cjs` sibling makes bun bundle zod as CommonJS and sidesteps
19
+ the bug while keeping zod v4. Server-only: the client bundle strips zod from rpc
20
+ modules (they become remote proxies), and dev never bundles.
21
+ */
22
+ export function zodCjsPlugin(cwd: string): BunPlugin {
23
+ /*
24
+ The (specifier, importer-dir) → cjs path mapping is deterministic per build —
25
+ zod's location doesn't move mid-build, and the filter fires for every zod
26
+ import in every module. Memoise on a `${path}\0${from}` key (mirrors the
27
+ resolver's resolveExtension cache) so the fs round-trips (resolveSync +
28
+ existsSync) run once per unique pair instead of once per import.
29
+ */
30
+ const cjsPathCache = new Map<string, string>()
31
+ return {
32
+ name: 'abide-zod-cjs',
33
+ setup(build) {
34
+ build.onResolve({ filter: /^zod($|\/)/ }, (args) => {
35
+ /* Resolve from the importer (or cwd for the entry) to zod's ESM
36
+ target, then swap to its `.cjs` sibling when one exists. */
37
+ const from = args.importer ? dirname(args.importer) : cwd
38
+ const key = `${args.path}\0${from}`
39
+ const cached = cjsPathCache.get(key)
40
+ if (cached !== undefined) {
41
+ return { path: cached }
42
+ }
43
+ const resolved = Bun.resolveSync(args.path, from)
44
+ const cjs = resolved.replace(/\.js$/, '.cjs')
45
+ const path = existsSync(cjs) ? cjs : resolved
46
+ cjsPathCache.set(key, path)
47
+ return { path }
48
+ })
49
+ },
50
+ }
51
+ }
@@ -1,4 +1,10 @@
1
1
  {
2
2
  "auto_install_extensions": { "abide": true },
3
- "semantic_tokens": "combined"
3
+ "semantic_tokens": "combined",
4
+ "global_lsp_settings": {
5
+ "semantic_token_rules": [
6
+ { "token_type": "tag", "style": ["tag"] },
7
+ { "token_type": "attribute", "style": ["attribute"] }
8
+ ]
9
+ }
4
10
  }
@@ -14,5 +14,8 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@abide/abide": "^0.27.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/bun": "^1.3.14"
17
20
  }
18
21
  }
@@ -1,8 +1,8 @@
1
1
  <script>
2
2
  /*
3
3
  Root layout. A layout.abide wraps every page at or below its folder — here
4
- src/ui/pages/, so it wraps the whole app. The page lands in the <slot/> outlet;
5
- the resolver finds it by filename (no per-page import), and the client router
4
+ src/ui/pages/, so it wraps the whole app. The page renders where this layout
5
+ calls {children()}; the resolver finds it by filename (no per-page import), and the client router
6
6
  keeps it mounted across navigation, so this chrome never re-mounts. A nested
7
7
  layout.abide (in a subfolder) wraps inside this one. Runs on the server during
8
8
  SSR and on the client after hydration.
@@ -16,6 +16,4 @@ import '../app.css'
16
16
  <a href="/about">About</a>
17
17
  </nav>
18
18
  </header>
19
- <main>
20
- <slot></slot>
21
- </main>
19
+ <main>{children()}</main>
@@ -1,16 +0,0 @@
1
- /*
2
- One DEBUG pattern against one channel name, npm-debug conventions:
3
- `*` matches everything, `abide:*` matches 'abide' and every 'abide:…'
4
- sub-channel, anything else matches exactly. Negation (`-` prefix) is the
5
- caller's concern — patterns arrive here already stripped.
6
- */
7
- export function matchesDebugPattern(name: string, pattern: string): boolean {
8
- if (pattern === '*') {
9
- return true
10
- }
11
- if (pattern.endsWith(':*')) {
12
- const prefix = pattern.slice(0, -2)
13
- return name === prefix || name.startsWith(`${prefix}:`)
14
- }
15
- return pattern === name
16
- }
@@ -1,21 +0,0 @@
1
- /*
2
- DEBUG's comma list, parsed once per distinct string — the gate runs on every
3
- log emission, and the env never changes server-side. A single-slot memo
4
- suffices: the browser's abide-debug localStorage toggle produces a new string
5
- that misses the memo and reparses.
6
- */
7
- let lastEnv: string | undefined
8
- let lastPatterns: string[] = []
9
-
10
- export function parseDebugPatterns(env: string | undefined): string[] {
11
- if (env !== lastEnv) {
12
- lastEnv = env
13
- lastPatterns = env
14
- ? env
15
- .split(',')
16
- .map((raw) => raw.trim())
17
- .filter((pattern) => pattern !== '')
18
- : []
19
- }
20
- return lastPatterns
21
- }