@abide/abide 0.41.1 → 0.43.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 (167) hide show
  1. package/AGENTS.md +234 -294
  2. package/CHANGELOG.md +82 -0
  3. package/README.md +51 -95
  4. package/package.json +3 -2
  5. package/src/abideLsp.ts +46 -8
  6. package/src/abideResolverPlugin.ts +107 -19
  7. package/src/buildCli.ts +1 -1
  8. package/src/discoveryEntry.ts +3 -3
  9. package/src/lib/bundle/BundleMenuItem.ts +1 -1
  10. package/src/lib/cli/createClient.ts +12 -12
  11. package/src/lib/cli/dispatchCommand.ts +1 -1
  12. package/src/lib/cli/printSessionStatus.ts +1 -1
  13. package/src/lib/cli/resolveCliTarget.ts +1 -1
  14. package/src/lib/cli/runCli.ts +1 -1
  15. package/src/lib/cli/types/CliManifest.ts +1 -1
  16. package/src/lib/cli/types/CliManifestEntry.ts +2 -2
  17. package/src/lib/mcp/annotationsForMethod.ts +5 -5
  18. package/src/lib/mcp/createMcpServer.ts +3 -3
  19. package/src/lib/mcp/mcpSurface.ts +14 -14
  20. package/src/lib/mcp/types/McpServerOptions.ts +1 -1
  21. package/src/lib/server/DELETE.ts +4 -4
  22. package/src/lib/server/GET.ts +4 -4
  23. package/src/lib/server/HEAD.ts +4 -4
  24. package/src/lib/server/PATCH.ts +4 -4
  25. package/src/lib/server/POST.ts +4 -4
  26. package/src/lib/server/PUT.ts +4 -4
  27. package/src/lib/server/agent.ts +4 -4
  28. package/src/lib/server/env.ts +1 -1
  29. package/src/lib/server/error.ts +49 -7
  30. package/src/lib/server/json.ts +1 -1
  31. package/src/lib/server/rpc/buildErrorConstructors.ts +19 -0
  32. package/src/lib/server/rpc/{defineVerb.ts → defineRpc.ts} +52 -34
  33. package/src/lib/server/rpc/{dispatchVerbInProcess.ts → dispatchRpcInProcess.ts} +3 -3
  34. package/src/lib/server/rpc/fieldErrorsFromIssues.ts +23 -0
  35. package/src/lib/server/rpc/{findVerbByCommandName.ts → findRpcByCommandName.ts} +5 -5
  36. package/src/lib/server/rpc/parseArgs.ts +6 -6
  37. package/src/lib/server/rpc/readBodyWithinLimit.ts +2 -2
  38. package/src/lib/server/rpc/registerRpc.ts +6 -0
  39. package/src/lib/server/rpc/{verbRegistry.ts → rpcRegistry.ts} +4 -4
  40. package/src/lib/server/rpc/{runWithVerbTimeout.ts → runWithRpcTimeout.ts} +4 -4
  41. package/src/lib/server/rpc/types/RemoteHandler.ts +9 -3
  42. package/src/lib/server/rpc/types/{VerbHelper.ts → RpcHelper.ts} +57 -23
  43. package/src/lib/server/rpc/types/{VerbRegistryEntry.ts → RpcRegistryEntry.ts} +3 -3
  44. package/src/lib/server/rpc/types/TypedResponse.ts +2 -2
  45. package/src/lib/server/rpc/unprocessed.ts +6 -7
  46. package/src/lib/server/rpc/validationError.ts +17 -0
  47. package/src/lib/server/runtime/buildInspectorSurface.ts +7 -7
  48. package/src/lib/server/runtime/buildOpenApiSpec.ts +6 -6
  49. package/src/lib/server/runtime/createRouteDispatcher.ts +7 -7
  50. package/src/lib/server/runtime/createServer.ts +4 -4
  51. package/src/lib/server/runtime/crossOriginForbidden.ts +1 -1
  52. package/src/lib/server/runtime/crossOriginGate.ts +4 -4
  53. package/src/lib/server/runtime/devClientFingerprint.ts +2 -1
  54. package/src/lib/server/runtime/logExposedSurfaces.ts +4 -4
  55. package/src/lib/server/runtime/registryManifests.ts +2 -2
  56. package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
  57. package/src/lib/server/runtime/types/InspectorRpc.ts +27 -0
  58. package/src/lib/server/runtime/types/InspectorSurface.ts +4 -4
  59. package/src/lib/server/runtime/types/RequestStore.ts +1 -1
  60. package/src/lib/server/runtime/warnUnguardedMcp.ts +2 -2
  61. package/src/lib/server/sockets/createSocketDispatcher.ts +15 -14
  62. package/src/lib/server/sockets/types/SocketOperation.ts +2 -2
  63. package/src/lib/shared/HttpError.ts +15 -1
  64. package/src/lib/shared/UNREACHABLE_STATUSES.ts +13 -0
  65. package/src/lib/shared/buildRpcRequest.ts +5 -5
  66. package/src/lib/shared/cache.ts +50 -168
  67. package/src/lib/shared/carriesBodyArgs.ts +4 -4
  68. package/src/lib/shared/createCacheStore.ts +12 -0
  69. package/src/lib/shared/createRemoteFunction.ts +11 -6
  70. package/src/lib/shared/decodeResponse.ts +2 -2
  71. package/src/lib/shared/detectRpcMethod.ts +17 -0
  72. package/src/lib/shared/emitLogRecord.ts +3 -3
  73. package/src/lib/shared/escapeRegex.ts +9 -0
  74. package/src/lib/shared/extraForwardHeaders.ts +1 -1
  75. package/src/lib/shared/fileName.ts +9 -0
  76. package/src/lib/shared/fileStem.ts +3 -1
  77. package/src/lib/shared/findExportCallSite.ts +30 -273
  78. package/src/lib/shared/forwardHeaders.ts +2 -2
  79. package/src/lib/shared/httpErrorFor.ts +26 -0
  80. package/src/lib/shared/isAsciiWhitespace.ts +5 -0
  81. package/src/lib/shared/isIdentPart.ts +9 -0
  82. package/src/lib/shared/isIdentStart.ts +8 -0
  83. package/src/lib/shared/isReadOnlyMethod.ts +4 -4
  84. package/src/lib/shared/keyForRemoteCall.ts +4 -4
  85. package/src/lib/shared/log.ts +1 -1
  86. package/src/lib/shared/outboxProbeSlot.ts +20 -0
  87. package/src/lib/shared/pending.ts +4 -1
  88. package/src/lib/shared/prepareRpcModule.ts +96 -14
  89. package/src/lib/shared/probeRegistries.ts +17 -4
  90. package/src/lib/shared/programNameForPackage.ts +3 -1
  91. package/src/lib/shared/queryStringFromArgs.ts +1 -1
  92. package/src/lib/shared/remoteMetaStore.ts +2 -2
  93. package/src/lib/shared/resolveClientFlags.ts +1 -1
  94. package/src/lib/shared/skipNonCode.ts +230 -0
  95. package/src/lib/shared/streamResponse.ts +9 -6
  96. package/src/lib/shared/stripImport.ts +6 -4
  97. package/src/lib/shared/types/CacheEntry.ts +2 -4
  98. package/src/lib/shared/types/CacheOnContext.ts +1 -11
  99. package/src/lib/shared/types/CacheStore.ts +13 -7
  100. package/src/lib/shared/types/ClientFlags.ts +1 -1
  101. package/src/lib/shared/types/ErrorConstructors.ts +17 -0
  102. package/src/lib/shared/types/ErrorDescriptor.ts +10 -0
  103. package/src/lib/shared/types/ErrorSpec.ts +11 -0
  104. package/src/lib/shared/types/HttpMethod.ts +1 -0
  105. package/src/lib/shared/types/Outbox.ts +9 -0
  106. package/src/lib/shared/types/OutboxEntry.ts +27 -0
  107. package/src/lib/shared/types/RawRemoteFunction.ts +2 -2
  108. package/src/lib/shared/types/RemoteCallable.ts +2 -2
  109. package/src/lib/shared/types/RemoteFunction.ts +21 -6
  110. package/src/lib/shared/types/RequestScopeInfo.ts +1 -1
  111. package/src/lib/shared/types/RpcErrorGuard.ts +40 -0
  112. package/src/lib/shared/types/RpcInvoker.ts +1 -1
  113. package/src/lib/shared/types/StandardSchemaV1.ts +1 -1
  114. package/src/lib/shared/types/ValidationErrorData.ts +20 -0
  115. package/src/lib/shared/url.ts +3 -3
  116. package/src/lib/shared/writeRpcDts.ts +7 -7
  117. package/src/lib/shared/writeTestRpcDts.ts +3 -3
  118. package/src/lib/test/createTestApp.ts +10 -10
  119. package/src/lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts +45 -0
  120. package/src/lib/ui/compile/abideUiPlugin.ts +2 -1
  121. package/src/lib/ui/compile/awaitPlan.ts +48 -0
  122. package/src/lib/ui/compile/compileShadow.ts +117 -31
  123. package/src/lib/ui/compile/createShadowLanguageService.ts +48 -0
  124. package/src/lib/ui/compile/desugarSignals.ts +42 -6
  125. package/src/lib/ui/compile/encodeSemanticTokens.ts +37 -0
  126. package/src/lib/ui/compile/generateBuild.ts +45 -62
  127. package/src/lib/ui/compile/generateSSR.ts +90 -60
  128. package/src/lib/ui/compile/ifPlan.ts +30 -0
  129. package/src/lib/ui/compile/makeVarNamer.ts +10 -0
  130. package/src/lib/ui/compile/offsetToPosition.ts +9 -0
  131. package/src/lib/ui/compile/parseTemplate.ts +565 -115
  132. package/src/lib/ui/compile/stripEffects.ts +17 -9
  133. package/src/lib/ui/compile/structuralBlockTokens.ts +101 -0
  134. package/src/lib/ui/compile/switchPlan.ts +22 -0
  135. package/src/lib/ui/compile/tryPlan.ts +29 -0
  136. package/src/lib/ui/compile/types/SemanticToken.ts +11 -0
  137. package/src/lib/ui/compile/types/TemplateAttr.ts +8 -2
  138. package/src/lib/ui/compile/types/TemplateNode.ts +15 -2
  139. package/src/lib/ui/createScope.ts +20 -2
  140. package/src/lib/ui/dom/appendText.ts +1 -5
  141. package/src/lib/ui/dom/applyResolved.ts +1 -5
  142. package/src/lib/ui/dom/disposeRange.ts +6 -10
  143. package/src/lib/ui/dom/hydrate.ts +2 -5
  144. package/src/lib/ui/dom/isComment.ts +6 -0
  145. package/src/lib/ui/dom/mount.ts +1 -2
  146. package/src/lib/ui/dom/withScope.ts +6 -9
  147. package/src/lib/ui/effect.ts +4 -2
  148. package/src/lib/ui/navigate.ts +40 -13
  149. package/src/lib/ui/outbox.ts +30 -110
  150. package/src/lib/ui/remoteProxy.ts +160 -10
  151. package/src/lib/ui/router.ts +3 -3
  152. package/src/lib/ui/rpcOutbox/createOutboxQueue.ts +281 -0
  153. package/src/lib/ui/rpcOutbox/outboxRegistry.ts +69 -0
  154. package/src/lib/ui/runtime/createDoc.ts +3 -3
  155. package/src/lib/ui/runtime/pathSegments.ts +10 -0
  156. package/src/lib/ui/socketChannel.ts +7 -1
  157. package/src/lib/ui/types/Scope.ts +20 -5
  158. package/template/.zed/settings.json +4 -0
  159. package/template/CLAUDE.md +1 -1
  160. package/template/src/server/rpc/getHello.ts +3 -3
  161. package/template/src/ui/pages/page.abide +4 -4
  162. package/template/test/app.test.ts +2 -2
  163. package/src/lib/server/rpc/registerVerb.ts +0 -6
  164. package/src/lib/server/runtime/types/InspectorVerb.ts +0 -27
  165. package/src/lib/shared/detectVerbMethod.ts +0 -17
  166. package/src/lib/shared/types/HttpVerb.ts +0 -1
  167. package/src/lib/ui/types/Outbox.ts +0 -14
