@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,36 @@
1
+ import ts from 'typescript'
2
+
3
+ /* Top-level declared names of a code block — a nested `<script>`'s locals, or a snippet's params
4
+ parsed as a destructuring pattern. Best-effort; on a parse miss returns empty (fail-open on the
5
+ binder side is safe: a script's names only ADD conservatism to a prefix-evaluability check when
6
+ present). Shared by the await-flight and child-render-flight classifiers (ADR-0034 / ADR-0037). */
7
+ export function declaredNames(code: string): Set<string> {
8
+ const names = new Set<string>()
9
+ let source: ts.SourceFile
10
+ try {
11
+ source = ts.createSourceFile('script.ts', code, ts.ScriptTarget.Latest, true)
12
+ } catch {
13
+ return names
14
+ }
15
+ const collectBinding = (name: ts.BindingName): void => {
16
+ if (ts.isIdentifier(name)) {
17
+ names.add(name.text)
18
+ return
19
+ }
20
+ for (const element of name.elements) {
21
+ if (ts.isBindingElement(element)) {
22
+ collectBinding(element.name)
23
+ }
24
+ }
25
+ }
26
+ for (const statement of source.statements) {
27
+ if (ts.isVariableStatement(statement)) {
28
+ for (const declaration of statement.declarationList.declarations) {
29
+ collectBinding(declaration.name)
30
+ }
31
+ } else if (ts.isFunctionDeclaration(statement) && statement.name !== undefined) {
32
+ names.add(statement.name.text)
33
+ }
34
+ }
35
+ return names
36
+ }
@@ -1,9 +1,152 @@
1
1
  import ts from 'typescript'
2
+ import { assignmentTargetNames } from './assignmentTargetNames.ts'
2
3
  import { type ReactiveImportBindings, reactiveImportBindings } from './resolveReactiveExport.ts'
3
4
  import { signalCallee } from './signalCallee.ts'
5
+ import type { InterpolationKind } from './types/InterpolationKind.ts'
6
+ import type { SeedTypeClassifier } from './types/SeedTypeClassifier.ts'
7
+
8
+ /* The routing decision for a no-marker `computed(seed)`: `true` routes to the eager
9
+ `trackedComputed` stream cell (read via `$$readCell`, the `cellReadNames` bucket), `false`
10
+ to the lazy `derive` doc slot (read as `name()`, the `computedNames` bucket). Shared by the
11
+ name-collection pass and the lowering so both land the binding in the identical bucket. */
12
+ type EagerStreamPredicate = (declaration: ts.VariableDeclaration) => boolean
4
13
 
5
14
  const factory = ts.factory
6
15
 
