@abide/abide 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/AGENTS.md +70 -52
  2. package/CHANGELOG.md +177 -0
  3. package/README.md +12 -7
  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
@@ -0,0 +1,820 @@
1
+ import { resolve } from 'node:path'
2
+ import ts from 'typescript'
3
+ import { HTTP_METHODS } from './HTTP_METHODS.ts'
4
+ import { jsonSchemaForType } from './jsonSchemaForType.ts'
5
+ import { loadProjectTsConfig } from './loadProjectTsConfig.ts'
6
+ import type { ErrorJsonSchemas } from './types/ErrorJsonSchemas.ts'
7
+ import type { HttpMethod } from './types/HttpMethod.ts'
8
+ import type { InputCoercion } from './types/InputCoercion.ts'
9
+ import type { OutputWirePlan } from './types/OutputWirePlan.ts'
10
+ import type { ReturnBody } from './types/ReturnBody.ts'
11
+ import type { WireKind } from './types/WireKind.ts'
12
+
13
+ const RPC_HELPERS = new Set<string>(HTTP_METHODS)
14
+ const NUMBER_FLAGS = ts.TypeFlags.Number | ts.TypeFlags.NumberLiteral
15
+ const BOOLEAN_FLAGS = ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral
16
+ const BIGINT_FLAGS = ts.TypeFlags.BigInt | ts.TypeFlags.BigIntLiteral
17
+ const NULLISH_FLAGS = ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void
18
+ // Only `undefined`/`void` make a property OPTIONAL (a `T | null` is a required-but-nullable field).
19
+ const OPTIONALITY_FLAGS = ts.TypeFlags.Undefined | ts.TypeFlags.Void
20
+
21
+ export type RpcServerProgram = {
22
+ /*
23
+ Whether the rpc module at `modulePath` has a STREAMING handler, decided through the
24
+ type graph: the handler's return type resolves to a `TypedResponse<AsyncIterable<…>>`
25
+ (the branded shape only `jsonl()`/`sse()` produce). Returns true/false when the export
26
+ call site and a call signature are found — INCLUDING when the stream is produced through
27
+ a wrapper function, the case the char-scan `detectStreaming` misses. undefined when the
28
+ module or its export call can't be resolved, so the caller fails open to the scan.
29
+ */
30
+ streamingForModule(modulePath: string): boolean | undefined
31
+ /*
32
+ The rpc's HTTP method, read from the export binding's helper SYMBOL (aliases and
33
+ re-exports followed through the checker) rather than the callee's source text. So an
34
+ `import { GET as read }` / a re-exported helper resolves to `GET`, where the `RPC_EXPORT`
35
+ regex — keyed on the literal helper name — misses it. undefined when the export call or
36
+ its helper symbol can't be resolved, so the caller fails open to `detectRpcMethod`.
37
+ */
38
+ methodForModule(modulePath: string): HttpMethod | undefined
39
+ /*
40
+ The input-args wire codec plan (ADR-0028 scalars, ADR-0029 structured): the endpoint's wire
41
+ `Args` type read off the exported RemoteFunction's call signature, projected to the
42
+ number/boolean/date/bigint/set/map fields parseArgs revives a plain-JSON wire value into. Reads
43
+ `InferInput` (the wire shape the schema validates), so a self-coercing schema whose input is
44
+ loose is correctly left uncoerced. undefined when the export call or its Args type can't be
45
+ resolved, or no field is a recognized WireKind — the caller then stamps no plan, so a GET field
46
+ stays a string exactly as today.
47
+ */
48
+ inputCoercionForModule(modulePath: string): InputCoercion | undefined
49
+ /*
50
+ The output-args wire codec plan (ADR-0029 output path): the endpoint's success RESPONSE body type
51
+ (resolved the same way `returnBodyForModule`/`walkSuccessBodies` read it — unwrap `Promise`, drop
52
+ the `TypedError` branches) projected to the structured `date`/`bigint`/`set`/`map` fields the
53
+ client proxy revives from a decoded response. The response-side sibling of
54
+ `inputCoercionForModule`: it is baked onto the CLIENT `remoteProxy` stub (not the server
55
+ `defineRpc`), so an abide client revives a `Set`/`Map`/`bigint`/`Date` the server encoded to
56
+ honest JSON. Only the structured kinds are listed — `number`/`boolean` already ride as their JSON
57
+ type. undefined when the export call or its success body can't be resolved, or no field is a
58
+ structured WireKind, so the client bakes no plan and a response array stays an array.
59
+ */
60
+ outputWirePlanForModule(modulePath: string): OutputWirePlan | undefined
61
+ /*
62
+ The endpoint's success-BODY type (ADR-0030), read from the handler's return type the same way
63
+ streaming detection reads it: unwrap `Promise`, drop the `TypedError` branches, take each
64
+ success `TypedResponse<Body>` body. `type` is that body rendered as a TS type string; a
65
+ streaming endpoint reports `streaming: true` and the per-FRAME type (the `AsyncIterable`
66
+ element) so a surface describes one streamed item. This is the output-side sibling of
67
+ `inputCoercionForModule` — the handler's return is the single source for the generated surfaces
68
+ (`.d.ts`/OpenAPI 200/MCP outputSchema) instead of a hand-written `schemas.output`. undefined
69
+ when the export call or its handler body type can't be resolved, so a consumer falls open to an
70
+ author-declared `schemas.output`.
71
+ */
72
+ returnBodyForModule(modulePath: string): ReturnBody | undefined
73
+ /*
74
+ The endpoint's success-body projected to JSON Schema (ADR-0030 D2), for the OpenAPI 200 / MCP
75
+ outputSchema / inspector surface when the author declared no `schemas.output` validator. Resolves
76
+ the same success-body `ts.Type`(s) `returnBodyForModule` reads — unwrap `Promise`, drop the
77
+ `TypedError` branches, frame-unwrap a streaming body — then projects each through
78
+ `jsonSchemaForType`, combining multiple success branches under `anyOf`. undefined when no body
79
+ resolves or the projection is bare permissive `{}`, so a surface omits the schema exactly as today
80
+ when no `schemas.output` is declared. The build stamps the resolved schema into the server
81
+ `defineRpc` call (like the ADR-0028 `coerce` plan) so the runtime registry can carry it.
82
+ */
83
+ returnBodySchemaForModule(modulePath: string): Record<string, unknown> | undefined
84
+ /*
85
+ The endpoint's typed-error branches surfaced as a status-keyed JSON-Schema map (ADR-0030) — the
86
+ error-branch sibling of `returnBodySchemaForModule`. Walks the SAME return union but KEEPS the
87
+ `TypedError` branches success projection drops: for each, reads the `error.typed(name, status,
88
+ schema?)` brand's numeric `status` and projects the error's `data` type (its schema's
89
+ `~standard.types.input`) through `jsonSchemaForType`. Errors sharing a status combine under
90
+ `anyOf`; a nullary error (no data) contributes a bare `{}` so the status still surfaces. undefined
91
+ when no handler signature resolves or the handler declares no typed errors, so a consumer omits
92
+ the error responses exactly as today. The build stamps the resolved map into the server
93
+ `defineRpc` call (like `outputJsonSchema`) so the runtime registry can carry it to OpenAPI.
94
+ */
95
+ errorSchemasForModule(modulePath: string): ErrorJsonSchemas | undefined
96
+ /*
97
+ The endpoint's INPUT args projected to JSON Schema (ADR-0030 input side — the input-surface
98
+ sibling of `returnBodySchemaForModule`), for the OpenAPI parameters/request body / MCP
99
+ inputSchema / inspector input surface when the author declared no `schemas.input` VALIDATOR.
100
+ Resolves the SAME wire `Args` bag `inputCoercionForModule` reads — the exported RemoteFunction's
101
+ first call-signature parameter, with the multipart `FormData` member dropped — then projects each
102
+ property through `jsonSchemaForType` into an object schema. File-typed properties are EXCLUDED
103
+ exactly as `filesSchema` keeps File parts out of the `inputSchema` projection (a File has no
104
+ honest JSON-Schema form). This is a SHAPE description only: it is never wired into runtime
105
+ validation, so it can never produce a 422 — an author who wants that still declares
106
+ `schemas.input`, which also OVERRIDES this projection on every surface. undefined when no call
107
+ signature / Args bag resolves or every property is excluded, so the surface behaves as today. The
108
+ build stamps the resolved schema into the server `defineRpc` call so the runtime registry carries
109
+ it.
110
+ */
111
+ inputSchemaForModule(modulePath: string): Record<string, unknown> | undefined
112
+ }
113
+
114
+ /*
115
+ Warms one `ts.Program` over every `.ts` under `rpcDir`, reused across every rpc transform
116
+ in the root (ADR-0025 D1 — the server-side mirror of the UI shadow program). The handler's
117
+ transitive imports resolve on demand through the default compiler host, so the type queries
118
+ see the real `jsonl()`/`sse()` return shape, the aliased helper's origin symbol, and an
119
+ imported const's literal type. Type queries only — never emits, never reports diagnostics; a
120
+ module that fails to type-check still yields its answers. Building is the expensive step (a
121
+ few hundred ms once per root), so the resolver plugin caches it per-root and builds it lazily
122
+ on the first rpc transform.
123
+ */
124
+ export function createRpcServerProgram(cwd: string, rpcDir: string): RpcServerProgram {
125
+ const rpcFiles = [...new Bun.Glob('**/*.ts').scanSync({ cwd: rpcDir, onlyFiles: true })].map(
126
+ (relative) => resolve(rpcDir, relative),
127
+ )
128
+ const { options } = loadProjectTsConfig(cwd)
129
+ const program = ts.createProgram({ rootNames: rpcFiles, options })
130
+ const checker = program.getTypeChecker()
131
+ /* Every query fails open to its char-scan/regex counterpart: an unresolved module or any
132
+ checker throw yields undefined so a type-resolution hiccup never breaks a build (ADR-0025
133
+ D3). */
134
+ const query = <T>(
135
+ modulePath: string,
136
+ read: (call: ResolvedRpcCall) => T | undefined,
137
+ ): T | undefined => {
138
+ try {
139
+ const sourceFile = program.getSourceFile(modulePath)
140
+ if (sourceFile === undefined) {
141
+ return undefined
142
+ }
143
+ const call = resolveRpcCall(checker, sourceFile)
144
+ if (call === undefined) {
145
+ return undefined
146
+ }
147
+ return read(call)
148
+ } catch {
149
+ return undefined
150
+ }
151
+ }
152
+ return {
153
+ streamingForModule(modulePath) {
154
+ return query(modulePath, (call) => handlerReturnsStream(checker, call.node))
155
+ },
156
+ methodForModule(modulePath) {
157
+ return query(modulePath, (call) => call.method)
158
+ },
159
+ inputCoercionForModule(modulePath) {
160
+ return query(modulePath, (call) => inputCoercionPlan(checker, call.node))
161
+ },
162
+ outputWirePlanForModule(modulePath) {
163
+ return query(modulePath, (call) => outputWirePlan(checker, call.node))
164
+ },
165
+ returnBodyForModule(modulePath) {
166
+ return query(modulePath, (call) => handlerReturnBody(checker, call.node))
167
+ },
168
+ returnBodySchemaForModule(modulePath) {
169
+ return query(modulePath, (call) => returnBodyJsonSchema(checker, call.node))
170
+ },
171
+ errorSchemasForModule(modulePath) {
172
+ return query(modulePath, (call) => errorBranchSchemas(checker, call.node))
173
+ },
174
+ inputSchemaForModule(modulePath) {
175
+ return query(modulePath, (call) => inputArgsJsonSchema(checker, call.node))
176
+ },
177
+ }
178
+ }
179
+
180
+ type ResolvedRpcCall = {
181
+ /* The exported `<METHOD>(handler, opts?)` CallExpression. */
182
+ node: ts.CallExpression
183
+ /* The method the callee's helper symbol resolves to (alias/re-export followed). */
184
+ method: HttpMethod
185
+ }
186
+
187
+ /*
188
+ The module's exported rpc call — the initializer of `export const <name> = <helper>(…)` whose
189
+ callee resolves (through the checker, following import aliases and re-exports) to one of the
190
+ rpc helper symbols. Scoped to top-level exported const initializers, so a nested `jsonl(` in a
191
+ handler body can't match and an aliased helper (`import { GET as read }`) still resolves. undefined
192
+ when no such export exists — the caller falls back to the char-scan.
193
+ */
194
+ function resolveRpcCall(
195
+ checker: ts.TypeChecker,
196
+ sourceFile: ts.SourceFile,
197
+ ): ResolvedRpcCall | undefined {
198
+ for (const statement of sourceFile.statements) {
199
+ if (!ts.isVariableStatement(statement)) {
200
+ continue
201
+ }
202
+ const isExported = statement.modifiers?.some(
203
+ (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
204
+ )
205
+ if (!isExported) {
206
+ continue
207
+ }
208
+ for (const declaration of statement.declarationList.declarations) {
209
+ const initializer = declaration.initializer
210
+ if (initializer === undefined || !ts.isCallExpression(initializer)) {
211
+ continue
212
+ }
213
+ const method = methodOfCallee(checker, initializer.expression)
214
+ if (method !== undefined) {
215
+ return { node: initializer, method }
216
+ }
217
+ }
218
+ }
219
+ return undefined
220
+ }
221
+
222
+ /*
223
+ The HTTP method a call's callee names — the origin helper symbol's name, following import
224
+ aliases and re-exports through the checker (so `read` from `import { GET as read }` yields
225
+ `GET`). Falls back to the callee's own identifier text when the symbol doesn't resolve, so a
226
+ bare `GET(` still reads as `GET` even if type resolution hiccups. undefined when neither is an
227
+ rpc helper.
228
+ */
229
+ function methodOfCallee(checker: ts.TypeChecker, callee: ts.Expression): HttpMethod | undefined {
230
+ if (!ts.isIdentifier(callee)) {
231
+ return undefined
232
+ }
233
+ let symbol = checker.getSymbolAtLocation(callee)
234
+ if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
235
+ symbol = checker.getAliasedSymbol(symbol)
236
+ }
237
+ const resolvedName = symbol?.getName()
238
+ if (resolvedName !== undefined && RPC_HELPERS.has(resolvedName)) {
239
+ return resolvedName as HttpMethod
240
+ }
241
+ if (RPC_HELPERS.has(callee.text)) {
242
+ return callee.text as HttpMethod
243
+ }
244
+ return undefined
245
+ }
246
+
247
+ /*
248
+ The rpc export's handler return type, tested for the streaming brand. undefined when no handler
249
+ call signature is found — the caller falls back to the char-scan. A resolvable handler that
250
+ returns a plain value yields `false` (definitively not streaming), so a warm program does not
251
+ needlessly defer to the scan for ordinary handlers.
252
+ */
253
+ function handlerReturnsStream(
254
+ checker: ts.TypeChecker,
255
+ call: ts.CallExpression,
256
+ ): boolean | undefined {
257
+ const handler = call.arguments[0]
258
+ if (handler === undefined) {
259
+ return undefined
260
+ }
261
+ const signature = checker.getTypeAtLocation(handler).getCallSignatures()[0]
262
+ if (signature === undefined) {
263
+ return undefined
264
+ }
265
+ const returnType = unwrapPromise(checker, checker.getReturnTypeOfSignature(signature))
266
+ return returnTypeIsStreaming(checker, returnType)
267
+ }
268
+
269
+ /* An async handler returns `Promise<TypedResponse<…>>`; unwrap one Promise layer so the
270
+ brand check sees the response type. */
271
+ function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
272
+ if (type.getSymbol()?.name === 'Promise') {
273
+ const inner = checker.getTypeArguments(type as ts.TypeReference)[0]
274
+ if (inner !== undefined) {
275
+ return inner
276
+ }
277
+ }
278
+ return type
279
+ }
280
+
281
+ /* True when the type is (or, across a union of return branches, contains) a
282
+ `TypedResponse<AsyncIterable<…>>` — a `__body` phantom property whose type is async-iterable.
283
+ `jsonl()`/`sse()` are the only constructors of that shape, so this is congruent by
284
+ construction with the RemoteCallable streaming conditional. */
285
+ function returnTypeIsStreaming(checker: ts.TypeChecker, type: ts.Type): boolean {
286
+ if (type.isUnion()) {
287
+ return type.types.some((member) => returnTypeIsStreaming(checker, member))
288
+ }
289
+ const bodyProperty = type.getProperty('__body')
290
+ if (bodyProperty === undefined) {
291
+ return false
292
+ }
293
+ const declaration = bodyProperty.valueDeclaration ?? bodyProperty.declarations?.[0]
294
+ if (declaration === undefined) {
295
+ return false
296
+ }
297
+ return typeIsAsyncIterable(
298
+ checker,
299
+ checker.getTypeOfSymbolAtLocation(bodyProperty, declaration),
300
+ )
301
+ }
302
+
303
+ /* Async-iterable iff the type exposes a `[Symbol.asyncIterator]` member — the computed
304
+ well-known symbol surfaces as an `__@asyncIterator@<n>` property name. Recurses through a
305
+ union so `AsyncIterable<A> | AsyncIterable<B>` still counts. */
306
+ function typeIsAsyncIterable(checker: ts.TypeChecker, type: ts.Type): boolean {
307
+ if (type.isUnion()) {
308
+ return type.types.some((member) => typeIsAsyncIterable(checker, member))
309
+ }
310
+ for (const property of checker.getPropertiesOfType(type)) {
311
+ if (property.getName().includes('asyncIterator')) {
312
+ return true
313
+ }
314
+ }
315
+ return false
316
+ }
317
+
318
+ /*
319
+ The rpc export's success-body descriptor (ADR-0030), projected from the handler's return type the
320
+ same way `handlerReturnsStream` reads it — unwrap one `Promise` layer, then walk the return union.
321
+ This mirrors `RpcHelper`'s `SuccessBody<R>`: a `TypedError` branch (its `__abideError` brand) is an
322
+ error response and is dropped; every success `TypedResponse<Body>` (its `__body` phantom) yields its
323
+ body. A branch is a streaming body when the body is async-iterable, in which case its per-FRAME type
324
+ (the `AsyncIterable` element) is taken and the descriptor marked streaming. undefined when no handler
325
+ call signature or no success body resolves — the caller falls open to an author-declared schema.
326
+ */
327
+ function handlerReturnBody(
328
+ checker: ts.TypeChecker,
329
+ call: ts.CallExpression,
330
+ ): ReturnBody | undefined {
331
+ const rendered: string[] = []
332
+ let streaming = false
333
+ const resolved = walkSuccessBodies(checker, call, (part, isStreaming) => {
334
+ if (isStreaming) {
335
+ streaming = true
336
+ }
337
+ // An untagged Response (no `__body` phantom) has an `unknown` success body — SuccessBody's fallback.
338
+ pushUnique(rendered, part === undefined ? 'unknown' : checker.typeToString(part))
339
+ })
340
+ if (!resolved || rendered.length === 0) {
341
+ return undefined
342
+ }
343
+ return { type: rendered.join(' | '), streaming }
344
+ }
345
+
346
+ /*
347
+ The rpc export's success body projected to JSON Schema (ADR-0030 D2) — the output-surface sibling of
348
+ `handlerReturnBody`, walking the identical success-body constituents but projecting each through
349
+ `jsonSchemaForType` instead of rendering a TS string. Multiple success branches combine under `anyOf`;
350
+ an untagged/unprojectable body contributes the permissive `{}`. undefined when no handler signature
351
+ resolves or the combined schema is bare permissive `{}` — the caller falls open to an author-declared
352
+ `schemas.output`.
353
+ */
354
+ function returnBodyJsonSchema(
355
+ checker: ts.TypeChecker,
356
+ call: ts.CallExpression,
357
+ ): Record<string, unknown> | undefined {
358
+ const schemas: Record<string, unknown>[] = []
359
+ const resolved = walkSuccessBodies(checker, call, (part) => {
360
+ const projected = part === undefined ? undefined : jsonSchemaForType(checker, part)
361
+ pushUniqueSchema(schemas, projected ?? {})
362
+ })
363
+ if (!resolved || schemas.length === 0) {
364
+ return undefined
365
+ }
366
+ const [onlySchema] = schemas
367
+ const combined =
368
+ schemas.length === 1 && onlySchema !== undefined ? onlySchema : { anyOf: schemas }
369
+ return Object.keys(combined).length === 0 ? undefined : combined
370
+ }
371
+
372
+ /*
373
+ Walks a handler's success-body constituents, shared by `handlerReturnBody` (renders each to a TS
374
+ string) and `returnBodyJsonSchema` (projects each to JSON Schema) so the two surfaces stay congruent.
375
+ Unwraps one `Promise`, then over the return union drops the `TypedError` branches (their `__abideError`
376
+ brand) and, for each success `TypedResponse<Body>` (its `__body` phantom), visits every meaningful body
377
+ part — frame-unwrapped and flagged streaming when async-iterable. `visit` receives undefined for an
378
+ untagged Response (its body is `unknown`). Returns false when no handler call signature resolves, so
379
+ callers fail open.
380
+ */
381
+ function walkSuccessBodies(
382
+ checker: ts.TypeChecker,
383
+ call: ts.CallExpression,
384
+ visit: (part: ts.Type | undefined, streaming: boolean) => void,
385
+ ): boolean {
386
+ const members = returnUnionMembers(checker, call)
387
+ if (members === undefined) {
388
+ return false
389
+ }
390
+ for (const member of members) {
391
+ if (member.getProperty('__abideError') !== undefined) {
392
+ continue
393
+ }
394
+ const bodyProperty = member.getProperty('__body')
395
+ if (bodyProperty === undefined) {
396
+ visit(undefined, false)
397
+ continue
398
+ }
399
+ const declaration = bodyProperty.valueDeclaration ?? bodyProperty.declarations?.[0]
400
+ if (declaration === undefined) {
401
+ visit(undefined, false)
402
+ continue
403
+ }
404
+ // The `__body?` phantom is optional, so reading its type adds a spurious `| undefined`;
405
+ // drop the nullish members (indistinguishable from an authored optional body — the surface
406
+ // describes the present value's shape either way).
407
+ const bodyType = checker.getTypeOfSymbolAtLocation(bodyProperty, declaration)
408
+ for (const part of meaningfulMembers(bodyType)) {
409
+ if (typeIsAsyncIterable(checker, part)) {
410
+ visit(asyncIterableElement(checker, part) ?? part, true)
411
+ } else {
412
+ visit(part, false)
413
+ }
414
+ }
415
+ }
416
+ return true
417
+ }
418
+
419
+ /* The handler's return-type union members — the shared front of both the success walk
420
+ (`walkSuccessBodies`) and the error walk (`errorBranchSchemas`): resolve the handler call
421
+ signature, unwrap one `Promise` layer, then split the union (a lone return is a one-member list).
422
+ undefined when no handler call signature resolves, so both callers fail open. */
423
+ function returnUnionMembers(
424
+ checker: ts.TypeChecker,
425
+ call: ts.CallExpression,
426
+ ): ts.Type[] | undefined {
427
+ const handler = call.arguments[0]
428
+ if (handler === undefined) {
429
+ return undefined
430
+ }
431
+ const signature = checker.getTypeAtLocation(handler).getCallSignatures()[0]
432
+ if (signature === undefined) {
433
+ return undefined
434
+ }
435
+ const returnType = unwrapPromise(checker, checker.getReturnTypeOfSignature(signature))
436
+ return returnType.isUnion() ? returnType.types : [returnType]
437
+ }
438
+
439
+ /*
440
+ The handler's typed-error branches projected to a status-keyed JSON-Schema map (ADR-0030) — the
441
+ error-branch sibling of `returnBodyJsonSchema`, walking the SAME return union but keeping the
442
+ `TypedError` members the success walk drops. For each `__abideError`-branded branch it reads the
443
+ brand's numeric `entry.status` (a literal, captured by `error.typed`'s `Status` type param) and
444
+ projects the error's `data` type (its schema's `~standard.types.input`) through `jsonSchemaForType`.
445
+ Branches sharing a status combine their distinct data schemas under `anyOf`; a nullary error (no
446
+ `data`) contributes a bare `{}` so the status still surfaces. undefined when no handler signature
447
+ resolves or no typed-error branch is present, so the caller stamps nothing and the surface omits the
448
+ error responses exactly as today.
449
+ */
450
+ function errorBranchSchemas(
451
+ checker: ts.TypeChecker,
452
+ call: ts.CallExpression,
453
+ ): ErrorJsonSchemas | undefined {
454
+ const members = returnUnionMembers(checker, call)
455
+ if (members === undefined) {
456
+ return undefined
457
+ }
458
+ // Status → its collected distinct data schemas (empty when only nullary errors share the status).
459
+ const byStatus = new Map<number, Record<string, unknown>[]>()
460
+ for (const member of members) {
461
+ const brand = member.getProperty('__abideError')
462
+ if (brand === undefined) {
463
+ continue
464
+ }
465
+ const entry = memberType(checker, checker.getTypeOfSymbol(brand), 'entry')
466
+ if (entry === undefined) {
467
+ continue
468
+ }
469
+ const status = numberLiteral(memberType(checker, entry, 'status'))
470
+ if (status === undefined) {
471
+ continue
472
+ }
473
+ let schemas = byStatus.get(status)
474
+ if (schemas === undefined) {
475
+ schemas = []
476
+ byStatus.set(status, schemas)
477
+ }
478
+ const dataSchema = errorDataSchema(checker, memberType(checker, entry, 'data'))
479
+ if (dataSchema !== undefined) {
480
+ pushUniqueSchema(schemas, dataSchema)
481
+ }
482
+ }
483
+ if (byStatus.size === 0) {
484
+ return undefined
485
+ }
486
+ const result: ErrorJsonSchemas = {}
487
+ for (const [status, schemas] of byStatus) {
488
+ const [onlySchema] = schemas
489
+ result[status] =
490
+ schemas.length === 0
491
+ ? {}
492
+ : schemas.length === 1 && onlySchema !== undefined
493
+ ? onlySchema
494
+ : { anyOf: schemas }
495
+ }
496
+ return result
497
+ }
498
+
499
+ /* An error branch's `data` type projected to JSON Schema — navigates the schema brand's phantom
500
+ `~standard.types.input` (the data payload the `error.typed` constructor receives) and projects it
501
+ via `jsonSchemaForType`. undefined for a nullary error (its `entry.data` is `undefined`, so the
502
+ `~standard` chain doesn't resolve) or an unprojectable/permissive data type, so the status
503
+ surfaces without a content schema. */
504
+ function errorDataSchema(
505
+ checker: ts.TypeChecker,
506
+ dataType: ts.Type | undefined,
507
+ ): Record<string, unknown> | undefined {
508
+ if (dataType === undefined) {
509
+ return undefined
510
+ }
511
+ const standard = memberType(checker, dataType, '~standard')
512
+ if (standard === undefined) {
513
+ return undefined
514
+ }
515
+ // `types` is `{ input; output } | undefined` (the spec's optional phantom) — take the non-nullish member.
516
+ const types = memberType(checker, standard, 'types')
517
+ if (types === undefined) {
518
+ return undefined
519
+ }
520
+ const input = memberType(checker, nonNullishType(types), 'input')
521
+ if (input === undefined) {
522
+ return undefined
523
+ }
524
+ return jsonSchemaForType(checker, input)
525
+ }
526
+
527
+ /* A named property's type, read location-free off a resolved type. undefined when the type has no
528
+ such property — the phantom-navigation guard for the error brand's nested `entry`/`status`/`data`
529
+ / `~standard`/`types`/`input` chain. */
530
+ function memberType(checker: ts.TypeChecker, type: ts.Type, name: string): ts.Type | undefined {
531
+ const symbol = type.getProperty(name)
532
+ return symbol === undefined ? undefined : checker.getTypeOfSymbol(symbol)
533
+ }
534
+
535
+ /* A type's numeric-literal value (e.g. `404`), or undefined when it isn't a single number literal
536
+ (a widened `number`, a union, or undefined) — so a computed error status the type can't pin down
537
+ is skipped rather than guessed. */
538
+ function numberLiteral(type: ts.Type | undefined): number | undefined {
539
+ if (type === undefined || (type.flags & ts.TypeFlags.NumberLiteral) === 0) {
540
+ return undefined
541
+ }
542
+ return (type as ts.NumberLiteralType).value
543
+ }
544
+
545
+ /* Drops the nullish members of a `T | undefined` union, returning the lone survivor; else the type
546
+ unchanged (a non-union or a genuine multi-member union stays as-is). Used to strip the spec's
547
+ optional `types?` phantom down to its concrete `Types` object. */
548
+ function nonNullishType(type: ts.Type): ts.Type {
549
+ if (!type.isUnion()) {
550
+ return type
551
+ }
552
+ const kept = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
553
+ const [onlyKept] = kept
554
+ return kept.length === 1 && onlyKept !== undefined ? onlyKept : type
555
+ }
556
+
557
+ /* The non-nullish constituents of a body type — a single-element list for a non-union, else the
558
+ union's members with `undefined`/`null`/`void` dropped (the `__body?` phantom's optionality). All
559
+ members nullish (an unusual `undefined`-only body) keeps them so something still renders. */
560
+ function meaningfulMembers(type: ts.Type): ts.Type[] {
561
+ if (!type.isUnion()) {
562
+ return [type]
563
+ }
564
+ const kept = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
565
+ return kept.length > 0 ? kept : type.types
566
+ }
567
+
568
+ /* The element type of an `AsyncIterable<Frame>` (or the AsyncIterableIterator/AsyncGenerator a
569
+ generator produces) — the per-frame type `jsonl()`/`sse()` streams. undefined when the type isn't
570
+ one of those references, so the caller renders the whole body instead. */
571
+ function asyncIterableElement(checker: ts.TypeChecker, type: ts.Type): ts.Type | undefined {
572
+ const name = type.getSymbol()?.name
573
+ if (name === 'AsyncIterable' || name === 'AsyncIterableIterator' || name === 'AsyncGenerator') {
574
+ return checker.getTypeArguments(type as ts.TypeReference)[0]
575
+ }
576
+ return undefined
577
+ }
578
+
579
+ /* Appends `value` only when not already present, so a union of identical success branches renders
580
+ once (the rendered list stays a small monomorphic string array). */
581
+ function pushUnique(list: string[], value: string): void {
582
+ if (!list.includes(value)) {
583
+ list.push(value)
584
+ }
585
+ }
586
+
587
+ /* Appends a projected schema only when structurally new, so identical success branches contribute one
588
+ `anyOf` member. Dedup by serialization — the schema objects are small plain data, and this build
589
+ path runs once per rpc. */
590
+ function pushUniqueSchema(list: Record<string, unknown>[], schema: Record<string, unknown>): void {
591
+ const serialized = JSON.stringify(schema)
592
+ if (!list.some((existing) => JSON.stringify(existing) === serialized)) {
593
+ list.push(schema)
594
+ }
595
+ }
596
+
597
+ /*
598
+ The wire codec plan for an rpc export's input args (ADR-0028 scalars, ADR-0029 structured). The
599
+ export's type is its RemoteFunction; its call signature's first parameter is the wire `Args` type
600
+ (`InferInput` of the input schema, or the handler's annotated param when schemaless). Each
601
+ number/boolean/date/bigint/set/map field of that Args object becomes a plan entry; every other
602
+ field is omitted. undefined when the RemoteFunction has no call signature, the Args aren't a
603
+ single object type, or no field is a recognized WireKind — the caller then ships no plan (today's
604
+ string-through behavior).
605
+ */
606
+ function inputCoercionPlan(
607
+ checker: ts.TypeChecker,
608
+ call: ts.CallExpression,
609
+ ): InputCoercion | undefined {
610
+ const signature = checker.getTypeAtLocation(call).getCallSignatures()[0]
611
+ if (signature === undefined) {
612
+ return undefined
613
+ }
614
+ const parameter = signature.getParameters()[0]
615
+ if (parameter === undefined) {
616
+ return undefined
617
+ }
618
+ const argsType = argsBagType(checker.getTypeOfSymbolAtLocation(parameter, call))
619
+ if (argsType === undefined) {
620
+ return undefined
621
+ }
622
+ const plan: InputCoercion = {}
623
+ for (const property of argsType.getProperties()) {
624
+ const kind = wireKind(checker, checker.getTypeOfSymbolAtLocation(property, call))
625
+ if (kind !== undefined) {
626
+ plan[property.getName()] = kind
627
+ }
628
+ }
629
+ /* An empty plan (no codec-eligible field) ships nothing, so no opts are injected. */
630
+ return Object.keys(plan).length === 0 ? undefined : plan
631
+ }
632
+
633
+ /*
634
+ The wire codec plan for an rpc export's success RESPONSE body (ADR-0029 output path) — the
635
+ response-side sibling of `inputCoercionPlan`. Walks the handler's success-body constituents the same
636
+ way the ADR-0030 surfaces do (`walkSuccessBodies` — unwrap `Promise`, drop the `TypedError` branches)
637
+ and, for each OBJECT body, reads each property's `WireKind` and keeps only the STRUCTURED kinds
638
+ (`date`/`bigint`/`set`/`map`) the client revives — `number`/`boolean` already ride as their JSON type.
639
+ A streaming body is skipped (its frame encoding is deferred). undefined when no handler signature
640
+ resolves or no structured field is present, so the client bakes no plan.
641
+ */
642
+ function outputWirePlan(
643
+ checker: ts.TypeChecker,
644
+ call: ts.CallExpression,
645
+ ): OutputWirePlan | undefined {
646
+ const plan: OutputWirePlan = {}
647
+ const resolved = walkSuccessBodies(checker, call, (part, streaming) => {
648
+ /* Frame-level encoding for jsonl()/sse() is deferred (ADR-0029), and an untagged Response
649
+ body is `unknown` — neither yields a top-level field plan. */
650
+ if (streaming || part === undefined) {
651
+ return
652
+ }
653
+ for (const property of part.getProperties()) {
654
+ const kind = wireKind(checker, checker.getTypeOfSymbol(property))
655
+ if (kind === 'date' || kind === 'bigint' || kind === 'set' || kind === 'map') {
656
+ plan[property.getName()] = kind
657
+ }
658
+ }
659
+ })
660
+ if (!resolved || Object.keys(plan).length === 0) {
661
+ return undefined
662
+ }
663
+ return plan
664
+ }
665
+
666
+ /*
667
+ The rpc export's INPUT args projected to JSON Schema (ADR-0030 input side) — the input-surface
668
+ sibling of `returnBodyJsonSchema`. Resolves the SAME wire `Args` bag `inputCoercionPlan` reads (the
669
+ exported RemoteFunction's first call-signature parameter, `FormData` dropped by `argsBagType`), then
670
+ builds an object schema, projecting each property through `jsonSchemaForType`. A File-typed property
671
+ is EXCLUDED exactly as `filesSchema` keeps File parts out of the `inputSchema` projection — a File has
672
+ no honest JSON-Schema form, so the multipart body advertises binaries generically instead. A property
673
+ is required unless it is `?` optional or its type bears `undefined`. This is purely a SHAPE
674
+ description: the surfaces consume it for docs, never for runtime validation, so it can't cause a 422.
675
+ undefined when no call signature / single-object Args bag resolves or every property is excluded, so
676
+ the caller stamps nothing and the surface falls open to an author-declared `schemas.input`.
677
+ */
678
+ function inputArgsJsonSchema(
679
+ checker: ts.TypeChecker,
680
+ call: ts.CallExpression,
681
+ ): Record<string, unknown> | undefined {
682
+ const signature = checker.getTypeAtLocation(call).getCallSignatures()[0]
683
+ if (signature === undefined) {
684
+ return undefined
685
+ }
686
+ const parameter = signature.getParameters()[0]
687
+ if (parameter === undefined) {
688
+ return undefined
689
+ }
690
+ const argsType = argsBagType(checker.getTypeOfSymbolAtLocation(parameter, call))
691
+ if (argsType === undefined) {
692
+ return undefined
693
+ }
694
+ const properties: Record<string, unknown> = {}
695
+ const required: string[] = []
696
+ for (const property of argsType.getProperties()) {
697
+ const propertyType = checker.getTypeOfSymbolAtLocation(property, call)
698
+ if (isFileType(propertyType)) {
699
+ continue
700
+ }
701
+ // jsonSchemaForType collapses a bare permissive projection to undefined; restore `{}` so an
702
+ // unprojectable property still appears as an "any JSON" field of the object.
703
+ properties[property.getName()] = jsonSchemaForType(checker, propertyType) ?? {}
704
+ const optional =
705
+ (property.flags & ts.SymbolFlags.Optional) !== 0 || bearsOptionality(propertyType)
706
+ if (!optional) {
707
+ required.push(property.getName())
708
+ }
709
+ }
710
+ if (Object.keys(properties).length === 0) {
711
+ return undefined
712
+ }
713
+ const schema: Record<string, unknown> = { type: 'object', properties }
714
+ if (required.length > 0) {
715
+ schema.required = required
716
+ }
717
+ return schema
718
+ }
719
+
720
+ /* True when a type is (or, across a union, contains) `File`/`Blob` — the multipart binary members
721
+ excluded from the input-schema projection, mirroring how `filesSchema` stays out of `inputSchema`. */
722
+ function isFileType(type: ts.Type): boolean {
723
+ if (type.isUnion()) {
724
+ return type.types.some((member) => isFileType(member))
725
+ }
726
+ const name = type.getSymbol()?.name
727
+ return name === 'File' || name === 'Blob'
728
+ }
729
+
730
+ /* True when a property's type bears `undefined`/`void` — an implicit optional even without the `?`
731
+ modifier (mirrors jsonSchemaForType's own `bearsUndefined` so the two projections agree). */
732
+ function bearsOptionality(type: ts.Type): boolean {
733
+ if (type.isUnion()) {
734
+ return type.types.some((member) => (member.flags & OPTIONALITY_FLAGS) !== 0)
735
+ }
736
+ return (type.flags & OPTIONALITY_FLAGS) !== 0
737
+ }
738
+
739
+ /*
740
+ The object `Args` constituent of a RemoteCallable parameter. Every rpc call signature types its
741
+ first parameter `Args | FormData` (the multipart upload escape hatch), so the FormData and any
742
+ nullish members are dropped and the lone remaining type returned. undefined when the arg is not
743
+ a single object type — a no-input rpc (`Args = undefined`) or a FormData-only body — so no plan
744
+ is produced.
745
+ */
746
+ function argsBagType(parameterType: ts.Type): ts.Type | undefined {
747
+ if (!parameterType.isUnion()) {
748
+ return parameterType
749
+ }
750
+ const kept = parameterType.types.filter(
751
+ (member) => member.getSymbol()?.name !== 'FormData' && (member.flags & NULLISH_FLAGS) === 0,
752
+ )
753
+ return kept.length === 1 ? kept[0] : undefined
754
+ }
755
+
756
+ /*
757
+ A field's wire codec kind, or undefined when it isn't codec-eligible. Unwraps `T | undefined` (an
758
+ optional field) and a `T[]` (a repeated query key / JSON array whose ELEMENTS revive per item) to
759
+ the target type, then classifies: a pure `number`/`boolean`/`bigint` by its type flag, a `Date`/
760
+ `Set`/`Map` by its symbol identity through the checker. Anything else — a string, a `number |
761
+ string` union, a plain object — → undefined, so it stays its JSON form and the schema decides. The
762
+ symbol checks are top-level only: a `Set` NESTED in another value is not descended into (ADR-0029
763
+ defers recursive descent).
764
+ */
765
+ function wireKind(checker: ts.TypeChecker, type: ts.Type): WireKind | undefined {
766
+ const bare = unwrapOptional(type)
767
+ const target = arrayElement(checker, bare) ?? bare
768
+ if (isKind(target, NUMBER_FLAGS)) {
769
+ return 'number'
770
+ }
771
+ if (isKind(target, BOOLEAN_FLAGS)) {
772
+ return 'boolean'
773
+ }
774
+ if (isKind(target, BIGINT_FLAGS)) {
775
+ return 'bigint'
776
+ }
777
+ const symbolName = target.getSymbol()?.name
778
+ if (symbolName === 'Date') {
779
+ return 'date'
780
+ }
781
+ if (symbolName === 'Set' || symbolName === 'ReadonlySet') {
782
+ return 'set'
783
+ }
784
+ if (symbolName === 'Map' || symbolName === 'ReadonlyMap') {
785
+ return 'map'
786
+ }
787
+ return undefined
788
+ }
789
+
790
+ /* Drops the nullish members of a `T | undefined` optional; returns the lone survivor, else the
791
+ type unchanged (a genuine multi-member union stays as-is for isKind to reject). */
792
+ function unwrapOptional(type: ts.Type): ts.Type {
793
+ if (!type.isUnion()) {
794
+ return type
795
+ }
796
+ const kept = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
797
+ const [onlyKept] = kept
798
+ return kept.length === 1 && onlyKept !== undefined ? onlyKept : type
799
+ }
800
+
801
+ /* The element type of a `T[]`/`readonly T[]`, so a repeated query key (`?tag=1&tag=2`) coerces
802
+ per element; undefined for a non-array (tuples included — their element type is heterogeneous). */
803
+ function arrayElement(checker: ts.TypeChecker, type: ts.Type): ts.Type | undefined {
804
+ const name = type.getSymbol()?.name
805
+ if (name === 'Array' || name === 'ReadonlyArray') {
806
+ return checker.getTypeArguments(type as ts.TypeReference)[0]
807
+ }
808
+ return undefined
809
+ }
810
+
811
+ /* True when every meaningful (non-nullish) member of the type matches `mask` — so `number`,
812
+ `number | undefined`, and a `1 | 2` literal union all read as number; a `number | string`
813
+ union does not. A bare `boolean` is a `true | false` union, handled by the recursion. */
814
+ function isKind(type: ts.Type, mask: ts.TypeFlags): boolean {
815
+ if (type.isUnion()) {
816
+ const meaningful = type.types.filter((member) => (member.flags & NULLISH_FLAGS) === 0)
817
+ return meaningful.length > 0 && meaningful.every((member) => isKind(member, mask))
818
+ }
819
+ return (type.flags & mask) !== 0
820
+ }