@@ -1,4 +1,5 @@
1
1
  import { assertExhaustive } from '../../shared/assertExhaustive.ts'
2
+ import { escapeRegex } from '../../shared/escapeRegex.ts'
2
3
  import { OUTLET_CLOSE, OUTLET_OPEN } from '../runtime/OUTLET_MARKER.ts'
3
4
  import { OUTLET_TAG } from '../runtime/OUTLET_TAG.ts'
4
5
  import {
@@ -7,17 +8,22 @@ import {
7
8
  RANGE_OPEN as RANGE_OPEN_DATA,
8
9
  } from '../runtime/RANGE_MARKER.ts'
9
10
  import { asOutlet } from './asOutlet.ts'
11
+ import { awaitPlan } from './awaitPlan.ts'
10
12
  import { composeProps } from './composeProps.ts'
11
13
  import { groupBindParts } from './groupBindParts.ts'
14
+ import { ifPlan } from './ifPlan.ts'
12
15
  import { isAnchorPositioned } from './isAnchorPositioned.ts'
13
16
  import { lowerContext } from './lowerContext.ts'
14
- import { resolveBranches } from './resolveBranches.ts'
17
+ import { makeVarNamer } from './makeVarNamer.ts'
15
18
  import { scopeAttr } from './scopeAttr.ts'
16
19
  import { skeletonContext } from './skeletonContext.ts'
17
20
  import { spreadExcludedNames } from './spreadExcludedNames.ts'
18
21
  import { staticAttr } from './staticAttr.ts'
19
22
  import { staticTextPart } from './staticTextPart.ts'
20
23
  import { stripEffects } from './stripEffects.ts'
24
+ import { switchPlan } from './switchPlan.ts'
25
+ import { tryPlan } from './tryPlan.ts'
26
+ import type { TemplateAttr } from './types/TemplateAttr.ts'
21
27
  import type { TemplateNode } from './types/TemplateNode.ts'
22
28
  import { VOID_TAGS } from './VOID_TAGS.ts'
23
29
 
@@ -55,10 +61,9 @@ export function generateSSR(
55
61
  computedNames: ReadonlySet<string>,
56
62
  isLayout = false,
57
63
  ): string {
58
- /* Compile-time counter for unique temp var names (runtime block ids, child render
59
- results) block ids themselves are allocated at runtime via `$ctx.next++`. */
60
- let varCounter = 0
61
- const nextVar = (prefix: string): string => `${prefix}${varCounter++}`
64
+ /* Unique temp var names (child render results); runtime block ids are
65
+ allocated separately at runtime via `$ctx.next++`. */
66
+ const nextVar = makeVarNamer()
62
67
 
63
68
  /* The shared signal→`model` lowering + branch-scoped nested-script deref scope. */
64
69
  const {
@@ -90,13 +95,12 @@ export function generateSSR(
90
95
  regex metacharacters escaped, or e.g. a trailing `$` would read as an end-anchor and the
91
96
  call site would never match — leaving an un-awaited Promise stringified as `[object
92
97
  Promise]`. */
93
- const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
94
98
  /* A leading boundary that, unlike `\b`, also fires before a `$`-leading name: `\b$row`
95
99
  never matches (`$` is a non-word char, so there is no word boundary before it), which
96
100
  would silently miss every `$row(...)` call. A negative lookbehind for word-or-`$`
97
101
  matches the same call sites as `\b` for word-leading names while also catching them. */
98
102
  const callPattern = (name: string): RegExp =>
99
- new RegExp(`(?<![$\\w])${escapeRegExp(name)}\\s*\\(`)
103
+ new RegExp(`(?<![$\\w])${escapeRegex(name)}\\s*\\(`)
100
104
  /* A subtree call to any of `names` from a TEXT interpolation (`name()` / `name(args)`). */
101
105
  const subtreeCalls = (children: TemplateNode[], names: ReadonlySet<string>): boolean =>
102
106
  children.some((child) => {
@@ -215,12 +219,9 @@ export function generateSSR(
215
219
  /* `case` children are the `elseif`/`else` branches in source order; the rest are
216
220
  the `then` content. Each `elseif` becomes an `else if`, the match-less `else`
217
221
  the trailing `else`. */
218
- const branches = node.children.filter(
219
- (child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
220
- )
221
- const thenChildren = node.children.filter((child) => child.kind !== 'case')
222
- let code = `if (${lowerExpression(node.condition)}) {\n${branchContent(thenChildren, target)}}`
223
- for (const branch of branches) {
222
+ const plan = ifPlan(node)
223
+ let code = `if (${lowerExpression(node.condition)}) {\n${branchContent(plan.thenChildren, target)}}`
224
+ for (const branch of plan.branches) {
224
225
  code +=
225
226
  branch.condition !== undefined
226
227
  ? ` else if (${lowerExpression(branch.condition)}) {\n${branchContent(branch.children, target)}}`
@@ -229,20 +230,17 @@ export function generateSSR(
229
230
  return `${anchor}${openRange(target)}${code}\n${closeRange(target)}`
230
231
  }
231
232
  if (node.kind === 'switch') {
232
- const cases = node.children.filter(
233
- (child): child is Extract<TemplateNode, { kind: 'case' }> => child.kind === 'case',
234
- )
233
+ const plan = switchPlan(node)
235
234
  let code = `{ const $s = (${lowerExpression(node.subject)});\n`
236
235
  let started = false
237
- for (const branch of cases) {
236
+ for (const branch of plan.cases) {
238
237
  if (branch.match !== undefined) {
239
238
  code += `${started ? 'else ' : ''}if ($s === (${lowerExpression(branch.match)})) {\n${branchContent(branch.children, target)}}\n`
240
239
  started = true
241
240
  }
242
241
  }
243
- const fallback = cases.find((branch) => branch.match === undefined)
244
- if (fallback !== undefined) {
245
- code += `${started ? 'else ' : ''}{\n${branchContent(fallback.children, target)}}\n`
242
+ if (plan.fallback !== undefined) {
243
+ code += `${started ? 'else ' : ''}{\n${branchContent(plan.fallback.children, target)}}\n`
246
244
  }
247
245
  return `${anchor}${openRange(target)}${code}}\n${closeRange(target)}`
248
246
  }
@@ -371,8 +369,25 @@ export function generateSSR(
371
369
  for (const scope of node.scopes ?? []) {
372
370
  code += push(target, scopeAttr(scope))
373
371
  }
372
+ /* `class:`/`style:` directives collapse with any static `class`/`style` into a SINGLE
373
+ merged attribute (a duplicate attribute would be invalid and ignored). The client's
374
+ classList.toggle / style.setProperty effects re-apply the same values on hydrate, so
375
+ the merged SSR value already matches the post-mount DOM — no flash, no desync. */
376
+ const classDirectives = node.attrs.filter(
377
+ (attr): attr is Extract<TemplateAttr, { kind: 'class' }> => attr.kind === 'class',
378
+ )
379
+ const styleDirectives = node.attrs.filter(
380
+ (attr): attr is Extract<TemplateAttr, { kind: 'style' }> => attr.kind === 'style',
381
+ )
374
382
  for (const attr of node.attrs) {
375
383
  if (attr.kind === 'static') {
384
+ /* A static class/style is folded into the merge below when directives exist. */
385
+ if (
386
+ (attr.name === 'class' && classDirectives.length > 0) ||
387
+ (attr.name === 'style' && styleDirectives.length > 0)
388
+ ) {
389
+ continue
390
+ }
376
391
  code += push(target, staticAttr(attr.name, attr.value))
377
392
  } else if (attr.kind === 'expression') {
378
393
  /* present/absent semantics matching the client `attr` binding:
@@ -400,6 +415,36 @@ export function generateSSR(
400
415
  code += `${target}.push(${JSON.stringify(` ${attr.property}="`)} + $esc(${bindRead(attr.code)}) + '"');\n`
401
416
  }
402
417
  }
418
+ /* Merged class: static value + each directive's name when its expression is truthy. */
419
+ if (classDirectives.length > 0) {
420
+ const staticClass = node.attrs.find(
421
+ (attr): attr is Extract<TemplateAttr, { kind: 'static' }> =>
422
+ attr.kind === 'static' && attr.name === 'class',
423
+ )
424
+ const parts = [
425
+ ...(staticClass ? [JSON.stringify(staticClass.value)] : []),
426
+ ...classDirectives.map(
427
+ (dir) => `((${lowerExpression(dir.code)}) ? ${JSON.stringify(dir.name)} : "")`,
428
+ ),
429
+ ]
430
+ code += `${target}.push(' class="' + $esc([${parts.join(', ')}].filter(Boolean).join(' ')) + '"');\n`
431
+ }
432
+ /* Merged style: static value + each directive's `prop:value` (String()-coerced, matching
433
+ the client's style.setProperty). */
434
+ if (styleDirectives.length > 0) {
435
+ const staticStyle = node.attrs.find(
436
+ (attr): attr is Extract<TemplateAttr, { kind: 'static' }> =>
437
+ attr.kind === 'static' && attr.name === 'style',
438
+ )
439
+ const parts = [
440
+ ...(staticStyle ? [JSON.stringify(staticStyle.value)] : []),
441
+ ...styleDirectives.map(
442
+ (dir) =>
443
+ `(${JSON.stringify(`${dir.property}:`)} + String(${lowerExpression(dir.code)}))`,
444
+ ),
445
+ ]
446
+ code += `${target}.push(' style="' + $esc([${parts.join(', ')}].filter(Boolean).join(';')) + '"');\n`
447
+ }
403
448
  code += push(target, '>')
404
449
  if (!VOID_TAGS.has(node.tag)) {
405
450
  /* Each child's skeleton position (whether its reactive text interleaves into an
@@ -464,27 +509,23 @@ export function generateSSR(
464
509
  node: Extract<TemplateNode, { kind: 'await' }>,
465
510
  target: string,
466
511
  ): string {
467
- const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
468
- const finallyChildren = finallyBranch?.children ?? []
469
- const resolvedChildren = node.children.filter((child) => child.kind !== 'branch')
470
- const resolvedAs = node.as ?? '_value'
471
- const errorAs = catchBranch?.as ?? '_error'
512
+ const plan = awaitPlan(node)
472
513
  const id = nextVar('$aid')
473
514
  let code = `const ${id} = $ctx.next++;\n`
474
515
  code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
475
516
  code += `try {\n`
476
- code += `const ${resolvedAs} = await (${lowerExpression(node.promise)});\n`
477
- code += branchContent(resolvedChildren, target)
478
- code += branchContent(finallyChildren, target)
479
- code += `$resume[${id}] = { ok: true, value: ${resolvedAs} };\n`
480
- if (catchBranch === undefined && finallyChildren.length === 0) {
517
+ code += `const ${plan.resolvedAs} = await (${lowerExpression(node.promise)});\n`
518
+ code += branchContent(plan.resolvedChildren, target)
519
+ code += branchContent(plan.finallyChildren, target)
520
+ code += `$resume[${id}] = { ok: true, value: ${plan.resolvedAs} };\n`
521
+ if (plan.surfaceRejection) {
481
522
  /* No catch/finally → let the rejection surface instead of an empty branch. */
482
523
  code += `} catch (_error) { throw _error; }\n`
483
524
  } else {
484
- code += `} catch (${errorAs}) {\n`
485
- code += branchContent(catchBranch?.children ?? [], target)
486
- code += branchContent(finallyChildren, target)
487
- code += `$resume[${id}] = { ok: false, error: String(${errorAs}) };\n`
525
+ code += `} catch (${plan.catchAs}) {\n`
526
+ code += branchContent(plan.catchChildren, target)
527
+ code += branchContent(plan.finallyChildren, target)
528
+ code += `$resume[${id}] = { ok: false, error: String(${plan.catchAs}) };\n`
488
529
  code += `}\n`
489
530
  }
490
531
  code += `${target}.push("<!--/abide:await:" + ${id} + "-->");\n`
@@ -502,29 +543,21 @@ export function generateSSR(
502
543
  node: Extract<TemplateNode, { kind: 'await' }>,
503
544
  target: string,
504
545
  ): string {
505
- const [thenBranch, catchBranch, finallyBranch] = resolveBranches(
506
- node,
507
- 'then',
508
- 'catch',
509
- 'finally',
510
- )
511
- const finallyChildren = finallyBranch?.children ?? []
512
- const pending = node.children.filter((child) => child.kind !== 'branch')
546
+ const plan = awaitPlan(node)
513
547
  const id = nextVar('$aid')
514
548
  let code = `const ${id} = $ctx.next++;\n`
515
549
  code += `${target}.push("<!--abide:await:" + ${id} + "-->");\n`
516
- code += branchContent(pending, target)
550
+ code += branchContent(plan.pending, target)
517
551
  code += `${target}.push("<!--/abide:await:" + ${id} + "-->");\n`
518
552
  const settled = (binding: string, children: TemplateNode[]) =>
519
- `async (${binding}) => { const $o = []; ${branchContent(children, '$o')}${branchContent(finallyChildren, '$o')}return $o.join(''); }`
520
- const catchProp =
521
- catchBranch === undefined && finallyChildren.length === 0
522
- ? ''
523
- : `catch: ${settled(catchBranch?.as ?? '_error', catchBranch?.children ?? [])} `
553
+ `async (${binding}) => { const $o = []; ${branchContent(children, '$o')}${branchContent(plan.finallyChildren, '$o')}return $o.join(''); }`
554
+ const catchProp = plan.surfaceRejection
555
+ ? ''
556
+ : `catch: ${settled(plan.catchAs, plan.catchChildren)} `
524
557
  code +=
525
558
  `$awaits.push({ id: ${id}, ` +
526
559
  `promise: () => (${lowerExpression(node.promise)}), ` +
527
- `then: ${settled(thenBranch?.as ?? '_value', thenBranch?.children ?? [])}, ` +
560
+ `then: ${settled(plan.resolvedAs, plan.resolvedChildren)}, ` +
528
561
  `${catchProp}});\n`
529
562
  return code
530
563
  }
@@ -536,24 +569,21 @@ export function generateSSR(
536
569
  an enclosing boundary / the 500 / the stream). Boundary comments let hydration
537
570
  discard the server content if the client adoption fails. */
538
571
  function generateTry(node: Extract<TemplateNode, { kind: 'try' }>, target: string): string {
539
- const [catchBranch, finallyBranch] = resolveBranches(node, 'catch', 'finally')
540
- const finallyChildren = finallyBranch?.children ?? []
541
- const guarded = node.children.filter((child) => child.kind !== 'branch')
542
- const errName = catchBranch?.as ?? '_error'
572
+ const plan = tryPlan(node)
543
573
  const id = nextVar('$tid')
544
574
  const mark = nextVar('$trim')
545
575
  let code = `const ${id} = $ctx.next++;\n`
546
576
  code += `${target}.push("<!--abide:try:" + ${id} + "-->");\n`
547
577
  code += `const ${mark} = ${target}.length;\n`
548
578
  code += `try {\n`
549
- code += branchContent(guarded, target)
550
- code += branchContent(finallyChildren, target)
551
- code += `} catch (${errName}) {\n${target}.length = ${mark};\n`
552
- if (catchBranch !== undefined) {
553
- code += branchContent(catchBranch.children, target)
554
- code += branchContent(finallyChildren, target)
579
+ code += branchContent(plan.guarded, target)
580
+ code += branchContent(plan.finallyChildren, target)
581
+ code += `} catch (${plan.catchAs}) {\n${target}.length = ${mark};\n`
582
+ if (plan.hasCatch) {
583
+ code += branchContent(plan.catchChildren, target)
584
+ code += branchContent(plan.finallyChildren, target)
555
585
  } else {
556
- code += `throw ${errName};\n`
586
+ code += `throw ${plan.catchAs};\n`
557
587
  }
558
588
  code += `}\n`
559
589
  code += `${target}.push("<!--/abide:try:" + ${id} + "-->");\n`
@@ -0,0 +1,30 @@
1
+ import type { TemplateNode } from './types/TemplateNode.ts'
2
+
3
+ type Case = Extract<TemplateNode, { kind: 'case' }>
4
+
5
+ /* The structural shape of an `if` chain, resolved once so the build and SSR back-ends share
6
+ one partition of its children and only own emission. Both previously recomputed the
7
+ then-content / branch split and the elseif/else classification. */
8
+ export type IfPlan = {
9
+ /* The `then` content — every child that is not an `elseif`/`else` branch. */
10
+ thenChildren: TemplateNode[]
11
+ /* The `elseif`/`else` branches in source order (each a `case` node: `condition` set for
12
+ `elseif`, both unset for `else`). */
13
+ branches: Case[]
14
+ /* Any branch carries a condition → a cond-chain (the build lowers it through `switchBlock`);
15
+ otherwise a binary `if`/`else`. */
16
+ hasElseif: boolean
17
+ /* The match-less `else` branch, if present. */
18
+ elseBranch: Case | undefined
19
+ }
20
+
21
+ /* Partitions an `if` node's children into the shared structural plan. */
22
+ export function ifPlan(node: Extract<TemplateNode, { kind: 'if' }>): IfPlan {
23
+ const branches = node.children.filter((child): child is Case => child.kind === 'case')
24
+ return {
25
+ thenChildren: node.children.filter((child) => child.kind !== 'case'),
26
+ branches,
27
+ hasElseif: branches.some((branch) => branch.condition !== undefined),
28
+ elseBranch: branches.find((branch) => branch.condition === undefined),
29
+ }
30
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ Compile-time generator of unique temp var names: each call appends a fresh
3
+ incrementing suffix to `prefix` (`row` → `row0`, `row1`, …). Shared by the
4
+ generateSSR / generateBuild codegen passes so the two hand-mirrored traversals
5
+ allocate names one way. Runtime block ids stay separate (`$ctx.next++`).
6
+ */
7
+ export function makeVarNamer(): (prefix: string) => string {
8
+ let counter = 0
9
+ return (prefix: string): string => `${prefix}${counter++}`
10
+ }
@@ -0,0 +1,9 @@
1
+ /* An absolute offset → LSP `{ line, character }` (0-based, UTF-16 code units). */
2
+ export function offsetToPosition(
3
+ text: string,
4
+ offset: number,
5
+ ): { line: number; character: number } {
6
+ const before = text.slice(0, offset)
7
+ const line = before.split('\n').length - 1
8
+ return { line, character: offset - (before.lastIndexOf('\n') + 1) }
9
+ }