@abide/abide 0.48.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 (360) hide show
  1. package/AGENTS.md +85 -59
  2. package/CHANGELOG.md +216 -0
  3. package/README.md +21 -10
  4. package/package.json +15 -4
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildCli.ts +3 -5
  9. package/src/buildDisconnected.ts +2 -5
  10. package/src/bundleApp.ts +2 -3
  11. package/src/checkAbide.ts +12 -2
  12. package/src/compile.ts +2 -4
  13. package/src/devEntry.ts +113 -28
  14. package/src/discoveryEntry.ts +3 -2
  15. package/src/lib/bundle/disconnected.abide +69 -73
  16. package/src/lib/bundle/infoPlist.ts +13 -7
  17. package/src/lib/bundle/installDownloads.ts +13 -3
  18. package/src/lib/bundle/installMacMenu.ts +13 -3
  19. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  20. package/src/lib/bundle/waitForServer.ts +6 -1
  21. package/src/lib/cli/parseArgvForRpc.ts +36 -9
  22. package/src/lib/cli/tokenizeArgvFlags.ts +4 -2
  23. package/src/lib/mcp/createMcpResourceServer.ts +13 -2
  24. package/src/lib/mcp/mcpTools.ts +15 -6
  25. package/src/lib/mcp/toolResultFromResponse.ts +40 -12
  26. package/src/lib/server/error.ts +6 -6
  27. package/src/lib/server/json.ts +17 -3
  28. package/src/lib/server/rpc/defineRpc.ts +58 -24
  29. package/src/lib/server/rpc/parseArgs.ts +113 -14
  30. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  31. package/src/lib/server/rpc/runWithRpcTimeout.ts +12 -1
  32. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  33. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  34. package/src/lib/server/rpc/validationError.ts +1 -1
  35. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  36. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  37. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  38. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  39. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  40. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  41. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  42. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  43. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  44. package/src/lib/server/runtime/createServer.ts +120 -324
  45. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  46. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  47. package/src/lib/server/runtime/finalizeResponse.ts +11 -1
  48. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  49. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  50. package/src/lib/server/runtime/pathStore.ts +15 -0
  51. package/src/lib/server/runtime/registryManifests.ts +1 -1
  52. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  53. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  54. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  55. package/src/lib/server/runtime/snapshotEntryFromCache.ts +24 -16
  56. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  57. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  58. package/src/lib/server/runtime/textResponse.ts +23 -0
  59. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  60. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  61. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  62. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  63. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  64. package/src/lib/server/sockets/defineSocket.ts +5 -4
  65. package/src/lib/server/sse.ts +5 -1
  66. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  67. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  68. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  69. package/src/lib/shared/HttpError.ts +1 -5
  70. package/src/lib/shared/MCP_PATH.ts +6 -0
  71. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  72. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  73. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  74. package/src/lib/shared/activeCacheStore.ts +1 -0
  75. package/src/lib/shared/activePage.ts +1 -0
  76. package/src/lib/shared/buildArtifact.ts +20 -0
  77. package/src/lib/shared/buildRpcRequest.ts +6 -2
  78. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  79. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  80. package/src/lib/shared/cache.ts +156 -131
  81. package/src/lib/shared/canonicalJson.ts +24 -1
  82. package/src/lib/shared/changeAffectsClient.ts +12 -7
  83. package/src/lib/shared/createCacheStore.ts +31 -8
  84. package/src/lib/shared/createChannelLog.ts +24 -0
  85. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  86. package/src/lib/shared/createReachable.ts +47 -78
  87. package/src/lib/shared/createRemoteFunction.ts +52 -30
  88. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  89. package/src/lib/shared/decodeRefJson.ts +4 -4
  90. package/src/lib/shared/decodeResponse.ts +2 -2
  91. package/src/lib/shared/decodeWireBody.ts +35 -0
  92. package/src/lib/shared/detectRpcMethod.ts +8 -1
  93. package/src/lib/shared/done.ts +8 -2
  94. package/src/lib/shared/encodeRefJson.ts +4 -4
  95. package/src/lib/shared/encodeWireBody.ts +18 -0
  96. package/src/lib/shared/exitOnBuildFailure.ts +4 -3
  97. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  98. package/src/lib/shared/generateDeclarations.ts +72 -0
  99. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  100. package/src/lib/shared/hydrationWindow.ts +53 -0
  101. package/src/lib/shared/isAsyncCell.ts +11 -0
  102. package/src/lib/shared/isAsyncIterable.ts +8 -0
  103. package/src/lib/shared/isSubscribable.ts +3 -3
  104. package/src/lib/shared/isThenable.ts +9 -0
  105. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  106. package/src/lib/shared/lenientDecode.ts +15 -0
  107. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  108. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  109. package/src/lib/shared/matchRoute.ts +4 -14
  110. package/src/lib/shared/parseEnv.ts +17 -6
  111. package/src/lib/shared/peek.ts +14 -0
  112. package/src/lib/shared/pending.ts +9 -6
  113. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  114. package/src/lib/shared/prepareRpcModule.ts +91 -93
  115. package/src/lib/shared/prepareSocketModule.ts +3 -2
  116. package/src/lib/shared/probeRegistries.ts +5 -18
  117. package/src/lib/shared/reachable.ts +7 -10
  118. package/src/lib/shared/refresh.ts +12 -2
  119. package/src/lib/shared/refreshing.ts +8 -2
  120. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  121. package/src/lib/shared/reviveWireField.ts +49 -0
  122. package/src/lib/shared/reviveWireOutput.ts +36 -0
  123. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  124. package/src/lib/shared/scanPages.ts +25 -0
  125. package/src/lib/shared/serializeEnv.ts +18 -6
  126. package/src/lib/shared/snapshotShippable.ts +8 -7
  127. package/src/lib/shared/snippet.ts +11 -6
  128. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  129. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  130. package/src/lib/shared/subscribableProbes.ts +1 -1
  131. package/src/lib/shared/tailProbeSlot.ts +1 -1
  132. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  133. package/src/lib/shared/types/AsyncState.ts +13 -0
  134. package/src/lib/shared/types/CacheEntry.ts +7 -7
  135. package/src/lib/shared/types/CacheOptions.ts +23 -37
  136. package/src/lib/shared/types/CachePolicy.ts +25 -0
  137. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  138. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  139. package/src/lib/shared/types/HttpMethod.ts +3 -1
  140. package/src/lib/shared/types/InputCoercion.ts +17 -0
  141. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  142. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  143. package/src/lib/shared/types/PagesScan.ts +7 -0
  144. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  145. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  146. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  147. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  148. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  149. package/src/lib/shared/types/ReturnBody.ts +15 -0
  150. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  151. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  152. package/src/lib/shared/types/Socket.ts +4 -4
  153. package/src/lib/shared/types/SsrPayload.ts +5 -1
  154. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  155. package/src/lib/shared/types/StreamedCells.ts +19 -0
  156. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  157. package/src/lib/shared/types/TailHooks.ts +1 -1
  158. package/src/lib/shared/types/WireKind.ts +11 -0
  159. package/src/lib/shared/url.ts +5 -0
  160. package/src/lib/shared/validationHttpError.ts +33 -0
  161. package/src/lib/shared/warmSeedKey.ts +16 -0
  162. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  163. package/src/lib/shared/writeRpcDts.ts +11 -1
  164. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  165. package/src/lib/test/createTestApp.ts +19 -1
  166. package/src/lib/ui/README.md +1 -1
  167. package/src/lib/ui/activePendingCells.ts +14 -0
  168. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  169. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  170. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -7
  171. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  172. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  173. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  174. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  175. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  176. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  177. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  178. package/src/lib/ui/compile/bindListenEvent.ts +5 -6
  179. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  180. package/src/lib/ui/compile/catchBinding.ts +7 -5
  181. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  182. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  183. package/src/lib/ui/compile/compileComponent.ts +19 -3
  184. package/src/lib/ui/compile/compileModule.ts +27 -58
  185. package/src/lib/ui/compile/compileSSR.ts +52 -13
  186. package/src/lib/ui/compile/compileShadow.ts +265 -42
  187. package/src/lib/ui/compile/composeProps.ts +15 -4
  188. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  189. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  190. package/src/lib/ui/compile/declaredNames.ts +36 -0
  191. package/src/lib/ui/compile/desugarSignals.ts +419 -37
  192. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  193. package/src/lib/ui/compile/generateBuild.ts +74 -17
  194. package/src/lib/ui/compile/generateSSR.ts +286 -75
  195. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  196. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  197. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  198. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  199. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  200. package/src/lib/ui/compile/isWhitespaceText.ts +10 -2
  201. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  202. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  203. package/src/lib/ui/compile/lowerContext.ts +10 -0
  204. package/src/lib/ui/compile/lowerDocAccess.ts +51 -10
  205. package/src/lib/ui/compile/lowerScript.ts +40 -4
  206. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  207. package/src/lib/ui/compile/parseTemplate.ts +18 -1090
  208. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  209. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  210. package/src/lib/ui/compile/renameSignalRefs.ts +26 -40
  211. package/src/lib/ui/compile/resolveReactiveExport.ts +4 -7
  212. package/src/lib/ui/compile/scopeCss.ts +27 -4
  213. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  214. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  215. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  216. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  217. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  218. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  219. package/src/lib/ui/compile/tryPlan.ts +4 -3
  220. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  221. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  222. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  223. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  224. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  225. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  226. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  227. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  228. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  229. package/src/lib/ui/computed.ts +28 -1
  230. package/src/lib/ui/createScope.ts +35 -68
  231. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  232. package/src/lib/ui/dom/appendText.ts +8 -3
  233. package/src/lib/ui/dom/awaitBlock.ts +62 -75
  234. package/src/lib/ui/dom/bindProp.ts +22 -0
  235. package/src/lib/ui/dom/bindableProp.ts +47 -0
  236. package/src/lib/ui/dom/cellPending.ts +24 -0
  237. package/src/lib/ui/dom/discardBoundary.ts +24 -6
  238. package/src/lib/ui/dom/disposeRange.ts +2 -1
  239. package/src/lib/ui/dom/each.ts +41 -12
  240. package/src/lib/ui/dom/eachAsync.ts +39 -3
  241. package/src/lib/ui/dom/fillBefore.ts +6 -7
  242. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  243. package/src/lib/ui/dom/fillRange.ts +4 -4
  244. package/src/lib/ui/dom/hydrate.ts +6 -12
  245. package/src/lib/ui/dom/isComment.ts +1 -1
  246. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  247. package/src/lib/ui/dom/mergeProps.ts +1 -1
  248. package/src/lib/ui/dom/mount.ts +1 -2
  249. package/src/lib/ui/dom/mountChild.ts +9 -40
  250. package/src/lib/ui/dom/mountRange.ts +2 -3
  251. package/src/lib/ui/dom/mountSlot.ts +1 -1
  252. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  253. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  254. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  255. package/src/lib/ui/dom/on.ts +2 -2
  256. package/src/lib/ui/dom/readCell.ts +40 -0
  257. package/src/lib/ui/dom/restProps.ts +2 -2
  258. package/src/lib/ui/dom/spreadProps.ts +3 -4
  259. package/src/lib/ui/dom/switchBlock.ts +30 -7
  260. package/src/lib/ui/dom/tryBlock.ts +233 -70
  261. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  262. package/src/lib/ui/dom/when.ts +12 -2
  263. package/src/lib/ui/dom/withScope.ts +9 -2
  264. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  265. package/src/lib/ui/flight.ts +56 -0
  266. package/src/lib/ui/html.ts +12 -1
  267. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  268. package/src/lib/ui/linked.ts +48 -2
  269. package/src/lib/ui/props.ts +17 -0
  270. package/src/lib/ui/remoteProxy.ts +84 -159
  271. package/src/lib/ui/renderChain.ts +53 -13
  272. package/src/lib/ui/renderToStream.ts +22 -18
  273. package/src/lib/ui/resumeSeedScript.ts +1 -1
  274. package/src/lib/ui/router.ts +86 -53
  275. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  276. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  277. package/src/lib/ui/runtime/CHILD_PRESENT.ts +2 -2
  278. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  279. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  280. package/src/lib/ui/runtime/RENDER.ts +10 -7
  281. package/src/lib/ui/runtime/RESUME.ts +4 -4
  282. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  283. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  284. package/src/lib/ui/runtime/applyPatchToTree.ts +17 -4
  285. package/src/lib/ui/runtime/blockId.ts +31 -0
  286. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  287. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  288. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  289. package/src/lib/ui/runtime/createDoc.ts +31 -54
  290. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  291. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  292. package/src/lib/ui/runtime/flushEffects.ts +33 -1
  293. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  294. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  295. package/src/lib/ui/runtime/renderPath.ts +16 -0
  296. package/src/lib/ui/runtime/scope.ts +11 -0
  297. package/src/lib/ui/runtime/toTeardown.ts +10 -3
  298. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  299. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  300. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  301. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  302. package/src/lib/ui/runtime/types/UiProps.ts +6 -5
  303. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  304. package/src/lib/ui/runtime/withPath.ts +16 -0
  305. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  306. package/src/lib/ui/runtime/withoutHydration.ts +22 -0
  307. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  308. package/src/lib/ui/settleAsyncCells.ts +24 -0
  309. package/src/lib/ui/socketProxy.ts +1 -1
  310. package/src/lib/ui/startClient.ts +16 -31
  311. package/src/lib/ui/state.ts +9 -3
  312. package/src/lib/ui/trackedComputed.ts +68 -0
  313. package/src/lib/ui/types/Scope.ts +8 -24
  314. package/src/lib/ui/watch.ts +3 -3
  315. package/src/serverEntry.ts +14 -0
  316. package/template/src/server/rpc/getHello.ts +15 -13
  317. package/template/test/app.test.ts +1 -1
  318. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  319. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  320. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  321. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  322. package/src/lib/shared/hydratingSlot.ts +0 -12
  323. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  324. package/src/lib/shared/types/Outbox.ts +0 -9
  325. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  326. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  327. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  328. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  329. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  330. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  331. package/src/lib/ui/compile/markupTokens.ts +0 -313
  332. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  333. package/src/lib/ui/dom/applyResolved.ts +0 -87
  334. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  335. package/src/lib/ui/dom/text.ts +0 -20
  336. package/src/lib/ui/history.ts +0 -101
  337. package/src/lib/ui/installHotBridge.ts +0 -93
  338. package/src/lib/ui/installInspectorBridge.ts +0 -140
  339. package/src/lib/ui/outbox.ts +0 -35
  340. package/src/lib/ui/persist.ts +0 -115
  341. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  342. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  343. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  344. package/src/lib/ui/runtime/captureModelDoc.ts +0 -28
  345. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  346. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  347. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  348. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  349. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  350. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  351. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  352. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  353. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  354. package/src/lib/ui/seedResolved.ts +0 -28
  355. package/src/lib/ui/sync.ts +0 -48
  356. package/src/lib/ui/types/History.ts +0 -14
  357. package/src/lib/ui/types/PersistHandle.ts +0 -11
  358. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  359. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  360. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -1,13 +1,14 @@