16
+ /* True when `node` holds an `await` that runs at the seed's own top level — one NOT nested
17
+ inside a further function. The walk stops at every nested function boundary, so an
18
+ `await` in an inner callback (`items.map(async (x) => await f(x))`) does not count: only
19
+ a top-level `await` marks the seed itself as an async thunk the async-cell path unwraps. */
20
+ function hasTopLevelAwait(node: ts.Node): boolean {
21
+ let found = false
22
+ const visit = (child: ts.Node): void => {
23
+ if (found) {
24
+ return
25
+ }
26
+ /* A nested function is its own await scope — don't descend into it. */
27
+ if (
28
+ ts.isFunctionDeclaration(child) ||
29
+ ts.isFunctionExpression(child) ||
30
+ ts.isArrowFunction(child)
31
+ ) {
32
+ return
33
+ }
34
+ if (ts.isAwaitExpression(child)) {
35
+ found = true
36
+ return
37
+ }
38
+ ts.forEachChild(child, visit)
39
+ }
40
+ visit(node)
41
+ return found
42
+ }
43
+
44
+ /* Normalises a `computed`/`linked` argument into a seed THUNK: a literal `() => …` /
45
+ `function` argument passes through unchanged (the author already wrote the thunk), any
46
+ other expression is wrapped as `() => arg`. The wrapper arrow is made ASYNC when the
47
+ expression contains a top-level `await` (`computed(await load())` → `async () => await
48
+ load()`), which is exactly the marker the runtime primitive uses to route to an async cell. */
49
+ function wrapSeed(argument: ts.Expression): ts.Expression {
50
+ if (ts.isArrowFunction(argument) || ts.isFunctionExpression(argument)) {
51
+ return argument
52
+ }
53
+ const modifiers = hasTopLevelAwait(argument)
54
+ ? [factory.createModifier(ts.SyntaxKind.AsyncKeyword)]
55
+ : undefined
56
+ return factory.createArrowFunction(
57
+ modifiers,
58
+ undefined,
59
+ [],
60
+ undefined,
61
+ factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
62
+ argument,
63
+ )
64
+ }
65
+
66
+ /* True for an async seed thunk — an arrow/function carrying the `async` modifier, whether
67
+ from `wrapSeed`'s `await` lowering or a passthrough `async () => …` literal the author
68
+ wrote. The routing signal: an async seed becomes an async cell (read via `$$readCell`),
69
+ a sync seed stays the lazy `derive` computed. */
70
+ function isAsyncSeed(node: ts.Expression): boolean {
71
+ if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) {
72
+ return false
73
+ }
74
+ const modifiers = ts.getModifiers(node)
75
+ return modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) ?? false
76
+ }
77
+
78
+ /* The seed argument of a `computed`/`linked` declaration (undefined for the arg-less
79
+ `computed()` edge). Reused by both the name-collection pass and the lowering. */
80
+ function seedArgument(declaration: ts.VariableDeclaration): ts.Expression | undefined {
81
+ return (declaration.initializer as ts.CallExpression).arguments[0]
82
+ }
83
+
84
+ /* True when a `computed(...)` declaration's wrapped seed is async — its await-lowered or
85
+ passthrough-async thunk routes to `scope().computed(...)` (an async cell) instead of the
86
+ lazy `scope().derive(...)`. The one predicate the collection pass and the lowering share. */
87
+ function isAsyncComputed(declaration: ts.VariableDeclaration): boolean {
88
+ const argument = seedArgument(declaration)
89
+ if (argument === undefined) {
90
+ return false
91
+ }
92
+ return isAsyncSeed(wrapSeed(argument))
93
+ }
94
+
95
+ /* True for a SYNC `computed(...)` seed that is a bare CALL or IDENTIFIER — `computed(getStream())`
96
+ / `computed(ref)`, the shape that may produce a stream/promise source. These route to the
97
+ eager `scope().trackedComputed(...)` (which probes its seed and auto-tracks a
98
+ `NamedAsyncIterable`), read via `$$readCell`; an arithmetic/member/literal seed stays on the
99
+ lazy `derive` path (read as `name()`). Excludes the async case (`isAsyncComputed` owns it) and
100
+ a literal `() => …` thunk (a direct thunk the author wrote — left lazy on `derive`). */
101
+ function isBareCallComputed(declaration: ts.VariableDeclaration): boolean {
102
+ const argument = seedArgument(declaration)
103
+ if (argument === undefined || isAsyncSeed(wrapSeed(argument))) {
104
+ return false
105
+ }
106
+ return ts.isCallExpression(argument) || ts.isIdentifier(argument)
107
+ }
108
+
109
+ /* Emits a compile WARNING (best-effort, never fatal) when a signal read appears AFTER the
110
+ first top-level `await` in an async seed — a value read there is no longer tracked, so the
111
+ cell won't reseed when it changes. Detection only: reads before the await (or with no
112
+ await) are fine. */
113
+ function warnPostAwaitReads(seed: ts.Expression, signalNames: ReadonlySet<string>): void {
114
+ if (signalNames.size === 0) {
115
+ return
116
+ }
117
+ /* Walk the seed's BODY — the arrow/function itself is the seed, not a nested scope, so
118
+ descend into it; the guard below then skips any FURTHER nested function. */
119
+ const body = ts.isArrowFunction(seed) || ts.isFunctionExpression(seed) ? seed.body : seed
120
+ let awaited = false
121
+ const flagged = new Set<string>()
122
+ const visit = (child: ts.Node): void => {
123
+ /* Nested functions have their own tracking; the await that matters is at the seed's
124
+ own top level, so don't descend into them (mirrors `hasTopLevelAwait`). */
125
+ if (
126
+ ts.isFunctionDeclaration(child) ||
127
+ ts.isFunctionExpression(child) ||
128
+ ts.isArrowFunction(child)
129
+ ) {
130
+ return
131
+ }
132
+ if (ts.isAwaitExpression(child)) {
133
+ awaited = true
134
+ ts.forEachChild(child, visit)
135
+ return
136
+ }
137
+ if (awaited && ts.isIdentifier(child) && signalNames.has(child.text)) {
138
+ flagged.add(child.text)
139
+ }
140
+ ts.forEachChild(child, visit)
141
+ }
142
+ visit(body)
143
+ for (const name of flagged) {
144
+ console.warn(
145
+ `[abide] \`${name}\` is read after an \`await\` in an async computed/linked seed — reads after the first await are not tracked, so the cell will not reseed when \`${name}\` changes. Read it before the await (capture it into a local) to keep it tracked.`,
146
+ )
147
+ }
148
+ }
149
+
7
150
  /* Throws on the removed `prop(...)` reader — props are now read by destructuring `props()`.
8
151
  Walks all calls, so a stray one nested in a function is caught too. Bare reactive
9
152
  primitives (`state(0)`) are the SURFACE now (recognised by import binding + lowered), so
@@ -44,13 +187,48 @@ between passes. Plain `state` becomes `model.x` access that `lowerDocAccess` low
44
187
  patches/reads. No reactive declarations → an identity transformer (the explicit
45
188
  `const model = doc(...)` form still works).
46
189
  */
