@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
@@ -1,1099 +1,25 @@
1
- import { decodeHtmlEntities } from './decodeHtmlEntities.ts'
2
- import { interpolatedTemplateLiteral } from './interpolatedTemplateLiteral.ts'
3
- import { isWhitespaceText } from './isWhitespaceText.ts'
4
- import type { TemplateAttr } from './types/TemplateAttr.ts'
1
+ import { AbideCompileError } from './AbideCompileError.ts'
2
+ import { parseTemplateRecovering } from './parseTemplateRecovering.ts'
5
3
  import type { TemplateNode } from './types/TemplateNode.ts'
6
- import type { TextPart } from './types/TextPart.ts'
7
- import { VOID_TAGS } from './VOID_TAGS.ts'
8
-
9
- /* Compiled once — the parser tests these per source character, so a per-call literal
10
- would recompile them across the whole template scan. */
11
- const WHITESPACE = /\s/
12
- const WHITESPACE_OR_GT = /[\s>]/
13
- const ATTR_NAME_END = /[\s=>/]/
14
- const TAG_NAME_END = /[\s>/]/
15
- const UPPERCASE_START = /^[A-Z]/
16
- const AWAIT_PREFIX = /^await\b/
17
- const LEADING_KEYWORD = /^\s*(\S+)/
18
4
 
19
5
  /*
20
- A minimal compile-time parser for the abide template subset: elements, text with
21
- `{expr}` interpolation, static/`{expr}`/`on<event>={expr}` attributes, and
22
- `{#if}/{#for}/{#await}/{#switch}/{#try}` block control flow. Not a full HTML parser it covers what
23
- components need and reads brace expressions with quote/nesting awareness so an
24
- expression containing `<`, `>`, or `}` parses intact. Void elements self-close.
25
- `<!-- -->` comments are dropped (no node emitted) so they leave no trace in the
26
- SSR/client output or hydration cursor.
27
-
28
- A `<style>` becomes a `style` node IN PLACE (not hoisted): its CSS body is read
29
- structurally (not via a raw-source regex) so a `<style>` sitting inside a `{expr}`
30
- or attribute — e.g. one quoted in a code sample — is read as that expression's
31
- text, never mistaken for a real style. Keeping it in the tree lets the front-end
32
- scope it to its sibling subtree (`analyzeComponent`); the node emits no DOM/markup.
6
+ The throwing facade over the error-recovering core (`parseTemplateRecovering`). The
7
+ compile path (SSR / client / type-check via `analyzeComponent` + `compileShadow`) is
8
+ FAIL-FAST: the first malformed construct must abort the build with a located error.
9
+ This delegates to the non-throwing core, then re-throws its FIRST diagnostic — the
10
+ first source-order failure as an `AbideCompileError` carrying the same message and
11
+ offset the parser recorded, so the loader resolves it to `file:line:col`.
12
+
13
+ The core keeps every diagnostic (the LSP consumes them via `parseTemplateRecovering`
14
+ directly). Well-formed input yields zero diagnostics, so this returns `{ nodes }`
15
+ unchanged no caller/type ripple, and the golden corpus stays byte-identical.
33
16
  */
