octane 0.2.2 → 0.2.4

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 (59) hide show
  1. package/dist/cjs/compatibility.cjs +35 -0
  2. package/dist/cjs/component-flags.cjs +8 -1
  3. package/dist/cjs/css.cjs +9 -0
  4. package/dist/cjs/dom-tables.cjs +8 -1
  5. package/dist/cjs/error-codes.client.generated.cjs +15 -2
  6. package/dist/cjs/error-codes.server.generated.cjs +5 -0
  7. package/dist/cjs/event-names.cjs +39 -0
  8. package/dist/cjs/index.cjs +13 -0
  9. package/dist/cjs/runtime.cjs +1345 -359
  10. package/dist/cjs/runtime.server.cjs +620 -183
  11. package/dist/cjs/server/index.cjs +26 -0
  12. package/dist/cjs/version.cjs +1 -1
  13. package/dist/compatibility.d.ts +7 -0
  14. package/dist/compatibility.js +10 -0
  15. package/dist/compiler/bundler.js +20 -30
  16. package/dist/compiler/compile-renderer-boundaries.js +1 -0
  17. package/dist/compiler/compile-universal.js +3 -0
  18. package/dist/compiler/compile.js +877 -627
  19. package/dist/compiler/hook-deps.js +100 -3
  20. package/dist/compiler/hook-names.js +1 -0
  21. package/dist/compiler/hydrate-boundaries.js +165 -15
  22. package/dist/compiler/manual-hooks.js +171 -0
  23. package/dist/compiler/parser.node.js +34 -8
  24. package/dist/compiler/plain-hook-memo.js +54 -5
  25. package/dist/compiler/slot-hooks.d.ts +43 -0
  26. package/dist/compiler/slot-hooks.js +154 -35
  27. package/dist/compiler/strong-mode.js +836 -3
  28. package/dist/compiler/style-scopes.js +1134 -0
  29. package/dist/compiler/volar.LICENSES.txt +1 -1
  30. package/dist/compiler/volar.js +4543 -2839
  31. package/dist/component-flags.d.ts +1 -1
  32. package/dist/component-flags.js +8 -1
  33. package/dist/css.d.ts +7 -0
  34. package/dist/css.js +8 -0
  35. package/dist/dom-tables.js +8 -1
  36. package/dist/error-codes.client.generated.d.ts +3 -1
  37. package/dist/error-codes.client.generated.js +15 -2
  38. package/dist/error-codes.server.generated.d.ts +1 -0
  39. package/dist/error-codes.server.generated.js +5 -0
  40. package/dist/event-names.d.ts +1 -0
  41. package/dist/event-names.js +15 -0
  42. package/dist/html-tree-validation.d.ts +17 -34
  43. package/dist/index.d.ts +3 -1
  44. package/dist/index.js +12 -1
  45. package/dist/jsx-runtime.d.ts +6 -1
  46. package/dist/public-types.d.ts +188 -0
  47. package/dist/public-types.js +0 -0
  48. package/dist/runtime.d.ts +128 -69
  49. package/dist/runtime.js +1349 -360
  50. package/dist/runtime.server.d.ts +67 -15
  51. package/dist/runtime.server.js +619 -184
  52. package/dist/server/index.d.ts +4 -2
  53. package/dist/server/index.js +23 -0
  54. package/dist/static/index.d.ts +2 -1
  55. package/dist/static/index.js +3 -1
  56. package/dist/universal-core.d.ts +8 -1
  57. package/dist/universal-core.js +95 -9
  58. package/dist/version.js +1 -1
  59. package/package.json +4 -3
@@ -39,6 +39,8 @@ import {
39
39
  strongHash,
40
40
  } from '@tsrx/core';
41
41
  import { parseModule } from '#octane/compiler-parser';
42
+ import { createStyleScopePass } from './style-scopes.js';
43
+ import { decodeHTMLStrict } from 'entities';
42
44
  import { print as esrapPrint } from 'esrap';
43
45
  import esrapTsx from 'esrap/languages/tsx';
44
46
  import { buildFatSegments } from './fat-segments.js';
@@ -106,7 +108,13 @@ import {
106
108
  cssStyleValue,
107
109
  hyphenateStyleName,
108
110
  } from '../dom-tables.js';
111
+ import { isDelegatedEventProp } from '../event-names.js';
109
112
  import { sanitizeURLAttribute } from '../sanitize-url.js';
113
+ import {
114
+ invalidHtmlNestingWithAncestor,
115
+ invalidHtmlNestingWithParent,
116
+ invalidHtmlTextNesting,
117
+ } from '../html-tree-validation.js';
110
118
 
111
119
  // --- Parser-AST immutability enforcement ------------------------------------
112
120
  //
