octane 0.6.2 → 0.7.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 (200) hide show
  1. package/dist/cjs/constants.cjs +3 -0
  2. package/dist/cjs/dom-binding-handoff.cjs +2 -1
  3. package/dist/cjs/dom-binding-protocol.cjs +4 -3
  4. package/dist/cjs/dom-binding-styles.cjs +3 -2
  5. package/dist/cjs/dom-bindings.cjs +24 -23
  6. package/dist/cjs/error-codes.client.generated.cjs +7 -1
  7. package/dist/cjs/error-codes.server.generated.cjs +27 -1
  8. package/dist/cjs/hydration/event-capture.cjs +2 -1
  9. package/dist/cjs/independent-hydration-protocol.cjs +3 -2
  10. package/dist/cjs/index.cjs +2 -0
  11. package/dist/cjs/runtime.cjs +866 -370
  12. package/dist/cjs/runtime.server.cjs +250 -62
  13. package/dist/cjs/server/index.cjs +2 -0
  14. package/dist/cjs/server/rpc-stream.cjs +19 -12
  15. package/dist/cjs/server/streamed-signals.cjs +18 -1
  16. package/dist/cjs/shared-value-helpers.cjs +24 -2
  17. package/dist/cjs/signals/control-binding.cjs +7 -6
  18. package/dist/cjs/signals/derived-facade.cjs +2 -1
  19. package/dist/cjs/signals/document-owner.cjs +4 -3
  20. package/dist/cjs/signals/facade.cjs +10 -9
  21. package/dist/cjs/signals/graph.cjs +5 -4
  22. package/dist/cjs/signals/native-read-client.cjs +4 -3
  23. package/dist/cjs/signals/owner-context.cjs +4 -3
  24. package/dist/cjs/signals/query-attempt-observer.cjs +2 -1
  25. package/dist/cjs/signals/query-facade.cjs +2 -1
  26. package/dist/cjs/signals/scalar-computations.cjs +2 -1
  27. package/dist/cjs/signals/transition-action.cjs +10 -9
  28. package/dist/cjs/signals/transition-candidate.cjs +2 -1
  29. package/dist/cjs/version.cjs +1 -1
  30. package/dist/compiler/arm-exits.js +151 -0
  31. package/dist/compiler/compile-universal.js +262 -65
  32. package/dist/compiler/compile.js +1929 -306
  33. package/dist/compiler/deferred-snapshots.js +791 -0
  34. package/dist/compiler/strong-mode.js +59 -10
  35. package/dist/constants.d.ts +5 -1
  36. package/dist/constants.js +2 -0
  37. package/dist/error-codes.client.generated.d.ts +1 -0
  38. package/dist/error-codes.client.generated.js +5 -0
  39. package/dist/error-codes.server.generated.d.ts +5 -0
  40. package/dist/error-codes.server.generated.js +25 -0
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +2 -0
  43. package/dist/node/aria-diagnostics.js +92 -0
  44. package/dist/node/behavior-form-submissions.js +228 -0
  45. package/dist/node/behavior-root-types.js +0 -0
  46. package/dist/node/behavior-root.js +746 -0
  47. package/dist/node/class-names.js +25 -0
  48. package/dist/node/compatibility.js +10 -0
  49. package/dist/node/component-flags.js +23 -0
  50. package/dist/node/constants.js +115 -0
  51. package/dist/node/context-epoch.js +13 -0
  52. package/dist/node/context-identity.js +46 -0
  53. package/dist/node/css.js +80 -0
  54. package/dist/node/data-encoding.js +134 -0
  55. package/dist/node/devtools-hook.js +229 -0
  56. package/dist/node/dom-binding-claims.js +4 -0
  57. package/dist/node/dom-binding-classes.js +152 -0
  58. package/dist/node/dom-binding-controls.js +6 -0
  59. package/dist/node/dom-binding-handoff.js +77 -0
  60. package/dist/node/dom-binding-program.js +1617 -0
  61. package/dist/node/dom-binding-projections.js +204 -0
  62. package/dist/node/dom-binding-protocol.js +130 -0
  63. package/dist/node/dom-binding-signals.js +135 -0
  64. package/dist/node/dom-binding-styles.js +280 -0
  65. package/dist/node/dom-bindings.js +1087 -0
  66. package/dist/node/dom-focused-move.js +46 -0
  67. package/dist/node/dom-stage.js +878 -0
  68. package/dist/node/dom-tables.js +382 -0
  69. package/dist/node/error-codes.client.generated.js +1071 -0
  70. package/dist/node/error-codes.server.generated.js +761 -0
  71. package/dist/node/error-message.js +27 -0
  72. package/dist/node/event-names.js +15 -0
  73. package/dist/node/form-diagnostics.js +96 -0
  74. package/dist/node/form-submission.js +43 -0
  75. package/dist/node/has-own.js +4 -0
  76. package/dist/node/head-ownership.js +31 -0
  77. package/dist/node/hook-slot-cache.js +91 -0
  78. package/dist/node/host-property-diagnostics.js +189 -0
  79. package/dist/node/html-tree-validation.js +193 -0
  80. package/dist/node/hydration/condition.js +15 -0
  81. package/dist/node/hydration/control-capture.js +237 -0
  82. package/dist/node/hydration/document-lifecycle.js +89 -0
  83. package/dist/node/hydration/event-capture.js +362 -0
  84. package/dist/node/hydration/idle.js +23 -0
  85. package/dist/node/hydration/independent-island.js +317 -0
  86. package/dist/node/hydration/index.js +74 -0
  87. package/dist/node/hydration/interaction-config.js +61 -0
  88. package/dist/node/hydration/interaction.js +35 -0
  89. package/dist/node/hydration/load.js +15 -0
  90. package/dist/node/hydration/media.js +33 -0
  91. package/dist/node/hydration/native-intent.js +68 -0
  92. package/dist/node/hydration/never.js +12 -0
  93. package/dist/node/hydration/stream-delivery.js +345 -0
  94. package/dist/node/hydration/stream-receiver.js +190 -0
  95. package/dist/node/hydration/stream-result-receiver.js +276 -0
  96. package/dist/node/hydration/streamed-signals.js +186 -0
  97. package/dist/node/hydration/types.js +0 -0
  98. package/dist/node/hydration/visible.js +69 -0
  99. package/dist/node/hydration-markers.js +44 -0
  100. package/dist/node/independent-hydration-protocol.js +91 -0
  101. package/dist/node/index.js +463 -0
  102. package/dist/node/internal/client.js +7 -0
  103. package/dist/node/internal/context.js +5 -0
  104. package/dist/node/internal/server.js +5 -0
  105. package/dist/node/internal/signal-read.js +4 -0
  106. package/dist/node/method-dep.js +25 -0
  107. package/dist/node/profiling.js +432 -0
  108. package/dist/node/public-types.js +0 -0
  109. package/dist/node/react/fiber-adapter.js +57 -0
  110. package/dist/node/react/index.js +397 -0
  111. package/dist/node/react/react-compat-envelope.js +78 -0
  112. package/dist/node/react/react-compat-server.js +129 -0
  113. package/dist/node/react/react-compat-shared.js +104 -0
  114. package/dist/node/react/react-compat.js +223 -0
  115. package/dist/node/react/server.js +81 -0
  116. package/dist/node/react/shared.js +75 -0
  117. package/dist/node/renderer-bridge.js +35 -0
  118. package/dist/node/resize-observer.js +105 -0
  119. package/dist/node/resource-hint-diagnostics.js +67 -0
  120. package/dist/node/runtime-tags.js +22 -0
  121. package/dist/node/runtime.js +29364 -0
  122. package/dist/node/runtime.server.js +8200 -0
  123. package/dist/node/sanitize-url.js +34 -0
  124. package/dist/node/server/early-signals.js +54 -0
  125. package/dist/node/server/index.js +338 -0
  126. package/dist/node/server/rpc-batch.js +129 -0
  127. package/dist/node/server/rpc-stream.js +132 -0
  128. package/dist/node/server/rpc.js +48 -0
  129. package/dist/node/server/signal-query-observation.js +134 -0
  130. package/dist/node/server/streamed-region.js +31 -0
  131. package/dist/node/server/streamed-signals.js +574 -0
  132. package/dist/node/server-call.js +75 -0
  133. package/dist/node/server-rpc-batch-client.js +119 -0
  134. package/dist/node/server-rpc-client.js +108 -0
  135. package/dist/node/server-rpc-protocol.js +109 -0
  136. package/dist/node/server-rpc-stream-client.js +98 -0
  137. package/dist/node/shared-value-helpers.js +60 -0
  138. package/dist/node/signals/actions.js +463 -0
  139. package/dist/node/signals/client.js +22 -0
  140. package/dist/node/signals/computations.js +452 -0
  141. package/dist/node/signals/control-binding.js +388 -0
  142. package/dist/node/signals/control-handoff.js +12 -0
  143. package/dist/node/signals/derived-facade.js +25 -0
  144. package/dist/node/signals/document-owner.js +48 -0
  145. package/dist/node/signals/early-values.js +124 -0
  146. package/dist/node/signals/encoding.js +6 -0
  147. package/dist/node/signals/engine.js +668 -0
  148. package/dist/node/signals/errors.js +54 -0
  149. package/dist/node/signals/facade.js +441 -0
  150. package/dist/node/signals/graph.js +817 -0
  151. package/dist/node/signals/handle-protocol.js +11 -0
  152. package/dist/node/signals/index.js +87 -0
  153. package/dist/node/signals/native-read-client.js +319 -0
  154. package/dist/node/signals/native-read-collector.js +132 -0
  155. package/dist/node/signals/native-read-events.js +45 -0
  156. package/dist/node/signals/native-read-inspection.js +20 -0
  157. package/dist/node/signals/native-read-retry.js +30 -0
  158. package/dist/node/signals/native-read-seeds.js +243 -0
  159. package/dist/node/signals/native-read-server.js +99 -0
  160. package/dist/node/signals/owner-context.js +88 -0
  161. package/dist/node/signals/query-attempt-observer.js +42 -0
  162. package/dist/node/signals/query-facade.js +41 -0
  163. package/dist/node/signals/read-protocol.js +125 -0
  164. package/dist/node/signals/requests.js +716 -0
  165. package/dist/node/signals/scalar-computations.js +57 -0
  166. package/dist/node/signals/scope-streams.js +147 -0
  167. package/dist/node/signals/server.js +22 -0
  168. package/dist/node/signals/transition-action.js +555 -0
  169. package/dist/node/signals/transition-candidate.js +101 -0
  170. package/dist/node/signals/transition-coordinator.js +255 -0
  171. package/dist/node/signals/transition-state.js +82 -0
  172. package/dist/node/signals/types.js +20 -0
  173. package/dist/node/static/index.js +10 -0
  174. package/dist/node/stream-protocol.js +114 -0
  175. package/dist/node/streamed-signals-protocol.js +130 -0
  176. package/dist/node/style-values.js +96 -0
  177. package/dist/node/sub-slot.js +35 -0
  178. package/dist/node/testing.js +14 -0
  179. package/dist/node/trusted-html.js +6 -0
  180. package/dist/node/tsrx-iterable.js +5 -0
  181. package/dist/node/tsrx-spread.js +10 -0
  182. package/dist/node/universal-core.js +10138 -0
  183. package/dist/node/universal-dom-boundary.js +237 -0
  184. package/dist/node/universal-native.js +28 -0
  185. package/dist/node/universal.js +17 -0
  186. package/dist/node/version.js +4 -0
  187. package/dist/runtime.d.ts +219 -8
  188. package/dist/runtime.js +714 -220
  189. package/dist/runtime.server.d.ts +33 -2
  190. package/dist/runtime.server.js +217 -29
  191. package/dist/server/index.d.ts +1 -1
  192. package/dist/server/index.js +2 -0
  193. package/dist/server/rpc-stream.js +19 -12
  194. package/dist/server/streamed-signals.js +18 -1
  195. package/dist/shared-value-helpers.d.ts +11 -0
  196. package/dist/shared-value-helpers.js +21 -1
  197. package/dist/universal-core.d.ts +3 -1
  198. package/dist/universal-core.js +178 -50
  199. package/dist/version.js +1 -1
  200. package/package.json +62 -1
@@ -51,6 +51,7 @@ import { decodeHTMLStrict } from 'entities';
51
51
  import { print as esrapPrint } from 'esrap';
52
52
  import esrapTsx from 'esrap/languages/tsx';
53
53
  import { buildFatSegments } from './fat-segments.js';