34
-
35
- /* A line-leading static `import` in a nested script body. The `(?=\s)` requires
36
- whitespace after the keyword (sparing `import.meta` and no-space `import(...)`),
37
- and `(?!\s*\()` spares a dynamic `import (...)` written with whitespace before the
38
- paren — both legitimate lazy paths — so only a true static import statement matches. */
39
- const NESTED_STATIC_IMPORT = /^[ \t]*import(?=\s)(?!\s*\()/m
40
-
41
- /* A braced template expression with the absolute source offset of its first
42
- (post-trim) character, so the type-checking shadow can map a diagnostic back. */
43
- type Braced = { code: string; loc: number }
44
-
45
17
  export function parseTemplate(source: string, baseOffset = 0): { nodes: TemplateNode[] } {
46
- let cursor = 0
47
-
48
- /* Reads a `{...}` expression starting at `cursor` (on the `{`), tracking
49
- string literals and nested braces so the matching `}` is found. `loc` is the
50
- absolute offset (baseOffset-relative) of the trimmed code's first char. */
51
- function readBracedExpression(): Braced {
52
- cursor += 1 // past '{'
53
- const start = cursor
54
- let depth = 1
55
- while (cursor < source.length && depth > 0) {
56
- const char = source.charAt(cursor)
57
- if (char === '"' || char === "'" || char === '`') {
58
- cursor += 1
59
- while (cursor < source.length && source.charAt(cursor) !== char) {
60
- if (source.charAt(cursor) === '\\') {
61
- cursor += 1
62
- }
63
- cursor += 1
64
- }
65
- } else if (char === '{') {
66
- depth += 1
67
- } else if (char === '}') {
68
- depth -= 1
69
- }
70
- cursor += 1
71
- }
72
- const raw = source.slice(start, cursor - 1)
73
- const leading = raw.length - raw.trimStart().length
74
- return { code: raw.trim(), loc: baseOffset + start + leading }
75
- }
76
-
77
- /* Skips an HTML comment starting at `cursor` (on `<!--`), advancing past its
78
- `-->`; an unterminated comment runs to the end of source. Emits no node. */
79
- function skipComment(): void {
80
- const close = source.indexOf('-->', cursor)
81
- cursor = close === -1 ? source.length : close + '-->'.length
82
- }
83
-
84
- /* True when `cursor` is on a `<style>` open tag — read raw (`readStyle`) so its
85
- CSS braces never misparse as `{expr}` interpolations. */
86
- function atStyleTag(): boolean {
87
- return (
88
- source.startsWith('<style', cursor) && WHITESPACE_OR_GT.test(source.charAt(cursor + 6))
89
- )
90
- }
91
-
92
- /* Reads a `<style>…</style>` block into a `style` node carrying its CSS body; an
93
- unterminated block runs to end. The body is read raw (not parsed as markup) so
94
- CSS braces never misparse as `{expr}`. */
95
- function readStyle(): TemplateNode {
96
- const openEnd = source.indexOf('>', cursor)
97
- const close = source.indexOf('</style>', cursor)
98
- const css =
99
- openEnd !== -1 && (close === -1 || openEnd < close)
100
- ? source.slice(openEnd + 1, close === -1 ? source.length : close).trim()
101
- : ''
102
- cursor = close === -1 ? source.length : close + '</style>'.length
103
- return { kind: 'style', css }
104
- }
105
-
106
- /* True when the cursor sits on a block-directive open `{#`. A `{:`/`{/` is only
107
- valid INSIDE a block (consumed by readBlockChildren), so at top level it would be
108
- a stray-branch error — handled where blocks are read, not here. */
109
- function atBlock(): boolean {
110
- return source.charAt(cursor) === '{' && source.charAt(cursor + 1) === '#'
111
- }
112
-
113
- /* `{children()}` is the slot fill point — the content a parent passed (a component)
114
- or the route chain below (a layout). It parses to the SAME node the retired `<slot>`
115
- element produced, so every downstream helper that branches on `tag === 'slot'` is
116
- unchanged. */
117
- const CHILDREN_CALL = /^\{\s*children\s*\(\s*\)\s*\}/
118
- function atChildrenCall(): boolean {
119
- return source.charAt(cursor) === '{' && CHILDREN_CALL.test(source.slice(cursor))
120
- }
121
- function readChildrenCall(): TemplateNode {
122
- cursor = source.indexOf('}', cursor) + 1 // consume `{children()}`
123
- return { kind: 'element', tag: 'slot', attrs: [], children: [] }
124
- }
125
-
126
- /* Reads a `{#…}` / `{:…}` / `{/…}` token starting on `{`. Tracks string literals
127
- and nested braces (same scan as readBracedExpression) so a brace/quote inside an
128
- expression (`{#if obj.has('}')}`) doesn't end the token early. Returns the sigil,
129
- the trimmed directive body WITHOUT the sigil, and the absolute loc of the body's
130
- first post-trim char. */
131
- function readBlockToken(): { sigil: '#' | ':' | '/'; body: string; loc: number } {
132
- const sigil = source.charAt(cursor + 1) as '#' | ':' | '/'
133
- cursor += 2 // past `{` and the sigil
134
- const start = cursor
135
- let depth = 1
136
- while (cursor < source.length && depth > 0) {
137
- const char = source.charAt(cursor)
138
- if (char === '"' || char === "'" || char === '`') {
139
- cursor += 1
140
- while (cursor < source.length && source.charAt(cursor) !== char) {
141
- if (source.charAt(cursor) === '\\') {
142
- cursor += 1
143
- }
144
- cursor += 1
145
- }
146
- } else if (char === '{') {
147
- depth += 1
148
- } else if (char === '}') {
149
- depth -= 1
150
- }
151
- cursor += 1
152
- }
153
- const raw = source.slice(start, cursor - 1)
154
- const leading = raw.length - raw.trimStart().length
155
- return { sigil, body: raw.trim(), loc: baseOffset + start + leading }
156
- }
157
-
158
- /* The leading keyword of a directive body (`if`, `for`, `await`, `switch`, `try`,
159
- `else`, `then`, `catch`, `finally`, `case`, `default`). */
160
- function headKeyword(body: string): string {
161
- const match = body.match(LEADING_KEYWORD)
162
- return match?.[1] ?? ''
163
- }
164
-
165
- /* Reads a `{#…}` control block: the open token, its children up to a continuation
166
- `{:…}` (a branch) or close `{/…}`, recursing. Emits `if`/`each`/`await`/`switch`/`try`
167
- nodes with their `case`/`branch` children — the same TemplateNode kinds the back-ends
168
- and `skeletonContext` consume. */
169
- function readBlock(): TemplateNode {
170
- const open = readBlockToken() // sigil is '#'
171
- const keyword = headKeyword(open.body)
172
- if (keyword === 'if') {
173
- const start = open.body.indexOf('if') + 2
174
- const condition = open.body.slice(start).trim()
175
- if (condition === '') {
176
- throw new Error('[abide] {#if} requires a condition expression')
177
- }
178
- const children = readBlockChildren('if')
179
- return { kind: 'if', condition, children, loc: exprLoc(open.loc, open.body, start) }
180
- }
181
- if (keyword === 'for') {
182
- const head = parseForHead(open.body, open.loc)
183
- const children = readBlockChildren('for')
184
- return {
185
- kind: 'each',
186
- items: head.items,
187
- as: head.as,
188
- index: head.index,
189
- key: head.key,
190
- async: head.async,
191
- children,
192
- loc: head.loc,
193
- asLoc: head.asLoc,
194
- keyLoc: head.keyLoc,
195
- indexLoc: head.indexLoc,
196
- }
197
- }
198
- if (keyword === 'await') {
199
- const start = open.body.indexOf('await') + 5
200
- const afterAwait = open.body.slice(start)
201
- const thenAt = keywordAtDepthZero(afterAwait, 'then')
202
- const promise = (thenAt === -1 ? afterAwait : afterAwait.slice(0, thenAt)).trim()
203
- if (promise === '') {
204
- throw new Error('[abide] {#await} requires a promise expression')
205
- }
206
- const as =
207
- thenAt === -1
208
- ? undefined
209
- : afterAwait.slice(thenAt + 'then'.length).trim() || undefined
210
- const children = readBlockChildren('await')
211
- return {
212
- kind: 'await',
213
- promise,
214
- blocking: thenAt !== -1,
215
- as,
216
- children,
217
- loc: exprLoc(open.loc, open.body, start),
218
- asLoc:
219
- as === undefined
220
- ? undefined
221
- : exprLoc(open.loc, open.body, start + thenAt + 'then'.length),
222
- }
223
- }
224
- if (keyword === 'switch') {
225
- const start = open.body.indexOf('switch') + 6
226
- const subject = open.body.slice(start).trim()
227
- if (subject === '') {
228
- throw new Error('[abide] {#switch} requires a subject expression')
229
- }
230
- const children = readBlockChildren('switch')
231
- return { kind: 'switch', subject, children, loc: exprLoc(open.loc, open.body, start) }
232
- }
233
- if (keyword === 'try') {
234
- const children = readBlockChildren('try')
235
- return { kind: 'try', children }
236
- }
237
- if (keyword === 'snippet') {
238
- const head = open.body.slice(open.body.indexOf('snippet') + 'snippet'.length).trim()
239
- const parenAt = head.indexOf('(')
240
- const name = (parenAt === -1 ? head : head.slice(0, parenAt)).trim()
241
- if (name === '') {
242
- throw new Error('[abide] {#snippet} requires a name, e.g. {#snippet row(item)}')
243
- }
244
- /* Params ride the parens: `{#snippet row({ item })}` → `{ item }`. */
245
- const params =
246
- parenAt === -1
247
- ? undefined
248
- : head.slice(parenAt + 1, head.lastIndexOf(')')).trim() || undefined
249
- const children = readBlockChildren('snippet')
250
- return {
251
- kind: 'snippet',
252
- name,
253
- params,
254
- children,
255
- loc:
256
- parenAt === -1
257
- ? undefined
258
- : exprLoc(open.loc, open.body, open.body.indexOf('(') + 1),
259
- }
260
- }
261
- throw new Error(
262
- `[abide] unknown control block {#${keyword}} — expected if/for/await/switch/try/snippet`,
263
- )
264
- }
265
-
266
- /* Shared scan body for block/branch child loops. Reads one node at the current
267
- cursor position and pushes it onto `nodes`. Returns false when the caller's
268
- terminator fires (caller decides what to do next); returns true while scanning
269
- continues. `onBranch` fires when `{:` is seen — block-children consume it as a
270
- new branch node; branch-children let the caller exit instead. */
271
- function scanNode(nodes: TemplateNode[], onBranch: (() => boolean) | undefined): boolean {
272
- /* Branch/close tokens — delegate to the caller's terminator logic. */
273
- if (source.startsWith('{/', cursor)) {
274
- return false
275
- }
276
- if (source.startsWith('{:', cursor)) {
277
- return onBranch === undefined ? false : onBranch()
278
- }
279
- if (atBlock()) {
280
- nodes.push(readBlock())
281
- } else if (atChildrenCall()) {
282
- nodes.push(readChildrenCall())
283
- } else if (source.startsWith('<!--', cursor)) {
284
- skipComment()
285
- } else if (atStyleTag()) {
286
- nodes.push(readStyle())
287
- } else if (source.charAt(cursor) === '<') {
288
- nodes.push(readElement())
289
- } else {
290
- nodes.push(readText())
291
- }
292
- return true
293
- }
294
-
295
- /* Reads children of a block until its close `{/<keyword>}`. A continuation token
296
- `{:…}` ends the current branch's children and starts a new `case`/`branch` node
297
- (per construct). The leading children (before the first `{:…}`) are the block's
298
- own children (the `if`/`await`/`try` then-content). Returns the full children list
299
- INCLUDING the case/branch nodes, producing the same TemplateNode tree the back-ends consume. */
300
- function readBlockChildren(keyword: string): TemplateNode[] {
301
- const nodes: TemplateNode[] = []
302
- while (cursor < source.length) {
303
- const keepGoing = scanNode(nodes, () => {
304
- nodes.push(readBranch(keyword))
305
- return true
306
- })
307
- if (!keepGoing) {
308
- const close = readBlockToken() // consume the close `{/keyword}`
309
- const closeKeyword = headKeyword(close.body)
310
- /* The close must name its open block — a mismatch (`{#if}…{/for}`) or
311
- crossed nesting (`{#if}{#for}…{/if}{/for}`) would otherwise silently
312
- mis-parse into a structurally wrong tree. */
313
- if (closeKeyword !== keyword) {
314
- throw new Error(
315
- `[abide] {/${closeKeyword}} does not close the open {#${keyword}} — expected {/${keyword}}`,
316
- )
317
- }
318
- return nodes
319
- }
320
- }
321
- throw new Error(`[abide] unterminated {#${keyword}} block — missing {/${keyword}}`)
322
- }
323
-
324
- /* Reads a continuation token `{:…}` and the children up to the NEXT continuation or
325
- close, returning the branch node for the parent construct. Handles `if`-chain
326
- branches (else / else if) and `for await` catch branches. */
327
- function readBranch(parentKeyword: string): TemplateNode {
328
- const token = readBlockToken() // sigil ':'
329
- const keyword = headKeyword(token.body)
330
- const branchChildren = readBranchChildren()
331
- if (parentKeyword === 'if') {
332
- if (keyword === 'else' && headKeyword(token.body.slice(4).trim()) === 'if') {
333
- const start = token.body.indexOf('if') + 2
334
- const condition = token.body.slice(start).trim()
335
- if (condition === '') {
336
- throw new Error('[abide] {:else if} requires a condition expression')
337
- }
338
- return {
339
- kind: 'case',
340
- match: undefined,
341
- condition,
342
- children: branchChildren,
343
- loc: exprLoc(token.loc, token.body, start),
344
- }
345
- }
346
- if (keyword === 'else') {
347
- return { kind: 'case', match: undefined, children: branchChildren }
348
- }
349
- }
350
- if (parentKeyword === 'for' && keyword === 'catch') {
351
- const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
352
- return {
353
- kind: 'branch',
354
- branch: 'catch',
355
- as,
356
- children: branchChildren,
357
- asLoc:
358
- as === undefined
359
- ? undefined
360
- : exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
361
- }
362
- }
363
- if (parentKeyword === 'switch') {
364
- if (keyword === 'case') {
365
- const start = token.body.indexOf('case') + 4
366
- const match = token.body.slice(start).trim()
367
- if (match === '') {
368
- throw new Error('[abide] {:case} requires a value expression')
369
- }
370
- return {
371
- kind: 'case',
372
- match,
373
- children: branchChildren,
374
- loc: exprLoc(token.loc, token.body, start),
375
- }
376
- }
377
- if (keyword === 'default') {
378
- return { kind: 'case', match: undefined, children: branchChildren }
379
- }
380
- }
381
- if (parentKeyword === 'await') {
382
- if (keyword === 'then') {
383
- const as = token.body.slice(token.body.indexOf('then') + 4).trim() || undefined
384
- return {
385
- kind: 'branch',
386
- branch: 'then',
387
- as,
388
- children: branchChildren,
389
- asLoc:
390
- as === undefined
391
- ? undefined
392
- : exprLoc(token.loc, token.body, token.body.indexOf('then') + 4),
393
- }
394
- }
395
- if (keyword === 'catch') {
396
- const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
397
- return {
398
- kind: 'branch',
399
- branch: 'catch',
400
- as,
401
- children: branchChildren,
402
- asLoc:
403
- as === undefined
404
- ? undefined
405
- : exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
406
- }
407
- }
408
- if (keyword === 'finally') {
409
- return {
410
- kind: 'branch',
411
- branch: 'finally',
412
- as: undefined,
413
- children: branchChildren,
414
- }
415
- }
416
- }
417
- if (parentKeyword === 'try') {
418
- if (keyword === 'catch') {
419
- const as = token.body.slice(token.body.indexOf('catch') + 5).trim() || undefined
420
- return {
421
- kind: 'branch',
422
- branch: 'catch',
423
- as,
424
- children: branchChildren,
425
- asLoc:
426
- as === undefined
427
- ? undefined
428
- : exprLoc(token.loc, token.body, token.body.indexOf('catch') + 5),
429
- }
430
- }
431
- if (keyword === 'finally') {
432
- return {
433
- kind: 'branch',
434
- branch: 'finally',
435
- as: undefined,
436
- children: branchChildren,
437
- }
438
- }
439
- }
440
- throw new Error(`[abide] {:${keyword}} is not valid inside {#${parentKeyword}}`)
441
- }
442
-
443
- /* Children of a branch: read until the next `{:…}` or `{/…}` WITHOUT consuming it
444
- (the caller's readBlockChildren loop handles those). */
445
- function readBranchChildren(): TemplateNode[] {
446
- const nodes: TemplateNode[] = []
447
- /* Pass undefined for onBranch so scanNode returns false on `{:`, leaving it unconsumed. */
448
- while (cursor < source.length && scanNode(nodes, undefined)) {
449
- // continue
450
- }
451
- return nodes
452
- }
453
-
454
- function readText(): TemplateNode {
455
- const parts: TextPart[] = []
456
- let literal = ''
457
- while (cursor < source.length && source.charAt(cursor) !== '<') {
458
- if (source.charAt(cursor) === '{') {
459
- if (atChildrenCall()) {
460
- break // a slot fill point — handled by the enclosing scan loop
461
- }
462
- const next = source.charAt(cursor + 1)
463
- if (next === '#' || next === ':' || next === '/') {
464
- break // a block/continuation/close token — not interpolation
465
- }
466
- if (literal !== '') {
467
- parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
468
- literal = ''
469
- }
470
- const { code, loc } = readBracedExpression()
471
- parts.push({ kind: 'expression', code, loc })
472
- } else {
473
- literal += source.charAt(cursor)
474
- cursor += 1
475
- }
476
- }
477
- if (literal !== '') {
478
- parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
479
- }
480
- return { kind: 'text', parts }
481
- }
482
-
483
- function readAttributes(): TemplateAttr[] {
484
- const attrs: TemplateAttr[] = []
485
- while (cursor < source.length) {
486
- while (WHITESPACE.test(source.charAt(cursor))) {
487
- cursor += 1
488
- }
489
- const char = source.charAt(cursor)
490
- if (char === '>' || char === '/' || char === undefined) {
491
- break
492
- }
493
- /* `{...expr}` standing where an attribute name would — a spread of an object's
494
- keys onto the tag: props on a component, attributes on a native element. Only
495
- a `<template>` directive rejects it (see `readElement`). */
496
- if (char === '{') {
497
- const { code, loc } = readBracedExpression()
498
- if (!code.startsWith('...')) {
499
- throw new Error(
500
- `[abide] a bare {expr} is not a valid attribute — write {...expr} to spread an object's keys as props`,
501
- )
502
- }
503
- /* Advance `loc` past `...` and any whitespace so it points at the spread
504
- EXPRESSION, not the dots — the shadow source-map invariant (source text at
505
- `loc` equals the emitted code) requires `loc` and `code` to align. */
506
- const inner = code.slice(3)
507
- const leading = inner.length - inner.trimStart().length
508
- attrs.push({ kind: 'spread', code: inner.trim(), loc: loc + 3 + leading })
509
- continue
510
- }
511
- let name = ''
512
- const nameLoc = baseOffset + cursor // absolute offset of the attribute name
513
- while (cursor < source.length && !ATTR_NAME_END.test(source.charAt(cursor))) {
514
- name += source.charAt(cursor)
515
- cursor += 1
516
- }
517
- while (WHITESPACE.test(source.charAt(cursor))) {
518
- cursor += 1
519
- }
520
- if (source.charAt(cursor) !== '=') {
521
- attrs.push({ kind: 'static', name, value: '', bare: true, nameLoc }) // boolean attribute
522
- continue
523
- }
524
- cursor += 1 // past '='
525
- while (WHITESPACE.test(source.charAt(cursor))) {
526
- cursor += 1
527
- }
528
- if (source.charAt(cursor) === '{') {
529
- const { code, loc } = readBracedExpression()
530
- if (name.startsWith('on')) {
531
- attrs.push({ kind: 'event', event: name.slice(2), code, loc })
532
- } else if (name.startsWith('bind:')) {
533
- attrs.push({ kind: 'bind', property: name.slice(5), code, loc })
534
- } else if (name.startsWith('class:')) {
535
- attrs.push({ kind: 'class', name: name.slice(6), code, loc })
536
- } else if (name.startsWith('style:')) {
537
- attrs.push({ kind: 'style', property: name.slice(6), code, loc })
538
- } else if (name === 'attach') {
539
- attrs.push({ kind: 'attach', code, loc })
540
- } else {
541
- attrs.push({ kind: 'expression', name, code, loc, nameLoc })
542
- }
543
- } else if (source.charAt(cursor) === '"' || source.charAt(cursor) === "'") {
544
- const quote = source.charAt(cursor)
545
- cursor += 1
546
- /* Scan the quoted span into static/`{expr}` parts (the text-node model),
547
- reusing the brace/quote-aware expression reader so a `}` inside an
548
- expression doesn't close the value early. */
549
- const parts: TextPart[] = []
550
- let literal = ''
551
- while (cursor < source.length && source.charAt(cursor) !== quote) {
552
- if (source.charAt(cursor) === '{') {
553
- if (literal !== '') {
554
- parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
555
- literal = ''
556
- }
557
- const { code, loc } = readBracedExpression()
558
- parts.push({ kind: 'expression', code, loc })
559
- } else {
560
- literal += source.charAt(cursor)
561
- cursor += 1
562
- }
563
- }
564
- cursor += 1 // past closing quote
565
- if (literal !== '') {
566
- parts.push({ kind: 'static', value: decodeHtmlEntities(literal) })
567
- }
568
- /* No `{expr}` → a plain static attribute; its literal is entity-decoded
569
- like a text node (SSR/clone re-escape it), so a value such as `&amp;`
570
- round-trips instead of double-escaping. Otherwise an interpolated value. */
571
- if (parts.every((part) => part.kind === 'static')) {
572
- const value = parts
573
- .map((part) => (part.kind === 'static' ? part.value : ''))
574
- .join('')
575
- attrs.push({ kind: 'static', name, value, nameLoc })
576
- } else {
577
- attrs.push({ kind: 'interpolated', name, parts, nameLoc })
578
- }
579
- } else {
580
- /* Unquoted value (`<input type=text>`): runs to the next whitespace or
581
- `>`, per the HTML unquoted-attribute rule. No delimiter to consume. */
582
- let value = ''
583
- while (cursor < source.length && !WHITESPACE_OR_GT.test(source.charAt(cursor))) {
584
- /* Stop before a `/` that closes the tag (`<Comp x=y/>`) so the value
585
- doesn't swallow the self-closing slash and defeat detection; a `/`
586
- elsewhere (e.g. a URL `href=/a/b`) stays part of the value. */
587
- if (source.charAt(cursor) === '/' && source.charAt(cursor + 1) === '>') {
588
- break
589
- }
590
- value += source.charAt(cursor)
591
- cursor += 1
592
- }
593
- /* Decode entities like the quoted path does — a real HTML parser decodes
594
- unquoted attribute values too, so `value=a&amp;b` is the value `a&b`. */
595
- attrs.push({ kind: 'static', name, value: decodeHtmlEntities(value) })
596
- }
597
- }
598
- return attrs
599
- }
600
-
601
- function readElement(): TemplateNode {
602
- cursor += 1 // past '<'
603
- let tag = ''
604
- const tagStart = cursor // absolute (baseOffset-relative) offset of the tag name
605
- while (cursor < source.length && !TAG_NAME_END.test(source.charAt(cursor))) {
606
- tag += source.charAt(cursor)
607
- cursor += 1
608
- }
609
- const attrs = readAttributes()
610
- let selfClosing = false
611
- if (source.charAt(cursor) === '/') {
612
- selfClosing = true
613
- cursor += 1
614
- }
615
- cursor += 1 // past '>'
616
- /* A nested `<script>` is a scoped reactive block: its body is raw JS read
617
- verbatim to its `</script>` (not parsed as markup), scoped by the
618
- containing branch when compiled. */
619
- if (tag === 'script' && !selfClosing) {
620
- /* Body starts at the current cursor; its absolute source offset lets the shadow
621
- map a diagnostic raised inside it (e.g. a top-level await) back to source. */
622
- const bodyLoc = baseOffset + cursor
623
- const close = source.indexOf('</script>', cursor)
624
- const end = close === -1 ? source.length : close
625
- const code = source.slice(cursor, end)
626
- cursor = close === -1 ? source.length : end + '</script>'.length
627
- /* A static `import` can't live here: a nested script compiles INTO the
628
- branch's render-function body, where an import is illegal — and an
629
- import nested in a branch falsely implies conditional/lazy loading ES
630
- imports can't do (they hoist module-wide and load unconditionally). The
631
- leading `<script>` hoists imports to module scope for the whole template,
632
- so they belong there. The pattern spares dynamic `import(...)` (with or
633
- without whitespace) and `import.meta` — the real lazy paths. */
634
- if (NESTED_STATIC_IMPORT.test(code)) {
635
- throw new Error(
636
- "import statements must live in the component's leading <script>, not a nested <template> script — they hoist to module scope for the whole template. For lazy loading, use a dynamic import() inside an effect.",
637
- )
638
- }
639
- return { kind: 'script', code, loc: bodyLoc }
640
- }
641
- /* A capitalised tag is a child component; its attributes become props and
642
- its children become slot content (rendered where the child puts <slot>). */
643
- if (UPPERCASE_START.test(tag)) {
644
- const slotted = selfClosing ? [] : readChildren(tag)
645
- return {
646
- kind: 'component',
647
- name: tag,
648
- loc: baseOffset + tagStart,
649
- props: toProps(attrs),
650
- children: slotted,
651
- }
652
- }
653
- /* `{...expr}` spreads onto a component (its props) or a native element (its
654
- attributes), but a `<template>` directive has no such bag — reject it there. */
655
- if (tag === 'template' && attrs.some((attr) => attr.kind === 'spread')) {
656
- throw new Error('[abide] {...expr} spread is not supported on a <template> directive')
657
- }
658
- const children = selfClosing || VOID_TAGS.has(tag) ? [] : readChildren(tag)
659
- if (tag === 'slot') {
660
- throw new Error(
661
- '[abide] the <slot> element was removed — render passed content with {children()} (with {#if children}{children()}{:else}…{/if} for a fallback)',
662
- )
663
- }
664
- if (tag === 'template') {
665
- return toSnippetOrTemplate(attrs, children)
666
- }
667
- return { kind: 'element', tag, attrs, children }
668
- }
669
-
670
- /* A `{:…}`/`{/…}` reached OUTSIDE a block (the root scan or an element's children —
671
- not readBlockChildren/readBranchChildren, which consume their own) is a continuation
672
- or close with no open `{#…}`. Surface it as an error: readText breaks on `{:`/`{/`
673
- without advancing, so falling through would loop forever. */
674
- function throwIfStrayBranch(): void {
675
- if (source.startsWith('{:', cursor) || source.startsWith('{/', cursor)) {
676
- const end = source.indexOf('}', cursor)
677
- const token = source.slice(cursor, end === -1 ? source.length : end + 1)
678
- throw new Error(`[abide] ${token} has no open {#…} block`)
679
- }
680
- }
681
-
682
- function readChildren(closeTag: string): TemplateNode[] {
683
- const nodes: TemplateNode[] = []
684
- while (cursor < source.length) {
685
- if (source.startsWith(`</${closeTag}`, cursor)) {
686
- cursor = source.indexOf('>', cursor) + 1
687
- break
688
- }
689
- throwIfStrayBranch()
690
- if (source.startsWith('<!--', cursor)) {
691
- skipComment()
692
- } else if (atStyleTag()) {
693
- nodes.push(readStyle())
694
- } else if (atBlock()) {
695
- nodes.push(readBlock())
696
- } else if (atChildrenCall()) {
697
- nodes.push(readChildrenCall())
698
- } else if (source.charAt(cursor) === '<') {
699
- nodes.push(readElement())
700
- } else {
701
- nodes.push(readText())
702
- }
703
- }
704
- return nodes
705
- }
706
-
707
- const roots: TemplateNode[] = []
708
- while (cursor < source.length) {
709
- throwIfStrayBranch()
710
- if (source.startsWith('<!--', cursor)) {
711
- skipComment()
712
- } else if (atStyleTag()) {
713
- roots.push(readStyle())
714
- } else if (atBlock()) {
715
- roots.push(readBlock())
716
- } else if (atChildrenCall()) {
717
- roots.push(readChildrenCall())
718
- } else if (source.charAt(cursor) === '<') {
719
- roots.push(readElement())
720
- } else {
721
- roots.push(readText())
722
- }
723
- }
724
- rejectStrayBranches(roots, undefined)
725
- return { nodes: roots }
726
- }
727
-
728
- /* Finds the index of a ` <token> ` keyword (` of `, ` by `) at brace/paren/bracket
729
- depth 0, scanning left to right, skipping string literals. Returns -1 if absent. */
730
- function indexOfKeywordAtDepthZero(text: string, keyword: string): number {
731
- let depth = 0
732
- let i = 0
733
- while (i < text.length) {
734
- const char = text.charAt(i)
735
- if (char === '"' || char === "'" || char === '`') {
736
- i += 1
737
- while (i < text.length && text.charAt(i) !== char) {
738
- if (text.charAt(i) === '\\') {
739
- i += 1
740
- }
741
- i += 1
742
- }
743
- } else if (char === '{' || char === '(' || char === '[') {
744
- depth += 1
745
- } else if (char === '}' || char === ')' || char === ']') {
746
- depth -= 1
747
- } else if (depth === 0 && text.startsWith(keyword, i)) {
748
- return i
749
- }
750
- i += 1
751
- }
752
- return -1
753
- }
754
-
755
- /* Index of a bare keyword (`then`) at brace/paren/bracket depth 0, requiring whitespace
756
- on both sides — so it isn't matched inside an identifier (`q.then(x)`) and MAY sit
757
- terminally. Unlike ` of `/` by `, an await `then` can end the head: `{#await p then}`
758
- is a blocking await with no value binding, which a trailing-space match would miss
759
- (folding `then` into the promise). Skips string literals. -1 if absent. */
760
- function keywordAtDepthZero(text: string, word: string): number {
761
- let depth = 0
762
- let i = 0
763
- while (i < text.length) {
764
- const char = text.charAt(i)
765
- if (char === '"' || char === "'" || char === '`') {
766
- i += 1
767
- while (i < text.length && text.charAt(i) !== char) {
768
- if (text.charAt(i) === '\\') {
769
- i += 1
770
- }
771
- i += 1
772
- }
773
- } else if (char === '{' || char === '(' || char === '[') {
774
- depth += 1
775
- } else if (char === '}' || char === ')' || char === ']') {
776
- depth -= 1
777
- } else if (
778
- depth === 0 &&
779
- text.startsWith(word, i) &&
780
- i > 0 &&
781
- WHITESPACE.test(text.charAt(i - 1)) &&
782
- (i + word.length === text.length || WHITESPACE.test(text.charAt(i + word.length)))
783
- ) {
784
- return i
785
- }
786
- i += 1
787
- }
788
- return -1
789
- }
790
-
791
- /* The depth-0 comma index in a binding (`{id, title}, i` → the comma after `}`),
792
- so a destructuring pattern's inner commas don't split the binding from its index. */
793
- function bindingCommaAtDepthZero(text: string): number {
794
- let depth = 0
795
- let i = 0
796
- while (i < text.length) {
797
- const char = text.charAt(i)
798
- if (char === '{' || char === '(' || char === '[') {
799
- depth += 1
800
- } else if (char === '}' || char === ')' || char === ']') {
801
- depth -= 1
802
- } else if (char === ',' && depth === 0) {
803
- return i
804
- }
805
- i += 1
806
- }
807
- return -1
808
- }
809
-
810
- /* Absolute source offset of the trimmed expression beginning at `start` within a
811
- directive `body` whose first char is at absolute `bodyLoc` — skips the leading
812
- whitespace `.trim()` drops so the offset points at the expression's first real char
813
- (the shadow source-map invariant: source text at `loc` equals the emitted code). */
814
- function exprLoc(bodyLoc: number, body: string, start: number): number {
815
- const raw = body.slice(start)
816
- return bodyLoc + start + (raw.length - raw.trimStart().length)
817
- }
818
-
819
- /* Parses `for [await] <binding>[, <index>] of <iterable> [by <key>]`. Offsets are
820
- tracked against the original `body` (not trimmed slices) so `loc` points at the
821
- iterable expression's first char — `bodyLoc` is the absolute offset of `body[0]`. */
822
- function parseForHead(
823
- body: string,
824
- bodyLoc: number,
825
- ): {
826
- items: string
827
- as: string
828
- index: string | undefined
829
- key: string | undefined
830
- async: boolean
831
- loc: number
832
- asLoc: number | undefined
833
- keyLoc: number | undefined
834
- indexLoc: number | undefined
835
- } {
836
- const leadingSpace = (text: string): number => text.length - text.trimStart().length
837
- const skipSpace = (i: number): number => {
838
- let at = i
839
- while (at < body.length && WHITESPACE.test(body.charAt(at))) {
840
- at += 1
841
- }
842
- return at
843
- }
844
- let bindingStart = skipSpace(body.indexOf('for') + 3)
845
- const isAsync = AWAIT_PREFIX.test(body.slice(bindingStart))
846
- if (isAsync) {
847
- bindingStart = skipSpace(bindingStart + 'await'.length)
848
- }
849
- const region = body.slice(bindingStart)
850
- const ofAt = indexOfKeywordAtDepthZero(region, ' of ')
851
- if (ofAt === -1) {
852
- throw new Error('[abide] {#for} requires `<binding> of <iterable>`')
853
- }
854
- const left = region.slice(0, ofAt).trim()
855
- const itemsStart = skipSpace(bindingStart + ofAt + ' of '.length)
856
- let itemsRegion = body.slice(itemsStart)
857
- const byAt = indexOfKeywordAtDepthZero(itemsRegion, ' by ')
858
- const keyRaw = byAt === -1 ? '' : itemsRegion.slice(byAt + ' by '.length)
859
- const key = byAt === -1 ? undefined : keyRaw.trim()
860
- const keyLoc =
861
- byAt === -1 ? undefined : bodyLoc + itemsStart + byAt + ' by '.length + leadingSpace(keyRaw)
862
- if (byAt !== -1) {
863
- itemsRegion = itemsRegion.slice(0, byAt)
864
- }
865
- const commaAt = bindingCommaAtDepthZero(left)
866
- const as = (commaAt === -1 ? left : left.slice(0, commaAt)).trim()
867
- const indexRaw = commaAt === -1 ? '' : left.slice(commaAt + 1)
868
- const index = commaAt === -1 ? undefined : indexRaw.trim()
869
- /* `left` is trimmed and starts at `bindingStart` (skipSpace'd), so the binding
870
- name begins exactly there; the index sits past the comma. */
871
- return {
872
- items: itemsRegion.trim(),
873
- as: as === '' ? '_item' : as,
874
- index,
875
- key,
876
- async: isAsync,
877
- loc: bodyLoc + itemsStart,
878
- asLoc: as === '' ? undefined : bodyLoc + bindingStart,
879
- keyLoc,
880
- indexLoc:
881
- commaAt === -1
882
- ? undefined
883
- : bodyLoc + bindingStart + commaAt + 1 + leadingSpace(indexRaw),
884
- }
885
- }
886
-
887
- /* A `case` node (`<template else>`/`<template case>`/`<template default>`) is valid
888
- only as a direct child of its `<template if>`/`<template switch>`; a `branch`
889
- (`then`/`catch`/`finally`) only inside its `<template await>`/`<template try>`.
890
- A sibling `<template else>` — closed off from its `if` — parses to a stray `case`
891
- sitting beside the `if`, which would otherwise be silently dropped. Reject it so
892
- the mistake surfaces at compile time. Recurses so a stray branch nested anywhere
893
- is caught, not just at the root. */
894
- function rejectStrayBranches(
895
- nodes: TemplateNode[],
896
- parentKind: TemplateNode['kind'] | undefined,
897
- ): void {
898
- for (const node of nodes) {
899
- if (node.kind === 'case' && parentKind !== 'if' && parentKind !== 'switch') {
900
- throw new Error(
901
- '[abide] <template else>/<template case> must be nested inside its <template if>/<template switch> — a sibling branch is not supported',
902
- )
903
- }
904
- /* `elseif` is an `if`-chain branch; inside a `switch` it would silently read as the
905
- default (match-less), so reject the cross-construct mix. */
906
- if (node.kind === 'case' && node.condition !== undefined && parentKind === 'switch') {
907
- throw new Error(
908
- '[abide] <template elseif> belongs to a <template if> chain, not a <template switch> — use <template case>',
909
- )
910
- }
911
- if (
912
- node.kind === 'branch' &&
913
- parentKind !== 'await' &&
914
- parentKind !== 'try' &&
915
- parentKind !== 'each'
916
- ) {
917
- throw new Error(
918
- '[abide] <template then>/<template catch>/<template finally> must be nested inside its <template await>/<template try>/<template each await>',
919
- )
920
- }
921
- if (node.kind === 'if' || node.kind === 'switch') {
922
- rejectMisplacedBranchContent(node)
923
- }
924
- if ('children' in node) {
925
- rejectStrayBranches(node.children, node.kind)
926
- }
927
- }
928
- }
929
-
930
- /* A `<template if>` chain's `then` content precedes its first branch tag
931
- (`elseif`/`else`); a `<template switch>` renders only its branch tags. So rendered
932
- content sitting AFTER the first branch in an `if` — or ANYWHERE in a `switch` —
933
- belongs to no branch: today it silently folds into `then` / is dropped. Reject it so
934
- the misplacement surfaces. Whitespace stays transparent, and `<script>`/`<style>` are
935
- directives (scoping, not rendered output), so both remain legal anywhere. */
936
- function rejectMisplacedBranchContent(
937
- node: Extract<TemplateNode, { kind: 'if' | 'switch' }>,
938
- ): void {
939
- const firstBranch = node.children.findIndex((child) => child.kind === 'case')
940
- node.children.forEach((child, index) => {
941
- const isRenderedContent =
942
- child.kind !== 'case' &&
943
- child.kind !== 'script' &&
944
- child.kind !== 'style' &&
945
- !isWhitespaceText(child)
946
- /* In a switch nothing but branches renders, so every position is illegal; in an if
947
- only content past the first branch is (the leading then-content is legal). */
948
- const illegalPosition =
949
- node.kind === 'switch' || (firstBranch !== -1 && index > firstBranch)
950
- if (isRenderedContent && illegalPosition) {
951
- throw new Error(
952
- node.kind === 'switch'
953
- ? '[abide] a <template switch> renders only its <template case>/<template default> branches — move stray content into a branch'
954
- : '[abide] content after a <template elseif>/<template else> belongs to no branch — the then-content must precede the first branch tag',
955
- )
956
- }
957
- })
958
- /* In an `if` chain the match-less `<template else>` is the trailing block, so any
959
- branch after it (a second `else`, or an `elseif`) compiles to invalid `} else {…}
960
- else if {…}` (SSR/type-shadow) or a silently-shadowed branch (the `switchBlock`
961
- default wins). Reject so the misordering surfaces here, not as opaque codegen. */
962
- if (node.kind === 'if') {
963
- const branches = node.children.filter(
964
- (child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
965
- )
966
- const elseIndex = branches.findIndex(
967
- (branch) => branch.match === undefined && branch.condition === undefined,
968
- )
969
- if (elseIndex !== -1 && elseIndex < branches.length - 1) {
970
- throw new Error(
971
- '[abide] <template else> must be the last branch of its <template if> chain — no <template elseif>/<template else> may follow it',
972
- )
973
- }
974
- }
975
- }
976
-
977
- /* Turns a component's attributes into props. A component has no directives —
978
- every attribute is a prop under its written name, so `on*`/`bind:`/`attach`
979
- round-trip to their original names (the kinds the tag-blind attribute parser
980
- assigned) instead of being dropped. A static value becomes a string literal —
981
- a bare attribute coerces to `true` instead; every other kind keeps its `code`,
982
- letting a prop hold any value, functions included (e.g. an `onclick` callback). */
983
- function toProps(
984
- attrs: TemplateAttr[],
985
- ): { name: string; code: string; loc?: number; nameLoc?: number; spread?: boolean }[] {
986
- return attrs.map((attr) => {
987
- /* A `{...expr}` spread carries no name — its keys merge in at runtime
988
- (`mergeProps`/`spreadProps`); `spread: true` marks it for the back-ends. */
989
- if (attr.kind === 'spread') {
990
- return { name: '', code: attr.code, loc: attr.loc, spread: true }
991
- }
992
- if (attr.kind === 'static') {
993
- /* A bare attribute (`<Toggle on />`) is a boolean flag: coerce it to
994
- `true` so the prop reads as a boolean, not the empty string a native
995
- element would serialise. An explicit `on=""` stays the empty string. */
996
- return {
997
- name: attr.name,
998
- code: attr.bare ? 'true' : JSON.stringify(attr.value),
999
- nameLoc: attr.nameLoc,
1000
- }
1001
- }
1002
- /* `label="hi {name}"` — a string-valued prop, the template-literal concatenation
1003
- of its parts (lowered downstream like any other prop value). */
1004
- if (attr.kind === 'interpolated') {
1005
- return {
1006
- name: attr.name,
1007
- code: interpolatedTemplateLiteral(attr.parts),
1008
- nameLoc: attr.nameLoc,
1009
- }
1010
- }
1011
- /* Every non-static kind keeps its `code`/`loc`; only the prop name differs —
1012
- a directive (`event`/`bind`/`class`/`style`/`attach`) round-trips to its written
1013
- name as a passthrough prop. */
1014
- const name =
1015
- attr.kind === 'event'
1016
- ? `on${attr.event}`
1017
- : attr.kind === 'bind'
1018
- ? `bind:${attr.property}`
1019
- : attr.kind === 'class'
1020
- ? `class:${attr.name}`
1021
- : attr.kind === 'style'
1022
- ? `style:${attr.property}`
1023
- : attr.kind === 'attach'
1024
- ? 'attach'
1025
- : attr.name
1026
- /* Only `expression` carries a name offset; event/bind/attach are framework-handled
1027
- passthrough excluded from the strict prop check, so theirs is unused. */
1028
- const nameLoc = attr.kind === 'expression' ? attr.nameLoc : undefined
1029
- return { name, code: attr.code, loc: attr.loc, nameLoc }
1030
- })
1031
- }
1032
-
1033
- /* The attribute's source name (`on<event>`/`bind:<property>` reconstructed). */
1034
- function attrName(attr: TemplateAttr): string {
1035
- if (attr.kind === 'event') {
1036
- return `on${attr.event}`
1037
- }
1038
- if (attr.kind === 'bind') {
1039
- return `bind:${attr.property}`
1040
- }
1041
- if (attr.kind === 'attach') {
1042
- return 'attach'
1043
- }
1044
- if (attr.kind === 'class') {
1045
- return `class:${attr.name}`
1046
- }
1047
- if (attr.kind === 'style') {
1048
- return `style:${attr.property}`
1049
- }
1050
- /* A spread has no name. `attrName` only feeds `<template>` directive lookups, and a
1051
- spread on a `<template>` is rejected at parse, so this branch is unreachable in
1052
- practice. (Spread itself IS supported — on components as props and on native
1053
- elements as attributes; only `<template>` directives reject it.) */
1054
- if (attr.kind === 'spread') {
1055
- return ''
1056
- }
1057
- return attr.name
1058
- }
1059
-
1060
- /* The control-flow attribute names that used to drive `<template>` directives — now
1061
- moved to `{#…}` blocks. A `<template>` carrying one is a migration error. */
1062
- const CONTROL_DIRECTIVES = new Set([
1063
- 'if',
1064
- 'elseif',
1065
- 'else',
1066
- 'each',
1067
- 'await',
1068
- 'then',
1069
- 'catch',
1070
- 'finally',
1071
- 'switch',
1072
- 'case',
1073
- 'default',
1074
- 'try',
1075
- ])
1076
-
1077
- /* A `<template>` is now ONLY a plain inert element (e.g. client-side cloning).
1078
- Snippet declarations moved to `{#snippet name(args)}…{/snippet}` blocks; control
1079
- flow moved to `{#…}` blocks — both throw migration errors here. */
1080
- function toSnippetOrTemplate(attrs: TemplateAttr[], children: TemplateNode[]): TemplateNode {
1081
- const find = (name: string) => attrs.find((attr) => attrName(attr) === name)
1082
- const directive = attrs.find((attr) => CONTROL_DIRECTIVES.has(attrName(attr)))
1083
- if (directive !== undefined) {
1084
- const name = attrName(directive)
1085
- const block = name === 'elseif' || name === 'else' ? 'if' : name
1086
- throw new Error(
1087
- `[abide] <template ${name}> control flow was removed — use the {#${block}…} block instead`,
1088
- )
1089
- }
1090
- /* `<template name>` snippet declarations were retired for the `{#snippet name(args)}`
1091
- block — reject with a migration error. */
1092
- if (find('name') !== undefined) {
1093
- throw new Error(
1094
- '[abide] <template name> snippet declarations were removed — use a {#snippet name(args)}…{/snippet} block',
1095
- )
18
+ const { nodes, diagnostics } = parseTemplateRecovering(source, baseOffset)
19
+ if (diagnostics.length > 0) {
20
+ // biome-ignore lint/style/noNonNullAssertion: length > 0 guarantees diagnostics[0] is defined
21
+ const first = diagnostics[0]!
22
+ throw new AbideCompileError(first.message, first.start)
1096
23
  }
1097
- /* A plain inert `<template>` element (e.g. client-side cloning) — keep as an element. */
1098
- return { kind: 'element', tag: 'template', attrs, children }
24
+ return { nodes }
1099
25
  }