@abide/abide 0.49.0 → 0.50.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 (325) hide show
  1. package/AGENTS.md +564 -437
  2. package/CHANGELOG.md +183 -0
  3. package/README.md +165 -202
  4. package/package.json +14 -5
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildDisconnected.ts +0 -2
  9. package/src/checkAbide.ts +12 -2
  10. package/src/devEntry.ts +113 -28
  11. package/src/discoveryEntry.ts +3 -2
  12. package/src/lib/bundle/disconnected.abide +69 -73
  13. package/src/lib/bundle/infoPlist.ts +13 -7
  14. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  15. package/src/lib/bundle/waitForServer.ts +6 -1
  16. package/src/lib/mcp/mcpTools.ts +15 -6
  17. package/src/lib/server/error.ts +6 -6
  18. package/src/lib/server/json.ts +17 -3
  19. package/src/lib/server/rpc/defineRpc.ts +58 -24
  20. package/src/lib/server/rpc/parseArgs.ts +98 -13
  21. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  22. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  23. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  24. package/src/lib/server/rpc/validationError.ts +1 -1
  25. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  26. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  27. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  28. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  29. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  30. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  31. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  32. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  33. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  34. package/src/lib/server/runtime/createServer.ts +120 -324
  35. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  36. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  37. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  38. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  39. package/src/lib/server/runtime/pathStore.ts +15 -0
  40. package/src/lib/server/runtime/registryManifests.ts +1 -1
  41. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  42. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  43. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  44. package/src/lib/server/runtime/snapshotEntryFromCache.ts +14 -14
  45. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  46. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  47. package/src/lib/server/runtime/textResponse.ts +23 -0
  48. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  49. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  50. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  51. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  52. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  53. package/src/lib/server/sockets/defineSocket.ts +5 -4
  54. package/src/lib/server/sse.ts +5 -1
  55. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  56. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  57. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  58. package/src/lib/shared/HttpError.ts +1 -5
  59. package/src/lib/shared/MCP_PATH.ts +6 -0
  60. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  61. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  62. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  63. package/src/lib/shared/activeCacheStore.ts +1 -0
  64. package/src/lib/shared/activePage.ts +1 -0
  65. package/src/lib/shared/buildArtifact.ts +4 -1
  66. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  67. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  68. package/src/lib/shared/cache.ts +156 -131
  69. package/src/lib/shared/changeAffectsClient.ts +12 -7
  70. package/src/lib/shared/createCacheStore.ts +31 -8
  71. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  72. package/src/lib/shared/createReachable.ts +47 -78
  73. package/src/lib/shared/createRemoteFunction.ts +52 -30
  74. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  75. package/src/lib/shared/decodeRefJson.ts +4 -4
  76. package/src/lib/shared/decodeResponse.ts +2 -2
  77. package/src/lib/shared/decodeWireBody.ts +35 -0
  78. package/src/lib/shared/detectRpcMethod.ts +8 -1
  79. package/src/lib/shared/done.ts +8 -2
  80. package/src/lib/shared/encodeRefJson.ts +4 -4
  81. package/src/lib/shared/encodeWireBody.ts +18 -0
  82. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  83. package/src/lib/shared/generateDeclarations.ts +72 -0
  84. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  85. package/src/lib/shared/hydrationWindow.ts +53 -0
  86. package/src/lib/shared/isAsyncCell.ts +11 -0
  87. package/src/lib/shared/isAsyncIterable.ts +8 -0
  88. package/src/lib/shared/isSubscribable.ts +3 -3
  89. package/src/lib/shared/isThenable.ts +9 -0
  90. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  91. package/src/lib/shared/lenientDecode.ts +15 -0
  92. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  93. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  94. package/src/lib/shared/matchRoute.ts +4 -14
  95. package/src/lib/shared/peek.ts +14 -0
  96. package/src/lib/shared/pending.ts +9 -6
  97. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  98. package/src/lib/shared/prepareRpcModule.ts +91 -93
  99. package/src/lib/shared/prepareSocketModule.ts +3 -2
  100. package/src/lib/shared/probeRegistries.ts +5 -18
  101. package/src/lib/shared/reachable.ts +7 -10
  102. package/src/lib/shared/refresh.ts +12 -2
  103. package/src/lib/shared/refreshing.ts +8 -2
  104. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  105. package/src/lib/shared/reviveWireField.ts +49 -0
  106. package/src/lib/shared/reviveWireOutput.ts +36 -0
  107. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  108. package/src/lib/shared/scanPages.ts +25 -0
  109. package/src/lib/shared/snapshotShippable.ts +8 -7
  110. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  111. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  112. package/src/lib/shared/subscribableProbes.ts +1 -1
  113. package/src/lib/shared/tailProbeSlot.ts +1 -1
  114. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  115. package/src/lib/shared/types/AsyncState.ts +13 -0
  116. package/src/lib/shared/types/CacheEntry.ts +7 -7
  117. package/src/lib/shared/types/CacheOptions.ts +23 -37
  118. package/src/lib/shared/types/CachePolicy.ts +25 -0
  119. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  120. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  121. package/src/lib/shared/types/HttpMethod.ts +3 -1
  122. package/src/lib/shared/types/InputCoercion.ts +17 -0
  123. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  124. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  125. package/src/lib/shared/types/PagesScan.ts +7 -0
  126. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  127. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  128. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  129. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  130. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  131. package/src/lib/shared/types/ReturnBody.ts +15 -0
  132. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  133. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  134. package/src/lib/shared/types/Socket.ts +4 -4
  135. package/src/lib/shared/types/SsrPayload.ts +5 -1
  136. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  137. package/src/lib/shared/types/StreamedCells.ts +19 -0
  138. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  139. package/src/lib/shared/types/TailHooks.ts +1 -1
  140. package/src/lib/shared/types/WireKind.ts +11 -0
  141. package/src/lib/shared/validationHttpError.ts +33 -0
  142. package/src/lib/shared/warmSeedKey.ts +16 -0
  143. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  144. package/src/lib/shared/writeRpcDts.ts +11 -1
  145. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  146. package/src/lib/test/createTestApp.ts +19 -1
  147. package/src/lib/ui/README.md +1 -1
  148. package/src/lib/ui/activePendingCells.ts +14 -0
  149. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  150. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  151. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -8
  152. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  153. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  154. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  155. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  156. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  157. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  158. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  159. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  160. package/src/lib/ui/compile/catchBinding.ts +7 -5
  161. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  162. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  163. package/src/lib/ui/compile/compileComponent.ts +19 -3
  164. package/src/lib/ui/compile/compileModule.ts +27 -58
  165. package/src/lib/ui/compile/compileSSR.ts +51 -12
  166. package/src/lib/ui/compile/compileShadow.ts +145 -18
  167. package/src/lib/ui/compile/composeProps.ts +12 -2
  168. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  169. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  170. package/src/lib/ui/compile/declaredNames.ts +36 -0
  171. package/src/lib/ui/compile/desugarSignals.ts +418 -36
  172. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  173. package/src/lib/ui/compile/generateBuild.ts +52 -9
  174. package/src/lib/ui/compile/generateSSR.ts +242 -43
  175. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  176. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  177. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  178. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  179. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  180. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  181. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  182. package/src/lib/ui/compile/lowerContext.ts +10 -0
  183. package/src/lib/ui/compile/lowerDocAccess.ts +26 -23
  184. package/src/lib/ui/compile/lowerScript.ts +40 -4
  185. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  186. package/src/lib/ui/compile/parseTemplate.ts +18 -1092
  187. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  188. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  189. package/src/lib/ui/compile/renameSignalRefs.ts +21 -22
  190. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  191. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  192. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  193. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  194. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  195. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  196. package/src/lib/ui/compile/tryPlan.ts +4 -3
  197. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  198. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  199. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  200. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  201. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  202. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  203. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  204. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  205. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  206. package/src/lib/ui/computed.ts +28 -1
  207. package/src/lib/ui/createScope.ts +35 -68
  208. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  209. package/src/lib/ui/dom/appendText.ts +8 -3
  210. package/src/lib/ui/dom/awaitBlock.ts +43 -94
  211. package/src/lib/ui/dom/bindProp.ts +22 -0
  212. package/src/lib/ui/dom/bindableProp.ts +47 -0
  213. package/src/lib/ui/dom/cellPending.ts +24 -0
  214. package/src/lib/ui/dom/disposeRange.ts +2 -1
  215. package/src/lib/ui/dom/each.ts +26 -4
  216. package/src/lib/ui/dom/eachAsync.ts +39 -3
  217. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  218. package/src/lib/ui/dom/fillRange.ts +4 -4
  219. package/src/lib/ui/dom/hydrate.ts +6 -12
  220. package/src/lib/ui/dom/isComment.ts +1 -1
  221. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  222. package/src/lib/ui/dom/mount.ts +1 -2
  223. package/src/lib/ui/dom/mountChild.ts +9 -40
  224. package/src/lib/ui/dom/mountRange.ts +2 -3
  225. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  226. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  227. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  228. package/src/lib/ui/dom/on.ts +2 -2
  229. package/src/lib/ui/dom/readCell.ts +40 -0
  230. package/src/lib/ui/dom/switchBlock.ts +30 -7
  231. package/src/lib/ui/dom/tryBlock.ts +230 -66
  232. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  233. package/src/lib/ui/dom/when.ts +12 -2
  234. package/src/lib/ui/dom/withScope.ts +2 -2
  235. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  236. package/src/lib/ui/flight.ts +56 -0
  237. package/src/lib/ui/html.ts +12 -1
  238. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  239. package/src/lib/ui/linked.ts +48 -2
  240. package/src/lib/ui/remoteProxy.ts +84 -159
  241. package/src/lib/ui/renderChain.ts +49 -13
  242. package/src/lib/ui/renderToStream.ts +22 -18
  243. package/src/lib/ui/resumeSeedScript.ts +1 -1
  244. package/src/lib/ui/router.ts +63 -36
  245. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  246. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  247. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  248. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  249. package/src/lib/ui/runtime/RENDER.ts +10 -7
  250. package/src/lib/ui/runtime/RESUME.ts +4 -4
  251. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  252. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  253. package/src/lib/ui/runtime/applyPatchToTree.ts +1 -1
  254. package/src/lib/ui/runtime/blockId.ts +31 -0
  255. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  256. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  257. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  258. package/src/lib/ui/runtime/createDoc.ts +3 -42
  259. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  260. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  261. package/src/lib/ui/runtime/flushEffects.ts +16 -6
  262. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  263. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  264. package/src/lib/ui/runtime/renderPath.ts +16 -0
  265. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  266. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  267. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  268. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  269. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  270. package/src/lib/ui/runtime/withPath.ts +16 -0
  271. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  272. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  273. package/src/lib/ui/settleAsyncCells.ts +24 -0
  274. package/src/lib/ui/socketProxy.ts +1 -1
  275. package/src/lib/ui/startClient.ts +16 -31
  276. package/src/lib/ui/trackedComputed.ts +68 -0
  277. package/src/lib/ui/types/Scope.ts +8 -24
  278. package/src/lib/ui/watch.ts +3 -3
  279. package/src/serverEntry.ts +14 -0
  280. package/template/src/server/rpc/getHello.ts +15 -13
  281. package/template/test/app.test.ts +1 -1
  282. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  283. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  284. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  285. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  286. package/src/lib/shared/hydratingSlot.ts +0 -12
  287. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  288. package/src/lib/shared/types/Outbox.ts +0 -9
  289. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  290. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  291. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  292. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  293. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  294. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  295. package/src/lib/ui/compile/markupTokens.ts +0 -313
  296. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  297. package/src/lib/ui/dom/applyResolved.ts +0 -87
  298. package/src/lib/ui/dom/mutateDocArray.ts +0 -38
  299. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  300. package/src/lib/ui/dom/text.ts +0 -20
  301. package/src/lib/ui/history.ts +0 -108
  302. package/src/lib/ui/installHotBridge.ts +0 -95
  303. package/src/lib/ui/installInspectorBridge.ts +0 -140
  304. package/src/lib/ui/outbox.ts +0 -35
  305. package/src/lib/ui/persist.ts +0 -115
  306. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  307. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  308. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  309. package/src/lib/ui/runtime/captureModelDoc.ts +0 -43
  310. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  311. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  312. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  313. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  314. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  315. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  316. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  317. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  318. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  319. package/src/lib/ui/seedResolved.ts +0 -28
  320. package/src/lib/ui/sync.ts +0 -48
  321. package/src/lib/ui/types/History.ts +0 -14
  322. package/src/lib/ui/types/PersistHandle.ts +0 -11
  323. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  324. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  325. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -6,7 +6,7 @@ them. The shell must exist before its contents are filled so a back-reference to