1
- import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
1
+ import type { ErrorJsonSchemas } from '../../../shared/types/ErrorJsonSchemas.ts'
2
2
  import type { RemoteFunction } from '../../../shared/types/RemoteFunction.ts'
3
3
  import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
4
4
 
5
5
  /*
6
6
  Per-rpc registry record on the server side. MCP and CLI enumerate this
7
- to discover which RPCs are advertised (clients flags) and what shapes
8
- they expect/return. The schemas and resolved clients stay off the public
9
- RemoteFunction shape so the browser-side proxy doesn't need to carry
10
- server-only state.
7
+ to discover what shapes an rpc expects/returns. The schemas stay off the
8
+ public RemoteFunction shape so the browser-side proxy doesn't need to carry
9
+ server-only state; the resolved `clients` and `crossOrigin` ride on
10
+ `entry.remote` (ADR-0020 sweep finding #4 — no duplicate fields), so readers
11
+ reach them via `entry.remote.clients` / `entry.remote.crossOrigin`.
11
12
 
12
13
  `inputSchema` validates the argument bag and feeds the MCP tool
13
14
  `inputSchema` / OpenAPI parameters; `outputSchema` describes the success
@@ -23,13 +24,27 @@ multipart body advertises the file parts generically as binary.
23
24
  export type RpcRegistryEntry = {
24
25
  remote: RemoteFunction<unknown, unknown>
25
26
  inputSchema: StandardSchemaV1 | undefined
27
+ /* The handler's input args projected to JSON Schema at build time (ADR-0030 input side) — the
28
+ input surface's fallback when no `inputSchema` VALIDATOR is declared, mirroring how
29
+ `outputJsonSchema` backs the output surface. Already JSON Schema (not a validator), so surfaces
30
+ use it verbatim; `inputSchema`, when present, overrides it. It also makes the endpoint
31
+ advertisable (MCP/CLI) — the same role a declared `inputSchema` plays — but purely as a SHAPE
32
+ description: it is never run as runtime validation, so it can't produce a 422. */
33
+ inputJsonSchema: Record<string, unknown> | undefined
26
34
  outputSchema: StandardSchemaV1 | undefined
