octane 0.2.2 → 0.2.3

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 (50) hide show
  1. package/dist/cjs/compatibility.cjs +35 -0
  2. package/dist/cjs/component-flags.cjs +8 -1
  3. package/dist/cjs/dom-tables.cjs +2 -0
  4. package/dist/cjs/error-codes.client.generated.cjs +15 -2
  5. package/dist/cjs/error-codes.server.generated.cjs +5 -0
  6. package/dist/cjs/event-names.cjs +39 -0
  7. package/dist/cjs/index.cjs +13 -0
  8. package/dist/cjs/runtime.cjs +1223 -332
  9. package/dist/cjs/runtime.server.cjs +362 -120
  10. package/dist/cjs/server/index.cjs +22 -0
  11. package/dist/cjs/version.cjs +1 -1
  12. package/dist/compatibility.d.ts +7 -0
  13. package/dist/compatibility.js +10 -0
  14. package/dist/compiler/bundler.js +13 -30
  15. package/dist/compiler/compile-renderer-boundaries.js +1 -0
  16. package/dist/compiler/compile-universal.js +3 -0
  17. package/dist/compiler/compile.js +619 -99
  18. package/dist/compiler/hook-deps.js +100 -3
  19. package/dist/compiler/hook-names.js +1 -0
  20. package/dist/compiler/manual-hooks.js +171 -0
  21. package/dist/compiler/plain-hook-memo.js +54 -5
  22. package/dist/compiler/slot-hooks.d.ts +43 -0
  23. package/dist/compiler/slot-hooks.js +154 -35
  24. package/dist/component-flags.d.ts +1 -1
  25. package/dist/component-flags.js +8 -1
  26. package/dist/dom-tables.js +2 -0
  27. package/dist/error-codes.client.generated.d.ts +3 -1
  28. package/dist/error-codes.client.generated.js +15 -2
  29. package/dist/error-codes.server.generated.d.ts +1 -0
  30. package/dist/error-codes.server.generated.js +5 -0
  31. package/dist/event-names.d.ts +1 -0
  32. package/dist/event-names.js +15 -0
  33. package/dist/html-tree-validation.d.ts +17 -34
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.js +12 -1
  36. package/dist/jsx-runtime.d.ts +6 -1
  37. package/dist/public-types.d.ts +188 -0
  38. package/dist/public-types.js +0 -0
  39. package/dist/runtime.d.ts +109 -66
  40. package/dist/runtime.js +1220 -332
  41. package/dist/runtime.server.d.ts +39 -14
  42. package/dist/runtime.server.js +356 -120
  43. package/dist/server/index.d.ts +4 -2
  44. package/dist/server/index.js +19 -0
  45. package/dist/static/index.d.ts +2 -1
  46. package/dist/static/index.js +3 -1
  47. package/dist/universal-core.d.ts +8 -1
  48. package/dist/universal-core.js +95 -9
  49. package/dist/version.js +1 -1
  50. package/package.json +2 -1
@@ -39,6 +39,7 @@ import {
39
39
  strongHash,
40
40
  } from '@tsrx/core';
41
41
  import { parseModule } from '#octane/compiler-parser';
42
+ import { decodeHTMLStrict } from 'entities';
42
43
  import { print as esrapPrint } from 'esrap';
43
44
  import esrapTsx from 'esrap/languages/tsx';
44
45
  import { buildFatSegments } from './fat-segments.js';
@@ -106,7 +107,13 @@ import {
106
107
  cssStyleValue,
107
108
  hyphenateStyleName,
108
109
  } from '../dom-tables.js';
110
+ import { isDelegatedEventProp } from '../event-names.js';
109
111
  import { sanitizeURLAttribute } from '../sanitize-url.js';
112
+ import {
113
+ invalidHtmlNestingWithAncestor,
114
+ invalidHtmlNestingWithParent,
115
+ invalidHtmlTextNesting,
116
+ } from '../html-tree-validation.js';
110
117
 
111
118
  // --- Parser-AST immutability enforcement ------------------------------------
112
119
  //