@@ -233,6 +241,10 @@ function inheritOriginLoc(root, origin) {
233
241
  if (origin == null || origin.loc == null) return root;
234
242
  const { start, end, loc } = origin;
235
243
  mapAst(root, (node) => {
244
+ // A parsed `<style>` body is a CSS AST whose positions are relative to the
245
+ // CSS text, never to the module: stamping module positions onto it would
246
+ // corrupt the scoped-CSS render (and it is parser-owned, so shared).
247
+ if (node.type === 'StyleSheet') return node;
236
248
  // Only real AST nodes are stamped — mapAst also visits typeless data
237
249
  // objects (TemplateElement.value, Literal.regex, embedded config), which
238
250
  // must stay untouched (and may be frozen).
@@ -310,6 +322,13 @@ function markSynthesizedAttr(attr) {
310
322
  return attr;
311
323
  }
312
324
 
325
+ /** A compiler-added scope-hash class, not an authored `class`/`className`. */
326
+ function isSynthesizedClassAttr(attr) {
327
+ const raw = jsxAttrRawName(attr);
328
+ if (raw !== 'class' && raw !== 'className') return false;
329
+ return attr.name?.metadata?.tsrx_synthesized_origin === true;
330
+ }
331
+
313
332
  // Curated exact origins (`inspect: true`). A generated token whose authored
314
333
  // counterpart the compiler KNOWS — the `$$click` slot key of an `onClick`, the
315
334
  // block helper an `@if`/`@for` lowers to — but which the module map alone
@@ -622,7 +641,7 @@ const CONTROLLED_KIND_HELPERS = {
622
641
  // recorder (recordBakedAttrOrigins) derives the name/value offsets from that
623
642
  // shape arithmetically — keep the invariant when adding forms.
624
643
  function bakeStaticAttr(attrName, lv, tag, namespace = 'html') {
625
- if (lv == null) return '';
644
+ if (lv == null || attrName === 'innerText' || attrName === 'textContent') return '';
626
645
  const isCustom =
627
646
  (namespace === 'html' || namespace === 'opaque') && tag !== undefined && tag.includes('-');
628
647
  const lower = attrName.toLowerCase();
@@ -663,6 +682,8 @@ function bakeStaticAttr(attrName, lv, tag, namespace = 'html') {
663
682
  return lv ? ` ${lower}=""` : '';
664
683
  }
665
684
  if (typeof lv === 'string') {
685
+ if (!isCustom && (lower === 'rowspan' || lower === 'start') && Number.isNaN(Number(lv)))
686
+ return '';
666
687
  if (!isCustom && POSITIVE_NUMERIC_ATTR_PROPS.has(lower) && !(Number(lv) >= 1)) return '';
667
688
  if (
668
689
  !isCustom &&
@@ -1565,12 +1586,13 @@ function inheritSoleCompRoot(bodyNodes, ctx) {
1565
1586
  // Collect the LOCAL names bound to the octane boundary builtins (see
1566
1587
  // inheritSoleCompRoot) from a module's import declarations. Aliased imports
1567
1588
  // (`import { Suspense as S }`) are matched by their local binding.
1568
- function collectOctaneBoundaryNames(astBody) {
1589
+ function collectOctaneBoundaryNames(astBody, onlySuspense = false) {
1569
1590
  const names = new Set();
1570
1591
  for (const node of astBody) {
1571
1592
  if (node.type !== 'ImportDeclaration' || node.source.value !== 'octane') continue;
1572
1593
  for (const sp of node.specifiers || []) {
1573
1594
  const imported = sp.imported?.name;
1595
+ if (onlySuspense && imported !== 'Suspense') continue;
1574
1596
  if (
1575
1597
  (imported === 'Suspense' ||
1576
1598
  imported === 'ErrorBoundary' ||
@@ -4713,6 +4735,225 @@ function containsRenderCall(stmts, memoCtx = null) {
4713
4735
  return found;
4714
4736
  }
4715
4737
 
4738
+ // Built-in hooks whose render-time work cannot schedule an update for the
4739
+ // calling component. Their function arguments run later (commit, event, or
4740
+ // never), so those bodies are deferred like any other closure.
4741
+ const SETUP_PASSIVE_HOOKS = new Set([
4742
+ 'useCallback',
4743
+ 'useRef',
4744
+ 'useEffect',
4745
+ 'useLayoutEffect',
4746
+ 'useInsertionEffect',
4747
+ 'useImperativeHandle',
4748
+ 'useEffectEvent',
4749
+ 'useId',
4750
+ 'useDeferredValue',
4751
+ 'useTransition',
4752
+ 'useFormStatus',
4753
+ 'useActionState',
4754
+ 'useFormState',
4755
+ 'useContext',
4756
+ 'use',
4757
+ ]);
4758
+ // Built-in hooks that invoke a function argument synchronously during render
4759
+ // (a memo factory, a lazy initializer, a snapshot reader, an optimistic
4760
+ // reducer, a linked-state reconciler). Those bodies belong to setup.
4761
+ const SETUP_SYNC_FACTORY_HOOKS = new Set([
4762
+ 'useMemo',
4763
+ 'useState',
4764
+ 'useLinkedState',
4765
+ 'useReducer',
4766
+ 'useSyncExternalStore',
4767
+ 'useOptimistic',
4768
+ ]);
4769
+ // Standard globals that only project their arguments. A user-defined
4770
+ // `toString`/`valueOf`/`toJSON` that schedules a render violates the hook rules
4771
+ // in React and Octane alike, so these count as call-free setup.
4772
+ const SETUP_PURE_GLOBAL_CALLEES = new Set([
4773
+ 'String',
4774
+ 'Number',
4775
+ 'Boolean',
4776
+ 'BigInt',
4777
+ 'Symbol',
4778
+ 'parseInt',
4779
+ 'parseFloat',
4780
+ 'isNaN',
4781
+ 'isFinite',
4782
+ 'encodeURI',
4783
+ 'encodeURIComponent',
4784
+ 'decodeURI',
4785
+ 'decodeURIComponent',
4786
+ ]);
4787
+ // Namespace members that never invoke an argument. `Array.from`,
4788
+ // `Object.groupBy`, `Object.fromEntries`, and friends iterate or call user
4789
+ // code, so they stay unclassified. JSON's replacer and reviver positions are
4790
+ // checked like a hook factory (see walkInvokedArgument).
4791
+ const SETUP_PURE_NAMESPACE_MEMBERS = new Map([
4792
+ ['Math', null],
4793
+ [
4794
+ 'Number',
4795
+ new Set(['isFinite', 'isInteger', 'isNaN', 'isSafeInteger', 'parseFloat', 'parseInt']),
4796
+ ],
4797
+ ['Object', new Set(['entries', 'freeze', 'hasOwn', 'is', 'isFrozen', 'keys', 'values'])],
4798
+ ['Array', new Set(['isArray', 'of'])],
4799
+ ['Date', new Set(['now', 'parse', 'UTC'])],
4800
+ ['JSON', new Set(['parse', 'stringify'])],
4801
+ ]);
4802
+
4803
+ // Every binding name the module declares anywhere. A global namespace shadowed
4804
+ // by any local, parameter, or import is treated as an unknown callee.
4805
+ function collectModuleBoundNames(ast) {
4806
+ const names = new Set();
4807
+ const seen = new WeakSet();
4808
+ function walk(n) {
4809
+ if (!n || typeof n !== 'object') return;
4810
+ if (Array.isArray(n)) {
4811
+ for (const x of n) walk(x);
4812
+ return;
4813
+ }
4814
+ if (seen.has(n)) return;
4815
+ seen.add(n);
4816
+ switch (n.type) {
4817
+ case 'VariableDeclarator':
4818
+ collectBindings(n.id, names);
4819
+ break;
4820
+ case 'FunctionDeclaration':
4821
+ case 'FunctionExpression':
4822
+ case 'ArrowFunctionExpression':
4823
+ if (n.id) names.add(n.id.name);
4824
+ for (const param of n.params || []) collectBindings(param, names);
4825
+ break;
4826
+ case 'ClassDeclaration':
4827
+ case 'ClassExpression':
4828
+ if (n.id) names.add(n.id.name);
4829
+ break;
4830
+ case 'CatchClause':
4831
+ collectBindings(n.param, names);
4832
+ break;
4833
+ case 'ImportSpecifier':
4834
+ case 'ImportDefaultSpecifier':
4835
+ case 'ImportNamespaceSpecifier':
4836
+ if (n.local) names.add(n.local.name);
4837
+ break;
4838
+ }
4839
+ for (const key in n) {
4840
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4841
+ walk(n[key]);
4842
+ }
4843
+ }
4844
+ walk(ast);
4845
+ return names;
4846
+ }
4847
+
4848
+ /**
4849
+ * Can this setup schedule a render-phase update for its own component? A
4850
+ * setter, dispatcher, transition start, custom hook, or any other call the
4851
+ * compiler cannot classify may; the built-in hooks above and standard pure
4852
+ * globals cannot. Closures are deferred unless a built-in hook runs them
4853
+ * during render. This admits the setup checkpoint, an optimization over the
4854
+ * runtime's own child-initialization guards, so an unclassified callee fails
4855
+ * closed to emitting the checkpoint.
4856
+ */
4857
+ function setupCanScheduleSelfUpdate(stmts, ctx) {
4858
+ let found = false;
4859
+ const seen = new WeakSet();
4860
+ const bound = ctx._moduleBoundNames;
4861
+ const isGlobal = (name) => bound !== undefined && !bound.has(name);
4862
+ function walk(n, deep) {
4863
+ if (found || !n) return;
4864
+ if (Array.isArray(n)) {
4865
+ for (const x of n) walk(x, deep);
4866
+ return;
4867
+ }
4868
+ if (typeof n !== 'object') return;
4869
+ const t = n.type;
4870
+ if (!t) return;
4871
+ if (seen.has(n)) return;
4872
+ seen.add(n);
4873
+ if (
4874
+ !deep &&
4875
+ (t === 'ArrowFunctionExpression' || t === 'FunctionExpression' || t === 'FunctionDeclaration')
4876
+ ) {
4877
+ return;
4878
+ }
4879
+ if (t === 'CallExpression' || t === 'OptionalCallExpression') {
4880
+ const hook = n._octaneImportedHook;
4881
+ if (hook !== undefined && SETUP_PASSIVE_HOOKS.has(hook)) {
4882
+ walk(n.arguments, deep);
4883
+ return;
4884
+ }
4885
+ if (hook !== undefined && SETUP_SYNC_FACTORY_HOOKS.has(hook)) {
4886
+ for (const argument of n.arguments) walkInvokedArgument(argument);
4887
+ return;
4888
+ }
4889
+ const callee = n.callee;
4890
+ if (
4891
+ callee?.type === 'Identifier' &&
4892
+ n._octaneUnboundCallee === true &&
4893
+ SETUP_PURE_GLOBAL_CALLEES.has(callee.name)
4894
+ ) {
4895
+ walk(n.arguments, true);
4896
+ return;
4897
+ }
4898
+ if (
4899
+ callee?.type === 'MemberExpression' &&
4900
+ !callee.computed &&
4901
+ callee.object?.type === 'Identifier' &&
4902
+ callee.property?.type === 'Identifier' &&
4903
+ SETUP_PURE_NAMESPACE_MEMBERS.has(callee.object.name) &&
4904
+ isGlobal(callee.object.name)
4905
+ ) {
4906
+ const members = SETUP_PURE_NAMESPACE_MEMBERS.get(callee.object.name);
4907
+ if (members === null || members.has(callee.property.name)) {
4908
+ if (callee.object.name === 'JSON') {
4909
+ walk(n.arguments[0], true);
4910
+ for (let index = 1; index < n.arguments.length; index++)
4911
+ walkInvokedArgument(n.arguments[index]);
4912
+ } else walk(n.arguments, true);
4913
+ return;
4914
+ }
4915
+ }
4916
+ found = true;
4917
+ return;
4918
+ }
4919
+ if (t === 'NewExpression' || t === 'TaggedTemplateExpression') {
4920
+ found = true;
4921
+ return;
4922
+ }
4923
+ for (const key in n) {
4924
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4925
+ walk(n[key], deep);
4926
+ }
4927
+ }
4928
+ // A sync-factory hook may invoke any argument whose value can be a function.
4929
+ // Only shapes that visibly cannot be callable, or whose bodies are visible
4930
+ // here, keep the analysis open; a named factory, member, call result, or
4931
+ // conditional fails closed exactly like an unclassified call.
4932
+ function walkInvokedArgument(argument) {
4933
+ if (found || !argument) return;
4934
+ switch (argument.type) {
4935
+ case 'ArrowFunctionExpression':
4936
+ case 'FunctionExpression':
4937
+ case 'Literal':
4938
+ case 'TemplateLiteral':
4939
+ case 'BinaryExpression':
4940
+ case 'UnaryExpression':
4941
+ case 'UpdateExpression':
4942
+ walk(argument, true);
4943
+ return;
4944
+ case 'ArrayExpression':
4945
+ // Dependency and initial-value arrays are never invoked; their
4946
+ // elements are ordinary setup expressions.
4947
+ walk(argument.elements, true);
4948
+ return;
4949
+ default:
4950
+ found = true;
4951
+ }
4952
+ }
4953
+ for (const s of stmts) walk(s, false);
4954
+ return found;
4955
+ }
4956
+
4716
4957
  // A hook call is identified by naming convention — the same signal React and
4717
4958
  // React Compiler key on — plus the exact `unstable_` and `UNSTABLE_` staging
4718
4959
  // prefixes bindings expose (`unstable_useRouterState` in @octanejs/remix-router
@@ -6485,9 +6726,21 @@ function mapCallbackCapturesLexicalReceiver(root) {
6485
6726
  return walk(root);
6486
6727
  }
6487
6728
 
6488
- // Recognise `{style (expr)}` — TSRX parses it as a plain
6489
- // `CallExpression(style, [expr])` (the dedicated `Style` intrinsic node is
6490
- // gone); resolveStyleExpr rewrites it into a scoped class-string expression.
6729
+ // Scoped `<style>` blocks, `$class`, and `apply` (RFC tsrx-org/RFCs#1): one
6730
+ // copy-on-write pre-pass per top-level statement, see ./style-scopes.js.
6731
+ const { applyStyleScopes } = createStyleScopePass({
6732
+ inheritOriginLoc,
6733
+ markSynthesized,
6734
+ markSynthesizedAttr,
6735
+ headResourceKind,
6736
+ isCompositeJsxTag,
6737
+ isStyleCall,
6738
+ });
6739
+
6740
+ // Recognise `style(expr)` — a plain `CallExpression(style, [expr])`, Octane's
6741
+ // class-string expression. The style-scope pre-pass (./style-scopes.js)
6742
+ // resolves it where TSRX reads a class value (attribute values and template
6743
+ // child holes, the `style` attribute excepted); anywhere else it is a user call.
6491
6744
  function isStyleCall(node) {
6492
6745
  return (
6493
6746
  node &&
@@ -6499,26 +6752,14 @@ function isStyleCall(node) {
6499
6752
  );
6500
6753
  }
6501
6754
 
6502
- // Resolve a `{style (expr)}` CallExpression (see isStyleCall) into a plain
6503
- // expression that yields a class string, with the component's scoped css hash
6504
- // prepended (so `{style ('row')}` in a component with hash "tsrx-abc" produces
6505
- // "tsrx-abc row"). Literal values inline; dynamic values become a runtime
6506
- // string concat. Components without a <style> block (no hash) — and any other
6507
- // expression shape — pass through untouched.
6508
- function resolveStyleExpr(node, cssHash) {
6509
- if (!node) return node;
6510
- if (!isStyleCall(node) || !cssHash) return node;
6511
- const inner = node.arguments[0];
6512
- if (inner.type === 'Literal' && typeof inner.value === 'string') {
6513
- const combined = inner.value ? `${cssHash} ${inner.value}` : cssHash;
6514
- // The resolved class string maps to the authored `{style (…)}` call.
6515
- return b.literal(combined, JSON.stringify(combined), node);
6516
- }
6517
- // Dynamic: emit `(<hash> + ' ' + (expr))` so absent/null produces "<hash> ".
6518
- return inheritOriginLoc(
6519
- b.binary('+', b.literal(cssHash + ' ', JSON.stringify(cssHash + ' ')), inner),
6520
- node,
6521
- );
6755
+ // The emitters' former `style(expr)` expansion. The pre-pass resolves every
6756
+ // class-string position before the emitters run, and a `style(...)` still on
6757
+ // the AST here is a user call in another position (a `style={style(p)}` value,
6758
+ // a child hole's call argument), which must print as authored — so this is a
6759
+ // pass-through kept at its call sites; `cssHash` is only the "this component
6760
+ // owns scoped CSS" flag they thread along.
6761
+ function resolveStyleExpr(node, _cssHash) {
6762
+ return node;
6522
6763
  }
6523
6764
 
6524
6765
  /**
@@ -6544,6 +6785,11 @@ function isComponentFunction(node) {
6544
6785
  // hooks slotted (via withSlot) and its returned JSX lowered to a descriptor, and
6545
6786
  // whatever USES it (`<Foo/>` / `{expr}`) is what renders it. (Async/generator
6546
6787
  // excluded; a `use*` custom hook that returns JSX is still a hook, not this.)
6788
+ //
6789
+ // Owned returns inside `if`/`else`/`switch`/`try`/loops count: a function whose
6790
+ // only JSX exits are nested still needs the return-JSX path so scoped `<style>`
6791
+ // (hash stamp, injectStyle, `ref` class-map writes) runs. Nested function
6792
+ // bodies are a different component/value boundary and are ignored.
6547
6793
  function isReturnJsxFunction(node) {
6548
6794
  if (!node) return false;
6549
6795
  if (node.type !== 'FunctionDeclaration' && node.type !== 'FunctionExpression') return false;
@@ -6553,9 +6799,58 @@ function isReturnJsxFunction(node) {
6553
6799
  if (node.id == null) return false;
6554
6800
  if (node.async || node.generator) return false;
6555
6801
  if (!node.body || node.body.type !== 'BlockStatement') return false;
6556
- return (node.body.body || []).some(
6557
- (s) => s.type === 'ReturnStatement' && s.argument && isJsxNode(s.argument),
6558
- );
6802
+ const statements = node.body.body || [];
6803
+ for (let i = 0; i < statements.length; i++) {
6804
+ if (statementHasOwnedJsxReturn(statements[i])) return true;
6805
+ }
6806
+ return false;
6807
+ }
6808
+
6809
+ /** @param {any} stmt @returns {boolean} */
6810
+ function statementHasOwnedJsxReturn(stmt) {
6811
+ if (!stmt) return false;
6812
+ if (stmt.type === 'ReturnStatement') return !!(stmt.argument && isJsxNode(stmt.argument));
6813
+ if (stmt.type === 'BlockStatement') {
6814
+ const list = stmt.body || [];
6815
+ for (let i = 0; i < list.length; i++) {
6816
+ if (statementHasOwnedJsxReturn(list[i])) return true;
6817
+ }
6818
+ return false;
6819
+ }
6820
+ if (stmt.type === 'IfStatement') {
6821
+ return (
6822
+ statementHasOwnedJsxReturn(stmt.consequent) || statementHasOwnedJsxReturn(stmt.alternate)
6823
+ );
6824
+ }
6825
+ if (stmt.type === 'SwitchStatement') {
6826
+ const cases = stmt.cases || [];
6827
+ for (let i = 0; i < cases.length; i++) {
6828
+ const consequent = cases[i].consequent || [];
6829
+ for (let j = 0; j < consequent.length; j++) {
6830
+ if (statementHasOwnedJsxReturn(consequent[j])) return true;
6831
+ }
6832
+ }
6833
+ return false;
6834
+ }
6835
+ if (stmt.type === 'TryStatement') {
6836
+ return (
6837
+ statementHasOwnedJsxReturn(stmt.block) ||
6838
+ (stmt.handler ? statementHasOwnedJsxReturn(stmt.handler.body) : false) ||
6839
+ statementHasOwnedJsxReturn(stmt.finalizer)
6840
+ );
6841
+ }
6842
+ if (
6843
+ stmt.type === 'ForStatement' ||
6844
+ stmt.type === 'ForInStatement' ||
6845
+ stmt.type === 'ForOfStatement' ||
6846
+ stmt.type === 'WhileStatement' ||
6847
+ stmt.type === 'DoWhileStatement' ||
6848
+ stmt.type === 'LabeledStatement' ||
6849
+ stmt.type === 'WithStatement'
6850
+ ) {
6851
+ return statementHasOwnedJsxReturn(stmt.body);
6852
+ }
6853
+ return false;
6559
6854
  }
6560
6855
 
6561
6856
  /** True when `node` is one plain lowercase host element (never a component/fragment). */
@@ -8333,7 +8628,9 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8333
8628
  throw new Error(`Unknown compile mode "${mode}" — expected 'client' or 'server'.`);
8334
8629
  }
8335
8630
  const cleanFilename = cleanCompileFilename(filename);
8336
- const analyzedAst = parseModule(source, cleanFilename);
8631
+ const analyzedAst = markParserSensitiveHosts(
8632
+ normalizeAuthoredJsxLiterals(parseModule(source, cleanFilename)),
8633
+ );
8337
8634
  analyzeTsrx(analyzedAst, cleanFilename);
8338
8635
  adoptParserAst(analyzedAst);
8339
8636
  assertNativeReadDiagnostics(analyzedAst, source, cleanFilename, options);
@@ -9023,8 +9320,12 @@ function compileInternal(
9023
9320
  // that are ever assigned (`foo = bar`) are excluded: their identity is then
9024
9321
  // as mutable as a module `let`, which fails closed.
9025
9322
  ctx.moduleFunctionDeclarations = collectImmutableModuleFunctions(ast.body);
9323
+ // Global namespaces are only trusted by the setup checkpoint analysis when
9324
+ // no module binding shadows them (see setupCanScheduleSelfUpdate).
9325
+ ctx._moduleBoundNames = collectModuleBoundNames(ast);
9026
9326
  // M3 inherit-range exclusion set (see inheritSoleCompRoot).
9027
9327
  ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
9328
+ ctx._octaneSuspenseNames = collectOctaneBoundaryNames(ast.body, true);
9028
9329
  const productionEffects = !ctx.hmr && !ctx.dev && !ctx.profile;
9029
9330
  const exportedComponents = ast.body.filter(
9030
9331
  (statement) =>
@@ -9579,16 +9880,11 @@ function compileInternal(
9579
9880
  // use, custom helpers, etc.) — merged into the single prelude import.
9580
9881
  addUserImportSpecifiers(ctx, node);
9581
9882
  } else {
9582
- // Style maps: rewrite `const x = <style>…</style>` before printing — the
9583
- // initialiser becomes an ObjectExpression with hashed class names, and
9584
- // the stylesheet flows through the regular cssInjections pipeline.
9585
- node = applyStyleMap(node, ctx);
9586
- // Also handle `export const x = <style>…</style>` (declaration wrapped
9587
- // in an ExportNamedDeclaration).
9588
- if (node.type === 'ExportNamedDeclaration' && node.declaration) {
9589
- const declaration = applyStyleMap(node.declaration, ctx);
9590
- if (declaration !== node.declaration) node = { ...node, declaration };
9591
- }
9883
+ // Style blocks anywhere in a non-component statement: assigned blocks
9884
+ // (`const x = <style>…</style>`, exported or nested in a helper) become
9885
+ // class-map objects and their sheets flow through cssInjections; scoped
9886
+ // blocks inside plain functions' templates get their scope hashes.
9887
+ node = applyStyleScopes(node, ctx).node;
9592
9888
  // HOOKS EVERYWHERE: a plain function (a custom hook, a helper) can hold
9593
9889
  // octane hooks too — slot them the same way components do, so their base
9594
9890
  // hooks get a per-call-site symbol (and custom-hook calls inside get one to
@@ -10143,6 +10439,7 @@ function compileServer(
10143
10439
  // M3 inherit-range exclusion set — must match the client compile's
10144
10440
  // (see inheritSoleCompRoot; both modes read the same import declarations).
10145
10441
  ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
10442
+ ctx._octaneSuspenseNames = collectOctaneBoundaryNames(ast.body, true);
10146
10443
 
10147
10444
  // Style maps are module values and can be referenced by a component declared
10148
10445
  // before the map itself. Lower every top-level map before compiling any
@@ -10156,11 +10453,18 @@ function compileServer(
10156
10453
  let newBody = null;
10157
10454
  const statements = ast.body;
10158
10455
  for (let i = 0; i < statements.length; i++) {
10159
- let node = applyStyleMap(statements[i], ctx);
10160
- if (node.type === 'ExportNamedDeclaration' && node.declaration) {
10161
- const declaration = applyStyleMap(node.declaration, ctx);
10162
- if (declaration !== node.declaration) node = { ...node, declaration };
10163
- }
10456
+ // Components lower their own scopes inside compileServerComponent so
10457
+ // only the CSS they own is emitted per render; every other statement's
10458
+ // blocks are module-level.
10459
+ const statement = statements[i];
10460
+ const declaration =
10461
+ statement.type === 'ExportDefaultDeclaration' || statement.type === 'ExportNamedDeclaration'
10462
+ ? statement.declaration
10463
+ : statement;
10464
+ const node =
10465
+ isComponentFunction(declaration) || isReturnJsxFunction(declaration)
10466
+ ? statement
10467
+ : applyStyleScopes(statement, ctx).node;
10164
10468
  if (newBody === null && node !== statements[i]) newBody = statements.slice(0, i);
10165
10469
  if (newBody !== null) newBody.push(node);
10166
10470
  }
@@ -10240,7 +10544,8 @@ function compileServer(
10240
10544
  },
10241
10545
  });
10242
10546
  } else {
10243
- bodyNodes.push(rewriteModuleJsxValues(node, ctx));
10547
+ const fnName = node.id?.name || node.declaration?.id?.name || 'module';
10548
+ bodyNodes.push(rewriteModuleJsxValues(rewriteHookCalls(node, ctx, fnName), ctx));
10244
10549
  }
10245
10550
  }
10246
10551
 
@@ -10319,11 +10624,11 @@ function compileServerComponent(node, ctx) {
10319
10624
  const isExported = !!(node.export || node.default);
10320
10625
  const isDefault = !!node.default;
10321
10626
 
10322
- // Scoped <style>: applyCssScoping stamps hash classes + registers cssInjections.
10627
+ // Scoped <style>: applyStyleScopes stamps scope classes + registers cssInjections.
10323
10628
  // Capture this component's entries to emit injectStyle INSIDE the body (so the
10324
10629
  // active server render collects CSS only for components it actually renders).
10325
10630
  const beforeCss = ctx.cssInjections.length;
10326
- const scoping = applyCssScoping(node, ctx);
10631
+ const scoping = applyStyleScopes(node, ctx);
10327
10632
  node = scoping.node;
10328
10633
  const cssHash = scoping.cssHash;
10329
10634
  const cssEntries = [...ctx.moduleCssInjections, ...ctx.cssInjections.slice(beforeCss)].sort(
@@ -10351,6 +10656,12 @@ function compileServerComponent(node, ctx) {
10351
10656
  cssEntries,
10352
10657
  'opaque',
10353
10658
  node.body?.type === 'JSXCodeBlock',
10659
+ null,
10660
+ false,
10661
+ false,
10662
+ // Imported themes this component applies are touched at the top of
10663
+ // its body (serverThemeTouchStatements) so their CSS precedes its own.
10664
+ scoping.runtimeApplied,
10354
10665
  );
10355
10666
  } finally {
10356
10667
  ctx.currentComponentLocals = prevLocals;
@@ -10366,7 +10677,10 @@ function compileServerComponent(node, ctx) {
10366
10677
  const warmStatement =
10367
10678
  warmNode === null
10368
10679
  ? null
10369
- : inheritOriginLoc(b.stmt(b.assignment('=', b.member(b.id(name), '__warm'), warmNode)), node);
10680
+ : inheritOriginLoc(
10681
+ b.stmt(b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), warmNode)),
10682
+ node,
10683
+ );
10370
10684
 
10371
10685
  // Mirror the client emission's hoisting rule (see compileComponent): a
10372
10686
  // component referenced ABOVE its declaration keeps real function-declaration
@@ -10382,7 +10696,7 @@ function compileServerComponent(node, ctx) {
10382
10696
  b.logical(
10383
10697
  '&&',
10384
10698
  b.binary('===', b.unary('typeof', b.id(name)), b.literal('function', "'function'")),
10385
- b.assignment('=', b.member(b.id(name), '__warm'), warmNode),
10699
+ b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), warmNode),
10386
10700
  ),
10387
10701
  ),
10388
10702
  node,
@@ -10432,6 +10746,8 @@ function ssrCompileBody(
10432
10746
  componentNs = null,
10433
10747
  returnedFragmentTemplate = false,
10434
10748
  returnedFragmentRoot = false,
10749
+ themeTouches = [],
10750
+ unboxedHtml = false,
10435
10751
  ) {
10436
10752
  const prevMapTemps = ctx.currentMapTemps;
10437
10753
  ctx.currentMapTemps = [];
@@ -10447,6 +10763,8 @@ function ssrCompileBody(
10447
10763
  componentNs,
10448
10764
  returnedFragmentTemplate,
10449
10765
  returnedFragmentRoot,
10766
+ themeTouches,
10767
+ unboxedHtml,
10450
10768
  );
10451
10769
  } finally {
10452
10770
  ctx.currentMapTemps = prevMapTemps;
@@ -10464,6 +10782,10 @@ function ssrCompileBodyWithMapTemps(
10464
10782
  componentNs,
10465
10783
  returnedFragmentTemplate,
10466
10784
  returnedFragmentRoot,
10785
+ // Imported themes the component's scopes apply — only the component body
10786
+ // itself passes them; nested branch/loop helpers (ssrCompileSub) pass none.
10787
+ themeTouches,
10788
+ unboxedHtml,
10467
10789
  ) {
10468
10790
  const returnedOutput = node.body?.type === 'JSXCodeBlock' && hasOwnValueReturn(node);
10469
10791
  const mapTemps = ctx.currentMapTemps;
@@ -10617,6 +10939,7 @@ function ssrCompileBodyWithMapTemps(
10617
10939
  ctx._valueDirectiveLowering = prevValueDirectiveLowering;
10618
10940
 
10619
10941
  const body = [];
10942
+ body.push(...serverThemeTouchStatements(themeTouches ?? [], ctx, node));
10620
10943
  if (cssEntries && cssEntries.length) {
10621
10944
  ctx.runtimeNeeded.add('injectStyle');
10622
10945
  for (const entry of cssEntries) {
@@ -10685,7 +11008,12 @@ function ssrCompileBodyWithMapTemps(
10685
11008
  node.params.length > 0
10686
11009
  ? [...node.params, b.id('__s'), b.id('__extra')]
10687
11010
  : [b.id('__props'), b.id('__s'), b.id('__extra')];
10688
- body.push(b.return(htmlExpr));
11011
+ // Private loop items feed only HTML concatenation, so their serialized tail
11012
+ // needs no carrier. Other bodies can cross a component/value boundary, where
11013
+ // the carrier distinguishes compiled HTML from authored text returns. This
11014
+ // flag belongs to this body only; nested callbacks keep their own contract.
11015
+ if (!unboxedHtml) ctx.runtimeNeeded.add('ssrHtml');
11016
+ body.push(b.return(unboxedHtml ? htmlExpr : ssrCall('ssrHtml', [htmlExpr], node)));
10689
11017
  const origin =
10690
11018
  node.loc != null
10691
11019
  ? node
@@ -10836,6 +11164,23 @@ function ssrCall(helper, args, origin) {
10836
11164
  );
10837
11165
  }
10838
11166
 
11167
+ /**
11168
+ * `_$touchStyleMap(theme)` for every imported theme a server component's
11169
+ * scopes apply, emitted before the component's own `injectStyle` calls so the
11170
+ * theme's CSS (a lazily injecting `_$styleMap`) lands first in the request
11171
+ * collector and the applying scope's rules win the cascade.
11172
+ */
11173
+ function serverThemeTouchStatements(runtimeApplied, ctx, origin) {
11174
+ if (!runtimeApplied || runtimeApplied.length === 0) return [];
11175
+ ctx.runtimeNeeded.add('touchStyleMap');
11176
+ return runtimeApplied.map((expression) =>
11177
+ inheritOriginLoc(
11178
+ b.stmt(ssrCall('touchStyleMap', [cloneAstNode(expression)], expression)),
11179
+ origin,
11180
+ ),
11181
+ );
11182
+ }
11183
+
10839
11184
  function ssrThunk(expression, origin) {
10840
11185
  return inheritOriginLoc(b.arrow([], expression), origin);
10841
11186
  }
@@ -10849,17 +11194,17 @@ function ssrSourcePair(present, value, origin) {
10849
11194
  }
10850
11195
 
10851
11196
  /**
10852
- * One `[spread, name, value]` source row for a grouped attribute helper. The
10853
- * name literal is the only token in the group that names THIS attribute, so it
10854
- * is what the authored name is claimed against (see registerAttrLoweringOrigin);
10855
- * the helper itself stays anchored on the element it serializes.
11197
+ * One `[spread, name, value, merge?]` source row for a grouped attribute helper.
11198
+ * The name literal is the only token in the group that names THIS attribute, so
11199
+ * it is what the authored name is claimed against (see registerAttrLoweringOrigin);
11200
+ * the helper itself stays anchored on the element it serializes. A trailing
11201
+ * `true` marks a synthesized scope class that must merge with a spread's class.
10856
11202
  */
10857
- function ssrDirectSource(ctx, name, value, origin) {
11203
+ function ssrDirectSource(ctx, name, value, origin, merge) {
10858
11204
  registerAttrLoweringOrigin(ctx, origin?.name, null, name);
10859
- return inheritOriginLoc(
10860
- b.array([b.literal(false, 'false'), b.literal(name, JSON.stringify(name)), value]),
10861
- origin,
10862
- );
11205
+ const cells = [b.literal(false, 'false'), b.literal(name, JSON.stringify(name)), value];
11206
+ if (merge) cells.push(b.literal(true, 'true'));
11207
+ return inheritOriginLoc(b.array(cells), origin);
10863
11208
  }
10864
11209
 
10865
11210
  /**
@@ -11022,14 +11367,15 @@ function ssrEmitNode(
11022
11367
  ctx.runtimeNeeded.add(fn);
11023
11368
  return ssrCall(fn, [resolveStyleExpr(rewriteHookCalls(expr, ctx, name), cssHash)], node);
11024
11369
  }
11025
- ctx.runtimeNeeded.add('ssrChild');
11026
11370
  // rewriteJsxValues lowers any JSX embedded in the expression (e.g.
11027
11371
  // `{cond && <div/>}`, a ternary, a `.map(x => <Row/>)`) to printable
11028
11372
  // createElement(...) descriptors — exactly like ssrEmitTsrxExpression and
11029
11373
  // the client makeChildCall. Without it the raw JSX leaks into the emitted
11030
11374
  // ssrChild(...) call as unparseable source.
11375
+ const childHelper = nlGuard ? 'ssrChildPre' : 'ssrChild';
11376
+ ctx.runtimeNeeded.add(childHelper);
11031
11377
  const childExpr = ssrCall(
11032
- 'ssrChild',
11378
+ childHelper,
11033
11379
  [
11034
11380
  resolveStyleExpr(rewriteJsxValues(rewriteHookCalls(expr, ctx, name), ctx), cssHash),
11035
11381
  b.id('__s'),
@@ -11617,7 +11963,15 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
11617
11963
  );
11618
11964
  attrExpr = tsrxExprNode(attrInner, ctx, name, inlinedSubs);
11619
11965
  }
11620
- attrSources.push(ssrDirectSource(ctx, rawAttrName, bindAttributeEvaluation(attrExpr), attr));
11966
+ attrSources.push(
11967
+ ssrDirectSource(
11968
+ ctx,
11969
+ rawAttrName,
11970
+ bindAttributeEvaluation(attrExpr),
11971
+ attr,
11972
+ isSynthesizedClassAttr(attr),
11973
+ ),
11974
+ );
11621
11975
  continue;
11622
11976
  }
11623
11977
 
@@ -11901,9 +12255,11 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
11901
12255
  staticTextLiteral(onlyChild0.expression) === null &&
11902
12256
  !isKnownStringChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
11903
12257
  ) {
11904
- ctx.runtimeNeeded.add('ssrChildText');
12258
+ const childHelper =
12259
+ tag === 'pre' || tag === 'textarea' || tag === 'listing' ? 'ssrChildTextPre' : 'ssrChildText';
12260
+ ctx.runtimeNeeded.add(childHelper);
11905
12261
  childrenExpr = ssrCall(
11906
- 'ssrChildText',
12262
+ childHelper,
11907
12263
  [
11908
12264
  resolveStyleExpr(
11909
12265
  rewriteJsxValues(rewriteHookCalls(onlyChild0.expression, ctx, name), ctx),
@@ -12202,8 +12558,7 @@ function ssrEmitComponent(node, ctx, name, inlinedSubs, parentNs, cssHash, compo
12202
12558
  const children = rewriteOpaqueTitles(sourceChildren, ctx, 'opaque');
12203
12559
  const opaqueChildren = !activityDescriptor && !isFragmentLongForm(node, ctx);
12204
12560
  const descriptorChildren =
12205
- (tagBindingName(node) !== null &&
12206
- ctx.descriptorChildrenBindings?.has(tagBindingName(node))) ||
12561
+ isDescriptorChildrenTag(node, ctx) ||
12207
12562
  ctx._tsxValuePos ||
12208
12563
  (returnedFragmentTemplate && !requiresTemplateNormalization(node, parentNs, true, ctx));
12209
12564
  if (descriptorChildren) {
@@ -12394,6 +12749,7 @@ function ssrCompileSub(
12394
12749
  componentNs,
12395
12750
  returnedFragmentTemplate = false,
12396
12751
  returnedFragmentRoot = false,
12752
+ unboxedHtml = false,
12397
12753
  ) {
12398
12754
  const fnName = `${baseName}$${ctx.nextHelperId++}`;
12399
12755
  const synth = { params: paramNodes || [], body: bodyStmts };
@@ -12422,6 +12778,9 @@ function ssrCompileSub(
12422
12778
  componentNs,
12423
12779
  returnedFragmentTemplate,
12424
12780
  returnedFragmentRoot,
12781
+ // Nested helpers touch no themes; the component body already did.
12782
+ [],
12783
+ unboxedHtml,
12425
12784
  );
12426
12785
  } finally {
12427
12786
  ctx._ssrFoldedExprHoles = prevFoldedExprHoles;
@@ -12584,6 +12943,9 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
12584
12943
  cssHash,
12585
12944
  parentNs,
12586
12945
  componentNs,
12946
+ false,
12947
+ false,
12948
+ true,
12587
12949
  );
12588
12950
  inlinedSubs.push(itemSub.fn);
12589
12951
  let emptyCall = ssrHtmlTemplate([], node, ctx);
@@ -13048,147 +13410,6 @@ function hasModuleStyleMaps(body) {
13048
13410
  });
13049
13411
  }
13050
13412
 
13051
- // Copy-on-write: returns the (possibly rebuilt) statement; the input is never
13052
- // modified. The core analyze/prepare pipeline mutates the sheet it is given,
13053
- // so it runs over a clone of the (bounded) StyleSheet subtree.
13054
- function applyStyleMap(stmt, ctx) {
13055
- if (stmt.type !== 'VariableDeclaration') return stmt;
13056
- let newDeclarations = null;
13057
- const declarations = stmt.declarations;
13058
- for (let i = 0; i < declarations.length; i++) {
13059
- const decl = declarations[i];
13060
- let next = decl;
13061
- const styleNode = decl.init && decl.init.type === 'JSXStyleElement' ? decl.init : null;
13062
- const parsedSheet = styleNode
13063
- ? (styleNode.children || []).find((c) => c && c.type === 'StyleSheet')
13064
- : null;
13065
- const hash = parsedSheet
13066
- ? styleNode.metadata?.styleScopeHash || parsedSheet.hash || null
13067
- : null;
13068
- if (parsedSheet && hash) {
13069
- const sheet = cloneAstNode(parsedSheet);
13070
- // `analyzeCss` marks `:global(...)` selectors (is_global / is_global_block
13071
- // metadata) so the renderer leaves them UNSCOPED. Without this pass
13072
- // `:global(a)` would be scoped to `.<hash>a`. Mirrors tsrx-ripple, which
13073
- // runs analyzeCss(stylesheet) before prepareStylesheetForRender.
13074
- analyzeCss(sheet);
13075
- prepareStylesheetForRender(sheet, true);
13076
- const css = renderStylesheets([sheet]);
13077
- ctx.cssInjections.push({
13078
- hash,
13079
- css,
13080
- order: styleNode.start ?? stmt.start ?? 0,
13081
- // The authored `<style>` element. `injectStyle` is emitted at module
13082
- // scope, so without this the whole block maps to the module origin
13083
- // and neither the tag nor its CSS is reachable from the output.
13084
- origin: styleNode,
13085
- });
13086
- ctx.runtimeNeeded.add('injectStyle');
13087
- // Replace the JSXStyleElement init with the class-map ObjectExpression
13088
- // (built loc-less by the core helper — it maps to the authored <style>).
13089
- next = {
13090
- ...decl,
13091
- init: inheritOriginLoc(createStyleClassMapFromStylesheet(sheet), styleNode),
13092
- };
13093
- }
13094
- if (newDeclarations === null && next !== decl) newDeclarations = declarations.slice(0, i);
13095
- if (newDeclarations !== null) newDeclarations.push(next);
13096
- }
13097
- return newDeclarations === null ? stmt : { ...stmt, declarations: newDeclarations };
13098
- }
13099
-
13100
- // Wrap every DYNAMIC `class` / `className` expression in a `normalizeClass(...)` call
13101
- // BEFORE the scoped-CSS hash is appended. `@tsrx/core`'s annotate_with_hash bakes the
13102
- // hash onto a dynamic class via a template literal — `` `${expr} <hash>` `` — which would
13103
- // stringify an array/object clsx value the wrong way (`['a','b']` → "a,b", `{}` →
13104
- // "[object Object]"). Normalizing first makes the interpolated slot a plain string, so
13105
- // the hash concat stays correct AND clsx composition works in scoped components. (It also
13106
- // turns a bare `class={undefined}` in a scoped component from "undefined <hash>" into just
13107
- // "<hash>".) Unscoped components need no wrap — the runtime `setClassName` / `ssrAttr`
13108
- // normalize the raw value directly. String literals are left alone so they keep folding
13109
- // into the static template. Stops at nested component function boundaries (their class
13110
- // exprs belong to a different scope), mirroring annotate_with_hash's own traversal.
13111
- // Copy-on-write: rebuilt spines only where a wrap lands; untouched subtrees
13112
- // are returned by reference and the input node is never written to. Callers
13113
- // must use the return value.
13114
- function wrapScopedClassExprs(node, ctx) {
13115
- if (!node || typeof node !== 'object') return node;
13116
- if (Array.isArray(node)) {
13117
- let out = null;
13118
- for (let i = 0; i < node.length; i++) {
13119
- const mapped = wrapScopedClassExprs(node[i], ctx);
13120
- if (out === null && mapped !== node[i]) out = node.slice(0, i);
13121
- if (out !== null) out.push(mapped);
13122
- }
13123
- return out ?? node;
13124
- }
13125
- if (
13126
- (node.type === 'FunctionDeclaration' ||
13127
- node.type === 'FunctionExpression' ||
13128
- node.type === 'ArrowFunctionExpression') &&
13129
- node.metadata?.tsrx_dynamic_wrapper !== true
13130
- ) {
13131
- return node;
13132
- }
13133
- let working = node;
13134
- if (node.type === 'JSXElement') {
13135
- const attrs = node.openingElement?.attributes;
13136
- if (Array.isArray(attrs)) {
13137
- let newAttrs = null;
13138
- for (let i = 0; i < attrs.length; i++) {
13139
- const attr = attrs[i];
13140
- let next = attr;
13141
- if (
13142
- attr?.type === 'JSXAttribute' &&
13143
- attr.name?.type === 'JSXIdentifier' &&
13144
- (attr.name.name === 'class' || attr.name.name === 'className') &&
13145
- attr.value?.type === 'JSXExpressionContainer'
13146
- ) {
13147
- const expr = attr.value.expression;
13148
- // Skip string literals (fold statically) and `{style (…)}` calls
13149
- // (resolveStyleExpr owns those). Everything else is a runtime value.
13150
- if (
13151
- expr &&
13152
- !(expr.type === 'Literal' && typeof expr.value === 'string') &&
13153
- !isStyleCall(expr)
13154
- ) {
13155
- next = {
13156
- ...attr,
13157
- value: {
13158
- ...attr.value,
13159
- // The wrapper maps to the class expression it normalizes.
13160
- expression: inheritOriginLoc(b.call('_$normalizeClass', expr), expr),
13161
- },
13162
- };
13163
- ctx.runtimeNeeded.add('normalizeClass');
13164
- }
13165
- }
13166
- if (newAttrs === null && next !== attr) newAttrs = attrs.slice(0, i);
13167
- if (newAttrs !== null) newAttrs.push(next);
13168
- }
13169
- if (newAttrs !== null) {
13170
- working = { ...node, openingElement: { ...node.openingElement, attributes: newAttrs } };
13171
- if ('attributes' in node) working.attributes = newAttrs;
13172
- }
13173
- }
13174
- }
13175
- // Generic descent AFTER the element-level wrap (mirrors the historical pass
13176
- // order): nested JSX inside attribute values and children is wrapped too.
13177
- let out = working === node ? null : working;
13178
- for (const key of Object.keys(working)) {
13179
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'parent') continue;
13180
- if (key === 'metadata' || key === 'css') continue;
13181
- const v = working[key];
13182
- if (!v || typeof v !== 'object') continue;
13183
- const mapped = wrapScopedClassExprs(v, ctx);
13184
- if (mapped !== v) {
13185
- if (out === null) out = { ...node };
13186
- out[key] = mapped;
13187
- }
13188
- }
13189
- return out ?? working;
13190
- }
13191
-
13192
13413
  // Local copy-on-write mirror of @tsrx/core's annotate_with_hash (scoping.js):
13193
13414
  // stamps the scoped-CSS hash class onto native (and dynamic-tag) JSX elements
13194
13415
  // and drops JSXStyleElement nodes from the rendered tree. The core helper
@@ -13203,301 +13424,6 @@ function isCompositeJsxTag(node) {
13203
13424
  return name.type === 'JSXMemberExpression';
13204
13425
  }
13205
13426
 
13206
- function addHashClassToElement(element, hash, classAttrName) {
13207
- const openingElement = element.openingElement;
13208
- const attrs = openingElement?.attributes || [];
13209
- const index = attrs.findIndex(
13210
- (attr) =>
13211
- attr?.type === 'JSXAttribute' &&
13212
- attr.name?.type === 'JSXIdentifier' &&
13213
- (attr.name.name === 'class' || attr.name.name === 'className'),
13214
- );
13215
- let newAttrs;
13216
- if (index === -1) {
13217
- // A synthesized class attribute maps to the element's opening tag — the
13218
- // loc keeps generated positions inside the element for source maps, but
13219
- // there is no authored `class` here, so it is marked NOT AUTHORED. Left
13220
- // unmarked, inspection reports the whole opening tag as the origin of the
13221
- // scoped class, and a hover on any attribute in that tag (`onClick`, …)
13222
- // resolves to the class the compiler added.
13223
- newAttrs = [
13224
- ...attrs,
13225
- markSynthesizedAttr(
13226
- inheritOriginLoc(
13227
- b.jsx_attribute(b.jsx_id(classAttrName), b.literal(hash, JSON.stringify(hash))),
13228
- openingElement,
13229
- ),
13230
- ),
13231
- ];
13232
- } else {
13233
- const existing = attrs[index];
13234
- const value = existing.value;
13235
- let newAttr;
13236
- if (!value) {
13237
- // The NAME is authored (`class`), the value is not.
13238
- newAttr = {
13239
- ...existing,
13240
- value: markSynthesized(inheritOriginLoc(b.literal(hash, JSON.stringify(hash)), existing)),
13241
- };
13242
- } else if (value.type === 'Literal' && typeof value.value === 'string') {
13243
- const merged = `${value.value} ${hash}`;
13244
- newAttr = { ...existing, value: { ...value, value: merged, raw: JSON.stringify(merged) } };
13245
- } else {
13246
- const expression = value.type === 'JSXExpressionContainer' ? value.expression : value;
13247
- // The template-literal bake maps to the authored class value it wraps.
13248
- newAttr = {
13249
- ...existing,
13250
- value: inheritOriginLoc(
13251
- b.jsx_expression_container(
13252
- b.template([b.quasi('', false), b.quasi(` ${hash}`, true)], [expression]),
13253
- ),
13254
- value,
13255
- ),
13256
- };
13257
- }
13258
- newAttrs = attrs.slice();
13259
- newAttrs[index] = newAttr;
13260
- }
13261
- // The core helper mirrors the attribute list onto `element.attributes` in
13262
- // every branch; keep that alias in sync on the rebuilt copy.
13263
- return {
13264
- ...element,
13265
- openingElement: { ...openingElement, attributes: newAttrs },
13266
- attributes: newAttrs,
13267
- };
13268
- }
13269
-
13270
- function annotateRootWithHash(node, hash, classAttrName) {
13271
- if (!node || typeof node !== 'object') return node;
13272
- if (
13273
- (node.type === 'FunctionDeclaration' ||
13274
- node.type === 'FunctionExpression' ||
13275
- node.type === 'ArrowFunctionExpression') &&
13276
- node.metadata?.tsrx_dynamic_wrapper !== true
13277
- ) {
13278
- return node;
13279
- }
13280
- if (node.type === 'JSXElement') {
13281
- let working = node;
13282
- if (!isCompositeJsxTag(node) || node.metadata?.dynamicElement) {
13283
- working = addHashClassToElement(node, hash, classAttrName);
13284
- }
13285
- const children = working.children;
13286
- if (Array.isArray(children)) {
13287
- let newChildren = null;
13288
- for (let i = 0; i < children.length; i++) {
13289
- const mapped = annotateRootWithHash(children[i], hash, classAttrName);
13290
- // Dropped style elements are filtered out (core: .filter(Boolean)).
13291
- if (newChildren === null && (mapped !== children[i] || !mapped)) {
13292
- newChildren = children.slice(0, i);
13293
- }
13294
- if (newChildren !== null && mapped) newChildren.push(mapped);
13295
- }
13296
- if (newChildren !== null) {
13297
- if (working === node) working = { ...node, children: newChildren };
13298
- else working.children = newChildren;
13299
- }
13300
- }
13301
- return working;
13302
- }
13303
- // Scoped-CSS styles leave the render tree here; Float style resources stay —
13304
- // the HeadHoist partition (normalizeChildren) owns removing them from body DOM.
13305
- if (node.type === 'JSXStyleElement') return headResourceKind(node) === 'style' ? node : null;
13306
- let out = null;
13307
- for (const key of Object.keys(node)) {
13308
- if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
13309
- continue;
13310
- }
13311
- const value = node[key];
13312
- if (!value || typeof value !== 'object') continue;
13313
- let mapped;
13314
- if (Array.isArray(value)) {
13315
- // Core maps generic arrays WITHOUT filtering — a dropped style element
13316
- // leaves a null slot the downstream child normalization skips.
13317
- let newArray = null;
13318
- for (let i = 0; i < value.length; i++) {
13319
- const item = annotateRootWithHash(value[i], hash, classAttrName);
13320
- if (newArray === null && item !== value[i]) newArray = value.slice(0, i);
13321
- if (newArray !== null) newArray.push(item);
13322
- }
13323
- mapped = newArray ?? value;
13324
- } else {
13325
- mapped = annotateRootWithHash(value, hash, classAttrName);
13326
- }
13327
- if (mapped !== value) {
13328
- if (out === null) out = { ...node };
13329
- out[key] = mapped;
13330
- }
13331
- }
13332
- return out ?? node;
13333
- }
13334
-
13335
- /**
13336
- * Find the JSX render roots owned by a component. `@{}` components expose one
13337
- * `JSXCodeBlock.render`; React-style functions can return JSX from any block in
13338
- * their own body. Nested functions are separate component/value boundaries and
13339
- * must not donate styles to their enclosing component.
13340
- */
13341
- function componentStyleRoots(componentNode) {
13342
- const body = componentNode.body;
13343
- if (!body) return [];
13344
- if (body.type === 'JSXCodeBlock') {
13345
- return body.render ? [{ kind: 'render', node: body.render }] : [];
13346
- }
13347
- if (body.type !== 'BlockStatement') return [];
13348
-
13349
- const roots = [];
13350
- function visit(node) {
13351
- if (!node || typeof node !== 'object') return;
13352
- if (Array.isArray(node)) {
13353
- for (const child of node) visit(child);
13354
- return;
13355
- }
13356
- if (
13357
- node.type === 'FunctionDeclaration' ||
13358
- node.type === 'FunctionExpression' ||
13359
- node.type === 'ArrowFunctionExpression'
13360
- ) {
13361
- return;
13362
- }
13363
- if (node.type === 'ReturnStatement') {
13364
- if (node.argument && isJsxNode(node.argument)) {
13365
- roots.push({ kind: 'return', statement: node, node: node.argument });
13366
- }
13367
- return;
13368
- }
13369
- for (const key of Object.keys(node)) {
13370
- if (
13371
- key === 'loc' ||
13372
- key === 'start' ||
13373
- key === 'end' ||
13374
- key === 'parent' ||
13375
- key === 'metadata' ||
13376
- key === 'css'
13377
- )
13378
- continue;
13379
- visit(node[key]);
13380
- }
13381
- }
13382
- visit(body);
13383
- return roots;
13384
- }
13385
-
13386
- /**
13387
- * Walk a component's owned render roots for `JSXStyleElement` nodes. For each
13388
- * one found:
13389
- * - Pull the pre-parsed `StyleSheet` AST out of its children.
13390
- * - Run `prepareStylesheetForRender` (rewrites `.foo` → `.foo.<hash>`) over a
13391
- * clone of the sheet — the core pipeline mutates what it is given.
13392
- * - Collect into a list rendered via `renderStylesheets` to a CSS string.
13393
- * - Register `{hash, css}` on `ctx.cssInjections` so a module-level
13394
- * `injectStyle(hash, css)` is emitted in the prelude.
13395
- * - Rebuild the render roots via `annotateRootWithHash` (COW) to stamp the
13396
- * hash class on every native JSX element AND remove the JSXStyleElement
13397
- * nodes from the rendered tree (they don't contribute DOM in the new model).
13398
- *
13399
- * Returns `{ cssHash, node }` — the hash (or `null` when no `<style>` blocks
13400
- * are present) and the possibly-rebuilt component node the caller must use.
13401
- *
13402
- * The first `JSXStyleElement` contributes the canonical hash for the whole
13403
- * component. @tsrx/core hashes individual style tags by source position, so
13404
- * multiple blocks are explicitly rebased onto that canonical component hash
13405
- * before selector rendering. Every rendered root receives the same hash class.
13406
- */
13407
- function applyCssScoping(componentNode, ctx) {
13408
- const roots = componentStyleRoots(componentNode);
13409
- if (roots.length === 0) return { cssHash: null, node: componentNode };
13410
- let cssHash = null;
13411
- const styles = [];
13412
- function collect(node) {
13413
- if (!node || typeof node !== 'object') return;
13414
- if (Array.isArray(node)) {
13415
- for (const i of node) collect(i);
13416
- return;
13417
- }
13418
- if (
13419
- node.type === 'FunctionDeclaration' ||
13420
- node.type === 'FunctionExpression' ||
13421
- node.type === 'ArrowFunctionExpression'
13422
- ) {
13423
- return;
13424
- }
13425
- if (node.type === 'JSXStyleElement') {
13426
- // A Float style resource ships its plain CSS by href identity — it
13427
- // neither contributes to the component's scope hash nor gets scoped.
13428
- if (headResourceKind(node) === 'style') return;
13429
- const sheet = (node.children || []).find((c) => c && c.type === 'StyleSheet');
13430
- if (sheet) {
13431
- styles.push({ node, sheet });
13432
- if (!cssHash) cssHash = node.metadata?.styleScopeHash || sheet.hash || null;
13433
- }
13434
- return;
13435
- }
13436
- for (const key of Object.keys(node)) {
13437
- if (
13438
- key === 'loc' ||
13439
- key === 'start' ||
13440
- key === 'end' ||
13441
- key === 'parent' ||
13442
- key === 'metadata' ||
13443
- key === 'css'
13444
- )
13445
- continue;
13446
- const v = node[key];
13447
- if (v && typeof v === 'object') collect(v);
13448
- }
13449
- }
13450
- for (const root of roots) collect(root.node);
13451
- if (!cssHash || styles.length === 0) return { cssHash: null, node: componentNode };
13452
- // A component has one scope even when its CSS is split for readability.
13453
- // Rebase before analyze/render: selector and keyframe rewriting read
13454
- // `sheet.hash`, while DOM annotation below uses `cssHash`. The core
13455
- // analyze/prepare pipeline mutates the sheet it is given, so it runs over a
13456
- // clone of each (bounded) StyleSheet subtree — the parser-owned sheet nodes
13457
- // stay pristine.
13458
- const preparedSheets = styles.map((style) => {
13459
- const sheet = cloneAstNode(style.sheet);
13460
- sheet.hash = cssHash;
13461
- // Mark `:global(...)` selectors before scoping so they render unscoped.
13462
- analyzeCss(sheet);
13463
- prepareStylesheetForRender(sheet);
13464
- return sheet;
13465
- });
13466
- const css = renderStylesheets(preparedSheets);
13467
- ctx.cssInjections.push({
13468
- hash: cssHash,
13469
- css,
13470
- order: styles[0]?.sheet.start ?? styles[0]?.node.start ?? componentNode.start ?? 0,
13471
- // The authored `<style>` element(s) this stylesheet came from.
13472
- // `injectStyle` is emitted at module scope, so without an origin the
13473
- // block maps to the module and neither the tag nor its CSS is reachable.
13474
- origins: styles.map((style) => style.node).filter(Boolean),
13475
- });
13476
- ctx.runtimeNeeded.add('injectStyle');
13477
- // Rebuild every owned render root copy-on-write: add the canonical hash
13478
- // class to native elements and strip JSXStyleElement nodes from DOM output.
13479
- // The rebuilt roots are grafted back through a spine rebuild of the
13480
- // component node; the received component tree is never modified.
13481
- let node = componentNode;
13482
- const returnReplacements = new Map();
13483
- for (const root of roots) {
13484
- // Normalize dynamic class exprs BEFORE the hash is appended (see helper), so
13485
- // clsx array/object values compose correctly alongside the scope hash.
13486
- const wrapped = wrapScopedClassExprs(root.node, ctx);
13487
- const annotated = annotateRootWithHash(wrapped, cssHash, 'class');
13488
- if (annotated === root.node) continue;
13489
- if (root.kind === 'render') {
13490
- node = { ...node, body: { ...node.body, render: annotated } };
13491
- } else {
13492
- returnReplacements.set(root.statement, { ...root.statement, argument: annotated });
13493
- }
13494
- }
13495
- if (returnReplacements.size > 0) {
13496
- node = mapAst(node, (n) => returnReplacements.get(n) ?? null);
13497
- }
13498
- return { cssHash, node };
13499
- }
13500
-
13501
13427
  // Strip comments and plain string literals so prose or route paths cannot
13502
13428
  // register as a component reference during the early-reference scan below.
13503
13429
  // Template literals are deliberately LEFT intact: `${Name}` interpolations are
@@ -13727,14 +13653,15 @@ function compileComponent(node, ctx, options) {
13727
13653
  : null;
13728
13654
  const firstStyle = ctx.cssInjections.length;
13729
13655
 
13730
- // Scoped `<style>` block. New TSRX surfaces each style block as a
13731
- // `JSXStyleElement` child of the rendered tree (parser pre-computes the
13732
- // content hash + parses CSS into a StyleSheet AST). Collect them, run the
13733
- // @tsrx/core scoping pipeline (rewrites `.foo` → `.foo.<hash>` AND stamps
13734
- // the hash class onto every element under this component), emit a single
13735
- // module-level `injectStyle(hash, css)`, and surface `cssHash` so
13736
- // resolveStyleExpr can also prefix any `{style (expr)}` class expressions.
13737
- const scoping = applyCssScoping(node, ctx);
13656
+ // Scoped `<style>` blocks. TSRX surfaces each block as a `JSXStyleElement`
13657
+ // item of the list it is written in (parser pre-computes the content hash
13658
+ // + parses CSS into a StyleSheet AST). The pre-pass gives every list that
13659
+ // holds a block its own hash, rewrites `.foo` → `.foo.<hash>`, stamps the
13660
+ // hash on the list's other items and their descendants (never on the
13661
+ // element that contains the list), emits one `injectStyle(hash, css)` per
13662
+ // scope, and surfaces the first scope's hash as `cssHash` — kept only as a
13663
+ // "this component owns scoped CSS" flag for resolveStyleExpr's fallback.
13664
+ const scoping = applyStyleScopes(node, ctx);
13738
13665
  node = scoping.node;
13739
13666
  let cssHash = scoping.cssHash;
13740
13667
  // Backwards-compat: internal callers (legacy synthetic Component shapes)
@@ -13855,7 +13782,9 @@ function compileComponent(node, ctx, options) {
13855
13782
  : fnNode,
13856
13783
  ];
13857
13784
  if (pendingWarm) {
13858
- statements.push(guarded(objectAssign(b.id(name), '__warm', pendingWarm)));
13785
+ statements.push(
13786
+ guarded(b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), pendingWarm)),
13787
+ );
13859
13788
  }
13860
13789
  if (hmrWrap && returnedOutput) {
13861
13790
  statements.push(
@@ -13890,7 +13819,12 @@ function compileComponent(node, ctx, options) {
13890
13819
  // identity by naming the inner FunctionExpression.
13891
13820
  const fnExpr = functionExpressionFromDeclaration(fnNode);
13892
13821
  const warmedFn = pendingWarm
13893
- ? markPure(inheritOriginLoc(objectAssign(fnExpr, '__warm', pendingWarm), node))
13822
+ ? markPure(
13823
+ inheritOriginLoc(
13824
+ b.call(requireRuntimeForContext(ctx, 'markWarm'), fnExpr, pendingWarm),
13825
+ node,
13826
+ ),
13827
+ )
13894
13828
  : fnExpr;
13895
13829
  const abiFn =
13896
13830
  hmrWrap && returnedOutput
@@ -14370,6 +14304,32 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14370
14304
  bodyStatements.push(...mapTemps.map((temp) => inheritOriginLoc(b.let(temp), node)));
14371
14305
  }
14372
14306
  bodyStatements.push(...rewrittenStatements);
14307
+ // A self-update scheduled by setup discards this output. The owning Block
14308
+ // replays setup before returning to its parent, so children initialize from
14309
+ // the settled state and no template/resource work escapes the first pass.
14310
+ // Setup that only reads built-in hooks and pure globals cannot schedule an
14311
+ // update; keep those bodies unchanged (see setupCanScheduleSelfUpdate).
14312
+ if (
14313
+ ctx.mode !== 'server' &&
14314
+ ctx._universalRuntimeUnit == null &&
14315
+ !returnedOutput &&
14316
+ setupCanScheduleSelfUpdate(statements, ctx)
14317
+ ) {
14318
+ bodyStatements.push(
14319
+ inheritOriginLoc(
14320
+ b.if(
14321
+ b.logical(
14322
+ '&&',
14323
+ b.member(b.member(b.id('__s'), 'block'), 'pending'),
14324
+ b.unary('!', b.member(b.member(b.id('__s'), 'block'), 'crossRenderUpdate')),
14325
+ ),
14326
+ b.return(null),
14327
+ null,
14328
+ ),
14329
+ node,
14330
+ ),
14331
+ );
14332
+ }
14373
14333
  // GLOBAL resource registrations (stylesheet precedence links, style
14374
14334
  // resources, async scripts) run here: after setup, so an href computed from
14375
14335
  // a body local is initialized, and BEFORE the constructs below mount any
@@ -14564,6 +14524,11 @@ function transformUniversalParallelUse(ast, ctx, metadata) {
14564
14524
  node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression';
14565
14525
  const unwrapAssignedFunction = (node) => {
14566
14526
  if (isFunction(node)) return node;
14527
+ if (
14528
+ calleeName(node) === runtimeAliasForContext(ctx, 'markWarm') &&
14529
+ isFunction(node.arguments?.[0])
14530
+ )
14531
+ return node.arguments[0];
14567
14532
  if (
14568
14533
  node?.type === 'CallExpression' &&
14569
14534
  node.callee?.type === 'MemberExpression' &&
@@ -14696,14 +14661,7 @@ function transformUniversalParallelUse(ast, ctx, metadata) {
14696
14661
  };
14697
14662
  // The warm-plan attach maps to the component function it decorates.
14698
14663
  const assignWarmPlan = (fn, warmNode) =>
14699
- inheritOriginLoc(
14700
- b.call(
14701
- b.member(b.id('Object'), 'assign'),
14702
- fn,
14703
- b.object([b.prop('init', b.id('__warm'), warmNode)]),
14704
- ),
14705
- fn,
14706
- );
14664
+ inheritOriginLoc(b.call(requireRuntimeForContext(ctx, 'markWarm'), fn, warmNode), fn);
14707
14665
 
14708
14666
  const transformFunction = (fn, name, component) => {
14709
14667
  if (!isFunction(fn) || transformed.has(fn)) return fn;
@@ -15764,7 +15722,11 @@ function parallelUseWalkJsx(nodes, ctx, componentName, creations, warmChildren,
15764
15722
  const nameNode = node.openingElement && node.openingElement.name;
15765
15723
  const isComponent =
15766
15724
  nameNode && nameNode.type === 'JSXIdentifier' && /^[A-Z]/.test(nameNode.name);
15767
- if (isComponent) {
15725
+ const transparentBoundary =
15726
+ isComponent &&
15727
+ ctx._octaneSuspenseNames?.has(nameNode.name) &&
15728
+ !ctx.currentComponentLocals?.has(nameNode.name);
15729
+ if (isComponent && !transparentBoundary) {
15768
15730
  // Server: creations flowing INTO child props (`<Kid p={make(x)}/>`)
15769
15731
  // need the same cross-pass identity as use()-site creations — every
15770
15732
  // streaming wave re-runs this body, and a recreated per-request
@@ -16424,7 +16386,7 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
16424
16386
  // The hoisted fetch plan: creations + child calls, params destructured
16425
16387
  // from the incoming props object. Single-param components only (the norm).
16426
16388
  // Returned as a bare arrow — compileComponent attaches it to the INNER
16427
- // function object via Object.assign, so the component's own body (where
16389
+ // function object via markWarm, so the component's own body (where
16428
16390
  // the function-expression name shadows the module const) sees it too;
16429
16391
  // hmr() forwards it from the wrapped fn onto the HMR wrapper.
16430
16392
  let warmNode = null;
@@ -16644,13 +16606,17 @@ const NUMERIC_HOOK_SLOT_POSITION = {
16644
16606
  useEffectEvent: 1,
16645
16607
  useImperativeHandle: 3,
16646
16608
  useActionState: 3,
16609
+ useFormState: 3,
16647
16610
  useOptimistic: 2,
16648
16611
  useSignal$: 1,
16649
16612
  };
16650
16613
 
16651
16614
  function appendHookSlotArgument(name, args, slot, numeric, origin) {
16652
16615
  const out = [...args];
16653
- const position = numeric ? NUMERIC_HOOK_SLOT_POSITION[name] : undefined;
16616
+ const position =
16617
+ numeric || name === 'useState' || name === 'useRef'
16618
+ ? NUMERIC_HOOK_SLOT_POSITION[name]
16619
+ : undefined;
16654
16620
  if (position !== undefined) {
16655
16621
  while (out.length < position) out.push(b.id('undefined', origin));
16656
16622
  }
@@ -16658,6 +16624,18 @@ function appendHookSlotArgument(name, args, slot, numeric, origin) {
16658
16624
  return out;
16659
16625
  }
16660
16626
 
16627
+ function wrapHookCallWithSlot(call, ctx, slot, callee, args) {
16628
+ const fn = call.typeArguments
16629
+ ? { type: 'TSInstantiationExpression', expression: callee, typeArguments: call.typeArguments }
16630
+ : callee;
16631
+ return {
16632
+ ...call,
16633
+ callee: b.id(requireRuntimeForContext(ctx, 'withSlot')),
16634
+ typeArguments: null,
16635
+ arguments: [typeof slot === 'string' ? b.id(slot) : slot, fn, ...args],
16636
+ };
16637
+ }
16638
+
16661
16639
  function arrayPatternObservesStateGetter(pattern) {
16662
16640
  const elements = pattern.elements || [];
16663
16641
  if (elements[2] != null) return true;
@@ -17308,20 +17286,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17308
17286
  const importedName = n._octaneImportedHook;
17309
17287
  const hookRuntimeImportedName = n._octaneHookRuntimeImportedHook;
17310
17288
  const name = importedName ?? hookRuntimeImportedName ?? localName;
17311
- // Three kinds of call get a trailing per-call-site hook slot:
17289
+ // Three kinds of call receive a per-call-site hook identity:
17312
17290
  // - a built-in base hook (HOOK_NAMES) — also needs its runtime import;
17313
17291
  // - a custom / library hook by React's `use[A-Z]` convention — e.g. a
17314
17292
  // `useStore` binding from @octanejs/zustand that WRAPS a base hook and
17315
- // FORWARDS the slot to it. `useContext` is keyed by context identity (no
17293
+ // resolves the enclosing call path. `useContext` is keyed by context identity (no
17316
17294
  // slot) and `use` has no uppercase suffix, so both are excluded here. We
17317
17295
  // do NOT import custom hooks — they're user/library imports;
17318
17296
  // - a server-mode `use(thenable)` — a stable suspense-cache key.
17319
17297
  // Distinct call sites get distinct slots, so `useStore(a)`/`useStore(b)`
17320
17298
  // (or the same hook twice) stay independent.
17321
17299
  //
17322
- // NB: a `use*` NAME is reserved for hooks (React's convention) — a non-hook
17323
- // function named like one gets a harmless extra trailing argument (though
17324
- // inside a plain JS loop the convention is enforced: rejectHookInJsLoop).
17300
+ // A `use*` name is reserved for hooks. The custom wrapper preserves its
17301
+ // authored argument list; inside a plain JS loop the convention is
17302
+ // enforced by rejectHookInJsLoop.
17325
17303
  const isBuiltin =
17326
17304
  (HOOK_NAMES.has(name) ||
17327
17305
  (ctx.nativeReads &&
@@ -17338,10 +17316,9 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17338
17316
  const inline = lowerLocalMemoExpression(n, name);
17339
17317
  if (inline !== null) return inline;
17340
17318
  }
17341
- // Keep a Symbol at custom-hook call boundaries: published bindings split
17342
- // that trailing value from optional user args and derive manual sub-slots
17343
- // from it. Proven render-scope base hooks can use the smaller numeric
17344
- // production ABI; arbitrary callable helpers cannot.
17319
+ // Keep a Symbol on custom-hook call paths so their internal base-hook
17320
+ // identities compose across modules. Proven render-scope base hooks
17321
+ // can use the smaller numeric production ABI.
17345
17322
  const hasSpread = n.arguments.some((arg) => arg.type === 'SpreadElement');
17346
17323
  const memoRoute = isBuiltin && canLowerClientMemo(n, name);
17347
17324
  const memoCandidate = memoRoute ? slotMemoCandidate(n, name) : null;
@@ -17406,18 +17383,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17406
17383
  // separately must not re-promote its nested hooks to render-local.
17407
17384
  const argsLocalRoot = localSlotMarks.get(n) === true;
17408
17385
  const args = n.arguments.map((a) => rewriteHookCalls(a, ctx, componentName, argsLocalRoot));
17409
- // NB: base hooks are ALSO `use[A-Z]`, so the wrap is for custom hooks ONLY
17410
- // (`isCustom && !isBuiltin`) — base hooks keep the plain trailing-slot form.
17411
- if (isCustom && !isBuiltin) {
17412
- // A CUSTOM hook is wrapped in `withSlot(sym, hook, ...args, sym)`: the
17413
- // withSlot pushes a call-site symbol on the path stack so the hook's
17414
- // inner BASE hooks combine it (→ the same custom hook reused at two
17415
- // sites keeps independent state — base hooks are "owned by octane" and
17416
- // need no wrapper). The TRAILING `sym` is retained so existing library
17417
- // bindings that extract the slot from their last argument keep working.
17418
- const withSlotAlias = requireRuntimeForContext(ctx, 'withSlot');
17419
- return b.call(withSlotAlias, b.id(symVar), n.callee, ...args, b.id(symVar));
17420
- }
17386
+ const callee = nativeMemo
17387
+ ? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
17388
+ : getterHelper !== null
17389
+ ? b.id(runtimeAliasForContext(ctx, getterHelper))
17390
+ : n.callee._octaneGenerated
17391
+ ? b.id(runtimeAliasForContext(ctx, name))
17392
+ : n.callee;
17393
+ // The call path supplies identity without adding an argument that
17394
+ // changes a custom hook's defaults, rest values or arguments.length.
17395
+ // State/ref spreads also need this path: an empty spread has no
17396
+ // initializer position into which a trailing slot may safely fall.
17397
+ if (isCustom || (hasSpread && (name === 'useState' || name === 'useRef')))
17398
+ return wrapHookCallWithSlot(n, ctx, slot, callee, args);
17399
+
17421
17400
  const hookArgs = explicitMemoSlot
17422
17401
  ? args
17423
17402
  : appendHookSlotArgument(name, args, slot, numericSlot, n);
@@ -17432,13 +17411,7 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17432
17411
  return {
17433
17412
  ...n,
17434
17413
  ...(memoRoute ? { _octaneInlineMemoHook: memoCandidate } : null),
17435
- callee: nativeMemo
17436
- ? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
17437
- : getterHelper !== null
17438
- ? b.id(runtimeAliasForContext(ctx, getterHelper))
17439
- : n.callee._octaneGenerated
17440
- ? b.id(runtimeAliasForContext(ctx, name))
17441
- : n.callee,
17414
+ callee,
17442
17415
  arguments: hookArgs,
17443
17416
  };
17444
17417
  }
@@ -17505,14 +17478,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17505
17478
  const args = n.arguments.map((a) =>
17506
17479
  rewriteHookCalls(a, ctx, componentName, localSlotMarks.get(n) === true),
17507
17480
  );
17481
+ const callee = nativeMemo
17482
+ ? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
17483
+ : getterHelper !== null
17484
+ ? b.id(runtimeAliasForContext(ctx, getterHelper))
17485
+ : n.callee;
17486
+ if (
17487
+ (name === 'useState' || name === 'useRef') &&
17488
+ args.some((arg) => arg.type === 'SpreadElement')
17489
+ )
17490
+ return wrapHookCallWithSlot(n, ctx, slot, callee, args);
17508
17491
  return {
17509
17492
  ...n,
17510
17493
  ...(memoRoute ? { _octaneInlineMemoHook: memoCandidate } : null),
17511
- callee: nativeMemo
17512
- ? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
17513
- : getterHelper !== null
17514
- ? b.id(runtimeAliasForContext(ctx, getterHelper))
17515
- : n.callee,
17494
+ callee,
17516
17495
  arguments: explicitMemoSlot
17517
17496
  ? args
17518
17497
  : appendHookSlotArgument(name, args, slot, numericSlot, n),
@@ -17524,9 +17503,8 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17524
17503
  // and the like). Same `use[A-Z]` convention, applied to the PROPERTY name.
17525
17504
  // The callee is a member access, so the plain withSlot form would sever
17526
17505
  // `this`; instead the WHOLE call is wrapped in a thunk —
17527
- // `_$withSlot(sym, () => obj.useX(...args, sym))` — which pushes the
17528
- // call-site path symbol for the hook's inner base hooks while the trailing
17529
- // `sym` still reaches slot-forwarding bindings (splitSlot convention).
17506
+ // `_$withSlot(sym, () => obj.useX(...args))` preserves the receiver and
17507
+ // authored arguments while supplying the inner base hooks' call path.
17530
17508
  if (
17531
17509
  n.type === 'CallExpression' &&
17532
17510
  !n.optional &&
@@ -17558,7 +17536,7 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17558
17536
  return b.call(
17559
17537
  withSlotAlias,
17560
17538
  b.id(symVar),
17561
- b.arrow([], b.call({ ...n.callee, object }, ...args, b.id(symVar))),
17539
+ b.arrow([], { ...n, callee: { ...n.callee, object }, arguments: args }),
17562
17540
  );
17563
17541
  }
17564
17542
  return null;
@@ -17574,7 +17552,7 @@ function compileReturnJsxFunction(node, ctx, options) {
17574
17552
  const hookMemoOpaqueOwner = ctx.inlineHookMemo && hasInlineMemoOpaqueDirective(node);
17575
17553
  recordProfileComponent(ctx, node, name);
17576
17554
  const beforeCss = ctx.cssInjections.length;
17577
- const scoping = applyCssScoping(node, ctx);
17555
+ const scoping = applyStyleScopes(node, ctx);
17578
17556
  node = scoping.node;
17579
17557
  const cssHash = scoping.cssHash;
17580
17558
  const cssEntries =
@@ -17696,6 +17674,7 @@ function compileReturnJsxFunction(node, ctx, options) {
17696
17674
  compInlinedSubs,
17697
17675
  cssHash,
17698
17676
  cssEntries,
17677
+ scoping.runtimeApplied,
17699
17678
  ),
17700
17679
  };
17701
17680
  }
@@ -17775,6 +17754,10 @@ function compileReturnJsxFunction(node, ctx, options) {
17775
17754
  }),
17776
17755
  );
17777
17756
  }
17757
+ if (ctx.mode === 'server' && !ctx.nativeReads) {
17758
+ // Imported themes inject on read; touch them ahead of this body's own CSS.
17759
+ returnBody.unshift(...serverThemeTouchStatements(scoping.runtimeApplied, ctx, node));
17760
+ }
17778
17761
  const emittedFunction = b.function_declaration(
17779
17762
  node.id,
17780
17763
  node.params,
@@ -17818,7 +17801,15 @@ function compileReturnJsxFunction(node, ctx, options) {
17818
17801
  return { nodes: [fn] };
17819
17802
  }
17820
17803
 
17821
- function nativeReturnedJsxValue(node, ctx, name, inlinedSubs, cssHash, cssEntries) {
17804
+ function nativeReturnedJsxValue(
17805
+ node,
17806
+ ctx,
17807
+ name,
17808
+ inlinedSubs,
17809
+ cssHash,
17810
+ cssEntries,
17811
+ runtimeApplied = [],
17812
+ ) {
17822
17813
  let value;
17823
17814
  if (requiresTemplateNormalization(node, 'html', true, ctx)) {
17824
17815
  // Head resources, directive roots and other compiler-only syntax keep
@@ -17838,7 +17829,8 @@ function nativeReturnedJsxValue(node, ctx, name, inlinedSubs, cssHash, cssEntrie
17838
17829
  }
17839
17830
  // A stored styled value may be constructed outside an SSR render. Its CSS
17840
17831
  // enters the request collector when interpreted, not a global pending queue.
17841
- if (cssHash === null && !ctx.nativeModuleStyles) return value;
17832
+ const themeTouches = serverThemeTouchStatements(runtimeApplied, ctx, node);
17833
+ if (cssHash === null && !ctx.nativeModuleStyles && themeTouches.length === 0) return value;
17842
17834
  const scopedElement = runtimeAliasForContext(ctx, 'nativeCreateScopedElement');
17843
17835
  const scopedValue = runtimeAliasForContext(ctx, 'nativeCreateScopedValue');
17844
17836
  // A static styled fragment needs a reader too, even when it only owns a
@@ -17859,15 +17851,18 @@ function nativeReturnedJsxValue(node, ctx, name, inlinedSubs, cssHash, cssEntrie
17859
17851
  node,
17860
17852
  );
17861
17853
  }
17862
- if (cssEntries.length === 0) return value;
17863
- ctx.runtimeNeeded.add('injectStyle');
17864
- const css = cssEntries.map((entry) => {
17865
- const origin = claimCssOrigins(ctx, entry) ?? node;
17866
- return inheritOriginLoc(
17867
- b.stmt(ssrCall('injectStyle', [b.literal(entry.hash), b.literal(entry.css)], origin)),
17868
- origin,
17869
- );
17870
- });
17854
+ if (cssEntries.length === 0 && themeTouches.length === 0) return value;
17855
+ if (cssEntries.length > 0) ctx.runtimeNeeded.add('injectStyle');
17856
+ const css = [
17857
+ ...themeTouches,
17858
+ ...cssEntries.map((entry) => {
17859
+ const origin = claimCssOrigins(ctx, entry) ?? node;
17860
+ return inheritOriginLoc(
17861
+ b.stmt(ssrCall('injectStyle', [b.literal(entry.hash), b.literal(entry.css)], origin)),
17862
+ origin,
17863
+ );
17864
+ }),
17865
+ ];
17871
17866
  const readerIndex =
17872
17867
  value.callee?.name === scopedValue ? 0 : value.callee?.name === scopedElement ? 2 : -1;
17873
17868
  if (readerIndex === -1) {
@@ -18012,11 +18007,22 @@ function requiresTemplateNormalization(
18012
18007
  t === 'FragmentStart' ||
18013
18008
  t === 'FragmentEnd' ||
18014
18009
  t === 'FoldedDirective' ||
18015
- t === 'HeadHoist' ||
18016
- t === 'JSXCodeBlock'
18010
+ t === 'HeadHoist'
18017
18011
  ) {
18018
18012
  return true;
18019
18013
  }
18014
+ if (t === 'JSXCodeBlock') {
18015
+ // A render-only child block is transparent template grouping (the same
18016
+ // rule normalizeChildren and extractFragment apply): only its render
18017
+ // decides. A setup-bearing or code-only block is a render scope of its
18018
+ // own and needs the compiled-fragment path. A styled static fragment
18019
+ // therefore stays a descriptor when its block only groups the fragment
18020
+ // that holds the `<style>` — the form amendment A1 leaves a value factory
18021
+ // written in plain TSX (raw CSS needs a `@{ … }` container).
18022
+ const body = node.body || [];
18023
+ if (body.length > 0 || !node.render) return true;
18024
+ return requiresTemplateNormalization(node.render, parentNs, allowHeadHoists, ctx);
18025
+ }
18020
18026
  if (t === 'Fragment' || t === 'JSXFragment' || t === 'Tsx' || t === 'Tsrx') {
18021
18027
  return (node.children || []).some((child) =>
18022
18028
  requiresTemplateNormalization(child, parentNs, allowHeadHoists, ctx),
@@ -18319,6 +18325,15 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
18319
18325
  // `{/* … */}` is a `JSXEmptyExpression` container — a JSX comment, which
18320
18326
  // renders to nothing (React drops it). Skip it so it never becomes a hole.
18321
18327
  if (!expr || expr.type === 'JSXEmptyExpression') continue;
18328
+ // String literals (`{" "}`, `{"hello"}`) are static text, same as JSXText.
18329
+ // The .tsrx and server paths fold them via staticTextLiteral; hoisting one
18330
+ // as hN here made .tsx client templates grow an extra text hole while the
18331
+ // server baked the string, so hydration consumed the next sibling (duplicate
18332
+ // elements). Leave the container in place so emitElementHtml bakes it.
18333
+ if (staticTextLiteral(expr) !== null) {
18334
+ newChildren.push(child);
18335
+ continue;
18336
+ }
18322
18337
  const hn = `h${holeProps.length}`;
18323
18338
  if (expr && expr.type === 'TSAsExpression') {
18324
18339
  // Preserve the `as T` cast in the renderer (it marks a dynamic TEXT hole).
@@ -19217,6 +19232,20 @@ function rewriteJsxValues(node, ctx, eagerMapCallbackRoots = false, eagerMapCall
19217
19232
  if (lower != null) node = lowerSetupValueDirectives(node, lower);
19218
19233
  return mapAst(node, (n) => {
19219
19234
  const t = n && n.type;
19235
+ if (isFunctionNode(n) && n.body?.type === 'JSXCodeBlock') {
19236
+ const name = n.id?.name ?? allocCompilerName(ctx, '__template');
19237
+ const previousLocals = ctx.currentComponentLocals;
19238
+ ctx.currentComponentLocals = collectComponentLocals(n);
19239
+ try {
19240
+ const compiled =
19241
+ ctx.mode === 'server'
19242
+ ? ssrCompileBody(n, ctx, name, null, [], 'opaque')
19243
+ : compileFunctionBody(n, ctx, name, 'opaque');
19244
+ return functionExpressionFromDeclaration({ ...compiled, id: n.id ?? null }, n);
19245
+ } finally {
19246
+ ctx.currentComponentLocals = previousLocals;
19247
+ }
19248
+ }
19220
19249
  if (
19221
19250
  ctx.nativeReads &&
19222
19251
  lower == null &&
@@ -20067,11 +20096,6 @@ function jsxTagName(n) {
20067
20096
  : null;
20068
20097
  }
20069
20098
 
20070
- /** @param {any} n @returns {boolean} */
20071
- function isHeadElementNode(n) {
20072
- return Boolean(n && n.type === 'JSXElement' && jsxTagName(n) === 'head');
20073
- }
20074
-
20075
20099
  /** @param {any} n @returns {boolean} */
20076
20100
  function isHoistableHeadElementNode(n) {
20077
20101
  if (n == null || (n.type !== 'JSXElement' && n.type !== 'Element')) return false;
@@ -20111,7 +20135,7 @@ function isHoistableHeadElementNode(n) {
20111
20135
  inner.value === 'stylesheet';
20112
20136
  }
20113
20137
  }
20114
- if (tag === 'link' && relStylesheet && hasPrecedence && (!hasHref || emptyHref)) return false;
20138
+ if (tag === 'link' && relStylesheet && (!hasPrecedence || !hasHref || emptyHref)) return false;
20115
20139
  return true;
20116
20140
  }
20117
20141
 
@@ -20329,7 +20353,7 @@ function deferredHeadAttrs(el) {
20329
20353
  // props.children under HTML, SVG, or MathML. namespaceHead resolves that choice
20330
20354
  // from its actual parent at render time while retaining ordinary component-block
20331
20355
  // hydration and keyed ownership.
20332
- function deferredTitleElement(el, ctx) {
20356
+ function deferredTitleElement(el, ctx, tag = 'title') {
20333
20357
  ctx.runtimeNeeded.add('namespaceHeadElement');
20334
20358
  let authoredKey = null;
20335
20359
  for (const attr of el.openingElement.attributes || []) {
@@ -20345,7 +20369,7 @@ function deferredTitleElement(el, ctx) {
20345
20369
  }
20346
20370
  const args = [
20347
20371
  b.literal(headKey(el, 0, ctx)),
20348
- b.literal('title'),
20372
+ b.literal(tag),
20349
20373
  deferredHeadAttrs(el),
20350
20374
  headTextExpression(el),
20351
20375
  ];
@@ -20658,9 +20682,137 @@ function emitHeadServer(headNodes, ctx) {
20658
20682
  });
20659
20683
  }
20660
20684
 
20685
+ // JSX indentation is syntax, while same-line spaces are rendered content. Cook
20686
+ // authored literals once, before either emitter sees them, so entities cannot be
20687
+ // decoded twice as a tree passes through fragment and value lowering.
20688
+ function normalizeAuthoredJsxLiterals(ast) {
20689
+ return mapAst(ast, (node) => {
20690
+ if (node.type === 'JSXText') {
20691
+ const lines = node.value.split(/\r\n|\n|\r/);
20692
+ let last = 0;
20693
+ for (let i = 0; i < lines.length; i++) if (/[^ \t]/.test(lines[i])) last = i;
20694
+ let value = '';
20695
+ for (let i = 0; i < lines.length; i++) {
20696
+ let line = lines[i].replace(/\t/g, ' ');
20697
+ if (i !== 0) line = line.replace(/^ +/, '');
20698
+ if (i !== lines.length - 1) line = line.replace(/ +$/, '');
20699
+ if (line !== '') value += line + (i !== last ? ' ' : '');
20700
+ }
20701
+ if (value.includes('&')) value = decodeHTMLStrict(value);
20702
+ return value === node.value ? node : { ...node, value, raw: value };
20703
+ }
20704
+ if (
20705
+ node.type === 'JSXAttribute' &&
20706
+ node.value?.type === 'Literal' &&
20707
+ typeof node.value.value === 'string' &&
20708
+ node.value.value.includes('&')
20709
+ ) {
20710
+ const value = decodeHTMLStrict(node.value.value);
20711
+ return { ...node, value: { ...node.value, value, raw: JSON.stringify(value) } };
20712
+ }
20713
+ if (
20714
+ node.type === 'JSXStyleElement' &&
20715
+ (hasJsxAttribute(node, 'dangerouslySetInnerHTML') || hasJsxAttribute(node, 'children'))
20716
+ ) {
20717
+ return normalizeAuthoredJsxLiterals(
20718
+ b.jsx_element(node, node.openingElement.attributes || [], []),
20719
+ );
20720
+ }
20721
+ return null;
20722
+ });
20723
+ }
20724
+
20725
+ function requiresImperativeHostTree(root, ctx) {
20726
+ const tag = jsxTagName(root) || elementTagName(root);
20727
+ return (
20728
+ (ctx.mode !== 'server' && root._octaneImperativeHost === true) ||
20729
+ hasJsxAttribute(root, 'key') ||
20730
+ tag === 'noscript' ||
20731
+ tag === 'html' ||
20732
+ tag === 'head' ||
20733
+ tag === 'body'
20734
+ );
20735
+ }
20736
+
20737
+ // Analyze parser repair once per authored tree. Mark only the affected ancestor,
20738
+ // preserving template compilation for unrelated hosts around that subtree.
20739
+ function markParserSensitiveHosts(ast) {
20740
+ const unsafe = new WeakSet();
20741
+ let found = false;
20742
+ const mark = (node) => {
20743
+ unsafe.add(node);
20744
+ found = true;
20745
+ };
20746
+ function visit(node, ancestors = [], namespace = 'html') {
20747
+ if (node === null || typeof node !== 'object') return;
20748
+ if (Array.isArray(node)) {
20749
+ for (const child of node) visit(child, ancestors, namespace);
20750
+ return;
20751
+ }
20752
+ if (node.type === 'JSXText') {
20753
+ if (ancestors.length && invalidHtmlTextNesting(node.value, ancestors[0].tag) !== null)
20754
+ mark(ancestors[0].node);
20755
+ return;
20756
+ }
20757
+ if (node.type === 'JSXFragment') {
20758
+ visit(node.children || [], ancestors, namespace);
20759
+ return;
20760
+ }
20761
+ if (node.type === 'JSXElement') {
20762
+ const tag = jsxTagName(node);
20763
+ if (isComponentTag(node)) {
20764
+ visit(node.children || []);
20765
+ return;
20766
+ }
20767
+ const self = nsForSelf(tag, namespace);
20768
+ if (self === 'html' && ancestors.length) {
20769
+ if (invalidHtmlNestingWithParent(tag, ancestors[0].tag) !== null) mark(ancestors[0].node);
20770
+ const tags = ancestors.map((entry) => entry.tag);
20771
+ for (let i = 1; i < ancestors.length; i++) {
20772
+ if (invalidHtmlNestingWithAncestor(tag, tags.slice(0, i + 1)) !== null)
20773
+ mark(ancestors[i].node);
20774
+ }
20775
+ }
20776
+ const childNamespace = nsForChildren(tag, namespace);
20777
+ visit(
20778
+ node.children || [],
20779
+ childNamespace === 'html' ? [{ node, tag }, ...ancestors] : [],
20780
+ childNamespace,
20781
+ );
20782
+ visit(node.openingElement?.attributes || []);
20783
+ return;
20784
+ }
20785
+ // A value expression or directive owns its insertion range. Its elements
20786
+ // are parsed separately, so lexical host ancestry does not cross it.
20787
+ for (const key in node) {
20788
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
20789
+ visit(node[key]);
20790
+ }
20791
+ }
20792
+ visit(ast);
20793
+ return found
20794
+ ? mapAst(ast, (node) => (unsafe.has(node) ? { ...node, _octaneImperativeHost: true } : null))
20795
+ : ast;
20796
+ }
20797
+
20798
+ function rewriteImperativeHeadElements(node, ctx, namespace = 'html', inNoscript = false) {
20799
+ if (node?.type !== 'JSXElement' && node?.type !== 'JSXFragment') return node;
20800
+ const tag = jsxTagName(node);
20801
+ const nextNamespace = tag ? nsForChildren(tag, namespace) : namespace;
20802
+ if (!inNoscript && namespace === 'html' && isHoistableHeadElementNode(node)) {
20803
+ return deferredTitleElement(node, ctx, tag);
20804
+ }
20805
+ return {
20806
+ ...node,
20807
+ children: (node.children || []).map((child) =>
20808
+ rewriteImperativeHeadElements(child, ctx, nextNamespace, inNoscript || tag === 'noscript'),
20809
+ ),
20810
+ };
20811
+ }
20812
+
20661
20813
  /**
20662
20814
  * Normalize a list of JSX child nodes into the shapes the emitters consume:
20663
- * - Whitespace-only JSXText / JSX comments (`{…}` empty containers) → dropped
20815
+ * - Indentation-only JSXText / JSX comments (`{…}` empty containers) → dropped
20664
20816
  * - JSXText with content → `Text` node wrapping a string Literal
20665
20817
  * - `{expr}` container → `Text` hole, or `TSRXExpression` when the
20666
20818
  * expression needs the rich dispatcher (portals, JSX ternaries,
@@ -20670,7 +20822,7 @@ function emitHeadServer(headNodes, ctx) {
20670
20822
  * - JSXElement → `Element` (with `<Fragment ref>` expanded to a
20671
20823
  * FragmentStart/…/FragmentEnd sequence, `<Activity>` lowered to an
20672
20824
  * ActivityStatement, eligible `<title>/<meta>/<link>` hoisted as `HeadHoist`,
20673
- * and `<head>` rejected)
20825
+ * and parser-sensitive host trees lowered as descriptors)
20674
20826
  * - Fragments (`<>…</>`, Tsx/Tsrx) → flattened (children inlined)
20675
20827
  * - JSXStyleElement → dropped (its CSS is handled by the scoping pipeline)
20676
20828
  * - Directive expressions (@if/@for/@try/@switch, `@{…}` child blocks) →
@@ -20681,13 +20833,19 @@ function emitHeadServer(headNodes, ctx) {
20681
20833
  // `inSvg`: the children being normalized sit inside an SVG-namespace subtree.
20682
20834
  // SVG has its own `<title>` (the accessibility tooltip element) — it must stay
20683
20835
  // where it is, NOT hoist to document.head (React 19 makes the same exception).
20684
- function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false) {
20836
+ function normalizeChildren(
20837
+ nodes,
20838
+ inSvg = false,
20839
+ ctx = null,
20840
+ inNoscript = false,
20841
+ allowImperative = true,
20842
+ ) {
20685
20843
  const out = [];
20686
20844
  if (!nodes) return out;
20687
20845
  for (const n of nodes) {
20688
20846
  if (!n) continue;
20689
20847
  if (n.type === 'JSXText') {
20690
- if (/^\s*$/.test(n.value)) continue;
20848
+ if (n.value === '' || (/^\s*$/.test(n.value) && /[\n\r]/.test(n.value))) continue;
20691
20849
  // The Text node and its literal map to the authored JSX text.
20692
20850
  out.push(
20693
20851
  inheritOriginLoc(
@@ -20788,14 +20946,29 @@ function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false)
20788
20946
  out.push({ type: 'ActivityStatement', mode, children: n.children || [] });
20789
20947
  continue;
20790
20948
  }
20791
- // `<head>` is no longer a construct (React-19 model): render
20792
- // `<title>`/`<meta>`/`<link>` directly and they hoist to document.head.
20793
- if (isHeadElementNode(n)) {
20794
- throw new Error(
20795
- '`<head>` is not supported in octane. Render `<title>`, `<meta>`, and ' +
20796
- '`<link>` directly (anywhere in the component) — they are hoisted to ' +
20797
- 'document.head automatically, React-19-style.',
20949
+ // Keys need a reconciliation boundary. Parser-sensitive host trees need
20950
+ // imperative construction so HTML repair cannot change binding paths.
20951
+ // The shared descriptor path provides both without taxing ordinary templates.
20952
+ if (
20953
+ ctx &&
20954
+ allowImperative &&
20955
+ !isComponentTag(n) &&
20956
+ (hasJsxAttribute(n, 'key') || (!inSvg && requiresImperativeHostTree(n, ctx)))
20957
+ ) {
20958
+ out.push(
20959
+ inheritOriginLoc(
20960
+ {
20961
+ type: 'Text',
20962
+ expression: jsxElementToCreateElement(
20963
+ rewriteImperativeHeadElements(n, ctx, inSvg ? 'svg' : 'html'),
20964
+ ctx,
20965
+ ),
20966
+ imperativeRoot: n,
20967
+ },
20968
+ n,
20969
+ ),
20798
20970
  );
20971
+ continue;
20799
20972
  }
20800
20973
  // `<title>`/`<meta>`/eligible `<link>` → hoist to the document-head channel (NOT
20801
20974
  // body DOM). Kept in `out` as a synthetic node so planJsx / ssrCompileBody
@@ -20843,6 +21016,18 @@ function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false)
20843
21016
  } else if (n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'JSXFragment') {
20844
21017
  out.push(...normalizeChildren(n.children || [], inSvg, ctx, inNoscript));
20845
21018
  } else if (n.type === 'JSXStyleElement') {
21019
+ if (hasJsxAttribute(n, 'dangerouslySetInnerHTML') || hasJsxAttribute(n, 'children')) {
21020
+ out.push(
21021
+ ...normalizeChildren(
21022
+ [b.jsx_element(n, n.openingElement.attributes || [], [])],
21023
+ inSvg,
21024
+ ctx,
21025
+ inNoscript,
21026
+ allowImperative,
21027
+ ),
21028
+ );
21029
+ continue;
21030
+ }
20846
21031
  // `<style href precedence>` is a Float style resource, not scoped CSS —
20847
21032
  // partition it to the head channel like precedence links.
20848
21033
  if (!inSvg && !inNoscript && headResourceKind(n) === 'style') {
@@ -20850,7 +21035,7 @@ function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false)
20850
21035
  continue;
20851
21036
  }
20852
21037
  // Drop a `<style>` block at child position — its CSS gets registered
20853
- // via the @tsrx/core scoping pipeline (applyCssScoping / applyStyleMap);
21038
+ // via the style-scope pre-pass (applyStyleScopes, ./style-scopes.js);
20854
21039
  // it contributes no DOM here.
20855
21040
  continue;
20856
21041
  } else if (isWrappedJsxDirective(n)) {
@@ -21852,9 +22037,9 @@ function planJsx(
21852
22037
  for (let rootI = 0; rootI < jsxNodes.length; rootI++) {
21853
22038
  const node = jsxNodes[rootI];
21854
22039
  const nodeIsComp = node.type === 'Element' && isComponentTag(node);
21855
- if (ctx.dev && node.type === 'Element' && !nodeIsComp) {
22040
+ if (ctx.dev && ((node.type === 'Element' && !nodeIsComp) || node.imperativeRoot)) {
21856
22041
  collectClientHtmlNestingWarnings(
21857
- node,
22042
+ node.imperativeRoot ?? node,
21858
22043
  ctx,
21859
22044
  parentNs === 'opaque' ? 'html' : parentNs,
21860
22045
  nestingWarningIdentities,
@@ -22692,6 +22877,7 @@ function planJsx(
22692
22877
  (fc.indexIndependent ? 8 : 0) |
22693
22878
  (fc.ssrMarkerless ? 16 : 0) |
22694
22879
  (fc.requiresScope ? 32 : 0) |
22880
+ (ctx.dev && fc.missingMapKey ? 64 : 0) |
22695
22881
  (fc.keyedSelectionIndex >= 0 ? fc.keyedSelectionIndex << 6 : 0);
22696
22882
  // Arg layout: forBlock(__s, slot, host, items, keyFn, body, flags?, deps?,
22697
22883
  // emptyBody?, anchor?, ownEnd?, selectionBody?). The final argument is
@@ -23638,7 +23824,7 @@ function cleanupsPush(fnNode) {
23638
23824
  }
23639
23825
 
23640
23826
  /**
23641
- * The `[spread, name, value]` rows a commit-phase source collector
23827
+ * The `[spread, name, value, merge?]` rows a commit-phase source collector
23642
23828
  * (`setHostPropSources` / `setFormControlSources`) resolves in authored order.
23643
23829
  * Shared by the mount and update emits, which differ only in how a source's
23644
23830
  * value is read back — a bag local at mount, a bag field on update.
@@ -23648,20 +23834,26 @@ function cleanupsPush(fnNode) {
23648
23834
  * split ssrDirectSource makes on the server); the collector itself serializes
23649
23835
  * every source of the element and stays anchored there. Nothing else in a row
23650
23836
  * reproduces the authored name — the value is the prop's expression — so
23651
- * without this the whole group answered only for the element.
23837
+ * without this the whole group answered only for the element. A trailing `true`
23838
+ * marks a synthesized scope class that must merge with a spread's class.
23652
23839
  */
23653
23840
  function commitSourceRows(sources, valueOf) {
23654
- return b.array(
23655
- sources.map(({ spread, name, nameOrigin, binding }) =>
23656
- spread
23657
- ? b.array([b.literal(true), valueOf(binding, true)])
23658
- : b.array([
23659
- b.literal(false),
23660
- inheritOriginLoc(b.literal(name), nameOrigin ?? null),
23661
- valueOf(binding, false),
23662
- ]),
23663
- ),
23664
- );
23841
+ const rows = [];
23842
+ for (let i = 0; i < sources.length; i++) {
23843
+ const source = sources[i];
23844
+ if (source.spread) {
23845
+ rows.push(b.array([b.literal(true), valueOf(source.binding, true)]));
23846
+ continue;
23847
+ }
23848
+ const cells = [
23849
+ b.literal(false),
23850
+ inheritOriginLoc(b.literal(source.name), source.nameOrigin ?? null),
23851
+ valueOf(source.binding, false),
23852
+ ];
23853
+ if (source.merge) cells.push(b.literal(true));
23854
+ rows.push(b.array(cells));
23855
+ }
23856
+ return b.array(rows);
23665
23857
  }
23666
23858
 
23667
23859
  // Mount for a DEFERRED property-write binding: store the element ref + seed the
@@ -23710,7 +23902,15 @@ function emitBindingMount(bind, elVar, bag) {
23710
23902
  // `suppressHydrationWarning`: stamp a JS flag (NOT a DOM attribute) the runtime reads to
23711
23903
  // keep the server value + skip the warning on a hydration mismatch for this element.
23712
23904
  if (bind.kind === 'suppress') {
23713
- return st(b.stmt(b.assignment('=', b.member(el(), '__oct_suppress'), b.literal(true))));
23905
+ return st(
23906
+ b.stmt(
23907
+ b.assignment(
23908
+ '=',
23909
+ b.member(el(), '__oct_suppress'),
23910
+ b.binary('===', bind.expr, b.literal(true)),
23911
+ ),
23912
+ ),
23913
+ );
23714
23914
  }
23715
23915
  if (bind.kind === 'dangerChild') {
23716
23916
  return st(b.stmt(b.call('_$markDangerouslySetInnerHTMLChildren', el())));
@@ -24850,7 +25050,7 @@ function collectClientHtmlNestingWarnings(root, ctx, inheritedNamespace, identit
24850
25050
  const visit = (node, namespace, ancestors) => {
24851
25051
  if (node?.type !== 'Element' && node?.type !== 'JSXElement') return;
24852
25052
  if (isComponentTag(node)) return;
24853
- const tag = elementTagName(node);
25053
+ const tag = jsxTagName(node) || elementTagName(node);
24854
25054
  if (tag === null) return;
24855
25055
  const semanticTag = tag.toLowerCase();
24856
25056
  const ownNamespace = nsForSelf(tag, namespace);
@@ -24884,7 +25084,13 @@ function collectClientHtmlNestingWarnings(root, ctx, inheritedNamespace, identit
24884
25084
  ? 'html'
24885
25085
  : nsForChildren(tag, namespace);
24886
25086
  const nextAncestors = [...ancestors, { tag: semanticTag, namespace: ownNamespace, location }];
24887
- const children = normalizeChildren(node.children || [], childNamespace === 'svg', ctx);
25087
+ const children = normalizeChildren(
25088
+ node.children || [],
25089
+ childNamespace === 'svg',
25090
+ ctx,
25091
+ false,
25092
+ false,
25093
+ );
24888
25094
  for (const child of children) {
24889
25095
  if (child.type === 'Text' && ownNamespace === 'html' && restrictedTextParent(semanticTag)) {
24890
25096
  const text = staticTextLiteral(child.expression);
@@ -25138,6 +25344,7 @@ function emitElementHtml(
25138
25344
  name: rawAttrName,
25139
25345
  nameOrigin: attr.name,
25140
25346
  binding,
25347
+ merge: isSynthesizedClassAttr(attr),
25141
25348
  });
25142
25349
  continue;
25143
25350
  }
@@ -25156,12 +25363,22 @@ function emitElementHtml(
25156
25363
  // `suppressHydrationWarning` (React shallow semantics): NEVER a DOM attribute (not
25157
25364
  // serialized client or server) — stamp a JS flag the runtime's hydration-mismatch
25158
25365
  // paths read, so a server/client divergence on this element keeps the server value
25159
- // and skips the warning. Bare or `={true}`/`={expr}` opts in; only `={false}` doesn't.
25366
+ // and skips the warning. Only a bare prop or a value equal to true opts in.
25160
25367
  if (rawAttrName === 'suppressHydrationWarning') {
25161
25368
  const v = attr.value;
25162
25369
  const inner = v && v.type === 'JSXExpressionContainer' ? v.expression : v;
25163
25370
  const isFalse = inner && inner.type === 'Literal' && inner.value === false;
25164
- if (!isFalse) bindings.push({ id: bindings.length, kind: 'suppress', path, origin: attr });
25371
+ if (!isFalse)
25372
+ bindings.push({
25373
+ id: bindings.length,
25374
+ kind: 'suppress',
25375
+ path,
25376
+ origin: attr,
25377
+ expr:
25378
+ inner == null
25379
+ ? b.literal(true, 'true', attr)
25380
+ : tsrxExprNode(inner, ctx, componentName, inlinedSubs),
25381
+ });
25165
25382
  continue;
25166
25383
  }
25167
25384
  const attrName = normalizeJsxAttrName(rawAttrName, tag, hostNs);
@@ -25383,6 +25600,7 @@ function emitElementHtml(
25383
25600
  // path; valid static attrs still bake with their real boolean semantics.
25384
25601
  if (
25385
25602
  isEventAttrName(attrName) ||
25603
+ MUST_USE_PROPERTY_PROPS.has(attrName.toLowerCase()) ||
25386
25604
  isAfterSpread ||
25387
25605
  classBeforeSpread ||
25388
25606
  (ctx.dev && needsDevStaticAttrValidation(attrName, true, tag, hostNs))
@@ -25473,6 +25691,7 @@ function emitElementHtml(
25473
25691
  if (
25474
25692
  inner.type === 'Literal' &&
25475
25693
  !isEventAttrName(attrName) &&
25694
+ !MUST_USE_PROPERTY_PROPS.has(attrName.toLowerCase()) &&
25476
25695
  !isAfterSpread &&
25477
25696
  !classBeforeSpread &&
25478
25697
  !(ctx.dev && needsDevStaticAttrValidation(attrName, inner.value, tag, hostNs))
@@ -25485,7 +25704,7 @@ function emitElementHtml(
25485
25704
  // Dynamic value — record a binding. (Also reached for literal values that
25486
25705
  // come after a spread, since those need to win over the spread at runtime.)
25487
25706
  const expr = tsrxExprNode(inner, ctx, componentName, inlinedSubs);
25488
- if (isEventAttrName(attrName)) {
25707
+ if (isEventAttrName(attrName) && !(tag.includes('-') && !isDelegatedEventProp(attrName))) {
25489
25708
  // React-shape: a trailing `Capture` selects the capture phase (fired
25490
25709
  // root→target before bubble handlers), stamped under `$$capture:<type>`.
25491
25710
  // The real events gotpointercapture / lostpointercapture literally end in
@@ -25501,7 +25720,14 @@ function emitElementHtml(
25501
25720
  capture = true;
25502
25721
  rest = rest.slice(0, -7);
25503
25722
  }
25504
- const eventName = rest === 'DoubleClick' ? 'dblclick' : rest.toLowerCase();
25723
+ const eventName =
25724
+ rest === 'DoubleClick'
25725
+ ? 'dblclick'
25726
+ : rest === 'Focus'
25727
+ ? 'focusin'
25728
+ : rest === 'Blur'
25729
+ ? 'focusout'
25730
+ : rest.toLowerCase();
25505
25731
  const slotKey = capture ? `$$capture:${eventName}` : `$$${eventName}`;
25506
25732
  // `onClick` → the `'$$click'` slot key. Without this the attribute name
25507
25733
  // is unreachable from the output: the key's map position resolves to
@@ -26182,7 +26408,10 @@ function emitElementHtml(
26182
26408
  };
26183
26409
  const ic = makeIfCall(asIf, ctx, inlinedSubs, childNs, cssHash);
26184
26410
  ic.hostPath = path;
26411
+ ic.anchorPath = [...path, childIdx];
26185
26412
  ifCalls.push(ic);
26413
+ appendTemplatePart(html, '<!>', 'anchor');
26414
+ childIdx++;
26186
26415
  } else if (isKnownStringChildExpression(expr, ctx.knownStringChildLocals)) {
26187
26416
  bindings.push({
26188
26417
  id: bindings.length,
@@ -26277,7 +26506,7 @@ function makePortalCall(callNode, ctx, componentName, inlinedSubs, parentNs, css
26277
26506
  bodyArg,
26278
26507
  );
26279
26508
  } else {
26280
- bodyExpr = tsrxExprNode(bodyArg, ctx, componentName, inlinedSubs);
26509
+ bodyExpr = tsrxExprNode(rewriteJsxValues(bodyArg, ctx), ctx, componentName, inlinedSubs);
26281
26510
  }
26282
26511
  return {
26283
26512
  id: ctx.nextHelperId++,
@@ -26707,6 +26936,21 @@ function tagBindingName(node) {
26707
26936
  return name.type === 'Identifier' || name.type === 'JSXIdentifier' ? name.name : null;
26708
26937
  }
26709
26938
 
26939
+ // A marked module binding can be shadowed by a parameter, block, or directive
26940
+ // item. Keep the name-only fast path for all other tags; a candidate needs the
26941
+ // same authored-scope proof used for Activity, shared by client and SSR emits.
26942
+ function isDescriptorChildrenTag(node, ctx) {
26943
+ const name = node.openingElement?.name || node.id;
26944
+ if (
26945
+ (name?.type !== 'Identifier' && name?.type !== 'JSXIdentifier') ||
26946
+ !ctx.descriptorChildrenBindings?.has(name.name)
26947
+ ) {
26948
+ return false;
26949
+ }
26950
+ const binding = activityReferenceBinding(name, node, ctx);
26951
+ return binding?.scope === ctx.activityLexical?.rootScope;
26952
+ }
26953
+
26710
26954
  // React-style render-prop detection: if a component's children are exactly one
26711
26955
  // `{fn}` expression hole whose expression is a function (arrow or function
26712
26956
  // expression) — `<Comp>{(data) => <jsx/>}</Comp>` — return that function node so
@@ -27059,7 +27303,7 @@ function makeCompCall(
27059
27303
  const children = rewriteOpaqueTitles(sourceChildren, ctx, 'opaque');
27060
27304
  const childrenParentNs =
27061
27305
  !activityDescriptor && !isFragmentLongForm(node, ctx) ? 'opaque' : parentNs;
27062
- if (compName !== null && ctx.descriptorChildrenBindings?.has(compName)) {
27306
+ if (compName !== null && isDescriptorChildrenTag(node, ctx)) {
27063
27307
  const kids = children
27064
27308
  .map((child) => lowerInspectableJsxChild(child, ctx))
27065
27309
  .filter((child) => child != null);
@@ -27707,16 +27951,20 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
27707
27951
  const keyHelper = `_key$${ctx.nextHelperId++}`;
27708
27952
  const keyOrigin = node.loc ? node : keyFn.loc ? keyFn : ctx._moduleOrigin;
27709
27953
  const keyBody = keyFn.body;
27954
+ const keyCapturesParent = [...collectFreeIdentifiers(keyFn, new Set())].some((name) =>
27955
+ ctx.currentComponentLocals?.has(name),
27956
+ );
27710
27957
  const inlineKey =
27711
- node.nativeArrayMap !== undefined &&
27712
- mapCallbackHasEventClosure(node.nativeArrayMap.callback) &&
27713
- !isDestructured &&
27714
- !node.index &&
27715
- keyBody?.type === 'MemberExpression' &&
27716
- keyBody.computed !== true &&
27717
- keyBody.object?.type === 'Identifier' &&
27718
- keyBody.object.name === itemName &&
27719
- keyBody.property?.name === 'id';
27958
+ keyCapturesParent ||
27959
+ (node.nativeArrayMap !== undefined &&
27960
+ mapCallbackHasEventClosure(node.nativeArrayMap.callback) &&
27961
+ !isDestructured &&
27962
+ !node.index &&
27963
+ keyBody?.type === 'MemberExpression' &&
27964
+ keyBody.computed !== true &&
27965
+ keyBody.object?.type === 'Identifier' &&
27966
+ keyBody.object.name === itemName &&
27967
+ keyBody.property?.name === 'id');
27720
27968
  if (!inlineKey) {
27721
27969
  ctx.hoistedHelpers.push(inheritOriginLoc(b.const(keyHelper, keyFn), keyOrigin));
27722
27970
  }
@@ -28129,6 +28377,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28129
28377
  );
28130
28378
 
28131
28379
  const mapCall = node.nativeArrayMap || null;
28380
+ if (keyCapturesParent) autoMemoDeps = null;
28132
28381
  return {
28133
28382
  id: ctx.nextHelperId++,
28134
28383
  loc: devLoc(ctx, node),
@@ -28148,6 +28397,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28148
28397
  b.id(mapCall.receiverName),
28149
28398
  ])
28150
28399
  : node.right, // the fold replaces this with a `props.hN` member read
28400
+ missingMapKey: mapCall !== null && node.key == null,
28151
28401
  mapMethodExpr: mapCall
28152
28402
  ? mapCall.methodName === null
28153
28403
  ? b.member(b.id(mapCall.receiverName), 'map')