35
+ /* The handler's return type projected to JSON Schema at build time (ADR-0030 D2) — the output
36
+ surface's fallback when no `outputSchema` VALIDATOR is declared. Already JSON Schema (not a
37
+ validator), so surfaces use it verbatim; `outputSchema`, when present, overrides it. */
38
+ outputJsonSchema: Record<string, unknown> | undefined
39
+ /* The handler's typed-error branches projected to a status-keyed JSON-Schema map at build time
40
+ (ADR-0030) — each `error.typed(...)` return branch's status + `data` payload. Feeds the OpenAPI
41
+ `responses[status]` entries alongside the 200; undefined when the handler declares no typed
42
+ errors or the build couldn't resolve them, so the surface omits the error responses. */
43
+ errorJsonSchemas: ErrorJsonSchemas | undefined
27
44
  filesSchema: StandardSchemaV1 | undefined
28
- clients: ClientFlags
29
45
  /* The rpc's declared opts, recorded so introspection (inspector) can report
30
- the deadline/body-cap/CSRF-exemption a handler runs under. Undefined = the
31
- framework default (no deadline, Bun's server-wide body ceiling, gated). */
46
+ the deadline/body-cap a handler runs under. Undefined = the framework default
47
+ (no deadline, Bun's server-wide body ceiling). */
32
48
  timeout: number | undefined
33
49
  maxBodySize: number | undefined
34
- crossOrigin: boolean | undefined
35
50
  }
