@abide/abide 0.49.0 → 0.50.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/AGENTS.md +564 -437
  2. package/CHANGELOG.md +183 -0
  3. package/README.md +165 -202
  4. package/package.json +14 -5
  5. package/src/abideLsp.ts +5 -6
  6. package/src/abideResolverPlugin.ts +197 -169
  7. package/src/build.ts +79 -56
  8. package/src/buildDisconnected.ts +0 -2
  9. package/src/checkAbide.ts +12 -2
  10. package/src/devEntry.ts +113 -28
  11. package/src/discoveryEntry.ts +3 -2
  12. package/src/lib/bundle/disconnected.abide +69 -73
  13. package/src/lib/bundle/infoPlist.ts +13 -7
  14. package/src/lib/bundle/spawnEmbeddedServer.ts +13 -4
  15. package/src/lib/bundle/waitForServer.ts +6 -1
  16. package/src/lib/mcp/mcpTools.ts +15 -6
  17. package/src/lib/server/error.ts +6 -6
  18. package/src/lib/server/json.ts +17 -3
  19. package/src/lib/server/rpc/defineRpc.ts +58 -24
  20. package/src/lib/server/rpc/parseArgs.ts +98 -13
  21. package/src/lib/server/rpc/resolveRpcJsonSchema.ts +28 -0
  22. package/src/lib/server/rpc/types/RpcHelper.ts +75 -105
  23. package/src/lib/server/rpc/types/RpcRegistryEntry.ts +24 -9
  24. package/src/lib/server/rpc/validationError.ts +1 -1
  25. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +0 -15
  26. package/src/lib/server/runtime/buildCacheSnapshot.ts +10 -9
  27. package/src/lib/server/runtime/buildInspectorSurface.ts +6 -4
  28. package/src/lib/server/runtime/buildOpenApiSpec.ts +55 -11
  29. package/src/lib/server/runtime/buildPreloadManifest.ts +12 -10
  30. package/src/lib/server/runtime/createAppAssetServer.ts +18 -15
  31. package/src/lib/server/runtime/createAppRouteResolver.ts +145 -0
  32. package/src/lib/server/runtime/createPlumbingRouter.ts +212 -0
  33. package/src/lib/server/runtime/createRouteDispatcher.ts +4 -10
  34. package/src/lib/server/runtime/createServer.ts +120 -324
  35. package/src/lib/server/runtime/createUiPageRenderer.ts +137 -26
  36. package/src/lib/server/runtime/devClientFingerprint.ts +19 -34
  37. package/src/lib/server/runtime/installAmbientScopeStore.ts +30 -9
  38. package/src/lib/server/runtime/logExposedSurfaces.ts +8 -7
  39. package/src/lib/server/runtime/pathStore.ts +15 -0
  40. package/src/lib/server/runtime/registryManifests.ts +1 -1
  41. package/src/lib/server/runtime/renderCellBarrierStore.ts +15 -0
  42. package/src/lib/server/runtime/runWithRequestScope.ts +3 -0
  43. package/src/lib/server/runtime/serializeCacheSnapshot.ts +6 -4
  44. package/src/lib/server/runtime/snapshotEntryFromCache.ts +14 -14
  45. package/src/lib/server/runtime/streamCacheResolutions.ts +14 -7
  46. package/src/lib/server/runtime/streamFromIterator.ts +30 -2
  47. package/src/lib/server/runtime/textResponse.ts +23 -0
  48. package/src/lib/server/runtime/types/DevReloadStamp.ts +6 -10
  49. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  50. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  51. package/src/lib/server/runtime/warnUnguardedMcp.ts +15 -5
  52. package/src/lib/server/sockets/createSocketDispatcher.ts +9 -11
  53. package/src/lib/server/sockets/defineSocket.ts +5 -4
  54. package/src/lib/server/sse.ts +5 -1
  55. package/src/lib/shared/ASYNC_CELL.ts +5 -0
  56. package/src/lib/shared/DEV_REBUILD_PATH.ts +6 -0
  57. package/src/lib/shared/HTTP_METHODS.ts +6 -0
  58. package/src/lib/shared/HttpError.ts +1 -5
  59. package/src/lib/shared/MCP_PATH.ts +6 -0
  60. package/src/lib/shared/PROXIED_SERVER_SUBDIRS.ts +15 -0
  61. package/src/lib/shared/REF_JSON_TAGS.ts +13 -1
  62. package/src/lib/shared/RPC_SHIM_GLOBALS.ts +9 -0
  63. package/src/lib/shared/activeCacheStore.ts +1 -0
  64. package/src/lib/shared/activePage.ts +1 -0
  65. package/src/lib/shared/buildArtifact.ts +4 -1
  66. package/src/lib/shared/buildSocketOverChannel.ts +4 -3
  67. package/src/lib/shared/bundleGraphFromMetafile.ts +68 -0
  68. package/src/lib/shared/cache.ts +156 -131
  69. package/src/lib/shared/changeAffectsClient.ts +12 -7
  70. package/src/lib/shared/createCacheStore.ts +31 -8
  71. package/src/lib/shared/createLifecycleChannel.ts +7 -1
  72. package/src/lib/shared/createReachable.ts +47 -78
  73. package/src/lib/shared/createRemoteFunction.ts +52 -30
  74. package/src/lib/shared/createRpcServerProgram.ts +820 -0
  75. package/src/lib/shared/decodeRefJson.ts +4 -4
  76. package/src/lib/shared/decodeResponse.ts +2 -2
  77. package/src/lib/shared/decodeWireBody.ts +35 -0
  78. package/src/lib/shared/detectRpcMethod.ts +8 -1
  79. package/src/lib/shared/done.ts +8 -2
  80. package/src/lib/shared/encodeRefJson.ts +4 -4
  81. package/src/lib/shared/encodeWireBody.ts +18 -0
  82. package/src/lib/{server/rpc → shared}/fieldErrorsFromIssues.ts +5 -1
  83. package/src/lib/shared/generateDeclarations.ts +72 -0
  84. package/src/lib/shared/hasSeedableRequest.ts +25 -0
  85. package/src/lib/shared/hydrationWindow.ts +53 -0
  86. package/src/lib/shared/isAsyncCell.ts +11 -0
  87. package/src/lib/shared/isAsyncIterable.ts +8 -0
  88. package/src/lib/shared/isSubscribable.ts +3 -3
  89. package/src/lib/shared/isThenable.ts +9 -0
  90. package/src/lib/shared/jsonSchemaForType.ts +258 -0
  91. package/src/lib/shared/lenientDecode.ts +15 -0
  92. package/src/lib/shared/loadProjectTsConfig.ts +28 -0
  93. package/src/lib/shared/markFrameworkSourcesIgnored.ts +1 -1
  94. package/src/lib/shared/matchRoute.ts +4 -14
  95. package/src/lib/shared/peek.ts +14 -0
  96. package/src/lib/shared/pending.ts +9 -6
  97. package/src/lib/shared/pendingAsyncCellsSlot.ts +13 -0
  98. package/src/lib/shared/prepareRpcModule.ts +91 -93
  99. package/src/lib/shared/prepareSocketModule.ts +3 -2
  100. package/src/lib/shared/probeRegistries.ts +5 -18
  101. package/src/lib/shared/reachable.ts +7 -10
  102. package/src/lib/shared/refresh.ts +12 -2
  103. package/src/lib/shared/refreshing.ts +8 -2
  104. package/src/lib/shared/resolvedCellsSlot.ts +13 -0
  105. package/src/lib/shared/reviveWireField.ts +49 -0
  106. package/src/lib/shared/reviveWireOutput.ts +36 -0
  107. package/src/lib/shared/rpcServerForRoot.ts +25 -0
  108. package/src/lib/shared/scanPages.ts +25 -0
  109. package/src/lib/shared/snapshotShippable.ts +8 -7
  110. package/src/lib/shared/streamedCellsSlot.ts +14 -0
  111. package/src/lib/shared/subscribableFromResponse.ts +4 -4
  112. package/src/lib/shared/subscribableProbes.ts +1 -1
  113. package/src/lib/shared/tailProbeSlot.ts +1 -1
  114. package/src/lib/shared/types/AsyncComputed.ts +20 -0
  115. package/src/lib/shared/types/AsyncState.ts +13 -0
  116. package/src/lib/shared/types/CacheEntry.ts +7 -7
  117. package/src/lib/shared/types/CacheOptions.ts +23 -37
  118. package/src/lib/shared/types/CachePolicy.ts +25 -0
  119. package/src/lib/shared/types/CacheSnapshotEntry.ts +6 -5
  120. package/src/lib/shared/types/ErrorJsonSchemas.ts +8 -0
  121. package/src/lib/shared/types/HttpMethod.ts +3 -1
  122. package/src/lib/shared/types/InputCoercion.ts +17 -0
  123. package/src/lib/shared/types/{Subscribable.ts → NamedAsyncIterable.ts} +1 -1
  124. package/src/lib/shared/types/OutputWirePlan.ts +17 -0
  125. package/src/lib/shared/types/PagesScan.ts +7 -0
  126. package/src/lib/shared/types/PendingAsyncCells.ts +10 -0
  127. package/src/lib/shared/types/RawRemoteFunction.ts +6 -0
  128. package/src/lib/shared/types/RemoteCallable.ts +8 -7
  129. package/src/lib/shared/types/RemoteFunction.ts +16 -15
  130. package/src/lib/shared/types/ResolvedCells.ts +11 -0
  131. package/src/lib/shared/types/ReturnBody.ts +15 -0
  132. package/src/lib/shared/types/RpcBuildStamps.ts +23 -0
  133. package/src/lib/shared/types/RpcErrorGuard.ts +3 -8
  134. package/src/lib/shared/types/Socket.ts +4 -4
  135. package/src/lib/shared/types/SsrPayload.ts +5 -1
  136. package/src/lib/shared/types/StreamPolicy.ts +11 -0
  137. package/src/lib/shared/types/StreamedCells.ts +19 -0
  138. package/src/lib/shared/types/StreamedResolution.ts +24 -6
  139. package/src/lib/shared/types/TailHooks.ts +1 -1
  140. package/src/lib/shared/types/WireKind.ts +11 -0
  141. package/src/lib/shared/validationHttpError.ts +33 -0
  142. package/src/lib/shared/warmSeedKey.ts +16 -0
  143. package/src/lib/shared/wireJsonReplacer.ts +30 -0
  144. package/src/lib/shared/writeRpcDts.ts +11 -1
  145. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  146. package/src/lib/test/createTestApp.ts +19 -1
  147. package/src/lib/ui/README.md +1 -1
  148. package/src/lib/ui/activePendingCells.ts +14 -0
  149. package/src/lib/ui/compile/BLOCK_KEYWORDS.ts +21 -0
  150. package/src/lib/ui/compile/SSR_ESCAPE.ts +4 -1
  151. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +35 -8
  152. package/src/lib/ui/compile/abideUiPlugin.ts +19 -2
  153. package/src/lib/ui/compile/analyzeComponent.ts +89 -6
  154. package/src/lib/ui/compile/assignmentTargetNames.ts +54 -0
  155. package/src/lib/ui/compile/asyncInterpolationFields.ts +152 -0
  156. package/src/lib/ui/compile/asyncValuePositionError.ts +21 -0
  157. package/src/lib/ui/compile/asyncValuePositionInterpolations.ts +64 -0
  158. package/src/lib/ui/compile/attrLiftPosition.ts +18 -0
  159. package/src/lib/ui/compile/cachedSourceFile.ts +40 -0
  160. package/src/lib/ui/compile/catchBinding.ts +7 -5
  161. package/src/lib/ui/compile/classifyInterpolationType.ts +46 -0
  162. package/src/lib/ui/compile/collectAbideDiagnostics.ts +96 -0
  163. package/src/lib/ui/compile/compileComponent.ts +19 -3
  164. package/src/lib/ui/compile/compileModule.ts +27 -58
  165. package/src/lib/ui/compile/compileSSR.ts +51 -12
  166. package/src/lib/ui/compile/compileShadow.ts +145 -18
  167. package/src/lib/ui/compile/composeProps.ts +12 -2
  168. package/src/lib/ui/compile/createShadowLanguageService.ts +116 -68
  169. package/src/lib/ui/compile/createShadowProgram.ts +35 -6
  170. package/src/lib/ui/compile/declaredNames.ts +36 -0
  171. package/src/lib/ui/compile/desugarSignals.ts +418 -36
  172. package/src/lib/ui/compile/expressionIsPrefixEvaluable.ts +19 -0
  173. package/src/lib/ui/compile/generateBuild.ts +52 -9
  174. package/src/lib/ui/compile/generateSSR.ts +242 -43
  175. package/src/lib/ui/compile/hoistableAwaits.ts +193 -0
  176. package/src/lib/ui/compile/hoistableChildRenders.ts +112 -0
  177. package/src/lib/ui/compile/interpolatedTemplateLiteral.ts +3 -1
  178. package/src/lib/ui/compile/interpolationClassifierForRoot.ts +37 -0
  179. package/src/lib/ui/compile/isSpuriousAsyncReadDiagnostic.ts +174 -0
  180. package/src/lib/ui/compile/liftAsyncSubExpressions.ts +166 -0
  181. package/src/lib/ui/compile/lowerAsyncInterpolations.ts +92 -0
  182. package/src/lib/ui/compile/lowerContext.ts +10 -0
  183. package/src/lib/ui/compile/lowerDocAccess.ts +26 -23
  184. package/src/lib/ui/compile/lowerScript.ts +40 -4
  185. package/src/lib/ui/compile/nodeAtShadowOffset.ts +28 -0
  186. package/src/lib/ui/compile/parseTemplate.ts +18 -1092
  187. package/src/lib/ui/compile/parseTemplateRecovering.ts +1385 -0
  188. package/src/lib/ui/compile/referencedIdentifiers.ts +35 -0
  189. package/src/lib/ui/compile/renameSignalRefs.ts +21 -22
  190. package/src/lib/ui/compile/seedTypeClassifierForRoot.ts +65 -0
  191. package/src/lib/ui/compile/shadowInterpolationClassifier.ts +47 -0
  192. package/src/lib/ui/compile/sourceFileOptionsSignature.ts +20 -0
  193. package/src/lib/ui/compile/structuralHeadTokens.ts +107 -0
  194. package/src/lib/ui/compile/templateSemanticTokens.ts +21 -0
  195. package/src/lib/ui/compile/templateStartOffset.ts +15 -0
  196. package/src/lib/ui/compile/tryPlan.ts +4 -3
  197. package/src/lib/ui/compile/types/AnalyzedComponent.ts +4 -0
  198. package/src/lib/ui/compile/types/AsyncInterpolationField.ts +19 -0
  199. package/src/lib/ui/compile/types/InterpolationClassifier.ts +12 -0
  200. package/src/lib/ui/compile/types/InterpolationKind.ts +7 -0
  201. package/src/lib/ui/compile/types/ParseDiagnostic.ts +8 -0
  202. package/src/lib/ui/compile/types/SeedTypeClassifier.ts +15 -0
  203. package/src/lib/ui/compile/types/TemplateNode.ts +36 -4
  204. package/src/lib/ui/compile/types/ValuePositionInterpolation.ts +13 -0
  205. package/src/lib/ui/compile/writtenTemplateNames.ts +84 -0
  206. package/src/lib/ui/computed.ts +28 -1
  207. package/src/lib/ui/createScope.ts +35 -68
  208. package/src/lib/ui/dom/anchoredBranch.ts +142 -0
  209. package/src/lib/ui/dom/appendText.ts +8 -3
  210. package/src/lib/ui/dom/awaitBlock.ts +43 -94
  211. package/src/lib/ui/dom/bindProp.ts +22 -0
  212. package/src/lib/ui/dom/bindableProp.ts +47 -0
  213. package/src/lib/ui/dom/cellPending.ts +24 -0
  214. package/src/lib/ui/dom/disposeRange.ts +2 -1
  215. package/src/lib/ui/dom/each.ts +26 -4
  216. package/src/lib/ui/dom/eachAsync.ts +39 -3
  217. package/src/lib/ui/dom/fillBoundary.ts +2 -3
  218. package/src/lib/ui/dom/fillRange.ts +4 -4
  219. package/src/lib/ui/dom/hydrate.ts +6 -12
  220. package/src/lib/ui/dom/isComment.ts +1 -1
  221. package/src/lib/ui/dom/matchingRangeClose.ts +29 -0
  222. package/src/lib/ui/dom/mount.ts +1 -2
  223. package/src/lib/ui/dom/mountChild.ts +9 -40
  224. package/src/lib/ui/dom/mountRange.ts +2 -3
  225. package/src/lib/ui/dom/mountStreamedChild.ts +84 -0
  226. package/src/lib/ui/dom/mountSwappableRange.ts +46 -4
  227. package/src/lib/ui/dom/mutateDocContainer.ts +65 -0
  228. package/src/lib/ui/dom/on.ts +2 -2
  229. package/src/lib/ui/dom/readCell.ts +40 -0
  230. package/src/lib/ui/dom/switchBlock.ts +30 -7
  231. package/src/lib/ui/dom/tryBlock.ts +230 -66
  232. package/src/lib/ui/dom/types/SwitchCase.ts +5 -1
  233. package/src/lib/ui/dom/when.ts +12 -2
  234. package/src/lib/ui/dom/withScope.ts +2 -2
  235. package/src/lib/ui/finalizeStreamedChildren.ts +89 -0
  236. package/src/lib/ui/flight.ts +56 -0
  237. package/src/lib/ui/html.ts +12 -1
  238. package/src/lib/ui/isolateCellBarrier.ts +17 -0
  239. package/src/lib/ui/linked.ts +48 -2
  240. package/src/lib/ui/remoteProxy.ts +84 -159
  241. package/src/lib/ui/renderChain.ts +49 -13
  242. package/src/lib/ui/renderToStream.ts +22 -18
  243. package/src/lib/ui/resumeSeedScript.ts +1 -1
  244. package/src/lib/ui/router.ts +63 -36
  245. package/src/lib/ui/runtime/AsyncCellError.ts +20 -0
  246. package/src/lib/ui/runtime/CELL_SEED.ts +14 -0
  247. package/src/lib/ui/runtime/CURRENT_BOUNDARY.ts +11 -0
  248. package/src/lib/ui/runtime/CURRENT_PATH.ts +29 -0
  249. package/src/lib/ui/runtime/RENDER.ts +10 -7
  250. package/src/lib/ui/runtime/RESUME.ts +4 -4
  251. package/src/lib/ui/runtime/STREAMED_CELLS.ts +57 -0
  252. package/src/lib/ui/runtime/ambientPathBacking.ts +32 -0
  253. package/src/lib/ui/runtime/applyPatchToTree.ts +1 -1
  254. package/src/lib/ui/runtime/blockId.ts +31 -0
  255. package/src/lib/ui/runtime/boundaryFor.ts +11 -0
  256. package/src/lib/ui/runtime/cellBarrierBacking.ts +30 -0
  257. package/src/lib/ui/runtime/createAsyncCell.ts +300 -0
  258. package/src/lib/ui/runtime/createDoc.ts +3 -42
  259. package/src/lib/ui/runtime/createEffectNode.ts +9 -0
  260. package/src/lib/ui/runtime/enterRenderPass.ts +5 -4
  261. package/src/lib/ui/runtime/flushEffects.ts +16 -6
  262. package/src/lib/ui/runtime/isAsyncFunction.ts +14 -0
  263. package/src/lib/ui/runtime/nextBlockId.ts +8 -7
  264. package/src/lib/ui/runtime/renderPath.ts +16 -0
  265. package/src/lib/ui/runtime/types/Boundary.ts +9 -0
  266. package/src/lib/ui/runtime/types/RenderContext.ts +9 -7
  267. package/src/lib/ui/runtime/types/SsrRender.ts +9 -2
  268. package/src/lib/ui/runtime/types/UiComponent.ts +1 -5
  269. package/src/lib/ui/runtime/withOptionalPath.ts +8 -0
  270. package/src/lib/ui/runtime/withPath.ts +16 -0
  271. package/src/lib/ui/runtime/withPathFrom.ts +20 -0
  272. package/src/lib/ui/seedStreamedResolution.ts +31 -6
  273. package/src/lib/ui/settleAsyncCells.ts +24 -0
  274. package/src/lib/ui/socketProxy.ts +1 -1
  275. package/src/lib/ui/startClient.ts +16 -31
  276. package/src/lib/ui/trackedComputed.ts +68 -0
  277. package/src/lib/ui/types/Scope.ts +8 -24
  278. package/src/lib/ui/watch.ts +3 -3
  279. package/src/serverEntry.ts +14 -0
  280. package/template/src/server/rpc/getHello.ts +15 -13
  281. package/template/test/app.test.ts +1 -1
  282. package/src/lib/server/runtime/devHotModuleResponse.ts +0 -41
  283. package/src/lib/shared/DEV_HOT_PREFIX.ts +0 -7
  284. package/src/lib/shared/UNREACHABLE_STATUSES.ts +0 -13
  285. package/src/lib/shared/hasReplayableRequest.ts +0 -17
  286. package/src/lib/shared/hydratingSlot.ts +0 -12
  287. package/src/lib/shared/outboxProbeSlot.ts +0 -20
  288. package/src/lib/shared/types/Outbox.ts +0 -9
  289. package/src/lib/shared/types/OutboxEntry.ts +0 -27
  290. package/src/lib/shared/types/SmartReadOptions.ts +0 -36
  291. package/src/lib/shared/wakeHydrationPeeks.ts +0 -20
  292. package/src/lib/ui/COMPONENT_WRAPPER_PREFIX.ts +0 -5
  293. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +0 -11
  294. package/src/lib/ui/compile/assertRuntimeHelpersBound.ts +0 -80
  295. package/src/lib/ui/compile/markupTokens.ts +0 -313
  296. package/src/lib/ui/compile/structuralBlockTokens.ts +0 -100
  297. package/src/lib/ui/dom/applyResolved.ts +0 -87
  298. package/src/lib/ui/dom/mutateDocArray.ts +0 -38
  299. package/src/lib/ui/dom/scopeLabel.ts +0 -21
  300. package/src/lib/ui/dom/text.ts +0 -20
  301. package/src/lib/ui/history.ts +0 -108
  302. package/src/lib/ui/installHotBridge.ts +0 -95
  303. package/src/lib/ui/installInspectorBridge.ts +0 -140
  304. package/src/lib/ui/outbox.ts +0 -35
  305. package/src/lib/ui/persist.ts +0 -115
  306. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +0 -281
  307. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +0 -69
  308. package/src/lib/ui/runtime/PATCH_BUS.ts +0 -28
  309. package/src/lib/ui/runtime/captureModelDoc.ts +0 -43
  310. package/src/lib/ui/runtime/hotInstances.ts +0 -10
  311. package/src/lib/ui/runtime/hotReloadEnabled.ts +0 -8
  312. package/src/lib/ui/runtime/hotReplace.ts +0 -38
  313. package/src/lib/ui/runtime/liveScopes.ts +0 -15
  314. package/src/lib/ui/runtime/localStoragePersistence.ts +0 -47
  315. package/src/lib/ui/runtime/registerHotInstance.ts +0 -23
  316. package/src/lib/ui/runtime/seedModelDoc.ts +0 -26
  317. package/src/lib/ui/runtime/types/HotInstance.ts +0 -22
  318. package/src/lib/ui/runtime/types/PatchEvent.ts +0 -16
  319. package/src/lib/ui/seedResolved.ts +0 -28
  320. package/src/lib/ui/sync.ts +0 -48
  321. package/src/lib/ui/types/History.ts +0 -14
  322. package/src/lib/ui/types/PersistHandle.ts +0 -11
  323. package/src/lib/ui/types/PersistenceStore.ts +0 -12
  324. package/src/lib/ui/types/ResolvedFrame.ts +0 -15
  325. package/src/lib/ui/types/SyncTransport.ts +0 -13
