@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # abide
2
2
 
3
+ ## 0.44.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 767b415: 02-zod-plugin-memo ([`0e00b21`](https://github.com/briancray/abide/commit/0e00b217f99352a7d3def4841172c5bff287bc5c))
8
+ - 767b415: 06-wakeSubtree-prefix-index ([`23ab920`](https://github.com/briancray/abide/commit/23ab9202fd0cb41e01811e78b76eac2843c9cf8f))
9
+ - 767b415: 09-when-via-switch ([`48f8d50`](https://github.com/briancray/abide/commit/48f8d5063cf74060004ad38691f584abcff0cab2))
10
+ - 767b415: 08-dts-codegen ([`5170542`](https://github.com/briancray/abide/commit/51705426e55a700140cc7bc38f7e21f30759fce3))
11
+ - 767b415: 03-socket-publish-gate ([`56fae49`](https://github.com/briancray/abide/commit/56fae49bf393fdfb3aa35cecc618a39443354ce4))
12
+ - 767b415: 10-cli-tokenizer ([`5e305c1`](https://github.com/briancray/abide/commit/5e305c154efd5f77d0a57e78831b72a30b4d9155))
13
+ - 767b415: 14-resolver-slot-factory-JUDGMENT ([`6e00809`](https://github.com/briancray/abide/commit/6e00809c343e24599d54b2304a50a9cecd90619f))
14
+ - 767b415: catch top-level await in a .abide <script> during abide check ([`76ff64e`](https://github.com/briancray/abide/commit/76ff64e9fb9ffed108ae2fde53f2ded2bf3a0abe))
15
+ - 767b415: 12-warm-clone-PARTIAL ([`8e58e0b`](https://github.com/briancray/abide/commit/8e58e0b2c1891174cc809797f949f01619529b89))
16
+ - 767b415: 05-compiler-double-pass ([`9216dae`](https://github.com/briancray/abide/commit/9216dae4c26e27fce49f39b116bd8b1dcf1dd226))
17
+ - 767b415: 04-lsp-shadow-cache ([`9f9f5fc`](https://github.com/briancray/abide/commit/9f9f5fc84792204259d591fa4aff261b2a19c46d))
18
+ - 767b415: 13-dev-rebuild-scoping-JUDGMENT ([`b5a1d49`](https://github.com/briancray/abide/commit/b5a1d49e6607a8fcf37efe03e9fe6f6b2c31ea45))
19
+ - 767b415: gate skeleton fragment-staging on a connected parent ([`c9380ee`](https://github.com/briancray/abide/commit/c9380eebdd9f6773ccffd446665b1acbc78b1123))
20
+ - 767b415: 07-debug-gate ([`c96466e`](https://github.com/briancray/abide/commit/c96466e73ea1f521d728b11aac44262530c3b09c))
21
+ - 767b415: rename paint's escape var to avoid shadowing the global ([`caf1322`](https://github.com/briancray/abide/commit/caf1322adc6598bc6ea32e0ba9047ba768b6f5e1))
22
+ - 767b415: reflow generateBuild attr-kind union to biome's multiline form ([`f9b3669`](https://github.com/briancray/abide/commit/f9b36690e4add2c955abb8f0127a9af9d513be53))
23
+ - 767b415: 01-bugs-B1-B2 ([`fa8659d`](https://github.com/briancray/abide/commit/fa8659d276265c0fbb4f7ca0740fc74702f00168))
24
+ - 767b415: address code-review findings ([`fb02df5`](https://github.com/briancray/abide/commit/fb02df58c924d7206fce35874cc80309949c9e59))
25
+ - 767b415: 11-server-log-micro ([`fb689bc`](https://github.com/briancray/abide/commit/fb689bc05bee9ba127ddf206e925e3a9e04841fd))
26
+
27
+ ## 0.44.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 24444a8: add zod→CommonJS shim plugin for bun ESM-cycle bug ([`0424b20`](https://github.com/briancray/abide/commit/0424b20151f4d1a98d7b022bbacbbb974d469b47))
32
+ - 24444a8: {children()} replaces the <slot> element ([`0bdfbe5`](https://github.com/briancray/abide/commit/0bdfbe5c3efd621492cdef09b745f9262c5be937))
33
+ - 24444a8: type page params via props(); declare children in the shadow ([`2e58768`](https://github.com/briancray/abide/commit/2e58768cfbfb5a2be2aec48c77482e7bf5ee1b8d))
34
+ - 24444a8: syntactic markup token classifier and zed grammar tokens ([`5cdef1f`](https://github.com/briancray/abide/commit/5cdef1f81762d9c1b8c63a418499335c6524b0a5))
35
+ - 24444a8: {#snippet} block replaces <template name> declarations ([`620c2c3`](https://github.com/briancray/abide/commit/620c2c3ba7d8b797ccb770664db47468e4265131))
36
+ - 24444a8: wire {#if children} lowering; migrate snippet/slot test fixtures ([`861b30b`](https://github.com/briancray/abide/commit/861b30be19deb83b5749b8bb2ca8dd526b29b1b3))
37
+ - 24444a8: page/layout params as reactive props() thunks; layout {#if children} ([`991cea7`](https://github.com/briancray/abide/commit/991cea79a08b09508285769a35494b03111b5113))
38
+ - 24444a8: attribute and prop value interpolation ([`ae0a3da`](https://github.com/briancray/abide/commit/ae0a3daa13411d6de71917efde4cb76c57c855c7))
39
+ - 24444a8: {#if children} lowers children to $props.$children ([`f749f47`](https://github.com/briancray/abide/commit/f749f47836cdf69ccbc51d7b7fb9fafa7d7189a4))
40
+
41
+ ### Patch Changes
42
+
43
+ - 24444a8: effect queue Set -> double-buffered array ([`03c6d09`](https://github.com/briancray/abide/commit/03c6d09334559a2a24c3c953cbf57e5f231ea9c6))
44
+ - 24444a8: prune each reconcile by generation stamp, not a per-pass Set ([`1b48eed`](https://github.com/briancray/abide/commit/1b48eedc9b1657946321cad8c9843930b0bec5d0))
45
+ - 24444a8: accumulated working-tree changes ([`1df2999`](https://github.com/briancray/abide/commit/1df2999b3f4b381334d365b1ff2df53711ad6b50))
46
+ - 24444a8: Breaking: reserve $$ injected namespace, relocate html to abide/ui/html ([`1ef8c2a`](https://github.com/briancray/abide/commit/1ef8c2a047cfaf086df1676d498cdcd2bcbe45d0))
47
+ - 24444a8: shadow block value-params on both back-ends ([`25f719a`](https://github.com/briancray/abide/commit/25f719a17c69aee71e2bc7884080231c35ef6578))
48
+ - 24444a8: shared element-emission plan across build/SSR backends ([`2a6cec5`](https://github.com/briancray/abide/commit/2a6cec5361051c1da021dba50ce40a656106e5a7))
49
+ - 24444a8: typed auto-popping shadow scope for compile lowering ([`2fd04a0`](https://github.com/briancray/abide/commit/2fd04a0ed8dc5453461efa0af920d046549a8877))
50
+ - 24444a8: dedup deref-scope push/pop into withScoped ([`3552fff`](https://github.com/briancray/abide/commit/3552fffd2dc2e69708b8268b42609c4e971eb932))
51
+ - 24444a8: lower await {:finally} outside the then/catch value-param shadow ([`4b33813`](https://github.com/briancray/abide/commit/4b33813433fc42455c6b069da1eb118d71b9af1b))
52
+ - 24444a8: iterate mark down single-subscriber chains ([`4ca3fd4`](https://github.com/briancray/abide/commit/4ca3fd419d58667eb074dd033afc2e29b10f56cd))
53
+ - 24444a8: unify warm-seed shippability + decode symmetry ([`5675e37`](https://github.com/briancray/abide/commit/5675e37ec240ab785e3e83eed7b2723f6dcce090))
54
+ - 24444a8: accumulated feat/arch-deepening working-tree changes ([`60cef30`](https://github.com/briancray/abide/commit/60cef30c2405db94ded7e708abddf2c18b4db555))
55
+ - 24444a8: clear typecheck and lint errors in the arch-deepening compile/dom backlog ([`782fb42`](https://github.com/briancray/abide/commit/782fb42769e495addd57904ccd1fb0148f81f9bb))
56
+ - 24444a8: add eachPlan + snippetPlan, route structure through plans (phase 1) ([`8cfffcc`](https://github.com/briancray/abide/commit/8cfffcc958b63b62da51e670f8f8f4c64a3ca5e5))
57
+ - 24444a8: {children()}/{#snippet}/props() route params ([`8f1dc89`](https://github.com/briancray/abide/commit/8f1dc89bfbf052c07082b939e893c9aee65aeea9))
58
+ - 24444a8: single-source bindings via plans + withBindings, exhaustive dispatch (phase 2) ([`9228e30`](https://github.com/briancray/abide/commit/9228e30d4ba16c3ef1bc14b7d243b9628bde1b59))
59
+ - 24444a8: resolve each key to its row once per reconcile ([`ce3e160`](https://github.com/briancray/abide/commit/ce3e160c8837ec5df4cf5cf625c1db50402defde))
60
+ - 24444a8: reorganize kitchen-sink nav and refresh README/scaffold ([`e31ba15`](https://github.com/briancray/abide/commit/e31ba159d9cc1db88cdebb96f40ff8d5c89b7e0f))
61
+ - 24444a8: scope the children slot reader through the signal transformer ([`e4a901d`](https://github.com/briancray/abide/commit/e4a901da9858f3b307363f98c97c3e72c3569640))
62
+ - 24444a8: faster structural descend via no-alloc walkPath + suffix walk ([`ef14dac`](https://github.com/briancray/abide/commit/ef14dace7c4eb870fe9b0487d4111ecfe635831c))
63
+ - 24444a8: elementPlan over skeletonContext, share emission decisions (phase 3) ([`f75703e`](https://github.com/briancray/abide/commit/f75703e0a276c37ce1fbbfde6b4c4b6064d551b8))
64
+ - 24444a8: effect is not ambient — it is a scope primitive ([`f8942ed`](https://github.com/briancray/abide/commit/f8942ed675d6d6493b3d53bd1cc4e991d1d1b7dd))
65
+
3
66
  ## 0.43.1
4
67
 
5
68
  ### Patch 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.1",
3
+ "version": "0.44.1",
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
@@ -247,10 +257,15 @@ export function abideResolverPlugin({
247
257
  return {
248
258
  name: 'abide-resolver',
249
259
  setup(build) {
250
- /* Fresh edge graph each build (dev watch reuses the plugin instance).
260
+ /* Fresh edge graph + resolution cache each build (dev watch reuses the
261
+ plugin instance). The resolution cache memoises (path → resolved file)
262
+ within a build; clearing it per build keeps the memo but stops a stale
263
+ miss — a path resolved before its file existed — surviving into the next
264
+ build, so a freshly-created file resolves instead of staying "not found".
251
265
  onStart is build-time only — absent in the runtime/preload plugin context. */
252
266
  build.onStart?.(() => {
253
267
  importerOf.clear()
268
+ resolveExtensionCache.clear()
254
269
  })
255
270
 
256
271
  /*
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
@@ -6,6 +6,7 @@ import { DEV_READY_MESSAGE } from './lib/server/runtime/DEV_READY_MESSAGE.ts'
6
6
  import { DEV_REBUILD_MESSAGE } from './lib/server/runtime/DEV_REBUILD_MESSAGE.ts'
7
7
  import { findOpenPort } from './lib/server/runtime/findOpenPort.ts'
8
8
  import { abideLog } from './lib/shared/abideLog.ts'
9
+ import { changeAffectsClient } from './lib/shared/changeAffectsClient.ts'
9
10
 
10
11
  /*
11
12
  Dev orchestrator. Replaces `bun --watch` (which only watches the import graph,
@@ -63,6 +64,7 @@ const buildOptions = {
63
64
  compress: false,
64
65
  clean: false,
65
66
  exitOnFailure: false,
67
+ dev: true,
66
68
  } as const
67
69
 
68
70
  // The worker currently meant to be serving; undefined while crashed or replaced.
@@ -173,30 +175,51 @@ async function replaceServer(port: number): Promise<void> {
173
175
 
174
176
  let building = false
175
177
  let queued = false
178
+ // True once any change collapsed into the queued run needs the client rebuilt;
179
+ // a client-affecting change can never be downgraded to a server-only restart.
180
+ let queuedNeedsClient = false
176
181
 
177
182
  /*
178
- Rebuild the client, then (on success) swap in a fresh server child. Serialized:
179
- a change arriving mid-build sets `queued` so exactly one more rebuild runs
180
- after, collapsing any further changes in between. A failed build leaves the
181
- current child untouched the error is logged and the last-good server keeps
182
- serving.
183
+ Apply a change: rebuild the client (unless `skipClientBuild` a server/MCP-only
184
+ change leaves the client bundle byte-identical), then on success swap in a fresh
185
+ server child. The worker restart always runs: SSR renders through Bun's module
186
+ cache, so a new process is the only reliable way to reflect any source edit,
187
+ client-affecting or not.
188
+
189
+ Serialized: a change arriving mid-build sets `queued` so exactly one more run
190
+ follows, collapsing further changes in between; `queuedNeedsClient` records
191
+ whether any of them needs the client, so the collapsed run never skips a client
192
+ rebuild a queued change required. A failed build leaves the current child
193
+ untouched — the error is logged and the last-good server keeps serving.
183
194
  */
184
- async function rebuild(port: number): Promise<void> {
195
+ async function rebuild(port: number, skipClientBuild = false): Promise<void> {
185
196
  if (building) {
186
197
  queued = true
198
+ queuedNeedsClient ||= !skipClientBuild
187
199
  return
188
200
  }
189
201
  building = true
190
202
  try {
191
- const succeeded = await build(buildOptions)
203
+ const succeeded = skipClientBuild ? true : await build(buildOptions)
192
204
  if (succeeded) {
193
205
  await replaceServer(port)
194
206
  }
207
+ /* A server/MCP-only change reuses the existing client bundle. That's correct for
208
+ an rpc/socket handler BODY edit, but changing an rpc's method, its export name,
209
+ or `outbox: true` changes the client proxy stub too — the stale bundle then
210
+ calls with the old shape (e.g. a 405). Flag the skip so that case isn't silent. */
211
+ if (skipClientBuild) {
212
+ abideLog.info(
213
+ 'server-only change — kept the existing client bundle; if you changed an rpc method, export name, or outbox, save a client file (or restart dev) for a full rebuild',
214
+ )
215
+ }
195
216
  } finally {
196
217
  building = false
197
218
  if (queued) {
198
219
  queued = false
199
- void rebuild(port)
220
+ const needsClient = queuedNeedsClient
221
+ queuedNeedsClient = false
222
+ void rebuild(port, !needsClient)
200
223
  }
201
224
  }
202
225
  }
@@ -222,14 +245,26 @@ an agent editing the app's own source — isn't yanked mid-run by a save.
222
245
  const manualRebuild = Bun.env.ABIDE_DEV_NO_WATCH === '1'
223
246
 
224
247
  let debounce: ReturnType<typeof setTimeout> | undefined
248
+ /*
249
+ True only while every change collapsed into the pending debounce is server/MCP-
250
+ only; one client-affecting change in the burst flips it false (latches until the
251
+ debounce fires), so a multi-file save that touches the client never skips the
252
+ client rebuild.
253
+ */
254
+ let pendingSkipClientBuild = true
225
255
  const watcher = manualRebuild
226
256
  ? undefined
227
257
  : watch(SOURCE_DIR, { recursive: true }, (_event, filename) => {
228
258
  if (!filename || isGenerated(filename)) {
229
259
  return
230
260
  }
261
+ pendingSkipClientBuild &&= !changeAffectsClient(filename)
231
262
  clearTimeout(debounce)
232
- debounce = setTimeout(() => void rebuild(port), REBUILD_DEBOUNCE_MS)
263
+ debounce = setTimeout(() => {
264
+ const skipClientBuild = pendingSkipClientBuild
265
+ pendingSkipClientBuild = true
266
+ void rebuild(port, skipClientBuild)
267
+ }, REBUILD_DEBOUNCE_MS)
233
268
  })
234
269
  if (manualRebuild) {
235
270
  abideLog.info(