6
6
  ancestor resolves to the already-allocated object — which is what reconnects a cycle.
7
7
  Inline primitives need no shell; they decode directly where they sit.
8
8
  */
9
- import { REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
9
+ import { REF_JSON_NUMBER_TOKENS, REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
10
10
  import { setOwnProperty } from './setOwnProperty.ts'
11
11
 
12
12
  export function decodeRefJson(text: string): unknown {
@@ -99,13 +99,13 @@ function fillShell(slot: unknown, shell: unknown, shells: unknown[]): void {
99
99
 
100
100
  // Reverse of encodeRefJson's numberToken.
101
101
  function decodeNumberToken(token: string): number {
102
- if (token === 'NaN') {
102
+ if (token === REF_JSON_NUMBER_TOKENS.NAN) {
103
103
  return Number.NaN
104
104
  }
105
- if (token === 'Infinity') {
105
+ if (token === REF_JSON_NUMBER_TOKENS.INFINITY) {
106
106
  return Number.POSITIVE_INFINITY
107
107
  }
108
- if (token === '-Infinity') {
108
+ if (token === REF_JSON_NUMBER_TOKENS.NEG_INFINITY) {
109
109
  return Number.NEGATIVE_INFINITY
110
110
  }
111
111
  return -0
@@ -18,7 +18,7 @@ propagation), and the success path types as Promise<Return> cleanly.
18
18
  Streaming Content-Types (SSE / JSONL / NDJSON) throw a clear error
19
19
  rather than silently doing the wrong thing: response.text() would hang
20
20
  forever on a never-ending body and response.json() would fail mid-parse.
21
- A streaming rpc's bare call already returns a Subscribable (the type makes
21
+ A streaming rpc's bare call already returns a NamedAsyncIterable (the type makes
22
22
  `await fn(args)` a compile error), so this is a backstop for the paths that
23
23
  still decode a raw Response — `cache()` and the one-shot stream reader. The
24
24
  error points callers at the right tools: `state(fn(args))` for a reactive
@@ -39,7 +39,7 @@ export async function decodeResponse(response: Response): Promise<unknown> {
39
39
  const kind = contentBodyKind(contentType)
40
40
  if (kind === 'streaming') {
41
41
  throw new Error(
42
- `[abide] response at ${response.url} is a stream (${contentType}) — a streaming rpc's bare call already returns a Subscribable: use state(fn(args)) for a reactive view or \`for await (… of fn(args))\` for iteration, not await/cache()`,
42
+ `[abide] response at ${response.url} is a stream (${contentType}) — a streaming rpc's bare call already returns a NamedAsyncIterable: use state(fn(args)) for a reactive view or \`for await (… of fn(args))\` for iteration, not await/cache()`,
43
43
  )
44
44
  }
45
45
  /* json/text go through the shared mapping warmValueFromSnapshot also uses, so a warm
@@ -0,0 +1,35 @@
1
+ /*
2
+ The single decode point for the "is this ref-json?" policy: abide's own client speaks
3
+ ref-json (encodeRefJson — restores the cycles/shared refs/structured types plain JSON
4
+ can't carry); a foreign client (curl, an OpenAPI SDK, a raw ws client) speaks ordinary
5
+ JSON. Both the rpc body face and the socket-frame face funnel through here so the
6
+ codec fork lives in exactly one place instead of being re-derived per face.
7
+
8
+ The discriminator is a genuine per-face input difference, kept thin at the call site:
9
+
10
+ - `isRefJson === true` — the rpc client stamped REF_JSON_HEADER, so decode as ref-json.
11
+ - `isRefJson === false` — no header (a non-abide rpc body), so plain JSON.parse.
12
+ - `isRefJson === undefined` — no header is available at all (a ws frame carries none),
13
+ so sniff: try ref-json, fall back to plain JSON. The ref-json `[rootValue, slots]`
14
+ envelope is ambiguous with a legitimate 2-element array, but a frame is always an
15
+ object, so a foreign plain-JSON frame makes decodeRefJson throw and we fall back.
16
+
17
+ Throws on a malformed payload (JSON.parse's own SyntaxError) — the caller decides
18
+ whether that's a 400 (rpc) or a dropped frame (ws).
19
+ */
20
+ import { decodeRefJson } from './decodeRefJson.ts'
21
+
22
+ export function decodeWireBody(text: string, isRefJson: boolean | undefined): unknown {
23
+ if (isRefJson === true) {
24
+ return decodeRefJson(text)
25
+ }
26
+ if (isRefJson === false) {
27
+ return JSON.parse(text)
28
+ }
29
+ // No discriminator (a ws frame has no header): try ref-json, fall back to plain JSON.
30
+ try {
31
+ return decodeRefJson(text)
32
+ } catch {
33
+ return JSON.parse(text)
34
+ }
35
+ }
@@ -1,3 +1,4 @@
1
+ import { HTTP_METHODS } from './HTTP_METHODS.ts'
1
2
  import type { HttpMethod } from './types/HttpMethod.ts'
2
3
 
3
4
  /*
@@ -9,8 +10,14 @@ undefined when no rpc export matches — the caller skips the file rather
9
10
  than guessing. Used by the rpc.d.ts codegen to type url() against
10
11
  query-carrying rpcs; matching the same convention the bundler rewrites
11
12
  keeps the two from drifting.
13
+
14
+ The FAIL-OPEN fallback for writeRpcDts's method resolution (ADR-0025 D2/D3): when a
15
+ warm server program is present the method is read off the export's helper SYMBOL
16
+ (alias/re-export-aware), and this regex — keyed on the literal helper name — is used
17
+ only when no program built or the query didn't resolve. Byte-identical to the pre-ADR
18
+ path when no program is present.
12
19
  */
13
- const RPC_EXPORT = /export\s+const\s+\w+\s*=\s*(GET|POST|PUT|PATCH|DELETE|HEAD)\s*[<(]/
20
+ const RPC_EXPORT = new RegExp(`export\\s+const\\s+\\w+\\s*=\\s*(${HTTP_METHODS.join('|')})\\s*[<(]`)
14
21
 
15
22
  export function detectRpcMethod(source: string): HttpMethod | undefined {
16
23
  return (source.match(RPC_EXPORT)?.[1] as HttpMethod | undefined) ?? undefined
@@ -1,5 +1,5 @@
1
1
  import { tailProbeSlot } from './tailProbeSlot.ts'
2
- import type { Subscribable } from './types/Subscribable.ts'
2
+ import type { NamedAsyncIterable } from './types/NamedAsyncIterable.ts'
3
3
 
4
4
  /*
5
5
  Reactive terminal-state reader for a stream: true once the source has closed (its
@@ -9,6 +9,12 @@ tail entry status === 'done'). Stream-only — a cache entry's "done" is just
9
9
  registered (server render, or tail never imported) reads as not-done.
10
10
  */
11
11
  // @documentation probes
12
- export function done(subscribable: Subscribable<unknown>): boolean {
12
+ export function done(subscribable: NamedAsyncIterable<unknown>): boolean {
13
+ /* Null-tolerant: a promise/iterable subexpression peek-lifts to `undefined` while
14
+ pending (ADR-0032), so `done(getFeed())` in a template hands us `undefined` on the
15
+ first pass — treat a missing source as not-done rather than throwing. */
16
+ if (subscribable == null) {
17
+ return false
18
+ }
13
19
  return tailProbeSlot.probe?.(subscribable.name)?.done ?? false
14
20
  }
@@ -11,7 +11,7 @@ symbols can't be serialised and encode as undefined, matching JSON.stringify. Ou
11
11
  shape is `[rootValue, slots]`. Decode with decodeRefJson. Not streaming — the whole
12
12
  graph is walked up front.
13
13
  */
14
- import { REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
14
+ import { REF_JSON_NUMBER_TOKENS, REF_JSON_TAGS } from './REF_JSON_TAGS.ts'
15
15
  import { setOwnProperty } from './setOwnProperty.ts'
16
16
 
17
17
  export function encodeRefJson(value: unknown): string {
@@ -99,10 +99,10 @@ export function encodeRefJson(value: unknown): string {
99
99
  // Stable token for the numbers JSON can't carry.
100
100
  function numberToken(value: number): string {
101
101
  if (Object.is(value, -0)) {
102
- return '-0'
102
+ return REF_JSON_NUMBER_TOKENS.NEG_ZERO
103
103
  }
104
104
  if (Number.isNaN(value)) {
105
- return 'NaN'
105
+ return REF_JSON_NUMBER_TOKENS.NAN
106
106
  }
107
- return value > 0 ? 'Infinity' : '-Infinity'
107
+ return value > 0 ? REF_JSON_NUMBER_TOKENS.INFINITY : REF_JSON_NUMBER_TOKENS.NEG_INFINITY
108
108
  }
@@ -0,0 +1,18 @@
1
+ /*
2
+ The single honest-JSON ENCODE point for the REST response faces (`json()` and the
3
+ per-frame `sse()` body). JSON.stringify driven by wireJsonReplacer so a structured
4
+ runtime value crosses as honest JSON — a `Set` → array, a `Map` → `[K,V]` entries, a
5
+ `bigint` → digit string — instead of the silent `{}` a bare Set stringifies to or the
6
+ throw a bare bigint triggers. Value-directed (not type-directed), so every consumer
7
+ reads the same honest JSON the projected schema (jsonSchemaForType) describes: abide's
8
+ own client and a foreign EventSource/curl alike.
9
+
10
+ This is the REST counterpart to the ws path's encodeRefJson: it preserves the
11
+ structured values losslessly the way ref-json does, but without the `[rootValue, slots]`
12
+ envelope a foreign REST consumer (a naive EventSource + JSON.parse) can't read.
13
+ */
14
+ import { wireJsonReplacer } from './wireJsonReplacer.ts'
15
+
16
+ export function encodeWireBody(value: unknown): string {
17
+ return JSON.stringify(value, wireJsonReplacer)
18
+ }
@@ -1,4 +1,4 @@
1
- import type { StandardSchemaV1 } from '../../shared/types/StandardSchemaV1.ts'
1
+ import type { StandardSchemaV1 } from './types/StandardSchemaV1.ts'
2
2
 
3
3
  /*
4
4
  Flattens Standard Schema issues into a top-level-field → first-message map, the
@@ -7,6 +7,10 @@ bare `PropertyKey` or a `{ key }` wrapper (the spec allows both), so normalize
7
7
  before reading. First message wins per field (a form shows one per input); the
8
8
  raw `issues` keep every message and the full path. Issues with no string field
9
9
  (root-level refinements) are omitted — they live only in `issues`.
10
+
11
+ Isomorphic (only the type-only StandardSchemaV1 import): the server 422 path
12
+ (validationError.ts) and the client pre-flight (validationHttpError.ts, ADR-0026)
13
+ both build the identical `fields` map from the same issue list.
10
14
  */
11
15
  export function fieldErrorsFromIssues(
12
16
  issues: readonly StandardSchemaV1.Issue[],
@@ -0,0 +1,72 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { Glob } from 'bun'
3
+ import { abideImportName } from './abideImportName.ts'
4
+ import { abideLog } from './abideLog.ts'
5
+ import { rpcServerForRoot } from './rpcServerForRoot.ts'
6
+ import { scanPages } from './scanPages.ts'
7
+ import { writeHealthDts } from './writeHealthDts.ts'
8
+ import { writePublicAssetsDts } from './writePublicAssetsDts.ts'
9
+ import { writeRoutesDts } from './writeRoutesDts.ts'
10
+ import { writeRpcDts } from './writeRpcDts.ts'
11
+ import { writeTestRpcDts } from './writeTestRpcDts.ts'
12
+ import { writeTestSocketsDts } from './writeTestSocketsDts.ts'
13
+
14
+ /* Globs a project subtree once, returning [] when the dir is absent so an app
15
+ missing a folder still generates the same (mirrors the plugin's scanDir /
16
+ scanPublicOnce). */
17
+ async function scanDir(dir: string, pattern: string, onlyFiles: boolean): Promise<string[]> {
18
+ if (!existsSync(dir)) {
19
+ return []
20
+ }
21
+ return await Array.fromAsync(new Glob(pattern).scan({ cwd: dir, onlyFiles }))
22
+ }
23
+
24
+ /*
25
+ The single producer of the dev-editor `.d.ts` artifacts under src/.abide
26
+ (routes/rpc/testRpc/testSockets/health/publicAssets). Owned by build() and run
27
+ CONCURRENTLY with Bun.build so its one rpc `ts.Program` build (ADR-0025 D2's
28
+ alias-aware method detection) hides under the bundle. The runtime server (dev
29
+ worker / `abide start`) no longer generates any `.d.ts` at boot — that removed
30
+ the per-boot program build. Every `.d.ts` here is a non-critical editor
31
+ convenience (CI typechecks via shadow programs, not these files), so the whole
32
+ body is guarded: a codegen hiccup LOGS and resolves rather than failing the
33
+ build.
34
+ */
35
+ export async function generateDeclarations({ cwd }: { cwd: string }): Promise<void> {
36
+ try {
37
+ const rpcDir = `${cwd}/src/server/rpc`
38
+ const socketsDir = `${cwd}/src/server/sockets`
39
+ const pagesDir = `${cwd}/src/ui/pages`
40
+ const publicDir = `${cwd}/src/ui/public`
41
+ const [importName, pagesScan, rpcFiles, socketFiles, publicFiles] = await Promise.all([
42
+ abideImportName(cwd),
43
+ scanPages(pagesDir),
44
+ scanDir(rpcDir, '**/*.ts', false),
45
+ scanDir(socketsDir, '**/*.ts', false),
46
+ scanDir(publicDir, '**/*', true),
47
+ ])
48
+ /* One program per build (ADR-0025 D2): resolve each rpc's method off its export helper
49
+ SYMBOL so an aliased/re-exported helper resolves where writeRpcDts's regex misses it.
50
+ Fails open to undefined, in which case writeRpcDts falls back to its own regex. */
51
+ const rpcServerProgram = rpcServerForRoot(new Map(), cwd, rpcDir)
52
+ const hasAppModule = existsSync(`${cwd}/src/app.ts`)
53
+ await Promise.all([
54
+ writeRoutesDts({ cwd, pageFiles: pagesScan.pageFiles, importName }),
55
+ writeRpcDts({
56
+ cwd,
57
+ rpcDir,
58
+ rpcFiles,
59
+ importName,
60
+ methodForModule: rpcServerProgram
61
+ ? (modulePath) => rpcServerProgram.methodForModule(modulePath)
62
+ : undefined,
63
+ }),
64
+ writeTestRpcDts({ cwd, rpcFiles, importName }),
65
+ writeTestSocketsDts({ cwd, socketFiles, importName }),
66
+ writeHealthDts({ cwd, hasAppModule, importName }),
67
+ writePublicAssetsDts({ cwd, publicFiles, importName }),
68
+ ])
69
+ } catch (error) {
70
+ abideLog.error(error)
71
+ }
72
+ }
@@ -0,0 +1,25 @@
1
+ import type { CacheEntry } from './types/CacheEntry.ts'
2
+
3
+ /*
4
+ Whether a cache entry carries a wire request that can be SEEDED into the SSR warm
5
+ snapshot — any method, so an inline (bare smart read) call hydrates warm on the
6
+ client instead of re-firing after render, regardless of verb. A value called
7
+ inline during render is being used as a read; the seed carries the SSR-computed
8
+ body so the client reads it warm rather than issuing a second call. Producer
9
+ entries (no request) and stream cells (a `NamedAsyncIterable` cell holds no wire
10
+ request) fail it — neither can round-trip a url/method/body through the snapshot.
11
+
12
+ This gates SEEDING, not re-firing. Re-firing unprompted (invalidate-policy
13
+ refetch, snapshot replay) stays GET-only (REPLAYABLE_METHODS / isReplayableMethod
14
+ in cache.ts) so a genuine write coalesces-only and stays re-submittable — a
15
+ seeded POST is read warm once, never auto-replayed.
16
+
17
+ The request half of snapshot shippability, factored out so the two gates that
18
+ need it cannot drift: `snapshotShippable` composes it with `settled` for the
19
+ store-filter sites, and `snapshotEntryFromCache` calls it alone — its
20
+ streaming-drain caller hands it still-pending entries it then awaits, so it can't
21
+ require `settled` up front.
22
+ */
23
+ export function hasSeedableRequest(entry: CacheEntry): boolean {
24
+ return entry.request !== undefined
25
+ }
@@ -0,0 +1,53 @@
1
+ import { cacheStores } from './cacheStores.ts'
2
+
3
+ /*
4
+ The one owner of the hydration pass lifecycle — the timing window during which the
5
+ client adopts the server-rendered DOM (the router's hydrate branch and `hydrate`).
6
+
7
+ Why it exists: the server materializes no cache value (materializeRetained/
8
+ cacheEntryFromSnapshot are client-only), so server-side peek is uniformly undefined
9
+ and the SSR render always shows the fallback. A snapshot-seeded warm value surfacing
10
+ DURING hydration would diverge from that server text and corrupt the claimed text
11
+ node (assertClaimedText desync), so `peek` withholds it while `active` is true and
12
+ `wake()` re-runs the withheld scopes once the pass ends.
13
+
14
+ `active` is a plain boolean — false on the server (no hydration) and after boot —
15
+ readable by the withhold check. `enter`/`exit` bracket a pass with save/restore
16
+ nesting (a `depth` counter) so a child hydrate can't clear an outer pass early;
17
+ `active` stays true until the OUTERMOST `exit`, which then fires `wake`.
18
+
19
+ `wake` marks every live cache entry's key: a peek scope taps the exact call key's
20
+ lifecycle channel (peek's trackLifecycle), so marking every live entry's key wakes
21
+ the scopes that withheld — cheap, one-time, and harmless for non-peek lifecycle
22
+ readers (they recompute the same value). Marks by key, not the bare store-wide
23
+ channel, because a peek(fn, args) scope taps the per-key prefix channel, which a
24
+ keyless mark misses.
25
+ */
26
+ export const hydrationWindow = {
27
+ active: false,
28
+ depth: 0,
29
+ /* Open a pass. Nested enters raise the depth without re-firing anything. */
30
+ enter(): void {
31
+ hydrationWindow.depth += 1
32
+ hydrationWindow.active = true
33
+ },
34
+ /* Close a pass. Only the OUTERMOST unwind (depth back to zero) clears `active`
35
+ and wakes the scopes the pass withheld — a nested child exit must not. */
36
+ exit(): void {
37
+ hydrationWindow.depth -= 1
38
+ if (hydrationWindow.depth === 0) {
39
+ hydrationWindow.active = false
40
+ hydrationWindow.wake()
41
+ }
42
+ },
43
+ /* Re-run the peeks the pass withheld, now that the retained value is congruent
44
+ to show. Mark every live entry's key across both cache stores. */
45
+ wake(): void {
46
+ const stores = cacheStores()
47
+ for (const store of stores) {
48
+ for (const key of store.entries.keys()) {
49
+ store.markLifecycle(key)
50
+ }
51
+ }
52
+ },
53
+ }
@@ -0,0 +1,11 @@
1
+ import { ASYNC_CELL } from './ASYNC_CELL.ts'
2
+ import type { AsyncComputed } from './types/AsyncComputed.ts'
3
+
4
+ /*
5
+ True for an async cell's facet — the brand the probe family tests to route a cell
6
+ (`AsyncComputed`/`AsyncState`) to its own `peek`/`pending`/`refreshing`/`refresh`
7
+ methods instead of the cache/stream registries.
8
+ */
9
+ export function isAsyncCell(value: unknown): value is AsyncComputed<unknown> {
10
+ return typeof value === 'object' && value !== null && ASYNC_CELL in value
11
+ }
@@ -0,0 +1,8 @@
1
+ /*
2
+ Any async iterable — a stream cell auto-tracks by consuming `Symbol.asyncIterator` alone, so
3
+ it needs no `name` (unlike `isSubscribable`, which the probe registry keys on the name). A
4
+ plain `async function*` generator qualifies here where the named check would reject it.
5
+ */
6
+ export function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
7
+ return value != null && typeof (value as any)[Symbol.asyncIterator] === 'function'
8
+ }
@@ -1,11 +1,11 @@
1
- import type { Subscribable } from './types/Subscribable.ts'
1
+ import type { NamedAsyncIterable } from './types/NamedAsyncIterable.ts'
2
2
 
3
3
  /*
4
- A Subscribable is a named AsyncIterable — distinguishes a stream argument from
4
+ A NamedAsyncIterable is an AsyncIterable carrying a `name` — distinguishes a stream argument from
5
5
  the other probe selector shapes (callables and `{ tags }` objects, neither of
6
6
  which carries Symbol.asyncIterator).
7
7
  */
8
- export function isSubscribable(value: unknown): value is Subscribable<unknown> {
8
+ export function isSubscribable(value: unknown): value is NamedAsyncIterable<unknown> {
9
9
  return (
10
10
  typeof value === 'object' &&
11
11
  value !== null &&
@@ -0,0 +1,9 @@
1
+ /* True for a Promise-like value — anything carrying a callable `then`. Used to tell an
2
+ async cell's unwrap path (a promise to await) from a synchronously-produced value. */
3
+ export function isThenable(value: unknown): value is PromiseLike<unknown> {
4
+ return (
5
+ value !== null &&
6
+ (typeof value === 'object' || typeof value === 'function') &&
7
+ typeof (value as { then?: unknown }).then === 'function'
8
+ )
9
+ }
@@ -0,0 +1,258 @@
1
+ import ts from 'typescript'
2
+
3
+ const STRINGISH_FLAGS =
4
+ ts.TypeFlags.String | ts.TypeFlags.TemplateLiteral | ts.TypeFlags.StringMapping
5
+ const NUMERIC_FLAGS = ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral
6
+ const NULLISH_FLAGS = ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void
7
+ const OPTIONALITY_FLAGS = ts.TypeFlags.Undefined | ts.TypeFlags.Void
8
+
9
+ /*
10
+ Projects a `ts.Type` to a JSON Schema (ADR-0030 D2) — the complement of jsonSchemaForSchema, which
11
+ projects a runtime VALIDATOR. This projects a bare typed handler return so an rpc's OpenAPI 200 / MCP
12
+ outputSchema is generated from the handler's return type when no `schemas.output` validator is
13
+ declared. Covers the subset abide's `json()`/`jsonl()` actually emit (primitives, literals, objects,
14
+ arrays, tuples, unions, Date, records). Returns the same `Record<string, unknown>` shape
15
+ jsonSchemaForSchema does, so the projected schema is drop-in for every surface.
16
+
17
+ Fails OPEN: an unsupported / unresolvable type projects to `{}` (permissive "any JSON") and, at the
18
+ top level, a bare `{}` collapses to undefined so the surface omits the schema — exactly today's
19
+ behavior with no `schemas.output`. Never throws, so a projection gap can't break a build. The `seen`
20
+ set guards a recursive type (a tree node referencing itself) from infinite-looping — a revisit
21
+ emits `{}` to break the cycle.
22
+ */
23
+ export function jsonSchemaForType(
24
+ checker: ts.TypeChecker,
25
+ type: ts.Type,
26
+ ): Record<string, unknown> | undefined {
27
+ const schema = project(checker, type, new Set())
28
+ // A bare permissive object carries no shape hint — omit it so the surface behaves as if unschema'd.
29
+ return Object.keys(schema).length === 0 ? undefined : schema
30
+ }
31
+
32
+ /* The recursive core. Always returns a schema object (never undefined) — `{}` is the permissive
33
+ fail-open value for anything unsupported, so every branch can compose the result inline. */
34
+ function project(
35
+ checker: ts.TypeChecker,
36
+ type: ts.Type,
37
+ seen: Set<ts.Type>,
38
+ ): Record<string, unknown> {
39
+ // any/unknown carry no shape — permissive.
40
+ if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
41
+ return {}
42
+ }
43
+ if (type.isUnion()) {
44
+ return projectUnion(checker, type, seen)
45
+ }
46
+ // An intersection rarely projects cleanly to a single JSON Schema — punt permissive (ADR-0030 scope).
47
+ if (type.isIntersection()) {
48
+ return {}
49
+ }
50
+ // `boolean` is a `true | false` union caught above; a lone literal / intrinsic lands here.
51
+ if ((type.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) !== 0) {
52
+ return { type: 'boolean' }
53
+ }
54
+ if ((type.flags & ts.TypeFlags.StringLiteral) !== 0) {
55
+ return { type: 'string', const: (type as ts.StringLiteralType).value }
56
+ }
57
+ // A template-literal / mapped string projects to a plain string (its pattern isn't modeled).
58
+ if ((type.flags & STRINGISH_FLAGS) !== 0) {
59
+ return { type: 'string' }
60
+ }
61
+ if ((type.flags & NUMERIC_FLAGS) !== 0) {
62
+ return { type: 'number' }
63
+ }
64
+ // JSON Schema has no bigint; matches the ADR-0029 wire representation (a bigint rides as a string).
65
+ if ((type.flags & (ts.TypeFlags.BigInt | ts.TypeFlags.BigIntLiteral)) !== 0) {
66
+ return { type: 'string' }
67
+ }
68
+ if ((type.flags & ts.TypeFlags.Null) !== 0) {
69
+ return { type: 'null' }
70
+ }
71
+ if ((type.flags & OPTIONALITY_FLAGS) !== 0) {
72
+ return {}
73
+ }
74
+ // A function/callable body isn't JSON — punt permissive.
75
+ if (type.getCallSignatures().length > 0) {
76
+ return {}
77
+ }
78
+ const symbolName = type.getSymbol()?.name
79
+ if (symbolName === 'Date') {
80
+ return { type: 'string', format: 'date-time' }
81
+ }
82
+ if (symbolName === 'Array' || symbolName === 'ReadonlyArray') {
83
+ return projectArray(checker, type, seen)
84
+ }
85
+ // A `Set`/`Map` rides the wire as a JSON array (ADR-0029 output encode), so the projected schema
86
+ // MUST agree with those bytes — a `Set<T>` is `T[]`, a `Map<K,V>` is `[K,V][]` entries.
87
+ if (symbolName === 'Set' || symbolName === 'ReadonlySet') {
88
+ return projectSet(checker, type, seen)
89
+ }
90
+ if (symbolName === 'Map' || symbolName === 'ReadonlyMap') {
91
+ return projectMap(checker, type, seen)
92
+ }
93
+ if (isTupleType(type)) {
94
+ return projectTuple(checker, type, seen)
95
+ }
96
+ if ((type.flags & ts.TypeFlags.Object) !== 0) {
97
+ return projectObject(checker, type, seen)
98
+ }
99
+ return {}
100
+ }
101
+
102
+ /*
103
+ A union projects by first stripping the `undefined`/`null`/`void` members that only encode
104
+ optionality (they make a property non-required, not a real branch). What survives: all-boolean →
105
+ `boolean`; all string-literal → `const` (one) or `enum` (many); one survivor → that member's schema;
106
+ otherwise `anyOf` of each. An all-nullish union (a bare `undefined`) has nothing to say → `{}`.
107
+ */
108
+ function projectUnion(
109
+ checker: ts.TypeChecker,
110
+ type: ts.UnionType,
111
+ seen: Set<ts.Type>,
112
+ ): Record<string, unknown> {
113
+ const members = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
114
+ if (members.length === 0) {
115
+ return {}
116
+ }
117
+ if (members.every((member) => (member.flags & ts.TypeFlags.BooleanLiteral) !== 0)) {
118
+ return { type: 'boolean' }
119
+ }
120
+ if (members.every((member) => (member.flags & ts.TypeFlags.StringLiteral) !== 0)) {
121
+ const values = members.map((member) => (member as ts.StringLiteralType).value)
122
+ return values.length === 1 ? { type: 'string', const: values[0] } : { enum: values }
123
+ }
124
+ const [onlyMember] = members
125
+ if (members.length === 1 && onlyMember !== undefined) {
126
+ return project(checker, onlyMember, seen)
127
+ }
128
+ seen.add(type)
129
+ const anyOf = members.map((member) => project(checker, member, seen))
130
+ seen.delete(type)
131
+ return { anyOf }
132
+ }
133
+
134
+ /* `T[]`/`readonly T[]` → `{ type: 'array', items: project(T) }`. */
135
+ function projectArray(
136
+ checker: ts.TypeChecker,
137
+ type: ts.Type,
138
+ seen: Set<ts.Type>,
139
+ ): Record<string, unknown> {
140
+ const element = checker.getTypeArguments(type as ts.TypeReference)[0]
141
+ seen.add(type)
142
+ const items = element === undefined ? {} : project(checker, element, seen)
143
+ seen.delete(type)
144
+ return { type: 'array', items }
145
+ }
146
+
147
+ /* `Set<T>`/`readonly Set<T>` → `{ type: 'array', items: project(T) }` — congruent with the
148
+ `wireJsonReplacer` encode (`[...set]`). */
149
+ function projectSet(
150
+ checker: ts.TypeChecker,
151
+ type: ts.Type,
152
+ seen: Set<ts.Type>,
153
+ ): Record<string, unknown> {
154
+ const element = checker.getTypeArguments(type as ts.TypeReference)[0]
155
+ seen.add(type)
156
+ const items = element === undefined ? {} : project(checker, element, seen)
157
+ seen.delete(type)
158
+ return { type: 'array', items }
159
+ }
160
+
161
+ /* `Map<K,V>`/`readonly Map<K,V>` → an array of `[K,V]` entry tuples — congruent with the
162
+ `wireJsonReplacer` encode (`[...map]`). Each entry is a fixed 2-tuple, so the item schema is
163
+ positional `prefixItems` with `items: false` to forbid extras (matching projectTuple). */
164
+ function projectMap(
165
+ checker: ts.TypeChecker,
166
+ type: ts.Type,
167
+ seen: Set<ts.Type>,
168
+ ): Record<string, unknown> {
169
+ const args = checker.getTypeArguments(type as ts.TypeReference)
170
+ seen.add(type)
171
+ const keySchema = args[0] === undefined ? {} : project(checker, args[0], seen)
172
+ const valueSchema = args[1] === undefined ? {} : project(checker, args[1], seen)
173
+ seen.delete(type)
174
+ return {
175
+ type: 'array',
176
+ items: { type: 'array', prefixItems: [keySchema, valueSchema], items: false },
177
+ }
178
+ }
179
+
180
+ /* A fixed-length tuple → positional `prefixItems`, `items: false` to forbid extras (JSON Schema
181
+ 2020-12 / OpenAPI 3.1 tuple encoding). */
182
+ function projectTuple(
183
+ checker: ts.TypeChecker,
184
+ type: ts.Type,
185
+ seen: Set<ts.Type>,
186
+ ): Record<string, unknown> {
187
+ seen.add(type)
188
+ const prefixItems = checker
189
+ .getTypeArguments(type as ts.TypeReference)
190
+ .map((element) => project(checker, element, seen))
191
+ seen.delete(type)
192
+ return { type: 'array', prefixItems, items: false }
193
+ }
194
+
195
+ /*
196
+ An object type → `{ type: 'object', properties, required }`. A property is required unless it is `?`
197
+ optional or its type bears `undefined`. A string index signature (`Record<string, V>` / `{ [k]: V }`)
198
+ becomes `additionalProperties: project(V)`. The `seen` guard short-circuits a self-referential type
199
+ (a recursive tree node) to `{}` so it can't infinite-loop.
200
+ */
201
+ function projectObject(
202
+ checker: ts.TypeChecker,
203
+ type: ts.Type,
204
+ seen: Set<ts.Type>,
205
+ ): Record<string, unknown> {
206
+ if (seen.has(type)) {
207
+ return {}
208
+ }
209
+ seen.add(type)
210
+ const properties: Record<string, unknown> = {}
211
+ const required: string[] = []
212
+ for (const symbol of checker.getPropertiesOfType(type)) {
213
+ const propertyType = typeOfProperty(checker, symbol)
214
+ const name = symbol.getName()
215
+ properties[name] = project(checker, propertyType, seen)
216
+ const optional =
217
+ (symbol.flags & ts.SymbolFlags.Optional) !== 0 || bearsUndefined(propertyType)
218
+ if (!optional) {
219
+ required.push(name)
220
+ }
221
+ }
222
+ const indexType = checker.getIndexTypeOfType(type, ts.IndexKind.String)
223
+ seen.delete(type)
224
+ const schema: Record<string, unknown> = { type: 'object' }
225
+ if (Object.keys(properties).length > 0) {
226
+ schema.properties = properties
227
+ }
228
+ if (required.length > 0) {
229
+ schema.required = required
230
+ }
231
+ if (indexType !== undefined) {
232
+ schema.additionalProperties = project(checker, indexType, seen)
233
+ }
234
+ return schema
235
+ }
236
+
237
+ /* A property symbol's type — from its declaration when it has one (the common case), else the
238
+ checker's symbol-level type (a synthesized/index property). */
239
+ function typeOfProperty(checker: ts.TypeChecker, symbol: ts.Symbol): ts.Type {
240
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0]
241
+ return declaration === undefined
242
+ ? checker.getTypeOfSymbol(symbol)
243
+ : checker.getTypeOfSymbolAtLocation(symbol, declaration)
244
+ }
245
+
246
+ /* True when a property's type includes `undefined`/`void` — an implicit optional even without `?`. */
247
+ function bearsUndefined(type: ts.Type): boolean {
248
+ if (type.isUnion()) {
249
+ return type.types.some((member) => (member.flags & OPTIONALITY_FLAGS) !== 0)
250
+ }
251
+ return (type.flags & OPTIONALITY_FLAGS) !== 0
252
+ }
253
+
254
+ /* A tuple is a TypeReference whose target (an ObjectType) carries the Tuple object flag. */
255
+ function isTupleType(type: ts.Type): boolean {
256
+ const target = (type as ts.TypeReference).target as ts.ObjectType | undefined
257
+ return target !== undefined && (target.objectFlags & ts.ObjectFlags.Tuple) !== 0
258
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ Percent-decodes a URL-derived value, keeping the raw text on a malformed escape
3
+ (`/%E0%A4%A`, `/%`) rather than throwing. Shared by matchRoute's param capture
4
+ and createServer's internal-route names (socket names) so
5
+ a bad escape in a navigation or request can't crash matching/dispatch — the
6
+ downstream lookup just misses naturally on the raw text. Callers that must
7
+ fail closed on a bad escape (the asset servers 404 instead) don't use this.
8
+ */
9
+ export function lenientDecode(value: string): string {
10
+ try {
11
+ return decodeURIComponent(value)
12
+ } catch {
13
+ return value
14
+ }
15
+ }