47
- export function desugarSignals(source: ts.SourceFile): {
190
+ export function desugarSignals(
191
+ source: ts.SourceFile,
192
+ /* Synthetic `const __cN = computed(...)` cells `analyzeComponent` injected for asyncIterable
193
+ interpolations (ADR-0019 Stage D). Their `computed` callee is UNIMPORTED, so import
194
+ resolution (`signalCallee`) can't recognize them — instead a declaration whose name is in
195
+ this set is treated as a bare-call computed slot: routed to an eager `trackedComputed`
196
+ stream cell and read via `$$readCell`, exactly as an explicit `state.computed(getStream())`
197
+ would be. Gated on the exact injected name (never a naming heuristic), so it can never
198
+ reclassify author code — an author's own `computed(...)` still needs its import. */
199
+ injectedCellNames: ReadonlySet<string> = new Set(),
200
+ /* The subset of `injectedCellNames` whose seed the author wrote `await` on (ADR-0032): a
201
+ BLOCKING peek-cell (`scope().trackedComputed(async …, false)` — an `AsyncComputed` that
202
+ joins the SSR barrier), vs a streaming one (`trackedComputed(async …, true)` — ships
203
+ pending, resolves on the client). Names not in this set are streaming. */
204
+ blockingCellNames: ReadonlySet<string> = new Set(),
205
+ /* Names the TEMPLATE writes (assigned in an event expression, or forwarded as a `bind:`
206
+ target) — see `writtenTemplateNames`. Unioned with the script's own write scan; a `props()`
207
+ binding whose name lands in the union is a WRITABLE prop (a `.value` cell via `bindableProp`)
208
+ rather than a read-only derive, so a two-way `bind:prop` on the parent can flow back. */
209
+ templateWrittenNames: ReadonlySet<string> = new Set(),
210
+ /* Type-directed seed classifier (ADR-0023): resolves a no-marker `computed(seed)`'s
211
+ async-ness from the seed's checker type via the warm shadow program. Absent (no warm
212
+ program, or any call site outside the shadow-warmed path) ⇒ every seed classifies
213
+ `undefined` ⇒ fail-open to the `isBareCallComputed` syntax heuristic — exactly today's
214
+ routing. The `await`-marker path (`isAsyncComputed`) is decided first and never consults it. */
215
+ seedClassify?: SeedTypeClassifier,
216
+ /* Absolute `.abide` source offset where this script's (trimmed) body begins — the base that
217
+ relocates a seed node's body-relative `getStart()` back to an ORIGINAL source location the
218
+ shadow's source→shadow `mappings` resolve. Unused when `seedClassify` is absent. */
219
+ scriptBase = 0,
220
+ ): {
48
221
  transformer: ts.TransformerFactory<ts.SourceFile>
49
222
  stateNames: Set<string>
50
223
  derivedNames: Set<string>
51
224
  computedNames: Set<string>
225
+ cellReadNames: Set<string>
52
226
  } {
53
227
  assertNoRemovedReaders(source)
228
+ /* The full set of names written anywhere (this script + the template). A prop in this set
229
+ is upgraded to a writable cell; a prop only read stays a cheap read-only derive. */
230
+ const writtenNames = new Set<string>(templateWrittenNames)
231
+ assignmentTargetNames(source, writtenNames)
54
232
  /* The file's reactive import bindings — each local (alias-safe) mapped to its
55
233
  canonical primitive. The single recognition authority: every callee below resolves
56
234
  against these import bindings and nothing else. */
@@ -58,9 +236,47 @@ export function desugarSignals(source: ts.SourceFile): {
58
236
  const stateNames = new Set<string>()
59
237
  const derivedNames = new Set<string>()
60
238
  const computedNames = new Set<string>()
239
+ /* Names read through `$$readCell(name)`: every `linked` (a plain `State` or, when its seed
240
+ tracks an async source, an `AsyncState`) and every async `computed` (an `AsyncComputed`).
241
+ One read shape covers both — `$$readCell` peeks an async cell and reads `.value` off a
242
+ sync one — so `linked(getStream())` auto-tracks with no read-site branching. */
243
+ const cellReadNames = new Set<string>()
61
244
  /* A `props()` destructure must be lowered even when it declares no reactive binding
62
245
  (a rest-only `const { ...rest } = props()`), so track its presence on its own. */
63
246
  let hasPropsDestructure = false
247
+ /* Resolves a computed seed's async-ness through the warm shadow classifier (ADR-0023):
248
+ maps the seed's absolute source location to its checker-type kind. `undefined` when no
249
+ classifier is threaded OR on any resolution failure (a throw / an unmapped seed), so the
250
+ caller degrades to `isBareCallComputed` — never mistakes a failure for a `sync` type. */
251
+ const seedKind = (seed: ts.Expression): InterpolationKind | undefined => {
252
+ if (seedClassify === undefined) {
253
+ return undefined
254
+ }
255
+ try {
256
+ const start = seed.getStart(source)
257
+ return seedClassify(scriptBase + start, source.text.slice(start, seed.getEnd()))
258
+ } catch {
259
+ return undefined
260
+ }
261
+ }
262
+ /* The single routing authority for a no-marker `computed(seed)` — shared by the
263
+ name-collection pass below and `computedStatements` so both land the binding in the same
264
+ read-name bucket (a divergence would lower a reference to the wrong read form). Type-
265
+ directed when the seed classifier resolves the seed (`asyncIterable` → the eager stream
266
+ cell; `promise`/`sync` → the lazy `derive` slot), fail-open to today's `isBareCallComputed`
267
+ syntax heuristic otherwise. The `await`-marker seed (`isAsyncComputed`) is decided first by
268
+ each caller and excluded here (its wrapped thunk is async → returns false). */
269
+ const isEagerStreamComputed: EagerStreamPredicate = (declaration) => {
270
+ const argument = seedArgument(declaration)
271
+ if (argument === undefined || isAsyncSeed(wrapSeed(argument))) {
272
+ return false
273
+ }
274
+ const kind = seedKind(argument)
275
+ if (kind !== undefined) {
276
+ return kind === 'asyncIterable'
277
+ }
278
+ return isBareCallComputed(declaration)
279
+ }
64
280
  for (const statement of source.statements) {
65
281
  if (!ts.isVariableStatement(statement)) {
66
282
  continue
@@ -78,25 +294,48 @@ export function desugarSignals(source: ts.SourceFile): {
78
294
  }
79
295
  hasPropsDestructure = true
80
296
  for (const binding of propsDestructure(declaration).bindings) {
81
- computedNames.add(binding.local)
297
+ /* A prop the component writes/forwards becomes a `.value` cell (`bindableProp`,
298
+ read/written like `state(x, transform)` → `derivedNames`); one only read stays
299
+ a read-only derive read as `name()` (`computedNames`). */
300
+ if (writtenNames.has(binding.local)) {
301
+ derivedNames.add(binding.local)
302
+ } else {
303
+ computedNames.add(binding.local)
304
+ }
82
305
  }
83
306
  continue
84
307
  }
85
308
  if (!ts.isIdentifier(declaration.name)) {
86
309
  continue
87
310
  }
311
+ if (injectedCellNames.has(declaration.name.text)) {
312
+ /* Injected asyncIterable-interpolation cell (`const __cN = computed(expr)`): an
313
+ eager `trackedComputed` stream cell, read via `$$readCell` — recognized by its
314
+ injected name since `computed` is unimported here. */
315
+ cellReadNames.add(declaration.name.text)
316
+ continue
317
+ }
88
318
  if (isPlainStateSlot(declaration, bindings)) {
89
319
  /* Plain `state(initial)` → a serializable `model` doc slot. */
90
320
  stateNames.add(declaration.name.text)
91
321
  } else if (isComputedSlot(declaration, bindings)) {
92
- /* Read-only `computed(compute)` → a computed `scope().derive` doc slot,
93
- referenced as `name()` (its string-free reader): a function of other
94
- paths, recomputed via the graph, never stored/serialized. */
95
- computedNames.add(declaration.name.text)
96
- } else if (callee === 'linked' || callee === 'state') {
97
- /* `.value` cells: `linked` and `state(initial, transform)` — they own
98
- a local store, so they stay cells (`computed` is always the read-only
99
- slot above; there is no writable-computed cell). */
322
+ /* `computed(compute)` → either a lazy `scope().derive` doc slot referenced as
323
+ `name()` (a sync/promise seed), or an eager cell read via `$$readCell(name)`
324
+ when the wrapped seed is async (an `await`-lowered / passthrough-`async` thunk)
325
+ or its seed type resolves to a stream (`isEagerStreamComputed`, ADR-0023). */
326
+ if (isAsyncComputed(declaration) || isEagerStreamComputed(declaration)) {
327
+ cellReadNames.add(declaration.name.text)
328
+ } else {
329
+ computedNames.add(declaration.name.text)
330
+ }
331
+ } else if (callee === 'linked') {
332
+ /* `linked` → a cell read via `$$readCell(name)`: a plain `State` when the seed
333
+ is synchronous, an `AsyncState` when it tracks a promise/stream — one read
334
+ shape auto-tracks whichever source the runtime primitive resolved to. */
335
+ cellReadNames.add(declaration.name.text)
336
+ } else if (callee === 'state') {
337
+ /* `state(initial, transform)` → a `.value` cell (its write-coercion transform
338
+ forces a local store); referenced as `name.value`, unchanged. */
100
339
  derivedNames.add(declaration.name.text)
101
340
  }
102
341
  }
@@ -106,6 +345,7 @@ export function desugarSignals(source: ts.SourceFile): {
106
345
  stateNames.size > 0 ||
107
346
  derivedNames.size > 0 ||
108
347
  computedNames.size > 0 ||
348
+ cellReadNames.size > 0 ||
109
349
  hasPropsDestructure
110
350
 
111
351
  const transformer: ts.TransformerFactory<ts.SourceFile> = () => (root) => {
@@ -123,13 +363,31 @@ export function desugarSignals(source: ts.SourceFile): {
123
363
  ),
124
364
  )
125
365
  }
366
+ /* Every component signal name — the read set the post-`await` tracking lint checks
367
+ an async seed's body against. */
368
+ const signalNames = new Set<string>([
369
+ ...stateNames,
370
+ ...derivedNames,
371
+ ...computedNames,
372
+ ...cellReadNames,
373
+ ])
126
374
  for (const statement of root.statements) {
127
- statements.push(...loweredStatement(statement, bindings))
375
+ statements.push(
376
+ ...loweredStatement(
377
+ statement,
378
+ bindings,
379
+ signalNames,
380
+ injectedCellNames,
381
+ blockingCellNames,
382
+ writtenNames,
383
+ isEagerStreamComputed,
384
+ ),
385
+ )
128
386
  }
129
387
  return factory.updateSourceFile(root, statements)
130
388
  }
131
389
 
132
- return { transformer, stateNames, derivedNames, computedNames }
390
+ return { transformer, stateNames, derivedNames, computedNames, cellReadNames }
133
391
  }
134
392
 
135
393
  /* The lowered form of a top-level statement: state slots → `model.x = init`
@@ -139,16 +397,69 @@ export function desugarSignals(source: ts.SourceFile): {
139
397
  function loweredStatement(
140
398
  statement: ts.Statement,
141
399
  bindings: ReactiveImportBindings,
400
+ signalNames: ReadonlySet<string>,
401
+ injectedCellNames: ReadonlySet<string>,
402
+ blockingCellNames: ReadonlySet<string>,
403
+ writtenNames: ReadonlySet<string>,
404
+ isEagerStreamComputed: EagerStreamPredicate,
142
405
  ): ts.Statement[] {
143
406
  rejectMixedDeclaration(statement, bindings)
144
407
  return (
408
+ injectedComputedStatements(statement, injectedCellNames, blockingCellNames) ??
145
409
  stateAssignmentStatements(statement, bindings) ??
146
- computedStatements(statement, bindings) ??
147
- propsStatements(statement, bindings) ??
148
- cellStatements(statement, bindings) ?? [statement]
410
+ computedStatements(statement, bindings, signalNames, isEagerStreamComputed) ??
411
+ propsStatements(statement, bindings, writtenNames) ??
412
+ cellStatements(statement, bindings, signalNames) ?? [statement]
149
413
  )
150
414
  }
151
415
 
416
+ /* Lowers a synthetic `const __vN = computed(<seed>)` (an async (sub)expression peek-cell
417
+ `analyzeComponent` injected, ADR-0032) to a `scope().trackedComputed(...)` cell read via
418
+ `$$readCell`. Two shapes, by the seed:
419
+ - a PROMISE seed (`computed(await X)` → an async `() => await X` thunk) → `trackedComputed(
420
+ thunk, <streaming>)`, where `streaming = !blockingCellNames.has(name)`: a blocking cell
421
+ (author `await`) joins the SSR barrier and resolves inline; a streaming one ships pending.
422
+ - an `AsyncIterable` seed (`computed(getStream())` → a bare `() => getStream()` thunk) →
423
+ `trackedComputed(thunk)`, byte-identical to an explicit `state.computed(getStream())`.
424
+ Recognized by the injected name (`computed` is unimported here); returns undefined for any
425
+ statement that is not one of these injected cells (each is its own single-declaration statement). */
426
+ function injectedComputedStatements(
427
+ statement: ts.Statement,
428
+ injectedCellNames: ReadonlySet<string>,
429
+ blockingCellNames: ReadonlySet<string>,
430
+ ): ts.Statement[] | undefined {
431
+ if (!ts.isVariableStatement(statement)) {
432
+ return undefined
433
+ }
434
+ const statements: ts.Statement[] = []
435
+ for (const declaration of statement.declarationList.declarations) {
436
+ if (!ts.isIdentifier(declaration.name) || !injectedCellNames.has(declaration.name.text)) {
437
+ return undefined
438
+ }
439
+ const name = declaration.name.text
440
+ const argument = seedArgument(declaration)
441
+ const wrapped =
442
+ argument === undefined
443
+ ? factory.createArrowFunction(
444
+ undefined,
445
+ undefined,
446
+ [],
447
+ undefined,
448
+ factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
449
+ factory.createIdentifier('undefined'),
450
+ )
451
+ : wrapSeed(argument)
452
+ /* A promise seed (async thunk) unwraps to the resolved value and carries the SSR tier flag;
453
+ a stream seed (bare thunk) probes to a frame cell and needs no flag (a stream never joins
454
+ the barrier), so it stays a single-arg call to byte-match the explicit form. */
455
+ const args = isAsyncSeed(wrapped)
456
+ ? [wrapped, blockingCellNames.has(name) ? factory.createFalse() : factory.createTrue()]
457
+ : [wrapped]
458
+ statements.push(constDeclaration(name, scopeMethodCall('trackedComputed', args)))
459
+ }
460
+ return statements
461
+ }
462
+
152
463
  /* Each lowering function above is all-or-nothing per VariableStatement: it returns
153
464
  undefined the moment one declaration doesn't match its kind, so the whole statement
154
465
  passes through verbatim. A statement mixing a reactive declaration with a differently-
@@ -243,6 +554,7 @@ function isComputedSlot(
243
554
  function cellStatements(
244
555
  statement: ts.Statement,
245
556
  bindings: ReactiveImportBindings,
557
+ signalNames: ReadonlySet<string>,
246
558
  ): ts.Statement[] | undefined {
247
559
  if (!ts.isVariableStatement(statement)) {
248
560
  return undefined
@@ -258,7 +570,23 @@ function cellStatements(
258
570
  return undefined
259
571
  }
260
572
  const args = (declaration.initializer as ts.CallExpression).arguments
261
- statements.push(constDeclaration(declaration.name.text, scopeMethodCall(callee, args)))
573
+ /* `linked` wraps its seed argument (a bare value/`await`/stream expression becomes a
574
+ thunk, a literal `() => …` passes through) so the runtime primitive can route it to
575
+ an async cell; `state` is a value-taker — its args pass verbatim, never wrapped. */
576
+ if (callee === 'linked' && args[0] !== undefined) {
577
+ const wrapped = wrapSeed(args[0])
578
+ if (isAsyncSeed(wrapped)) {
579
+ warnPostAwaitReads(wrapped, signalNames)
580
+ }
581
+ statements.push(
582
+ constDeclaration(
583
+ declaration.name.text,
584
+ scopeMethodCall('linked', [wrapped, ...args.slice(1)]),
585
+ ),
586
+ )
587
+ } else {
588
+ statements.push(constDeclaration(declaration.name.text, scopeMethodCall(callee, args)))
589
+ }
262
590
  }