54
+ import { markDeferredLive, markDeferredRead, snapshotDeferredReads } from './deferred-snapshots.js';
54
55
  import {
55
56
  METHOD_DEP_IMPORT,
56
57
  analyzeCallbackDependencies,
@@ -76,6 +77,12 @@ import {
76
77
  UNIVERSAL_THREAD_RUNTIME_IMPORTS,
77
78
  } from './compile-universal.js';
78
79
  import { compileValdi, VALDI_COMPILER_RUNTIME_IMPORTS } from './compile-valdi.js';
80
+ import {
81
+ ARM_BREAK_MESSAGE,
82
+ ARM_VALUE_RETURN_MESSAGE,
83
+ armJump,
84
+ assertTemplateJumps,
85
+ } from './arm-exits.js';
79
86
  import { HOOK_NAMES, NATIVE_SIGNAL_HOOK_NAMES } from './hook-names.js';
80
87
  export { HOOK_NAMES } from './hook-names.js';
81
88
  import {
@@ -667,9 +674,14 @@ function directSignalSite(ctx, node, kind = 'binding') {
667
674
  }
668
675
 
669
676
  function componentInvocationSite(ctx, node) {
670
- const position = node?.start ?? `${node?.loc?.start?.line ?? 0}:${node?.loc?.start?.column ?? 0}`;
671
677
  // Authored positions identify call sites independently of the server/client
672
678
  // helper names introduced for loops, branches, and value-position fragments.
679
+ // Hash line:col, not the parser offset: one call site in returned JSX reaches
680
+ // the client as the raw JSXElement but the server as a normalized template
681
+ // Element, and normalization keeps only `loc`.
682
+ const start = node?.loc?.start;
683
+ const position =
684
+ start != null ? `${start.line ?? 0}:${start.column ?? 0}` : (node?.start ?? '0:0');
673
685
  return `c:${strongHash(
674
686
  `octane:component-invocation-site:1\0${normalizeTextTypeFilename(ctx.filename) ?? ctx.filename}\0${position}`,
675
687
  )}`;
@@ -1051,6 +1063,90 @@ function rejectTextareaValueChildren(tag, node, ctx) {
1051
1063
  );
1052
1064
  }
1053
1065
 
1066
+ const TEXTAREA_CHILD_KINDS = {
1067
+ Element: 'an element',
1068
+ TSRXExpression: 'a JSX expression',
1069
+ IfStatement: 'an `@if` block',
1070
+ ForOfStatement: 'an `@for` block or a mapped JSX list',
1071
+ SwitchStatement: 'an `@switch` block',
1072
+ TryStatement: 'an `@try` block',
1073
+ };
1074
+
1075
+ // A `<textarea>`'s content is RCDATA: the HTML parser keeps markup and comments
1076
+ // inside it as literal text, so neither the client template nor the server can
1077
+ // place a `<!>` placeholder, a `<!-- -->` separator or a `<!--[-->` frame there.
1078
+ // Its children are therefore text on both sides: they fold into ONE text value
1079
+ // that the client binds as the host's only Text node (runtime `textareaText`)
1080
+ // and the server serializes markerless (`ssrTextareaText`). Returns the parts in
1081
+ // source order, or null when the ordinary only-child path already emits
1082
+ // markerless text on both sides (one static literal or one proven text hole).
1083
+ // Throws for a child that cannot be text, on both emit paths.
1084
+ function textareaTextParts(tag, ns, children, ctx) {
1085
+ if (tag !== 'textarea' || (ns !== 'html' && ns !== 'opaque') || children.length === 0) {
1086
+ return null;
1087
+ }
1088
+ const parts = [];
1089
+ for (const child of children) {
1090
+ if (child.type !== 'Text') {
1091
+ const element = child.type === 'HeadHoist' ? child.element : child;
1092
+ const kind =
1093
+ element.type === 'JSXElement' || (element.type === 'Element' && !isComponentTag(element))
1094
+ ? `\`<${jsxTagName(element) || elementTagName(element)}>\``
1095
+ : (TEXTAREA_CHILD_KINDS[child.type] ?? 'a non-text child');
1096
+ const l = (element.loc ?? child.expression?.loc)?.start;
1097
+ const at = l
1098
+ ? ` (${ctx.mapSourceName ? ctx.mapSourceName + ':' : ''}${l.line}:${l.column})`
1099
+ : '';
1100
+ throw new Error(
1101
+ `\`<textarea>\` children must be text, but it contains ${kind}. A textarea's content ` +
1102
+ 'is its default value and the HTML parser keeps markup inside it as literal ' +
1103
+ `text: move the markup outside the textarea, or pass a string expression.${at}`,
1104
+ );
1105
+ }
1106
+ // Authored binding views address each hole by its own marker range.
1107
+ if (
1108
+ child._octaneBindingText ||
1109
+ child._octaneBindingOpaque ||
1110
+ child._octaneBindingValue ||
1111
+ child._octaneBindingSlot
1112
+ ) {
1113
+ return null;
1114
+ }
1115
+ const literal = staticTextLiteral(child.expression);
1116
+ if (literal === '') continue;
1117
+ const previous = parts.at(-1);
1118
+ if (literal !== null && previous?.kind === 'static') {
1119
+ parts[parts.length - 1] = { ...previous, value: previous.value + literal };
1120
+ } else if (literal !== null) {
1121
+ parts.push({ kind: 'static', value: literal, node: child });
1122
+ } else {
1123
+ const text =
1124
+ isDirectSignalHandleExpression(child.expression) ||
1125
+ isKnownTextChildExpression(child.expression, ctx.knownStringChildLocals);
1126
+ parts.push({ kind: text ? 'text' : 'child', expr: child.expression, node: child });
1127
+ }
1128
+ }
1129
+ // Static text alone is baked into the template and serialized as one merged
1130
+ // run; a sole text hole mounts with htext and serializes unframed.
1131
+ if (parts.every((part) => part.kind === 'static')) return null;
1132
+ if (children.length === 1 && parts[0].kind === 'text') return null;
1133
+ return parts;
1134
+ }
1135
+
1136
+ // The runtime call's arguments: the parts array, then a 't' at each
1137
+ // `{x as string}` part (omitted when there is none).
1138
+ function textareaTextArgs(parts, lowerHole) {
1139
+ const values = parts.map((part) =>
1140
+ part.kind === 'static'
1141
+ ? b.literal(part.value, JSON.stringify(part.value))
1142
+ : lowerHole(part.expr, part.kind === 'text', part.node),
1143
+ );
1144
+ const textHoles = parts.map((part) => (part.kind === 'text' ? 't' : '-')).join('');
1145
+ return textHoles.includes('t')
1146
+ ? [b.array(values), b.literal(textHoles, JSON.stringify(textHoles))]
1147
+ : [b.array(values)];
1148
+ }
1149
+
1054
1150
  // React's raw-HTML contract is mutually exclusive with a non-nullish child.
1055
1151
  // Static TSRX can reject definitely contradictory shapes before either the
1056
1152
  // client or server renderer runs them. Preserve React's accepted null/undefined
@@ -1452,6 +1548,8 @@ const INTERNAL_CLIENT_RUNTIME_HELPERS = new Set([
1452
1548
  'enableSignalBindings',
1453
1549
  'createElementAt',
1454
1550
  'createElementFromConfig',
1551
+ 'isRenderCall',
1552
+ 'deferRecord',
1455
1553
  'bindSignalText',
1456
1554
  'bindSignalChild',
1457
1555
  'bindSignalAttribute',
@@ -1491,6 +1589,7 @@ const INTERNAL_SERVER_RUNTIME_HELPERS = new Set([
1491
1589
  'enableServerSignalBindings',
1492
1590
  'createElementAt',
1493
1591
  'createElementFromConfig',
1592
+ 'isRenderCall',
1494
1593
  'ssrSignalValue',
1495
1594
  'ssrSignalControlValue',
1496
1595
  'ssrSignalControlAttrs',
@@ -2686,13 +2785,88 @@ function collectComponentLocals(componentNode) {
2686
2785
  for (const stmt of stmts) {
2687
2786
  if (stmt.type === 'VariableDeclaration') {
2688
2787
  for (const d of stmt.declarations || []) collectBindings(d.id, locals);
2689
- } else if (stmt.type === 'FunctionDeclaration') {
2788
+ } else if (stmt.type === 'FunctionDeclaration' || stmt.type === 'ClassDeclaration') {
2690
2789
  if (stmt.id) locals.add(stmt.id.name);
2691
2790
  }
2692
2791
  }
2693
2792
  return locals;
2694
2793
  }
2695
2794
 
2795
+ // Every name a callback binds for code in its own body: its params, a function
2796
+ // expression's own name, and each declaration at ANY statement depth, where
2797
+ // collectComponentLocals reads only the top level. A name from a sibling block is
2798
+ // included too, which is sound only because a name is threaded when code reads it
2799
+ // free: that read already means whatever the name resolves to at the call site.
2800
+ // collectFreeIdentifiers must therefore never report a nested scope's own names
2801
+ // (a `@{ … }` block's locals, a `@catch` param) as free. Nested functions and
2802
+ // render bodies bind their own names, and are visited on their own.
2803
+ function collectCallbackBindings(fn) {
2804
+ const names = new Set();
2805
+ for (const p of fn.params || []) collectBindings(p, names);
2806
+ if (fn.type === 'FunctionExpression' && fn.id) names.add(fn.id.name);
2807
+ visit(fn.body);
2808
+ return names;
2809
+
2810
+ function visit(node) {
2811
+ if (node == null || typeof node !== 'object') return;
2812
+ if (Array.isArray(node)) {
2813
+ for (const statement of node) visit(statement);
2814
+ return;
2815
+ }
2816
+ switch (node.type) {
2817
+ case 'VariableDeclaration':
2818
+ for (const d of node.declarations || []) collectBindings(d.id, names);
2819
+ return;
2820
+ case 'FunctionDeclaration':
2821
+ case 'ClassDeclaration':
2822
+ if (node.id) names.add(node.id.name);
2823
+ return;
2824
+ case 'BlockStatement':
2825
+ visit(node.body);
2826
+ return;
2827
+ case 'IfStatement':
2828
+ visit(node.consequent);
2829
+ visit(node.alternate);
2830
+ return;
2831
+ case 'ForStatement':
2832
+ visit(node.init);
2833
+ visit(node.body);
2834
+ return;
2835
+ case 'ForInStatement':
2836
+ case 'ForOfStatement':
2837
+ visit(node.left);
2838
+ visit(node.body);
2839
+ return;
2840
+ case 'WhileStatement':
2841
+ case 'DoWhileStatement':
2842
+ case 'LabeledStatement':
2843
+ visit(node.body);
2844
+ return;
2845
+ case 'TryStatement':
2846
+ visit(node.block);
2847
+ if (node.handler?.param) collectBindings(node.handler.param, names);
2848
+ visit(node.handler?.body);
2849
+ visit(node.finalizer);
2850
+ return;
2851
+ case 'SwitchStatement':
2852
+ for (const c of node.cases || []) visit(c.consequent);
2853
+ return;
2854
+ default:
2855
+ return;
2856
+ }
2857
+ }
2858
+ }
2859
+
2860
+ // Whether `node` reads a name an enclosing callback binds. Such a name is in scope
2861
+ // where `node` is written, but not in the owning body a hoisted helper lands in.
2862
+ function readsCallbackScope(node, callbackScope) {
2863
+ if (callbackScope == null || callbackScope.size === 0) return false;
2864
+ for (const name of collectFreeIdentifiers(node, [])) {
2865
+ if (callbackScope.has(name)) return true;
2866
+ }
2867
+ return false;
2868
+ }
2869
+
2696
2870
  /**
2697
2871
  * Compute the set of component-local names that are guaranteed STABLE across
2698
2872
  * renders. Used by the auto-callback pass below to decide which `const X =
@@ -3953,6 +4127,12 @@ function collectFreeIdentifiers(root, initiallyBound, ignoreNodes = null) {
3953
4127
  walk(n.body, scope);
3954
4128
  return;
3955
4129
  }
4130
+ // The compiler's own renderable child hole (a normalized `{expr}` or
4131
+ // `@{ … }` child) shares the prefix, but its expression is runtime code.
4132
+ if (t === 'TSRXExpression') {
4133
+ walk(n.expression, scope);
4134
+ return;
4135
+ }
3956
4136
  if (t.startsWith('TS')) return;
3957
4137
 
3958
4138
  if (t === 'Identifier') {
@@ -4056,15 +4236,37 @@ function collectFreeIdentifiers(root, initiallyBound, ignoreNodes = null) {
4056
4236
  return;
4057
4237
  }
4058
4238
 
4059
- // CatchClause introduces its param.
4239
+ // CatchClause introduces its param, and a `@catch (error, reset)` clause
4240
+ // also its reset param.
4060
4241
  if (t === 'CatchClause') {
4061
4242
  const newScope = new Set(scope);
4062
4243
  if (n.param) collectBindings(n.param, newScope);
4244
+ if (n.resetParam) collectBindings(n.resetParam, newScope);
4063
4245
  if (n.param) walkPatternExpressions(n.param, newScope);
4064
4246
  walk(n.body, newScope);
4065
4247
  return;
4066
4248
  }
4067
4249
 
4250
+ // A `@{ … }` block's setup declarations are scoped to its setup and render
4251
+ // output, like a block statement's. Otherwise a block-local reads as a
4252
+ // capture of any outer name it shares, which a caller would thread from a
4253
+ // call site where that outer name may not be in scope.
4254
+ if (t === 'JSXCodeBlock') {
4255
+ const newScope = new Set(scope);
4256
+ for (const stmt of n.body || []) {
4257
+ if (stmt.type === 'VariableDeclaration') {
4258
+ for (const d of stmt.declarations || []) collectBindings(d.id, newScope);
4259
+ } else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
4260
+ newScope.add(stmt.id.name);
4261
+ }
4262
+ }
4263
+ for (const key in n) {
4264
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4265
+ walk(n[key], newScope);
4266
+ }
4267
+ return;
4268
+ }
4269
+
4068
4270
  // for / for-in / for-of — a DECLARATION left introduces bindings, but a
4069
4271
  // bare left-hand side is an ASSIGNMENT to something already in scope.
4070
4272
  if (t === 'ForStatement' || t === 'ForInStatement' || t === 'ForOfStatement') {
@@ -7620,20 +7822,39 @@ function isPlainHostRoot(node) {
7620
7822
  );
7621
7823
  }
7622
7824
 
7825
+ /**
7826
+ * True when `node` compiles to exactly one template element in place, in both
7827
+ * compile modes. Every single-root proof (keyed rows, sole-root components,
7828
+ * host-only @if/@switch arms, and the SSR ranges that mirror them) uses this
7829
+ * rather than isPlainHostRoot, because normalizeChildren plans some plain
7830
+ * hosts differently:
7831
+ * - keyed, `noscript`/document, and parser-repaired hosts lower to a
7832
+ * descriptor hole, which is an anchor plus the element;
7833
+ * - head-hoisted metadata and Float resources render nothing in place.
7834
+ * The server writes a parser-repaired host from its template but frames it in
7835
+ * the same hole range, so both modes reject it too. The planner exempts SVG
7836
+ * and `<noscript>` content from some of these rules; ignoring that context
7837
+ * here only keeps a marker pair it could have skipped.
7838
+ */
7839
+ function isSingleTemplateHost(node) {
7840
+ return (
7841
+ isPlainHostRoot(node) &&
7842
+ node._octaneImperativeHost !== true &&
7843
+ !alwaysImperativeHost(node) &&
7844
+ !isHoistableHeadElementNode(node) &&
7845
+ headResourceKind(node) === null
7846
+ );
7847
+ }
7848
+
7623
7849
  /**
7624
7850
  * A direct body host can adopt its enclosing control-flow range on hydration.
7625
7851
  *
7626
7852
  * Keep this narrower than the general single-root proof: setup statements,
7627
- * components, nested directives, and head-hoisted hosts cannot guarantee that
7628
- * the range's first child is the branch's own hydratable element.
7853
+ * components, and nested directives cannot guarantee that the range's first
7854
+ * child is the branch's own hydratable element.
7629
7855
  */
7630
7856
  function canBorrowSsrHostBranchRange(statements) {
7631
- if (statements == null || statements.length !== 1 || !isPlainHostRoot(statements[0])) {
7632
- return false;
7633
- }
7634
- const host = statements[0];
7635
- const tag = host.id?.name ?? host.openingElement?.name?.name;
7636
- return tag !== 'title' && tag !== 'meta' && tag !== 'link';
7857
+ return statements != null && statements.length === 1 && isSingleTemplateHost(statements[0]);
7637
7858
  }
7638
7859
 
7639
7860
  function statementsOf(node) {
@@ -7659,16 +7880,19 @@ function hasSwitchCaseLocalBinding(statements) {
7659
7880
  }
7660
7881
 
7661
7882
  /**
7662
- * A directive @if/@else whose every reachable arm emits exactly one plain
7663
- * host. The chosen tag may change, but the item always owns one element; the
7664
- * runtime propagates a branch replacement to enclosing shared boundaries.
7883
+ * A directive @if/@else whose every reachable arm emits exactly one template
7884
+ * host (isSingleTemplateHost). The chosen tag may change, but the item always
7885
+ * owns one element; the runtime propagates a branch replacement to enclosing
7886
+ * shared boundaries.
7665
7887
  */
7666
7888
  function isSingleHostIfRoot(node) {
7667
7889
  if (!isIfDirective(node) || node.alternate == null) return false;
7668
7890
  const armIsSingleHost = (arm) => {
7669
7891
  if (isIfDirective(arm)) return isSingleHostIfRoot(arm);
7670
- const render = statementsOf(arm).filter((s) => isJsxNode(s) || isIfDirective(s));
7671
- return render.length === 1 && isPlainHostRoot(render[0]);
7892
+ const statements = statementsOf(arm);
7893
+ if (armMayExit(statements)) return false;
7894
+ const render = statements.filter((s) => isJsxNode(s) || isIfDirective(s));
7895
+ return render.length === 1 && isSingleTemplateHost(render[0]);
7672
7896
  };
7673
7897
  return armIsSingleHost(node.consequent) && armIsSingleHost(node.alternate);
7674
7898
  }
@@ -7699,7 +7923,7 @@ function singleRootComponentArmName(node, locals, ctx) {
7699
7923
  /**
7700
7924
  * Transitive definition-site single-root proof for a void `@{}` body whose
7701
7925
  * sole root is an exhaustive `@if`/`@else` or `@switch` tree: every reachable
7702
- * arm must render exactly one plain host OR one qualifying same-module
7926
+ * arm must render exactly one template host OR one qualifying same-module
7703
7927
  * component call (see
7704
7928
  * singleRootComponentArmName). Returns the set of callee names the proof
7705
7929
  * depends on (empty when every arm is a host), or null when the shape does
@@ -7723,12 +7947,13 @@ function collectSingleRootIfDeps(render, locals, ctx) {
7723
7947
  if (isSwitchDirective(arm)) return switchOk(arm);
7724
7948
  const statements = Array.isArray(arm) ? arm : statementsOf(arm);
7725
7949
  if (insideSwitch && hasSwitchCaseLocalBinding(statements)) return false;
7950
+ if (armMayExit(statements)) return false;
7726
7951
  const out = statements.filter((s) => isJsxNode(s) || isIfDirective(s) || isSwitchDirective(s));
7727
7952
  if (out.length !== 1) return false;
7728
7953
  const sole = out[0];
7729
7954
  if (isIfDirective(sole)) return ifOk(sole, insideSwitch);
7730
7955
  if (isSwitchDirective(sole)) return switchOk(sole);
7731
- if (isPlainHostRoot(sole)) return true;
7956
+ if (isSingleTemplateHost(sole)) return true;
7732
7957
  const name = singleRootComponentArmName(sole, locals, ctx);
7733
7958
  if (name === null) return false;
7734
7959
  deps.add(name);
@@ -7828,12 +8053,13 @@ function anchorlessRootShape(node) {
7828
8053
  * mount, but its current SSR representation still begins with that construct's
7829
8054
  * own hydration markers. A direct host always begins with the row element, so
7830
8055
  * the hydrator can use it as the keyed item boundary without an item pair.
8056
+ * `itemBody` is forItemTemplateBody's result, whose root key has already been
8057
+ * folded into the item key when that is safe.
7831
8058
  */
7832
- function isSsrMarkerlessForItem(node) {
7833
- if (node?._octaneBindingSite !== undefined) return false;
7834
- const body = node?.body?.body || [];
7835
- const jsxChildren = body.filter((s) => isJsxNode(s));
7836
- return jsxChildren.length === 1 && isPlainHostRoot(jsxChildren[0]);
8059
+ function isSsrMarkerlessForItem(node, itemBody) {
8060
+ if (node?._octaneBindingSite !== undefined || armMayExit(itemBody)) return false;
8061
+ const jsxChildren = itemBody.filter((s) => isJsxNode(s));
8062
+ return jsxChildren.length === 1 && isSingleTemplateHost(jsxChildren[0]);
7837
8063
  }
7838
8064
 
7839
8065
  /**
@@ -7843,9 +8069,10 @@ function isSsrMarkerlessForItem(node) {
7843
8069
  * again. Its component call then adopts the host through the established
7844
8070
  * singleRoot path. Explicit keys, spread/children overrides, imported or
7845
8071
  * dynamic callees, and additional item statements cannot use that proof.
8072
+ * `body` is forItemTemplateBody's output, which the client's single-root proof
8073
+ * also reads, so a removed row-key attribute leaves both sides in agreement.
7846
8074
  */
7847
- function canShareSsrComponentItemRange(node, ctx) {
7848
- const body = node?.body?.body || [];
8075
+ function canShareSsrComponentItemRange(body, ctx) {
7849
8076
  if (body.length !== 1) return false;
7850
8077
  const component = body[0];
7851
8078
  if (
@@ -7929,6 +8156,13 @@ const HOST_MOUNT_SAFE_TAGS = new Set([
7929
8156
  'ul',
7930
8157
  ]);
7931
8158
 
8159
+ // Attributes whose writes carry their own lifecycle or ordering keep a row on
8160
+ // forBlock. `style` is a deliberate, conservative member: its writers also run
8161
+ // hydration comparison, hidden-Activity display enforcement and transition
8162
+ // snapshots, none of which the direct mount was audited against. The cost is
8163
+ // confined to refilling an already-mounted, empty list with at least
8164
+ // FAST_HOST_LIST_MIN_ITEMS rows: the first mount and every update, including
8165
+ // each virtualized scroll, run through forBlock whichever helper is named.
7932
8166
  const HOST_MOUNT_UNSAFE_ATTRIBUTES = new Set([
7933
8167
  'autofocus',
7934
8168
  'checked',
@@ -8065,14 +8299,18 @@ function alwaysCompletesAbruptly(statements) {
8065
8299
  * template, so normalize every component-level early return through `?? null`.
8066
8300
  * This preserves one evaluation and every renderable value while making bare or
8067
8301
  * explicitly-undefined returns an unambiguous empty output. Nested functions are
8068
- * separate execution scopes and remain untouched.
8302
+ * separate execution scopes and remain untouched. So is JSX: a return inside a
8303
+ * directive arm is that arm's early exit, which the arm lowers exactly as it
8304
+ * does in a template body, so the server and client ranges agree.
8069
8305
  */
8070
8306
  function normalizeOwnRenderableReturns(statement, preserveJsx = false) {
8071
8307
  return mapAst(statement, (node) => {
8072
8308
  if (
8073
8309
  node.type === 'FunctionDeclaration' ||
8074
8310
  node.type === 'FunctionExpression' ||
8075
- node.type === 'ArrowFunctionExpression'
8311
+ node.type === 'ArrowFunctionExpression' ||
8312
+ JSX_CHILDREN_BEARING_TYPES.has(node.type) ||
8313
+ SETUP_VALUE_DIRECTIVE_TYPES.has(node.type)
8076
8314
  )
8077
8315
  return node;
8078
8316
  if (node.type !== 'ReturnStatement') return null;
@@ -8166,7 +8404,7 @@ export function hasOnlyLowerableNullishExits(node) {
8166
8404
  }
8167
8405
 
8168
8406
  /**
8169
- * Prove that a component always returns one host element.
8407
+ * Prove that a component always returns one template host element.
8170
8408
  *
8171
8409
  * TSRX's `@{}` form carries that output as `body.render`. Ordinary TSX keeps a
8172
8410
  * real `return`; accept only a final host-element return and reject any other
@@ -8175,12 +8413,13 @@ export function hasOnlyLowerableNullishExits(node) {
8175
8413
  */
8176
8414
  function singleHostComponentRoot(node) {
8177
8415
  if (node?.body?.type === 'JSXCodeBlock') {
8178
- return isVoidJsxCodeBlockFunction(node) && isPlainHostRoot(node.body.render);
8416
+ return isVoidJsxCodeBlockFunction(node) && isSingleTemplateHost(node.body.render);
8179
8417
  }
8180
8418
  if (node?.body?.type !== 'BlockStatement') return false;
8181
8419
  const stmts = node.body.body || [];
8182
8420
  const final = stmts[stmts.length - 1];
8183
- if (!final || final.type !== 'ReturnStatement' || !isPlainHostRoot(final.argument)) return false;
8421
+ if (!final || final.type !== 'ReturnStatement' || !isSingleTemplateHost(final.argument))
8422
+ return false;
8184
8423
 
8185
8424
  let returns = 0;
8186
8425
  const seen = new WeakSet();
@@ -9771,6 +10010,7 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
9771
10010
  normalizeAuthoredJsxLiterals(parseModule(source, cleanFilename)),
9772
10011
  );
9773
10012
  analyzeTsrx(analyzedAst, cleanFilename);
10013
+ assertTemplateJumps(analyzedAst, source, cleanFilename);
9774
10014
  adoptParserAst(analyzedAst);
9775
10015
  assertNoLegacyContextProviders(analyzedAst, source, cleanFilename);
9776
10016
  options = nativeReadOptions(analyzedAst, options);
@@ -10241,6 +10481,9 @@ function compileInternal(
10241
10481
  const errorBoundaryLowering = lowerImportedErrorBoundaries(ast);
10242
10482
  ast = errorBoundaryLowering.ast;
10243
10483
  const consumedRuntimeLocals = errorBoundaryLowering.consumed;
10484
+ // A module-level `@{ … }` function that code calls directly also gets its
10485
+ // returned-JSX form, before the passes below rewrite its authored returns.
10486
+ ast = splitDirectlyCalledComponents(ast);
10244
10487
  // A null-only shorthand guard is template control flow, not an arbitrary
10245
10488
  // JavaScript return value. Lower it in every compiler mode so SSR, hydration,
10246
10489
  // and HMR share one DOM-range shape. HMR still emits the generic component
@@ -10249,6 +10492,8 @@ function compileInternal(
10249
10492
  // Same contract for React-style conditional JSX returns: branch-selected
10250
10493
  // output compiles as template control flow instead of de-opt descriptors.
10251
10494
  ast = lowerJsxReturnBranchComponents(ast);
10495
+ // A nested `@{ … }` function that code calls directly returns JSX instead.
10496
+ ast = lowerDirectlyCalledTemplateFunctions(ast);
10252
10497
  // Omitted dependency lists are compiler-owned: infer reactive captures
10253
10498
  // before any component splitting/hoisting so every lexical binding is still
10254
10499
  // visible to the shared TSRX/TSX analysis. Explicit arrays and `null` pass
@@ -10599,8 +10844,7 @@ function compileInternal(
10599
10844
  if (
10600
10845
  item.id?.type === 'Identifier' &&
10601
10846
  init?.type === 'CallExpression' &&
10602
- init.callee?.type === 'Identifier' &&
10603
- memoImportNames.has(init.callee.name) &&
10847
+ isOctaneMemoCallee(init.callee, ctx, memoImportNames) &&
10604
10848
  (init.arguments?.length ?? 0) === 1
10605
10849
  ) {
10606
10850
  ctx.defaultMemoBindings.add(item.id.name);
@@ -10708,7 +10952,9 @@ function compileInternal(
10708
10952
  (isComponentFunction(node.declaration) || isReturnJsxFunction(node.declaration))
10709
10953
  )
10710
10954
  compNode = node.declaration;
10711
- if (compNode && compNode.id) {
10955
+ // A component's returned-JSX twin runs only for direct calls, never as a
10956
+ // component.
10957
+ if (compNode && compNode.id && compNode._octaneDirectCallTwin !== true) {
10712
10958
  ctx.componentInfo.set(compNode.id.name, {
10713
10959
  eligible: false,
10714
10960
  autoMemoSafe: false,
@@ -11779,10 +12025,12 @@ function compileServer(
11779
12025
  const errorBoundaryLowering = lowerImportedErrorBoundaries(ast);
11780
12026
  ast = errorBoundaryLowering.ast;
11781
12027
  const consumedRuntimeLocals = errorBoundaryLowering.consumed;
12028
+ ast = splitDirectlyCalledComponents(ast);
11782
12029
  // Mirror the client transform so SSR emits the same control-flow ranges
11783
12030
  // hydration expects in both development and production.
11784
12031
  ast = lowerNullishComponentExits(ast);
11785
12032
  ast = lowerJsxReturnBranchComponents(ast);
12033
+ ast = lowerDirectlyCalledTemplateFunctions(ast);
11786
12034
  // Mirror the client transform exactly. Effects are server no-ops, but
11787
12035
  // useMemo/useCallback execute during SSR and must receive the same inferred
11788
12036
  // dependency shape as hydration's client compile.
@@ -12087,6 +12335,11 @@ function compileServerComponent(node, ctx) {
12087
12335
  const beforeCss = ctx.cssInjections.length;
12088
12336
  const scoping = applyStyleScopes(node, ctx);
12089
12337
  node = scoping.node;
12338
+ // A plain return-JSX declaration can also be called directly; its returned
12339
+ // JSX then needs the same deferred value the client emits.
12340
+ if (node.body?.type === 'BlockStatement' && !node._octaneBindingView) {
12341
+ node = { ...node, _octaneDirectCallReturn: true };
12342
+ }
12090
12343
  const cssHash = scoping.cssHash;
12091
12344
  const cssEntries = [...ctx.moduleCssInjections, ...ctx.cssInjections.slice(beforeCss)].sort(
12092
12345
  (a, b) => a.order - b.order,
@@ -12120,6 +12373,7 @@ function compileServerComponent(node, ctx) {
12120
12373
  // its body (serverThemeTouchStatements) so their CSS precedes its own.
12121
12374
  scoping.runtimeApplied,
12122
12375
  );
12376
+ fn = withDirectCallGuard(fn, node, ctx);
12123
12377
  } finally {
12124
12378
  ctx.currentComponentLocals = prevLocals;
12125
12379
  ctx.knownStringLocals = prevKnownStr;
@@ -12144,8 +12398,11 @@ function compileServerComponent(node, ctx) {
12144
12398
  // hoisting instead of a TDZ `const` binding. Server and client compiles must
12145
12399
  // agree, or the same route module renders on one side and crashes on the other.
12146
12400
  // Writable declarations also keep their module binding inside their own body
12147
- // and preserve declaration-form live default exports.
12401
+ // and preserve declaration-form live default exports. A component's
12402
+ // returned-JSX twin is hoisted like the client's, because a direct call of
12403
+ // its component can run while the module evaluates.
12148
12404
  if (
12405
+ node._octaneDirectCallTwin === true ||
12149
12406
  componentReferencedAboveDeclaration(ctx, node, name) ||
12150
12407
  isReassignedComponentDeclaration(node, ctx)
12151
12408
  ) {
@@ -12211,7 +12468,9 @@ function ssrReturnedJsxNode(argument, ctx) {
12211
12468
  argument.type === 'JSXFragment' ||
12212
12469
  argument.type === 'Fragment' ||
12213
12470
  isFragmentLongForm(argument, ctx) ||
12214
- (!returnedHostRoot && requiresTemplateNormalization(argument, 'html', true, ctx))
12471
+ (returnedHostRoot
12472
+ ? isRangedReturnedHost(argument)
12473
+ : requiresTemplateNormalization(argument, 'html', true, ctx))
12215
12474
  ) {
12216
12475
  return {
12217
12476
  type: 'TSRXExpression',
@@ -12280,6 +12539,7 @@ function ssrCompileBodyWithMapTemps(
12280
12539
 
12281
12540
  let statements;
12282
12541
  let jsxNodes;
12542
+ let directCallRoot = null;
12283
12543
  if (node.body && node.body.type === 'JSXCodeBlock') {
12284
12544
  statements = returnedOutput
12285
12545
  ? (node.body.body || []).map(normalizeOwnRenderableReturns)
@@ -12293,11 +12553,18 @@ function ssrCompileBodyWithMapTemps(
12293
12553
  // itself. rewriteEarlyExits wants the array.
12294
12554
  const bodyStmts =
12295
12555
  node.body && node.body.type === 'BlockStatement' ? node.body.body || [] : node.body || [];
12296
- const bodyRewritten = rewriteEarlyExits(bodyStmts);
12556
+ // A statement array is a synthetic arm body (see splitArmExits); a function
12557
+ // body keeps its returns on the generic value ABI.
12558
+ const bodyRewritten = Array.isArray(node.body)
12559
+ ? splitArmExits(unwrapOutputCodeBlock(bodyStmts), ctx)
12560
+ : rewriteEarlyExits(unwrapOutputCodeBlock(bodyStmts));
12297
12561
  statements = [];
12298
12562
  jsxNodes = [];
12299
12563
  for (const child of bodyRewritten) {
12300
12564
  if (child.type === 'ReturnStatement' && child.argument && isJsxNode(child.argument)) {
12565
+ if (directCallRoot === null && node._octaneDirectCallReturn === true) {
12566
+ directCallRoot = child.argument;
12567
+ }
12301
12568
  // return-JSX form: the returned host element (+ its directive children) is
12302
12569
  // the render output — route it to jsxNodes so it flows through ssrEmitNode
12303
12570
  // (byte-identical SSR to the `@{}` form), not printed as `return <jsx>`.
@@ -12425,6 +12692,51 @@ function ssrCompileBodyWithMapTemps(
12425
12692
  ctx._returnedFragmentTemplate = prevReturnedFragmentTemplate;
12426
12693
  ctx._tsxValuePos = prevValuePos;
12427
12694
  ctx._valueDirectiveLowering = prevValueDirectiveLowering;
12695
+ // Mirrors compileReturnJsxFunction: a direct call defers the record that a
12696
+ // body call renders now. Its fragment sub is declared only in that branch, and
12697
+ // it is lowered after the body template so body-call output is unchanged.
12698
+ let directCall = null;
12699
+ const directCallKind = directCallRoot === null ? null : directCallReturnKind(directCallRoot, ctx);
12700
+ if (directCallKind === 'value') {
12701
+ // Directives in the returned value fold into helpers owned by the direct
12702
+ // call, so a body call never declares them.
12703
+ const subs = [];
12704
+ const previousLowering = ctx._valueDirectiveLowering;
12705
+ ctx._valueDirectiveLowering = serverValueDirectiveFold(ctx, name, subs, cssHash);
12706
+ try {
12707
+ const returned = rewriteHookCalls(b.return(directCallRoot), ctx, name, localSetupSlots);
12708
+ // Like the client's deferred record, the value reads bindings when it renders.
12709
+ directCall = { subs, value: markDeferredLive(rewriteJsxValues(returned.argument, ctx)) };
12710
+ } finally {
12711
+ ctx._valueDirectiveLowering = previousLowering;
12712
+ }
12713
+ } else if (directCallKind === 'fragment') {
12714
+ const subs = [];
12715
+ const record = serverValueDirectiveFold(
12716
+ ctx,
12717
+ name,
12718
+ subs,
12719
+ cssHash,
12720
+ false,
12721
+ 'html',
12722
+ )(directCallRoot);
12723
+ ctx.runtimeNeeded.add('createScopedValue');
12724
+ directCall = {
12725
+ subs,
12726
+ value: inheritOriginLoc(
12727
+ b.call(rtAlias('createScopedValue'), b.arrow([], record)),
12728
+ directCallRoot,
12729
+ ),
12730
+ };
12731
+ }
12732
+ const renderCallTest =
12733
+ directCall === null
12734
+ ? null
12735
+ : () =>
12736
+ inheritOriginLoc(
12737
+ b.call(requireRuntimeForContext(ctx, 'isRenderCall'), b.id('__s')),
12738
+ directCallRoot,
12739
+ );
12428
12740
 
12429
12741
  const body = [];
12430
12742
  body.push(...serverThemeTouchStatements(themeTouches ?? [], ctx, node));
@@ -12488,14 +12800,55 @@ function ssrCompileBodyWithMapTemps(
12488
12800
  }
12489
12801
  }
12490
12802
  }
12491
- body.push(...frontHead, ...rewritten, ...inlinedSubs, ...deferredHead);
12492
- // PROPS-FIRST ABI (matches the client): `(…userParams, __s, __extra)`. A leading
12803
+ // A direct call's deferred fragment registers its own head elements when it
12804
+ // renders, so the body-call registrations run only for a body call.
12805
+ const guardHead = (statements) =>
12806
+ renderCallTest === null || statements.length === 0
12807
+ ? statements
12808
+ : [inheritOriginLoc(b.if(renderCallTest(), b.block(statements), null), statements[0])];
12809
+ body.push(...guardHead(frontHead), ...rewritten, ...inlinedSubs, ...guardHead(deferredHead));
12810
+ if (directCall !== null) {
12811
+ const scopeNames = collectFunctionScopeBindings(
12812
+ node.params.length > 0 ? node.params : [b.id('__props')],
12813
+ [...rewritten, ...inlinedSubs],
12814
+ new Set([...mapTemps, '__s', '__extra']),
12815
+ );
12816
+ const captures = directCallCaptures(node, scopeNames, directCall.subs, directCall.value);
12817
+ const branch =
12818
+ captures === null
12819
+ ? [...directCall.subs, b.return(directCall.value)]
12820
+ : [
12821
+ b.return(
12822
+ hoistCapturingHelper(
12823
+ ctx,
12824
+ `${name}$direct`,
12825
+ captures,
12826
+ directCall.subs,
12827
+ directCall.value,
12828
+ directCallRoot,
12829
+ ),
12830
+ ),
12831
+ ];
12832
+ body.push(
12833
+ inheritOriginLoc(b.if(b.unary('!', renderCallTest()), b.block(branch), null), directCallRoot),
12834
+ );
12835
+ }
12836
+ // PROPS-FIRST ABI (matches the client): `(props, __s, __extra)`. A leading
12493
12837
  // `__props` placeholder stands in when there are no user params, so a verbatim
12494
12838
  // `function Foo(props)` and a compiled component both bind props from arg 0.
12495
- const params =
12496
- node.params.length > 0
12497
- ? [...node.params, b.id('__s'), b.id('__extra')]
12498
- : [b.id('__props'), b.id('__s'), b.id('__extra')];
12839
+ // A synthetic sub-body (a statement array) is called by its construct instead,
12840
+ // with its own leading arguments: `(item, index, __s, __extra)` for a @for
12841
+ // item, `(error, reset, __s, __extra)` for a @catch arm.
12842
+ const { params, bindings: paramBindings } = Array.isArray(node.body)
12843
+ ? {
12844
+ params: [
12845
+ ...(node.params.length > 0 ? node.params : [b.id('__props')]),
12846
+ b.id('__s'),
12847
+ b.id('__extra'),
12848
+ ],
12849
+ bindings: [],
12850
+ }
12851
+ : compiledBodyParams(node.params);
12499
12852
  // Private loop items feed only HTML concatenation, so their serialized tail
12500
12853
  // needs no carrier. Other bodies can cross a component/value boundary, where
12501
12854
  // the carrier distinguishes compiled HTML from authored text returns. This
@@ -12519,14 +12872,11 @@ function ssrCompileBodyWithMapTemps(
12519
12872
  : ([...(node.params || []), ...(Array.isArray(node.body) ? node.body : [])].find(
12520
12873
  (part) => part?.loc != null,
12521
12874
  ) ?? ctx._moduleOrigin);
12875
+ const scopedBody = ctx.nativeReads
12876
+ ? wrapNativeReadScope(body, b.id('__s'), nativeReadNames(ctx))
12877
+ : body;
12522
12878
  return inheritOriginLoc(
12523
- b.function_declaration(
12524
- b.id(name),
12525
- params,
12526
- b.block(
12527
- ctx.nativeReads ? wrapNativeReadScope(body, b.id('__s'), nativeReadNames(ctx)) : body,
12528
- ),
12529
- ),
12879
+ b.function_declaration(b.id(name), params, b.block([...paramBindings, ...scopedBody])),
12530
12880
  origin,
12531
12881
  );
12532
12882
  }
@@ -12932,6 +13282,14 @@ function ssrEmitNode(
12932
13282
  case 'Element':
12933
13283
  if (isComponentTag(node))
12934
13284
  return ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
13285
+ if (node.parserRepairRange === true) {
13286
+ ctx.runtimeNeeded.add('ssrBlock');
13287
+ return ssrCall(
13288
+ 'ssrBlock',
13289
+ [ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs)],
13290
+ node,
13291
+ );
13292
+ }
12935
13293
  return ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
12936
13294
  case 'TSRXExpression':
12937
13295
  return ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs);
@@ -13926,15 +14284,37 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
13926
14284
  // still gets a `<!--[-->…<!--]-->` block). Must match the client's only-child
13927
14285
  // markerless condition exactly so both sides agree for hydration: a single `Text`
13928
14286
  // child that is neither a static literal (baked into HTML) nor proven text
13929
- // (emitted via `ssrText`).
14287
+ // (emitted via `ssrText`). A spread or raw-HTML writer does not change the
14288
+ // client's shape, so it must not change this one: inside RCDATA (<textarea>)
14289
+ // a frame around a primitive would parse as literal text.
13930
14290
  const onlyChild0 =
13931
14291
  normChildren.length === 1 && normChildren[0].type === 'Text' ? normChildren[0] : null;
14292
+ const textareaParts = textareaTextParts(tag, selfNs, normChildren, ctx);
13932
14293
  let childrenExpr;
13933
14294
  if (authoredStaticScriptContent !== undefined) {
13934
14295
  const content = escapeInlineScriptContent(authoredStaticScriptContent);
13935
14296
  childrenExpr = ssrHtmlTemplate(content === '' ? [] : [content], node, ctx);
14297
+ } else if (textareaParts !== null) {
14298
+ // The client binds these as one Text node (see textareaTextParts).
14299
+ ctx.runtimeNeeded.add('ssrTextareaText');
14300
+ childrenExpr = ssrCall(
14301
+ 'ssrTextareaText',
14302
+ textareaTextArgs(textareaParts, (expression, text, part) =>
14303
+ ssrSignalValue(
14304
+ resolveStyleExpr(
14305
+ text
14306
+ ? rewriteHookCalls(expression, ctx, name)
14307
+ : rewriteJsxValues(rewriteHookCalls(expression, ctx, name), ctx),
14308
+ cssHash,
14309
+ ),
14310
+ ctx,
14311
+ part,
14312
+ true,
14313
+ ),
14314
+ ),
14315
+ node,
14316
+ );
13936
14317
  } else if (
13937
- htmlSources.length === 0 &&
13938
14318
  onlyChild0 !== null &&
13939
14319
  !onlyChild0._octaneBindingOpaque &&
13940
14320
  !onlyChild0._octaneBindingValue &&
@@ -14008,7 +14388,15 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
14008
14388
  ctx.runtimeNeeded.add('ssrChildrenSources');
14009
14389
  childrenExpr = ssrCall(
14010
14390
  'ssrChildrenSources',
14011
- [b.array(effectiveChildrenPropSources), ssrThunk(childrenExpr, node), b.id('__s')],
14391
+ [
14392
+ b.array(effectiveChildrenPropSources),
14393
+ ssrThunk(childrenExpr, node),
14394
+ b.id('__s'),
14395
+ // A textarea's children prop is text too (see textareaTextParts).
14396
+ ...(tag === 'textarea' && (selfNs === 'html' || selfNs === 'opaque')
14397
+ ? [b.literal(true, 'true')]
14398
+ : []),
14399
+ ],
14012
14400
  node,
14013
14401
  );
14014
14402
  }
@@ -14722,21 +15110,8 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
14722
15110
  ),
14723
15111
  node,
14724
15112
  );
14725
- let explicitKey = null;
14726
15113
  let keyDeclaration = null;
14727
- const firstEl = (node.body.body || []).find(
14728
- (child) => child.type === 'Element' || child.type === 'JSXElement',
14729
- );
14730
- if (firstEl) {
14731
- const keyAttr = (firstEl.attributes || firstEl.openingElement?.attributes || []).find(
14732
- (attr) => (attr.name?.name || attr.name) === 'key',
14733
- );
14734
- if (keyAttr?.value != null) {
14735
- explicitKey =
14736
- keyAttr.value.type === 'JSXExpressionContainer' ? keyAttr.value.expression : keyAttr.value;
14737
- }
14738
- }
14739
- if (explicitKey === null) explicitKey = node.key || null;
15114
+ const explicitKey = forRowKeyAttribute(node, ctx) ?? node.key ?? null;
14740
15115
  if (explicitKey !== null) {
14741
15116
  const keyParams = [itemId];
14742
15117
  if (node.index) keyParams.push(node.index);
@@ -14751,7 +15126,7 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
14751
15126
  } else if (node.index) {
14752
15127
  itemKey = inheritOriginLoc(b.id('__i'), node.index);
14753
15128
  }
14754
- const markerlessItem = isSsrMarkerlessForItem(node);
15129
+ const markerlessItem = isSsrMarkerlessForItem(node, itemBody);
14755
15130
  // ssrArm exists solely to make use()/component identity distinct per item.
14756
15131
  // Skip it when the item is compiler-proven synchronous and transparent:
14757
15132
  // no render-time calls, nested components/control flow, or renderable child
@@ -14772,7 +15147,7 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
14772
15147
  node,
14773
15148
  );
14774
15149
  const sharedItemRange =
14775
- !bindingSite && (markerlessItem || canShareSsrComponentItemRange(node, ctx));
15150
+ !bindingSite && (markerlessItem || canShareSsrComponentItemRange(itemBody, ctx));
14776
15151
  const itemHtml =
14777
15152
  sharedItemRange || bindingSite ? itemCall : ssrCall('ssrBlock', [itemCall], node);
14778
15153
  let renderItem = itemNeedsIdentity
@@ -15103,12 +15478,15 @@ function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash,
15103
15478
  node,
15104
15479
  );
15105
15480
  }
15106
- if (node.returnedJsxValue === true && requiresTemplateNormalization(expr, 'html', true, ctx)) {
15481
+ if (
15482
+ node.returnedJsxValue === true &&
15483
+ (isRangedReturnedHost(expr) || requiresTemplateNormalization(expr, 'html', true, ctx))
15484
+ ) {
15107
15485
  // A returned JSX value that contains template-only syntax is one compiled
15108
15486
  // renderer on the client (rather than a descriptor array whose value
15109
15487
  // lowering cannot represent directives, sentinels, head hoists, or child
15110
- // code blocks). Mirror that component boundary on the server: the local
15111
- // sub-function keeps access to the
15488
+ // code blocks), as is an imperatively built host root. Mirror that component
15489
+ // boundary on the server: the local sub-function keeps access to the
15112
15490
  // enclosing return component's props/locals, while ssrComponent emits the
15113
15491
  // range the client descriptor adopts during hydration.
15114
15492
  // The final flag mirrors extractFragment's component-child decision inside
@@ -15136,7 +15514,10 @@ function ssrEmitTsrxExpression(node, ctx, name, inlinedSubs, parentNs, cssHash,
15136
15514
  b.literal(false),
15137
15515
  undefinedNode(),
15138
15516
  undefinedNode(),
15139
- b.literal(componentInvocationSite(ctx, node)),
15517
+ // Hash the authored JSX, as the client's lowerHostFragment does, not
15518
+ // the synthetic wrapper: every signal instance below this renderer
15519
+ // chains its key from this site.
15520
+ b.literal(componentInvocationSite(ctx, expr)),
15140
15521
  ],
15141
15522
  node,
15142
15523
  );
@@ -15328,13 +15709,25 @@ function singleRootInitializer(ctx, component) {
15328
15709
  return markPure(b.call('_$__s', component));
15329
15710
  }
15330
15711
 
15712
+ function isOctaneMemoCallee(callee, ctx, memoImportNames) {
15713
+ if (callee?.type === 'Identifier') return memoImportNames.has(callee.name);
15714
+ return (
15715
+ callee?.type === 'MemberExpression' &&
15716
+ !callee.computed &&
15717
+ callee.object?.type === 'Identifier' &&
15718
+ callee.property?.type === 'Identifier' &&
15719
+ callee.property.name === 'memo' &&
15720
+ ctx.octaneImportNamespaces?.has(callee.object.name) === true
15721
+ );
15722
+ }
15723
+
15331
15724
  // An exact public memo wrapper preserves the already-proven host output of its
15332
15725
  // immutable local component. Stamp only the fresh compiler-owned wrapper:
15333
15726
  // probing arbitrary component metadata would invoke observable getters, and
15334
15727
  // dev/HMR, custom comparators, imported components, and renderer units remain
15335
15728
  // deliberately opaque.
15336
15729
  function markSingleRootMemoInitializers(node, ctx, memoImportNames) {
15337
- if (ctx.hmr || ctx.dev || ctx.profile || memoImportNames.size === 0) return node;
15730
+ if (ctx.hmr || ctx.dev || ctx.profile || ctx.defaultMemoBindings.size === 0) return node;
15338
15731
  const exported = node.type === 'ExportNamedDeclaration';
15339
15732
  const declaration = exported ? node.declaration : node;
15340
15733
  if (declaration?.type !== 'VariableDeclaration' || declaration.kind !== 'const') return node;
@@ -15346,8 +15739,7 @@ function markSingleRootMemoInitializers(node, ctx, memoImportNames) {
15346
15739
  item.id?.type !== 'Identifier' ||
15347
15740
  !ctx.defaultMemoBindings.has(item.id.name) ||
15348
15741
  init?.type !== 'CallExpression' ||
15349
- init.callee?.type !== 'Identifier' ||
15350
- !memoImportNames.has(init.callee.name) ||
15742
+ !isOctaneMemoCallee(init.callee, ctx, memoImportNames) ||
15351
15743
  init.arguments.length !== 1 ||
15352
15744
  wrapped?.type !== 'Identifier' ||
15353
15745
  !ctx.moduleFunctionDeclarations.has(wrapped.name) ||
@@ -15533,11 +15925,15 @@ function compileComponent(node, ctx, options) {
15533
15925
  // other inner compileFunctionBody calls leave their arrows untouched
15534
15926
  // (they rarely declare arrow consts; if they do, the stability oracle
15535
15927
  // would need to be redefined relative to the inner scope).
15536
- fnNode = compileFunctionBody(node, ctx, name, 'opaque', cssHash, {
15537
- autoCallback: true,
15538
- localHookSlots: true,
15539
- returnedOutput,
15540
- });
15928
+ fnNode = withDirectCallGuard(
15929
+ compileFunctionBody(node, ctx, name, 'opaque', cssHash, {
15930
+ autoCallback: true,
15931
+ localHookSlots: true,
15932
+ returnedOutput,
15933
+ }),
15934
+ node,
15935
+ ctx,
15936
+ );
15541
15937
  } finally {
15542
15938
  ctx.currentComponentLocals = prevLocals;
15543
15939
  ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
@@ -15990,11 +16386,21 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
15990
16386
  // Keep the old split + rewriteEarlyExits path for these.
15991
16387
  let statements;
15992
16388
  let jsxNodes;
16389
+ const previousComponentLocals = ctx.currentComponentLocals;
15993
16390
  if (node.body && node.body.type === 'JSXCodeBlock') {
15994
16391
  statements = node.body.body || [];
15995
16392
  jsxNodes = node.body.render ? [node.body.render] : [];
15996
16393
  } else {
15997
- const bodyRewritten = rewriteEarlyExits(node.body);
16394
+ const bodyRewritten = splitArmExits(unwrapOutputCodeBlock(node.body), ctx);
16395
+ // The split can declare a name of its own (a hooked tail's handoff), which
16396
+ // the arms nested in this body capture like any other local of it.
16397
+ if (previousComponentLocals) {
16398
+ const declared = new Set();
16399
+ for (const statement of bodyRewritten) collectStatementBindings(statement, declared);
16400
+ if ([...declared].some((name) => !previousComponentLocals.has(name))) {
16401
+ ctx.currentComponentLocals = new Set([...previousComponentLocals, ...declared]);
16402
+ }
16403
+ }
15998
16404
  statements = [];
15999
16405
  jsxNodes = [];
16000
16406
  for (const child of bodyRewritten) {
@@ -16196,11 +16602,11 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
16196
16602
  // Rewrite hook calls and `<tsrx>` blocks in statements before printing them.
16197
16603
  // A `<tsrx>` block at expression position (e.g. `const f = <tsrx>...</tsrx>`)
16198
16604
  // is hoisted as a render function in inlinedSubs and replaced with an
16199
- // identifier reference. Suitable for top-level render-prop patterns where
16200
- // the block doesn't capture local arrow params.
16605
+ // identifier reference. One that reads a setup callback's names stays for
16606
+ // rewriteJsxValues, which compiles it in place inside that callback.
16201
16607
  const rewrittenStatements = preparedStatements
16202
16608
  .map((s) => rewriteHookCalls(s, ctx, name, options?.localHookSlots === true))
16203
- .map((s) => rewriteTsrxBlocks(s, ctx, name, inlinedSubs))
16609
+ .map((s) => rewriteTsrxBlocks(s, ctx, name, inlinedSubs, 'html', null, true))
16204
16610
  // JSX component element at VALUE position in setup (e.g. `const el = <App/>`)
16205
16611
  // → createElement(App, props). Output JSX (jsxNodes) was already split off.
16206
16612
  .map((s) => rewriteJsxValues(s, ctx));
@@ -16535,11 +16941,10 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
16535
16941
  );
16536
16942
  presentationHostEnd++;
16537
16943
  }
16538
- // PROPS-FIRST convention: `(…userProps, __s, __extra)`. The scope is the 2nd arg
16944
+ // PROPS-FIRST convention: `(props, __s, __extra)`. The scope is the 2nd arg
16539
16945
  // (a placeholder leads when there are no user params), so a plain function
16540
16946
  // `App(props)` binds `props`, while compiled bodies still read `__s` by name.
16541
- const userParams = node.params && node.params.length > 0 ? node.params : [b.id('__props')];
16542
- const fnParams = [...userParams, b.id('__s'), b.id('__extra')];
16947
+ const { params: fnParams, bindings: paramBindings } = compiledBodyParams(node.params ?? []);
16543
16948
  ctx.currentAutoMemoOffset = prevAutoMemoOffset;
16544
16949
  ctx.currentAutoMemoCacheName = prevAutoMemoCacheName;
16545
16950
  ctx.currentAutoMemoCommittedName = prevAutoMemoCommittedName;
@@ -16548,6 +16953,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
16548
16953
  ctx.currentHookMemoNames = prevHookMemoNames;
16549
16954
  ctx.currentHookMemoOwnerSafe = prevHookMemoOwnerSafe;
16550
16955
  ctx.currentMapTemps = prevMapTemps;
16956
+ ctx.currentComponentLocals = previousComponentLocals;
16551
16957
  // ONE FunctionDeclaration node — the caller prints it (once, with the full
16552
16958
  // esrap map for top-level components) or embeds it in an enclosing body.
16553
16959
  const presentationBody = preparePresentationHydration(
@@ -16558,14 +16964,14 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
16558
16964
  );
16559
16965
  ctx.scalarBindingClaims = previousScalarBindingClaims;
16560
16966
  ctx.presentationHydration = previousPresentationHydration;
16967
+ const scopedBody =
16968
+ ctx.nativeReads && !isStaticHostComponent(node)
16969
+ ? wrapNativeReadScope(presentationBody, b.id('__s'), nativeReadNames(ctx))
16970
+ : presentationBody;
16561
16971
  const emittedFunction = b.function_declaration(
16562
16972
  b.id(name, node.id ?? node),
16563
16973
  fnParams,
16564
- b.block(
16565
- ctx.nativeReads && !isStaticHostComponent(node)
16566
- ? wrapNativeReadScope(presentationBody, b.id('__s'), nativeReadNames(ctx))
16567
- : presentationBody,
16568
- ),
16974
+ b.block([...paramBindings, ...scopedBody]),
16569
16975
  );
16570
16976
  return inheritOriginLoc(
16571
16977
  hookMemoOpaqueOwner
@@ -19990,6 +20396,9 @@ function compileReturnJsxFunction(node, ctx, options) {
19990
20396
  // their closure over setup locals/props — and only their values + the control
19991
20397
  // expression are threaded into the renderer as `props.hN` holes.
19992
20398
  const compInlinedSubs = [];
20399
+ // Set once a returned record needs a separate direct-call value.
20400
+ let renderCall = null;
20401
+ const returnedRecords = [];
19993
20402
  // Fold a directive found at value position into a hoisted renderer owned by THIS
19994
20403
  // body — the same fold the `@{}` body and the server emitter build.
19995
20404
  const lowerBodyValueDirective =
@@ -20088,17 +20497,20 @@ function compileReturnJsxFunction(node, ctx, options) {
20088
20497
  // represented render scope, not captured into fragment props while
20089
20498
  // the factory runs. Reuse value lowering in both emitters so its
20090
20499
  // public type/props and deferred children also stay inspectable.
20500
+ // Like other returned records, it reads variables when it renders.
20091
20501
  if (ctx.nativeReads) {
20092
20502
  return {
20093
20503
  ...h,
20094
- argument: nativeReturnedJsxValue(
20095
- h.argument,
20096
- ctx,
20097
- name,
20098
- compInlinedSubs,
20099
- cssHash,
20100
- cssEntries,
20101
- scoping.runtimeApplied,
20504
+ argument: markDeferredLive(
20505
+ nativeReturnedJsxValue(
20506
+ h.argument,
20507
+ ctx,
20508
+ name,
20509
+ compInlinedSubs,
20510
+ cssHash,
20511
+ cssEntries,
20512
+ scoping.runtimeApplied,
20513
+ ),
20102
20514
  ),
20103
20515
  };
20104
20516
  }
@@ -20121,7 +20533,33 @@ function compileReturnJsxFunction(node, ctx, options) {
20121
20533
  };
20122
20534
  }
20123
20535
  }
20124
- return { ...h, argument: lowerReturnJsx(h.argument, ctx, compInlinedSubs, cssHash) };
20536
+ const record = lowerReturnJsx(h.argument, ctx, compInlinedSubs, cssHash);
20537
+ if (node._octaneBindingView || !directCallNeedsClientBranch(h.argument, node, ctx)) {
20538
+ return { ...h, argument: record };
20539
+ }
20540
+ // A component's returned-JSX twin runs only for direct calls, so it
20541
+ // needs no Scope parameter and returns only the deferred value.
20542
+ renderCall ??= node._octaneDirectCallTwin
20543
+ ? { params: node.params, scope: null }
20544
+ : renderCallScopeParam(node, ctx);
20545
+ // Built once the function's top-level bindings are all known.
20546
+ const placeholder = inheritOriginLoc(b.id('__octaneReturnedRecord'), h.argument);
20547
+ returnedRecords.push({
20548
+ placeholder,
20549
+ record,
20550
+ origin: h.argument,
20551
+ test:
20552
+ renderCall.scope === null
20553
+ ? null
20554
+ : inheritOriginLoc(
20555
+ b.call(
20556
+ requireRuntimeForContext(ctx, 'isRenderCall'),
20557
+ cloneAstNode(renderCall.scope),
20558
+ ),
20559
+ h.argument,
20560
+ ),
20561
+ });
20562
+ return { ...h, argument: placeholder };
20125
20563
  }
20126
20564
  return rewriteJsxValues(h, ctx);
20127
20565
  });
@@ -20133,6 +20571,20 @@ function compileReturnJsxFunction(node, ctx, options) {
20133
20571
  ctx.currentAutoCalculatedRenderableRefs = prevAutoCalculatedRenderableRefs;
20134
20572
  ctx.currentMapTemps = prevMapTemps;
20135
20573
  }
20574
+ if (returnedRecords.length > 0) {
20575
+ const scopeNames = collectFunctionScopeBindings(
20576
+ renderCall.params,
20577
+ [...compInlinedSubs, ...newStatements],
20578
+ new Set(mapTemps),
20579
+ );
20580
+ const replacements = new Map(
20581
+ returnedRecords.map((entry) => [
20582
+ entry.placeholder,
20583
+ splitReturnedRecord(ctx, node, name, scopeNames, entry),
20584
+ ]),
20585
+ );
20586
+ newStatements = mapAst(newStatements, (candidate) => replacements.get(candidate) ?? null);
20587
+ }
20136
20588
  if (
20137
20589
  ctx.autoMemo &&
20138
20590
  ctx.privateUnescapedComponents.has(name) &&
@@ -20184,7 +20636,7 @@ function compileReturnJsxFunction(node, ctx, options) {
20184
20636
  }
20185
20637
  const emittedFunction = b.function_declaration(
20186
20638
  node.id,
20187
- node.params,
20639
+ renderCall?.params ?? node.params,
20188
20640
  b.block(
20189
20641
  ctx.nativeReads ? wrapNativeReadScope(returnBody, b.void0, nativeReadNames(ctx)) : returnBody,
20190
20642
  ),
@@ -20532,6 +20984,321 @@ function requiresTemplateNormalization(
20532
20984
  );
20533
20985
  }
20534
20986
 
20987
+ // A named return-JSX declaration is also a component. Its body call evaluates
20988
+ // the returned record in the scope that renders it. A direct call returns a JSX
20989
+ // value instead, which must resolve where it renders, like the value an arrow
20990
+ // function returns. Both emitters classify the same authored root: 'fragment'
20991
+ // defers the compiled fragment record, 'value' uses ordinary value lowering, and
20992
+ // null marks static host markup that renders the same anywhere. The server's
20993
+ // body call renders the whole tree to HTML, so every non-literal hole and every
20994
+ // component below the root needs deferral there.
20995
+ function directCallReturnKind(node, ctx, stableNames = null) {
20996
+ if (VALUE_DIRECTIVE_ARM_TYPES.has(node.type)) return 'fragment';
20997
+ if (requiresTemplateNormalization(node, 'html', true, ctx)) return 'fragment';
20998
+ if (node.type !== 'Element' && node.type !== 'JSXElement') {
20999
+ return jsxValueChildrenNeedRenderScope(node, false, stableNames) ? 'value' : null;
21000
+ }
21001
+ if (isComponentTag(node)) return 'value';
21002
+ return jsxValueRootNeedsRenderScope(node, stableNames) ||
21003
+ jsxValueChildrenNeedRenderScope(node, false, stableNames)
21004
+ ? 'fragment'
21005
+ : null;
21006
+ }
21007
+
21008
+ // Whether the client's body-call record evaluates anything a direct call must
21009
+ // defer. The client record only stores hole values and interprets them where
21010
+ // it mounts, so reading an unreassigned local is the same during the call and
21011
+ // at render. Value-lowered roots already defer, except a component's own props.
21012
+ function directCallNeedsClientBranch(node, fn, ctx) {
21013
+ const stable = directCallStableNames(fn);
21014
+ const kind = directCallReturnKind(node, ctx, stable);
21015
+ if (kind === 'fragment') return true;
21016
+ return (
21017
+ kind === 'value' &&
21018
+ (node.type === 'Element' || node.type === 'JSXElement') &&
21019
+ jsxValueRootNeedsRenderScope(node, stable)
21020
+ );
21021
+ }
21022
+
21023
+ // Names that code anywhere in the function assigns after declaring them.
21024
+ const DIRECT_CALL_ASSIGNED_NAMES = new WeakMap();
21025
+ function directCallAssignedNames(fn) {
21026
+ const cached = DIRECT_CALL_ASSIGNED_NAMES.get(fn);
21027
+ if (cached !== undefined) return cached;
21028
+ const assigned = new Set();
21029
+ const target = (node) => {
21030
+ if (node?.type === 'Identifier') assigned.add(node.name);
21031
+ else if (node != null && typeof node === 'object') collectBindings(node, assigned);
21032
+ };
21033
+ mapAst(fn.body, (node) => {
21034
+ if (node.type === 'AssignmentExpression') target(node.left);
21035
+ else if (node.type === 'UpdateExpression') target(node.argument);
21036
+ else if (
21037
+ (node.type === 'ForInStatement' || node.type === 'ForOfStatement') &&
21038
+ node.left?.type !== 'VariableDeclaration'
21039
+ ) {
21040
+ target(node.left);
21041
+ }
21042
+ return null;
21043
+ });
21044
+ DIRECT_CALL_ASSIGNED_NAMES.set(fn, assigned);
21045
+ return assigned;
21046
+ }
21047
+
21048
+ // Parameters and top-level declarations that nothing in the function assigns.
21049
+ // Reading one runs no user code and yields the same value during the call and
21050
+ // at render, so the client record needs no deferral for it.
21051
+ function directCallStableNames(fn) {
21052
+ const names = collectComponentLocals(fn);
21053
+ for (const name of directCallAssignedNames(fn)) names.delete(name);
21054
+ return names;
21055
+ }
21056
+
21057
+ // A direct call's deferred value reads the function's locals after the call has
21058
+ // returned. Declaring its closure inside the function would make those locals
21059
+ // context slots, allocating a context on every call, body calls included, so
21060
+ // the value is built by a module function that receives them as arguments.
21061
+ // Returns those arguments, or null when a copy could differ from the live
21062
+ // binding: a captured authored local that the function reassigns, or a read of
21063
+ // `this` or `arguments`.
21064
+ function directCallCaptures(fn, scopeNames, statements, value) {
21065
+ const body = b.block([...statements, b.return(value)]);
21066
+ let contextual = false;
21067
+ mapAst(body, (node) => {
21068
+ if (
21069
+ node.type === 'ThisExpression' ||
21070
+ node.type === 'Super' ||
21071
+ node.type === 'MetaProperty' ||
21072
+ (node.type === 'Identifier' && node.name === 'arguments')
21073
+ ) {
21074
+ contextual = true;
21075
+ }
21076
+ return contextual ? node : null;
21077
+ });
21078
+ if (contextual) return null;
21079
+ const captures = [...collectFreeIdentifiers(body, new Set())]
21080
+ .filter((identifier) => scopeNames.has(identifier))
21081
+ .sort();
21082
+ const assigned = directCallAssignedNames(fn);
21083
+ return captures.some((identifier) => assigned.has(identifier)) ? null : captures;
21084
+ }
21085
+
21086
+ // Declare `function helper(captures) { ...statements; return value; }` among the
21087
+ // module's hoisted helpers and return a call to it.
21088
+ function hoistCapturingHelper(ctx, preferred, captures, statements, value, origin) {
21089
+ const helper = allocCompilerName(ctx, preferred);
21090
+ const ids = () => captures.map((identifier) => inheritOriginLoc(b.id(identifier), origin));
21091
+ ctx.hoistedHelpers.push(
21092
+ inheritOriginLoc(
21093
+ b.function_declaration(b.id(helper), ids(), b.block([...statements, b.return(value)])),
21094
+ origin,
21095
+ ),
21096
+ );
21097
+ return inheritOriginLoc(b.call(helper, ...ids()), origin);
21098
+ }
21099
+
21100
+ // The client's returned record, split between a body call and a direct call.
21101
+ // The record is printed once, in a module function the body call invokes; a
21102
+ // direct call defers that same function until the value renders. Without a
21103
+ // body-call `test`, the function only ever returns the direct call's value.
21104
+ function splitReturnedRecord(ctx, fn, name, scopeNames, { record, test, origin }) {
21105
+ const captures = directCallCaptures(fn, scopeNames, [], record);
21106
+ if (captures === null) {
21107
+ ctx.runtimeNeeded.add('createScopedValue');
21108
+ // A direct call's record reads bindings when it renders, as the server's does.
21109
+ const direct = inheritOriginLoc(
21110
+ b.call(
21111
+ rtAlias('createScopedValue'),
21112
+ b.arrow([], markDeferredLive(test === null ? record : cloneAstNode(record))),
21113
+ ),
21114
+ origin,
21115
+ );
21116
+ return test === null ? direct : inheritOriginLoc(b.conditional(test, record, direct), origin);
21117
+ }
21118
+ const read = hoistCapturingHelper(ctx, `${name}$record`, captures, [], record, origin);
21119
+ const direct = inheritOriginLoc(
21120
+ b.call(
21121
+ requireRuntimeForContext(ctx, 'deferRecord'),
21122
+ b.id(read.callee.name),
21123
+ ...read.arguments.map((argument) => cloneAstNode(argument)),
21124
+ ),
21125
+ origin,
21126
+ );
21127
+ return test === null ? direct : inheritOriginLoc(b.conditional(test, read, direct), origin);
21128
+ }
21129
+
21130
+ // Names an emitted function body binds in its own scope: parameters, top-level
21131
+ // declarations, and `var` declarations in nested blocks.
21132
+ function collectFunctionScopeBindings(params, statements, into) {
21133
+ for (const param of params) collectBindings(param, into);
21134
+ const visit = (statement, topLevel) => {
21135
+ if (statement == null || typeof statement !== 'object') return;
21136
+ switch (statement.type) {
21137
+ case 'VariableDeclaration':
21138
+ if (topLevel || statement.kind === 'var') {
21139
+ for (const declaration of statement.declarations || []) {
21140
+ collectBindings(declaration.id, into);
21141
+ }
21142
+ }
21143
+ return;
21144
+ case 'FunctionDeclaration':
21145
+ case 'ClassDeclaration':
21146
+ if (topLevel && statement.id) into.add(statement.id.name);
21147
+ return;
21148
+ case 'BlockStatement':
21149
+ for (const child of statement.body || []) visit(child, false);
21150
+ return;
21151
+ case 'IfStatement':
21152
+ visit(statement.consequent, false);
21153
+ visit(statement.alternate, false);
21154
+ return;
21155
+ case 'ForStatement':
21156
+ visit(statement.init, false);
21157
+ visit(statement.body, false);
21158
+ return;
21159
+ case 'ForInStatement':
21160
+ case 'ForOfStatement':
21161
+ visit(statement.left, false);
21162
+ visit(statement.body, false);
21163
+ return;
21164
+ case 'WhileStatement':
21165
+ case 'DoWhileStatement':
21166
+ case 'LabeledStatement':
21167
+ visit(statement.body, false);
21168
+ return;
21169
+ case 'TryStatement':
21170
+ visit(statement.block, false);
21171
+ visit(statement.handler?.body, false);
21172
+ visit(statement.finalizer, false);
21173
+ return;
21174
+ case 'SwitchStatement':
21175
+ for (const clause of statement.cases || []) {
21176
+ for (const child of clause.consequent || []) visit(child, false);
21177
+ }
21178
+ return;
21179
+ default:
21180
+ return;
21181
+ }
21182
+ };
21183
+ for (const statement of statements) visit(statement, true);
21184
+ return into;
21185
+ }
21186
+
21187
+ // A compiled body's parameters. The runtime calls every body as
21188
+ // `body(props, scope, extra)`, so the body takes `(props, __s, __extra)`: its
21189
+ // first authored parameter, or a `__props` placeholder, then the scope and the
21190
+ // extra. Each later authored parameter is a leading `var` bound to the argument
21191
+ // at its authored position, so it holds what the returned-JSX form's parameter
21192
+ // holds: a direct call's own argument, and for a body call the scope, the extra,
21193
+ // and then nothing. A rest parameter binds every argument from its position
21194
+ // on. The slice names no global, which a module may shadow
21195
+ // (`import { Array } from 'effect'`). A TypeScript `this` parameter types the
21196
+ // receiver and takes no argument.
21197
+ function compiledBodyParams(authored) {
21198
+ const receiver = authored[0]?.type === 'Identifier' && authored[0].name === 'this' ? 1 : 0;
21199
+ const own = authored.slice(receiver);
21200
+ const first = own.length > 0 && own[0].type !== 'RestElement' ? own[0] : null;
21201
+ const params = [
21202
+ ...authored.slice(0, receiver),
21203
+ first ?? b.id('__props'),
21204
+ b.id('__s'),
21205
+ b.id('__extra'),
21206
+ ];
21207
+ const bindings = [];
21208
+ for (let i = first === null ? 0 : 1; i < own.length; i++) {
21209
+ const param = own[i];
21210
+ const argument = () =>
21211
+ i === 1
21212
+ ? b.id('__s')
21213
+ : i === 2
21214
+ ? b.id('__extra')
21215
+ : b.member(b.id('arguments'), b.literal(i), true);
21216
+ let binding;
21217
+ if (param.type === 'RestElement') {
21218
+ const slice = b.member(b.member(b.array([]), 'slice'), 'call');
21219
+ binding = b.declarator(
21220
+ param.argument,
21221
+ b.call(slice, b.id('arguments'), ...(i > 0 ? [b.literal(i)] : [])),
21222
+ );
21223
+ } else if (param.type === 'AssignmentPattern') {
21224
+ binding = b.declarator(
21225
+ param.left,
21226
+ b.conditional(b.binary('===', argument(), b.void0), param.right, argument()),
21227
+ );
21228
+ } else {
21229
+ binding = b.declarator(param, argument());
21230
+ }
21231
+ bindings.push({
21232
+ ...inheritOriginLoc(b.declaration('var', [binding]), param),
21233
+ _octaneParamBinding: true,
21234
+ });
21235
+ }
21236
+ return { params, bindings };
21237
+ }
21238
+
21239
+ // Argument 1 of a function whose own parameters already reach it: the second
21240
+ // parameter when the function never reassigns it, `arguments[1]` otherwise.
21241
+ function secondArgument(fn) {
21242
+ const params = fn.params || [];
21243
+ const second = params[1]?.type === 'AssignmentPattern' ? params[1].left : params[1];
21244
+ return second?.type === 'Identifier' &&
21245
+ params[0]?.type !== 'RestElement' &&
21246
+ !(params[0]?.type === 'Identifier' && params[0].name === 'this') &&
21247
+ !directCallAssignedNames(fn).has(second.name)
21248
+ ? b.id(second.name)
21249
+ : b.member(b.id('arguments'), b.literal(1), true);
21250
+ }
21251
+
21252
+ // The argument a body call receives its Scope in. A spare default parameter
21253
+ // keeps the function's `length` and avoids an `arguments` object; functions
21254
+ // that cannot take one read `arguments[1]`.
21255
+ function renderCallScopeParam(node, ctx) {
21256
+ const params = node.params || [];
21257
+ const statements = node.body?.body || [];
21258
+ const strictBody = statements.some(
21259
+ (statement) => statement.type === 'ExpressionStatement' && statement.directive === 'use strict',
21260
+ );
21261
+ if (
21262
+ params.length > 1 ||
21263
+ strictBody ||
21264
+ params.some(
21265
+ (param) =>
21266
+ param.type === 'RestElement' || (param.type === 'Identifier' && param.name === 'this'),
21267
+ )
21268
+ ) {
21269
+ return { params, scope: inheritOriginLoc(secondArgument(node), node) };
21270
+ }
21271
+ const scope = inheritOriginLoc(b.id(allocCompilerName(ctx, '__renderScope')), node);
21272
+ const spare = (identifier) => inheritOriginLoc(b.assignment_pattern(identifier, b.void0), node);
21273
+ return {
21274
+ params:
21275
+ params.length === 0
21276
+ ? [spare(inheritOriginLoc(b.id(allocCompilerName(ctx, '__props')), node)), spare(scope)]
21277
+ : [...params, spare(scope)],
21278
+ scope,
21279
+ };
21280
+ }
21281
+
21282
+ /**
21283
+ * A returned host root that keeps its fragment renderer's own range.
21284
+ *
21285
+ * The client renders every returned host root through a compiled fragment. That
21286
+ * fragment mounts markerless when the host is its one template element, so the
21287
+ * server inlines the host without a range. A host the client builds
21288
+ * imperatively (keyed, `noscript`, document, or parser-repaired) is a descriptor
21289
+ * hole inside the fragment instead, and the server frames that hole with its
21290
+ * own range. A markerless fragment would adopt that inner range as its own
21291
+ * during hydration, so both sides give this root the fragment range that
21292
+ * returned fragments keep.
21293
+ */
21294
+ function isRangedReturnedHost(node) {
21295
+ return (
21296
+ (node.type === 'Element' || node.type === 'JSXElement') &&
21297
+ !isComponentTag(node) &&
21298
+ (node._octaneImperativeHost === true || alwaysImperativeHost(node))
21299
+ );
21300
+ }
21301
+
20535
21302
  // Lower JSX at return position. Host roots always use the compiled-fragment
20536
21303
  // path. Any other root whose subtree contains template-only syntax uses it too.
20537
21304
  // Ordinary component/Fragment values retain descriptor lowering and its identity
@@ -20670,6 +21437,11 @@ function rewriteExtractedFragmentHole(expression, ctx, parentNs) {
20670
21437
  }
20671
21438
  const fold = ctx._foldCtx;
20672
21439
  if (fold?.compInlinedSubs === undefined) return lowered;
21440
+ // The owner's body sits outside any callback the directive is written in, so a
21441
+ // sub-template reading that callback's names compiles in place instead. The
21442
+ // hole props are built at the call site, inside the callback, so its closure
21443
+ // sees them.
21444
+ if (readsCallbackScope(lowered, ctx._callbackScopeNames)) return rewriteJsxValues(lowered, ctx);
20673
21445
  return rewriteTsrxBlocks(lowered, ctx, 'fragment', fold.compInlinedSubs, parentNs, fold.cssHash);
20674
21446
  }
20675
21447
 
@@ -20741,7 +21513,18 @@ function isStaticReturnedFragmentComponent(node, ctx) {
20741
21513
  // text, nested host elements/fragments) stays in the template; nested component
20742
21514
  // children become renderable holes. The result is a self-contained fragment whose
20743
21515
  // only inputs are its props — compilable as an ordinary renderer.
20744
- function extractFragment(node, ctx, holeProps, parentNs = 'html') {
21516
+ //
21517
+ // The renderer builds a keyed, `noscript`/document, or parser-repaired host as a
21518
+ // descriptor (isDescriptorBuiltHost), and a descriptor child must be a value: a
21519
+ // FoldedDirective or template-only component placeholder under it would be
21520
+ // dropped. Inside such a host (`inDescriptor`), directives, components, and child
21521
+ // `@{}` blocks lower to value holes here in the owning component, as a `@{}` body
21522
+ // and the server lower them. Misreading a template host as a descriptor host costs
21523
+ // only the template fast path; the reverse drops children.
21524
+ function extractFragment(node, ctx, holeProps, parentNs = 'html', inDescriptor = false) {
21525
+ const descriptor =
21526
+ inDescriptor ||
21527
+ (node.type === 'JSXElement' && isDescriptorBuiltHost(node, parentNs === 'svg', ctx));
20745
21528
  const attrs = node.attributes || node.openingElement?.attributes || [];
20746
21529
  const newAttrs = [];
20747
21530
  const mergedFragmentSpread = isFragmentLongForm(node, ctx) && hasJsxSpreadAttribute(node);
@@ -20851,14 +21634,16 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
20851
21634
  // Lower `{xs.map(item => <jsx key/>)}` children to a synthetic `@for`
20852
21635
  // (ForOfStatement) up front so they take the directive fold path below
20853
21636
  // (forBlock) instead of becoming a childSlot descriptor-array hole. Only when
20854
- // we have a fold context (always true on the .tsx host-fragment path).
20855
- const fragChildren = ctx._foldCtx
20856
- ? (node.children || []).map((child) =>
20857
- child && child.type === 'JSXExpressionContainer'
20858
- ? mapCallToForOf(child.expression, ctx) || child
20859
- : child,
20860
- )
20861
- : node.children || [];
21637
+ // we have a fold context (always true on the .tsx host-fragment path) and the
21638
+ // children are template children.
21639
+ const fragChildren =
21640
+ ctx._foldCtx && !descriptor
21641
+ ? (node.children || []).map((child) =>
21642
+ child && child.type === 'JSXExpressionContainer'
21643
+ ? mapCallToForOf(child.expression, ctx) || child
21644
+ : child,
21645
+ )
21646
+ : node.children || [];
20862
21647
  const nodeTag = jsxTagName(node) || elementTagName(node);
20863
21648
  const childNs =
20864
21649
  typeof nodeTag === 'string' && !isComponentTag(node)
@@ -20908,9 +21693,12 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
20908
21693
  newChildren.push({ ...child, expression: rendered });
20909
21694
  }
20910
21695
  } else if (t === 'Element' || t === 'JSXElement') {
20911
- if (isLongFormTemplateSentinel(child, childNs, true, ctx)) {
21696
+ if (descriptor && !isComponentTag(child)) {
21697
+ newChildren.push(extractFragment(child, ctx, holeProps, childNs, true));
21698
+ } else if (!descriptor && isLongFormTemplateSentinel(child, childNs, true, ctx)) {
20912
21699
  newChildren.push(extractFragment(child, ctx, holeProps, childNs));
20913
21700
  } else if (
21701
+ !descriptor &&
20914
21702
  isComponentTag(child) &&
20915
21703
  (child._octaneBindingSite ||
20916
21704
  (ctx._foldCtx?.templateComponentChildren === true &&
@@ -20955,7 +21743,24 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
20955
21743
  // A fragment nested inside the returned fragment shares the hoisted
20956
21744
  // renderer. Extract its dynamic values/directives too; leaving it raw would
20957
21745
  // make authored outer locals resolve against the renderer's hole-props object.
20958
- newChildren.push(extractFragment(child, ctx, holeProps, childNs));
21746
+ newChildren.push(extractFragment(child, ctx, holeProps, childNs, descriptor));
21747
+ } else if (descriptor && VALUE_DIRECTIVE_ARM_TYPES.has(t)) {
21748
+ // lowerJsxChild folds the directive into a renderer owned by this
21749
+ // component, the value a `@{}` body passes the same descriptor.
21750
+ const hn = `h${holeProps.length}`;
21751
+ holeProps.push(objectProp(hn, lowerJsxChild(child, ctx)));
21752
+ newChildren.push(b.jsx_expression_container(memberProps(hn, child)));
21753
+ } else if (descriptor && t === 'JSXCodeBlock') {
21754
+ // lowerJsxChild lowers the block as it does under a `@{}` body's descriptor:
21755
+ // a render-only block transparently, and a setup-bearing one through this
21756
+ // component's fold. The renderer must never fold it, since that would move
21757
+ // the block's setup away from the locals it closes over.
21758
+ const value = lowerJsxChild(child, ctx);
21759
+ if (value !== null) {
21760
+ const hn = `h${holeProps.length}`;
21761
+ holeProps.push(objectProp(hn, value));
21762
+ newChildren.push(b.jsx_expression_container(memberProps(hn, child)));
21763
+ }
20959
21764
  } else if (t === 'JSXCodeBlock') {
20960
21765
  const body = child.body || [];
20961
21766
  if (body.length === 0) {
@@ -21389,11 +22194,16 @@ function lowerHostFragment(
21389
22194
  node,
21390
22195
  );
21391
22196
  const renderer = compileFunctionBody(synthFn, ctx, fragName, parentNs, cssHash);
21392
- if ((node.type === 'Element' || node.type === 'JSXElement') && !isComponentTag(node)) {
22197
+ if (
22198
+ (node.type === 'Element' || node.type === 'JSXElement') &&
22199
+ !isComponentTag(node) &&
22200
+ !isRangedReturnedHost(node)
22201
+ ) {
21393
22202
  // A host fragment is a SINGLE root element, so it can mount markerless (the
21394
22203
  // element self-delimits) — matching `@{}`'s inline render exactly (no extra
21395
22204
  // comment markers), which is required for byte-equal DOM when folding `@{}`.
21396
- // A returned JSX fragment may have multiple roots and must retain its range.
22205
+ // A returned JSX fragment may have multiple roots and must retain its range,
22206
+ // as must an imperatively built host (see isRangedReturnedHost).
21397
22207
  ctx.hoistedHelpers.push(
21398
22208
  inheritOriginLoc(
21399
22209
  b.const(fragName, singleRootInitializer(ctx, functionExpressionFromDeclaration(renderer))),
@@ -21428,8 +22238,10 @@ function lowerHostFragment(
21428
22238
  *
21429
22239
  * In both cases the helper is added to `inlinedSubs` (visible in the
21430
22240
  * surrounding component-body scope) so it captures the parent component's
21431
- * locals via closure. It cannot capture params of nested arrows — see
21432
- * compiler README.
22241
+ * locals via closure. The names a callback inside `node` binds are not in scope
22242
+ * there, so with `leaveCallbackReads` a sub-template that reads one is left in
22243
+ * place, for the rewriteJsxValues pass that must follow to compile inside that
22244
+ * callback.
21433
22245
  */
21434
22246
  function rewriteTsrxBlocks(
21435
22247
  node,
@@ -21438,8 +22250,15 @@ function rewriteTsrxBlocks(
21438
22250
  inlinedSubs,
21439
22251
  parentNs = 'html',
21440
22252
  cssHash = null,
22253
+ leaveCallbackReads = false,
21441
22254
  ) {
21442
- return mapAst(node, (n) => {
22255
+ return rewrite(node, null);
22256
+
22257
+ function rewrite(root, callbackScope) {
22258
+ return mapAst(root, (n) => visit(n, callbackScope));
22259
+ }
22260
+
22261
+ function visit(n, callbackScope) {
21443
22262
  if (n.type === 'Tsrx' || n.type === 'Tsx') {
21444
22263
  const helperName = `__tsrx$${ctx.nextHelperId++}`;
21445
22264
  const fakeBody = {
@@ -21453,6 +22272,8 @@ function rewriteTsrxBlocks(
21453
22272
  return inheritOriginLoc(b.id(helperName), n);
21454
22273
  }
21455
22274
  if (n.type === 'ArrowFunctionExpression' && n.body && n.body.type === 'JSXCodeBlock') {
22275
+ // The callback's names do not exist in the body the helper would join.
22276
+ if (readsCallbackScope(n, callbackScope)) return n;
21456
22277
  // `() => @{ … }` — new sub-template form. Hoist as a regular component
21457
22278
  // body so its body.body (setup) + body.render (JSX) feed back through
21458
22279
  // the standard compileFunctionBody path.
@@ -21466,11 +22287,90 @@ function rewriteTsrxBlocks(
21466
22287
  );
21467
22288
  fakeBody.generator = n.generator;
21468
22289
  if (n.returnType !== undefined) fakeBody.returnType = n.returnType;
21469
- inlinedSubs.push(compileFunctionBody(fakeBody, ctx, helperName, parentNs, cssHash));
22290
+ inlinedSubs.push(
22291
+ withNestedTemplateScope(fakeBody, ctx, () =>
22292
+ compileFunctionBody(fakeBody, ctx, helperName, parentNs, cssHash),
22293
+ ),
22294
+ );
21470
22295
  return inheritOriginLoc(b.id(helperName), n);
21471
22296
  }
22297
+ if (leaveCallbackReads && isFunctionNode(n)) {
22298
+ const scope = new Set(callbackScope);
22299
+ for (const name of collectCallbackBindings(n)) scope.add(name);
22300
+ let out = n;
22301
+ for (const key in n) {
22302
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
22303
+ const child = n[key];
22304
+ if (child === null || typeof child !== 'object') continue;
22305
+ const mapped = rewrite(child, scope);
22306
+ if (mapped !== child) {
22307
+ if (out === n) out = { ...n };
22308
+ out[key] = mapped;
22309
+ }
22310
+ }
22311
+ return out;
22312
+ }
21472
22313
  return null;
21473
- });
22314
+ }
22315
+ }
22316
+
22317
+ /**
22318
+ * Compile a template function nested inside another body — a `() => @{ … }`
22319
+ * sub-template (including the scoped child a setup-bearing `@{ … }` child
22320
+ * lowers to) or a `function F() @{ … }` declared in a component — with its own
22321
+ * params and locals visible to the constructs inside it.
22322
+ *
22323
+ * The nested function closes over the enclosing body, but its `@if`/`@for`/
22324
+ * `@switch`/`@try` arms and lifted event bundles hoist to module scope and
22325
+ * receive what they read through an env tuple that `helperCaptures` builds
22326
+ * from `ctx.currentComponentLocals`. Every name visible at those call sites
22327
+ * must be in that set: the enclosing body's, which the function still reaches,
22328
+ * plus the function's own. A missing name is left out of the tuple and the
22329
+ * hoisted arm reads it as a free identifier.
22330
+ *
22331
+ * A name the function introduces can shadow an enclosing lifetime-invariant
22332
+ * binding with one that changes between renders, so it must not inherit that
22333
+ * proof. `compileFunctionBody` recomputes the nested body's own invariants.
22334
+ *
22335
+ * Inside a module-level callback (`untracked`), nothing tracks the names the
22336
+ * enclosing callbacks bind, so no env tuple can carry them. The whole nested
22337
+ * compile, including the templates nested in it, then runs with no component
22338
+ * context, which keeps every arm inline where it closes over those names.
22339
+ */
22340
+ function withNestedTemplateScope(fn, ctx, compile, untracked = false) {
22341
+ const prevLocals = ctx.currentComponentLocals;
22342
+ const prevInvariantLocals = ctx.currentInvariantLocals;
22343
+ const prevEventInvariantLocals = ctx.currentEventInvariantLocals;
22344
+ const prevUntracked = ctx._untrackedScope;
22345
+ const introduced = collectComponentLocals(fn);
22346
+ if (prevLocals == null && (untracked || prevUntracked === true)) {
22347
+ ctx._untrackedScope = true;
22348
+ } else {
22349
+ const locals = new Set(prevLocals);
22350
+ for (const name of introduced) locals.add(name);
22351
+ ctx.currentComponentLocals = locals;
22352
+ }
22353
+ ctx.currentInvariantLocals = withoutShadowedNames(prevInvariantLocals, introduced);
22354
+ ctx.currentEventInvariantLocals = withoutShadowedNames(prevEventInvariantLocals, introduced);
22355
+ try {
22356
+ return compile();
22357
+ } finally {
22358
+ ctx._untrackedScope = prevUntracked;
22359
+ ctx.currentComponentLocals = prevLocals;
22360
+ ctx.currentInvariantLocals = prevInvariantLocals;
22361
+ ctx.currentEventInvariantLocals = prevEventInvariantLocals;
22362
+ }
22363
+ }
22364
+
22365
+ function withoutShadowedNames(names, introduced) {
22366
+ if (names == null || introduced.size === 0) return names;
22367
+ let out = names;
22368
+ for (const name of introduced) {
22369
+ if (!out.has(name)) continue;
22370
+ if (out === names) out = new Set(names);
22371
+ out.delete(name);
22372
+ }
22373
+ return out;
21474
22374
  }
21475
22375
 
21476
22376
  const SETUP_VALUE_DIRECTIVE_TYPES = new Set([
@@ -21485,7 +22385,7 @@ const SETUP_VALUE_DIRECTIVE_TYPES = new Set([
21485
22385
  // `@{ … }` block is also folded as a setup value, but it is a sub-template rather
21486
22386
  // than a set of arms — `rewriteTsrxBlocks` owns its expression-position handling,
21487
22387
  // and the unowned-directive diagnostic's advice does not apply to it — so it is
21488
- // deliberately absent here.
22388
+ // deliberately absent here. With no owning body, lowerJsxChild compiles it in place.
21489
22389
  const VALUE_DIRECTIVE_ARM_TYPES = new Set([
21490
22390
  'JSXIfExpression',
21491
22391
  'JSXForExpression',
@@ -21841,18 +22741,18 @@ function rewriteJsxValues(node, ctx, eagerMapCallbackRoots = false, eagerMapCall
21841
22741
  const t = n && n.type;
21842
22742
  if (isFunctionNode(n) && n.body?.type === 'JSXCodeBlock') {
21843
22743
  const name = n.id?.name ?? allocCompilerName(ctx, '__template');
21844
- const previousLocals = ctx.currentComponentLocals;
21845
22744
  // A nested body must not replace the enclosing component's warm plan.
21846
22745
  const previousWarm = ctx._pendingWarm;
21847
- ctx.currentComponentLocals = collectComponentLocals(n);
22746
+ const compile = () =>
22747
+ ctx.mode === 'server'
22748
+ ? ssrCompileBody(n, ctx, name, null, [], 'opaque')
22749
+ : compileFunctionBody(n, ctx, name, 'opaque');
21848
22750
  try {
21849
- const compiled =
21850
- ctx.mode === 'server'
21851
- ? ssrCompileBody(n, ctx, name, null, [], 'opaque')
21852
- : compileFunctionBody(n, ctx, name, 'opaque');
22751
+ // A missing fold marks a module-level callback, whose names only this
22752
+ // function's closure can reach.
22753
+ const compiled = withNestedTemplateScope(n, ctx, compile, lower == null);
21853
22754
  return functionExpressionFromDeclaration({ ...compiled, id: n.id ?? null }, n);
21854
22755
  } finally {
21855
- ctx.currentComponentLocals = previousLocals;
21856
22756
  ctx._pendingWarm = previousWarm;
21857
22757
  }
21858
22758
  }
@@ -21926,10 +22826,13 @@ function rewriteJsxValues(node, ctx, eagerMapCallbackRoots = false, eagerMapCall
21926
22826
  // arms beside values they cannot reach. Drop the fold for this subtree so
21927
22827
  // re-entries below (an attribute value re-enters rewriteJsxValues, with no
21928
22828
  // function node left in view) cannot pick it back up, and the directive
21929
- // reaches the unowned diagnostic instead of folding into the wrong scope.
22829
+ // reaches the unowned diagnostic instead of folding into the wrong scope. A
22830
+ // `@{ … }` child block has no arms, so lowerJsxChild compiles it in place.
21930
22831
  ctx._valueDirectiveLowering = null;
21931
22832
  } else {
21932
- const introduced = collectComponentLocals(n);
22833
+ // Including names bound in the callback's nested blocks, which a fold
22834
+ // written in one of those blocks reads just the same.
22835
+ const introduced = collectCallbackBindings(n);
21933
22836
  const extended = new Set(previousLocals);
21934
22837
  for (const name of introduced) extended.add(name);
21935
22838
  ctx.currentComponentLocals = extended;
@@ -22000,6 +22903,13 @@ function rewriteJsxValues(node, ctx, eagerMapCallbackRoots = false, eagerMapCall
22000
22903
  function lowerInspectableJsxChild(child, ctx) {
22001
22904
  const fold = ctx._valueDirectiveLowering;
22002
22905
  if (fold == null) return lowerJsxChild(child, ctx);
22906
+ // A render-only @{} block is transparent grouping: lowerJsxChild unwraps it
22907
+ // to its render root. Letting lowerSetupValueDirectives fold it would produce
22908
+ // a component range on the server while the descriptor path emits the
22909
+ // unwrapped children on the client, breaking hydration.
22910
+ if (child && child.type === 'JSXCodeBlock' && (child.body?.length ?? 0) === 0) {
22911
+ return lowerJsxChild(child, ctx);
22912
+ }
22003
22913
  const prepared = lowerSetupValueDirectives(child, fold);
22004
22914
  const t = prepared && prepared.type;
22005
22915
  if (
@@ -22039,6 +22949,19 @@ function lowerJsxChild(child, ctx) {
22039
22949
  if (fold != null) return fold(child);
22040
22950
  rejectUnownedValueDirective(child);
22041
22951
  }
22952
+ if (t === 'JSXCodeBlock') {
22953
+ // A render-only block is transparent grouping, as under a template host.
22954
+ if ((child.body?.length ?? 0) === 0) {
22955
+ return child.render ? lowerJsxChild(child.render, ctx) : null;
22956
+ }
22957
+ // A setup-bearing or code-only block is its own render scope. The owning
22958
+ // body's fold compiles it into a renderer that closes over that body, the
22959
+ // value both targets build. With no owning body, as inside a module-level
22960
+ // callback, it compiles in place as the `() => @{ … }` child that
22961
+ // normalizeChildren makes of it, closing over the callback's params.
22962
+ const fold = ctx._valueDirectiveLowering;
22963
+ return fold != null ? fold(child) : rewriteJsxValues(childCodeBlockArrow(child), ctx);
22964
+ }
22042
22965
  if (t === 'JSXFragment' || t === 'Fragment') {
22043
22966
  const els = [];
22044
22967
  for (const c of child.children || []) {
@@ -22066,7 +22989,7 @@ function lowerJsxChild(child, ctx) {
22066
22989
  scopedElement,
22067
22990
  inheritOriginLoc(b.id(rtAlias('Fragment')), child),
22068
22991
  inheritOriginLoc(b.object([]), child),
22069
- inheritOriginLoc(b.arrow([], children), child),
22992
+ inheritOriginLoc(deferredRead(ctx, children), child),
22070
22993
  ),
22071
22994
  child,
22072
22995
  );
@@ -22096,18 +23019,19 @@ function jsxNameToExpr(name) {
22096
23019
  // hit the TDZ, while a member, coercion, spread, or computed key can invoke
22097
23020
  // user code without containing a call. Function expressions are values here;
22098
23021
  // their bodies still run only when an event/render-prop consumer invokes them.
22099
- function isStaticJsxValueExpression(expression) {
23022
+ function isStaticJsxValueExpression(expression, stableNames = null) {
22100
23023
  const value = unwrapTsExpr(expression);
22101
23024
  return (
22102
23025
  isInvariantLiteral(value) ||
22103
23026
  value?.type === 'ArrowFunctionExpression' ||
22104
- value?.type === 'FunctionExpression'
23027
+ value?.type === 'FunctionExpression' ||
23028
+ (stableNames !== null && value?.type === 'Identifier' && stableNames.has(value.name))
22105
23029
  );
22106
23030
  }
22107
23031
 
22108
23032
  // A root member tag, spread, or dynamic prop can invoke user code before its
22109
23033
  // descriptor exists. Defer the complete record rather than just its children.
22110
- function jsxValueRootNeedsRenderScope(node) {
23034
+ function jsxValueRootNeedsRenderScope(node, stableNames = null) {
22111
23035
  const name = node.openingElement?.name || node.id;
22112
23036
  if (
22113
23037
  name?.type === 'MemberExpression' ||
@@ -22120,7 +23044,7 @@ function jsxValueRootNeedsRenderScope(node) {
22120
23044
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') return true;
22121
23045
  if (attr.type !== 'Attribute' && attr.type !== 'JSXAttribute') continue;
22122
23046
  if (attr.value?.type !== 'JSXExpressionContainer') continue;
22123
- if (!isStaticJsxValueExpression(attr.value.expression)) return true;
23047
+ if (!isStaticJsxValueExpression(attr.value.expression, stableNames)) return true;
22124
23048
  }
22125
23049
  return false;
22126
23050
  }
@@ -22129,21 +23053,27 @@ function jsxValueRootNeedsRenderScope(node) {
22129
23053
  // syntax is effectful. Nested component/tag resolution and dynamic attributes
22130
23054
  // belong to their represented parent, so they make that parent's children lazy
22131
23055
  // even when the nested element has no expression children of its own.
22132
- function jsxValueChildrenNeedRenderScope(node, descendantElementsOwnChildren = false) {
23056
+ function jsxValueChildrenNeedRenderScope(
23057
+ node,
23058
+ descendantElementsOwnChildren = false,
23059
+ stableNames = null,
23060
+ ) {
22133
23061
  for (const child of node.children || []) {
22134
23062
  if (child == null || child.type === 'JSXText' || child.type === 'Text') continue;
22135
23063
  if (child.type === 'JSXExpressionContainer') {
22136
23064
  if (
22137
23065
  child.expression &&
22138
23066
  child.expression.type !== 'JSXEmptyExpression' &&
22139
- !isStaticJsxValueExpression(child.expression)
23067
+ !isStaticJsxValueExpression(child.expression, stableNames)
22140
23068
  ) {
22141
23069
  return true;
22142
23070
  }
22143
23071
  continue;
22144
23072
  }
22145
23073
  if (child.type === 'Fragment' || child.type === 'JSXFragment') {
22146
- if (jsxValueChildrenNeedRenderScope(child, descendantElementsOwnChildren)) return true;
23074
+ if (jsxValueChildrenNeedRenderScope(child, descendantElementsOwnChildren, stableNames)) {
23075
+ return true;
23076
+ }
22147
23077
  continue;
22148
23078
  }
22149
23079
  if (child.type !== 'Element' && child.type !== 'JSXElement') return true;
@@ -22152,11 +23082,16 @@ function jsxValueChildrenNeedRenderScope(node, descendantElementsOwnChildren = f
22152
23082
  if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') return true;
22153
23083
  if (attr.type !== 'Attribute' && attr.type !== 'JSXAttribute') return true;
22154
23084
  if (attr.value?.type !== 'JSXExpressionContainer') continue;
22155
- if (!isStaticJsxValueExpression(attr.value.expression)) return true;
23085
+ if (!isStaticJsxValueExpression(attr.value.expression, stableNames)) return true;
22156
23086
  }
22157
23087
  // A fragment can retain its inspectable positional array when only a nested
22158
23088
  // element's descendants are dynamic: that element already owns their scope.
22159
- if (!descendantElementsOwnChildren && jsxValueChildrenNeedRenderScope(child)) return true;
23089
+ if (
23090
+ !descendantElementsOwnChildren &&
23091
+ jsxValueChildrenNeedRenderScope(child, false, stableNames)
23092
+ ) {
23093
+ return true;
23094
+ }
22160
23095
  }
22161
23096
  return false;
22162
23097
  }
@@ -22452,8 +23387,9 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
22452
23387
  const memoizedDescriptor = inheritOriginLoc(
22453
23388
  b.call(
22454
23389
  requireRuntimeForContext(ctx, ctx.nativeReads ? 'nativePuMemo' : 'useMemo'),
22455
- b.arrow(
22456
- [],
23390
+ // Runs synchronously inside readChildren, so it reads the same snapshots.
23391
+ deferredRead(
23392
+ ctx,
22457
23393
  b.sequence([b.assignment('=', b.id(freshName), b.literal(true)), childrenValue]),
22458
23394
  ),
22459
23395
  b.array([rows, ...(captures ?? []).map((name) => inheritOriginLoc(b.id(name), host))]),
@@ -22504,7 +23440,10 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
22504
23440
  const scopedElement = ctx.nativeReads
22505
23441
  ? requireRuntimeForContext(ctx, 'nativeCreateScopedElement')
22506
23442
  : rtAlias('createScopedElement');
22507
- const readChildren = inheritOriginLoc(b.arrow([], memoizedChildrenBody ?? childrenValue), node);
23443
+ const readChildren = inheritOriginLoc(
23444
+ deferredRead(ctx, memoizedChildrenBody ?? childrenValue),
23445
+ node,
23446
+ );
22508
23447
  descriptor = inheritOriginLoc(
22509
23448
  b.call(
22510
23449
  scopedElement,
@@ -22538,11 +23477,19 @@ function jsxElementToCreateElement(node, ctx, eagerRoot = false) {
22538
23477
  ? requireRuntimeForContext(ctx, 'nativeCreateScopedValue')
22539
23478
  : rtAlias('createScopedValue');
22540
23479
  return inheritOriginLoc(
22541
- b.call(scopedValue, inheritOriginLoc(b.arrow([], descriptor), node)),
23480
+ b.call(scopedValue, inheritOriginLoc(deferredRead(ctx, descriptor), node)),
22542
23481
  node,
22543
23482
  );
22544
23483
  }
22545
23484
 
23485
+ // A value-position JSX thunk: its body runs when Octane renders or inspects the
23486
+ // element. printNodeWithMap snapshots the reassigned bindings it reads, so it
23487
+ // sees their values from when the JSX evaluated (see deferred-snapshots.js).
23488
+ function deferredRead(ctx, body) {
23489
+ ctx.deferredReads = true;
23490
+ return markDeferredRead(b.arrow([], body));
23491
+ }
23492
+
22546
23493
  // Short, unique, path-free slot description for non-HMR output: a djb2 hash of
22547
23494
  // the module filename + the per-module hook index. The DESCRIPTION is
22548
23495
  // load-bearing beyond debugging: resolveSlot/currentPathSlot (runtime) compose
@@ -23398,9 +24345,17 @@ function normalizeAuthoredJsxLiterals(ast) {
23398
24345
  }
23399
24346
 
23400
24347
  function requiresImperativeHostTree(root, ctx) {
24348
+ return (
24349
+ (ctx.mode !== 'server' && root._octaneImperativeHost === true) || alwaysImperativeHost(root)
24350
+ );
24351
+ }
24352
+
24353
+ // Hosts that both compile modes build imperatively. The server writes a
24354
+ // parser-repaired tree as a string, so repair cannot move its bindings there;
24355
+ // normalizeChildren frames it in the hole range the client hydrates instead.
24356
+ function alwaysImperativeHost(root) {
23401
24357
  const tag = jsxTagName(root) || elementTagName(root);
23402
24358
  return (
23403
- (ctx.mode !== 'server' && root._octaneImperativeHost === true) ||
23404
24359
  hasJsxAttribute(root, 'key') ||
23405
24360
  tag === 'noscript' ||
23406
24361
  tag === 'html' ||
@@ -23485,6 +24440,17 @@ function rewriteImperativeHeadElements(node, ctx, namespace = 'html', inNoscript
23485
24440
  };
23486
24441
  }
23487
24442
 
24443
+ // A JSXElement that normalizeChildren lowers with jsxElementToCreateElement
24444
+ // instead of the template compiler, so its whole subtree becomes descriptor
24445
+ // values. extractFragment uses the same test to keep that subtree's children in
24446
+ // value form.
24447
+ function isDescriptorBuiltHost(node, inSvg, ctx) {
24448
+ return (
24449
+ !isComponentTag(node) &&
24450
+ (hasJsxAttribute(node, 'key') || (!inSvg && requiresImperativeHostTree(node, ctx)))
24451
+ );
24452
+ }
24453
+
23488
24454
  /**
23489
24455
  * Normalize a list of JSX child nodes into the shapes the emitters consume:
23490
24456
  * - Indentation-only JSXText / JSX comments (`{…}` empty containers) → dropped
@@ -23632,12 +24598,7 @@ function normalizeChildren(
23632
24598
  // Keys need a reconciliation boundary. Parser-sensitive host trees need
23633
24599
  // imperative construction so HTML repair cannot change binding paths.
23634
24600
  // The shared descriptor path provides both without taxing ordinary templates.
23635
- if (
23636
- ctx &&
23637
- allowImperative &&
23638
- !isComponentTag(n) &&
23639
- (hasJsxAttribute(n, 'key') || (!inSvg && requiresImperativeHostTree(n, ctx)))
23640
- ) {
24601
+ if (ctx && allowImperative && isDescriptorBuiltHost(n, inSvg, ctx)) {
23641
24602
  out.push(
23642
24603
  inheritOriginLoc(
23643
24604
  {
@@ -23691,6 +24652,13 @@ function normalizeChildren(
23691
24652
  // `unstable_Activity` starts with a lowercase letter but is a component
23692
24653
  // when it resolves to the builtin. Keep that fact on this rare node only.
23693
24654
  if (isActivityLongForm(n, ctx)) element.activityDescriptor = true;
24655
+ // The client builds a parser-repaired host through the descriptor hole
24656
+ // above. The server keeps its template string and nesting diagnostics,
24657
+ // but frames it as that hole's range: the browser's repair then stays
24658
+ // inside a range the client can adopt or discard, and later siblings
24659
+ // keep their positions.
24660
+ if (ctx?.mode === 'server' && allowImperative && !inSvg && n._octaneImperativeHost === true)
24661
+ element.parserRepairRange = true;
23694
24662
  // Preserve @tsrx/core's raw-text script discriminator without changing
23695
24663
  // the normalized object shape of every ordinary element.
23696
24664
  if (elementTagName(element) === 'script' && typeof n.content === 'string') {
@@ -25756,7 +26724,10 @@ function planJsx(
25756
26724
  // Const-seeded straight into the bag factory args — no mount statement.
25757
26725
  if (cc.isChild && !noTemplate) {
25758
26726
  bag.constField(`_chv$${cc.id}`, 'null');
25759
- bag.constField(`_chp$${cc.id}`, 'undefined');
26727
+ // An only-child hole's first render must reach childTextHole even for
26728
+ // `undefined`: while hydrating, that call reconciles the host's server
26729
+ // children, which no other binding owns.
26730
+ bag.constField(`_chp$${cc.id}`, cc.onlyChildText ? 'unset' : 'undefined');
25760
26731
  }
25761
26732
  },
25762
26733
  });
@@ -25801,15 +26772,23 @@ function planJsx(
25801
26772
  );
25802
26773
  }
25803
26774
  // Const-seeded fields keep their registry strings until the factory call.
25804
- // A mixed-style scalar starts at its private owning scope so its first
25805
- // deferred write still runs for null/undefined. The same identity lets its
25806
- // setter distinguish a fresh mount from a preserved suspended retry.
26775
+ // An `unset` field starts at its private owning scope, which no rendered
26776
+ // value equals, so its first write still runs for null/undefined. For a
26777
+ // mixed-style scalar, the same identity lets its setter distinguish a fresh
26778
+ // mount from a preserved suspended retry.
25807
26779
  const constArgNode = (expr) =>
25808
- expr === 'null' ? b.literal(null) : expr === 'style-unset' ? b.id('__s') : b.id(expr);
25809
- const bagFieldValue = (f) => (f.constExpr !== null ? constArgNode(f.constExpr) : b.id(f.local));
26780
+ expr === 'null' ? b.literal(null) : expr === 'unset' ? b.id('__s') : b.id(expr);
26781
+ const bagFieldValue = (f) =>
26782
+ f.hostVar !== null
26783
+ ? b.id(f.hostVar)
26784
+ : f.constExpr !== null
26785
+ ? constArgNode(f.constExpr)
26786
+ : b.id(f.local);
25810
26787
  const rootArg = () => (single ? b.id('_root') : b.literal(null));
25811
26788
  if (bag.fields.length <= BAG_FACTORY_MAX) {
25812
26789
  ctx.runtimeNeeded.add(`bag${bag.fields.length}`);
26790
+ // The arity factory's object literal still declares every field, so an
26791
+ // omitted trailing `undefined` seed keeps the same bag shape.
25813
26792
  mountLines.push(
25814
26793
  inheritOriginLoc(
25815
26794
  b.stmt(
@@ -25820,7 +26799,9 @@ function planJsx(
25820
26799
  `_$bag${bag.fields.length}`,
25821
26800
  b.id('__s'),
25822
26801
  rootArg(),
25823
- ...bag.fields.map(bagFieldValue),
26802
+ ...optionalCallArgs(
26803
+ ...bag.fields.map((f) => (f.constExpr === 'undefined' ? null : bagFieldValue(f))),
26804
+ ),
25824
26805
  ),
25825
26806
  ),
25826
26807
  ),
@@ -26461,10 +27442,12 @@ function planJsx(
26461
27442
  cc.valueExpr,
26462
27443
  b.literal(cc.signalSite),
26463
27444
  childAnchor ?? b.literal(null),
26464
- cc.anchorVar ? b.literal(true) : undefinedNode(),
26465
- undefinedNode(),
26466
- cc.coalesceRange ? b.literal(true) : undefinedNode(),
26467
- cc.onlyChildText ? b.literal(true) : undefinedNode(),
27445
+ ...optionalCallArgs(
27446
+ cc.anchorVar ? b.literal(true) : null,
27447
+ null,
27448
+ cc.coalesceRange ? b.literal(true) : null,
27449
+ cc.onlyChildText ? b.literal(true) : null,
27450
+ ),
26468
27451
  ),
26469
27452
  ),
26470
27453
  ),
@@ -26662,15 +27645,13 @@ function planJsx(
26662
27645
  );
26663
27646
  const memoAnchor = anchorNodeFor(cc, 'compAnchor');
26664
27647
  const trailing = liteMemo
26665
- ? [memoAnchor ?? undefinedNode(), b.literal(cc.invocationSite)]
26666
- : [
26667
- memoAnchor ?? undefinedNode(),
26668
- undefinedNode(),
26669
- cc.singleRoot ? b.literal(true) : undefinedNode(),
26670
- cc.inheritRange ? b.literal(true) : undefinedNode(),
26671
- undefinedNode(),
27648
+ ? optionalCallArgs(b.literal(cc.invocationSite), memoAnchor)
27649
+ : optionalCallArgs(
26672
27650
  b.literal(cc.invocationSite),
26673
- ];
27651
+ memoAnchor,
27652
+ cc.singleRoot ? b.literal(true) : null,
27653
+ cc.inheritRange ? b.literal(true) : null,
27654
+ );
26674
27655
  const witnessMiss = cc.autoMemoWitnesses.length
26675
27656
  ? witnessMissChain(cc.autoMemoWitnesses)
26676
27657
  : null;
@@ -26711,7 +27692,7 @@ function planJsx(
26711
27692
  continue;
26712
27693
  }
26713
27694
  // M3 inherit-range: the sole comp-call root of a `@{}` body — the slot
26714
- // BORROWS the enclosing block's marker range (10th positional arg), so it
27695
+ // BORROWS the enclosing block's marker range (the `inherit` argument), so it
26715
27696
  // mints nothing and the server skips the child's frame pair at the same
26716
27697
  // site (ssrEmitComponent reads the same predicate). Supersedes lite (the
26717
27698
  // borrow needs a real Block behind the slot) and singleRoot (the borrow
@@ -26722,7 +27703,6 @@ function planJsx(
26722
27703
  if (cc.inheritRange) {
26723
27704
  const componentHelper = cc.voidComponent ? '_$componentSlotVoid' : '_$componentSlot';
26724
27705
  ctx.runtimeNeeded.add(cc.voidComponent ? 'componentSlotVoid' : 'componentSlot');
26725
- const inheritAnchor = anchorNodeFor(cc, 'compAnchor') ?? undefinedNode();
26726
27706
  pushAfterStmt(
26727
27707
  cc.id,
26728
27708
  org,
@@ -26734,12 +27714,12 @@ function planJsx(
26734
27714
  hostExpr(),
26735
27715
  cc.compNode,
26736
27716
  cc.propsExpr,
26737
- inheritAnchor,
26738
- undefinedNode(),
26739
- undefinedNode(),
26740
- b.literal(true),
26741
- undefinedNode(),
26742
- b.literal(cc.invocationSite),
27717
+ ...optionalCallArgs(
27718
+ b.literal(cc.invocationSite),
27719
+ anchorNodeFor(cc, 'compAnchor'),
27720
+ null,
27721
+ b.literal(true),
27722
+ ),
26743
27723
  ),
26744
27724
  ),
26745
27725
  );
@@ -26766,8 +27746,7 @@ function planJsx(
26766
27746
  hostExpr(),
26767
27747
  cc.compNode,
26768
27748
  cc.propsExpr,
26769
- liteAnchor ?? undefinedNode(),
26770
- b.literal(cc.invocationSite),
27749
+ ...optionalCallArgs(b.literal(cc.invocationSite), liteAnchor),
26771
27750
  ),
26772
27751
  ),
26773
27752
  );
@@ -26780,14 +27759,14 @@ function planJsx(
26780
27759
  // unmount move the slot DOM along with the block; an element host with
26781
27760
  // no in-template anchor can safely append).
26782
27761
  const compAnchor = anchorNodeFor(cc, 'compAnchor');
26783
- const trailing = [
26784
- compAnchor ?? undefinedNode(),
26785
- cc.keyExpr ?? undefinedNode(),
26786
- cc.singleRoot ? b.literal(true) : cc.maybeSingleRoot ? b.literal(2) : undefinedNode(),
26787
- undefinedNode(),
26788
- cc.keyExpr != null ? b.literal(true) : undefinedNode(),
27762
+ const trailing = optionalCallArgs(
26789
27763
  b.literal(cc.invocationSite),
26790
- ];
27764
+ compAnchor,
27765
+ cc.singleRoot ? b.literal(true) : cc.maybeSingleRoot ? b.literal(2) : null,
27766
+ null,
27767
+ cc.keyExpr ?? null,
27768
+ cc.keyExpr != null ? b.literal(true) : null,
27769
+ );
26791
27770
  let invocation = b.call(
26792
27771
  componentHelper,
26793
27772
  b.id('__s'),
@@ -26990,8 +27969,11 @@ const DEFERRABLE_MOUNT_KINDS = new Set([
26990
27969
  // field names are object properties, so unlike locals a minifier can never
26991
27970
  // shorten them; 1-char names are the shipped-bytes win. Each field is either
26992
27971
  // LOCAL-backed (the mount path assigns a pre-declared `_mN` local; the runtime
26993
- // bag factory receives it positionally) or CONST-seeded (`null`/`undefined`
26994
- // seeds pass straight to the factory — no local, no mount statement).
27972
+ // bag factory receives it positionally), CONST-seeded (`null`/`undefined`
27973
+ // seeds pass straight to the factory — no local, no mount statement), or
27974
+ // HOST-backed (a DOM host already held by the mount block's own `const _root` /
27975
+ // `const _elN`, which ensureVar declares before any binding mount and so before
27976
+ // the factory call: the factory reads that const directly, with no alias local).
26995
27977
  // Registration order = mount-write order = the factory's positional args =
26996
27978
  // the letter sequence, so `_$bagN(s, root, v0, v1, …)` builds `{a: v0, b: v1,
26997
27979
  // …}` with every field carrying its REAL mount value. `letter()` throws for a
@@ -27002,6 +27984,9 @@ const BAG_UC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
27002
27984
  // Highest shared-factory arity (bag0..bag16 in the runtime); bigger bags fall
27003
27985
  // back to `bagOf(__s, root, { … })` with an inline literal of real values.
27004
27986
  const BAG_FACTORY_MAX = 16;
27987
+ // Hosts the mount block declares as top-level consts. `__block.parentNode` is a
27988
+ // property read, so it keeps an ordinary local.
27989
+ const BAG_CONST_HOST = /^(?:_root|_el\d+)$/;
27005
27990
  function bagLetter(i) {
27006
27991
  if (i < 26) return BAG_LC[i];
27007
27992
  if (i < 52) return BAG_UC[i - 26];
@@ -27024,6 +28009,7 @@ function makeBag() {
27024
28009
  name: bagLetter(i),
27025
28010
  local: constExpr === undefined ? `_m${i}` : null,
27026
28011
  constExpr: constExpr === undefined ? null : constExpr,
28012
+ hostVar: null,
27027
28013
  };
27028
28014
  fields.push(r);
27029
28015
  byKey.set(key, r);
@@ -27031,16 +28017,34 @@ function makeBag() {
27031
28017
  return r;
27032
28018
  };
27033
28019
  return {
27034
- /** Mount-write target for `key` — the pre-declared local. */
27035
- local: (key) => reg(key, undefined).local,
27036
- /** Register one immutable DOM-host field; aliases reuse its first mount write. */
28020
+ /**
28021
+ * Mount-time reference for `key` — the pre-declared local, or a host-backed
28022
+ * field's const (read-only: host() returned false, so nothing assigns it).
28023
+ */
28024
+ local: (key) => {
28025
+ const r = reg(key, undefined);
28026
+ return r.hostVar ?? r.local;
28027
+ },
28028
+ /**
28029
+ * Register one immutable DOM-host field; aliases reuse its first mount write.
28030
+ * Returns true when the caller must emit `local(key) = host`.
28031
+ */
27037
28032
  host: (key, host) => {
27038
28033
  const existing = byHost.get(host);
27039
28034
  if (existing !== undefined) {
27040
28035
  byKey.set(key, existing);
27041
28036
  return false;
27042
28037
  }
27043
- byHost.set(host, reg(key, undefined));
28038
+ // Only a field this call creates can be host-backed: a key already read
28039
+ // through local() may have handed its `_mN` name to emitted code.
28040
+ const fresh = !byKey.has(key);
28041
+ const r = reg(key, undefined);
28042
+ byHost.set(host, r);
28043
+ if (fresh && BAG_CONST_HOST.test(host)) {
28044
+ r.local = null;
28045
+ r.hostVar = host;
28046
+ return false;
28047
+ }
27044
28048
  return true;
27045
28049
  },
27046
28050
  /** Seed `key` with a constant expression (no local, no mount write). */
@@ -27078,6 +28082,19 @@ function tsrxExprNode(node, ctx, componentName, inlinedSubs, parentNs = 'html',
27078
28082
  const undefinedNode = () => b.id('undefined');
27079
28083
  const nullNode = () => b.literal(null);
27080
28084
 
28085
+ // Optional positional arguments of a runtime writer whose parameters default to
28086
+ // undefined. Pass `null` for an absent entry: trailing absent entries are
28087
+ // omitted and interior ones become explicit `undefined` placeholders. Only use
28088
+ // it for writers that never inspect `arguments.length` (bindSignalText does).
28089
+ // componentSlot, componentSlotVoid, and componentSlotLite order their tail after
28090
+ // `props` by how often a call site supplies it — (invocationSite, anchor,
28091
+ // singleRoot, inherit, key, hasKey) — so the common call keeps two of the six.
28092
+ function optionalCallArgs(...entries) {
28093
+ let end = entries.length;
28094
+ while (end > 0 && entries[end - 1] == null) end--;
28095
+ return entries.slice(0, end).map((entry) => entry ?? undefinedNode());
28096
+ }
28097
+
27081
28098
  // Reference a compiled mount local / host var by NAME. ensureVar hands back
27082
28099
  // `__block.parentNode` for bagless hosts — resolve the member chain; every
27083
28100
  // other name (`_root`, `_el3`) is a plain identifier.
@@ -27263,7 +28280,7 @@ function emitDeferredMount(bind, elVar, bag) {
27263
28280
  : bind.kind === 'style' || bind.kind === 'styleProperties'
27264
28281
  ? `_sty$${bind.id}`
27265
28282
  : `_prev$${bind.id}`,
27266
- bind.kind === 'styleProperty' || bind.kind === 'styleProperties' ? 'style-unset' : 'undefined',
28283
+ bind.kind === 'styleProperty' || bind.kind === 'styleProperties' ? 'unset' : 'undefined',
27267
28284
  );
27268
28285
  if (bind.kind === 'styleProperties') {
27269
28286
  if (bind.spread) bag.constField(`_styFull$${bind.id}`, 'undefined');
@@ -27297,22 +28314,22 @@ function directSignalBindingHelper(bind) {
27297
28314
  function directSignalBindingArgs(bind, host, previous, value = bind.expr, previousValue) {
27298
28315
  const common = [b.id('__s'), previous, host];
27299
28316
  if (bind.kind === 'text' || bind.kind === 'textOnlyChild') {
28317
+ // (…, onlyChild, previousValue, seededText, bindingMarker). An ordinary
28318
+ // update passes only its raw-value cache: seededText matters only before
28319
+ // the first write. A binding-view marker stays on every call because
28320
+ // presentation hydration can replay a retry through the update path.
27300
28321
  return [
27301
28322
  ...common,
27302
28323
  value,
27303
28324
  b.literal(bind.signalSite, JSON.stringify(bind.signalSite)),
27304
28325
  b.literal(bind.kind === 'textOnlyChild'),
27305
- ...(previousValue !== undefined
27306
- ? [
27307
- bind.seededText ? b.literal(1) : undefinedNode(),
27308
- bind.bindingMarker ? b.literal(bind.bindingMarker) : undefinedNode(),
27309
- previousValue,
27310
- ]
27311
- : bind.bindingMarker
27312
- ? [bind.seededText ? b.literal(1) : undefinedNode(), b.literal(bind.bindingMarker)]
27313
- : bind.seededText
27314
- ? [b.literal(1)]
27315
- : []),
28326
+ ...optionalCallArgs(
28327
+ previousValue ?? null,
28328
+ bind.seededText && (previousValue === undefined || bind.bindingMarker)
28329
+ ? b.literal(1)
28330
+ : null,
28331
+ bind.bindingMarker ? b.literal(bind.bindingMarker) : null,
28332
+ ),
27316
28333
  ];
27317
28334
  }
27318
28335
  if (
@@ -29918,6 +30935,9 @@ function emitElementHtml(
29918
30935
  }
29919
30936
 
29920
30937
  let children = normalizeChildren(sourceChildren, childNs === 'svg', ctx, tag === 'noscript');
30938
+ // Before metadata lifting, so a `<title>` inside a textarea is rejected here
30939
+ // exactly as the server rejects it.
30940
+ const textareaParts = textareaTextParts(tag, hostNs, children, ctx);
29921
30941
  // NESTED document metadata / Float resources are zero-DOM children: lift
29922
30942
  // them to the enclosing plan's head list (mounted out-of-band by
29923
30943
  // emitHeadClient — scope-owned metadata, global resources), mirroring the
@@ -29933,8 +30953,51 @@ function emitElementHtml(
29933
30953
  }
29934
30954
  if (hasNestedHoist) children = children.filter((n) => n.type !== 'HeadHoist');
29935
30955
  }
29936
- // Special case: a single Text child (only-child text fast path).
29937
- if (
30956
+ if (textareaParts !== null) {
30957
+ // Every child folds into one text binding (see textareaTextParts). It is a
30958
+ // direct signal binding when any hole may carry a handle: the runtime then
30959
+ // returns one derived handle over the parts.
30960
+ let signalCapable = false;
30961
+ for (const part of textareaParts) {
30962
+ if (part.kind === 'static') continue;
30963
+ if (canCarryDirectSignalHandle(part.expr)) signalCapable = true;
30964
+ else if (canCarryDirectSignalHandle(part.expr, false, true)) ctx.signalBindingsUsed = true;
30965
+ if (isDirectSignalHandleExpression(part.expr)) ctx.signalBindingsEager = true;
30966
+ }
30967
+ if (signalCapable) ctx.signalBindingsUsed = true;
30968
+ const expr = inheritOriginLoc(
30969
+ b.call(
30970
+ requireRuntimeForContext(ctx, 'textareaText'),
30971
+ ...textareaTextArgs(textareaParts, (expression, text) =>
30972
+ text
30973
+ ? resolveStyleExpr(expression, cssHash)
30974
+ : tsrxExprNode(
30975
+ resolveStyleExpr(rewriteChildHoleValue(expression, ctx), cssHash),
30976
+ ctx,
30977
+ componentName,
30978
+ inlinedSubs,
30979
+ childNs,
30980
+ cssHash,
30981
+ ),
30982
+ ),
30983
+ ),
30984
+ node,
30985
+ );
30986
+ // A customized built-in's constructor may inspect children while cloned.
30987
+ const seededText = !directPropNames.has('is');
30988
+ bindings.push({
30989
+ id: bindings.length,
30990
+ kind: 'textOnlyChild',
30991
+ expr,
30992
+ path,
30993
+ seededText,
30994
+ ...(signalCapable
30995
+ ? { signalDirect: true, signalSite: directSignalSite(ctx, node, 'binding') }
30996
+ : {}),
30997
+ });
30998
+ if (seededText) appendTemplatePart(html, ' ', 'text');
30999
+ } else if (
31000
+ // Special case: a single Text child (only-child text fast path).
29938
31001
  children.length === 1 &&
29939
31002
  children[0].type === 'Text' &&
29940
31003
  !children[0]._octaneBindingText &&
@@ -32003,77 +33066,100 @@ function keyedSelectionDepIndex(itemName, keyBody, subStmts, runtimeDepNames, ct
32003
33066
  return runtimeDepNames.indexOf(selected.name);
32004
33067
  }
32005
33068
 
32006
- // The first root's legacy key already owns the entire row through keyFn.
32007
- // Avoid a second descriptor boundary for that same key. Only consume a host
32008
- // key when the row cannot change a collected key before a later child reads it.
32009
- // Opaque rendering, spreads and duplicate keys retain the existing boundary.
32010
- // Component keys and nested host keys retain their own reconciliation ranges.
33069
+ const isKeyAttribute = (attribute) => (attribute.name?.name || attribute.name) === 'key';
33070
+
33071
+ // The legacy row-key spelling: a valued `key` attribute on the first element of
33072
+ // an @for body. It takes precedence over a header key, as in @tsrx/core's React
33073
+ // target. The reconciler reads row keys before the body runs, so the key can
33074
+ // read the item, the index and names outside the loop, but nothing the body
33075
+ // declares: the hoisted key function would throw a ReferenceError at runtime or
33076
+ // silently read an outer binding of the same name.
33077
+ function forRowKeyAttribute(node, ctx) {
33078
+ const statements = node.body.body;
33079
+ const element = statements.find((n) => n.type === 'Element' || n.type === 'JSXElement');
33080
+ const attribute = (element?.attributes || element?.openingElement?.attributes || []).find(
33081
+ isKeyAttribute,
33082
+ );
33083
+ // A valueless `<li key>` or a comment-only `key={}` carries no expression and
33084
+ // falls through to the header key, the index, or the `x.id ?? x` default.
33085
+ if (attribute?.value == null) return null;
33086
+ const expression =
33087
+ attribute.value.type === 'JSXExpressionContainer'
33088
+ ? attribute.value.expression
33089
+ : attribute.value;
33090
+ if (expression.type === 'JSXEmptyExpression') return null;
33091
+ // The keyed element is the row's only output node, so every other statement
33092
+ // is setup that runs in the row function.
33093
+ const setup = statements.filter((statement) => statement !== element);
33094
+ if (setup.length === 0) return expression;
33095
+ // Analyze the setup with the key placed after it, as the body sees it. Only
33096
+ // row declarations are in scope, so a key identifier that resolves to the
33097
+ // row block, or to the row function a `var` hoists into, reads one. A
33098
+ // binding the key declares itself, such as a callback parameter, resolves to
33099
+ // a scope of its own.
33100
+ const probe = b.stmt(expression);
33101
+ const lexical = createLexicalAnalysis(b.block([...setup, probe]));
33102
+ const rowScope = lexical.nodeScopes.get(probe);
33103
+ const visit = (child, parent, key) => {
33104
+ // Array holes, as in `[, row.id]`, arrive as null entries.
33105
+ if (!child || typeof child !== 'object') return;
33106
+ if (child.type === 'Identifier' && isIdentifierReference(child, parent, key, lexical)) {
33107
+ const binding = lexical.resolveBinding(lexical.nodeScopes.get(child) ?? rowScope, child.name);
33108
+ if (binding?.scope === rowScope || binding?.scope === lexical.rootScope) {
33109
+ const l = attribute.loc && attribute.loc.start;
33110
+ const at = l
33111
+ ? ` (${ctx.mapSourceName ? ctx.mapSourceName + ':' : ''}${l.line}:${l.column})`
33112
+ : '';
33113
+ throw new Error(
33114
+ `The \`key\` attribute on this \`@for\` row reads \`${child.name}\`, which is declared ` +
33115
+ 'inside the loop body. Row keys are computed before the body runs, so they can only ' +
33116
+ 'read the item, its `index` binding, and names from outside the loop. Derive the key ' +
33117
+ `from the item in the loop header instead: \`@for (const item of items; key …)\`.${at}`,
33118
+ );
33119
+ }
33120
+ }
33121
+ forEachRuntimeAstChild(child, (grandchild, childKey) => visit(grandchild, child, childKey));
33122
+ };
33123
+ visit(expression, probe, 'expression');
33124
+ return expression;
33125
+ }
33126
+
33127
+ // A `key` on the only output root of an @for body names the row, never a
33128
+ // separate element: a valued key is the row key above, and the root cannot
33129
+ // change identity inside a row that shares it. Leaving the key on the root
33130
+ // would give it a boundary that only repeats the row key: an intrinsic root
33131
+ // lowers to a keyed descriptor instead of the native template, and a component
33132
+ // root loses the row memo and its shared single-root range. Remove it so the
33133
+ // row compiles exactly as the header spelling does. Like a header key, the
33134
+ // reconciler reads it; an input that changes while rows render takes effect at
33135
+ // the next reconcile. Keyed elements below the root keep their own boundaries.
32011
33136
  function forItemTemplateBody(node, ctx) {
32012
33137
  const body = node.body.body;
32013
- if (ctx._universalRuntimeUnit != null || body.length !== 1) return body;
32014
- const root = body[0];
32015
- if (!isPlainHostRoot(root) || isActivityLongForm(root, ctx) || isFragmentLongForm(root, ctx))
33138
+ if (ctx._universalRuntimeUnit != null) return body;
33139
+ let root = null;
33140
+ for (const statement of body) {
33141
+ if (!isJsxNode(statement)) continue;
33142
+ if (root !== null) return body;
33143
+ root = statement;
33144
+ }
33145
+ if (
33146
+ (root?.type !== 'Element' && root?.type !== 'JSXElement') ||
33147
+ (!isPlainHostRoot(root) && !isComponentTag(root)) ||
33148
+ isActivityLongForm(root, ctx) ||
33149
+ isFragmentLongForm(root, ctx)
33150
+ )
32016
33151
  return body;
32017
33152
  const attrs = root.attributes || root.openingElement?.attributes || [];
32018
- let keyIndex = -1;
32019
- for (let i = 0; i < attrs.length; i++) {
32020
- const attr = attrs[i];
32021
- if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') return body;
32022
- if (jsxAttrRawName(attr) !== 'key') continue;
32023
- if (keyIndex !== -1 || attr.value == null) return body;
32024
- keyIndex = i;
32025
- }
32026
- if (keyIndex === -1) return body;
32027
- const value = attrs[keyIndex].value;
32028
- const key = value.type === 'JSXExpressionContainer' ? value.expression : value;
32029
- if (!isDeferralSafeBundleArg(key)) return body;
32030
- // Destructuring must not invoke defaults, computed reads or an iterator.
32031
- if (!isAutoMemoPropsParam(node.left.declarations[0].id)) return body;
32032
- function stableHost(host) {
32033
- if (!isPlainHostRoot(host) || isActivityLongForm(host, ctx) || isFragmentLongForm(host, ctx))
32034
- return false;
32035
- const tag = host.id?.name ?? host.openingElement?.name?.name;
32036
- if (tag.includes('-') || tag === 'script') return false;
32037
- const attributes = host.attributes || host.openingElement?.attributes || [];
32038
- if (
32039
- attributes.some(
32040
- (attr) =>
32041
- attr.type === 'SpreadAttribute' ||
32042
- attr.type === 'JSXSpreadAttribute' ||
32043
- jsxAttrRawName(attr) === 'is' ||
32044
- jsxAttrRawName(attr) === 'children',
32045
- )
32046
- )
32047
- return false;
32048
- // Inspect values so deferred event/ref callbacks remain eligible.
32049
- const values = attributes.map((attr) => attr.value);
32050
- if (containsRenderCall(values) || containsAutoMemoUnsafeStructure(values)) return false;
32051
- for (const child of host.children || []) {
32052
- if (child.type === 'JSXText') continue;
32053
- if (child.type === 'Text' || child.type === 'JSXExpressionContainer') {
32054
- const expr = child.expression;
32055
- if (!expr || expr.type === 'JSXEmptyExpression') continue;
32056
- if (
32057
- !isKnownTextChildExpression(expr) ||
32058
- containsRenderCall([expr]) ||
32059
- containsAutoMemoUnsafeStructure([expr])
32060
- )
32061
- return false;
32062
- } else if (!stableHost(child)) return false;
32063
- }
32064
- return true;
32065
- }
32066
- if (!stableHost(root)) return body;
32067
- const kept = attrs.filter((_, index) => index !== keyIndex);
32068
- return [
32069
- root.openingElement
32070
- ? {
32071
- ...root,
32072
- ...(root.attributes === undefined ? {} : { attributes: kept }),
32073
- openingElement: { ...root.openingElement, attributes: kept },
32074
- }
32075
- : { ...root, attributes: kept },
32076
- ];
33153
+ const kept = attrs.filter((attribute) => !isKeyAttribute(attribute));
33154
+ if (kept.length === attrs.length) return body;
33155
+ const unkeyed = root.openingElement
33156
+ ? {
33157
+ ...root,
33158
+ ...(root.attributes === undefined ? {} : { attributes: kept }),
33159
+ openingElement: { ...root.openingElement, attributes: kept },
33160
+ }
33161
+ : { ...root, attributes: kept };
33162
+ return body.map((statement) => (statement === root ? unkeyed : statement));
32077
33163
  }
32078
33164
 
32079
33165
  function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
@@ -32128,26 +33214,10 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32128
33214
  return inheritOriginLoc(b.arrow(params, keyExpr), keyExpr);
32129
33215
  }
32130
33216
 
32131
- let keyFn = null;
32132
33217
  // New TSRX surfaces `key` on the JSXForExpression itself (read via `node.key`
32133
- // below). Legacy / `<li key={…}>` attribute syntax is also accepted: scan the
32134
- // body for the first Element and pull its `key=` attr if any. Accept both
32135
- // the old `Element` IR and the raw new `JSXElement` shape that's reached
32136
- // here when the body wasn't routed through normalizeChildren.
32137
- const firstEl = node.body.body.find((n) => n.type === 'Element' || n.type === 'JSXElement');
32138
- if (firstEl) {
32139
- const keyAttr = (firstEl.attributes || firstEl.openingElement?.attributes || []).find(
32140
- (a) => (a.name?.name || a.name) === 'key',
32141
- );
32142
- // A valueless `<li key>` carries no expression — skip it (mirroring
32143
- // makeCompCall's null-value handling) and fall through to the header key /
32144
- // index / `x.id ?? x` default instead of crashing on `keyAttr.value.type`.
32145
- if (keyAttr && keyAttr.value != null) {
32146
- const inner =
32147
- keyAttr.value.type === 'JSXExpressionContainer' ? keyAttr.value.expression : keyAttr.value;
32148
- keyFn = mkKeyFn(inner);
32149
- }
32150
- }
33218
+ // below). The legacy `<li key={…}>` attribute spelling takes precedence.
33219
+ const attributeKey = forRowKeyAttribute(node, ctx);
33220
+ let keyFn = attributeKey === null ? null : mkKeyFn(attributeKey);
32151
33221
  if (!keyFn && node.key) {
32152
33222
  keyFn = mkKeyFn(node.key);
32153
33223
  }
@@ -32555,10 +33625,10 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32555
33625
  // parent is laid out per child (e.g. <tbody> in js-framework-benchmark).
32556
33626
  // In addition to a direct host root, accept only narrowly-proven sole
32557
33627
  // component and host-vs-host conditional roots; all other shapes keep item
32558
- // ranges.
33628
+ // ranges. An item that can exit early renders no root when it does.
32559
33629
  let singleRoot = false;
32560
33630
  let singleRootExpr = null;
32561
- {
33631
+ if (!armMayExit(subStmts)) {
32562
33632
  const jsxChildren = subStmts.filter((s) => isJsxNode(s));
32563
33633
  if (jsxChildren.length === 1) {
32564
33634
  const c = jsxChildren[0];
@@ -32567,7 +33637,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32567
33637
  // row itself is the block-boundary host, no Comment markers needed).
32568
33638
  if (isSingleHostIfRoot(c)) {
32569
33639
  singleRoot = true;
32570
- } else if (isPlainHostRoot(c)) {
33640
+ } else if (isSingleTemplateHost(c)) {
32571
33641
  singleRoot = true;
32572
33642
  } else if ((c.type === 'Element' || c.type === 'JSXElement') && isComponentTag(c)) {
32573
33643
  // A sole component item can share the component's proven host root as
@@ -32607,7 +33677,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32607
33677
  singleRoot = false;
32608
33678
  singleRootExpr = null;
32609
33679
  }
32610
- const ssrMarkerless = isSsrMarkerlessForItem(node);
33680
+ const ssrMarkerless = isSsrMarkerlessForItem(node, subStmts);
32611
33681
  const hostRootTag = subStmts[0]?.id?.name ?? subStmts[0]?.openingElement?.name?.name;
32612
33682
  const hostMountSafe =
32613
33683
  ctx.autoMemo === true &&
@@ -32856,6 +33926,232 @@ function rewriteEarlyExits(body, allowExplicitNull = false) {
32856
33926
  return out;
32857
33927
  }
32858
33928
 
33929
+ /**
33930
+ * Split a directive arm (an @if/@else, @switch case, @try/@pending/@catch, or
33931
+ * @for/@empty body) into its guard-nested form. The arm renders its final node
33932
+ * unless it exits first, and an exit is `return;`, `return null;`, or a
33933
+ * `continue;` that targets the `@for`. Wherever the exit sits in the arm's
33934
+ * setup, it ends only that arm's output, exactly like the top-level guard
33935
+ * rewriteEarlyExits nests. An exit inside a setup statement sets a flag and
33936
+ * breaks out of that statement, and the flag becomes the guard:
33937
+ *
33938
+ * if (a) { if (b) return null; f(); } rest
33939
+ * ⇒
33940
+ * let __exit = false;
33941
+ * __exit: if (a) { if (b) { __exit = true; break __exit; } f(); }
33942
+ * if (!__exit) { rest }
33943
+ *
33944
+ * Jumps that target a loop, `switch`, or label inside the arm, and returns in
33945
+ * a nested function, stay JavaScript. Statements the split renders (directives
33946
+ * and JSX-bearing `if`/`for`/`try`) are arms themselves and lower their own
33947
+ * exits when they compile. Legacy synthetic bodies only: a real function body
33948
+ * returns through the generic value ABI.
33949
+ */
33950
+ function splitArmExits(body, ctx) {
33951
+ const tail = hookedTailStart(body);
33952
+ if (tail !== -1) return retainTailHookState(body, tail, ctx);
33953
+ let out = null;
33954
+ for (let i = 0; i < body.length; i++) {
33955
+ const stmt = body[i];
33956
+ let lowered = stmt;
33957
+ if (!isEarlyExitIf(stmt, true) && !isJsxNode(stmt)) {
33958
+ // An unconditional exit: nothing after it runs.
33959
+ if (isEarlyExitStatement(stmt, true)) return rewriteEarlyExits(out ?? body.slice(0, i), true);
33960
+ let flag = null;
33961
+ const replaced = replaceArmExits(stmt, false, false, ctx, (exit) => {
33962
+ flag ??= allocCompilerName(ctx, '__exit');
33963
+ return inheritOriginLoc(
33964
+ b.block([
33965
+ b.stmt(b.assignment('=', b.id(flag), b.literal(true, 'true'))),
33966
+ { type: 'BreakStatement', label: b.id(flag) },
33967
+ ]),
33968
+ exit,
33969
+ );
33970
+ });
33971
+ if (flag !== null) {
33972
+ lowered = [
33973
+ inheritOriginLoc(b.let(b.id(flag), b.literal(false, 'false')), stmt),
33974
+ inheritOriginLoc(b.labeled(flag, replaced), stmt),
33975
+ inheritOriginLoc(b.if(b.id(flag), b.return(), null), stmt),
33976
+ ];
33977
+ }
33978
+ }
33979
+ if (out === null && lowered !== stmt) out = body.slice(0, i);
33980
+ if (out !== null) Array.isArray(lowered) ? out.push(...lowered) : out.push(lowered);
33981
+ }
33982
+ return rewriteEarlyExits(out ?? body, true);
33983
+ }
33984
+
33985
+ /**
33986
+ * Keep the state of hooks declared after an early exit. A hook's state lives
33987
+ * as long as the component, `@for` row, or directive arm that calls it, and an
33988
+ * exit only skips the rest of one render, exactly like a plain `if` around the
33989
+ * hook, a custom hook that returns early, or a value-returning component. The
33990
+ * guard-nested form would give the tail its own arm scope, which unmounts, and
33991
+ * resets that state, whenever the exit is taken. So when a slot-keyed hook
33992
+ * follows the first exit, only the tail's output moves into the guarded arm.
33993
+ * Its setup stays in this body as ordinary guard-nested JavaScript and hands
33994
+ * the output the tail names it reads:
33995
+ *
33996
+ * if (x) return; const [s] = useState(0); <b>{s}</b>
33997
+ * ⇒
33998
+ * let __tail = false;
33999
+ * if (!x) { const [s] = useState(0); __tail = [s]; }
34000
+ * if (__tail) { const [s] = __tail; <b>{s}</b> }
34001
+ *
34002
+ * An effect the exit skips still disconnects and reconnects, like any effect
34003
+ * whose call site a completed render does not reach. `first` is the index
34004
+ * hookedTailStart found.
34005
+ */
34006
+ function retainTailHookState(body, first, ctx) {
34007
+ const setup = [];
34008
+ const output = [];
34009
+ for (let i = first; i < body.length; i++) (isJsxNode(body[i]) ? output : setup).push(body[i]);
34010
+ const declared = new Set();
34011
+ for (const statement of setup) collectStatementBindings(statement, declared);
34012
+ const read = collectFreeIdentifiers(b.block(output), []);
34013
+ const names = [...declared].filter((name) => read.has(name)).sort();
34014
+ const tail = allocCompilerName(ctx, '__tail');
34015
+ const origin = body[first];
34016
+ const handoff = names.length === 0 ? b.literal(true, 'true') : b.array(names.map((n) => b.id(n)));
34017
+ const lowered = splitArmExits(
34018
+ [
34019
+ ...body.slice(0, first),
34020
+ ...setup,
34021
+ inheritOriginLoc(b.stmt(b.assignment('=', b.id(tail), handoff)), origin),
34022
+ ],
34023
+ ctx,
34024
+ );
34025
+ const unpack =
34026
+ names.length === 0
34027
+ ? []
34028
+ : [inheritOriginLoc(b.const(b.array_pattern(names.map((n) => b.id(n))), b.id(tail)), origin)];
34029
+ return [
34030
+ inheritOriginLoc(b.let(b.id(tail), b.literal(false, 'false')), origin),
34031
+ ...lowered,
34032
+ inheritOriginLoc(b.if(b.id(tail), b.block([...unpack, ...output]), null), origin),
34033
+ ];
34034
+ }
34035
+
34036
+ /**
34037
+ * Index of the first statement that can exit the body when a slot-keyed hook
34038
+ * call follows it in setup and output follows it too, or -1. Otherwise the
34039
+ * guard-nested form already leaves every hook on the body's own scope.
34040
+ *
34041
+ * The tail's output must also follow its last exit. An authored arm or `@{}`
34042
+ * body always ends in its one output node, but a statement list with output
34043
+ * ahead of a later exit renders that output under only the exits before it:
34044
+ * the guard-nested form expresses that, and one guarded output cannot.
34045
+ */
34046
+ function hookedTailStart(body) {
34047
+ const first = body.findIndex(
34048
+ (statement) => !isJsxNode(statement) && ownsArmJump(statement, false, false),
34049
+ );
34050
+ if (first === -1) return -1;
34051
+ let output = false;
34052
+ let hook = false;
34053
+ for (let i = first; i < body.length; i++) {
34054
+ const statement = body[i];
34055
+ if (isJsxNode(statement)) {
34056
+ output = true;
34057
+ continue;
34058
+ }
34059
+ if (output && ownsArmJump(statement, false, false)) return -1;
34060
+ hook ||= containsHookCall(statement, true);
34061
+ }
34062
+ return output && hook ? first : -1;
34063
+ }
34064
+
34065
+ /**
34066
+ * Replace each exit a setup statement owns with `exit(node)`. A value return
34067
+ * and a `break` that targets the directive have no arm equivalent, so they are
34068
+ * compile errors instead of literal JavaScript that renders "null"/"undefined",
34069
+ * leaves stale DOM, or breaks out of a function.
34070
+ */
34071
+ function replaceArmExits(node, loop, breakable, ctx, exit) {
34072
+ if (node === null || typeof node !== 'object') return node;
34073
+ if (Array.isArray(node)) {
34074
+ let out = null;
34075
+ for (let i = 0; i < node.length; i++) {
34076
+ const mapped = replaceArmExits(node[i], loop, breakable, ctx, exit);
34077
+ if (out === null && mapped !== node[i]) out = node.slice(0, i);
34078
+ if (out !== null) out.push(mapped);
34079
+ }
34080
+ return out ?? node;
34081
+ }
34082
+ const type = node.type;
34083
+ if (isArmJumpBoundary(type)) return node;
34084
+ const jump = armJump(node, loop, breakable);
34085
+ if (jump === 'exit') return exit(node);
34086
+ if (jump === 'value') {
34087
+ throw armExitError(ctx, node, ARM_VALUE_RETURN_MESSAGE);
34088
+ }
34089
+ if (jump === 'break') {
34090
+ throw armExitError(ctx, node, ARM_BREAK_MESSAGE);
34091
+ }
34092
+ const innerLoop = loop || LOOP_TYPES.has(type);
34093
+ const innerBreakable = breakable || innerLoop || type === 'SwitchStatement';
34094
+ let out = null;
34095
+ for (const key in node) {
34096
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
34097
+ const child = node[key];
34098
+ if (child === null || typeof child !== 'object') continue;
34099
+ const mapped = replaceArmExits(child, innerLoop, innerBreakable, ctx, exit);
34100
+ if (mapped !== child) (out ??= { ...node })[key] = mapped;
34101
+ }
34102
+ return out ?? node;
34103
+ }
34104
+
34105
+ /**
34106
+ * Whether an arm's setup can end it before its output node, so the output is
34107
+ * conditional. Single-root and markerless proofs that pick an arm's output by
34108
+ * its JSX statements must reject such an arm: when the exit is taken there is
34109
+ * no host to serve as the boundary.
34110
+ */
34111
+ function armMayExit(statements) {
34112
+ return statements.some(
34113
+ (statement) => !isJsxNode(statement) && ownsArmJump(statement, false, false),
34114
+ );
34115
+ }
34116
+
34117
+ function ownsArmJump(node, loop, breakable) {
34118
+ if (node === null || typeof node !== 'object') return false;
34119
+ if (Array.isArray(node)) return node.some((child) => ownsArmJump(child, loop, breakable));
34120
+ const type = node.type;
34121
+ if (isArmJumpBoundary(type)) return false;
34122
+ if (armJump(node, loop, breakable) !== null) return true;
34123
+ const innerLoop = loop || LOOP_TYPES.has(type);
34124
+ const innerBreakable = breakable || innerLoop || type === 'SwitchStatement';
34125
+ for (const key in node) {
34126
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
34127
+ const child = node[key];
34128
+ if (
34129
+ child !== null &&
34130
+ typeof child === 'object' &&
34131
+ ownsArmJump(child, innerLoop, innerBreakable)
34132
+ )
34133
+ return true;
34134
+ }
34135
+ return false;
34136
+ }
34137
+
34138
+ // Nested functions own their returns, and JSX and directives own their arms.
34139
+ function isArmJumpBoundary(type) {
34140
+ return (
34141
+ typeof type !== 'string' ||
34142
+ FN_TYPES.has(type) ||
34143
+ type === 'StaticBlock' ||
34144
+ JSX_CHILDREN_BEARING_TYPES.has(type) ||
34145
+ SETUP_VALUE_DIRECTIVE_TYPES.has(type)
34146
+ );
34147
+ }
34148
+
34149
+ function armExitError(ctx, node, message) {
34150
+ const l = node.loc && node.loc.start;
34151
+ const at = l ? ` (${ctx.mapSourceName ? ctx.mapSourceName + ':' : ''}${l.line}:${l.column})` : '';
34152
+ return new Error(message + at);
34153
+ }
34154
+
32859
34155
  /**
32860
34156
  * Turn compiler-proven shorthand guards into ordinary template control flow:
32861
34157
  *
@@ -32870,6 +34166,9 @@ function rewriteEarlyExits(body, allowExplicitNull = false) {
32870
34166
  function lowerNullishComponentExits(ast) {
32871
34167
  const statements = ast.body || [];
32872
34168
  let out = null;
34169
+ // Compiler-name allocation for retainTailHookState, before the compile
34170
+ // context exists. That context later collects these names from the AST.
34171
+ let names = null;
32873
34172
  for (let i = 0; i < statements.length; i++) {
32874
34173
  const statement = statements[i];
32875
34174
  let replacement = statement;
@@ -32879,7 +34178,15 @@ function lowerNullishComponentExits(ast) {
32879
34178
  if (hasOnlyLowerableNullishExits(node)) {
32880
34179
  const body = node.body;
32881
34180
  const sequence = [...(body.body || []), ...(body.render ? [body.render] : [])];
32882
- const rewritten = rewriteEarlyExits(sequence, true);
34181
+ const tail = hookedTailStart(sequence);
34182
+ const rewritten =
34183
+ tail === -1
34184
+ ? rewriteEarlyExits(sequence, true)
34185
+ : retainTailHookState(
34186
+ sequence,
34187
+ tail,
34188
+ (names ??= { usedCompilerNames: collectIdentifierNames(ast) }),
34189
+ );
32883
34190
  const renderIndex = rewritten.findIndex(isJsxNode);
32884
34191
  if (renderIndex !== -1 && !rewritten.slice(renderIndex + 1).some(isJsxNode)) {
32885
34192
  const loweredNode = {
@@ -32963,13 +34270,16 @@ function jsxArmRoot(node) {
32963
34270
  * separate scopes and excluded). Content that moves into an if/else ARM must
32964
34271
  * not call hooks: on the value path a hook behind an early return keeps its
32965
34272
  * slot-keyed state on the COMPONENT scope across branch flips, while an arm's
32966
- * scope is torn down with the arm.
34273
+ * scope is torn down with the arm. `slotKeyed` skips `use` and `useContext`,
34274
+ * which are keyed by call order and context identity and hold no such state.
32967
34275
  */
32968
- function containsHookCall(node) {
34276
+ function containsHookCall(node, slotKeyed = false) {
32969
34277
  const seen = new WeakSet();
32970
34278
  // The shared convention (HOOK_NAME_CONVENTION_RE) — covers `use`, `useX`,
32971
34279
  // and the `unstable_`/`UNSTABLE_` prefixed forms.
32972
- const isHookName = isHookCalleeName;
34280
+ const isHookName = slotKeyed
34281
+ ? (name) => name !== 'use' && name !== 'useContext' && isHookCalleeName(name)
34282
+ : isHookCalleeName;
32973
34283
  const walk = (value) => {
32974
34284
  if (value == null || typeof value !== 'object') return false;
32975
34285
  if (Array.isArray(value)) {
@@ -33190,15 +34500,18 @@ function collectModuleLevelBindings(moduleBody) {
33190
34500
  }
33191
34501
 
33192
34502
  function collectOctaneComponentWrapperLocals(moduleBody) {
34503
+ return collectOctaneImportLocals(moduleBody, ['memo', 'lazy']);
34504
+ }
34505
+
34506
+ // Local names of the given `octane` named imports.
34507
+ function collectOctaneImportLocals(moduleBody, importedNames) {
33193
34508
  const names = new Set();
33194
34509
  for (const stmt of moduleBody) {
33195
34510
  if (stmt.type !== 'ImportDeclaration' || stmt.source?.value !== 'octane') continue;
33196
34511
  for (const spec of stmt.specifiers || []) {
33197
34512
  if (spec.type !== 'ImportSpecifier') continue;
33198
34513
  const imported = spec.imported?.name ?? spec.imported?.value;
33199
- if ((imported === 'memo' || imported === 'lazy') && spec.local?.name) {
33200
- names.add(spec.local.name);
33201
- }
34514
+ if (importedNames.includes(imported) && spec.local?.name) names.add(spec.local.name);
33202
34515
  }
33203
34516
  }
33204
34517
  return names;
@@ -33305,6 +34618,8 @@ function lowerJsxReturnBranchesOf(node, moduleBody, moduleAnalysis = null) {
33305
34618
  return null;
33306
34619
  }
33307
34620
  if (!/^[A-Z]/.test(node.id.name)) return null;
34621
+ // A component's returned-JSX twin exists only for direct calls.
34622
+ if (node._octaneDirectCallTwin === true) return null;
33308
34623
  const state = { failed: false, totalReturns: 0, branchReturn: false, jsxReturns: 0, roots: [] };
33309
34624
  const result = armifyComponentReturnList(node.body.body || [], state, false);
33310
34625
  if (result === null || state.failed) return null;
@@ -33375,6 +34690,295 @@ function lowerJsxReturnBranchComponents(ast) {
33375
34690
  return out === null ? ast : { ...ast, body: out };
33376
34691
  }
33377
34692
 
34693
+ /**
34694
+ * A `@{ … }` body is shorthand for returning JSX: `(v) => @{ …; <p /> }` means
34695
+ * `(v) => { …; return <p />; }`. A template function declared inside another
34696
+ * function, or written inline as an argument, normally compiles as a render
34697
+ * body instead, which receives its Scope as an argument and renders into it.
34698
+ * That is sound only while the runtime is the one calling it: a `{helper}`
34699
+ * child, a `<Helper />` tag, a render prop, or a portal body. A direct call
34700
+ * `helper(v)` passes no Scope, so the body reads `__s.slots` of undefined, and
34701
+ * `rows.map(helper)` passes the row index where the Scope would go.
34702
+ *
34703
+ * Lower such a function to its returned-JSX form, so a call returns a JSX value
34704
+ * exactly as the `=> <jsx>` form does on the client and the server, whenever
34705
+ * other code may call it:
34706
+ *
34707
+ * - a nested binding that code which can see it calls directly (`helper(v)`,
34708
+ * `helper.call(…)`), or passes to a call (`rows.map(helper)`, `run(helper)`);
34709
+ * - an inline function passed straight to a call or `new`, at any depth,
34710
+ * module scope included: `rows.map((row) => @{ <li /> })`.
34711
+ *
34712
+ * The runtime's own calls render the lowered value too, so this costs only the
34713
+ * template fast path. A call through a shadowing binding of the same name also
34714
+ * lowers the helper. The runtime itself renders the function arguments of
34715
+ * three calls, which keep that path: a `createPortal` body, matched by name as
34716
+ * the portal lowering matches it, and the component or render-function child
34717
+ * given to octane's `memo` or `createElement`.
34718
+ *
34719
+ * Module-level template bindings are components, which this pass leaves to
34720
+ * the component pipeline. Async and generator functions keep their bodies,
34721
+ * because their returned-JSX forms return a promise or an iterator instead of
34722
+ * JSX.
34723
+ */
34724
+ function lowerDirectlyCalledTemplateFunctions(ast) {
34725
+ // Template function expressions, found anywhere.
34726
+ const expressions = new Set();
34727
+ // Nested template function node → [binding name, the function declaring it].
34728
+ const candidates = new Map();
34729
+ walkWithEnclosingFunctions(ast.body, (node, functions) => {
34730
+ const type = node.type;
34731
+ if (type === 'ArrowFunctionExpression' || type === 'FunctionExpression') {
34732
+ if (isLowerableTemplateFunction(node)) expressions.add(node);
34733
+ return;
34734
+ }
34735
+ if (functions.length === 0) return;
34736
+ let fn = null;
34737
+ let name;
34738
+ if (type === 'FunctionDeclaration') {
34739
+ fn = node;
34740
+ name = node.id?.name;
34741
+ } else if (type === 'VariableDeclarator' && node.id?.type === 'Identifier') {
34742
+ const init = unwrapTsExpr(node.init);
34743
+ if (init?.type === 'ArrowFunctionExpression' || init?.type === 'FunctionExpression') {
34744
+ fn = init;
34745
+ name = node.id.name;
34746
+ }
34747
+ }
34748
+ if (fn !== null && name !== undefined && isLowerableTemplateFunction(fn)) {
34749
+ candidates.set(fn, [name, functions[functions.length - 1]]);
34750
+ }
34751
+ });
34752
+ if (expressions.size === 0 && candidates.size === 0) return ast;
34753
+
34754
+ let renderingCallees = null;
34755
+ // True when the runtime, not the callee, renders the call's function arguments.
34756
+ const rendersArguments = (call) => {
34757
+ const callee = unwrapTsExpr(call.callee);
34758
+ if (callee?.type !== 'Identifier') return false;
34759
+ if (callee.name === 'createPortal') return true;
34760
+ renderingCallees ??= collectOctaneImportLocals(ast.body, ['memo', 'createElement']);
34761
+ return renderingCallees.has(callee.name);
34762
+ };
34763
+ const lowered = new Set();
34764
+ const names = new Set([...candidates.values()].map(([name]) => name));
34765
+ // Function node → candidate names that code inside it calls or passes on.
34766
+ const calledIn = new Map();
34767
+ const markCalled = (functions, name) => {
34768
+ for (const fn of functions) {
34769
+ let called = calledIn.get(fn);
34770
+ if (called === undefined) calledIn.set(fn, (called = new Set()));
34771
+ called.add(name);
34772
+ }
34773
+ };
34774
+ walkWithEnclosingFunctions(ast.body, (node, functions) => {
34775
+ const type = node.type;
34776
+ if (
34777
+ type !== 'CallExpression' &&
34778
+ type !== 'OptionalCallExpression' &&
34779
+ type !== 'NewExpression'
34780
+ ) {
34781
+ return;
34782
+ }
34783
+ const name = directlyCalledName(node);
34784
+ if (name !== null && names.has(name)) markCalled(functions, name);
34785
+ // The callee calls a function passed to it, as `rows.map(fn)` does with
34786
+ // the row index where a Scope would go.
34787
+ const args = node.arguments;
34788
+ for (let i = 0; i < args.length; i++) {
34789
+ const arg = unwrapTsExpr(args[i]);
34790
+ if (expressions.has(arg)) {
34791
+ if (!rendersArguments(node)) lowered.add(arg);
34792
+ } else if (arg?.type === 'Identifier' && names.has(arg.name) && !rendersArguments(node)) {
34793
+ markCalled(functions, arg.name);
34794
+ }
34795
+ }
34796
+ });
34797
+ for (const [fn, [name, owner]] of candidates) {
34798
+ if (calledIn.get(owner)?.has(name)) lowered.add(fn);
34799
+ }
34800
+ if (lowered.size === 0) return ast;
34801
+ const lower = (node) =>
34802
+ mapAst(node, (n) => (lowered.has(n) ? returnedJsxFunction(n, lower) : null));
34803
+ return lower(ast);
34804
+ }
34805
+
34806
+ function isLowerableTemplateFunction(fn) {
34807
+ return fn.body?.type === 'JSXCodeBlock' && !fn.async && !fn.generator;
34808
+ }
34809
+
34810
+ /**
34811
+ * The name a call invokes as a function, or null. `helper(…)` and
34812
+ * `helper?.(…)` call it, and so do `helper.call(…)`, `helper.apply(…)`, and
34813
+ * `rows.map(helper)`, which passes the row index where a render body takes its
34814
+ * Scope. Optional calls are an `optional` CallExpression inside a
34815
+ * ChainExpression, or Babel's OptionalCallExpression and
34816
+ * OptionalMemberExpression.
34817
+ */
34818
+ function directlyCalledName(node) {
34819
+ if (node.type !== 'CallExpression' && node.type !== 'OptionalCallExpression') return null;
34820
+ let callee = unwrapTsExpr(node.callee);
34821
+ if (callee?.type === 'MemberExpression' || callee?.type === 'OptionalMemberExpression') {
34822
+ const method = callee.computed ? callee.property?.value : callee.property?.name;
34823
+ callee =
34824
+ method === 'call' || method === 'apply'
34825
+ ? unwrapTsExpr(callee.object)
34826
+ : method === 'map'
34827
+ ? unwrapTsExpr(node.arguments[0])
34828
+ : null;
34829
+ }
34830
+ return callee?.type === 'Identifier' ? callee.name : null;
34831
+ }
34832
+
34833
+ /**
34834
+ * A `@{ … }` function rewritten to its returned-JSX form `{ setup; return
34835
+ * <jsx>; }`. `lower` maps the setup statements and the output first.
34836
+ */
34837
+ function returnedJsxFunction(fn, lower) {
34838
+ const block = fn.body;
34839
+ const render = block.render;
34840
+ const statements = [...lower(block.body)];
34841
+ if (render != null) {
34842
+ // A directive output returns as a value-position directive.
34843
+ const value = lower(render);
34844
+ // `() => @{ <p /> }` is exactly `() => <p />`.
34845
+ if (statements.length === 0 && fn.type === 'ArrowFunctionExpression') {
34846
+ return { ...fn, body: value, expression: true };
34847
+ }
34848
+ statements.push(inheritOriginLoc(b.return(value), render));
34849
+ }
34850
+ const body = inheritOriginLoc(b.block(statements), block);
34851
+ return fn.type === 'ArrowFunctionExpression'
34852
+ ? { ...fn, body, expression: false }
34853
+ : { ...fn, body };
34854
+ }
34855
+
34856
+ /**
34857
+ * A module-level `@{ … }` function is a component: rendering it as `<F />` or
34858
+ * as a `{F}` child calls its compiled body with the Scope it renders into. It is
34859
+ * also shorthand for `function F(v) { …; return <jsx>; }`, whose direct call
34860
+ * `F(v)` returns a JSX value, and a direct call passes no Scope.
34861
+ *
34862
+ * When code in the module calls the function directly, or hands it to `.map`,
34863
+ * declare that returned-JSX form beside it as `F$direct` and mark the
34864
+ * component. Its compiled body then starts with
34865
+ * `if (!isRenderCall(__s)) return F$direct(…)`, so body calls keep the
34866
+ * compiled template while a direct call runs the returned-JSX form: its setup
34867
+ * and hooks run in the caller, and its JSX resolves where the value renders.
34868
+ * The body has already bound the parameters, so the returned-JSX form takes
34869
+ * the names they bind.
34870
+ *
34871
+ * This runs before the passes that rewrite component bodies, because the
34872
+ * returned-JSX form keeps the authored returns: `if (!v) return null;` still
34873
+ * returns null from a direct call. A call through a shadowing binding of the
34874
+ * same name also marks the component, which costs only bytes.
34875
+ */
34876
+ function splitDirectlyCalledComponents(ast) {
34877
+ const statements = ast.body || [];
34878
+ let names = null;
34879
+ for (const statement of statements) {
34880
+ const node = moduleStatementDeclaration(statement);
34881
+ if (isSplittableComponent(node)) (names ??= new Set()).add(node.id.name);
34882
+ }
34883
+ if (names === null) return ast;
34884
+ const called = new Set();
34885
+ walkWithEnclosingFunctions(statements, (node) => {
34886
+ const name = directlyCalledName(node);
34887
+ if (name !== null && names.has(name)) called.add(name);
34888
+ });
34889
+ if (called.size === 0) return ast;
34890
+ const used = collectIdentifierNames(statements);
34891
+ const out = [];
34892
+ for (const statement of statements) {
34893
+ const node = moduleStatementDeclaration(statement);
34894
+ if (!isSplittableComponent(node) || !called.has(node.id.name)) {
34895
+ out.push(statement);
34896
+ continue;
34897
+ }
34898
+ let twin = `${node.id.name}$direct`;
34899
+ while (used.has(twin)) twin += '$';
34900
+ used.add(twin);
34901
+ const bound = new Set();
34902
+ for (const param of node.params || []) {
34903
+ // A TypeScript `this` parameter declares a type, not an argument.
34904
+ if (param.type !== 'Identifier' || param.name !== 'this') collectBindings(param, bound);
34905
+ }
34906
+ const args = [...bound];
34907
+ const params = args.map((name) => inheritOriginLoc(b.id(name), node));
34908
+ // The two functions compile differently, so they must not share nodes:
34909
+ // module-wide walks skip a node they have already visited, and later
34910
+ // passes key facts by node identity.
34911
+ const returned = returnedJsxFunction(
34912
+ inheritOriginLoc(b.function_declaration(b.id(twin), params, cloneAstNode(node.body)), node),
34913
+ (value) => value,
34914
+ );
34915
+ out.push({ ...returned, _octaneDirectCallTwin: true });
34916
+ const marked = { ...node, _octaneDirectCall: { name: twin, args } };
34917
+ out.push(statement === node ? marked : { ...statement, declaration: marked });
34918
+ }
34919
+ return { ...ast, body: out };
34920
+ }
34921
+
34922
+ function moduleStatementDeclaration(statement) {
34923
+ return statement.type === 'ExportNamedDeclaration' ||
34924
+ statement.type === 'ExportDefaultDeclaration'
34925
+ ? statement.declaration
34926
+ : statement;
34927
+ }
34928
+
34929
+ function isSplittableComponent(node) {
34930
+ return (
34931
+ isComponentFunction(node) && node.id != null && node.async !== true && node.generator !== true
34932
+ );
34933
+ }
34934
+
34935
+ // `if (!isRenderCall(__s)) return F$direct(…);` for a component that
34936
+ // splitDirectlyCalledComponents marked.
34937
+ function directCallGuard(node, ctx) {
34938
+ const { name, args } = node._octaneDirectCall;
34939
+ const call = b.call(name, ...args.map((arg) => inheritOriginLoc(b.id(arg), node)));
34940
+ const test = b.unary('!', b.call(requireRuntimeForContext(ctx, 'isRenderCall'), b.id('__s')));
34941
+ return inheritOriginLoc(b.if(test, inheritOriginLoc(b.return(call), node), null), node);
34942
+ }
34943
+
34944
+ function withDirectCallGuard(fn, node, ctx) {
34945
+ if (node._octaneDirectCall === undefined) return fn;
34946
+ // The guard passes the parameters on, so it follows the leading bindings of
34947
+ // those that compiledBodyParams binds from the arguments.
34948
+ const body = fn.body.body;
34949
+ let bound = 0;
34950
+ while (body[bound]?._octaneParamBinding === true) bound++;
34951
+ return {
34952
+ ...fn,
34953
+ body: {
34954
+ ...fn.body,
34955
+ body: [...body.slice(0, bound), directCallGuard(node, ctx), ...body.slice(bound)],
34956
+ },
34957
+ };
34958
+ }
34959
+
34960
+ /** Visit every node with the functions that enclose it, outermost first. */
34961
+ function walkWithEnclosingFunctions(root, visit) {
34962
+ const functions = [];
34963
+ const walk = (node) => {
34964
+ if (node == null || typeof node !== 'object') return;
34965
+ if (Array.isArray(node)) {
34966
+ for (const child of node) walk(child);
34967
+ return;
34968
+ }
34969
+ visit(node, functions);
34970
+ const isFunction = isFunctionNode(node);
34971
+ if (isFunction) functions.push(node);
34972
+ for (const key in node) {
34973
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
34974
+ const child = node[key];
34975
+ if (child !== null && typeof child === 'object') walk(child);
34976
+ }
34977
+ if (isFunction) functions.pop();
34978
+ };
34979
+ walk(root);
34980
+ }
34981
+
33378
34982
  function isJsxNode(node) {
33379
34983
  if (!node) return false;
33380
34984
  if (isWrappedJsxDirective(node)) return true;
@@ -33384,12 +34988,16 @@ function isJsxNode(node) {
33384
34988
  if (node.type === 'JSXElement' || node.type === 'JSXFragment') return true;
33385
34989
  // New TSRX directive nodes — always JSX-position. normalizeChildren will
33386
34990
  // lower them to IfStatement / ForOfStatement / TryStatement / SwitchStatement
33387
- // when planJsx runs over them.
34991
+ // when planJsx runs over them. A child `@{ … }` block in a statement list is
34992
+ // a directive arm's output node, never setup: normalizeChildren makes a
34993
+ // render-only block transparent and gives a setup-bearing one its own scope,
34994
+ // exactly as it does for a block among element children.
33388
34995
  if (
33389
34996
  node.type === 'JSXIfExpression' ||
33390
34997
  node.type === 'JSXForExpression' ||
33391
34998
  node.type === 'JSXTryExpression' ||
33392
34999
  node.type === 'JSXSwitchExpression' ||
35000
+ node.type === 'JSXCodeBlock' ||
33393
35001
  node.type === 'JSXExpressionContainer' ||
33394
35002
  node.type === 'JSXText' ||
33395
35003
  node.type === 'JSXStyleElement'
@@ -33420,6 +35028,18 @@ function isWrappedJsxDirective(node) {
33420
35028
  );
33421
35029
  }
33422
35030
 
35031
+ // `@for` item bodies and `@switch` cases wrap a child `@{ … }` block written as
35032
+ // their output in an ExpressionStatement. The parser admits a block only as a
35033
+ // body's final statement, so unwrap just that one. A block inside plain JS
35034
+ // control flow in the body's setup is wrapped the same way and stays a
35035
+ // JavaScript expression, exactly like an element written there.
35036
+ function unwrapOutputCodeBlock(statements) {
35037
+ const last = statements[statements.length - 1];
35038
+ return last?.type === 'ExpressionStatement' && last.expression?.type === 'JSXCodeBlock'
35039
+ ? [...statements.slice(0, -1), last.expression]
35040
+ : statements;
35041
+ }
35042
+
33423
35043
  function bodyContainsJsx(node) {
33424
35044
  if (!node) return false;
33425
35045
  if (node.type === 'BlockStatement') return node.body.some(isJsxNode);
@@ -33487,10 +35107,13 @@ const esrapCommentOptions = {
33487
35107
  * literal raws are re-derived centrally before the print.
33488
35108
  */
33489
35109
  function printNodeWithMap(node, ctx) {
33490
- const printable = stripTsOnlyWrappers(escapeMultilineStringLiterals(node), {
35110
+ let printable = stripTsOnlyWrappers(escapeMultilineStringLiterals(node), {
33491
35111
  filename: ctx.mapSourceName,
33492
35112
  enums: null,
33493
35113
  });
35114
+ if (ctx.deferredReads === true) {
35115
+ printable = snapshotDeferredReads(printable, (name) => allocCompilerName(ctx, `${name}$`));
35116
+ }
33494
35117
  if (assertPrintedLocs()) assertNodeLocs(printable);
33495
35118
  const { code, map } = esrapPrint(printable, withDeferredImports(esrapTsx(esrapCommentOptions)), {
33496
35119
  sourceMapSource: ctx.mapSourceName,