@@ -1,5 +1,8 @@
1
1
  import ts from 'typescript'
2
2
  import { ABIDE_PACKAGE_NAME } from '../../shared/ABIDE_PACKAGE_NAME.ts'
3
+ import { attrLiftPosition } from './attrLiftPosition.ts'
4
+ import { isWhitespaceText } from './isWhitespaceText.ts'
5
+ import { type LiftPosition, liftAsyncSubExpressions } from './liftAsyncSubExpressions.ts'
3
6
  import { parseTemplate } from './parseTemplate.ts'
4
7
  import {
5
8
  NESTED_REACTIVE_BINDINGS,
@@ -8,7 +11,9 @@ import {
8
11
  resolveReactiveExport,
9
12
  } from './resolveReactiveExport.ts'
10
13
  import { signalCallee } from './signalCallee.ts'
14
+ import { LEADING_SCRIPT } from './templateStartOffset.ts'
11
15
  import type { CompiledShadow, ShadowDiagnostic, ShadowMapping } from './types/CompiledShadow.ts'
16
+ import type { InterpolationClassifier } from './types/InterpolationClassifier.ts'
12
17
  import type { TemplateNode } from './types/TemplateNode.ts'
13
18
 
14
19
  /*
@@ -69,9 +74,13 @@ The script's signal surface is rewritten to value types:
69
74
  Everything else (functions, plain consts, imports) is emitted verbatim, so
70
75
  expressions inside it (e.g. a computed's compute body) are checked and mapped too.
71
76
  */
72
- export function compileShadow(source: string, propsType = 'Record<string, any>'): CompiledShadow {
73
- const builder = createBuilder()
74
- const leadingScript = source.match(/^\s*<script[^>]*>([\s\S]*?)<\/script>/)
77
+ export function compileShadow(
78
+ source: string,
79
+ propsType = 'Record<string, any>',
80
+ classify?: InterpolationClassifier,
81
+ ): CompiledShadow {
82
+ const builder = createBuilder(classify)
83
+ const leadingScript = source.match(LEADING_SCRIPT)
75
84
  const scriptBody = leadingScript?.[1] ?? ''
76
85
  /* Body starts just past the opening `<script …>`; template just past `</script>`. */
77
86
  const scriptStart = leadingScript ? source.indexOf('>', leadingScript.index) + 1 : 0
@@ -80,6 +89,18 @@ export function compileShadow(source: string, propsType = 'Record<string, any>')
80
89
  const { imports, types, scope, propsShapes, diagnostics, importedReactives, propsLocalName } =
81
90
  analyzeScript(scriptBody, scriptStart)
82
91
  builder.raw(shadowPreamble(importedReactives))
92
+ /* The peek helpers the async-interpolation wrap targets (ADR-0032): `$$peek` unwraps a promise
93
+ sub-expression to its resolved value (`undefined` while pending) and `$$peekStream` reads an
94
+ async iterable's latest frame, so `getFoo()?.name`/`{#if getFoo()}` type-check against the
95
+ RESOLVED value the runtime peeks — not the raw `Promise`. Emitted only when a classifier is
96
+ present (the wrap is type-directed); their names carry the reserved `$$` prefix an author
97
+ binding can never take, so they cannot collide with user code. */
98
+ if (classify !== undefined) {
99
+ builder.raw('declare function $$peek<T>(v: T): Awaited<T> | undefined\n')
100
+ builder.raw(
101
+ 'declare function $$peekStream<S>(v: S): (S extends AsyncIterable<infer F> ? F : unknown) | undefined\n',
102
+ )
103
+ }
83
104
  /* `props` is a required import (`abide/ui/props`). The shadow owns its type so the
84
105
  return is file-contextual — the route param shape (page/layout) or `Record<string,
85
106
  any>` (component), intersected with the author's annotation `T` so declared props
@@ -171,6 +192,12 @@ type Builder = {
171
192
  mapped: (text: string, sourceLoc: number | undefined) => void
172
193
  expr: (code: string, sourceLoc: number | undefined) => void
173
194
  stmt: (code: string, sourceLoc: number | undefined) => void
195
+ /* Like `expr`/`stmt` but for a template interpolation: async (sub)expressions are wrapped in a
196
+ peek helper (`$$peek`/`$$peekStream`) so their RESOLVED type composes (ADR-0032). Every source
197
+ span still maps 1:1 — the inserted wrapper chars are unmapped — so diagnostics land precisely.
198
+ With no classifier (or no async sub-expression) these are byte-identical to `expr`/`stmt`. */
199
+ asyncExpr: (code: string, sourceLoc: number | undefined, position: LiftPosition) => void
200
+ asyncStmt: (code: string, sourceLoc: number | undefined, position: LiftPosition) => void
174
201
  flush: (line: ScopeLine) => void
175
202
  /* A fresh shadow-local binding name (`__<base>_<n>`) — for synthesised bindings
176
203
  like an await's resolved value, kept distinct so nested blocks never collide. */
@@ -182,10 +209,55 @@ type Builder = {
182
209
  emitted verbatim from the original script), offset-relative to the line start. */
183
210
  type ScopeLine = { text: string; segments: ShadowMapping[] }
184
211
 
185
- function createBuilder(): Builder {
212
+ function createBuilder(classify?: InterpolationClassifier): Builder {
186
213
  let code = ''
187
214
  let uniqueCounter = 0
188
215
  const mappings: ShadowMapping[] = []
216
+ /* The non-blocking async (sub)expression spans in one interpolation — the peek-wrap targets.
217
+ A leading `await` (blocking) stays verbatim: `await X` already resolves in the async shadow
218
+ render fn. Fail-open: no classifier, an unparseable field, or the D4a `AsyncIterable`-in-`each`
219
+ throw all yield no spans, so the emit is verbatim (`collectValuePositionDiagnostics` still
220
+ reports the D4a error separately). */
221
+ const peekSpans = (exprCode: string, sourceLoc: number, position: LiftPosition) => {
222
+ if (classify === undefined) {
223
+ return []
224
+ }
225
+ try {
226
+ let counter = 0
227
+ const result = liftAsyncSubExpressions(
228
+ exprCode,
229
+ sourceLoc,
230
+ classify,
231
+ () => `__v${counter++}`,
232
+ position,
233
+ )
234
+ return result.spans.filter((span) => !span.blocking)
235
+ } catch {
236
+ return []
237
+ }
238
+ }
239
+ /* Appends `exprCode` mapped 1:1 to source, but wraps each async span in its peek helper — the
240
+ wrapper chars are unmapped, every original char keeps its true source offset. */
241
+ const emitPeeked = (exprCode: string, sourceLoc: number, position: LiftPosition): void => {
242
+ const spans = peekSpans(exprCode, sourceLoc, position)
243
+ if (spans.length === 0) {
244
+ builder.mapped(exprCode, sourceLoc)
245
+ return
246
+ }
247
+ let cursor = 0
248
+ for (const span of spans) {
249
+ if (span.start > cursor) {
250
+ builder.mapped(exprCode.slice(cursor, span.start), sourceLoc + cursor)
251
+ }
252
+ builder.raw(span.kind === 'asyncIterable' ? '$$peekStream(' : '$$peek(')
253
+ builder.mapped(exprCode.slice(span.start, span.end), sourceLoc + span.start)
254
+ builder.raw(')')
255
+ cursor = span.end
256
+ }
257
+ if (cursor < exprCode.length) {
258
+ builder.mapped(exprCode.slice(cursor), sourceLoc + cursor)
259
+ }
260
+ }
189
261
  const builder: Builder = {
190
262
  unique(base) {
191
263
  return `__${base}_${uniqueCounter++}`
@@ -219,6 +291,21 @@ function createBuilder(): Builder {
219
291
  builder.expr(exprCode, sourceLoc)
220
292
  code += ';\n'
221
293
  },
294
+ asyncExpr(exprCode, sourceLoc, position) {
295
+ /* No source offset ⇒ can't map or classify — emit verbatim, like `expr`. */
296
+ if (sourceLoc === undefined) {
297
+ code += `(${exprCode})`
298
+ return
299
+ }
300
+ code += '('
301
+ emitPeeked(exprCode, sourceLoc, position)
302
+ code += ')'
303
+ },
304
+ asyncStmt(exprCode, sourceLoc, position) {
305
+ code += ';'
306
+ builder.asyncExpr(exprCode, sourceLoc, position)
307
+ code += ';\n'
308
+ },
222
309
  flush(line) {
223
310
  const base = code.length
224
311
  for (const segment of line.segments) {
@@ -481,12 +568,20 @@ function scopeLineFor(
481
568
  `verbatim(call.expression)` below keeps the AUTHOR's text for the trailing hover ref. */
482
569
  const callee = resolveReactiveExport(call.expression, bindings)
483
570
  if (callee === 'props') {
484
- /* `const {…} = props<Shape>()`: the type arg (default `Record<string, any>`)
485
- IS the parent-facing prop shape, and the destructure projects verbatim against
486
- the declared typed `props()` so each binding inherits its value type. */
487
- const shape = call.typeArguments?.[0]
571
+ /* The parent-facing prop shape. Like `state`/`computed` below, it comes from the
572
+ generic (`const {…} = props<Shape>()`) OR the destructure's binding annotation
573
+ (`const {…}: Shape = props()`) either TS-idiomatic form resolves through the
574
+ shadow's own imports, so an imported/aliased type just works, no inline gotcha.
575
+ Default `Record<string, any>`; the destructure then projects verbatim so each
576
+ binding inherits its value type. */
577
+ const shape = call.typeArguments?.[0] ?? declaration.type
488
578
  propsShapes.push(shape === undefined ? 'Record<string, any>' : verbatim(shape))
489
- return { text: `const ${verbatim(declaration)};`, segments: [span(declaration, 6)] }
579
+ /* `let`, not `const`: a prop the component writes back through a two-way `bind:prop`
580
+ (`value += 1`, or forwarding it to another `bind:`) is a legal reassignment, so the
581
+ destructure must not read as a `const`. Bindability is dynamic (decided by what the
582
+ parent passed), so the shadow can't single out which props are written — it relaxes
583
+ the whole destructure, matching the runtime's usage-inferred upgrade. */
584
+ return { text: `let ${verbatim(declaration)};`, segments: [span(declaration, 4)] }
490
585
  }
491
586
  /* The rewrite drops the callee from the line, so hovering `state`/`computed`/
492
587
  `linked` at its call site has nothing to resolve. Append the callee as a
@@ -596,7 +691,7 @@ function emitNode(node: TemplateNode, builder: Builder): void {
596
691
  case 'text':
597
692
  for (const part of node.parts) {
598
693
  if (part.kind === 'expression') {
599
- builder.stmt(part.code, part.loc)
694
+ builder.asyncStmt(part.code, part.loc, 'content')
600
695
  }
601
696
  }
602
697
  return
@@ -617,12 +712,22 @@ function emitNode(node: TemplateNode, builder: Builder): void {
617
712
  /* An interpolated value checks each `{expr}` part on its own offset. */
618
713
  for (const part of attr.parts) {
619
714
  if (part.kind === 'expression') {
620
- builder.stmt(part.code, part.loc)
715
+ builder.asyncStmt(part.code, part.loc, 'attribute')
621
716
  }
622
717
  }
623
718
  } else if (attr.kind !== 'static') {
624
- /* Every other dynamic attribute checks its single `code`. */
625
- builder.stmt(attr.code, attr.loc)
719
+ /* Every other dynamic attribute checks its single `code`. `attrLiftPosition` —
720
+ the shared gate the runtime front-end lifts through — decides whether the
721
+ value is async-liftable: only `name={code}` peek-wraps (its promise/stream
722
+ sub-expressions project to their resolved type), while a directive
723
+ (`on*`/`bind:`/`class:`/`style:`/`{...}`) checks its code verbatim, matching
724
+ that the runtime never injects a cell for it. */
725
+ const position = attrLiftPosition(attr)
726
+ if (position !== undefined) {
727
+ builder.asyncStmt(attr.code, attr.loc, position)
728
+ } else {
729
+ builder.stmt(attr.code, attr.loc)
730
+ }
626
731
  }
627
732
  }
628
733
  emitNodes(node.children, builder)
@@ -644,6 +749,12 @@ function emitNode(node: TemplateNode, builder: Builder): void {
644
749
  prop.name.startsWith('style:') ||
645
750
  prop.name === 'attach'
646
751
  const hasSpread = node.props.some((prop) => prop.spread)
752
+ /* Default slot content (`<Card>…</Card>`) lowers to a `children` Snippet prop at
753
+ runtime (see generateBuild.slotPart), so the completeness check must treat
754
+ `children` as supplied when it's present (handled below). Mirror the runtime's
755
+ predicate: any child that isn't whitespace-only text is renderable slot content
756
+ — an empty/whitespace slot sets no `children` layer, so it doesn't count. */
757
+ const hasSlotContent = node.children.some((child) => !isWhitespaceText(child))
647
758
  for (const prop of node.props.filter(handled)) {
648
759
  /* Lead with a defensive `;`: an IIFE/object-literal arg starts with `(` or
649
760
  `{`, so without it a preceding scope statement left unterminated (a script
@@ -679,14 +790,30 @@ function emitNode(node: TemplateNode, builder: Builder): void {
679
790
  builder.raw(');\n')
680
791
  }
681
792
  } else {
682
- builder.raw(`;((__c: Parameters<typeof ${node.name}>[0]): void => { void __c })({`)
793
+ /* With slot content the child's `children` prop is supplied by the slot, so
794
+ check completeness against the shape with `children` made OPTIONAL rather
795
+ than removed — dropping it via a bare `Omit` collapses a children-only
796
+ shape to `{}`, a weak type that then lets excess props slip by. `Omit` +
797
+ `Partial<Pick<…>>` keeps every other prop required and `children` as a
798
+ known-optional key (a no-op `Partial<{}>` when the child declares none), so
799
+ completeness and excess-prop checks both stay intact. The explicit
800
+ `children={…}` attribute is dropped from the literal — the slot's key wins
801
+ (`composeProps`), and leaving it in would read as excess. */
802
+ const propsShape = `Parameters<typeof ${node.name}>[0]`
803
+ const completenessType = hasSlotContent
804
+ ? `Omit<${propsShape}, 'children'> & Partial<Pick<${propsShape}, Extract<keyof ${propsShape}, 'children'>>>`
805
+ : propsShape
806
+ const literalProps = hasSlotContent
807
+ ? dataProps.filter((prop) => prop.name !== 'children')
808
+ : dataProps
809
+ builder.raw(`;((__c: ${completenessType}): void => { void __c })({`)
683
810
  /* A zero-length anchor right after `{`, pointing at the tag: a missing-
684
811
  required-prop error spans the literal from `{`, so it overlaps this and
685
812
  maps to the tag (an empty span trivially satisfies the source-text ==
686
813
  shadow-text invariant). */
687
814
  builder.mapped('', node.loc)
688
815
  builder.raw('\n')
689
- for (const prop of dataProps) {
816
+ for (const prop of literalProps) {
690
817
  /* The key mapped to its source name (excess-prop errors land on the key);
691
818
  the value verbatim-mapped (wrong-type errors land on the value).
692
819
  Hyphenated names (`aria-label`, `data-*`) aren't valid identifiers, so
@@ -726,7 +853,7 @@ function emitNode(node: TemplateNode, builder: Builder): void {
726
853
  )
727
854
  const thenChildren = node.children.filter((child) => child.kind !== 'case')
728
855
  builder.raw('if ')
729
- builder.expr(node.condition, node.loc)
856
+ builder.asyncExpr(node.condition, node.loc, 'if')
730
857
  builder.raw(' {\n')
731
858
  emitNodes(thenChildren, builder)
732
859
  builder.raw('}')
@@ -735,7 +862,7 @@ function emitNode(node: TemplateNode, builder: Builder): void {
735
862
  for (const branch of branches) {
736
863
  if (branch.condition !== undefined) {
737
864
  builder.raw(' else if ')
738
- builder.expr(branch.condition, branch.loc)
865
+ builder.asyncExpr(branch.condition, branch.loc, 'if')
739
866
  builder.raw(' {\n')
740
867
  } else {
741
868
  builder.raw(' else {\n')
@@ -817,7 +944,7 @@ function emitNode(node: TemplateNode, builder: Builder): void {
817
944
  non-case children (whitespace between cases) carry nothing and are
818
945
  skipped. `break` keeps cases independent under `noFallthroughCasesInSwitch`. */
819
946
  builder.raw('switch (')
820
- builder.expr(node.subject, node.loc)
947
+ builder.asyncExpr(node.subject, node.loc, 'switch')
821
948
  builder.raw(') {\n')
822
949
  for (const child of node.children) {
823
950
  if (child.kind !== 'case') {
@@ -20,9 +20,19 @@ export function composeProps(
20
20
  props: ComponentProps,
21
21
  lowerExpression: (code: string) => string,
22
22
  slotPart: string | undefined,
23
+ /* The two-way bind accessors (`lowerContext`): `bindRead` lowers a target to its
24
+ current value, `bindWrite` to the statement that writes a value back. Shared with
25
+ element `bind:`, so a component bind accepts the same lvalue / `{ get, set }` targets. */
26
+ bindRead: (code: string) => string,
27
+ bindWrite: (code: string, valueExpr: string) => string,
23
28
  ): string {
24
- const propThunk = (prop: { name: string; code: string }): string =>
25
- `${JSON.stringify(prop.name)}: () => (${lowerExpression(prop.code)})`
29
+ /* A `bind:<name>={target}` prop: the normal `() => value` entry, annotated with a
30
+ `set` write-back channel (`bindProp`) so the child can push changes upstream. A
31
+ read-only consumer still just calls the thunk; the setter rides along unseen. */
32
+ const propThunk = (prop: { name: string; code: string; bind?: true }): string =>
33
+ prop.bind === true
34
+ ? `${JSON.stringify(prop.name)}: $$bindProp(() => (${bindRead(prop.code)}), ($value) => { ${bindWrite(prop.code, '$value')} })`
35
+ : `${JSON.stringify(prop.name)}: () => (${lowerExpression(prop.code)})`
26
36
  if (!props.some((prop) => prop.spread)) {
27
37
  const parts = props.map(propThunk)
28
38
  if (slotPart !== undefined) {
@@ -4,14 +4,18 @@ import { messageFromError } from '../../shared/messageFromError.ts'
4
4
  import { mapSyntacticClassification, mapTsClassification } from './ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
5
5
  import { assetModulesFile } from './assetModulesFile.ts'
6
6
  import { compileShadow } from './compileShadow.ts'
7
+ import { isSpuriousAsyncReadDiagnostic } from './isSpuriousAsyncReadDiagnostic.ts'
7
8
  import { loadShadowTsConfig } from './loadShadowTsConfig.ts'
8
9
  import { pagePropsType } from './pagePropsType.ts'
9
10
  import { remapShadowDiagnostic } from './remapShadowDiagnostic.ts'
10
11
  import { resolveAbideImports } from './resolveAbideImports.ts'
12
+ import { shadowInterpolationClassifier } from './shadowInterpolationClassifier.ts'
11
13
  import { shadowNaming } from './shadowNaming.ts'
12
14
  import { sourceToShadowOffset } from './sourceToShadowOffset.ts'
15
+ import { templateStartOffset } from './templateStartOffset.ts'
13
16
  import type { AbideDiagnostic } from './types/AbideDiagnostic.ts'
14
17
  import type { CompiledShadow } from './types/CompiledShadow.ts'
18
+ import type { InterpolationClassifier } from './types/InterpolationClassifier.ts'
15
19
  import type { SemanticToken } from './types/SemanticToken.ts'
16
20
 
17
21
  const { suffixed, isShadow, sourceOf } = shadowNaming
@@ -45,12 +49,6 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
45
49
  const { options, fileNames } = loadShadowTsConfig(cwd)
46
50
  const overlays = new Map<string, string>()
47
51
  const versions = new Map<string, number>()
48
- const shadows = new Map<string, CompiledShadow>()
49
- const parseErrors = new Map<string, string>()
50
- /* Memo of `shadowText` output keyed by source path, tagged with the shadow
51
- version it was compiled at; a stale tag forces recompilation. `update`/`close`
52
- bump that version, so the next read recompiles. */
53
- const compiledAt = new Map<string, { version: number; code: string }>()
54
52
  /* Memo of `pagePropsType` (route re-parse) per source path; the path → props
55
53
  type mapping is immutable, so no version tag is needed. */
56
54
  const propsTypes = new Map<string, string | undefined>()
@@ -69,33 +67,6 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
69
67
  const exists = (abidePath: string): boolean =>
70
68
  overlays.has(abidePath) || ts.sys.fileExists(abidePath)
71
69
 
72
- /* Compiles (and caches) a component's shadow from its overlay or disk text; a
73
- template parse error yields a minimal valid module + a recorded message.
74
- Memoised against the shadow's version (bumped by `update`/`close`), so
75
- repeated reads at the same version skip the ~700-line compile and the
76
- `shadows`/`parseErrors` caches stay current. */
77
- const shadowText = (abidePath: string): string => {
78
- const version = versions.get(suffixed(abidePath)) ?? 0
79
- const memo = compiledAt.get(abidePath)
80
- if (memo !== undefined && memo.version === version) {
81
- return memo.code
82
- }
83
- const source = overlays.get(abidePath) ?? ts.sys.readFile(abidePath) ?? ''
84
- try {
85
- const compiled = compileShadow(source, propsTypeOf(abidePath))
86
- shadows.set(abidePath, compiled)
87
- parseErrors.delete(abidePath)
88
- compiledAt.set(abidePath, { version, code: compiled.code })
89
- return compiled.code
90
- } catch (error) {
91
- shadows.set(abidePath, { code: '', mappings: [] })
92
- parseErrors.set(abidePath, messageFromError(error))
93
- const code = 'export default function (): void {}\n'
94
- compiledAt.set(abidePath, { version, code })
95
- return code
96
- }
97
- }
98
-
99
70
  /* All component shadows: those on disk plus any opened (possibly unsaved) ones. */
100
71
  const shadowNames = (): string[] => {
101
72
  const disk = [...new Bun.Glob('**/*.abide').scanSync({ cwd, onlyFiles: true })]
@@ -104,44 +75,99 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
104
75
  return [...new Set([...disk, ...overlays.keys()])].map(suffixed)
105
76
  }
106
77
 
107
- const moduleResolutionHost: ts.ModuleResolutionHost = {
108
- fileExists: (fileName) =>
109
- fileName === assets.path ||
110
- (isShadow(fileName) ? exists(sourceOf(fileName)) : ts.sys.fileExists(fileName)),
111
- readFile: (fileName) =>
112
- fileName === assets.path
113
- ? assets.content
114
- : isShadow(fileName)
115
- ? shadowText(sourceOf(fileName))
116
- : ts.sys.readFile(fileName),
117
- }
118
-
119
- const host: ts.LanguageServiceHost = {
120
- getScriptFileNames: () => [assets.path, ...fileNames, ...shadowNames()],
121
- getScriptVersion: (fileName) => String(versions.get(fileName) ?? 0),
122
- getScriptSnapshot: (fileName) => {
123
- if (fileName === assets.path) {
124
- return ts.ScriptSnapshot.fromString(assets.content)
78
+ /* One incremental language service over the shadow world, its shadows built by `compile`. Two
79
+ are made: a VERBATIM one (the classifier source — interpolations un-wrapped, so `getFoo()`
80
+ still types as `Promise`), and the WRAPPED main one (async interpolations peek-wrapped, ADR-
81
+ 0032, so hover/completions/diagnostics see the RESOLVED value). They share `overlays`/`versions`
82
+ so both reflect unsaved edits, but hold SEPARATE shadow caches + document registries (the same
83
+ shadow file name carries different text in each). */
84
+ const buildService = (compile: (source: string, abidePath: string) => CompiledShadow) => {
85
+ const shadows = new Map<string, CompiledShadow>()
86
+ const parseErrors = new Map<string, string>()
87
+ /* Memo of `shadowText` output keyed by source path, tagged with the shadow version it was
88
+ compiled at; a stale tag forces recompilation. `update`/`close` bump that version. */
89
+ const compiledAt = new Map<string, { version: number; code: string }>()
90
+ const shadowText = (abidePath: string): string => {
91
+ const version = versions.get(suffixed(abidePath)) ?? 0
92
+ const memo = compiledAt.get(abidePath)
93
+ if (memo !== undefined && memo.version === version) {
94
+ return memo.code
125
95
  }
126
- if (isShadow(fileName)) {
127
- return exists(sourceOf(fileName))
128
- ? ts.ScriptSnapshot.fromString(shadowText(sourceOf(fileName)))
129
- : undefined
96
+ const source = overlays.get(abidePath) ?? ts.sys.readFile(abidePath) ?? ''
97
+ try {
98
+ const compiled = compile(source, abidePath)
99
+ shadows.set(abidePath, compiled)
100
+ parseErrors.delete(abidePath)
101
+ compiledAt.set(abidePath, { version, code: compiled.code })
102
+ return compiled.code
103
+ } catch (error) {
104
+ shadows.set(abidePath, { code: '', mappings: [] })
105
+ parseErrors.set(abidePath, messageFromError(error))
106
+ const code = 'export default function (): void {}\n'
107
+ compiledAt.set(abidePath, { version, code })
108
+ return code
130
109
  }
131
- const text = ts.sys.readFile(fileName)
132
- return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text)
133
- },
134
- getCurrentDirectory: () => cwd,
135
- getCompilationSettings: () => options,
136
- getDefaultLibFileName: (compilerOptions) => ts.getDefaultLibFilePath(compilerOptions),
137
- fileExists: moduleResolutionHost.fileExists,
138
- readFile: moduleResolutionHost.readFile,
139
- readDirectory: ts.sys.readDirectory,
140
- directoryExists: ts.sys.directoryExists,
141
- getDirectories: ts.sys.getDirectories,
142
- resolveModuleNames: resolveAbideImports(options, moduleResolutionHost),
110
+ }
111
+ const moduleResolutionHost: ts.ModuleResolutionHost = {
112
+ fileExists: (fileName) =>
113
+ fileName === assets.path ||
114
+ (isShadow(fileName) ? exists(sourceOf(fileName)) : ts.sys.fileExists(fileName)),
115
+ readFile: (fileName) =>
116
+ fileName === assets.path
117
+ ? assets.content
118
+ : isShadow(fileName)
119
+ ? shadowText(sourceOf(fileName))
120
+ : ts.sys.readFile(fileName),
121
+ }
122
+ const host: ts.LanguageServiceHost = {
123
+ getScriptFileNames: () => [assets.path, ...fileNames, ...shadowNames()],
124
+ getScriptVersion: (fileName) => String(versions.get(fileName) ?? 0),
125
+ getScriptSnapshot: (fileName) => {
126
+ if (fileName === assets.path) {
127
+ return ts.ScriptSnapshot.fromString(assets.content)
128
+ }
129
+ if (isShadow(fileName)) {
130
+ return exists(sourceOf(fileName))
131
+ ? ts.ScriptSnapshot.fromString(shadowText(sourceOf(fileName)))
132
+ : undefined
133
+ }
134
+ const text = ts.sys.readFile(fileName)
135
+ return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text)
136
+ },
137
+ getCurrentDirectory: () => cwd,
138
+ getCompilationSettings: () => options,
139
+ getDefaultLibFileName: (compilerOptions) => ts.getDefaultLibFilePath(compilerOptions),
140
+ fileExists: moduleResolutionHost.fileExists,
141
+ readFile: moduleResolutionHost.readFile,
142
+ readDirectory: ts.sys.readDirectory,
143
+ directoryExists: ts.sys.directoryExists,
144
+ getDirectories: ts.sys.getDirectories,
145
+ resolveModuleNames: resolveAbideImports(options, moduleResolutionHost),
146
+ }
147
+ return {
148
+ service: ts.createLanguageService(host, ts.createDocumentRegistry()),
149
+ shadows,
150
+ parseErrors,
151
+ shadowText,
152
+ }
153
+ }
154
+
155
+ const verbatim = buildService((source, abidePath) =>
156
+ compileShadow(source, propsTypeOf(abidePath)),
157
+ )
158
+ /* The wrapped shadow's peek-wrap is type-directed: it asks the verbatim program which
159
+ sub-expressions are async. `getProgram()` reflects the shared overlays/versions, so the
160
+ classifier tracks unsaved edits; fail-open to `undefined` (verbatim shadow) if unavailable. */
161
+ const classifierFor = (abidePath: string): InterpolationClassifier | undefined => {
162
+ const program = verbatim.service.getProgram()
163
+ return program === undefined
164
+ ? undefined
165
+ : shadowInterpolationClassifier(program, verbatim.shadows, abidePath)
143
166
  }
144
- const service = ts.createLanguageService(host, ts.createDocumentRegistry())
167
+ const main = buildService((source, abidePath) =>
168
+ compileShadow(source, propsTypeOf(abidePath), classifierFor(abidePath)),
169
+ )
170
+ const { service, shadows, parseErrors, shadowText } = main
145
171
 
146
172
  const bump = (abidePath: string): void => {
147
173
  const fileName = suffixed(abidePath)
@@ -178,6 +204,12 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
178
204
  ]
179
205
  }
180
206
  const mappings = shadows.get(abidePath)?.mappings ?? []
207
+ /* The shadow file + checker + template boundary, for the ADR-0032 bare-async-read
208
+ suppression (mirrors `collectAbideDiagnostics` so the editor and CLI agree). */
209
+ const shadowFile = service.getProgram()?.getSourceFile(fileName)
210
+ const checker = service.getProgram()?.getTypeChecker()
211
+ const source = overlays.get(abidePath) ?? ts.sys.readFile(abidePath) ?? ''
212
+ const templateStart = templateStartOffset(source)
181
213
  return raw.flatMap((diagnostic) => {
182
214
  if (diagnostic.start === undefined) {
183
215
  return []
@@ -190,6 +222,22 @@ export function createShadowLanguageService(cwd: string): ShadowLanguageService
190
222
  if (located === undefined) {
191
223
  return []
192
224
  }
225
+ /* Drop the spurious "property missing on Promise" / "condition always defined" a
226
+ bare async read provokes — the runtime peeks the resolved value (ADR-0032). */
227
+ if (
228
+ shadowFile !== undefined &&
229
+ checker !== undefined &&
230
+ located.start >= templateStart &&
231
+ isSpuriousAsyncReadDiagnostic(
232
+ shadowFile,
233
+ checker,
234
+ diagnostic.code,
235
+ diagnostic.start,
236
+ diagnostic.length ?? 0,
237
+ )
238
+ ) {
239
+ return []
240
+ }
193
241
  return [
194
242
  {
195
243
  file: abidePath,
@@ -2,12 +2,15 @@ import { resolve } from 'node:path'
2
2
  import ts from 'typescript'
3
3
  import { messageFromError } from '../../shared/messageFromError.ts'
4
4
  import { assetModulesFile } from './assetModulesFile.ts'
5
+ import { cachedSourceFile } from './cachedSourceFile.ts'
5
6
  import { compileShadow } from './compileShadow.ts'
6
7
  import { loadShadowTsConfig } from './loadShadowTsConfig.ts'
7
8
  import { pagePropsType } from './pagePropsType.ts'
8
9
  import { resolveAbideImports } from './resolveAbideImports.ts'
9
10
  import { shadowNaming } from './shadowNaming.ts'
11
+ import { sourceFileOptionsSignature } from './sourceFileOptionsSignature.ts'
10
12
  import type { CompiledShadow } from './types/CompiledShadow.ts'
13
+ import type { InterpolationClassifier } from './types/InterpolationClassifier.ts'
11
14
 
12
15
  const { suffixed, isShadow, sourceOf } = shadowNaming
13
16
 
@@ -29,7 +32,16 @@ the project's tsconfig (lib/paths/baseUrl) so the shadows type-check against the
29
32
  same world the app does; `noUnusedLocals`/`noUnusedParameters` are forced off
30
33
  because the shadow legitimately declares scope bindings a template may not read.
31
34
  */
32
- export function createShadowProgram(cwd: string, abidePaths?: string[]): ShadowProgram {
35
+ export function createShadowProgram(
36
+ cwd: string,
37
+ abidePaths?: string[],
38
+ /* Per-file interpolation classifier (ADR-0032). When provided, each shadow peek-wraps its async
39
+ sub-expressions so they type-check against the RESOLVED value; it must be backed by a SEPARATE
40
+ verbatim program (see `checkAbide`), since a classifier reading these same wrapped shadows
41
+ would be circular. Absent ⇒ verbatim shadows (the classifier-source pass, and any caller that
42
+ doesn't need the peek types). */
43
+ classifierFor?: (abidePath: string) => InterpolationClassifier | undefined,
44
+ ): ShadowProgram {
33
45
  const { options, fileNames } = loadShadowTsConfig(cwd)
34
46
  const shadows = new Map<string, CompiledShadow>()
35
47
  const parseErrors = new Map<string, string>()
@@ -47,7 +59,11 @@ export function createShadowProgram(cwd: string, abidePaths?: string[]): ShadowP
47
59
  const shadowText = (abidePath: string): string => {
48
60
  const source = ts.sys.readFile(abidePath) ?? ''
49
61
  try {
50
- const compiled = compileShadow(source, pagePropsType(abidePath))
62
+ const compiled = compileShadow(
63
+ source,
64
+ pagePropsType(abidePath),
65
+ classifierFor?.(abidePath),
66
+ )
51
67
  shadows.set(abidePath, compiled)
52
68
  parseErrors.delete(abidePath)
53
69
  return compiled.code
@@ -61,22 +77,35 @@ export function createShadowProgram(cwd: string, abidePaths?: string[]): ShadowP
61
77
  /* Ambient declarations for bundler-handled asset imports (`*.css`, …). */
62
78
  const assets = assetModulesFile(cwd)
63
79
 
80
+ /* The parse/bind-affecting options are identical for every source in a program, so compute
81
+ the cache signature once and reuse it for every file (see `cachedSourceFile`). */
82
+ const signature = sourceFileOptionsSignature(options)
83
+
64
84
  const host = ts.createCompilerHost(options, true)
65
85
  const originalGetSourceFile = host.getSourceFile.bind(host)
66
86
  host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreate) => {
67
87
  if (fileName === assets.path) {
68
- return ts.createSourceFile(fileName, assets.content, languageVersionOrOptions, true)
88
+ return cachedSourceFile(fileName, assets.content, languageVersionOrOptions, signature)
69
89
  }
70
90
  if (isShadow(fileName)) {
71
- return ts.createSourceFile(
91
+ /* `shadowText` populates the per-program `shadows`/`parseErrors` maps as a side effect,
92
+ so it must run on every call — only its expensive TS parse is memoised by `cachedSourceFile`. */
93
+ return cachedSourceFile(
72
94
  fileName,
73
95
  shadowText(sourceOf(fileName)),
74
96
  languageVersionOrOptions,
75
- true,
97
+ signature,
76
98
  ts.ScriptKind.TS,
77
99
  )
78
100
  }
79
- return originalGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreate)
101
+ /* Real `.ts`/`.d.ts` inputs (the ~3MB of default libs plus resolved dependencies) — read the
102
+ text and reuse the parsed file across programs when it is byte-identical. Fall back to the
103
+ default host on a read failure so its missing-file / `onError` handling is preserved. */
104
+ const text = ts.sys.readFile(fileName)
105
+ if (text === undefined) {
106
+ return originalGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreate)
107
+ }
108
+ return cachedSourceFile(fileName, text, languageVersionOrOptions, signature)
80
109
  }
81
110
  host.fileExists = (fileName) =>
82
111
  fileName === assets.path ||