263
591
  return statements
264
592
  }
@@ -268,6 +596,8 @@ function cellStatements(
268
596
  function computedStatements(
269
597
  statement: ts.Statement,
270
598
  bindings: ReactiveImportBindings,
599
+ signalNames: ReadonlySet<string>,
600
+ isEagerStreamComputed: EagerStreamPredicate,
271
601
  ): ts.Statement[] | undefined {
272
602
  if (!ts.isVariableStatement(statement)) {
273
603
  return undefined
@@ -282,22 +612,40 @@ function computedStatements(
282
612
  return undefined
283
613
  }
284
614
  const name = declaration.name.text
285
- const compute =
286
- (declaration.initializer as ts.CallExpression).arguments[0] ??
287
- factory.createArrowFunction(
288
- undefined,
289
- undefined,
290
- [],
291
- undefined,
292
- factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
293
- factory.createIdentifier('undefined'),
615
+ const argument = seedArgument(declaration)
616
+ /* The seed thunk: a wrapped argument (bare expr / `await` / literal thunk), or an arg-
617
+ less `computed()` degenerating to `() => undefined`. */
618
+ const wrapped =
619
+ argument === undefined
620
+ ? factory.createArrowFunction(
621
+ undefined,
622
+ undefined,
623
+ [],
624
+ undefined,
625
+ factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
626
+ factory.createIdentifier('undefined'),
627
+ )
628
+ : wrapSeed(argument)
629
+ if (isAsyncSeed(wrapped)) {
630
+ /* Async seed → the eager `computed` primitive (an `AsyncComputed` cell, read via
631
+ `$$readCell`); the runtime unwraps the promise. */
632
+ warnPostAwaitReads(wrapped, signalNames)
633
+ statements.push(constDeclaration(name, scopeMethodCall('computed', [wrapped])))
634
+ } else if (isEagerStreamComputed(declaration)) {
635
+ /* Stream seed → the eager `trackedComputed`, which probes the seed and auto-tracks a
636
+ stream (`AsyncComputed`) or falls back to a lazy computed; read via `$$readCell`.
637
+ Reached when the seed type resolves to `asyncIterable` (ADR-0023) or, fail-open with
638
+ no warm program, when it is a bare call/identifier (`isBareCallComputed`). */
639
+ statements.push(constDeclaration(name, scopeMethodCall('trackedComputed', [wrapped])))
640
+ } else {
641
+ /* Sync arithmetic/member/literal seed → the lazy `derive` doc slot, read as `name()`. */
642
+ statements.push(
643
+ constDeclaration(
644
+ name,
645
+ scopeMethodCall('derive', [factory.createStringLiteral(name), wrapped]),
646
+ ),
294
647
  )
295
- statements.push(
296
- constDeclaration(
297
- name,
298
- scopeMethodCall('derive', [factory.createStringLiteral(name), compute]),
299
- ),
300
- )
648
+ }
301
649
  }
302
650
  return statements
303
651
  }
@@ -374,14 +722,18 @@ function propsBindingKey(element: ts.BindingElement): string {
374
722
  throw new Error('abide: computed prop keys in `props()` destructuring are not supported')
375
723
  }
376
724
 
377
- /* If `statement` is a `const {…, ...rest} = props()` destructure, returns one reactive
378
- computed per named binding — `scope().derive("name", () => $props["key"]?.() ?? default)`,
379
- read as `name()` plus a `const rest = restProps($props, [consumed])` for a rest
380
- binding; otherwise undefined. The `?? default` applies the binding's `= default`
381
- fallback when the prop is absent. */
725
+ /* If `statement` is a `const {…, ...rest} = props()` destructure, returns one binding per
726
+ named propplus a `const rest = restProps($props, [consumed])` for a rest binding;
727
+ otherwise undefined. A prop the component only READS is a read-only reactive computed
728
+ (`scope().derive("name", () => $props["key"]?.() ?? default)`, read as `name()`). A prop
729
+ it WRITES or forwards (in `writtenNames`) is a writable `.value` cell instead
730
+ (`$$bindableProp($props, "key", () => default)`, read/written as `name.value`), so a
731
+ two-way `bind:prop` on the parent flows back. The `?? default` / `fallback` thunk applies
732
+ the binding's `= default` when the prop is absent. */
382
733
  function propsStatements(
383
734
  statement: ts.Statement,
384
735
  bindings: ReactiveImportBindings,
736
+ writtenNames: ReadonlySet<string>,
385
737
  ): ts.Statement[] | undefined {
386
738
  if (!ts.isVariableStatement(statement)) {
387
739
  return undefined
@@ -396,6 +748,36 @@ function propsStatements(
396
748
  }
397
749
  const { bindings: propBindings, rest } = propsDestructure(declaration)
398
750
  for (const { local, key, initializer } of propBindings) {
751
+ /* A written/forwarded prop → `$$bindableProp($props, "key", () => default)`. */
752
+ if (writtenNames.has(local)) {
753
+ const args: ts.Expression[] = [
754
+ factory.createIdentifier('$props'),
755
+ factory.createStringLiteral(key),
756
+ ]
757
+ if (initializer !== undefined) {
758
+ args.push(
759
+ factory.createArrowFunction(
760
+ undefined,
761
+ undefined,
762
+ [],
763
+ undefined,
764
+ factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
765
+ factory.createParenthesizedExpression(initializer),
766
+ ),
767
+ )
768
+ }
769
+ statements.push(
770
+ constDeclaration(
771
+ local,
772
+ factory.createCallExpression(
773
+ factory.createIdentifier('$$bindableProp'),
774
+ undefined,
775
+ args,
776
+ ),
777
+ ),
778
+ )
779
+ continue
780
+ }
399
781
  /* `$props["key"]?.()` — the parent thunk, optionally called. */
400
782
  const read = factory.createCallChain(
401
783
  factory.createElementAccessExpression(
@@ -0,0 +1,19 @@
1
+ import { referencedIdentifiers } from './referencedIdentifiers.ts'
2
+
3
+ /* True when EVERY referenced identifier of `expression` is prefix-evaluable: none is a
4
+ template-local binder and none is an async-cell name (still pending at prefix time). Over-collects
5
+ identifiers, so any doubt fails closed. Shared by the await-flight classifier (hoistableAwaits) and
6
+ the child-render-flight classifier (hoistableChildRenders) — a hoisted await's promise and a
7
+ hoisted child's props must clear the same bar to start in the render prefix. */
8
+ export function expressionIsPrefixEvaluable(
9
+ expression: string,
10
+ binders: ReadonlySet<string>,
11
+ cellReadNames: ReadonlySet<string>,
12
+ ): boolean {
13
+ for (const name of referencedIdentifiers(expression)) {
14
+ if (binders.has(name) || cellReadNames.has(name)) {
15
+ return false
16
+ }
17
+ }
18
+ return true
19
+ }