@abide/abide 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/AGENTS.md +293 -219
  2. package/CHANGELOG.md +46 -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 +11 -1
  7. package/src/build.ts +9 -0
  8. package/src/buildDisconnected.ts +2 -0
  9. package/src/devEntry.ts +1 -0
  10. package/src/lib/bundle/probeAbideServer.ts +1 -1
  11. package/src/lib/cli/parseArgvForRpc.ts +4 -2
  12. package/src/lib/cli/types/CliManifest.ts +6 -4
  13. package/src/lib/mcp/annotationsForMethod.ts +1 -1
  14. package/src/lib/mcp/toolResultFromResponse.ts +1 -1
  15. package/src/lib/mcp/types/JsonRpcRequest.ts +3 -3
  16. package/src/lib/server/AppModule.ts +1 -1
  17. package/src/lib/server/cli/handleCliDownload.ts +7 -5
  18. package/src/lib/server/cli/installScript.ts +3 -3
  19. package/src/lib/server/prompts/promptRegistry.ts +1 -1
  20. package/src/lib/server/prompts/types/Prompt.ts +2 -1
  21. package/src/lib/server/rpc/dispatchRpcInProcess.ts +2 -2
  22. package/src/lib/server/rpc/types/RpcHelper.ts +1 -1
  23. package/src/lib/server/runtime/createPublicAssetServer.ts +2 -2
  24. package/src/lib/server/runtime/createRouteDispatcher.ts +1 -1
  25. package/src/lib/server/runtime/createServer.ts +21 -0
  26. package/src/lib/server/runtime/createUiPageRenderer.ts +7 -11
  27. package/src/lib/server/runtime/crossOriginForbidden.ts +2 -2
  28. package/src/lib/server/runtime/crossOriginGate.ts +1 -1
  29. package/src/lib/server/runtime/devHotModuleResponse.ts +2 -2
  30. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +1 -1
  31. package/src/lib/server/runtime/logExposedSurfaces.ts +3 -4
  32. package/src/lib/server/runtime/serializeCacheSnapshot.ts +3 -8
  33. package/src/lib/server/runtime/snapshotEntryFromCache.ts +10 -7
  34. package/src/lib/server/runtime/streamFromIterator.ts +2 -2
  35. package/src/lib/server/runtime/types/InspectorRpc.ts +2 -2
  36. package/src/lib/server/socket.ts +1 -1
  37. package/src/lib/server/sockets/createSocketDispatcher.ts +2 -2
  38. package/src/lib/server/sockets/socketRegistry.ts +2 -2
  39. package/src/lib/server/sockets/types/SocketClientFrame.ts +3 -3
  40. package/src/lib/shared/CACHE_WRAPPED.ts +3 -2
  41. package/src/lib/shared/REMOTE_FUNCTION.ts +1 -1
  42. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +3 -2
  43. package/src/lib/shared/bodyValueForKind.ts +35 -0
  44. package/src/lib/shared/cache.ts +1 -1
  45. package/src/lib/shared/cacheEntryFromSnapshot.ts +23 -18
  46. package/src/lib/shared/createPushIterator.ts +1 -1
  47. package/src/lib/shared/decodeResponse.ts +11 -8
  48. package/src/lib/shared/hasReplayableRequest.ts +17 -0
  49. package/src/lib/shared/html.ts +7 -26
  50. package/src/lib/shared/manifestModule.ts +2 -2
  51. package/src/lib/shared/online.ts +1 -1
  52. package/src/lib/shared/outboxProbeSlot.ts +1 -1
  53. package/src/lib/shared/pageUrlForFile.ts +1 -1
  54. package/src/lib/shared/routeParamsShape.ts +20 -0
  55. package/src/lib/shared/snapshotShippable.ts +22 -0
  56. package/src/lib/shared/types/CacheEntry.ts +1 -1
  57. package/src/lib/shared/types/CacheInvalidation.ts +3 -3
  58. package/src/lib/shared/types/ClientFlags.ts +5 -3
  59. package/src/lib/shared/types/LastConnection.ts +1 -1
  60. package/src/lib/shared/url.ts +4 -1
  61. package/src/lib/shared/withBase.ts +1 -2
  62. package/src/lib/shared/writeDts.ts +2 -2
  63. package/src/lib/shared/writeRoutesDts.ts +3 -31
  64. package/src/lib/test/createTestSocketChannel.ts +3 -3
  65. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +1 -1
  66. package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +40 -6
  67. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +1 -1
  68. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +49 -37
  69. package/src/lib/ui/compile/abideUiPlugin.ts +1 -1
  70. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +3 -1
  71. package/src/lib/ui/compile/awaitPlan.ts +14 -2
  72. package/src/lib/ui/compile/catchBinding.ts +8 -0
  73. package/src/lib/ui/compile/classStyleMergePlan.ts +108 -0
  74. package/src/lib/ui/compile/collectAbideDiagnostics.ts +11 -0
  75. package/src/lib/ui/compile/compileComponent.ts +4 -4
  76. package/src/lib/ui/compile/compileModule.ts +20 -6
  77. package/src/lib/ui/compile/compileSSR.ts +12 -10
  78. package/src/lib/ui/compile/compileShadow.ts +107 -16
  79. package/src/lib/ui/compile/composeProps.ts +3 -3
  80. package/src/lib/ui/compile/createShadowLanguageService.ts +30 -2
  81. package/src/lib/ui/compile/createShadowProgram.ts +2 -1
  82. package/src/lib/ui/compile/createShadowScope.ts +42 -0
  83. package/src/lib/ui/compile/decodeHtmlEntities.ts +4 -0
  84. package/src/lib/ui/compile/desugarSignals.ts +12 -7
  85. package/src/lib/ui/compile/eachPlan.ts +61 -0
  86. package/src/lib/ui/compile/elementPlan.ts +55 -0
  87. package/src/lib/ui/compile/encodeSemanticTokens.ts +24 -1
  88. package/src/lib/ui/compile/generateBuild.ts +150 -127
  89. package/src/lib/ui/compile/generateSSR.ts +112 -73
  90. package/src/lib/ui/compile/ifPlan.ts +7 -1
  91. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +25 -0
  92. package/src/lib/ui/compile/isPlainIdentifier.ts +7 -0
  93. package/src/lib/ui/compile/isTextLeaf.ts +8 -7
  94. package/src/lib/ui/compile/lowerContext.ts +36 -46
  95. package/src/lib/ui/compile/lowerDocAccess.ts +2 -2
  96. package/src/lib/ui/compile/lowerScript.ts +1 -1
  97. package/src/lib/ui/compile/markupTokens.ts +313 -0
  98. package/src/lib/ui/compile/pagePropsType.ts +23 -0
  99. package/src/lib/ui/compile/parseTemplate.ts +131 -61
  100. package/src/lib/ui/compile/reactiveBinding.ts +47 -0
  101. package/src/lib/ui/compile/renameSignalRefs.ts +44 -10
  102. package/src/lib/ui/compile/skeletonable.ts +5 -5
  103. package/src/lib/ui/compile/snippetPlan.ts +28 -0
  104. package/src/lib/ui/compile/spreadExcludedNames.ts +4 -4
  105. package/src/lib/ui/compile/structuralBlockTokens.ts +3 -4
  106. package/src/lib/ui/compile/switchPlan.ts +7 -1
  107. package/src/lib/ui/compile/tryPlan.ts +8 -1
  108. package/src/lib/ui/compile/types/AnalyzedComponent.ts +2 -2
  109. package/src/lib/ui/compile/types/Binding.ts +24 -0
  110. package/src/lib/ui/compile/types/CompiledShadow.ts +13 -1
  111. package/src/lib/ui/compile/types/ElementPlan.ts +43 -0
  112. package/src/lib/ui/compile/types/ShadowKind.ts +9 -0
  113. package/src/lib/ui/compile/types/ShadowScope.ts +19 -0
  114. package/src/lib/ui/compile/types/TemplateAttr.ts +7 -0
  115. package/src/lib/ui/compile/types/TemplateNode.ts +8 -8
  116. package/src/lib/ui/compile/withBindings.ts +45 -0
  117. package/src/lib/ui/createScope.ts +2 -2
  118. package/src/lib/ui/dom/MATHML_NAMESPACE.ts +2 -2
  119. package/src/lib/ui/dom/SVG_NAMESPACE.ts +3 -2
  120. package/src/lib/ui/dom/appendText.ts +1 -1
  121. package/src/lib/ui/dom/applyResolved.ts +1 -1
  122. package/src/lib/ui/dom/buildDetachedRange.ts +4 -3
  123. package/src/lib/ui/dom/cloneStatic.ts +3 -2
  124. package/src/lib/ui/dom/each.ts +26 -7
  125. package/src/lib/ui/dom/foreignWrapperTag.ts +2 -2
  126. package/src/lib/ui/dom/mountChild.ts +8 -1
  127. package/src/lib/ui/dom/readCall.ts +6 -4
  128. package/src/lib/ui/dom/scopeLabel.ts +3 -3
  129. package/src/lib/ui/dom/switchBlock.ts +3 -1
  130. package/src/lib/ui/dom/types/EachRow.ts +4 -0
  131. package/src/lib/ui/dom/types/SkeletonHoles.ts +4 -2
  132. package/src/lib/ui/history.ts +1 -2
  133. package/src/lib/ui/html.ts +28 -0
  134. package/src/lib/ui/installHotBridge.ts +0 -2
  135. package/src/lib/ui/renderChain.ts +16 -3
  136. package/src/lib/ui/router.ts +66 -15
  137. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +1 -1
  138. package/src/lib/ui/runtime/CHILD_PRESENT.ts +8 -0
  139. package/src/lib/ui/runtime/OUTLET_MARKER.ts +2 -2
  140. package/src/lib/ui/runtime/RANGE_MARKER.ts +1 -1
  141. package/src/lib/ui/runtime/REACTIVE_CONTEXT.ts +9 -3
  142. package/src/lib/ui/runtime/captureModelDoc.ts +1 -1
  143. package/src/lib/ui/runtime/createDoc.ts +5 -1
  144. package/src/lib/ui/runtime/createEffectNode.ts +5 -5
  145. package/src/lib/ui/runtime/flushEffects.ts +22 -9
  146. package/src/lib/ui/runtime/historyEntries.ts +1 -1
  147. package/src/lib/ui/runtime/track.ts +4 -2
  148. package/src/lib/ui/runtime/trigger.ts +34 -20
  149. package/src/lib/ui/runtime/types/Route.ts +2 -2
  150. package/src/lib/ui/runtime/types/UiComponent.ts +3 -2
  151. package/src/lib/ui/runtime/types/UiProps.ts +9 -12
  152. package/src/lib/ui/runtime/walkPath.ts +15 -3
  153. package/src/lib/ui/startClient.ts +41 -14
  154. package/src/lib/ui/state.ts +3 -3
  155. package/src/lib/ui/types/Scope.ts +3 -3
  156. package/src/serverBuildPlugins.ts +11 -5
  157. package/src/zodCjsPlugin.ts +36 -0
  158. package/template/.zed/settings.json +7 -1
  159. package/template/package.json +3 -0
  160. package/template/src/ui/pages/layout.abide +3 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # abide