@@ -622,7 +629,7 @@ const CONTROLLED_KIND_HELPERS = {
622
629
  // recorder (recordBakedAttrOrigins) derives the name/value offsets from that
623
630
  // shape arithmetically — keep the invariant when adding forms.
624
631
  function bakeStaticAttr(attrName, lv, tag, namespace = 'html') {
625
- if (lv == null) return '';
632
+ if (lv == null || attrName === 'innerText' || attrName === 'textContent') return '';
626
633
  const isCustom =
627
634
  (namespace === 'html' || namespace === 'opaque') && tag !== undefined && tag.includes('-');
628
635
  const lower = attrName.toLowerCase();
@@ -663,6 +670,8 @@ function bakeStaticAttr(attrName, lv, tag, namespace = 'html') {
663
670
  return lv ? ` ${lower}=""` : '';
664
671
  }
665
672
  if (typeof lv === 'string') {
673
+ if (!isCustom && (lower === 'rowspan' || lower === 'start') && Number.isNaN(Number(lv)))
674
+ return '';
666
675
  if (!isCustom && POSITIVE_NUMERIC_ATTR_PROPS.has(lower) && !(Number(lv) >= 1)) return '';
667
676
  if (
668
677
  !isCustom &&
@@ -1565,12 +1574,13 @@ function inheritSoleCompRoot(bodyNodes, ctx) {
1565
1574
  // Collect the LOCAL names bound to the octane boundary builtins (see
1566
1575
  // inheritSoleCompRoot) from a module's import declarations. Aliased imports
1567
1576
  // (`import { Suspense as S }`) are matched by their local binding.
1568
- function collectOctaneBoundaryNames(astBody) {
1577
+ function collectOctaneBoundaryNames(astBody, onlySuspense = false) {
1569
1578
  const names = new Set();
1570
1579
  for (const node of astBody) {
1571
1580
  if (node.type !== 'ImportDeclaration' || node.source.value !== 'octane') continue;
1572
1581
  for (const sp of node.specifiers || []) {
1573
1582
  const imported = sp.imported?.name;
1583
+ if (onlySuspense && imported !== 'Suspense') continue;
1574
1584
  if (
1575
1585
  (imported === 'Suspense' ||
1576
1586
  imported === 'ErrorBoundary' ||
@@ -4713,6 +4723,225 @@ function containsRenderCall(stmts, memoCtx = null) {
4713
4723
  return found;
4714
4724
  }
4715
4725
 
4726
+ // Built-in hooks whose render-time work cannot schedule an update for the
4727
+ // calling component. Their function arguments run later (commit, event, or
4728
+ // never), so those bodies are deferred like any other closure.
4729
+ const SETUP_PASSIVE_HOOKS = new Set([
4730
+ 'useCallback',
4731
+ 'useRef',
4732
+ 'useEffect',
4733
+ 'useLayoutEffect',
4734
+ 'useInsertionEffect',
4735
+ 'useImperativeHandle',
4736
+ 'useEffectEvent',
4737
+ 'useId',
4738
+ 'useDeferredValue',
4739
+ 'useTransition',
4740
+ 'useFormStatus',
4741
+ 'useActionState',
4742
+ 'useFormState',
4743
+ 'useContext',
4744
+ 'use',
4745
+ ]);
4746
+ // Built-in hooks that invoke a function argument synchronously during render
4747
+ // (a memo factory, a lazy initializer, a snapshot reader, an optimistic
4748
+ // reducer, a linked-state reconciler). Those bodies belong to setup.
4749
+ const SETUP_SYNC_FACTORY_HOOKS = new Set([
4750
+ 'useMemo',
4751
+ 'useState',
4752
+ 'useLinkedState',
4753
+ 'useReducer',
4754
+ 'useSyncExternalStore',
4755
+ 'useOptimistic',
4756
+ ]);
4757
+ // Standard globals that only project their arguments. A user-defined
4758
+ // `toString`/`valueOf`/`toJSON` that schedules a render violates the hook rules
4759
+ // in React and Octane alike, so these count as call-free setup.
4760
+ const SETUP_PURE_GLOBAL_CALLEES = new Set([
4761
+ 'String',
4762
+ 'Number',
4763
+ 'Boolean',
4764
+ 'BigInt',
4765
+ 'Symbol',
4766
+ 'parseInt',
4767
+ 'parseFloat',
4768
+ 'isNaN',
4769
+ 'isFinite',
4770
+ 'encodeURI',
4771
+ 'encodeURIComponent',
4772
+ 'decodeURI',
4773
+ 'decodeURIComponent',
4774
+ ]);
4775
+ // Namespace members that never invoke an argument. `Array.from`,
4776
+ // `Object.groupBy`, `Object.fromEntries`, and friends iterate or call user
4777
+ // code, so they stay unclassified. JSON's replacer and reviver positions are
4778
+ // checked like a hook factory (see walkInvokedArgument).
4779
+ const SETUP_PURE_NAMESPACE_MEMBERS = new Map([
4780
+ ['Math', null],
4781
+ [
4782
+ 'Number',
4783
+ new Set(['isFinite', 'isInteger', 'isNaN', 'isSafeInteger', 'parseFloat', 'parseInt']),
4784
+ ],
4785
+ ['Object', new Set(['entries', 'freeze', 'hasOwn', 'is', 'isFrozen', 'keys', 'values'])],
4786
+ ['Array', new Set(['isArray', 'of'])],
4787
+ ['Date', new Set(['now', 'parse', 'UTC'])],
4788
+ ['JSON', new Set(['parse', 'stringify'])],
4789
+ ]);
4790
+
4791
+ // Every binding name the module declares anywhere. A global namespace shadowed
4792
+ // by any local, parameter, or import is treated as an unknown callee.
4793
+ function collectModuleBoundNames(ast) {
4794
+ const names = new Set();
4795
+ const seen = new WeakSet();
4796
+ function walk(n) {
4797
+ if (!n || typeof n !== 'object') return;
4798
+ if (Array.isArray(n)) {
4799
+ for (const x of n) walk(x);
4800
+ return;
4801
+ }
4802
+ if (seen.has(n)) return;
4803
+ seen.add(n);
4804
+ switch (n.type) {
4805
+ case 'VariableDeclarator':
4806
+ collectBindings(n.id, names);
4807
+ break;
4808
+ case 'FunctionDeclaration':
4809
+ case 'FunctionExpression':
4810
+ case 'ArrowFunctionExpression':
4811
+ if (n.id) names.add(n.id.name);
4812
+ for (const param of n.params || []) collectBindings(param, names);
4813
+ break;
4814
+ case 'ClassDeclaration':
4815
+ case 'ClassExpression':
4816
+ if (n.id) names.add(n.id.name);
4817
+ break;
4818
+ case 'CatchClause':
4819
+ collectBindings(n.param, names);
4820
+ break;
4821
+ case 'ImportSpecifier':
4822
+ case 'ImportDefaultSpecifier':
4823
+ case 'ImportNamespaceSpecifier':
4824
+ if (n.local) names.add(n.local.name);
4825
+ break;
4826
+ }
4827
+ for (const key in n) {
4828
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4829
+ walk(n[key]);
4830
+ }
4831
+ }
4832
+ walk(ast);
4833
+ return names;
4834
+ }
4835
+
4836
+ /**
4837
+ * Can this setup schedule a render-phase update for its own component? A
4838
+ * setter, dispatcher, transition start, custom hook, or any other call the
4839
+ * compiler cannot classify may; the built-in hooks above and standard pure
4840
+ * globals cannot. Closures are deferred unless a built-in hook runs them
4841
+ * during render. This admits the setup checkpoint, an optimization over the
4842
+ * runtime's own child-initialization guards, so an unclassified callee fails
4843
+ * closed to emitting the checkpoint.
4844
+ */
4845
+ function setupCanScheduleSelfUpdate(stmts, ctx) {
4846
+ let found = false;
4847
+ const seen = new WeakSet();
4848
+ const bound = ctx._moduleBoundNames;
4849
+ const isGlobal = (name) => bound !== undefined && !bound.has(name);
4850
+ function walk(n, deep) {
4851
+ if (found || !n) return;
4852
+ if (Array.isArray(n)) {
4853
+ for (const x of n) walk(x, deep);
4854
+ return;
4855
+ }
4856
+ if (typeof n !== 'object') return;
4857
+ const t = n.type;
4858
+ if (!t) return;
4859
+ if (seen.has(n)) return;
4860
+ seen.add(n);
4861
+ if (
4862
+ !deep &&
4863
+ (t === 'ArrowFunctionExpression' || t === 'FunctionExpression' || t === 'FunctionDeclaration')
4864
+ ) {
4865
+ return;
4866
+ }
4867
+ if (t === 'CallExpression' || t === 'OptionalCallExpression') {
4868
+ const hook = n._octaneImportedHook;
4869
+ if (hook !== undefined && SETUP_PASSIVE_HOOKS.has(hook)) {
4870
+ walk(n.arguments, deep);
4871
+ return;
4872
+ }
4873
+ if (hook !== undefined && SETUP_SYNC_FACTORY_HOOKS.has(hook)) {
4874
+ for (const argument of n.arguments) walkInvokedArgument(argument);
4875
+ return;
4876
+ }
4877
+ const callee = n.callee;
4878
+ if (
4879
+ callee?.type === 'Identifier' &&
4880
+ n._octaneUnboundCallee === true &&
4881
+ SETUP_PURE_GLOBAL_CALLEES.has(callee.name)
4882
+ ) {
4883
+ walk(n.arguments, true);
4884
+ return;
4885
+ }
4886
+ if (
4887
+ callee?.type === 'MemberExpression' &&
4888
+ !callee.computed &&
4889
+ callee.object?.type === 'Identifier' &&
4890
+ callee.property?.type === 'Identifier' &&
4891
+ SETUP_PURE_NAMESPACE_MEMBERS.has(callee.object.name) &&
4892
+ isGlobal(callee.object.name)
4893
+ ) {
4894
+ const members = SETUP_PURE_NAMESPACE_MEMBERS.get(callee.object.name);
4895
+ if (members === null || members.has(callee.property.name)) {
4896
+ if (callee.object.name === 'JSON') {
4897
+ walk(n.arguments[0], true);
4898
+ for (let index = 1; index < n.arguments.length; index++)
4899
+ walkInvokedArgument(n.arguments[index]);
4900
+ } else walk(n.arguments, true);
4901
+ return;
4902
+ }
4903
+ }
4904
+ found = true;
4905
+ return;
4906
+ }
4907
+ if (t === 'NewExpression' || t === 'TaggedTemplateExpression') {
4908
+ found = true;
4909
+ return;
4910
+ }
4911
+ for (const key in n) {
4912
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4913
+ walk(n[key], deep);
4914
+ }
4915
+ }
4916
+ // A sync-factory hook may invoke any argument whose value can be a function.
4917
+ // Only shapes that visibly cannot be callable, or whose bodies are visible
4918
+ // here, keep the analysis open; a named factory, member, call result, or
4919
+ // conditional fails closed exactly like an unclassified call.
4920
+ function walkInvokedArgument(argument) {
4921
+ if (found || !argument) return;
4922
+ switch (argument.type) {
4923
+ case 'ArrowFunctionExpression':
4924
+ case 'FunctionExpression':
4925
+ case 'Literal':
4926
+ case 'TemplateLiteral':
4927
+ case 'BinaryExpression':
4928
+ case 'UnaryExpression':
4929
+ case 'UpdateExpression':
4930
+ walk(argument, true);
4931
+ return;
4932
+ case 'ArrayExpression':
4933
+ // Dependency and initial-value arrays are never invoked; their
4934
+ // elements are ordinary setup expressions.
4935
+ walk(argument.elements, true);
4936
+ return;
4937
+ default:
4938
+ found = true;
4939
+ }
4940
+ }
4941
+ for (const s of stmts) walk(s, false);
4942
+ return found;
4943
+ }
4944
+
4716
4945
  // A hook call is identified by naming convention — the same signal React and
4717
4946
  // React Compiler key on — plus the exact `unstable_` and `UNSTABLE_` staging
4718
4947
  // prefixes bindings expose (`unstable_useRouterState` in @octanejs/remix-router
@@ -8333,7 +8562,9 @@ function compileAuthored(source, filename, options, bundlerMetadata) {
8333
8562
  throw new Error(`Unknown compile mode "${mode}" — expected 'client' or 'server'.`);
8334
8563
  }
8335
8564
  const cleanFilename = cleanCompileFilename(filename);
8336
- const analyzedAst = parseModule(source, cleanFilename);
8565
+ const analyzedAst = markParserSensitiveHosts(
8566
+ normalizeAuthoredJsxLiterals(parseModule(source, cleanFilename)),
8567
+ );
8337
8568
  analyzeTsrx(analyzedAst, cleanFilename);
8338
8569
  adoptParserAst(analyzedAst);
8339
8570
  assertNativeReadDiagnostics(analyzedAst, source, cleanFilename, options);
@@ -9023,8 +9254,12 @@ function compileInternal(
9023
9254
  // that are ever assigned (`foo = bar`) are excluded: their identity is then
9024
9255
  // as mutable as a module `let`, which fails closed.
9025
9256
  ctx.moduleFunctionDeclarations = collectImmutableModuleFunctions(ast.body);
9257
+ // Global namespaces are only trusted by the setup checkpoint analysis when
9258
+ // no module binding shadows them (see setupCanScheduleSelfUpdate).
9259
+ ctx._moduleBoundNames = collectModuleBoundNames(ast);
9026
9260
  // M3 inherit-range exclusion set (see inheritSoleCompRoot).
9027
9261
  ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
9262
+ ctx._octaneSuspenseNames = collectOctaneBoundaryNames(ast.body, true);
9028
9263
  const productionEffects = !ctx.hmr && !ctx.dev && !ctx.profile;
9029
9264
  const exportedComponents = ast.body.filter(
9030
9265
  (statement) =>
@@ -10143,6 +10378,7 @@ function compileServer(
10143
10378
  // M3 inherit-range exclusion set — must match the client compile's
10144
10379
  // (see inheritSoleCompRoot; both modes read the same import declarations).
10145
10380
  ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
10381
+ ctx._octaneSuspenseNames = collectOctaneBoundaryNames(ast.body, true);
10146
10382
 
10147
10383
  // Style maps are module values and can be referenced by a component declared
10148
10384
  // before the map itself. Lower every top-level map before compiling any
@@ -10240,7 +10476,8 @@ function compileServer(
10240
10476
  },
10241
10477
  });
10242
10478
  } else {
10243
- bodyNodes.push(rewriteModuleJsxValues(node, ctx));
10479
+ const fnName = node.id?.name || node.declaration?.id?.name || 'module';
10480
+ bodyNodes.push(rewriteModuleJsxValues(rewriteHookCalls(node, ctx, fnName), ctx));
10244
10481
  }
10245
10482
  }
10246
10483
 
@@ -10366,7 +10603,10 @@ function compileServerComponent(node, ctx) {
10366
10603
  const warmStatement =
10367
10604
  warmNode === null
10368
10605
  ? null
10369
- : inheritOriginLoc(b.stmt(b.assignment('=', b.member(b.id(name), '__warm'), warmNode)), node);
10606
+ : inheritOriginLoc(
10607
+ b.stmt(b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), warmNode)),
10608
+ node,
10609
+ );
10370
10610
 
10371
10611
  // Mirror the client emission's hoisting rule (see compileComponent): a
10372
10612
  // component referenced ABOVE its declaration keeps real function-declaration
@@ -10382,7 +10622,7 @@ function compileServerComponent(node, ctx) {
10382
10622
  b.logical(
10383
10623
  '&&',
10384
10624
  b.binary('===', b.unary('typeof', b.id(name)), b.literal('function', "'function'")),
10385
- b.assignment('=', b.member(b.id(name), '__warm'), warmNode),
10625
+ b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), warmNode),
10386
10626
  ),
10387
10627
  ),
10388
10628
  node,
@@ -10432,6 +10672,7 @@ function ssrCompileBody(
10432
10672
  componentNs = null,
10433
10673
  returnedFragmentTemplate = false,
10434
10674
  returnedFragmentRoot = false,
10675
+ unboxedHtml = false,
10435
10676
  ) {
10436
10677
  const prevMapTemps = ctx.currentMapTemps;
10437
10678
  ctx.currentMapTemps = [];
@@ -10447,6 +10688,7 @@ function ssrCompileBody(
10447
10688
  componentNs,
10448
10689
  returnedFragmentTemplate,
10449
10690
  returnedFragmentRoot,
10691
+ unboxedHtml,
10450
10692
  );
10451
10693
  } finally {
10452
10694
  ctx.currentMapTemps = prevMapTemps;
@@ -10464,6 +10706,7 @@ function ssrCompileBodyWithMapTemps(
10464
10706
  componentNs,
10465
10707
  returnedFragmentTemplate,
10466
10708
  returnedFragmentRoot,
10709
+ unboxedHtml,
10467
10710
  ) {
10468
10711
  const returnedOutput = node.body?.type === 'JSXCodeBlock' && hasOwnValueReturn(node);
10469
10712
  const mapTemps = ctx.currentMapTemps;
@@ -10685,7 +10928,12 @@ function ssrCompileBodyWithMapTemps(
10685
10928
  node.params.length > 0
10686
10929
  ? [...node.params, b.id('__s'), b.id('__extra')]
10687
10930
  : [b.id('__props'), b.id('__s'), b.id('__extra')];
10688
- body.push(b.return(htmlExpr));
10931
+ // Private loop items feed only HTML concatenation, so their serialized tail
10932
+ // needs no carrier. Other bodies can cross a component/value boundary, where
10933
+ // the carrier distinguishes compiled HTML from authored text returns. This
10934
+ // flag belongs to this body only; nested callbacks keep their own contract.
10935
+ if (!unboxedHtml) ctx.runtimeNeeded.add('ssrHtml');
10936
+ body.push(b.return(unboxedHtml ? htmlExpr : ssrCall('ssrHtml', [htmlExpr], node)));
10689
10937
  const origin =
10690
10938
  node.loc != null
10691
10939
  ? node
@@ -11022,14 +11270,15 @@ function ssrEmitNode(
11022
11270
  ctx.runtimeNeeded.add(fn);
11023
11271
  return ssrCall(fn, [resolveStyleExpr(rewriteHookCalls(expr, ctx, name), cssHash)], node);
11024
11272
  }
11025
- ctx.runtimeNeeded.add('ssrChild');
11026
11273
  // rewriteJsxValues lowers any JSX embedded in the expression (e.g.
11027
11274
  // `{cond && <div/>}`, a ternary, a `.map(x => <Row/>)`) to printable
11028
11275
  // createElement(...) descriptors — exactly like ssrEmitTsrxExpression and
11029
11276
  // the client makeChildCall. Without it the raw JSX leaks into the emitted
11030
11277
  // ssrChild(...) call as unparseable source.
11278
+ const childHelper = nlGuard ? 'ssrChildPre' : 'ssrChild';
11279
+ ctx.runtimeNeeded.add(childHelper);
11031
11280
  const childExpr = ssrCall(
11032
- 'ssrChild',
11281
+ childHelper,
11033
11282
  [
11034
11283
  resolveStyleExpr(rewriteJsxValues(rewriteHookCalls(expr, ctx, name), ctx), cssHash),
11035
11284
  b.id('__s'),
@@ -11901,9 +12150,11 @@ function ssrEmitElement(node, ctx, name, inlinedSubs, parentNs, cssHash, compone
11901
12150
  staticTextLiteral(onlyChild0.expression) === null &&
11902
12151
  !isKnownStringChildExpression(onlyChild0.expression, ctx.knownStringChildLocals)
11903
12152
  ) {
11904
- ctx.runtimeNeeded.add('ssrChildText');
12153
+ const childHelper =
12154
+ tag === 'pre' || tag === 'textarea' || tag === 'listing' ? 'ssrChildTextPre' : 'ssrChildText';
12155
+ ctx.runtimeNeeded.add(childHelper);
11905
12156
  childrenExpr = ssrCall(
11906
- 'ssrChildText',
12157
+ childHelper,
11907
12158
  [
11908
12159
  resolveStyleExpr(
11909
12160
  rewriteJsxValues(rewriteHookCalls(onlyChild0.expression, ctx, name), ctx),
@@ -12394,6 +12645,7 @@ function ssrCompileSub(
12394
12645
  componentNs,
12395
12646
  returnedFragmentTemplate = false,
12396
12647
  returnedFragmentRoot = false,
12648
+ unboxedHtml = false,
12397
12649
  ) {
12398
12650
  const fnName = `${baseName}$${ctx.nextHelperId++}`;
12399
12651
  const synth = { params: paramNodes || [], body: bodyStmts };
@@ -12422,6 +12674,7 @@ function ssrCompileSub(
12422
12674
  componentNs,
12423
12675
  returnedFragmentTemplate,
12424
12676
  returnedFragmentRoot,
12677
+ unboxedHtml,
12425
12678
  );
12426
12679
  } finally {
12427
12680
  ctx._ssrFoldedExprHoles = prevFoldedExprHoles;
@@ -12584,6 +12837,9 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
12584
12837
  cssHash,
12585
12838
  parentNs,
12586
12839
  componentNs,
12840
+ false,
12841
+ false,
12842
+ true,
12587
12843
  );
12588
12844
  inlinedSubs.push(itemSub.fn);
12589
12845
  let emptyCall = ssrHtmlTemplate([], node, ctx);
@@ -13855,7 +14111,9 @@ function compileComponent(node, ctx, options) {
13855
14111
  : fnNode,
13856
14112
  ];
13857
14113
  if (pendingWarm) {
13858
- statements.push(guarded(objectAssign(b.id(name), '__warm', pendingWarm)));
14114
+ statements.push(
14115
+ guarded(b.call(requireRuntimeForContext(ctx, 'markWarm'), b.id(name), pendingWarm)),
14116
+ );
13859
14117
  }
13860
14118
  if (hmrWrap && returnedOutput) {
13861
14119
  statements.push(
@@ -13890,7 +14148,12 @@ function compileComponent(node, ctx, options) {
13890
14148
  // identity by naming the inner FunctionExpression.
13891
14149
  const fnExpr = functionExpressionFromDeclaration(fnNode);
13892
14150
  const warmedFn = pendingWarm
13893
- ? markPure(inheritOriginLoc(objectAssign(fnExpr, '__warm', pendingWarm), node))
14151
+ ? markPure(
14152
+ inheritOriginLoc(
14153
+ b.call(requireRuntimeForContext(ctx, 'markWarm'), fnExpr, pendingWarm),
14154
+ node,
14155
+ ),
14156
+ )
13894
14157
  : fnExpr;
13895
14158
  const abiFn =
13896
14159
  hmrWrap && returnedOutput
@@ -14370,6 +14633,32 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
14370
14633
  bodyStatements.push(...mapTemps.map((temp) => inheritOriginLoc(b.let(temp), node)));
14371
14634
  }
14372
14635
  bodyStatements.push(...rewrittenStatements);
14636
+ // A self-update scheduled by setup discards this output. The owning Block
14637
+ // replays setup before returning to its parent, so children initialize from
14638
+ // the settled state and no template/resource work escapes the first pass.
14639
+ // Setup that only reads built-in hooks and pure globals cannot schedule an
14640
+ // update; keep those bodies unchanged (see setupCanScheduleSelfUpdate).
14641
+ if (
14642
+ ctx.mode !== 'server' &&
14643
+ ctx._universalRuntimeUnit == null &&
14644
+ !returnedOutput &&
14645
+ setupCanScheduleSelfUpdate(statements, ctx)
14646
+ ) {
14647
+ bodyStatements.push(
14648
+ inheritOriginLoc(
14649
+ b.if(
14650
+ b.logical(
14651
+ '&&',
14652
+ b.member(b.member(b.id('__s'), 'block'), 'pending'),
14653
+ b.unary('!', b.member(b.member(b.id('__s'), 'block'), 'crossRenderUpdate')),
14654
+ ),
14655
+ b.return(null),
14656
+ null,
14657
+ ),
14658
+ node,
14659
+ ),
14660
+ );
14661
+ }
14373
14662
  // GLOBAL resource registrations (stylesheet precedence links, style
14374
14663
  // resources, async scripts) run here: after setup, so an href computed from
14375
14664
  // a body local is initialized, and BEFORE the constructs below mount any
@@ -14564,6 +14853,11 @@ function transformUniversalParallelUse(ast, ctx, metadata) {
14564
14853
  node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression';
14565
14854
  const unwrapAssignedFunction = (node) => {
14566
14855
  if (isFunction(node)) return node;
14856
+ if (
14857
+ calleeName(node) === runtimeAliasForContext(ctx, 'markWarm') &&
14858
+ isFunction(node.arguments?.[0])
14859
+ )
14860
+ return node.arguments[0];
14567
14861
  if (
14568
14862
  node?.type === 'CallExpression' &&
14569
14863
  node.callee?.type === 'MemberExpression' &&
@@ -14696,14 +14990,7 @@ function transformUniversalParallelUse(ast, ctx, metadata) {
14696
14990
  };
14697
14991
  // The warm-plan attach maps to the component function it decorates.
14698
14992
  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
- );
14993
+ inheritOriginLoc(b.call(requireRuntimeForContext(ctx, 'markWarm'), fn, warmNode), fn);
14707
14994
 
14708
14995
  const transformFunction = (fn, name, component) => {
14709
14996
  if (!isFunction(fn) || transformed.has(fn)) return fn;
@@ -15764,7 +16051,11 @@ function parallelUseWalkJsx(nodes, ctx, componentName, creations, warmChildren,
15764
16051
  const nameNode = node.openingElement && node.openingElement.name;
15765
16052
  const isComponent =
15766
16053
  nameNode && nameNode.type === 'JSXIdentifier' && /^[A-Z]/.test(nameNode.name);
15767
- if (isComponent) {
16054
+ const transparentBoundary =
16055
+ isComponent &&
16056
+ ctx._octaneSuspenseNames?.has(nameNode.name) &&
16057
+ !ctx.currentComponentLocals?.has(nameNode.name);
16058
+ if (isComponent && !transparentBoundary) {
15768
16059
  // Server: creations flowing INTO child props (`<Kid p={make(x)}/>`)
15769
16060
  // need the same cross-pass identity as use()-site creations — every
15770
16061
  // streaming wave re-runs this body, and a recreated per-request
@@ -16424,7 +16715,7 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
16424
16715
  // The hoisted fetch plan: creations + child calls, params destructured
16425
16716
  // from the incoming props object. Single-param components only (the norm).
16426
16717
  // Returned as a bare arrow — compileComponent attaches it to the INNER
16427
- // function object via Object.assign, so the component's own body (where
16718
+ // function object via markWarm, so the component's own body (where
16428
16719
  // the function-expression name shadows the module const) sees it too;
16429
16720
  // hmr() forwards it from the wrapped fn onto the HMR wrapper.
16430
16721
  let warmNode = null;
@@ -16644,13 +16935,17 @@ const NUMERIC_HOOK_SLOT_POSITION = {
16644
16935
  useEffectEvent: 1,
16645
16936
  useImperativeHandle: 3,
16646
16937
  useActionState: 3,
16938
+ useFormState: 3,
16647
16939
  useOptimistic: 2,
16648
16940
  useSignal$: 1,
16649
16941
  };
16650
16942
 
16651
16943
  function appendHookSlotArgument(name, args, slot, numeric, origin) {
16652
16944
  const out = [...args];
16653
- const position = numeric ? NUMERIC_HOOK_SLOT_POSITION[name] : undefined;
16945
+ const position =
16946
+ numeric || name === 'useState' || name === 'useRef'
16947
+ ? NUMERIC_HOOK_SLOT_POSITION[name]
16948
+ : undefined;
16654
16949
  if (position !== undefined) {
16655
16950
  while (out.length < position) out.push(b.id('undefined', origin));
16656
16951
  }
@@ -16658,6 +16953,18 @@ function appendHookSlotArgument(name, args, slot, numeric, origin) {
16658
16953
  return out;
16659
16954
  }
16660
16955
 
16956
+ function wrapHookCallWithSlot(call, ctx, slot, callee, args) {
16957
+ const fn = call.typeArguments
16958
+ ? { type: 'TSInstantiationExpression', expression: callee, typeArguments: call.typeArguments }
16959
+ : callee;
16960
+ return {
16961
+ ...call,
16962
+ callee: b.id(requireRuntimeForContext(ctx, 'withSlot')),
16963
+ typeArguments: null,
16964
+ arguments: [typeof slot === 'string' ? b.id(slot) : slot, fn, ...args],
16965
+ };
16966
+ }
16967
+
16661
16968
  function arrayPatternObservesStateGetter(pattern) {
16662
16969
  const elements = pattern.elements || [];
16663
16970
  if (elements[2] != null) return true;
@@ -17308,20 +17615,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17308
17615
  const importedName = n._octaneImportedHook;
17309
17616
  const hookRuntimeImportedName = n._octaneHookRuntimeImportedHook;
17310
17617
  const name = importedName ?? hookRuntimeImportedName ?? localName;
17311
- // Three kinds of call get a trailing per-call-site hook slot:
17618
+ // Three kinds of call receive a per-call-site hook identity:
17312
17619
  // - a built-in base hook (HOOK_NAMES) — also needs its runtime import;
17313
17620
  // - a custom / library hook by React's `use[A-Z]` convention — e.g. a
17314
17621
  // `useStore` binding from @octanejs/zustand that WRAPS a base hook and
17315
- // FORWARDS the slot to it. `useContext` is keyed by context identity (no
17622
+ // resolves the enclosing call path. `useContext` is keyed by context identity (no
17316
17623
  // slot) and `use` has no uppercase suffix, so both are excluded here. We
17317
17624
  // do NOT import custom hooks — they're user/library imports;
17318
17625
  // - a server-mode `use(thenable)` — a stable suspense-cache key.
17319
17626
  // Distinct call sites get distinct slots, so `useStore(a)`/`useStore(b)`
17320
17627
  // (or the same hook twice) stay independent.
17321
17628
  //
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).
17629
+ // A `use*` name is reserved for hooks. The custom wrapper preserves its
17630
+ // authored argument list; inside a plain JS loop the convention is
17631
+ // enforced by rejectHookInJsLoop.
17325
17632
  const isBuiltin =
17326
17633
  (HOOK_NAMES.has(name) ||
17327
17634
  (ctx.nativeReads &&
@@ -17338,10 +17645,9 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17338
17645
  const inline = lowerLocalMemoExpression(n, name);
17339
17646
  if (inline !== null) return inline;
17340
17647
  }
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.
17648
+ // Keep a Symbol on custom-hook call paths so their internal base-hook
17649
+ // identities compose across modules. Proven render-scope base hooks
17650
+ // can use the smaller numeric production ABI.
17345
17651
  const hasSpread = n.arguments.some((arg) => arg.type === 'SpreadElement');
17346
17652
  const memoRoute = isBuiltin && canLowerClientMemo(n, name);
17347
17653
  const memoCandidate = memoRoute ? slotMemoCandidate(n, name) : null;
@@ -17406,18 +17712,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17406
17712
  // separately must not re-promote its nested hooks to render-local.
17407
17713
  const argsLocalRoot = localSlotMarks.get(n) === true;
17408
17714
  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
- }
17715
+ const callee = nativeMemo
17716
+ ? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
17717
+ : getterHelper !== null
17718
+ ? b.id(runtimeAliasForContext(ctx, getterHelper))
17719
+ : n.callee._octaneGenerated
17720
+ ? b.id(runtimeAliasForContext(ctx, name))
17721
+ : n.callee;
17722
+ // The call path supplies identity without adding an argument that
17723
+ // changes a custom hook's defaults, rest values or arguments.length.
17724
+ // State/ref spreads also need this path: an empty spread has no
17725
+ // initializer position into which a trailing slot may safely fall.
17726
+ if (isCustom || (hasSpread && (name === 'useState' || name === 'useRef')))
17727
+ return wrapHookCallWithSlot(n, ctx, slot, callee, args);
17728
+
17421
17729
  const hookArgs = explicitMemoSlot
17422
17730
  ? args
17423
17731
  : appendHookSlotArgument(name, args, slot, numericSlot, n);
@@ -17432,13 +17740,7 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17432
17740
  return {
17433
17741
  ...n,
17434
17742
  ...(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,
17743
+ callee,
17442
17744
  arguments: hookArgs,
17443
17745
  };
17444
17746
  }
@@ -17505,14 +17807,20 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17505
17807
  const args = n.arguments.map((a) =>
17506
17808
  rewriteHookCalls(a, ctx, componentName, localSlotMarks.get(n) === true),
17507
17809
  );
17810
+ const callee = nativeMemo
17811
+ ? b.id(runtimeAliasForContext(ctx, 'nativePuMemo'))
17812
+ : getterHelper !== null
17813
+ ? b.id(runtimeAliasForContext(ctx, getterHelper))
17814
+ : n.callee;
17815
+ if (
17816
+ (name === 'useState' || name === 'useRef') &&
17817
+ args.some((arg) => arg.type === 'SpreadElement')
17818
+ )
17819
+ return wrapHookCallWithSlot(n, ctx, slot, callee, args);
17508
17820
  return {
17509
17821
  ...n,
17510
17822
  ...(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,
17823
+ callee,
17516
17824
  arguments: explicitMemoSlot
17517
17825
  ? args
17518
17826
  : appendHookSlotArgument(name, args, slot, numericSlot, n),
@@ -17524,9 +17832,8 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17524
17832
  // and the like). Same `use[A-Z]` convention, applied to the PROPERTY name.
17525
17833
  // The callee is a member access, so the plain withSlot form would sever
17526
17834
  // `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).
17835
+ // `_$withSlot(sym, () => obj.useX(...args))` preserves the receiver and
17836
+ // authored arguments while supplying the inner base hooks' call path.
17530
17837
  if (
17531
17838
  n.type === 'CallExpression' &&
17532
17839
  !n.optional &&
@@ -17558,7 +17865,7 @@ function rewriteHookCalls(node, ctx, componentName, localRoot = false) {
17558
17865
  return b.call(
17559
17866
  withSlotAlias,
17560
17867
  b.id(symVar),
17561
- b.arrow([], b.call({ ...n.callee, object }, ...args, b.id(symVar))),
17868
+ b.arrow([], { ...n, callee: { ...n.callee, object }, arguments: args }),
17562
17869
  );
17563
17870
  }
17564
17871
  return null;
@@ -19217,6 +19524,20 @@ function rewriteJsxValues(node, ctx, eagerMapCallbackRoots = false, eagerMapCall
19217
19524
  if (lower != null) node = lowerSetupValueDirectives(node, lower);
19218
19525
  return mapAst(node, (n) => {
19219
19526
  const t = n && n.type;
19527
+ if (isFunctionNode(n) && n.body?.type === 'JSXCodeBlock') {
19528
+ const name = n.id?.name ?? allocCompilerName(ctx, '__template');
19529
+ const previousLocals = ctx.currentComponentLocals;
19530
+ ctx.currentComponentLocals = collectComponentLocals(n);
19531
+ try {
19532
+ const compiled =
19533
+ ctx.mode === 'server'
19534
+ ? ssrCompileBody(n, ctx, name, null, [], 'opaque')
19535
+ : compileFunctionBody(n, ctx, name, 'opaque');
19536
+ return functionExpressionFromDeclaration({ ...compiled, id: n.id ?? null }, n);
19537
+ } finally {
19538
+ ctx.currentComponentLocals = previousLocals;
19539
+ }
19540
+ }
19220
19541
  if (
19221
19542
  ctx.nativeReads &&
19222
19543
  lower == null &&
@@ -20067,11 +20388,6 @@ function jsxTagName(n) {
20067
20388
  : null;
20068
20389
  }
20069
20390
 
20070
- /** @param {any} n @returns {boolean} */
20071
- function isHeadElementNode(n) {
20072
- return Boolean(n && n.type === 'JSXElement' && jsxTagName(n) === 'head');
20073
- }
20074
-
20075
20391
  /** @param {any} n @returns {boolean} */
20076
20392
  function isHoistableHeadElementNode(n) {
20077
20393
  if (n == null || (n.type !== 'JSXElement' && n.type !== 'Element')) return false;
@@ -20111,7 +20427,7 @@ function isHoistableHeadElementNode(n) {
20111
20427
  inner.value === 'stylesheet';
20112
20428
  }
20113
20429
  }
20114
- if (tag === 'link' && relStylesheet && hasPrecedence && (!hasHref || emptyHref)) return false;
20430
+ if (tag === 'link' && relStylesheet && (!hasPrecedence || !hasHref || emptyHref)) return false;
20115
20431
  return true;
20116
20432
  }
20117
20433
 
@@ -20329,7 +20645,7 @@ function deferredHeadAttrs(el) {
20329
20645
  // props.children under HTML, SVG, or MathML. namespaceHead resolves that choice
20330
20646
  // from its actual parent at render time while retaining ordinary component-block
20331
20647
  // hydration and keyed ownership.
20332
- function deferredTitleElement(el, ctx) {
20648
+ function deferredTitleElement(el, ctx, tag = 'title') {
20333
20649
  ctx.runtimeNeeded.add('namespaceHeadElement');
20334
20650
  let authoredKey = null;
20335
20651
  for (const attr of el.openingElement.attributes || []) {
@@ -20345,7 +20661,7 @@ function deferredTitleElement(el, ctx) {
20345
20661
  }
20346
20662
  const args = [
20347
20663
  b.literal(headKey(el, 0, ctx)),
20348
- b.literal('title'),
20664
+ b.literal(tag),
20349
20665
  deferredHeadAttrs(el),
20350
20666
  headTextExpression(el),
20351
20667
  ];
@@ -20658,9 +20974,137 @@ function emitHeadServer(headNodes, ctx) {
20658
20974
  });
20659
20975
  }
20660
20976
 
20977
+ // JSX indentation is syntax, while same-line spaces are rendered content. Cook
20978
+ // authored literals once, before either emitter sees them, so entities cannot be
20979
+ // decoded twice as a tree passes through fragment and value lowering.
20980
+ function normalizeAuthoredJsxLiterals(ast) {
20981
+ return mapAst(ast, (node) => {
20982
+ if (node.type === 'JSXText') {
20983
+ const lines = node.value.split(/\r\n|\n|\r/);
20984
+ let last = 0;
20985
+ for (let i = 0; i < lines.length; i++) if (/[^ \t]/.test(lines[i])) last = i;
20986
+ let value = '';
20987
+ for (let i = 0; i < lines.length; i++) {
20988
+ let line = lines[i].replace(/\t/g, ' ');
20989
+ if (i !== 0) line = line.replace(/^ +/, '');
20990
+ if (i !== lines.length - 1) line = line.replace(/ +$/, '');
20991
+ if (line !== '') value += line + (i !== last ? ' ' : '');
20992
+ }
20993
+ if (value.includes('&')) value = decodeHTMLStrict(value);
20994
+ return value === node.value ? node : { ...node, value, raw: value };
20995
+ }
20996
+ if (
20997
+ node.type === 'JSXAttribute' &&
20998
+ node.value?.type === 'Literal' &&
20999
+ typeof node.value.value === 'string' &&
21000
+ node.value.value.includes('&')
21001
+ ) {
21002
+ const value = decodeHTMLStrict(node.value.value);
21003
+ return { ...node, value: { ...node.value, value, raw: JSON.stringify(value) } };
21004
+ }
21005
+ if (
21006
+ node.type === 'JSXStyleElement' &&
21007
+ (hasJsxAttribute(node, 'dangerouslySetInnerHTML') || hasJsxAttribute(node, 'children'))
21008
+ ) {
21009
+ return normalizeAuthoredJsxLiterals(
21010
+ b.jsx_element(node, node.openingElement.attributes || [], []),
21011
+ );
21012
+ }
21013
+ return null;
21014
+ });
21015
+ }
21016
+
21017
+ function requiresImperativeHostTree(root, ctx) {
21018
+ const tag = jsxTagName(root) || elementTagName(root);
21019
+ return (
21020
+ (ctx.mode !== 'server' && root._octaneImperativeHost === true) ||
21021
+ hasJsxAttribute(root, 'key') ||
21022
+ tag === 'noscript' ||
21023
+ tag === 'html' ||
21024
+ tag === 'head' ||
21025
+ tag === 'body'
21026
+ );
21027
+ }
21028
+
21029
+ // Analyze parser repair once per authored tree. Mark only the affected ancestor,
21030
+ // preserving template compilation for unrelated hosts around that subtree.
21031
+ function markParserSensitiveHosts(ast) {
21032
+ const unsafe = new WeakSet();
21033
+ let found = false;
21034
+ const mark = (node) => {
21035
+ unsafe.add(node);
21036
+ found = true;
21037
+ };
21038
+ function visit(node, ancestors = [], namespace = 'html') {
21039
+ if (node === null || typeof node !== 'object') return;
21040
+ if (Array.isArray(node)) {
21041
+ for (const child of node) visit(child, ancestors, namespace);
21042
+ return;
21043
+ }
21044
+ if (node.type === 'JSXText') {
21045
+ if (ancestors.length && invalidHtmlTextNesting(node.value, ancestors[0].tag) !== null)
21046
+ mark(ancestors[0].node);
21047
+ return;
21048
+ }
21049
+ if (node.type === 'JSXFragment') {
21050
+ visit(node.children || [], ancestors, namespace);
21051
+ return;
21052
+ }
21053
+ if (node.type === 'JSXElement') {
21054
+ const tag = jsxTagName(node);
21055
+ if (isComponentTag(node)) {
21056
+ visit(node.children || []);
21057
+ return;
21058
+ }
21059
+ const self = nsForSelf(tag, namespace);
21060
+ if (self === 'html' && ancestors.length) {
21061
+ if (invalidHtmlNestingWithParent(tag, ancestors[0].tag) !== null) mark(ancestors[0].node);
21062
+ const tags = ancestors.map((entry) => entry.tag);
21063
+ for (let i = 1; i < ancestors.length; i++) {
21064
+ if (invalidHtmlNestingWithAncestor(tag, tags.slice(0, i + 1)) !== null)
21065
+ mark(ancestors[i].node);
21066
+ }
21067
+ }
21068
+ const childNamespace = nsForChildren(tag, namespace);
21069
+ visit(
21070
+ node.children || [],
21071
+ childNamespace === 'html' ? [{ node, tag }, ...ancestors] : [],
21072
+ childNamespace,
21073
+ );
21074
+ visit(node.openingElement?.attributes || []);
21075
+ return;
21076
+ }
21077
+ // A value expression or directive owns its insertion range. Its elements
21078
+ // are parsed separately, so lexical host ancestry does not cross it.
21079
+ for (const key in node) {
21080
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata') continue;
21081
+ visit(node[key]);
21082
+ }
21083
+ }
21084
+ visit(ast);
21085
+ return found
21086
+ ? mapAst(ast, (node) => (unsafe.has(node) ? { ...node, _octaneImperativeHost: true } : null))
21087
+ : ast;
21088
+ }
21089
+
21090
+ function rewriteImperativeHeadElements(node, ctx, namespace = 'html', inNoscript = false) {
21091
+ if (node?.type !== 'JSXElement' && node?.type !== 'JSXFragment') return node;
21092
+ const tag = jsxTagName(node);
21093
+ const nextNamespace = tag ? nsForChildren(tag, namespace) : namespace;
21094
+ if (!inNoscript && namespace === 'html' && isHoistableHeadElementNode(node)) {
21095
+ return deferredTitleElement(node, ctx, tag);
21096
+ }
21097
+ return {
21098
+ ...node,
21099
+ children: (node.children || []).map((child) =>
21100
+ rewriteImperativeHeadElements(child, ctx, nextNamespace, inNoscript || tag === 'noscript'),
21101
+ ),
21102
+ };
21103
+ }
21104
+
20661
21105
  /**
20662
21106
  * Normalize a list of JSX child nodes into the shapes the emitters consume:
20663
- * - Whitespace-only JSXText / JSX comments (`{…}` empty containers) → dropped
21107
+ * - Indentation-only JSXText / JSX comments (`{…}` empty containers) → dropped
20664
21108
  * - JSXText with content → `Text` node wrapping a string Literal
20665
21109
  * - `{expr}` container → `Text` hole, or `TSRXExpression` when the
20666
21110
  * expression needs the rich dispatcher (portals, JSX ternaries,
@@ -20670,7 +21114,7 @@ function emitHeadServer(headNodes, ctx) {
20670
21114
  * - JSXElement → `Element` (with `<Fragment ref>` expanded to a
20671
21115
  * FragmentStart/…/FragmentEnd sequence, `<Activity>` lowered to an
20672
21116
  * ActivityStatement, eligible `<title>/<meta>/<link>` hoisted as `HeadHoist`,
20673
- * and `<head>` rejected)
21117
+ * and parser-sensitive host trees lowered as descriptors)
20674
21118
  * - Fragments (`<>…</>`, Tsx/Tsrx) → flattened (children inlined)
20675
21119
  * - JSXStyleElement → dropped (its CSS is handled by the scoping pipeline)
20676
21120
  * - Directive expressions (@if/@for/@try/@switch, `@{…}` child blocks) →
@@ -20681,13 +21125,19 @@ function emitHeadServer(headNodes, ctx) {
20681
21125
  // `inSvg`: the children being normalized sit inside an SVG-namespace subtree.
20682
21126
  // SVG has its own `<title>` (the accessibility tooltip element) — it must stay
20683
21127
  // where it is, NOT hoist to document.head (React 19 makes the same exception).
20684
- function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false) {
21128
+ function normalizeChildren(
21129
+ nodes,
21130
+ inSvg = false,
21131
+ ctx = null,
21132
+ inNoscript = false,
21133
+ allowImperative = true,
21134
+ ) {
20685
21135
  const out = [];
20686
21136
  if (!nodes) return out;
20687
21137
  for (const n of nodes) {
20688
21138
  if (!n) continue;
20689
21139
  if (n.type === 'JSXText') {
20690
- if (/^\s*$/.test(n.value)) continue;
21140
+ if (n.value === '' || (/^\s*$/.test(n.value) && /[\n\r]/.test(n.value))) continue;
20691
21141
  // The Text node and its literal map to the authored JSX text.
20692
21142
  out.push(
20693
21143
  inheritOriginLoc(
@@ -20788,14 +21238,29 @@ function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false)
20788
21238
  out.push({ type: 'ActivityStatement', mode, children: n.children || [] });
20789
21239
  continue;
20790
21240
  }
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.',
21241
+ // Keys need a reconciliation boundary. Parser-sensitive host trees need
21242
+ // imperative construction so HTML repair cannot change binding paths.
21243
+ // The shared descriptor path provides both without taxing ordinary templates.
21244
+ if (
21245
+ ctx &&
21246
+ allowImperative &&
21247
+ !isComponentTag(n) &&
21248
+ (hasJsxAttribute(n, 'key') || (!inSvg && requiresImperativeHostTree(n, ctx)))
21249
+ ) {
21250
+ out.push(
21251
+ inheritOriginLoc(
21252
+ {
21253
+ type: 'Text',
21254
+ expression: jsxElementToCreateElement(
21255
+ rewriteImperativeHeadElements(n, ctx, inSvg ? 'svg' : 'html'),
21256
+ ctx,
21257
+ ),
21258
+ imperativeRoot: n,
21259
+ },
21260
+ n,
21261
+ ),
20798
21262
  );
21263
+ continue;
20799
21264
  }
20800
21265
  // `<title>`/`<meta>`/eligible `<link>` → hoist to the document-head channel (NOT
20801
21266
  // body DOM). Kept in `out` as a synthetic node so planJsx / ssrCompileBody
@@ -20843,6 +21308,18 @@ function normalizeChildren(nodes, inSvg = false, ctx = null, inNoscript = false)
20843
21308
  } else if (n.type === 'Tsx' || n.type === 'Tsrx' || n.type === 'JSXFragment') {
20844
21309
  out.push(...normalizeChildren(n.children || [], inSvg, ctx, inNoscript));
20845
21310
  } else if (n.type === 'JSXStyleElement') {
21311
+ if (hasJsxAttribute(n, 'dangerouslySetInnerHTML') || hasJsxAttribute(n, 'children')) {
21312
+ out.push(
21313
+ ...normalizeChildren(
21314
+ [b.jsx_element(n, n.openingElement.attributes || [], [])],
21315
+ inSvg,
21316
+ ctx,
21317
+ inNoscript,
21318
+ allowImperative,
21319
+ ),
21320
+ );
21321
+ continue;
21322
+ }
20846
21323
  // `<style href precedence>` is a Float style resource, not scoped CSS —
20847
21324
  // partition it to the head channel like precedence links.
20848
21325
  if (!inSvg && !inNoscript && headResourceKind(n) === 'style') {
@@ -21852,9 +22329,9 @@ function planJsx(
21852
22329
  for (let rootI = 0; rootI < jsxNodes.length; rootI++) {
21853
22330
  const node = jsxNodes[rootI];
21854
22331
  const nodeIsComp = node.type === 'Element' && isComponentTag(node);
21855
- if (ctx.dev && node.type === 'Element' && !nodeIsComp) {
22332
+ if (ctx.dev && ((node.type === 'Element' && !nodeIsComp) || node.imperativeRoot)) {
21856
22333
  collectClientHtmlNestingWarnings(
21857
- node,
22334
+ node.imperativeRoot ?? node,
21858
22335
  ctx,
21859
22336
  parentNs === 'opaque' ? 'html' : parentNs,
21860
22337
  nestingWarningIdentities,
@@ -22692,6 +23169,7 @@ function planJsx(
22692
23169
  (fc.indexIndependent ? 8 : 0) |
22693
23170
  (fc.ssrMarkerless ? 16 : 0) |
22694
23171
  (fc.requiresScope ? 32 : 0) |
23172
+ (ctx.dev && fc.missingMapKey ? 64 : 0) |
22695
23173
  (fc.keyedSelectionIndex >= 0 ? fc.keyedSelectionIndex << 6 : 0);
22696
23174
  // Arg layout: forBlock(__s, slot, host, items, keyFn, body, flags?, deps?,
22697
23175
  // emptyBody?, anchor?, ownEnd?, selectionBody?). The final argument is
@@ -23710,7 +24188,15 @@ function emitBindingMount(bind, elVar, bag) {
23710
24188
  // `suppressHydrationWarning`: stamp a JS flag (NOT a DOM attribute) the runtime reads to
23711
24189
  // keep the server value + skip the warning on a hydration mismatch for this element.
23712
24190
  if (bind.kind === 'suppress') {
23713
- return st(b.stmt(b.assignment('=', b.member(el(), '__oct_suppress'), b.literal(true))));
24191
+ return st(
24192
+ b.stmt(
24193
+ b.assignment(
24194
+ '=',
24195
+ b.member(el(), '__oct_suppress'),
24196
+ b.binary('===', bind.expr, b.literal(true)),
24197
+ ),
24198
+ ),
24199
+ );
23714
24200
  }
23715
24201
  if (bind.kind === 'dangerChild') {
23716
24202
  return st(b.stmt(b.call('_$markDangerouslySetInnerHTMLChildren', el())));
@@ -24850,7 +25336,7 @@ function collectClientHtmlNestingWarnings(root, ctx, inheritedNamespace, identit
24850
25336
  const visit = (node, namespace, ancestors) => {
24851
25337
  if (node?.type !== 'Element' && node?.type !== 'JSXElement') return;
24852
25338
  if (isComponentTag(node)) return;
24853
- const tag = elementTagName(node);
25339
+ const tag = jsxTagName(node) || elementTagName(node);
24854
25340
  if (tag === null) return;
24855
25341
  const semanticTag = tag.toLowerCase();
24856
25342
  const ownNamespace = nsForSelf(tag, namespace);
@@ -24884,7 +25370,13 @@ function collectClientHtmlNestingWarnings(root, ctx, inheritedNamespace, identit
24884
25370
  ? 'html'
24885
25371
  : nsForChildren(tag, namespace);
24886
25372
  const nextAncestors = [...ancestors, { tag: semanticTag, namespace: ownNamespace, location }];
24887
- const children = normalizeChildren(node.children || [], childNamespace === 'svg', ctx);
25373
+ const children = normalizeChildren(
25374
+ node.children || [],
25375
+ childNamespace === 'svg',
25376
+ ctx,
25377
+ false,
25378
+ false,
25379
+ );
24888
25380
  for (const child of children) {
24889
25381
  if (child.type === 'Text' && ownNamespace === 'html' && restrictedTextParent(semanticTag)) {
24890
25382
  const text = staticTextLiteral(child.expression);
@@ -25156,12 +25648,22 @@ function emitElementHtml(
25156
25648
  // `suppressHydrationWarning` (React shallow semantics): NEVER a DOM attribute (not
25157
25649
  // serialized client or server) — stamp a JS flag the runtime's hydration-mismatch
25158
25650
  // 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.
25651
+ // and skips the warning. Only a bare prop or a value equal to true opts in.
25160
25652
  if (rawAttrName === 'suppressHydrationWarning') {
25161
25653
  const v = attr.value;
25162
25654
  const inner = v && v.type === 'JSXExpressionContainer' ? v.expression : v;
25163
25655
  const isFalse = inner && inner.type === 'Literal' && inner.value === false;
25164
- if (!isFalse) bindings.push({ id: bindings.length, kind: 'suppress', path, origin: attr });
25656
+ if (!isFalse)
25657
+ bindings.push({
25658
+ id: bindings.length,
25659
+ kind: 'suppress',
25660
+ path,
25661
+ origin: attr,
25662
+ expr:
25663
+ inner == null
25664
+ ? b.literal(true, 'true', attr)
25665
+ : tsrxExprNode(inner, ctx, componentName, inlinedSubs),
25666
+ });
25165
25667
  continue;
25166
25668
  }
25167
25669
  const attrName = normalizeJsxAttrName(rawAttrName, tag, hostNs);
@@ -25383,6 +25885,7 @@ function emitElementHtml(
25383
25885
  // path; valid static attrs still bake with their real boolean semantics.
25384
25886
  if (
25385
25887
  isEventAttrName(attrName) ||
25888
+ MUST_USE_PROPERTY_PROPS.has(attrName.toLowerCase()) ||
25386
25889
  isAfterSpread ||
25387
25890
  classBeforeSpread ||
25388
25891
  (ctx.dev && needsDevStaticAttrValidation(attrName, true, tag, hostNs))
@@ -25473,6 +25976,7 @@ function emitElementHtml(
25473
25976
  if (
25474
25977
  inner.type === 'Literal' &&
25475
25978
  !isEventAttrName(attrName) &&
25979
+ !MUST_USE_PROPERTY_PROPS.has(attrName.toLowerCase()) &&
25476
25980
  !isAfterSpread &&
25477
25981
  !classBeforeSpread &&
25478
25982
  !(ctx.dev && needsDevStaticAttrValidation(attrName, inner.value, tag, hostNs))
@@ -25485,7 +25989,7 @@ function emitElementHtml(
25485
25989
  // Dynamic value — record a binding. (Also reached for literal values that
25486
25990
  // come after a spread, since those need to win over the spread at runtime.)
25487
25991
  const expr = tsrxExprNode(inner, ctx, componentName, inlinedSubs);
25488
- if (isEventAttrName(attrName)) {
25992
+ if (isEventAttrName(attrName) && !(tag.includes('-') && !isDelegatedEventProp(attrName))) {
25489
25993
  // React-shape: a trailing `Capture` selects the capture phase (fired
25490
25994
  // root→target before bubble handlers), stamped under `$$capture:<type>`.
25491
25995
  // The real events gotpointercapture / lostpointercapture literally end in
@@ -25501,7 +26005,14 @@ function emitElementHtml(
25501
26005
  capture = true;
25502
26006
  rest = rest.slice(0, -7);
25503
26007
  }
25504
- const eventName = rest === 'DoubleClick' ? 'dblclick' : rest.toLowerCase();
26008
+ const eventName =
26009
+ rest === 'DoubleClick'
26010
+ ? 'dblclick'
26011
+ : rest === 'Focus'
26012
+ ? 'focusin'
26013
+ : rest === 'Blur'
26014
+ ? 'focusout'
26015
+ : rest.toLowerCase();
25505
26016
  const slotKey = capture ? `$$capture:${eventName}` : `$$${eventName}`;
25506
26017
  // `onClick` → the `'$$click'` slot key. Without this the attribute name
25507
26018
  // is unreachable from the output: the key's map position resolves to
@@ -26182,7 +26693,10 @@ function emitElementHtml(
26182
26693
  };
26183
26694
  const ic = makeIfCall(asIf, ctx, inlinedSubs, childNs, cssHash);
26184
26695
  ic.hostPath = path;
26696
+ ic.anchorPath = [...path, childIdx];
26185
26697
  ifCalls.push(ic);
26698
+ appendTemplatePart(html, '<!>', 'anchor');
26699
+ childIdx++;
26186
26700
  } else if (isKnownStringChildExpression(expr, ctx.knownStringChildLocals)) {
26187
26701
  bindings.push({
26188
26702
  id: bindings.length,
@@ -26277,7 +26791,7 @@ function makePortalCall(callNode, ctx, componentName, inlinedSubs, parentNs, css
26277
26791
  bodyArg,
26278
26792
  );
26279
26793
  } else {
26280
- bodyExpr = tsrxExprNode(bodyArg, ctx, componentName, inlinedSubs);
26794
+ bodyExpr = tsrxExprNode(rewriteJsxValues(bodyArg, ctx), ctx, componentName, inlinedSubs);
26281
26795
  }
26282
26796
  return {
26283
26797
  id: ctx.nextHelperId++,
@@ -27707,16 +28221,20 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
27707
28221
  const keyHelper = `_key$${ctx.nextHelperId++}`;
27708
28222
  const keyOrigin = node.loc ? node : keyFn.loc ? keyFn : ctx._moduleOrigin;
27709
28223
  const keyBody = keyFn.body;
28224
+ const keyCapturesParent = [...collectFreeIdentifiers(keyFn, new Set())].some((name) =>
28225
+ ctx.currentComponentLocals?.has(name),
28226
+ );
27710
28227
  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';
28228
+ keyCapturesParent ||
28229
+ (node.nativeArrayMap !== undefined &&
28230
+ mapCallbackHasEventClosure(node.nativeArrayMap.callback) &&
28231
+ !isDestructured &&
28232
+ !node.index &&
28233
+ keyBody?.type === 'MemberExpression' &&
28234
+ keyBody.computed !== true &&
28235
+ keyBody.object?.type === 'Identifier' &&
28236
+ keyBody.object.name === itemName &&
28237
+ keyBody.property?.name === 'id');
27720
28238
  if (!inlineKey) {
27721
28239
  ctx.hoistedHelpers.push(inheritOriginLoc(b.const(keyHelper, keyFn), keyOrigin));
27722
28240
  }
@@ -28129,6 +28647,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28129
28647
  );
28130
28648
 
28131
28649
  const mapCall = node.nativeArrayMap || null;
28650
+ if (keyCapturesParent) autoMemoDeps = null;
28132
28651
  return {
28133
28652
  id: ctx.nextHelperId++,
28134
28653
  loc: devLoc(ctx, node),
@@ -28148,6 +28667,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
28148
28667
  b.id(mapCall.receiverName),
28149
28668
  ])
28150
28669
  : node.right, // the fold replaces this with a `props.hN` member read
28670
+ missingMapKey: mapCall !== null && node.key == null,
28151
28671
  mapMethodExpr: mapCall
28152
28672
  ? mapCall.methodName === null
28153
28673
  ? b.member(b.id(mapCall.receiverName), 'map')