@@ -1,7 +1,7 @@
1
+ import { fieldErrorsFromIssues } from '../../shared/fieldErrorsFromIssues.ts'
1
2
  import type { StandardSchemaV1 } from '../../shared/types/StandardSchemaV1.ts'
2
3
  import type { ValidationErrorData } from '../../shared/types/ValidationErrorData.ts'
3
4
  import { typedErrorResponse } from '../runtime/typedErrorResponse.ts'
4
- import { fieldErrorsFromIssues } from './fieldErrorsFromIssues.ts'
5
5
 
6
6
  /*
7
7
  The framework-reserved `validation` typed error a 422 carries: the raw Standard
@@ -21,11 +21,9 @@ slept still reloads it. The initial connect runs even when the page loads
21
21
  hidden (the baseline must be the serving worker's stamp, captured before a swap
22
22
  can replace it) and releases itself once that first event lands.
23
23
  */
24
- import { DEV_HOT_PREFIX } from '../../shared/DEV_HOT_PREFIX.ts'
25
24
  import { DEV_RELOAD_PATH } from '../../shared/DEV_RELOAD_PATH.ts'
26
25
 
27
26
  export const DEV_RELOAD_CLIENT_SCRIPT = `<script>
28
- window.__abideDev = true;
29
27
  ;(() => {
30
28
  let stamp
31
29
  let source
@@ -48,18 +46,6 @@ window.__abideDev = true;
48
46
  }
49
47
  }
50
48
  }
51
- function swapComponents(next, prev) {
52
- const components = next.components || {}
53
- const before = prev.components || {}
54
- for (const id in components) {
55
- if (components[id] !== before[id]) {
56
- // The hot module sources its runtime from window.__abide and self-invokes
57
- // hotReplace; a load/compile failure or a component with no live instance
58
- // falls back to a reload.
59
- import('${DEV_HOT_PREFIX}' + id + '?v=' + components[id]).catch(() => location.reload())
60
- }
61
- }
62
- }
63
49
  function connect() {
64
50
  if (source) {
65
51
  return
@@ -84,7 +70,6 @@ window.__abideDev = true;
84
70
  if (next.cssHref && next.cssHref !== stamp.cssHref) {
85
71
  swapCss(next.cssHref)
86
72
  }
87
- swapComponents(next, stamp)
88
73
  stamp = next
89
74
  }
90
75
  source.onerror = () => {
@@ -18,20 +18,20 @@ function preview(value: unknown): string | undefined {
18
18
  }
19
19
  }
20
20
 
21
- /* The entry's armed swr policy as a label, if it declared one. A bare `swr: true`
22
- has no window, so it labels as plain `swr`. */
21
+ /* The entry's armed refetch (stale-while-revalidate) policy as a label, if it declared
22
+ one. A windowless policy refetches immediately, so it labels as plain `refetch`. */
23
23
  function policyLabel(entry: CacheEntry): string | undefined {
24
24
  const policy = entry.invalidation
25
25
  if (!policy) {
26
26
  return undefined
27
27
  }
28
28
  if (policy.debounce !== undefined) {
29
- return `swr debounce ${policy.debounce}ms`
29
+ return `refetch debounce ${policy.debounce}ms`
30
30
  }
31
31
  if (policy.throttle !== undefined) {
32
- return `swr throttle ${policy.throttle}ms`
32
+ return `refetch throttle ${policy.throttle}ms`
33
33
  }
34
- return 'swr'
34
+ return 'refetch'
35
35
  }
36
36
 
37
37
  function projectEntry(entry: CacheEntry, now: number): InspectorCacheEntry {
@@ -48,10 +48,11 @@ function projectEntry(entry: CacheEntry, now: number): InspectorCacheEntry {
48
48
  }
49
49
 
50
50
  /*
51
- Snapshots the process-level cache store (the persistent one `cache(fn, { shared:
52
- true, ttl: Infinity })` writes to) for the inspector. Read at call time, so it
53
- reflects the store as it stands; request-scoped stores are deliberately
54
- excluded — they're ephemeral and already visible as per-request cache tallies.
51
+ Snapshots the process-level cache store (the persistent one an endpoint declaring
52
+ `cache: { shared: true, ttl }` or a producer `cache(fn, args, { shared: true, ttl })`
53
+ writes to) for the inspector. Read at call time, so it reflects the store as it
54
+ stands; request-scoped stores are deliberately excluded — they're ephemeral and
55
+ already visible as per-request cache tallies.
55
56
  */
56
57
  export function buildCacheSnapshot(): InspectorCacheSnapshot {
57
58
  const now = Date.now()
@@ -1,5 +1,5 @@
1
- import { jsonSchemaForSchema } from '../../shared/jsonSchemaForSchema.ts'
2
1
  import { promptRegistry } from '../prompts/promptRegistry.ts'
2
+ import { resolveInputJsonSchema, resolveOutputJsonSchema } from '../rpc/resolveRpcJsonSchema.ts'
3
3
  import { rpcRegistry } from '../rpc/rpcRegistry.ts'
4
4
  import { socketOperations } from '../sockets/socketOperations.ts'
5
5
  import { socketRegistry } from '../sockets/socketRegistry.ts'
@@ -18,12 +18,14 @@ export function buildInspectorSurface(): InspectorSurface {
18
18
  url: entry.remote.url,
19
19
  method: entry.remote.method,
20
20
  clients: { ...entry.remote.clients },
21
- inputSchema: entry.inputSchema ? jsonSchemaForSchema(entry.inputSchema) : undefined,
22
- outputSchema: entry.outputSchema ? jsonSchemaForSchema(entry.outputSchema) : undefined,
21
+ // The declared inputSchema VALIDATOR wins; else the build-projected input-type schema
22
+ // (ADR-0030 input side); with neither the rpc advertises no input shape, as before.
23
+ inputSchema: resolveInputJsonSchema(entry),
24
+ outputSchema: resolveOutputJsonSchema(entry),
23
25
  files: entry.filesSchema !== undefined,
24
26
  timeout: entry.timeout,
25
27
  maxBodySize: entry.maxBodySize,
26
- crossOrigin: entry.crossOrigin,
28
+ crossOrigin: entry.remote.crossOrigin,
27
29
  }))
28
30
  const sockets = Array.from(socketRegistry.values()).map((entry) => ({
29
31
  name: entry.socket.name,
@@ -1,7 +1,10 @@
1
1
  import { carriesBodyArgs } from '../../shared/carriesBodyArgs.ts'
2
2
  import { commandNameForUrl } from '../../shared/commandNameForUrl.ts'
3
3
  import { jsonSchemaForSchema } from '../../shared/jsonSchemaForSchema.ts'
4
+ import type { ErrorJsonSchemas } from '../../shared/types/ErrorJsonSchemas.ts'
5
+ import { resolveInputJsonSchema, resolveOutputJsonSchema } from '../rpc/resolveRpcJsonSchema.ts'
4
6
  import { rpcRegistry } from '../rpc/rpcRegistry.ts'
7
+ import { STATUS_TEXT } from './STATUS_TEXT.ts'
5
8
 
6
9
  /*
7
10
  Turns a rpc's resolved JSON Schema into OpenAPI query parameters — one
@@ -43,6 +46,34 @@ function multipartBodySchema(textSchema: Record<string, unknown>): Record<string
43
46
  return schema
44
47
  }
45
48
 
49
+ /*
50
+ Adds one `responses[status]` entry per typed-error branch (ADR-0030) — its description is the
51
+ status's standard reason phrase, and its `application/json` content is the error's projected `data`
52
+ schema (omitted for a bare `{}`, a nullary error with no payload). An already-present status (the
53
+ 200, or any explicit response) is left untouched, so the success body and validation responses win a
54
+ collision. A no-op when the handler declared no typed errors (`errorJsonSchemas` undefined).
55
+ */
56
+ function mergeErrorResponses(
57
+ responses: Record<string, unknown>,
58
+ errorJsonSchemas: ErrorJsonSchemas | undefined,
59
+ ): void {
60
+ if (errorJsonSchemas === undefined) {
61
+ return
62
+ }
63
+ for (const [status, schema] of Object.entries(errorJsonSchemas)) {
64
+ if (responses[status] !== undefined) {
65
+ continue
66
+ }
67
+ const response: Record<string, unknown> = {
68
+ description: STATUS_TEXT[Number(status)] ?? 'Error',
69
+ }
70
+ if (Object.keys(schema).length > 0) {
71
+ response.content = { 'application/json': { schema } }
72
+ }
73
+ responses[status] = response
74
+ }
75
+ }
76
+
46
77
  /*
47
78
  Builds an OpenAPI 3.1 document from the rpc registry — the HTTP surface
48
79
  every rpc exposes regardless of which non-browser clients it advertises.
@@ -58,25 +89,38 @@ export function buildOpenApiSpec(info: {
58
89
  for (const entry of rpcRegistry.values()) {
59
90
  const url = entry.remote.url
60
91
  const method = entry.remote.method
61
- const jsonSchema = jsonSchemaForSchema(entry.inputSchema)
92
+ /*
93
+ Describe the parameters/request body from the `inputSchema` VALIDATOR when declared, else from
94
+ the handler's input parameter type projected to JSON Schema at build time (ADR-0030 input side —
95
+ `entry.inputJsonSchema`); otherwise the opaque fallback. The validator overrides the projection
96
+ (a runtime narrowing the type can't express), mirroring the 200-body output path below.
97
+ */
98
+ const jsonSchema = resolveInputJsonSchema(entry) ?? jsonSchemaForSchema(undefined)
62
99
  const description = jsonSchema.description as string | undefined
63
100
  /*
64
- When the rpc declares an `outputSchema`, describe the 200 body
65
- with it so external tooling sees the real return shape; otherwise
66
- fall back to a bare OK.
101
+ Describe the 200 body from the `outputSchema` VALIDATOR when declared, else from the handler
102
+ return type projected to JSON Schema at build time (ADR-0030 D2 — `entry.outputJsonSchema`);
103
+ otherwise fall back to a bare OK. The validator overrides the projection (a runtime narrowing
104
+ the type can't express).
67
105
  */
68
106
  const okResponse: Record<string, unknown> = { description: 'OK' }
69
- if (entry.outputSchema) {
70
- okResponse.content = {
71
- 'application/json': {
72
- schema: jsonSchemaForSchema(entry.outputSchema),
73
- },
74
- }
107
+ const outputSchema = resolveOutputJsonSchema(entry)
108
+ if (outputSchema) {
109
+ okResponse.content = { 'application/json': { schema: outputSchema } }
75
110
  }
111
+ /*
112
+ Merge the handler's typed-error branches (ADR-0030 — `entry.errorJsonSchemas`, a status-keyed
113
+ data-schema map baked from the `error.typed(...)` return branches) into the responses. Each
114
+ status gets a `responses[status]` entry describing the error's data payload; an existing
115
+ response (the 200, or a future explicit entry) is never clobbered. So the 200 success body and
116
+ every typed error's status + payload are documented from the one handler return type.
117
+ */
118
+ const responses: Record<string, unknown> = { '200': okResponse }
119
+ mergeErrorResponses(responses, entry.errorJsonSchemas)
76
120
  const operation: Record<string, unknown> = {
77
121
  operationId: commandNameForUrl(url),
78
122
  ...(description ? { description } : {}),
79
- responses: { '200': okResponse },
123
+ responses,
80
124
  }
81
125
  if (carriesBodyArgs(method)) {
82
126
  operation.requestBody = entry.filesSchema
@@ -18,7 +18,7 @@ const ROUTE_CHUNK = /"(\/[^"]*)"\s*:\s*\(\)\s*=>\s*import\("\.\/((?:page|layout)
18
18
  /* Reads a `_app` chunk's source by filename — gunzipped from the embedded asset map
19
19
  (standalone compile) or off disk (dev + `abide start`). Undefined on a miss. */
20
20
  function chunkReader(
21
- distDir: string,
21
+ appDir: string,
22
22
  assets?: Assets,
23
23
  ): (name: string) => Promise<string | undefined> {
24
24
  if (assets) {
@@ -28,23 +28,25 @@ function chunkReader(
28
28
  }
29
29
  }
30
30
  return async (name) => {
31
- const file = Bun.file(`${distDir}/_app/${name}`)
31
+ const file = Bun.file(`${appDir}/${name}`)
32
32
  return (await file.exists()) ? file.text() : undefined
33
33
  }
34
34
  }
35
35
 
36
- /* The hashed client entry filename (`client-<hash>.js`), from the asset map or disk. */
37
- async function findEntry(distDir: string, assets?: Assets): Promise<string | undefined> {
36
+ /* The hashed client entry filename (`client-<hash>.js`), from the asset map or disk.
37
+ Each build's `appDir` holds exactly one entry (production `_app`, or a dev
38
+ generation dir), so `.find` is unambiguous. */
39
+ async function findEntry(appDir: string, assets?: Assets): Promise<string | undefined> {
38
40
  const isEntry = (name: string): boolean => /^client-[a-z0-9]+\.js$/i.test(name)
39
41
  if (assets) {
40
42
  const key = Object.keys(assets).find((path) => isEntry(path.replace('/_app/', '')))
41
43
  return key?.replace('/_app/', '')
42
44
  }
43
- if (!existsSync(`${distDir}/_app`)) {
45
+ if (!existsSync(appDir)) {
44
46
  return undefined
45
47
  }
46
48
  const names = await Array.fromAsync(
47
- new Glob('client-*.js').scan({ cwd: `${distDir}/_app`, onlyFiles: true }),
49
+ new Glob('client-*.js').scan({ cwd: appDir, onlyFiles: true }),
48
50
  )
49
51
  return names.find(isEntry)
50
52
  }
@@ -64,14 +66,14 @@ stream-close instead of waterfalling after it. Returns an empty map when the bun
64
66
  absent or unparseable, so rendering degrades to the entry-only preload.
65
67
  */
66
68
  export async function buildPreloadManifest({
67
- distDir,
69
+ appDir,
68
70
  assets,
69
71
  }: {
70
- distDir: string
72
+ appDir: string
71
73
  assets?: Assets
72
74
  }): Promise<Record<string, string[]>> {
73
- const read = chunkReader(distDir, assets)
74
- const entryName = await findEntry(distDir, assets)
75
+ const read = chunkReader(appDir, assets)
76
+ const entryName = await findEntry(appDir, assets)
75
77
  const entrySource = entryName ? await read(entryName) : undefined
76
78
  if (entryName === undefined || entrySource === undefined) {
77
79
  return {}
@@ -1,11 +1,10 @@
1
- import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
2
- import { TEXT_PLAIN } from '../../shared/TEXT_PLAIN.ts'
3
1
  import { acceptsGzip } from './acceptsGzip.ts'
4
2
  import { cacheControlForAsset } from './cacheControlForAsset.ts'
5
3
  import { containsTraversal } from './containsTraversal.ts'
6
4
  import { createAssetHeaderCache } from './createAssetHeaderCache.ts'
7
5
  import { globToPathSet } from './globToPathSet.ts'
8
6
  import { respondWithEmbeddedAsset } from './respondWithEmbeddedAsset.ts'
7
+ import { textResponse } from './textResponse.ts'
9
8
  import type { Assets } from './types/Assets.ts'
10
9
 
11
10
  /*
@@ -24,23 +23,29 @@ Unlike the public server this answers every `/_app/` request itself (404 on
24
23
  a miss — nothing falls through past the build tree). The path-traversal
25
24
  guard inspects the raw request URL because the WHATWG parser normalizes
26
25
  encoded `..` segments away before `url.pathname` is visible.
26
+
27
+ `appDir` is the physical directory the `/_app/*` URLs map onto: `dist/_app` for
28
+ a production build, or — under `abide dev` — the generation dir this worker was
29
+ spawned on (`ABIDE_APP_DIR`). The URL space stays `/_app/*` either way; only the
30
+ disk location this worker reads from moves, so a rebuild's new worker reads a new
31
+ dir while this one keeps serving its own, immutable for its lifetime.
27
32
  */