2
2
 
3
+ ## 0.44.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 24444a8: add zod→CommonJS shim plugin for bun ESM-cycle bug ([`0424b20`](https://github.com/briancray/abide/commit/0424b20151f4d1a98d7b022bbacbbb974d469b47))
8
+ - 24444a8: {children()} replaces the <slot> element ([`0bdfbe5`](https://github.com/briancray/abide/commit/0bdfbe5c3efd621492cdef09b745f9262c5be937))
9
+ - 24444a8: type page params via props(); declare children in the shadow ([`2e58768`](https://github.com/briancray/abide/commit/2e58768cfbfb5a2be2aec48c77482e7bf5ee1b8d))
10
+ - 24444a8: syntactic markup token classifier and zed grammar tokens ([`5cdef1f`](https://github.com/briancray/abide/commit/5cdef1f81762d9c1b8c63a418499335c6524b0a5))
11
+ - 24444a8: {#snippet} block replaces <template name> declarations ([`620c2c3`](https://github.com/briancray/abide/commit/620c2c3ba7d8b797ccb770664db47468e4265131))
12
+ - 24444a8: wire {#if children} lowering; migrate snippet/slot test fixtures ([`861b30b`](https://github.com/briancray/abide/commit/861b30be19deb83b5749b8bb2ca8dd526b29b1b3))
13
+ - 24444a8: page/layout params as reactive props() thunks; layout {#if children} ([`991cea7`](https://github.com/briancray/abide/commit/991cea79a08b09508285769a35494b03111b5113))
14
+ - 24444a8: attribute and prop value interpolation ([`ae0a3da`](https://github.com/briancray/abide/commit/ae0a3daa13411d6de71917efde4cb76c57c855c7))
15
+ - 24444a8: {#if children} lowers children to $props.$children ([`f749f47`](https://github.com/briancray/abide/commit/f749f47836cdf69ccbc51d7b7fb9fafa7d7189a4))
16
+
17
+ ### Patch Changes
18
+
19
+ - 24444a8: effect queue Set -> double-buffered array ([`03c6d09`](https://github.com/briancray/abide/commit/03c6d09334559a2a24c3c953cbf57e5f231ea9c6))
20
+ - 24444a8: prune each reconcile by generation stamp, not a per-pass Set ([`1b48eed`](https://github.com/briancray/abide/commit/1b48eedc9b1657946321cad8c9843930b0bec5d0))
21
+ - 24444a8: accumulated working-tree changes ([`1df2999`](https://github.com/briancray/abide/commit/1df2999b3f4b381334d365b1ff2df53711ad6b50))
22
+ - 24444a8: Breaking: reserve $$ injected namespace, relocate html to abide/ui/html ([`1ef8c2a`](https://github.com/briancray/abide/commit/1ef8c2a047cfaf086df1676d498cdcd2bcbe45d0))
23
+ - 24444a8: shadow block value-params on both back-ends ([`25f719a`](https://github.com/briancray/abide/commit/25f719a17c69aee71e2bc7884080231c35ef6578))
24
+ - 24444a8: shared element-emission plan across build/SSR backends ([`2a6cec5`](https://github.com/briancray/abide/commit/2a6cec5361051c1da021dba50ce40a656106e5a7))
25
+ - 24444a8: typed auto-popping shadow scope for compile lowering ([`2fd04a0`](https://github.com/briancray/abide/commit/2fd04a0ed8dc5453461efa0af920d046549a8877))
26
+ - 24444a8: dedup deref-scope push/pop into withScoped ([`3552fff`](https://github.com/briancray/abide/commit/3552fffd2dc2e69708b8268b42609c4e971eb932))
27
+ - 24444a8: lower await {:finally} outside the then/catch value-param shadow ([`4b33813`](https://github.com/briancray/abide/commit/4b33813433fc42455c6b069da1eb118d71b9af1b))
28
+ - 24444a8: iterate mark down single-subscriber chains ([`4ca3fd4`](https://github.com/briancray/abide/commit/4ca3fd419d58667eb074dd033afc2e29b10f56cd))
29
+ - 24444a8: unify warm-seed shippability + decode symmetry ([`5675e37`](https://github.com/briancray/abide/commit/5675e37ec240ab785e3e83eed7b2723f6dcce090))
30
+ - 24444a8: accumulated feat/arch-deepening working-tree changes ([`60cef30`](https://github.com/briancray/abide/commit/60cef30c2405db94ded7e708abddf2c18b4db555))
31
+ - 24444a8: clear typecheck and lint errors in the arch-deepening compile/dom backlog ([`782fb42`](https://github.com/briancray/abide/commit/782fb42769e495addd57904ccd1fb0148f81f9bb))
32
+ - 24444a8: add eachPlan + snippetPlan, route structure through plans (phase 1) ([`8cfffcc`](https://github.com/briancray/abide/commit/8cfffcc958b63b62da51e670f8f8f4c64a3ca5e5))
33
+ - 24444a8: {children()}/{#snippet}/props() route params ([`8f1dc89`](https://github.com/briancray/abide/commit/8f1dc89bfbf052c07082b939e893c9aee65aeea9))
34
+ - 24444a8: single-source bindings via plans + withBindings, exhaustive dispatch (phase 2) ([`9228e30`](https://github.com/briancray/abide/commit/9228e30d4ba16c3ef1bc14b7d243b9628bde1b59))
35
+ - 24444a8: resolve each key to its row once per reconcile ([`ce3e160`](https://github.com/briancray/abide/commit/ce3e160c8837ec5df4cf5cf625c1db50402defde))
36
+ - 24444a8: reorganize kitchen-sink nav and refresh README/scaffold ([`e31ba15`](https://github.com/briancray/abide/commit/e31ba159d9cc1db88cdebb96f40ff8d5c89b7e0f))
37
+ - 24444a8: scope the children slot reader through the signal transformer ([`e4a901d`](https://github.com/briancray/abide/commit/e4a901da9858f3b307363f98c97c3e72c3569640))
38
+ - 24444a8: faster structural descend via no-alloc walkPath + suffix walk ([`ef14dac`](https://github.com/briancray/abide/commit/ef14dace7c4eb870fe9b0487d4111ecfe635831c))
39
+ - 24444a8: elementPlan over skeletonContext, share emission decisions (phase 3) ([`f75703e`](https://github.com/briancray/abide/commit/f75703e0a276c37ce1fbbfde6b4c4b6064d551b8))
40
+ - 24444a8: effect is not ambient — it is a scope primitive ([`f8942ed`](https://github.com/briancray/abide/commit/f8942ed675d6d6493b3d53bd1cc4e991d1d1b7dd))
41
+
42
+ ## 0.43.1
43
+
44
+ ### Patch Changes
45
+
46
+ - 943402b: restore reload scroll flash-free via native restoration ([`876b99c`](https://github.com/briancray/abide/commit/876b99cd6e285b6a41afe87ca053cf0631099c03))
47
+ - ed96096: abide check: quote hyphenated component prop keys (`aria-label`, `data-*`) so the props shim parses, and treat `on*` callbacks as ordinary declared props (not DOM passthrough) so a passed required `onsave`/`oncancel` no longer reads as missing while an undeclared handler is still caught
48
+
3
49
  ## 0.43.0
4
50
 
5
51
  ### Minor Changes
package/README.md CHANGED
@@ -1,29 +1,38 @@
1
1
  # abide
2
2
 
3
- **One typed declaration fans out to an HTTP endpoint, a CLI subcommand, an MCP tool, and an OpenAPI operation — and the same callable runs on the server and in the browser.**
3
+ **One typed declaration fans out to HTTP, a CLI, an MCP tool, and an OpenAPI
4
+ operation — and the same callable runs on the server and in the browser.**
4
5
 
5
- abide is a type-safe isomorphic framework on Bun and web standards. You declare an RPC, a socket, or a `.abide` component once; the bundler swaps the runtime per side (in-process on the server, a typed `fetch` proxy in the browser) so the name, the call, and the behaviour are identical wherever you import it. Built for humans *and* the machines that now read and drive software.
6
+ abide is an isomorphic, multimodal HTTP framework for a single Bun runtime: you
7
+ declare a typed RPC once and it becomes a browser fetch, an in-process SSR call,
8
+ an MCP tool, a CLI subcommand, and an OpenAPI operation; the bundler swaps the
9
+ runtime per side so the same name behaves the same on both. Built for humans
10
+ *and* machines.
6
11
 
7
- - One direct dependency (TypeScript). Bun is the only runtime no Node, no separate bundler, no dev server to wire up.
12
+ - One direct dependency (TypeScript). One runtime (Bun 1.3).
13
+ - `.abide` single-file components render on the server and hydrate in place.
8
14
 
9
15
  ## Quick start
10
16
 
11
17
  ```sh
12
- bunx abide scaffold my-app # scaffolds the project, installs it, and starts dev
18
+ bunx abide scaffold my-app # scaffolds, installs, and starts the dev server
13
19
  ```
14
20
 
15
- Or read the runnable tour — every primitive below, live:
21
+ Or clone the kitchen-sink example:
16
22
 
17
23
  ```sh
18
- git clone git@github.com:briancray/abide.git
24
+ git clone https://github.com/briancray/abide
19
25
  cd abide/examples/kitchen-sink
20
26
  bun install
21
- bun dev
27
+ bun run dev
22
28
  ```
23
29
 
24
30
  ## RPCs
25
31
 
26
- An RPC is one export per file under `src/server/rpc/`. The file path is the URL (`getMessages.ts` → `/rpc/getMessages`), and a Standard Schema (zod / valibot / arktype, unadapted) validates the args once and projects the same declaration into every surface.
32
+ An RPC is one export under `src/server/rpc/`. The file path is the URL; the
33
+ export's schema validates the args and projects the MCP tool, the CLI flags, and
34
+ the OpenAPI operation. The schema contract is Standard Schema — pass a zod,
35
+ valibot, or arktype schema directly, unadapted.
27
36
 
28
37
  ```ts
29
38
  // src/server/rpc/getMessages.ts
@@ -31,97 +40,212 @@ import { GET } from '@abide/abide/server/GET'
31
40
  import { json } from '@abide/abide/server/json'
32
41
  import { z } from 'zod'
33
42
 
34
- const inputSchema = z.object({ room: z.string() })
35
-
36
- export const getMessages = GET(({ room }) => json(db.messages(room)), { inputSchema })
43
+ export const getMessages = GET(
44
+ async ({ channel }) => json(await db.recentMessages(channel)),
45
+ { inputSchema: z.object({ channel: z.coerce.string() }) },
46
+ )
37
47
  ```
38
48
 
39
- That single declaration fans out:
49
+ One declaration, every surface:
40
50
 
41
51
  ```text
42
- export const getMessages = GET(fn, { inputSchema })
43
-
44
- ┌────────────┬────────────┬────────────┬────────────┐
45
-
46
- SSR call browser MCP tool CLI cmd OpenAPI
47
- cache(fn)() fetch proxy (read-only) abide cli /openapi.json
52
+ export const getMessages = GET(fn, { inputSchema })
53
+
54
+ ┌───────────────┬─────────────┼─────────────┬───────────────┐
55
+
56
+ SSR call browser fetch MCP tool CLI command OpenAPI op
57
+ cache(fn)() typed proxy (read-only) getMessages … /openapi.json
58
+ (in-process) (swapped fn())
48
59
  ```
49
60
 
50
- The schema is the gate: it unlocks the CLI and — for read-only methods (GET/HEAD) — the MCP tool automatically. A mutating method (POST/PUT/PATCH/DELETE) never auto-exposes to MCP; it opts in with `clients: { mcp: true }`. Consume a call four ways: `cache(getMessages)()` in-process (warm SSR + hydration), the swapped `fetch` proxy in the browser, `getMessages.raw(args)` for the untouched `Response`, and `getMessages.stream(args)` for a frame stream.
61
+ A schema unlocks the CLI for any method and auto-exposes read-only methods
62
+ (`GET`/`HEAD`) as MCP tools; a mutating method (`POST`/`PUT`/`PATCH`/`DELETE`)
63
+ never auto-exposes to MCP — it needs an explicit `clients: { mcp: true }`.
64
+ Consume the RPC four ways: `cache(getMessages)({ channel })` in-process during
65
+ SSR, the swapped `fetch` proxy in the browser, `getMessages.raw(args)` for the
66
+ underlying `Response`, and `getMessages.stream(args)` for a frame iterable.
51
67
 
52
- > Query args travel as strings — use `z.coerce.*` for numbers/booleans on GET/DELETE/HEAD. A per-RPC `timeout` (a 504 on every surface) is distinct from the client-wide `ABIDE_CLIENT_TIMEOUT`.
68
+ > Query args travel as strings — validate with `z.coerce.*`. A per-RPC
69
+ > `timeout` (504 on every surface) is distinct from the client-side
70
+ > `ABIDE_CLIENT_TIMEOUT`.
53
71
 
54
72
  ## Sockets
55
73
 
56
- A socket is one broadcast topic per file under `src/server/sockets/`. A `Socket<T>` is an isomorphic `AsyncIterable<T>` — `for await (const m of chat)` is the live stream — and every socket multiplexes onto one WebSocket at `/__abide/sockets`.
74
+ A socket is one broadcast topic per file under `src/server/sockets/`. A
75
+ `Socket<T>` is an isomorphic `AsyncIterable<T>` — `for await (const m of chat)`
76
+ is the live stream — and every socket multiplexes onto one WebSocket at
77
+ `/__abide/sockets`.
57
78
 
58
79
  ```ts
59
80
  // src/server/sockets/chat.ts
60
81
  import { socket } from '@abide/abide/server/socket'
61
82
  import { z } from 'zod'
62
83
 
63
- const schema = z.object({ from: z.string(), text: z.string(), at: z.number() })
64
-
65
- // tail: retain the last 100 frames for late joiners; ttl: evict frames older than an hour.
66
- export const chat = socket({ schema, tail: 100, ttl: 3_600_000 })
67
- export type ChatMessage = z.infer<typeof schema>
84
+ export const chat = socket({
85
+ schema: z.object({ id: z.string(), author: z.string(), text: z.string() }),
86
+ tail: 50, // retain the last 50 frames for replay
87
+ ttl: 60_000, // evict retained frames older than 60s
88
+ })
68
89
  ```
69
90
 
70
- Each socket also has an HTTP face at `/__abide/sockets/<name>`: `GET` returns the retained tail, `POST` publishes (only when the socket declares `clientPublish`). `chat.publish(m)` is isomorphic — server-side it fans out in-process and to remote subscribers; client-side it sends a validated `pub` frame.
91
+ Each socket also has an HTTP face at `/__abide/sockets/<name>`: `GET` returns the
92
+ retained tail, `POST` publishes a frame (gated by `clientPublish`).
71
93
 
72
94
  ## Components
73
95
 
74
- A component is a `.abide` file: valid HTML with a `<script>`, `{#…}` control-flow blocks, `<template name>` snippets, `{expr}` bindings, and a component-scoped `<style>`. This page imports the RPC and socket above — `cache(getMessages)` seeds the list warm at SSR, `tail(chat)` layers live frames on top after hydration.
96
+ A `.abide` component renders on the server and hydrates in the browser. The page
97
+ below imports the RPC and the socket above and exercises the whole template
98
+ grammar. Reactive state is reached through `scope()` — the documented default is
99
+ to destructure it once at the top of the `<script>` and call the primitives bare.
75
100
 
76
101
  ```html
77
102
  <script>
78
- import { cache } from '@abide/abide/shared/cache'
79
- import { tail } from '@abide/abide/ui/tail'
80
- import { getMessages } from '$server/rpc/getMessages.ts'
81
- import { createMessage } from '$server/rpc/createMessage.ts'
82
- import { chat } from '$server/sockets/chat.ts'
83
- import Avatar from '$ui/Avatar.abide'
84
-
85
- const { room = 'lobby' } = props()
86
- const { state, computed } = scope()
87
-
88
- let draft = state('')
89
- let notify = state(true)
90
- const live = computed(() => tail(chat)) // read-only derived
91
- const ready = computed(() => draft.trim().length > 0)
92
-
93
- async function send(event: SubmitEvent) {
94
- event.preventDefault()
95
- await createMessage({ room, text: draft, notify })
103
+ import { cache } from '@abide/abide/shared/cache'
104
+ import { tail } from '@abide/abide/ui/tail'
105
+ import { html } from '@abide/abide/ui/html'
106
+ import { getMessages } from '$server/rpc/getMessages'
107
+ import { sendMessage } from '$server/rpc/sendMessage'
108
+ import { chat } from '$server/sockets/chat'
109
+ import Card from '$ui/Card.abide'
110
+ import Message from '$ui/Message.abide'
111
+ import Risky from '$ui/Risky.abide'
112
+
113
+ // props() is the ambient prop reader — no import; destructure with defaults + rest
114
+ const { channel = 'general', ...rest } = props()
115
+
116
+ // destructure-once: reach every reactive primitive through scope(), then call bare
117
+ const { state, computed, linked, effect } = scope()
118
+
119
+ let draft = state('')
120
+ let count = state(0)
121
+ let muted = state(false)
122
+ let topic = state('all')
123
+ const doubled = computed(() => count * 2)
124
+ const banner = linked(() => `# ${channel}`)
125
+
126
+ // live reads off the §3 socket: latest frame, plus a window of the last 20
127
+ const latest = tail(chat)
128
+ const recent = tail(chat, { last: 20 })
129
+
130
+ // effect runs client-only (stripped from SSR)
131
+ effect(() => {
132
+ document.title = `${channel} (${count})`
133
+ })
134
+
135
+ // an event handler calling a mutating RPC
136
+ async function send() {
137
+ await sendMessage({ channel, text: draft })
96
138
  draft = ''
97
- }
98
- </script>
139
+ count = count + 1
140
+ }
99
141
 
100
- <template name="row" args={{ msg }}>
101
- <li><Avatar name={msg.from} /> {msg.text}</li>
102
- </template>
142
+ // the derived two-way binding's accessors
143
+ const get = () => count
144
+ const set = (value) => (count = Number(value))
103
145
 
104
- <form onsubmit={send}>
105
- <input bind:value={draft} placeholder="message" />
106
- <label><input type="checkbox" bind:checked={notify} /> notify</label>
107
- <button disabled={!ready()}>Send</button>
108
- </form>
109
-
110
- {#if live()}<p class="muted">latest: {live().text}</p>{/if}
146
+ // an attachment: runs against the element at mount
147
+ const focus = (node) => node.focus()
148
+ </script>
111
149
 
112
- {#await cache(getMessages)({ room })}
113
- <p>Loading…</p>
114
- {:then messages}
150
+ <main {...rest}>
151
+ <h1 class:muted={muted} style:opacity={muted ? 0.5 : 1}>{banner}</h1>
152
+ <p>seen {count} — doubled {doubled}</p>
153
+ <p>{html`<em>latest:</em> ${latest?.text ?? '—'}`}</p>
154
+
155
+ <form onsubmit={send}>
156
+ <input bind:value={draft} attach={focus} placeholder="message" />
157
+ <label><input type="checkbox" bind:checked={muted} /> mute</label>
158
+ <label><input type="radio" bind:group={topic} value="all" /> all</label>
159
+ <label><input type="radio" bind:group={topic} value="mine" /> mine</label>
160
+ <input type="range" min="0" max="50" bind:value={{ get, set }} />
161
+ <button>send</button>
162
+ </form>
163
+
164
+ {#snippet row(message)}
165
+ <li>{html`<strong>${message.author}</strong>`}: {message.text}</li>
166
+ {/snippet}
167
+
168
+ {#if recent.length === 0}
169
+ <p>No messages yet.</p>
170
+ {:else if muted}
171
+ <p>Muted — {recent.length} hidden.</p>
172
+ <script>
173
+ // a nested script scoped to this branch — its own scope(), no imports
174
+ const { computed } = scope()
175
+ const note = computed(() => `${recent.length} while muted`)
176
+ </script>
177
+ <small>{note}</small>
178
+ <style>
179
+ small { color: gray; }
180
+ </style>
181
+ {:else}
115
182
  <ul>
116
- {#for msg of messages by msg.at}{row({ msg })}{/for}
183
+ {#for message, i of recent by message.id}
184
+ <Card title={`#${i}`}>{row(message)}</Card>
185
+ {/for}
117
186
  </ul>
118
- {:catch error}
119
- <p class="error">{error.message}</p>
120
- {/await}
187
+ {/if}
188
+
189
+ <h2>filter</h2>
190
+ {#switch topic}
191
+ {:case 'all'}<p>showing everything</p>
192
+ {:case 'mine'}<p>showing mine</p>
193
+ {:default}<p>unknown filter</p>
194
+ {/switch}
195
+
196
+ <h2>history</h2>
197
+ {#await cache(getMessages)({ channel })}
198
+ <p>loading…</p>
199
+ {:then list}
200
+ <ul>
201
+ {#for entry of list by entry.id}
202
+ <Message {...entry} />
203
+ {/for}
204
+ </ul>
205
+ {:catch error}
206
+ <p>failed: {error.message}</p>
207
+ {:finally}
208
+ <hr />
209
+ {/await}
210
+
211
+ <h2>live</h2>
212
+ <ul>
213
+ {#for await frame of chat}
214
+ <li>{frame.text}</li>
215
+ {/for}
216
+ </ul>
217
+
218
+ {#try}
219
+ <Risky />
220
+ {:catch error}
221
+ <p>widget crashed: {error.message}</p>
222
+ {:finally}
223
+ <span>boundary settled</span>
224
+ {/try}
225
+ </main>
226
+
227
+ <style>
228
+ main { font-family: system-ui; }
229
+ .muted { text-decoration: line-through; }
230
+ </style>
231
+ ```
232
+
233
+ A capitalised tag is a child component; the content nested inside it renders
234
+ where the child calls `{children()}` (the `<slot>` element was removed — there
235
+ are no named slots). `{#if children}…{:else}…{/if}` is the fallback form:
236
+
237
+ ```html
238
+ <script>
239
+ const { title = 'Card' } = props()
240
+ </script>
241
+
242
+ <section class="card">
243
+ <h3>{title}</h3>
244
+ {#if children}{children()}{:else}<p>empty</p>{/if}
245
+ </section>
121
246
 
122
247
  <style>
123
- .muted { color: #64748b; }
124
- .error { color: #b91c1c; }
248
+ .card { border: 1px solid #ddd; }
125
249
  </style>
126
250
  ```
127
251
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
@@ -63,7 +63,6 @@
63
63
  "./shared/refreshing": "./src/lib/shared/refreshing.ts",
64
64
  "./shared/HttpError": "./src/lib/shared/HttpError.ts",
65
65
  "./shared/ValidationErrorData": "./src/lib/shared/types/ValidationErrorData.ts",
66
- "./shared/html": "./src/lib/shared/html.ts",
67
66
  "./shared/snippet": "./src/lib/shared/snippet.ts",
68
67
  "./shared/withJsonSchema": "./src/lib/shared/withJsonSchema.ts",
69
68
  "./shared/health": "./src/lib/shared/health.ts",
@@ -73,6 +72,7 @@
73
72
  "./shared/trace": "./src/lib/shared/trace.ts",
74
73
  "./shared/url": "./src/lib/shared/url.ts",
75
74
  "./shared/createSubscriber": "./src/lib/shared/createSubscriber.ts",
75
+ "./ui/html": "./src/lib/ui/html.ts",
76
76
  "./ui/effect": "./src/lib/ui/effect.ts",
77
77
  "./ui/scope": "./src/lib/ui/scope.ts",
78
78
  "./ui/enterScope": "./src/lib/ui/enterScope.ts",
package/src/abideLsp.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  } from './lib/ui/compile/createShadowLanguageService.ts'
8
8
  import { createShadowLanguageService } from './lib/ui/compile/createShadowLanguageService.ts'
9
9
  import { encodeSemanticTokens } from './lib/ui/compile/encodeSemanticTokens.ts'
10
+ import { markupTokens } from './lib/ui/compile/markupTokens.ts'
10
11
  import { nearestProjectRoot } from './lib/ui/compile/nearestProjectRoot.ts'
11
12
  import { offsetToPosition } from './lib/ui/compile/offsetToPosition.ts'
12
13
  import { structuralBlockTokens } from './lib/ui/compile/structuralBlockTokens.ts'
@@ -24,10 +25,12 @@ nearest tsconfig, so files in a monorepo opened at its root are checked against
24
25
  their own project — matching `abide check` run from that package.
25
26
  */
26
27
  /*
27
- The semantic-tokens `data` array for one component: the structural `{#…}` framing
28
- merged with the shadow's type-aware expression tokens, encoded to the LSP wire
29
- format. Never throws on any internal failure it yields an empty stream so the
30
- editor falls back to tree-sitter highlighting.
28
+ The semantic-tokens `data` array for one component: the HTML markup structure and
29
+ the structural `{#…}` framing merged with the shadow's type-aware expression
30
+ tokens, encoded to the LSP wire format. The markup tokens make the LSP own the
31
+ element/attribute coloring too, so a tree-sitter parse desynced by an `attr={expr}`
32
+ value can't bleed miscoloring below it. Never throws — on any internal failure it
33
+ yields an empty stream so the editor falls back to tree-sitter highlighting.
31
34
  */
32
35
  export function componentSemanticTokens(
33
36
  service: ShadowLanguageService,
@@ -36,6 +39,7 @@ export function componentSemanticTokens(
36
39
  ): number[] {
37
40
  try {
38
41
  const tokens = [
42
+ ...markupTokens(text),
39
43
  ...structuralBlockTokens(text),
40
44
  ...service.semanticClassifications(abidePath),
41
45
  ]
@@ -84,12 +84,22 @@ Bun plugin that wires every virtual import abide produces at build time:
84
84
  - `abide:rpc` — { rpcUrl: () => import(rpc-module) } HTTP-method manifest
85
85
  - `abide:sockets` — { socketName: () => import(socket-module) } socket manifest
86
86
  - `abide:pages` — { pageUrl: () => import(page.abide) } manifest
87
+ - `abide:layouts` — { layoutUrl: () => import(layout.abide) } layout manifest
87
88
  - `abide:prompts` — { promptName: () => import(prompt-module) } manifest
88
- - `abide:app` — { init?, handle?, handleError? } from src/app.ts
89
+ - `abide:app` — { init?, handle?, handleError?, health?, forwardHeaders? } from src/app.ts
90
+ - `abide:config` — re-exports src/server/config.ts for boot-time env validation
89
91
  - `abide:assets` — gzip-compressed chunk bytes embedded for standalone compile
90
92
  - `abide:public-assets` — gzip-embedded src/ui/public files
91
93
  - `abide:mcp-resources` — gzip-embedded src/mcp/resources files
94
+ - `abide:mcp` — generated MCP server (createMcpServer)
92
95
  - `abide:shell` — app.html content (custom or default)
96
+ - `abide:app-info` — { name, version } from package.json for OpenAPI info
97
+ - `abide:cli-manifest` — baked per-rpc manifest for the CLI binary
98
+ - `abide:cli-name` — program name from package.json
99
+ - `abide:cli-chrome` — banner/footer text from src/cli/
100
+ - `abide:bundle-window` — optional bundle window config
101
+ - `abide:bundle-disconnected` — baked connect-screen HTML
102
+ - `abide:bundle-disconnected-component` — connect-screen abide-ui component
93
103
 
94
104
  Also rewrites modules under src/server/rpc and src/server/sockets:
95
105
  - src/server/rpc/<file>.ts: each HTTP-method export is bound to a runtime
package/src/build.ts CHANGED
@@ -40,12 +40,14 @@ export async function build({
40
40
  compress = true,
41
41
  clean = true,
42
42
  exitOnFailure = true,
43
+ dev = false,
43
44
  }: {
44
45
  cwd?: string
45
46
  minify?: boolean
46
47
  compress?: boolean
47
48
  clean?: boolean
48
49
  exitOnFailure?: boolean
50
+ dev?: boolean
49
51
  } = {}): Promise<boolean> {
50
52
  const distDir = `${cwd}/dist`
51
53
  const outDir = `${distDir}/_app`
@@ -84,6 +86,13 @@ export async function build({
84
86
  chunk: '[name]-[hash].[ext]',
85
87
  asset: '[name].[ext]',
86
88
  },
89
+ /* Build-time flag the client reads to fold away dev-only machinery: production
90
+ (`dev: false`) defines it false so the hot-reload subtree — mountChild's
91
+ record path, hotReplace, captureModelDoc — dead-code-eliminates instead of
92
+ shipping behind a runtime flag the minifier can't prove. Dev defines it true
93
+ to keep HMR. Under `bun test` no define applies; the source falls back to true
94
+ so `hotReloadEnabled` alone governs (see startClient/mountChild). */
95
+ define: { __ABIDE_DEV__: dev ? 'true' : 'false' },
87
96
  plugins,
88
97
  })
89
98
 
@@ -51,6 +51,8 @@ export async function buildDisconnected({
51
51
  entrypoints: [ENTRY, CSS_ENTRY],
52
52
  target: 'browser',
53
53
  minify: true,
54
+ // Connect screen ships in production bundles only — fold dev-only machinery out.
55
+ define: { __ABIDE_DEV__: 'false' },
54
56
  plugins,
55
57
  })
56
58
  exitOnBuildFailure(result)
package/src/devEntry.ts CHANGED
@@ -63,6 +63,7 @@ const buildOptions = {
63
63
  compress: false,
64
64
  clean: false,
65
65
  exitOnFailure: false,
66
+ dev: true,
66
67
  } as const
67
68
 
68
69
  // The worker currently meant to be serving; undefined while crashed or replaced.
@@ -10,7 +10,7 @@ Confirms a URL points at a abide server before the launcher navigates the app
10
10
  window there, by fetching its unauthenticated health endpoint. Returns the
11
11
  server's identity on success, or undefined when nothing abide answers — a network
12
12
  error, the wrong port, or a non-abide page (a bare 403/404, a different app, a
13
- captive portal's 200 — hence the `abide: true` body check, never response.ok
13
+ captive portal's 200 — hence the `abide: true` (or version string) body check, never response.ok
14
14
  alone). The endpoint bypasses the app's own middleware, so an auth-guarded abide
15
15
  app still verifies here even though its pages would later redirect to a login.
16
16
  Probes HEALTH_PATH first, falling back to the IDENTITY_PATH alias so a newer
@@ -9,8 +9,10 @@ on the manifest entry (when present) drives flag typing:
9
9
 
10
10
  For complex shapes (nested objects, unions, anyOf) the CLI exposes
11
11
  `--json <stringified-args>` as an escape hatch that supplies the whole
12
- args bag verbatim. Stdin: if a JSON object arrives piped in, it's used
13
- as the full args bag (flags layer on top).
12
+ args bag verbatim. Stdin: if a JSON object arrives piped in with no argv flags, it is used
13
+ as the full args bag. Stdin and flags are mutually exclusive — stdin is
14
+ skipped when argv is non-empty to avoid hanging on a pipe that never
15
+ sends EOF.
14
16
 
15
17
  Unrecognised flags throw — early loud feedback is more useful than
16
18
  silent drops.
@@ -1,9 +1,11 @@
1
1
  import type { CliManifestEntry } from './CliManifestEntry.ts'
2
2
 
3
3
  /*
4
- Map from rpc export-name (e.g. "getReport") to its manifest entry. Built
5
- by the bundler from the same rpcRegistry MCP consumes; entries are
6
- emitted only for rpcs with `clients.cli: true`. The CLI binary and any
7
- programmatic createClient caller read this to dispatch calls.
4
+ Map from command name (URL-derived via commandNameForUrl, e.g. "getReport"
5
+ or "users-list") to its manifest entry. Built by the bundler from
6
+ rpcRegistry and socketRegistry; entries are emitted for rpcs and sockets
7
+ with `clients.cli: true`. Sockets produce `<base>-tail` (and `-publish`
8
+ when `allowClientPublish` is set). The CLI binary and any programmatic
9
+ createClient caller read this to dispatch calls.
8
10
  */
9
11
  export type CliManifest = Record<string, CliManifestEntry>
@@ -2,7 +2,7 @@ import type { HttpMethod } from '../shared/types/HttpMethod.ts'
2
2
 
3
3
  /*
4
4
  Maps an HTTP method to MCP tool annotations so a model can tell a read from
5
- a write before calling. Abide derives these from the rpc the RPC was
5
+ a write before calling. Abide derives these from the HTTP method the RPC was
6
6
  declared with rather than asking the author to repeat the intent:
7
7
  - GET / HEAD → read-only, non-destructive
8
8
  - POST → creates; not idempotent, not (necessarily) destructive
@@ -15,7 +15,7 @@ function asText(value: unknown): string {
15
15
  }
16
16
 
17
17
  /*
18
- Turns an rpc/socket Response into an MCP `tools/call` result. Always
18
+ Turns an RPC Response into an MCP `tools/call` result. Always
19
19
  carries a `text` content block for backward compatibility; adds
20
20
  `structuredContent` (an object, per the MCP spec) so models that
21
21
  understand structured output get the typed value instead of a stringified
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  JSON-RPC 2.0 request frame as MCP delivers it over Streamable HTTP. The
3
- `id` is absent for notifications (which we don't currently receive from
4
- clients but accept silently). `method` is a string like "tools/list" or
5
- "resources/read".
3
+ `id` is absent for notifications; they are dispatched for their side effect
4
+ and acknowledged with a 202 (JSON-RPC forbids replying with an envelope).
5
+ `method` is a string like "tools/list" or "resources/read".
6
6
  */
7
7
  export type JsonRpcRequest = {
8
8
  jsonrpc: '2.0'
@@ -10,7 +10,7 @@ WebSockets are not exposed here — abide's only native WebSocket
10
10
  surface is the sockets hub (see `abide/server/socket`), multiplexed onto a
11
11
  single framework-owned connection per client at `/__abide/sockets`.
12
12
  Inside request scopes, the live Bun.Server is reachable via the
13
- exported `server()` function from `abide/server`; `init` receives it
13
+ exported `server()` function from `abide/server/server`; `init` receives it
14
14
  explicitly because it runs outside a request.
15
15
  */
16
16
  // @documentation plumbing
@@ -92,12 +92,14 @@ async function computeBinary(
92
92
  }
93
93
 
94
94
  /*
95
- Handles GET /__abide/cli/<platform> — streams a gzipped tarball
96
- containing the platform-specific thin binary + a `.env` carrying
97
- ABIDE_APP_URL (and ABIDE_APP_TOKEN if the inbound request was authenticated).
95
+ Handles GET /__abide/cli/<platform> — streams a gzipped tarball containing
96
+ the platform-specific thin binary, the sibling server binary, and a `.env`
97
+ carrying ABIDE_APP_URL (and ABIDE_APP_TOKEN if the inbound request was
98
+ authenticated).
98
99
 
99
- Thin binaries live at `dist/cli-thin/<platform>/<programName>`
100
- (produced by `abide cli` with ABIDE_APP_URL set). Missing platforms produce
100
+ Thin binaries live at `dist/cli-thin/<platform>/<programName>` (produced
101
+ lazily by `buildCli` on first request). The `.env` baking `ABIDE_APP_URL`
102
+ is generated at download time, not build time. Missing platforms produce
101
103
  404 — the install script reports it, doesn't try to fall back.
102
104
  */
103
105
  export async function handleCliDownload(
@@ -7,9 +7,9 @@ platform-specific tarball and extracts it into `$ABIDE_INSTALL_DIR`
7
7
  (default `~/.local/bin`). The tarball already contains the `.env` next
8
8
  to the binary — no separate config write step in the script.
9
9
 
10
- The script is rendered server-side so `<ABIDE_APP_URL>` is the request's own
11
- origin and the embedded curl URL needs no escaping or quoting beyond
12
- basic shell hygiene.
10
+ The script is rendered server-side so the request origin is baked into the
11
+ `URL` variable at generation time and the embedded curl URL needs no escaping
12
+ or quoting beyond basic shell hygiene.
13
13
  */
14
14
  export function installScript(appUrl: string, programName: string): string {
15
15
  return `#!/usr/bin/env sh