28
33
  export async function createAppAssetServer({
29
- distDir,
34
+ appDir,
30
35
  assets,
31
36
  }: {
32
- distDir: string
37
+ appDir: string
33
38
  assets?: Assets
34
39
  }): Promise<(req: Request, url: URL) => Promise<Response>> {
35
40
  // Per-pathname asset header bundles, hashed-chunk-aware Cache-Control.
36
41
  const headersForAsset = createAssetHeaderCache(cacheControlForAsset)
37
- /* Boot snapshot of every disk `_app` path, mirroring createPublicAssetServer: the
38
- header cache keys on (request-controlled) pathnames, so building bundles for junk
39
- `/_app/*` probes would grow it without bound. A rebuild restarts the server (same
40
- restart `bun --watch` triggers on a code change), re-snapshotting. */
42
+ /* Boot snapshot of every disk chunk path (keyed by its `/_app/*` URL), mirroring
43
+ createPublicAssetServer: the header cache keys on (request-controlled) pathnames,
44
+ so building bundles for junk `/_app/*` probes would grow it without bound. A
45
+ rebuild spawns a fresh worker (against a fresh generation dir), re-snapshotting. */
41
46
  const diskPaths = assets
42
47
  ? new Set<string>()
43
- : await globToPathSet(`${distDir}/_app`, '**/*', (file) => `/_app/${file}`)
48
+ : await globToPathSet(appDir, '**/*', (file) => `/_app/${file}`)
44
49
  /* Derive the precompressed `.gz` sibling set from the single tree scan (it already
45
50
  includes the `.gz` files) — a gzip-capable client gets those bytes without
46
51
  on-the-fly compression. Keyed by the base path (the `.gz` suffix stripped). */
@@ -49,11 +54,7 @@ export async function createAppAssetServer({
49
54
  )
50
55
 
51
56
  // Fresh 404 per call — a Response body is single-use, so it can't be hoisted to a const.
52
- const notFound = (): Response =>
53
- new Response('Not Found', {
54
- status: 404,
55
- headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
56
- })
57
+ const notFound = (): Response => textResponse(404)
57
58
 
58
59
  return async function serveAppAsset(req, url) {
59
60
  if (containsTraversal(req.url)) {
@@ -88,7 +89,9 @@ export async function createAppAssetServer({
88
89
  return notFound()
89
90
  }
90
91
  const { base: baseHeaders, gzip: gzipHeaders } = headersForAsset(assetPath)
91
- const diskPath = distDir + assetPath
92
+ // Map the `/_app/<file>` URL onto the physical dir: strip the `/_app` prefix
93
+ // (5 chars) and join under appDir, which already IS the `_app` (or generation) dir.
94
+ const diskPath = appDir + assetPath.slice('/_app'.length)
92
95
  if (acceptsGzip(req) && diskGzipPaths.has(assetPath)) {
93
96
  return new Response(Bun.file(`${diskPath}.gz`), { headers: gzipHeaders })
94
97
  }
@@ -0,0 +1,145 @@
1
+ import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
2
+ import { matchRoute } from '../../shared/matchRoute.ts'
3
+ import { normalizePathname } from '../../shared/normalizePathname.ts'
4
+ import { parseRouteSegments } from '../../shared/parseRouteSegments.ts'
5
+ import type { Pages } from '../../ui/types/Pages.ts'
6
+ import type { RemoteRoutes } from '../rpc/types/RemoteRoutes.ts'
7
+ import type { createRouteDispatcher } from './createRouteDispatcher.ts'
8
+
9
+ /* A route handler pre-bound to one registered URL — the dispatcher's output. */
10
+ type RouteHandler = ReturnType<ReturnType<typeof createRouteDispatcher>>
11
+
12
+ /*
13
+ The URL-shape decision for one request, once the framework plumbing has passed
14
+ on it. A `handler` case (rpc, page, or the openapi doc) is dispatched through
15
+ the request scope; a `redirect` is returned verbatim; the asset cases name
16
+ which asset server serves the path, with `publicAsset` as the catch-all bucket
17
+ whose miss falls through to the 404 path. Keeping the decision data-only lets a
18
+ test assert it without booting Bun.serve — the effects (ALS dispatch, asset
19
+ serving, 404 render) stay in the fetch closure.
20
+ */
21
+ export type RouteResolution =
22
+ | { kind: 'redirect'; response: Response }
23
+ | { kind: 'handler'; handler: RouteHandler; params: Record<string, string> }
24
+ | { kind: 'appAsset' }
25
+ | { kind: 'publicAsset' }
26
+
27
+ /*
28
+ Owns the URL-shape decisions that used to sit inline in the Bun.serve fetch
29
+ closure: canonical-slash redirects (308), the openapi document's build +
30
+ memo, and the asset-precedence ordering. Route dispatch (rpc-vs-page-vs-404,
31
+ method matching) stays behind createRouteDispatcher; this resolver composes its
32
+ per-URL handlers and adds the request-URL → decision layer on top, so both the
33
+ redirect logic and the memo semantics become unit-testable without a listener.
34
+
35
+ App routes are matched AFTER the `/__abide/*` plumbing (a reserved namespace no
36
+ app route occupies) and BEFORE the root-level framework surfaces (the openapi
37
+ doc, `/_app/`, public/ files), so a page route shadows a same-path public file
38
+ — the precedence the Bun routes table used to impose implicitly, now pinned
39
+ here. rpc URLs are flat literals (always `/rpc/...`) resolved by direct lookup;
40
+ page URLs carry `[name]` / `[[name]]` / `[...rest]` segments resolved through
41
+ the shared matchRoute — the same matcher the client router runs — so params
42
+ decode and precedence agree across the sides by construction.
43
+ */
44
+ export function createAppRouteResolver({
45
+ pages,
46
+ rpc,
47
+ buildRouteHandler,
48
+ openApiPath,
49
+ buildOpenApiDocument,
50
+ }: {
51
+ pages: Pages
52
+ rpc: RemoteRoutes
53
+ buildRouteHandler: ReturnType<typeof createRouteDispatcher>
54
+ openApiPath: string
55
+ buildOpenApiDocument: () => Promise<Record<string, unknown>>
56
+ }): (req: Request, url: URL) => RouteResolution {
57
+ /* Handlers pre-bound per registered URL. rpc URLs dispatch by direct lookup;
58
+ page URLs resolve through matchRoute against pageRouteUrls below. */
59
+ const rpcHandlers = new Map<string, RouteHandler>()
60
+ for (const routeUrl of Object.keys(rpc)) {
61
+ rpcHandlers.set(routeUrl, buildRouteHandler(routeUrl))
62
+ }
63
+ const pageHandlers = new Map<string, RouteHandler>()
64
+ for (const routeUrl of Object.keys(pages)) {
65
+ /* A `[...rest]` consumes every remaining segment, so segments after it
66
+ can never constrain matching — the route would silently serve paths
67
+ it shouldn't. Fail at boot instead. */
68
+ const segments = parseRouteSegments(routeUrl)
69
+ const catchAllIndex = segments.findIndex(
70
+ (segment) => segment.kind === 'param' && segment.catchAll,
71
+ )
72
+ if (catchAllIndex !== -1 && catchAllIndex !== segments.length - 1) {
73
+ throw new Error(
74
+ `[abide] invalid page route ${routeUrl}: a [...name] catch-all must be the last segment`,
75
+ )
76
+ }
77
+ pageHandlers.set(routeUrl, buildRouteHandler(routeUrl))
78
+ }
79
+ const pageRouteUrls = Object.keys(pages)
80
+
81
+ /* Built on first request, then reused — the rpc registry is frozen after load.
82
+ Memoised as a promise so two concurrent cold requests share one build instead
83
+ of both building (the second otherwise clobbering the first). A rejected build
84
+ clears the memo so the next request retries rather than caching the failure. */
85
+ let openApiSpec: Promise<Record<string, unknown>> | undefined
86
+ const openApiHandler: RouteHandler = async () => {
87
+ openApiSpec ??= buildOpenApiDocument().catch((error) => {
88
+ // Don't cache a failed build — clear the memo so a later request
89
+ // retries instead of 500-ing forever.
90
+ openApiSpec = undefined
91
+ throw error
92
+ })
93
+ return Response.json(await openApiSpec, { headers: { 'Cache-Control': NO_STORE } })
94
+ }
95
+
96
+ return function resolveAppRoute(_req, url): RouteResolution {
97
+ const rpcHandler = rpcHandlers.get(url.pathname)
98
+ if (rpcHandler) {
99
+ return { kind: 'handler', handler: rpcHandler, params: {} }
100
+ }
101
+ /*
102
+ Pages match only in canonical slash form; a non-canonical request
103
+ (`/admin/`, `//admin`) that would match is 308'd to the canonical URL
104
+ instead of served. Serving it directly would hand app.handle — the auth
105
+ seam — a pathname the matcher silently normalized away, so an exact-match
106
+ guard on `/admin` never sees the request it's guarding (the old Bun routes
107
+ table 404'd these forms; the redirect keeps the guard sound AND the URL
108
+ friendly). rpc URLs stay exact-match strict, as they always were.
109
+ */
110
+ const canonicalPathname = normalizePathname(url.pathname)
111
+ if (canonicalPathname !== url.pathname) {
112
+ if (matchRoute(pageRouteUrls, canonicalPathname)) {
113
+ return {
114
+ kind: 'redirect',
115
+ response: new Response(null, {
116
+ status: 308,
117
+ headers: {
118
+ Location: `${canonicalPathname}${url.search}`,
119
+ 'Cache-Control': NO_STORE,
120
+ },
121
+ }),
122
+ }
123
+ }
124
+ } else {
125
+ const matchedPage = matchRoute(pageRouteUrls, url.pathname)
126
+ if (matchedPage) {
127
+ const pageHandler = pageHandlers.get(matchedPage.route)
128
+ if (pageHandler) {
129
+ return { kind: 'handler', handler: pageHandler, params: matchedPage.params }
130
+ }
131
+ }
132
+ }
133
+ if (url.pathname === openApiPath) {
134
+ return { kind: 'handler', handler: openApiHandler, params: {} }
135
+ }
136
+ /* Static assets sidestep ALS + the per-request CacheStore + app.handle
137
+ (see the fetch closure); `/_app/` build output takes precedence over
138
+ public/ files, and anything else falls to the public bucket whose miss
139
+ becomes the 404. */
140
+ if (url.pathname.startsWith('/_app/')) {
141
+ return { kind: 'appAsset' }
142
+ }
143
+ return { kind: 'publicAsset' }
144
+ }
145
+ }