octane 0.1.28 → 0.1.30

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.
@@ -1758,6 +1758,62 @@ function moduleImportsViewTransition(astBody) {
1758
1758
  }
1759
1759
  }
1760
1760
 
1761
+ function classifyViewTransitionOwnership(astBody, production, ownComponents) {
1762
+ if (!moduleImportsViewTransition(astBody)) return { global: false, owners: new Set() };
1763
+ if (!production) return { global: true, owners: new Set() };
1764
+
1765
+ const imported = new Set();
1766
+ for (const statement of astBody) {
1767
+ if (statement.type === 'ImportDeclaration' && statement.source?.value === 'octane') {
1768
+ for (const specifier of statement.specifiers || []) {
1769
+ if (specifier.type === 'ImportNamespaceSpecifier') {
1770
+ return { global: true, owners: new Set() };
1771
+ }
1772
+ const name = specifier.imported?.name ?? specifier.imported?.value;
1773
+ if (name === 'ViewTransition' || name === 'unstable_ViewTransition') {
1774
+ imported.add(specifier.local.name);
1775
+ }
1776
+ }
1777
+ } else if (
1778
+ (statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportAllDeclaration') &&
1779
+ statement.source?.value === 'octane'
1780
+ ) {
1781
+ return { global: true, owners: new Set() };
1782
+ }
1783
+ }
1784
+ if (imported.size === 0) return { global: true, owners: new Set() };
1785
+
1786
+ const mentionsImportedTransition = (root) => {
1787
+ const free = collectFreeIdentifiers(
1788
+ root,
1789
+ isComponentFunction(root) ? collectComponentLocals(root) : new Set(),
1790
+ );
1791
+ for (const name of imported) {
1792
+ if (free.has(name)) return true;
1793
+ }
1794
+ return false;
1795
+ };
1796
+
1797
+ let global = false;
1798
+ const owners = new Set();
1799
+ for (const statement of astBody) {
1800
+ if (statement.type === 'ImportDeclaration') continue;
1801
+ const declaration =
1802
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
1803
+ ? statement.declaration
1804
+ : statement;
1805
+ if (!mentionsImportedTransition(declaration ?? statement)) continue;
1806
+ const exported =
1807
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration';
1808
+ if (ownComponents && exported && isComponentFunction(declaration)) {
1809
+ owners.add(declaration.id.name);
1810
+ } else {
1811
+ global = true;
1812
+ }
1813
+ }
1814
+ return { global, owners };
1815
+ }
1816
+
1761
1817
  export const HOOK_NAMES = new Set([
1762
1818
  'useState',
1763
1819
  'useLinkedState',
@@ -2213,7 +2269,7 @@ function isArrowStableOver(arrow, stable, componentLocals) {
2213
2269
  * Idempotent: a const we already rewrote into `useCallback(...)` won't be
2214
2270
  * re-wrapped (its init is now a CallExpression, not an ArrowFunctionExpression).
2215
2271
  */
2216
- function rewriteAutoCallback(stmt, stable, componentLocals, ctx) {
2272
+ function rewriteAutoCallback(stmt, stable, componentLocals, ctx, invariant) {
2217
2273
  if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const') return stmt;
2218
2274
  let modified = false;
2219
2275
  const newDecls = stmt.declarations.map((decl) => {
@@ -2245,7 +2301,11 @@ function rewriteAutoCallback(stmt, stable, componentLocals, ctx) {
2245
2301
  // `_octaneGenerated` tells rewriteHookCalls (which slots this call next)
2246
2302
  // that the callee is compiler-inserted — it renames it to the shadow-proof
2247
2303
  // `_$useCallback` alias instead of treating it as a user identifier.
2248
- { ...b.id('useCallback'), _octaneGenerated: true },
2304
+ {
2305
+ ...b.id('useCallback'),
2306
+ _octaneGenerated: true,
2307
+ _octaneLifetimeInvariant: invariant.has(decl.id.name),
2308
+ },
2249
2309
  arrow,
2250
2310
  b.array(deps.map((n) => b.id(n))),
2251
2311
  ),
@@ -2336,6 +2396,68 @@ function rewriteAutoCalculation(stmt, componentLocals, renderReadNames, ctx) {
2336
2396
  };
2337
2397
  }
2338
2398
 
2399
+ // A cached calculation can own a renderable array only when its value never
2400
+ // escapes into setup, a callback, a component prop, or another expression. The
2401
+ // bare renderable holes are the sole permitted reads: then the same immutable
2402
+ // projection contract that admitted the calculation also witnesses the whole
2403
+ // array. Preserve the exact Identifier nodes rather than just their spelling so
2404
+ // a nested shadow cannot accidentally inherit an outer calculation's proof.
2405
+ function collectAutoCalculatedRenderableRefs(statements, jsxNodes, calculated) {
2406
+ if (calculated.size === 0) return null;
2407
+ const references = new Map();
2408
+ const seen = new WeakSet();
2409
+ function visit(node, isJsxChild = false) {
2410
+ if (node === null || typeof node !== 'object') return;
2411
+ if (Array.isArray(node)) {
2412
+ for (const child of node) visit(child, isJsxChild);
2413
+ return;
2414
+ }
2415
+ if (seen.has(node)) return;
2416
+ seen.add(node);
2417
+ if (node.type === 'Element' || node.type === 'JSXElement') {
2418
+ visit(node.children, true);
2419
+ return;
2420
+ }
2421
+ if (
2422
+ isJsxChild &&
2423
+ (node.type === 'Text' ||
2424
+ node.type === 'TSRXExpression' ||
2425
+ node.type === 'JSXExpressionContainer')
2426
+ ) {
2427
+ const expression = node.expression;
2428
+ if (expression?.type === 'Identifier' && calculated.has(expression.name)) {
2429
+ let nodes = references.get(expression.name);
2430
+ if (nodes === undefined) references.set(expression.name, (nodes = new Set()));
2431
+ nodes.add(expression);
2432
+ }
2433
+ return;
2434
+ }
2435
+ for (const key in node) {
2436
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
2437
+ visit(node[key], key === 'children');
2438
+ }
2439
+ }
2440
+ visit(jsxNodes, true);
2441
+
2442
+ let proven = null;
2443
+ for (const [name, declaration] of calculated) {
2444
+ const nodes = references.get(name);
2445
+ if (nodes === undefined) continue;
2446
+ let escaped = false;
2447
+ for (const statement of statements) {
2448
+ if (statement === declaration) continue;
2449
+ if (collectFreeIdentifiers(statement, []).has(name)) {
2450
+ escaped = true;
2451
+ break;
2452
+ }
2453
+ }
2454
+ if (escaped || collectFreeIdentifiers(jsxNodes, [], nodes).has(name)) continue;
2455
+ if (proven === null) proven = new WeakSet();
2456
+ for (const node of nodes) proven.add(node);
2457
+ }
2458
+ return proven;
2459
+ }
2460
+
2339
2461
  // Names the render tree actually reads, resolved against the scopes the
2340
2462
  // statement walker cannot see (nested functions, loops, `@for` item bindings,
2341
2463
  // directive-arm blocks) so a shadowing inner binding never nominates an
@@ -3343,6 +3465,67 @@ function containsComponentCallOrControlFlow(stmts) {
3343
3465
  return found;
3344
3466
  }
3345
3467
 
3468
+ /**
3469
+ * A host-only conditional can share its keyed row's immutable item/dependency
3470
+ * proof, but entering that conditional still needs the row's active scope. Keep
3471
+ * components and every other control-flow boundary on their existing path.
3472
+ */
3473
+ function hasOnlyHostConditionalItemBodies(stmts) {
3474
+ let hasConditional = false;
3475
+ let disallowed = false;
3476
+ const seen = new WeakSet();
3477
+ function walk(node) {
3478
+ if (disallowed || !node) return;
3479
+ if (Array.isArray(node)) {
3480
+ for (const child of node) walk(child);
3481
+ return;
3482
+ }
3483
+ if (typeof node !== 'object') return;
3484
+ const type = node.type;
3485
+ if (!type || seen.has(node)) return;
3486
+ seen.add(node);
3487
+ if (
3488
+ type === 'ArrowFunctionExpression' ||
3489
+ type === 'FunctionExpression' ||
3490
+ type === 'FunctionDeclaration'
3491
+ ) {
3492
+ return;
3493
+ }
3494
+ if ((type === 'Element' || type === 'JSXElement') && isComponentTag(node)) {
3495
+ disallowed = true;
3496
+ return;
3497
+ }
3498
+ if (type === 'IfStatement' || type === 'JSXIfExpression') {
3499
+ hasConditional = true;
3500
+ } else if (
3501
+ type === 'ForStatement' ||
3502
+ type === 'ForInStatement' ||
3503
+ type === 'ForOfStatement' ||
3504
+ type === 'WhileStatement' ||
3505
+ type === 'DoWhileStatement' ||
3506
+ type === 'TryStatement' ||
3507
+ type === 'SwitchStatement' ||
3508
+ type === 'ActivityStatement' ||
3509
+ type === 'JSXForExpression' ||
3510
+ type === 'JSXTryExpression' ||
3511
+ type === 'JSXSwitchExpression' ||
3512
+ type === 'JSXActivityExpression' ||
3513
+ ((type === 'TSRXExpression' || type === 'JSXExpressionContainer') &&
3514
+ node.expression &&
3515
+ isCreatePortalCall(node.expression))
3516
+ ) {
3517
+ disallowed = true;
3518
+ return;
3519
+ }
3520
+ for (const key in node) {
3521
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
3522
+ walk(node[key]);
3523
+ }
3524
+ }
3525
+ for (const statement of stmts) walk(statement);
3526
+ return hasConditional && !disallowed;
3527
+ }
3528
+
3346
3529
  /**
3347
3530
  * Classify the narrow opaque shape that an automatically memoized keyed item
3348
3531
  * may safely contain: ordinary JSX component calls, but no template control
@@ -3535,15 +3718,16 @@ function containsRenderCall(stmts, memoCtx = null) {
3535
3718
  }
3536
3719
 
3537
3720
  // A hook call is identified by naming convention — the same signal React and
3538
- // React Compiler key on — plus React's own `unstable_` staging prefix, which
3539
- // bindings mirror (`unstable_useRouterState` in @octanejs/remix-router). The
3540
- // prefix is enumerated rather than matched as "any `_use`" so an ordinary
3541
- // helper cannot be mistaken for a hook by spelling alone.
3721
+ // React Compiler key on — plus the exact `unstable_` and `UNSTABLE_` staging
3722
+ // prefixes bindings expose (`unstable_useRouterState` in @octanejs/remix-router
3723
+ // and `UNSTABLE_useTreeGridState` in @octanejs/aria). Enumerate the prefixes
3724
+ // rather than matching "any `_use`" so ordinary helpers are not mistaken for
3725
+ // hooks by spelling alone.
3542
3726
  //
3543
3727
  // Getting this wrong in the permissive direction is not a staleness bug: a
3544
3728
  // cache wrapped around a hook call freezes its subscription and its state cell
3545
3729
  // for the life of the component.
3546
- const HOOK_NAME_CONVENTION_RE = /^(?:unstable_)?use(?:$|[A-Z])/;
3730
+ const HOOK_NAME_CONVENTION_RE = /^(?:(?:unstable|UNSTABLE)_)?use(?:$|[A-Z])/;
3547
3731
 
3548
3732
  function isHookCalleeName(name) {
3549
3733
  return HOOK_NAME_CONVENTION_RE.test(name);
@@ -3716,6 +3900,202 @@ function collectImmutableModuleFunctions(body) {
3716
3900
  return declared;
3717
3901
  }
3718
3902
 
3903
+ // A child warm plan is useful only if that child can reach an async creation.
3904
+ // Same-module declarations are the only closed call graph we can prove: an
3905
+ // imported/dynamic component, custom hook, helper call, or lazy state initializer
3906
+ // may suspend behind an opaque boundary, so each keeps its existing warm edge.
3907
+ // A useState call with a primitive literal initializer and publishing its stable
3908
+ // setter are synchronous, so neither turns a synchronous tree into a warm plan.
3909
+ function classifySameModuleWarmPotential(ctx) {
3910
+ for (const [, info] of ctx.componentInfo) {
3911
+ const component = info.node;
3912
+ const statements = component.body.body || [];
3913
+ const locals = collectComponentLocals(component);
3914
+ const invariant = computeInvariantLocals(statements, locals, false);
3915
+ const dependencies = new Set();
3916
+ const seen = new WeakSet();
3917
+ // A reassigned function binding can point at an async component by the
3918
+ // time its warm edge runs. Destructuring/default/rest parameters can also
3919
+ // invoke user code before the authored component body is reached.
3920
+ let opaque =
3921
+ !ctx.moduleFunctionDeclarations.has(component.id?.name) ||
3922
+ (component.params || []).some((parameter) => parameter.type !== 'Identifier');
3923
+
3924
+ function walk(node) {
3925
+ if (opaque || node === null || typeof node !== 'object') return;
3926
+ if (Array.isArray(node)) {
3927
+ for (const child of node) walk(child);
3928
+ return;
3929
+ }
3930
+ if (seen.has(node)) return;
3931
+ seen.add(node);
3932
+
3933
+ // Deferred handlers do not execute during this component's render. State
3934
+ // initializers are admitted only when proven primitive and non-callable.
3935
+ if (FN_TYPES.has(node.type)) return;
3936
+
3937
+ if ((node.type === 'Element' || node.type === 'JSXElement') && isComponentTag(node)) {
3938
+ const name = tagBindingName(node);
3939
+ if (
3940
+ name === null ||
3941
+ locals.has(name) ||
3942
+ ctx._octaneBoundaryNames.has(name) ||
3943
+ !ctx.componentInfo.has(name)
3944
+ ) {
3945
+ opaque = true;
3946
+ return;
3947
+ }
3948
+ dependencies.add(name);
3949
+ } else if (node.type === 'CallExpression' || node.type === 'NewExpression') {
3950
+ const hook = stableHookCallName(node);
3951
+ if (
3952
+ hook !== 'useState' ||
3953
+ node.arguments.length > 1 ||
3954
+ (node.arguments.length === 1 && !isInvariantLiteral(unwrapTsExpr(node.arguments[0])))
3955
+ ) {
3956
+ opaque = true;
3957
+ return;
3958
+ }
3959
+ } else if (node.type === 'VariableDeclarator' && node.id?.type === 'ArrayPattern') {
3960
+ // The built-in state tuple is the only iterator this proof owns. Any
3961
+ // other destructuring can execute a custom iterator or rest/default.
3962
+ if (
3963
+ stableHookCallName(unwrapTsExpr(node.init)) !== 'useState' ||
3964
+ node.id.elements.some((element) => element !== null && element.type !== 'Identifier')
3965
+ ) {
3966
+ opaque = true;
3967
+ return;
3968
+ }
3969
+ } else if (node.type === 'AssignmentExpression') {
3970
+ if (
3971
+ node.operator !== '=' ||
3972
+ node.left?.type !== 'Identifier' ||
3973
+ node.right?.type !== 'Identifier' ||
3974
+ !invariant.has(node.right.name)
3975
+ ) {
3976
+ opaque = true;
3977
+ return;
3978
+ }
3979
+ } else if (
3980
+ node.type === 'AwaitExpression' ||
3981
+ node.type === 'YieldExpression' ||
3982
+ node.type === 'MemberExpression' ||
3983
+ node.type === 'JSXMemberExpression' ||
3984
+ node.type === 'OptionalMemberExpression' ||
3985
+ node.type === 'OptionalCallExpression' ||
3986
+ node.type === 'SpreadElement' ||
3987
+ node.type === 'SpreadAttribute' ||
3988
+ node.type === 'JSXSpreadAttribute' ||
3989
+ node.type === 'ObjectPattern' ||
3990
+ node.type === 'AssignmentPattern' ||
3991
+ node.type === 'RestElement' ||
3992
+ node.type === 'ForOfStatement' ||
3993
+ node.type === 'JSXForExpression' ||
3994
+ node.type === 'ForInStatement' ||
3995
+ node.type === 'ForStatement' ||
3996
+ node.type === 'WhileStatement' ||
3997
+ node.type === 'DoWhileStatement' ||
3998
+ node.type === 'ThrowStatement' ||
3999
+ node.type === 'TryStatement' ||
4000
+ node.type === 'JSXTryExpression' ||
4001
+ node.type === 'ImportExpression' ||
4002
+ node.type === 'TaggedTemplateExpression' ||
4003
+ node.type === 'UpdateExpression'
4004
+ ) {
4005
+ opaque = true;
4006
+ return;
4007
+ }
4008
+
4009
+ for (const key in node) {
4010
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4011
+ walk(node[key]);
4012
+ }
4013
+ }
4014
+
4015
+ walk(statements);
4016
+ walk(component.body.render);
4017
+ info.warmPotential = opaque;
4018
+ info.warmDependencies = dependencies;
4019
+ }
4020
+
4021
+ // Propagate async reachability through forward references and recursive
4022
+ // same-module chains. An all-synchronous cycle stays false; one opaque or
4023
+ // async descendant makes every component that can reach it conservative.
4024
+ let changed = true;
4025
+ while (changed) {
4026
+ changed = false;
4027
+ for (const [, info] of ctx.componentInfo) {
4028
+ if (info.warmPotential) continue;
4029
+ for (const name of info.warmDependencies) {
4030
+ if (ctx.componentInfo.get(name)?.warmPotential !== false) {
4031
+ info.warmPotential = true;
4032
+ changed = true;
4033
+ break;
4034
+ }
4035
+ }
4036
+ }
4037
+ }
4038
+ }
4039
+
4040
+ // Omitting a JSX descriptor also omits its live `Component.defaultProps` read.
4041
+ // Only private declarations whose every reference is an immediately rendered,
4042
+ // attribute-free host child can therefore bypass descriptor construction. JSX
4043
+ // descriptors returned/stored/passed elsewhere expose their `.type`, so those
4044
+ // sites count as escapes even though their tag looks equally static.
4045
+ function collectPrivateUnescapedComponents(body, ctx) {
4046
+ const candidates = new Set();
4047
+ for (const statement of body) {
4048
+ if (
4049
+ statement.type === 'FunctionDeclaration' &&
4050
+ statement.id?.type === 'Identifier' &&
4051
+ ctx.componentInfo.get(statement.id.name)?.returnJsx === true &&
4052
+ ctx.moduleFunctionDeclarations.has(statement.id.name)
4053
+ ) {
4054
+ candidates.add(statement.id.name);
4055
+ }
4056
+ }
4057
+ if (candidates.size === 0) return candidates;
4058
+
4059
+ const allowed = new Set();
4060
+ const visitHost = (host) => {
4061
+ for (const child of host.children || []) {
4062
+ if (child?.type !== 'Element' && child?.type !== 'JSXElement') continue;
4063
+ if (!isComponentTag(child)) {
4064
+ visitHost(child);
4065
+ continue;
4066
+ }
4067
+ const name = tagBindingName(child);
4068
+ const attrs = child.attributes || child.openingElement?.attributes || [];
4069
+ if (name !== null && candidates.has(name) && attrs.length === 0 && !child.children?.length) {
4070
+ allowed.add(child);
4071
+ }
4072
+ }
4073
+ };
4074
+ for (const info of ctx.componentInfo.values()) {
4075
+ if (!info.returnJsx) continue;
4076
+ for (const statement of info.node.body.body || []) {
4077
+ if (statement.type === 'ReturnStatement' && isPlainHostRoot(statement.argument)) {
4078
+ visitHost(statement.argument);
4079
+ }
4080
+ }
4081
+ }
4082
+
4083
+ // An array deliberately does not introduce a synthetic module Block scope:
4084
+ // sibling function references must remain free for the escape proof.
4085
+ const escaped = collectFreeIdentifiers(body, [], allowed);
4086
+ for (const name of candidates) {
4087
+ if (escaped.has(name)) {
4088
+ candidates.delete(name);
4089
+ continue;
4090
+ }
4091
+ // A declaration binds its own name inside its body. Scan that disjoint
4092
+ // subtree once more so self-assigned properties cannot hide behind it.
4093
+ const declaration = ctx.moduleFunctionDeclarations.get(name);
4094
+ if (collectFreeIdentifiers(declaration.body, [], allowed).has(name)) candidates.delete(name);
4095
+ }
4096
+ return candidates;
4097
+ }
4098
+
3719
4099
  // Conservative semantic boundary for compiler-owned component-region memoization.
3720
4100
  // The cached region assumes React Compiler's pure-render / immutable-snapshot
3721
4101
  // contract, but still fails closed for constructs whose commit or retry behavior
@@ -4370,6 +4750,111 @@ function isSsrMarkerlessForItem(node) {
4370
4750
  return jsxChildren.length === 1 && isPlainHostRoot(jsxChildren[0]);
4371
4751
  }
4372
4752
 
4753
+ // Direct host-row mounting skips component-render bookkeeping. Keep the proof
4754
+ // narrower than markerless item rendering so opaque host lifecycles fail closed.
4755
+ const HOST_MOUNT_SAFE_TAGS = new Set([
4756
+ 'a',
4757
+ 'article',
4758
+ 'aside',
4759
+ 'b',
4760
+ 'br',
4761
+ 'div',
4762
+ 'em',
4763
+ 'footer',
4764
+ 'h1',
4765
+ 'h2',
4766
+ 'h3',
4767
+ 'h4',
4768
+ 'h5',
4769
+ 'h6',
4770
+ 'header',
4771
+ 'hr',
4772
+ 'i',
4773
+ 'li',
4774
+ 'main',
4775
+ 'nav',
4776
+ 'ol',
4777
+ 'p',
4778
+ 'section',
4779
+ 'small',
4780
+ 'span',
4781
+ 'strong',
4782
+ 'table',
4783
+ 'tbody',
4784
+ 'td',
4785
+ 'tfoot',
4786
+ 'th',
4787
+ 'thead',
4788
+ 'tr',
4789
+ 'ul',
4790
+ ]);
4791
+
4792
+ const HOST_MOUNT_UNSAFE_ATTRIBUTES = new Set([
4793
+ 'autofocus',
4794
+ 'checked',
4795
+ 'dangerouslysetinnerhtml',
4796
+ 'defaultchecked',
4797
+ 'defaultvalue',
4798
+ 'form',
4799
+ 'formaction',
4800
+ 'formenctype',
4801
+ 'formmethod',
4802
+ 'formnovalidate',
4803
+ 'formtarget',
4804
+ 'innerhtml',
4805
+ 'is',
4806
+ 'multiple',
4807
+ 'ref',
4808
+ 'selected',
4809
+ 'slot',
4810
+ 'src',
4811
+ 'srcset',
4812
+ 'style',
4813
+ 'value',
4814
+ ]);
4815
+
4816
+ function isHostMountSafeTree(root) {
4817
+ const seen = new WeakSet();
4818
+ function walk(node) {
4819
+ if (node == null || typeof node !== 'object') return true;
4820
+ if (Array.isArray(node)) return node.every(walk);
4821
+ if (seen.has(node)) return true;
4822
+ seen.add(node);
4823
+ const type = node.type;
4824
+ if (
4825
+ type === 'ArrowFunctionExpression' ||
4826
+ type === 'FunctionExpression' ||
4827
+ type === 'FunctionDeclaration'
4828
+ ) {
4829
+ // Delegated event callbacks run after the live row has mounted.
4830
+ return true;
4831
+ }
4832
+ if (type === 'JSXFragment' || type === 'Fragment') return false;
4833
+ if (type === 'Element' || type === 'JSXElement') {
4834
+ const tag = node.id || node.openingElement?.name;
4835
+ if (
4836
+ (tag?.type !== 'Identifier' && tag?.type !== 'JSXIdentifier') ||
4837
+ !HOST_MOUNT_SAFE_TAGS.has(tag.name)
4838
+ ) {
4839
+ return false;
4840
+ }
4841
+ for (const attribute of node.attributes || node.openingElement?.attributes || []) {
4842
+ if (attribute.type !== 'Attribute' && attribute.type !== 'JSXAttribute') return false;
4843
+ const name = jsxAttrRawName(attribute);
4844
+ if (typeof name !== 'string' || HOST_MOUNT_UNSAFE_ATTRIBUTES.has(name.toLowerCase())) {
4845
+ return false;
4846
+ }
4847
+ }
4848
+ }
4849
+ for (const key in node) {
4850
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4851
+ if (!walk(node[key])) return false;
4852
+ }
4853
+ return true;
4854
+ }
4855
+ return walk(root);
4856
+ }
4857
+
4373
4858
  /**
4374
4859
  * Whether a function can return from its own body before reaching its compiled
4375
4860
  * output. Nested functions are separate execution scopes and do not affect the
@@ -6162,15 +6647,22 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6162
6647
  exactOrigins: new Map(), // see registerExactOrigin
6163
6648
  originAliases: [], // see registerOriginAlias
6164
6649
  hoistedTemplates: [], // { name, ast, html, ns, frag, origins }
6650
+ internedTemplates: new Map(), // HTML -> one template or namespace/fragment variants
6165
6651
  hoistedHelpers: [], // statement NODES (sub-components, hook Symbols, key fns) + hook-slot-base markers
6166
6652
  delegatedEvents: new Set(), // bubble event names seen in JSX — auto-emits delegateEvents(...)
6167
6653
  capturedEvents: new Set(), // capture-phase event names (onXxxCapture) — auto-emits delegateCaptureEvents(...)
6654
+ unownedDelegatedEvents: new Set(),
6655
+ unownedCapturedEvents: new Set(),
6168
6656
  cssInjections: [], // { hash, css } — one entry per component with a <style> block
6657
+ ownedCssInjections: new Set(),
6658
+ componentOwners: [],
6659
+ currentComponentOwner: null,
6169
6660
  currentComponentLocals: null, // Set<string> while compiling a component body; null otherwise
6170
6661
  currentMapTemps: null, // receiver/method temps owned by the current emitted function
6171
6662
  currentAutoMemoOffset: 0, // flat compiler-cache cell offset for the body being emitted
6172
6663
  currentAutoMemoCacheName: null, // collision-free local bound to the body's cache array
6173
6664
  currentAutoMemoCommittedName: null, // committed cache snapshot (copy-on-write source)
6665
+ currentAutoCalculatedRenderableRefs: null, // proven non-escaping calculation holes, inherited by lexical child bodies
6174
6666
  nextAutoMemoCacheId: 0, // unique non-index slots property per compiled render function
6175
6667
  inlineHookMemo: inlineHookMemoEnabled, // de-callbacked useMemo/useCallback + pu creations
6176
6668
  _puInlineLowering: false, // true only while a body pipeline ends in inlineHookMemoPass
@@ -6375,8 +6867,54 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6375
6867
  ctx.moduleFunctionDeclarations = collectImmutableModuleFunctions(ast.body);
6376
6868
  // M3 inherit-range exclusion set (see inheritSoleCompRoot).
6377
6869
  ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
6378
- // Client prelude `_$vtSeen()` module-load hint (view-transitions plan).
6379
- ctx._usesViewTransition = moduleImportsViewTransition(ast.body);
6870
+ const productionEffects = !ctx.hmr && !ctx.dev && !ctx.profile;
6871
+ const exportedComponents = ast.body.filter(
6872
+ (statement) =>
6873
+ (statement.type === 'ExportNamedDeclaration' ||
6874
+ statement.type === 'ExportDefaultDeclaration') &&
6875
+ isComponentFunction(statement.declaration),
6876
+ );
6877
+ // Prelude registration historically precedes every authored module effect.
6878
+ // An effect above any component could observe its stylesheet, delegated
6879
+ // listeners, or transition capability, so retain the entire module prelude.
6880
+ let effectBeforeComponent = false;
6881
+ let precedingModuleEffect = false;
6882
+ if (productionEffects && exportedComponents.length > 1) {
6883
+ for (const statement of ast.body) {
6884
+ const declaration =
6885
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
6886
+ ? statement.declaration
6887
+ : statement;
6888
+ if (isComponentFunction(declaration)) {
6889
+ if (precedingModuleEffect) {
6890
+ effectBeforeComponent = true;
6891
+ break;
6892
+ }
6893
+ continue;
6894
+ }
6895
+ if (
6896
+ statement.type === 'ImportDeclaration' ||
6897
+ statement.type === 'ExportAllDeclaration' ||
6898
+ (statement.type === 'ExportNamedDeclaration' && declaration == null) ||
6899
+ declaration?.type === 'FunctionDeclaration' ||
6900
+ declaration?.type === 'TSInterfaceDeclaration' ||
6901
+ declaration?.type === 'TSTypeAliasDeclaration' ||
6902
+ statement.type === 'EmptyStatement'
6903
+ ) {
6904
+ continue;
6905
+ }
6906
+ precedingModuleEffect = true;
6907
+ }
6908
+ }
6909
+ ctx.componentEffectOwnership =
6910
+ productionEffects && exportedComponents.length > 1 && !effectBeforeComponent;
6911
+ const viewTransitions = classifyViewTransitionOwnership(
6912
+ ast.body,
6913
+ productionEffects,
6914
+ ctx.componentEffectOwnership,
6915
+ );
6916
+ ctx._usesViewTransition = viewTransitions.global;
6917
+ ctx.viewTransitionOwners = viewTransitions.owners;
6380
6918
 
6381
6919
  // List of exported components needing HMR wrapping. Each entry: { name,
6382
6920
  // exportKind: 'default' | 'named' }. We emit the `Comp = hmr(Comp)` lines
@@ -6425,6 +6963,19 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6425
6963
  });
6426
6964
  }
6427
6965
  }
6966
+ // Return-based JSX functions never attach fetch-tree warm plans, and a sole
6967
+ // same-module component already rejects a self-only warm edge below. Avoid a
6968
+ // whole-module graph walk unless a compiled-void body can actually reach a
6969
+ // distinct same-module declaration.
6970
+ if (
6971
+ ctx.componentInfo.size > 1 &&
6972
+ [...ctx.componentInfo.values()].some((info) => info.node.body?.type === 'JSXCodeBlock')
6973
+ ) {
6974
+ classifySameModuleWarmPotential(ctx);
6975
+ }
6976
+ ctx.privateUnescapedComponents = ctx.autoMemo
6977
+ ? collectPrivateUnescapedComponents(ast.body, ctx)
6978
+ : new Set();
6428
6979
  for (const [, info] of ctx.componentInfo) {
6429
6980
  const compNode = info.node;
6430
6981
  const locals = collectComponentLocals(compNode);
@@ -6736,6 +7287,8 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6736
7287
  }
6737
7288
  }
6738
7289
 
7290
+ finalizeComponentInitializers(ctx, bodyNodes);
7291
+
6739
7292
  // Auto-emit delegateEvents([...]) / delegateCaptureEvents([...]) once at module
6740
7293
  // scope for every (bubble / capture) event seen.
6741
7294
  if (ctx.delegatedEvents.size > 0) {
@@ -6777,22 +7330,24 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6777
7330
  ),
6778
7331
  );
6779
7332
  }
6780
- const styleNodes = ctx.cssInjections.map((i) => {
6781
- // Point the authored `<style>` block at the CSS this injection carries.
6782
- // Both sides are whole units — the block and the stylesheet it became —
6783
- // which is the useful pairing for a scoped style.
6784
- const anchor = claimCssOrigins(ctx, i);
6785
- return inheritOriginLoc(
6786
- b.stmt(
6787
- b.call(
6788
- '_$injectStyle',
6789
- b.literal(i.hash, JSON.stringify(i.hash)),
6790
- b.literal(i.css, JSON.stringify(i.css)),
7333
+ const styleNodes = ctx.cssInjections
7334
+ .filter((i) => !ctx.ownedCssInjections.has(i))
7335
+ .map((i) => {
7336
+ // Point the authored `<style>` block at the CSS this injection carries.
7337
+ // Both sides are whole units — the block and the stylesheet it became —
7338
+ // which is the useful pairing for a scoped style.
7339
+ const anchor = claimCssOrigins(ctx, i);
7340
+ return inheritOriginLoc(
7341
+ b.stmt(
7342
+ b.call(
7343
+ '_$injectStyle',
7344
+ b.literal(i.hash, JSON.stringify(i.hash)),
7345
+ b.literal(i.css, JSON.stringify(i.css)),
7346
+ ),
6791
7347
  ),
6792
- ),
6793
- anchor ?? moduleOrigin,
6794
- );
6795
- });
7348
+ anchor ?? moduleOrigin,
7349
+ );
7350
+ });
6796
7351
  const templateNodes = ctx.hoistedTemplates.map((t) => {
6797
7352
  const args = [b.literal(t.html, JSON.stringify(t.html))];
6798
7353
  if (t.ns || t.frag) args.push(b.literal(t.ns | 0));
@@ -10152,6 +10707,103 @@ function singleRootInitializer(ctx, component) {
10152
10707
  return markPure(b.call('_$__s', component));
10153
10708
  }
10154
10709
 
10710
+ function finalizeComponentInitializers(ctx, bodyNodes) {
10711
+ if (!ctx.componentEffectOwnership || ctx.componentOwners.length === 0) return;
10712
+
10713
+ const delegatedOwners = new Map();
10714
+ const capturedOwners = new Map();
10715
+ const ownableStyles = new Set();
10716
+ for (const owner of ctx.componentOwners) {
10717
+ for (const event of owner.delegatedEvents) {
10718
+ delegatedOwners.set(event, (delegatedOwners.get(event) ?? 0) + 1);
10719
+ }
10720
+ for (const event of owner.capturedEvents) {
10721
+ capturedOwners.set(event, (capturedOwners.get(event) ?? 0) + 1);
10722
+ }
10723
+ for (const style of owner.styles) ownableStyles.add(style);
10724
+ }
10725
+ // The prelude emitted all stylesheets in authored order. Moving only owned
10726
+ // sheets behind an unowned style map or local component reverses that cascade.
10727
+ const preserveModuleStyleOrder = ctx.cssInjections.some((style) => !ownableStyles.has(style));
10728
+
10729
+ const replacements = new Map();
10730
+ for (const owner of ctx.componentOwners) {
10731
+ const calls = [];
10732
+ if (owner.transition && !ctx._usesViewTransition) {
10733
+ ctx.runtimeNeeded.add('__vtSeen');
10734
+ calls.push(b.call(rtAlias('__vtSeen')));
10735
+ }
10736
+
10737
+ const delegated = [...owner.delegatedEvents]
10738
+ .filter((event) => delegatedOwners.get(event) === 1 && !ctx.unownedDelegatedEvents.has(event))
10739
+ .sort();
10740
+ if (delegated.length !== 0) {
10741
+ for (const event of delegated) ctx.delegatedEvents.delete(event);
10742
+ ctx.runtimeNeeded.add('delegateEvents');
10743
+ calls.push(
10744
+ b.call(
10745
+ '_$delegateEvents',
10746
+ b.array(delegated.map((event) => b.literal(event, JSON.stringify(event)))),
10747
+ ),
10748
+ );
10749
+ }
10750
+
10751
+ const captured = [...owner.capturedEvents]
10752
+ .filter((event) => capturedOwners.get(event) === 1 && !ctx.unownedCapturedEvents.has(event))
10753
+ .sort();
10754
+ if (captured.length !== 0) {
10755
+ for (const event of captured) ctx.capturedEvents.delete(event);
10756
+ ctx.runtimeNeeded.add('delegateCaptureEvents');
10757
+ calls.push(
10758
+ b.call(
10759
+ '_$delegateCaptureEvents',
10760
+ b.array(captured.map((event) => b.literal(event, JSON.stringify(event)))),
10761
+ ),
10762
+ );
10763
+ }
10764
+
10765
+ if (!preserveModuleStyleOrder) {
10766
+ for (const style of owner.styles) {
10767
+ ctx.ownedCssInjections.add(style);
10768
+ const origin = claimCssOrigins(ctx, style);
10769
+ calls.push(
10770
+ inheritOriginLoc(
10771
+ b.call(
10772
+ '_$injectStyle',
10773
+ b.literal(style.hash, JSON.stringify(style.hash)),
10774
+ b.literal(style.css, JSON.stringify(style.css)),
10775
+ ),
10776
+ origin ?? owner.origin,
10777
+ ),
10778
+ );
10779
+ }
10780
+ }
10781
+ if (calls.length === 0) continue;
10782
+
10783
+ const declarator = owner.declaration.declarations[0];
10784
+ const initializer = inheritOriginLoc(
10785
+ markPure(b.call(b.arrow([], b.sequence([...calls, declarator.init])))),
10786
+ owner.origin,
10787
+ );
10788
+ replacements.set(owner.declaration, {
10789
+ ...owner.declaration,
10790
+ declarations: [{ ...declarator, init: initializer }],
10791
+ });
10792
+ }
10793
+
10794
+ if (replacements.size === 0) return;
10795
+ for (let index = 0; index < bodyNodes.length; index++) {
10796
+ const statement = bodyNodes[index];
10797
+ const replacement = replacements.get(statement);
10798
+ if (replacement !== undefined) {
10799
+ bodyNodes[index] = replacement;
10800
+ } else if (statement.type === 'ExportNamedDeclaration') {
10801
+ const declaration = replacements.get(statement.declaration);
10802
+ if (declaration !== undefined) bodyNodes[index] = { ...statement, declaration };
10803
+ }
10804
+ }
10805
+ }
10806
+
10155
10807
  function compileComponent(node, ctx, options) {
10156
10808
  const name = node.id.name;
10157
10809
  rejectAsyncOrGenerator(node, name);
@@ -10160,6 +10812,19 @@ function compileComponent(node, ctx, options) {
10160
10812
  const isDefault = !!node.default;
10161
10813
  const hmrWrap = !!(options && options.hmrWrap);
10162
10814
  const returnedOutput = node.body?.type === 'JSXCodeBlock' && hasOwnValueReturn(node);
10815
+ const owner =
10816
+ ctx.componentEffectOwnership && isExported
10817
+ ? {
10818
+ name,
10819
+ origin: node,
10820
+ delegatedEvents: new Set(),
10821
+ capturedEvents: new Set(),
10822
+ styles: [],
10823
+ transition: ctx.viewTransitionOwners.has(name),
10824
+ declaration: null,
10825
+ }
10826
+ : null;
10827
+ const firstStyle = ctx.cssInjections.length;
10163
10828
 
10164
10829
  // Scoped `<style>` block. New TSRX surfaces each style block as a
10165
10830
  // `JSXStyleElement` child of the rendered tree (parser pre-computes the
@@ -10191,9 +10856,11 @@ function compileComponent(node, ctx, options) {
10191
10856
  const prevAutoMemoCallsitesSafe = ctx.currentAutoMemoCallsitesSafe;
10192
10857
  const prevKnownStr = ctx.knownStringLocals;
10193
10858
  const prevProfileComponentId = ctx.currentProfileComponentId;
10859
+ const previousComponentOwner = ctx.currentComponentOwner;
10194
10860
  ctx.currentComponentLocals = collectComponentLocals(node);
10195
10861
  ctx.currentAutoMemoCallsitesSafe = ctx.componentInfo.get(name)?.autoMemoCallsitesSafe !== false;
10196
10862
  ctx.knownStringLocals = collectKnownStringLocals(node);
10863
+ ctx.currentComponentOwner = owner;
10197
10864
  if (ctx.profile) ctx.currentProfileComponentId = profileComponentId(ctx, name, node);
10198
10865
  let fnNode;
10199
10866
  try {
@@ -10211,7 +10878,9 @@ function compileComponent(node, ctx, options) {
10211
10878
  ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
10212
10879
  ctx.knownStringLocals = prevKnownStr;
10213
10880
  ctx.currentProfileComponentId = prevProfileComponentId;
10881
+ ctx.currentComponentOwner = previousComponentOwner;
10214
10882
  }
10883
+ if (owner !== null) owner.styles = ctx.cssInjections.slice(firstStyle);
10215
10884
 
10216
10885
  // Parallel-use warm plan: attached to the INNER function object (not the
10217
10886
  // module const) so the component's own body — where the function-
@@ -10252,6 +10921,11 @@ function compileComponent(node, ctx, options) {
10252
10921
  sourceBeforeNode !== '' &&
10253
10922
  new RegExp(`\\b${name.replace(/\$/g, '\\$')}\\b`).test(sourceBeforeNode);
10254
10923
  if (referencedAboveDeclaration) {
10924
+ if (owner !== null) {
10925
+ for (const event of owner.delegatedEvents) ctx.unownedDelegatedEvents.add(event);
10926
+ for (const event of owner.capturedEvents) ctx.unownedCapturedEvents.add(event);
10927
+ if (owner.transition) ctx._usesViewTransition = true;
10928
+ }
10255
10929
  // Every stamp is `typeof`-guarded: route code-splitters (TanStack's) may
10256
10930
  // EXTRACT the declaration into its own module and leave these statements
10257
10931
  // behind — `typeof` on the then-undeclared identifier short-circuits
@@ -10341,6 +11015,10 @@ function compileComponent(node, ctx, options) {
10341
11015
  b.declaration(declKind, [b.declarator(b.id(name, node.id ?? node), valueExpr)]),
10342
11016
  node,
10343
11017
  );
11018
+ if (owner !== null) {
11019
+ owner.declaration = declNode;
11020
+ ctx.componentOwners.push(owner);
11021
+ }
10344
11022
  if (isDefault) {
10345
11023
  if (options && options.hmrMutable) {
10346
11024
  return {
@@ -10371,6 +11049,8 @@ function compileComponent(node, ctx, options) {
10371
11049
  */
10372
11050
  function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null, options = null) {
10373
11051
  const returnedOutput = options?.returnedOutput === true;
11052
+ const previousAutoCalculatedRenderableRefs = ctx.currentAutoCalculatedRenderableRefs;
11053
+ let autoCalculatedDeclarations = null;
10374
11054
  const prevMapTemps = ctx.currentMapTemps;
10375
11055
  const mapTemps = [];
10376
11056
  ctx.currentMapTemps = mapTemps;
@@ -10444,7 +11124,8 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
10444
11124
  );
10445
11125
  }
10446
11126
  workingStatements = removeMountEventCallbackDeclarations(statements, mountCallbackSinks).map(
10447
- (s) => rewriteAutoCallback(s, stableSet, ctx.currentComponentLocals, ctx),
11127
+ (s) =>
11128
+ rewriteAutoCallback(s, stableSet, ctx.currentComponentLocals, ctx, bodyInvariantLocals),
10448
11129
  );
10449
11130
  }
10450
11131
  if (returnedOutput) {
@@ -10496,9 +11177,21 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
10496
11177
  if (ctx.currentComponentLocals && ctx.mode !== 'server') {
10497
11178
  const renderReadNames = collectRenderReadNames(jsxNodes, ctx);
10498
11179
  if (renderReadNames.size > 0) {
10499
- workingStatements = workingStatements.map((statement) =>
10500
- rewriteAutoCalculation(statement, ctx.currentComponentLocals, renderReadNames, ctx),
10501
- );
11180
+ workingStatements = workingStatements.map((statement) => {
11181
+ const rewritten = rewriteAutoCalculation(
11182
+ statement,
11183
+ ctx.currentComponentLocals,
11184
+ renderReadNames,
11185
+ ctx,
11186
+ );
11187
+ if (ctx.autoMemo && rewritten !== statement) {
11188
+ (autoCalculatedDeclarations ??= new Map()).set(
11189
+ statement.declarations[0].id.name,
11190
+ statement,
11191
+ );
11192
+ }
11193
+ return rewritten;
11194
+ });
10502
11195
  }
10503
11196
  }
10504
11197
  workingStatements = parallelUseMemoizePass(workingStatements, ctx, name, creations, [], null);
@@ -10607,6 +11300,19 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
10607
11300
  // node itself always does.
10608
11301
  const prevFnOrigin = ctx._fnOrigin;
10609
11302
  ctx._fnOrigin = node.loc ? node : (node.id ?? prevFnOrigin);
11303
+ if (autoCalculatedDeclarations !== null) {
11304
+ const refs = collectAutoCalculatedRenderableRefs(
11305
+ statements,
11306
+ jsxNodes,
11307
+ autoCalculatedDeclarations,
11308
+ );
11309
+ if (refs !== null) {
11310
+ ctx.currentAutoCalculatedRenderableRefs = {
11311
+ nodes: refs,
11312
+ parent: previousAutoCalculatedRenderableRefs,
11313
+ };
11314
+ }
11315
+ }
10610
11316
  // Located origin for the function SHELL itself: synthetic helper shapes
10611
11317
  // (hoisted @for bodies, `__tsrx$N` sub-templates) carry no loc of their
10612
11318
  // own — their scaffolding inherits the nearest located enclosing origin.
@@ -10639,6 +11345,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
10639
11345
  ctx.currentBodyIsComponentScope = prevBodyIsComponentScope;
10640
11346
  ctx._inheritBody = prevInheritBody;
10641
11347
  ctx._fnOrigin = prevFnOrigin;
11348
+ ctx.currentAutoCalculatedRenderableRefs = previousAutoCalculatedRenderableRefs;
10642
11349
  ctx._foldedDirectiveCalls = prevFDC;
10643
11350
  ctx._valueDirectiveLowering = prevValueDirectiveLowering;
10644
11351
 
@@ -12390,6 +13097,12 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
12390
13097
  const warmKids = warmChildren.filter(
12391
13098
  (w) =>
12392
13099
  guardOk(w.guards, w.locals) &&
13100
+ // Server and universal-renderer warm plans have independent compilation
13101
+ // contexts. Only the DOM client's closed same-module graph can prove
13102
+ // that this edge and every descendant are synchronously render-only.
13103
+ (ctx.mode === 'server' ||
13104
+ ctx._universalRuntimeUnit != null ||
13105
+ ctx.componentInfo.get(w.compName)?.warmPotential !== false) &&
12393
13106
  !paramNames.has(w.compName) &&
12394
13107
  !(locals && locals.has(w.compName)) &&
12395
13108
  !(w.locals && w.locals.has(w.compName)) &&
@@ -13051,7 +13764,17 @@ function authoredHookMemoOf(stmt) {
13051
13764
  if (containsHookShapedCall(fn.body)) return null;
13052
13765
  if (fn.body.type === 'BlockStatement' && !blockBodyInlineSafe(fn.body)) return null;
13053
13766
  }
13054
- return { name, decl, kind: stmt.kind, fn, deps };
13767
+ return {
13768
+ name,
13769
+ decl,
13770
+ kind: stmt.kind,
13771
+ fn,
13772
+ deps,
13773
+ generatedInvariant:
13774
+ name === 'useCallback' &&
13775
+ callee._octaneGenerated === true &&
13776
+ callee._octaneLifetimeInvariant === true,
13777
+ };
13055
13778
  }
13056
13779
 
13057
13780
  // Compute statements writing the site's result into `target` (an expression
@@ -13127,7 +13850,7 @@ function lowerPuMemoDecl(stmt, ctx) {
13127
13850
  function lowerAuthoredHookMemo(stmt, ctx) {
13128
13851
  const entry = authoredHookMemoOf(stmt);
13129
13852
  if (entry === null) return null;
13130
- const { name, decl, kind, fn, deps } = entry;
13853
+ const { name, decl, kind, fn, deps, generatedInvariant } = entry;
13131
13854
  // Explicit `null` deps: recompute every render — no cache at all. A
13132
13855
  // useCallback degenerates to the factory itself; a useMemo evaluates
13133
13856
  // inline (via the shared block machinery when the body is a block).
@@ -13143,6 +13866,24 @@ function lowerAuthoredHookMemo(stmt, ctx) {
13143
13866
  }
13144
13867
  const names = hookMemoNames(ctx);
13145
13868
  const base = ctx.currentHookMemoOffset;
13869
+ if (generatedInvariant) {
13870
+ // A compiler-owned callback whose captures are lifetime-invariant cannot
13871
+ // miss after its first render. Its function value doubles as the init flag,
13872
+ // and publishing it immediately preserves identity across suspension.
13873
+ ctx.currentHookMemoOffset = base + 1;
13874
+ const valueCell = () => b.member(b.id(names.cache), hkNumLit(base), true);
13875
+ return [
13876
+ {
13877
+ ...stmt,
13878
+ declarations: [
13879
+ {
13880
+ ...decl,
13881
+ init: b.logical('??', valueCell(), b.assignment('=', valueCell(), fn)),
13882
+ },
13883
+ ],
13884
+ },
13885
+ ];
13886
+ }
13146
13887
  const k = deps.length;
13147
13888
  ctx.currentHookMemoOffset = base + k + 2;
13148
13889
  const cellRef = (i) => b.member(b.id(names.cache), hkNumLit(i), true);
@@ -13510,11 +14251,32 @@ function compileReturnJsxFunction(node, ctx, options) {
13510
14251
  ctx.currentMapTemps = mapTemps;
13511
14252
  let newStatements;
13512
14253
  try {
13513
- newStatements = (node.body.body || []).map((sourceStatement) => {
14254
+ const authoredStatements = node.body.body || [];
14255
+ const renderedRoots = authoredStatements
14256
+ .filter((statement) => statement.type === 'ReturnStatement' && isJsxNode(statement.argument))
14257
+ .map((statement) => statement.argument);
14258
+ const renderReadNames = collectRenderReadNames(renderedRoots, ctx);
14259
+ let renderScopeEstablished = false;
14260
+ newStatements = authoredStatements.map((sourceStatement) => {
14261
+ // Return-JSX functions keep their ordinary callable ABI. Introducing a
14262
+ // cache into a hookless function would make an existing direct call
14263
+ // require a render scope, so only declarations following an authored,
14264
+ // unconditional Octane hook may use the existing calculation lowering.
14265
+ const calculated = renderScopeEstablished
14266
+ ? rewriteAutoCalculation(sourceStatement, ctx.currentComponentLocals, renderReadNames, ctx)
14267
+ : sourceStatement;
14268
+ if (!renderScopeEstablished && sourceStatement.type === 'VariableDeclaration') {
14269
+ renderScopeEstablished = (sourceStatement.declarations || []).some(
14270
+ (declaration) => stableHookCallName(unwrapTsExpr(declaration.init)) !== null,
14271
+ );
14272
+ } else if (!renderScopeEstablished && sourceStatement.type === 'ExpressionStatement') {
14273
+ renderScopeEstablished =
14274
+ stableHookCallName(unwrapTsExpr(sourceStatement.expression)) !== null;
14275
+ }
13514
14276
  // A return-based component's undefined output is ambiguous with the compiled
13515
14277
  // void-body signal at runtime. Preserve JSX roots for the specialized lowering
13516
14278
  // below, but normalize every other owned return to an explicit empty value.
13517
- const s = normalizeOwnRenderableReturns(sourceStatement, true);
14279
+ const s = normalizeOwnRenderableReturns(calculated, true);
13518
14280
  const prepared =
13519
14281
  s.type === 'ReturnStatement' && s.argument && isJsxNode(s.argument)
13520
14282
  ? s
@@ -13535,6 +14297,30 @@ function compileReturnJsxFunction(node, ctx, options) {
13535
14297
  ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
13536
14298
  ctx.currentMapTemps = prevMapTemps;
13537
14299
  }
14300
+ if (
14301
+ ctx.autoMemo &&
14302
+ ctx.privateUnescapedComponents.has(name) &&
14303
+ (node.params?.length ?? 0) === 0 &&
14304
+ (node.body.body?.length ?? 0) === 1 &&
14305
+ isPlainHostRoot(node.body.body[0]?.argument) &&
14306
+ compInlinedSubs.length === 0 &&
14307
+ mapTemps.length === 0 &&
14308
+ cssHash === null
14309
+ ) {
14310
+ const descriptor = newStatements[0]?.argument;
14311
+ const renderer = descriptor?.arguments?.[0];
14312
+ const props = descriptor?.arguments?.[1];
14313
+ if (
14314
+ descriptor?.type === 'CallExpression' &&
14315
+ descriptor.callee?.type === 'Identifier' &&
14316
+ descriptor.callee.name === '_$createElement' &&
14317
+ descriptor.arguments.length === 2 &&
14318
+ renderer?.type === 'Identifier' &&
14319
+ isStaticFragmentRendererProps(props, ctx)
14320
+ ) {
14321
+ ctx.componentInfo.get(name).staticFragmentRenderer = { name: renderer.name, props };
14322
+ }
14323
+ }
13538
14324
  // The rebuilt function shell maps to the authored declaration. Hoisted
13539
14325
  // helper fns (compInlinedSubs — filled by the statement mapping above) are
13540
14326
  // function DECLARATION nodes embedded at the top of the body, matching the
@@ -13714,6 +14500,57 @@ function objectProp(hn, valNode) {
13714
14500
  return b.prop('init', b.id(hn), valNode);
13715
14501
  }
13716
14502
 
14503
+ // A bare, immutable same-module component needs no descriptor when its JSX is
14504
+ // consumed immediately by a returned host. Keep every value/props boundary on
14505
+ // the ordinary path: only this attribute-free call can remain in the template
14506
+ // without moving authored expression evaluation into its hoisted renderer.
14507
+ function isStaticFragmentRendererProps(props, ctx) {
14508
+ if (props?.type !== 'ObjectExpression') return false;
14509
+ for (const property of props.properties || []) {
14510
+ if (
14511
+ property.type !== 'Property' ||
14512
+ property.kind !== 'init' ||
14513
+ property.computed ||
14514
+ property.method ||
14515
+ property.shorthand
14516
+ ) {
14517
+ return false;
14518
+ }
14519
+ const descriptor = property.value;
14520
+ const component = descriptor?.arguments?.[0];
14521
+ const componentProps = descriptor?.arguments?.[1];
14522
+ if (
14523
+ descriptor?.type !== 'CallExpression' ||
14524
+ descriptor.callee?.type !== 'Identifier' ||
14525
+ descriptor.callee.name !== '_$createElement' ||
14526
+ descriptor.arguments.length !== 2 ||
14527
+ component?.type !== 'Identifier' ||
14528
+ !ctx.privateUnescapedComponents.has(component.name) ||
14529
+ componentProps?.type !== 'ObjectExpression' ||
14530
+ componentProps.properties.length !== 0
14531
+ ) {
14532
+ return false;
14533
+ }
14534
+ }
14535
+ return true;
14536
+ }
14537
+
14538
+ function isStaticReturnedFragmentComponent(node, ctx) {
14539
+ if (!ctx.autoMemo || ctx._foldCtx?.immediateRenderedOutput !== true) return false;
14540
+ const name = tagBindingName(node);
14541
+ if (
14542
+ name === null ||
14543
+ ctx.currentComponentLocals == null ||
14544
+ ctx.currentComponentLocals.has(name) ||
14545
+ !ctx.privateUnescapedComponents.has(name) ||
14546
+ ctx.componentInfo.get(name)?.staticFragmentRenderer === undefined
14547
+ ) {
14548
+ return false;
14549
+ }
14550
+ const attrs = node.attributes || node.openingElement?.attributes || [];
14551
+ return attrs.length === 0 && (node.children || []).length === 0;
14552
+ }
14553
+
13717
14554
  // Walk a host element or JSX fragment, replacing each DYNAMIC part (an
13718
14555
  // attribute/child expression) with `props.hN` and collecting
13719
14556
  // `{ hN: <originalExpr> }` into `holeProps`. Static structure (tag, literal attrs,
@@ -13831,6 +14668,22 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
13831
14668
  // a hole too, since it may be a local/member/dynamic tag that the hoisted
13832
14669
  // renderer cannot reference directly.
13833
14670
  newChildren.push(extractFragmentComponent(child, ctx, holeProps, childNs));
14671
+ } else if (isComponentTag(child) && isStaticReturnedFragmentComponent(child, ctx)) {
14672
+ // The original function remains callable and still returns its authored
14673
+ // descriptor. Only this nonescaping call can reuse its already-hoisted,
14674
+ // hookless fragment directly through the existing lite component ABI.
14675
+ const extracted = extractFragment(child, ctx, holeProps, childNs);
14676
+ newChildren.push({
14677
+ ...extracted,
14678
+ openingElement: {
14679
+ ...extracted.openingElement,
14680
+ metadata: {
14681
+ ...extracted.openingElement.metadata,
14682
+ staticFragmentRenderer: ctx.componentInfo.get(tagBindingName(child))
14683
+ .staticFragmentRenderer,
14684
+ },
14685
+ },
14686
+ });
13834
14687
  } else if (isComponentTag(child)) {
13835
14688
  const hn = `h${holeProps.length}`;
13836
14689
  holeProps.push(
@@ -16108,6 +16961,7 @@ function emitAutoMemoRegion(
16108
16961
  contextAware,
16109
16962
  depNode,
16110
16963
  initValue = null,
16964
+ restoreCachedContext = false,
16111
16965
  ) {
16112
16966
  const cell = allocAutoMemoCell(ctx, dependencies.length + (contextAware ? 1 : 0));
16113
16967
  const contextIndex = contextAware ? cell.base + dependencies.length : null;
@@ -16157,7 +17011,15 @@ function emitAutoMemoRegion(
16157
17011
  }
16158
17012
  ctx.runtimeNeeded.add('compilerCacheContext');
16159
17013
  const cacheContextCall = () =>
16160
- b.call('_$compilerCacheContext', b.id('__s'), b.literal(slotIndex), cacheAt(contextIndex));
17014
+ restoreCachedContext
17015
+ ? b.call(
17016
+ '_$compilerCacheContext',
17017
+ b.id('__s'),
17018
+ b.literal(slotIndex),
17019
+ cacheAt(contextIndex),
17020
+ b.literal(true),
17021
+ )
17022
+ : b.call('_$compilerCacheContext', b.id('__s'), b.literal(slotIndex), cacheAt(contextIndex));
16161
17023
  return b.block([
16162
17024
  ...depDecls,
16163
17025
  b.if(
@@ -16773,10 +17635,11 @@ function planJsx(
16773
17635
  const elVar = ensureVar(c.hostPath || []);
16774
17636
  c.elVar = elVar;
16775
17637
  const org = c.origin ?? planOrigin;
16776
- if (!noTemplate) {
17638
+ const key = `_${hostKey}$${c.id}`;
17639
+ if (!noTemplate && bag.host(key, elVar)) {
16777
17640
  mountLines.push(
16778
17641
  inheritOriginLoc(
16779
- b.stmt(b.assignment('=', b.id(bag.local(`_${hostKey}$${c.id}`)), hostVarNode(elVar))),
17642
+ b.stmt(b.assignment('=', b.id(bag.local(key)), hostVarNode(elVar))),
16780
17643
  org,
16781
17644
  ),
16782
17645
  );
@@ -17035,11 +17898,26 @@ function planJsx(
17035
17898
  const pushAfterStmt = (id, org, node) => pushAfter(id, inheritOriginLoc(node, org));
17036
17899
  for (const fc of forCalls) {
17037
17900
  const isMappedList = fc.mapMethodExpr !== null;
17038
- ctx.runtimeNeeded.add(isMappedList ? 'mapSlot' : 'forBlock');
17901
+ const forHelper = isMappedList
17902
+ ? fc.hostMountSafe
17903
+ ? 'fastMapSlot'
17904
+ : 'mapSlot'
17905
+ : fc.keyedSelectionIndex >= 0
17906
+ ? fc.hostMountSafe
17907
+ ? 'fastKeyedForBlock'
17908
+ : 'keyedForBlock'
17909
+ : fc.hostMountSafe
17910
+ ? 'fastForBlock'
17911
+ : 'forBlock';
17912
+ ctx.runtimeNeeded.add(forHelper);
17913
+ if (isMappedList && fc.hostMountSafe && fc.autoMemoDeps !== null) {
17914
+ // Memoized maps query the original native-array guard before dispatch.
17915
+ ctx.runtimeNeeded.add('mapSlot');
17916
+ }
17039
17917
  const slotIndex = fc.slotIndex;
17040
17918
  const org = fc.origin ?? planOrigin;
17041
17919
  registerDirectiveOrigin(ctx, org, [
17042
- isMappedList ? '_$mapSlot' : '_$forBlock',
17920
+ rtAlias(forHelper),
17043
17921
  fc.keyHelper,
17044
17922
  fc.bodyHelper,
17045
17923
  fc.emptyHelper,
@@ -17052,13 +17930,19 @@ function planJsx(
17052
17930
  // for survivors when deps unchanged this render),
17053
17931
  // bit 3 = indexIndependent (body binds no `index` → a pure reorder
17054
17932
  // that only changes a survivor's position need not re-render it),
17055
- // bit 4 = SSR emitted markerless direct-host items; hydrate them by root.
17933
+ // bit 4 = SSR emitted markerless direct-host items; hydrate them by root,
17934
+ // bit 5 = conditional item bodies require their active scope,
17935
+ // bits 6+ = the zero-based keyedForBlock selection dependency index.
17936
+ // keyedForBlock itself identifies a selection, including dependency zero;
17937
+ // ordinary forBlock reuses bit 5 without an argument or tuple allocation.
17056
17938
  const flags =
17057
17939
  (fc.pure ? 1 : 0) |
17058
17940
  (fc.singleRoot ? 2 : 0) |
17059
17941
  (fc.depEligible ? 4 : 0) |
17060
17942
  (fc.indexIndependent ? 8 : 0) |
17061
- (fc.ssrMarkerless ? 16 : 0);
17943
+ (fc.ssrMarkerless ? 16 : 0) |
17944
+ (fc.requiresScope ? 32 : 0) |
17945
+ (fc.keyedSelectionIndex >= 0 ? fc.keyedSelectionIndex << 6 : 0);
17062
17946
  // Arg layout: forBlock(__s, slot, host, items, keyFn, body, flags?, deps?,
17063
17947
  // emptyBody?, anchor?, ownEnd?).
17064
17948
  // Optional args backfill positionally: `flags`/`deps` placeholders
@@ -17117,7 +18001,7 @@ function planJsx(
17117
18001
  if (fc.autoMemoDeps !== null) {
17118
18002
  const mappedCall = b.stmt(
17119
18003
  b.call(
17120
- '_$mapSlot',
18004
+ rtAlias(forHelper),
17121
18005
  b.id('__s'),
17122
18006
  b.literal(slotIndex),
17123
18007
  hostExpr(),
@@ -17159,7 +18043,7 @@ function planJsx(
17159
18043
  org,
17160
18044
  b.stmt(
17161
18045
  b.call(
17162
- '_$mapSlot',
18046
+ rtAlias(forHelper),
17163
18047
  b.id('__s'),
17164
18048
  b.literal(slotIndex),
17165
18049
  hostExpr(),
@@ -17188,7 +18072,7 @@ function planJsx(
17188
18072
  slotIndex,
17189
18073
  b.stmt(
17190
18074
  b.call(
17191
- '_$forBlock',
18075
+ rtAlias(forHelper),
17192
18076
  b.id('__s'),
17193
18077
  b.literal(slotIndex),
17194
18078
  hostExpr(),
@@ -17207,7 +18091,7 @@ function planJsx(
17207
18091
  org,
17208
18092
  b.stmt(
17209
18093
  b.call(
17210
- '_$forBlock',
18094
+ rtAlias(forHelper),
17211
18095
  b.id('__s'),
17212
18096
  b.literal(slotIndex),
17213
18097
  hostExpr(),
@@ -17327,6 +18211,59 @@ function planJsx(
17327
18211
  continue;
17328
18212
  }
17329
18213
  ctx.runtimeNeeded.add('setText');
18214
+ const updateHole = () =>
18215
+ b.if(
18216
+ b.logical('||', b.id('_o'), b.binary('!==', chp(), V())),
18217
+ b.block([
18218
+ b.const('_t', chv()),
18219
+ b.if(
18220
+ andChain([
18221
+ b.binary('!=', b.id('_t'), b.literal(null)),
18222
+ b.unary('!', b.id('_o')),
18223
+ b.binary('!==', V(), b.literal(null)),
18224
+ ]),
18225
+ b.stmt(b.call('_$setText', b.id('_t'), V())),
18226
+ b.stmt(
18227
+ b.assignment(
18228
+ '=',
18229
+ chv(),
18230
+ b.call(
18231
+ '_$childTextHole',
18232
+ b.id('__s'),
18233
+ b.literal(slotIndex),
18234
+ hostExpr(),
18235
+ V(),
18236
+ b.id('_t'),
18237
+ ),
18238
+ ),
18239
+ ),
18240
+ ),
18241
+ b.stmt(b.assignment('=', chp(), V())),
18242
+ ]),
18243
+ null,
18244
+ );
18245
+ const ordinary = updateHole();
18246
+ let update = ordinary;
18247
+ if (cc.autoMemoValue === true) {
18248
+ // Only a compiler-owned, non-escaping plain data array may skip
18249
+ // descriptor reconciliation. Accessor-backed, nested, or deferred
18250
+ // children must still be observed on every parent render; the
18251
+ // runtime caches eligibility by immutable snapshot identity.
18252
+ ctx.runtimeNeeded.add('compilerCacheArray');
18253
+ const cached = emitAutoMemoRegion(
18254
+ ctx,
18255
+ ['_v'],
18256
+ slotIndex,
18257
+ updateHole(),
18258
+ // Array → scalar → the same array must reconstruct the list.
18259
+ b.binary('!==', chp(), V()),
18260
+ true,
18261
+ undefined,
18262
+ null,
18263
+ true,
18264
+ );
18265
+ update = b.if(b.call('_$compilerCacheArray', V(), chp()), cached, ordinary);
18266
+ }
17330
18267
  pushAfterStmt(
17331
18268
  cc.id,
17332
18269
  org,
@@ -17344,36 +18281,7 @@ function planJsx(
17344
18281
  ),
17345
18282
  ),
17346
18283
  ),
17347
- b.if(
17348
- b.logical('||', b.id('_o'), b.binary('!==', chp(), V())),
17349
- b.block([
17350
- b.const('_t', chv()),
17351
- b.if(
17352
- andChain([
17353
- b.binary('!=', b.id('_t'), b.literal(null)),
17354
- b.unary('!', b.id('_o')),
17355
- b.binary('!==', V(), b.literal(null)),
17356
- ]),
17357
- b.stmt(b.call('_$setText', b.id('_t'), V())),
17358
- b.stmt(
17359
- b.assignment(
17360
- '=',
17361
- chv(),
17362
- b.call(
17363
- '_$childTextHole',
17364
- b.id('__s'),
17365
- b.literal(slotIndex),
17366
- hostExpr(),
17367
- V(),
17368
- b.id('_t'),
17369
- ),
17370
- ),
17371
- ),
17372
- ),
17373
- b.stmt(b.assignment('=', chp(), V())),
17374
- ]),
17375
- null,
17376
- ),
18284
+ update,
17377
18285
  ]),
17378
18286
  );
17379
18287
  continue;
@@ -17809,6 +18717,7 @@ function bagLetter(i) {
17809
18717
  function makeBag() {
17810
18718
  const fields = [];
17811
18719
  const byKey = new Map();
18720
+ const byHost = new Map();
17812
18721
  const reg = (key, constExpr) => {
17813
18722
  let r = byKey.get(key);
17814
18723
  if (r === undefined) {
@@ -17831,6 +18740,16 @@ function makeBag() {
17831
18740
  return {
17832
18741
  /** Mount-write target for `key` — the pre-declared local. */
17833
18742
  local: (key) => reg(key, undefined).local,
18743
+ /** Register one immutable DOM-host field; aliases reuse its first mount write. */
18744
+ host: (key, host) => {
18745
+ const existing = byHost.get(host);
18746
+ if (existing !== undefined) {
18747
+ byKey.set(key, existing);
18748
+ return false;
18749
+ }
18750
+ byHost.set(host, reg(key, undefined));
18751
+ return true;
18752
+ },
17834
18753
  /** Seed `key` with a constant expression (no local, no mount write). */
17835
18754
  constField: (key, expr) => {
17836
18755
  reg(key, expr);
@@ -17975,8 +18894,10 @@ function emitDeferredMount(bind, elVar, bag) {
17975
18894
  if (!(bind.kind === 'class' && bind.fresh)) {
17976
18895
  bag.constField(bind.kind === 'style' ? `_sty$${bind.id}` : `_prev$${bind.id}`, 'undefined');
17977
18896
  }
18897
+ const key = `_el$${bind.id}`;
18898
+ if (!bag.host(key, elVar)) return null;
17978
18899
  return inheritOriginLoc(
17979
- b.stmt(b.assignment('=', b.id(bag.local(`_el$${bind.id}`)), hostVarNode(elVar))),
18900
+ b.stmt(b.assignment('=', b.id(bag.local(key)), hostVarNode(elVar))),
17980
18901
  bindingOrigin(bind),
17981
18902
  );
17982
18903
  }
@@ -17994,6 +18915,9 @@ function emitBindingMount(bind, elVar, bag) {
17994
18915
  const st = (node) => inheritOriginLoc(node, org);
17995
18916
  const el = () => hostVarNode(elVar);
17996
18917
  const local = (key) => b.id(bag.local(key));
18918
+ const hostKey = `_el$${bind.id}`;
18919
+ const mountHost = () =>
18920
+ bag.host(hostKey, elVar) ? [b.stmt(b.assignment('=', local(hostKey), el()))] : [];
17997
18921
  const V = () => b.id('_v');
17998
18922
  // The tokens that NAME this binding's lowering carry the authored attribute
17999
18923
  // name; everything else in the call maps to the value expression.
@@ -18011,10 +18935,7 @@ function emitBindingMount(bind, elVar, bag) {
18011
18935
  return st(b.stmt(b.call('_$markNativeChangeDiagnosticStatic', el())));
18012
18936
  }
18013
18937
  if (bind.kind === 'nativeChangeRuntime') {
18014
- return [
18015
- st(b.stmt(b.assignment('=', local(`_el$${bind.id}`), el()))),
18016
- st(b.stmt(b.call('_$queueNativeChangeDiagnostic', el()))),
18017
- ];
18938
+ return [...mountHost().map(st), st(b.stmt(b.call('_$queueNativeChangeDiagnostic', el())))];
18018
18939
  }
18019
18940
  switch (bind.kind) {
18020
18941
  case 'textOnlyChild': {
@@ -18036,7 +18957,7 @@ function emitBindingMount(bind, elVar, bag) {
18036
18957
  b.block([
18037
18958
  b.const('_v', bind.expr),
18038
18959
  b.stmt(b.call('_$setDangerouslySetInnerHTML', el(), V())),
18039
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
18960
+ ...mountHost(),
18040
18961
  b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
18041
18962
  ]),
18042
18963
  );
@@ -18065,7 +18986,7 @@ function emitBindingMount(bind, elVar, bag) {
18065
18986
  return st(
18066
18987
  b.block([
18067
18988
  b.stmt(b.call('_$setDangerouslySetInnerHTMLSources', el(), sources)),
18068
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
18989
+ ...mountHost(),
18069
18990
  ]),
18070
18991
  );
18071
18992
  }
@@ -18074,14 +18995,11 @@ function emitBindingMount(bind, elVar, bag) {
18074
18995
  b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
18075
18996
  );
18076
18997
  return st(
18077
- b.block([
18078
- b.stmt(b.call('_$setFormControlSources', el(), sources)),
18079
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
18080
- ]),
18998
+ b.block([b.stmt(b.call('_$setFormControlSources', el(), sources)), ...mountHost()]),
18081
18999
  );
18082
19000
  }
18083
19001
  case 'hostCommit': {
18084
- const elLocal = local(`_el$${bind.id}`);
19002
+ const hostMount = mountHost();
18085
19003
  const propsLocal = local(`_host$${bind.id}`);
18086
19004
  const sources = commitSourceRows(bind.sources, (binding, spread) =>
18087
19005
  b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
@@ -18112,7 +19030,7 @@ function emitBindingMount(bind, elVar, bag) {
18112
19030
  );
18113
19031
  return st(
18114
19032
  b.block([
18115
- b.stmt(b.assignment('=', elLocal, el())),
19033
+ ...hostMount,
18116
19034
  b.stmt(
18117
19035
  b.assignment(
18118
19036
  '=',
@@ -18155,7 +19073,7 @@ function emitBindingMount(bind, elVar, bag) {
18155
19073
  b.block([
18156
19074
  b.const('_v', bind.expr),
18157
19075
  b.stmt(b.call(callee(), el(), nameLit(), V())),
18158
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
19076
+ ...mountHost(),
18159
19077
  b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
18160
19078
  ]),
18161
19079
  );
@@ -18172,12 +19090,7 @@ function emitBindingMount(bind, elVar, bag) {
18172
19090
  // update must re-run every render to reassert drift (React's
18173
19091
  // controlled contract). Not in DEFERRABLE_MOUNT_KINDS: the mount
18174
19092
  // runs inside the hydration window and arms the element.
18175
- return st(
18176
- b.block([
18177
- b.stmt(b.call(callee(), el(), bind.expr)),
18178
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
18179
- ]),
18180
- );
19093
+ return st(b.block([b.stmt(b.call(callee(), el(), bind.expr)), ...mountHost()]));
18181
19094
  }
18182
19095
  case 'autoFocus': {
18183
19096
  // Mount-only (React ignores later autoFocus changes); the focus
@@ -18187,11 +19100,7 @@ function emitBindingMount(bind, elVar, bag) {
18187
19100
  case 'class': {
18188
19101
  // On SVG/MathML hosts the `className` property is read-only — fall back
18189
19102
  // to setAttribute. Compile-time choice, zero runtime branching.
18190
- const body = [
18191
- b.const('_v', bind.expr),
18192
- b.stmt(b.call(callee(), el(), V())),
18193
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
18194
- ];
19103
+ const body = [b.const('_v', bind.expr), b.stmt(b.call(callee(), el(), V())), ...mountHost()];
18195
19104
  if (!bind.fresh) body.push(b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())));
18196
19105
  return st(b.block(body));
18197
19106
  }
@@ -18200,7 +19109,7 @@ function emitBindingMount(bind, elVar, bag) {
18200
19109
  b.block([
18201
19110
  b.const('_v', bind.expr),
18202
19111
  b.stmt(b.call(callee(), el(), V(), undefinedNode())),
18203
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
19112
+ ...mountHost(),
18204
19113
  b.stmt(b.assignment('=', local(`_sty$${bind.id}`), V())),
18205
19114
  ]),
18206
19115
  );
@@ -18223,7 +19132,7 @@ function emitBindingMount(bind, elVar, bag) {
18223
19132
  : [];
18224
19133
  // Register the mount locals BEFORE the cleanup closure resolves their
18225
19134
  // bag letters (letter() throws for a never-mounted field).
18226
- const elLocal = local(`_el$${bind.id}`);
19135
+ const hostMount = mountHost();
18227
19136
  const spLocal = local(`_sp$${bind.id}`);
18228
19137
  const cleanup = b.arrow(
18229
19138
  [],
@@ -18250,7 +19159,7 @@ function emitBindingMount(bind, elVar, bag) {
18250
19159
  b.block([
18251
19160
  b.const('_v', bind.expr),
18252
19161
  b.stmt(b.call('_$setSpread', el(), V(), undefinedNode(), b.id('__s'), ...flags)),
18253
- b.stmt(b.assignment('=', elLocal, el())),
19162
+ ...hostMount,
18254
19163
  b.stmt(b.assignment('=', spLocal, V())),
18255
19164
  cleanupsPush(cleanup),
18256
19165
  ]),
@@ -18264,7 +19173,7 @@ function emitBindingMount(bind, elVar, bag) {
18264
19173
  b.assignment('=', b.member(el(), slotKeyLiteral(bind), true), value),
18265
19174
  );
18266
19175
  if (bind.mountOnly) return st(slotAssign);
18267
- return [st(b.stmt(b.assignment('=', local(`_el$${bind.id}`), el()))), st(slotAssign)];
19176
+ return [...mountHost().map(st), st(slotAssign)];
18268
19177
  }
18269
19178
  case 'formAction': {
18270
19179
  // <form action={fn}> / <button formAction={fn}>: wire the submit handler
@@ -18274,7 +19183,7 @@ function emitBindingMount(bind, elVar, bag) {
18274
19183
  b.block([
18275
19184
  b.const('_v', bind.expr),
18276
19185
  b.stmt(b.call(callee(), el(), nameLit(), V(), undefinedNode())),
18277
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
19186
+ ...mountHost(),
18278
19187
  b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
18279
19188
  ]),
18280
19189
  );
@@ -18314,6 +19223,7 @@ function emitBindingMount(bind, elVar, bag) {
18314
19223
  // cleanup read because updates re-point it. Assigning both bag locals inside
18315
19224
  // the queue call evaluates each mount value once while avoiding throwaway
18316
19225
  // temporaries; Suspense still receives the exact ref/target pair.
19226
+ const initializeHost = bag.host(hostKey, elVar);
18317
19227
  return st(
18318
19228
  b.block([
18319
19229
  b.stmt(
@@ -18321,7 +19231,7 @@ function emitBindingMount(bind, elVar, bag) {
18321
19231
  '_$queueRefAttach',
18322
19232
  b.id('__s'),
18323
19233
  b.assignment('=', local(`_ref$${bind.id}`), bind.expr),
18324
- b.assignment('=', local(`_el$${bind.id}`), el()),
19234
+ initializeHost ? b.assignment('=', local(hostKey), el()) : local(hostKey),
18325
19235
  ),
18326
19236
  ),
18327
19237
  cleanupsPush(
@@ -19616,8 +20526,21 @@ function emitElementHtml(
19616
20526
  // is unreachable from the output: the key's map position resolves to
19617
20527
  // the HANDLER expression, and the name itself emits nothing.
19618
20528
  registerExactOrigin(ctx, attr.name, attr.name?.end, [`'${slotKey}'`, `"${slotKey}"`]);
19619
- if (capture) ctx.capturedEvents.add(eventName);
19620
- else ctx.delegatedEvents.add(eventName);
20529
+ if (capture) {
20530
+ ctx.capturedEvents.add(eventName);
20531
+ if (ctx.currentComponentOwner !== null) {
20532
+ ctx.currentComponentOwner.capturedEvents.add(eventName);
20533
+ } else {
20534
+ ctx.unownedCapturedEvents.add(eventName);
20535
+ }
20536
+ } else {
20537
+ ctx.delegatedEvents.add(eventName);
20538
+ if (ctx.currentComponentOwner !== null) {
20539
+ ctx.currentComponentOwner.delegatedEvents.add(eventName);
20540
+ } else {
20541
+ ctx.unownedDelegatedEvents.add(eventName);
20542
+ }
20543
+ }
19621
20544
  // Hot-path optimisation: `() => fn(arg, …)` arrows with zero params get
19622
20545
  // compiled to a `{ fn, args }` bundle so the runtime can identity-diff
19623
20546
  // fn + each arg and skip the property reassignment when nothing
@@ -20613,7 +21536,7 @@ function soleRenderPropChild(children) {
20613
21536
  // expression remains AST throughout so a nested `() => @{…}` sub-template
20614
21537
  // hoists (and server-mode `use(thenable)` calls get their stable keys).
20615
21538
  function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash) {
20616
- return {
21539
+ const child = {
20617
21540
  id: ctx.nextHelperId++,
20618
21541
  loc: devLoc(ctx, expr),
20619
21542
  origin: expr,
@@ -20625,6 +21548,27 @@ function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash) {
20625
21548
  inlinedSubs,
20626
21549
  ),
20627
21550
  };
21551
+ if (
21552
+ ctx.autoMemo &&
21553
+ // Imported calculations do not expose their return type. Restrict the
21554
+ // array guard to deferred component-child bodies, where cached descriptor
21555
+ // lists otherwise cross an expensive value-position boundary; an ordinary
21556
+ // scalar directly in its owning component must not acquire this region.
21557
+ ctx.currentBodyIsComponentScope !== true &&
21558
+ expr?.type === 'Identifier'
21559
+ ) {
21560
+ for (
21561
+ let proof = ctx.currentAutoCalculatedRenderableRefs;
21562
+ proof !== null;
21563
+ proof = proof.parent
21564
+ ) {
21565
+ if (proof.nodes.has(expr)) {
21566
+ child.autoMemoValue = true;
21567
+ break;
21568
+ }
21569
+ }
21570
+ }
21571
+ return child;
20628
21572
  }
20629
21573
 
20630
21574
  const AST_STRUCTURAL_KEY_SKIP = new Set([
@@ -20761,7 +21705,10 @@ function makeCompCall(
20761
21705
  ) {
20762
21706
  const id = ctx.nextHelperId++;
20763
21707
  const compName = tagBindingName(node);
20764
- const compNode = tagExprNode(node);
21708
+ const staticFragmentRenderer = node.openingElement?.metadata?.staticFragmentRenderer;
21709
+ const compNode = staticFragmentRenderer
21710
+ ? inheritOriginLoc(b.id(staticFragmentRenderer.name), node.openingElement?.name || node.id)
21711
+ : tagExprNode(node);
20765
21712
  // `</Card>` emits nothing — lend it the opening name's generated ranges so a
20766
21713
  // closing tag is reachable for components the way it is for host elements.
20767
21714
  registerOriginAlias(ctx, node.closingElement?.name, node.id || node.openingElement?.name);
@@ -20878,7 +21825,7 @@ function makeCompCall(
20878
21825
  }
20879
21826
 
20880
21827
  // The props object as a node; the call-site emit embeds it directly.
20881
- const propsExpr = inheritOriginLoc(b.object(propNodes), node);
21828
+ const propsExpr = staticFragmentRenderer?.props ?? inheritOriginLoc(b.object(propNodes), node);
20882
21829
 
20883
21830
  // Design (c) v0: decide whether the call site can use componentSlotLite
20884
21831
  // (Scope-only, no Block / no Comment markers / no CompSlot wrapper).
@@ -20909,7 +21856,12 @@ function makeCompCall(
20909
21856
  // elides iff the callee carries the definition-site `$$singleRoot` stamp
20910
21857
  // (docs/comment-marker-elision-plan.md M1).
20911
21858
  let maybeSingleRoot = false;
20912
- if (ctx.componentInfo && compName !== null) {
21859
+ if (staticFragmentRenderer) {
21860
+ // The renderer is already a void, hookless component body. A memo boundary
21861
+ // would force a full Block and could hide a hookful descendant's update.
21862
+ liteEligible = true;
21863
+ voidComponent = true;
21864
+ } else if (ctx.componentInfo && compName !== null) {
20913
21865
  const callSiteOk = !hasSpreadProp && !hasChildrenProp;
20914
21866
  const calleeInfo = ctx.componentInfo.get(compName);
20915
21867
  if (calleeInfo) {
@@ -21196,6 +22148,96 @@ function makeSwitchCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = nul
21196
22148
  // for-of inside element children → forBlock call
21197
22149
  // ===========================================================================
21198
22150
 
22151
+ /**
22152
+ * Prove that one parent dependency affects a keyed host row only through its
22153
+ * root class's strict comparison with the row key. The runtime can then update
22154
+ * only the old/new selected blocks when every other dependency and the iterable
22155
+ * snapshot remain unchanged.
22156
+ *
22157
+ * This recognizes `selected === item.id` (or its reverse) under `key item.id`,
22158
+ * and the same proof for any other explicit, noncomputed item property.
22159
+ * Dynamic ternary arms, extra references to
22160
+ * `selected` anywhere in the row (including deferred event callbacks), spreads,
22161
+ * and anything other than one direct host root all fail closed.
22162
+ */
22163
+ function keyedSelectionDepIndex(itemName, keyBody, subStmts, runtimeDepNames, ctx) {
22164
+ if (subStmts.length !== 1 || !isPlainHostRoot(subStmts[0])) return -1;
22165
+ const root = subStmts[0];
22166
+ const attrs = root.attributes || root.openingElement?.attributes || [];
22167
+ if (attrs.some((attr) => attr.type !== 'Attribute' && attr.type !== 'JSXAttribute')) {
22168
+ return -1;
22169
+ }
22170
+ const classAttrs = attrs.filter((attr) => {
22171
+ const name = attr.name?.name || attr.name;
22172
+ return name === 'class' || name === 'className';
22173
+ });
22174
+ if (classAttrs.length !== 1) return -1;
22175
+ const value = classAttrs[0].value;
22176
+ const conditional = unwrapTsExpr(
22177
+ value?.type === 'JSXExpressionContainer' ? value.expression : value,
22178
+ );
22179
+ if (conditional?.type !== 'ConditionalExpression') return -1;
22180
+ const consequent = unwrapTsExpr(conditional.consequent);
22181
+ const alternate = unwrapTsExpr(conditional.alternate);
22182
+ if (
22183
+ consequent?.type !== 'Literal' ||
22184
+ typeof consequent.value !== 'string' ||
22185
+ alternate?.type !== 'Literal' ||
22186
+ typeof alternate.value !== 'string'
22187
+ ) {
22188
+ return -1;
22189
+ }
22190
+
22191
+ const test = unwrapTsExpr(conditional.test);
22192
+ if (test?.type !== 'BinaryExpression' || test.operator !== '===') return -1;
22193
+ const key = unwrapTsExpr(keyBody);
22194
+ if (
22195
+ key?.type !== 'MemberExpression' ||
22196
+ key.computed === true ||
22197
+ key.optional === true ||
22198
+ key.object?.type !== 'Identifier' ||
22199
+ key.object.name !== itemName ||
22200
+ key.property?.type !== 'Identifier'
22201
+ ) {
22202
+ return -1;
22203
+ }
22204
+ const keyProperty = key.property.name;
22205
+ const isItemKey = (expression) => {
22206
+ const member = unwrapTsExpr(expression);
22207
+ return (
22208
+ member?.type === 'MemberExpression' &&
22209
+ member.computed !== true &&
22210
+ member.optional !== true &&
22211
+ member.object?.type === 'Identifier' &&
22212
+ member.object.name === itemName &&
22213
+ member.property?.type === 'Identifier' &&
22214
+ member.property.name === keyProperty
22215
+ );
22216
+ };
22217
+ const left = unwrapTsExpr(test.left);
22218
+ const right = unwrapTsExpr(test.right);
22219
+ const selected = isItemKey(left) ? right : isItemKey(right) ? left : null;
22220
+ if (
22221
+ selected?.type !== 'Identifier' ||
22222
+ selected.name === itemName ||
22223
+ !ctx.currentComponentLocals?.has(selected.name)
22224
+ ) {
22225
+ return -1;
22226
+ }
22227
+ // Ignore this exact comparison operand, then use the existing scope-aware
22228
+ // reference walker to detect any other capture of the same outer binding.
22229
+ // Shadowed callback parameters therefore stay harmless while a real event
22230
+ // closure over the selection correctly disables the specialization.
22231
+ if (
22232
+ collectFreeIdentifiers(b.block(subStmts), new Set([itemName]), new Set([selected])).has(
22233
+ selected.name,
22234
+ )
22235
+ ) {
22236
+ return -1;
22237
+ }
22238
+ return runtimeDepNames.indexOf(selected.name);
22239
+ }
22240
+
21199
22241
  function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
21200
22242
  // `@for await (...)` (async iteration) has no meaning for the runtime's
21201
22243
  // synchronous keyed reconciler. The TSRX parser currently rejects the surface
@@ -21345,16 +22387,16 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21345
22387
  // - PURE: body closes over nothing parent-reactive, no hooks, no comps,
21346
22388
  // no control flow. Reconciler skips renderBlock when item ref + index
21347
22389
  // unchanged. Identified by `pure = true`.
21348
- // - DEP-PURE: body DOES close over parent locals but is otherwise as
21349
- // clean as PURE. The compiler emits an explicit deps array at the
21350
- // forBlock call site so the reconciler can do ONE deps-equality check
21351
- // per parent render and, if unchanged, treat the body as PURE for the
21352
- // survivor short-circuit. Saves the body call entirely for
21353
- // item-ref-and-index-stable survivors — no per-row snapshot work.
22390
+ // - DEP-PURE: body DOES close over parent locals but has no hooks or opaque
22391
+ // components. Proven host-only conditional content is permitted when the
22392
+ // runtime preserves the row's active scope. The compiler emits an explicit
22393
+ // deps array so the reconciler can do ONE equality check per parent render
22394
+ // and, if unchanged, skip item-ref-and-index-stable survivors entirely.
21354
22395
  // - NORMAL: anything else → body runs every render.
21355
22396
  let pure = false;
21356
22397
  const depNames = [];
21357
22398
  let depEligible = false;
22399
+ let requiresScope = false;
21358
22400
  let itemMemo = false;
21359
22401
  let itemMemoContextAware = false;
21360
22402
  let itemMemoWitnesses = [];
@@ -21528,8 +22570,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21528
22570
  // survivor shortcut has no compiler-cache epoch cell to consult).
21529
22571
  if (itemMemoContextAware && autoMemoDeps === null) itemMemo = false;
21530
22572
  const hostPure = !hasParentClosure && !hasHook && !hasNestedComp && !hasRenderCall;
22573
+ const conditionalHostDepEligible =
22574
+ ctx.autoMemo === true &&
22575
+ hasNestedComp &&
22576
+ hasParentClosure &&
22577
+ !hasHook &&
22578
+ !hasRenderCall &&
22579
+ node.nativeArrayMap === undefined &&
22580
+ autoMemoDeps !== null &&
22581
+ autoMemoDeps.every((name) => seenDeps.has(name)) &&
22582
+ !containsAutoMemoContextRead(bodyAst, ctx) &&
22583
+ hasOnlyHostConditionalItemBodies(subStmts);
21531
22584
  const hostDepEligible =
21532
- !hostPure && !hasHook && hasParentClosure && !hasNestedComp && !hasRenderCall;
22585
+ !hostPure &&
22586
+ !hasHook &&
22587
+ hasParentClosure &&
22588
+ (!hasNestedComp || conditionalHostDepEligible) &&
22589
+ !hasRenderCall;
21533
22590
  if (itemMemo && itemMemoWitnesses.length > 0) {
21534
22591
  pure = hostPure;
21535
22592
  depEligible = hostDepEligible;
@@ -21538,6 +22595,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21538
22595
  pure = hostPure || (itemMemo && depNames.length === 0);
21539
22596
  depEligible = !pure && !hasHook && (hostDepEligible || (itemMemo && depNames.length > 0));
21540
22597
  }
22598
+ requiresScope = depEligible && conditionalHostDepEligible;
21541
22599
  depNames.sort();
21542
22600
  }
21543
22601
 
@@ -21565,6 +22623,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21565
22623
  envNames === null
21566
22624
  ? depNames
21567
22625
  : [...envNames, ...depNames.filter((name) => !envNames.includes(name))];
22626
+ // Restrict targeted invalidation to the existing production-only pure-render
22627
+ // contract. The whole-list memo proof rules out render-time mutations and
22628
+ // opaque state, while DEP-PURE guarantees a hookless host-only item body.
22629
+ // Native `.map()` uses a distinct mapSlot ABI and stays on its current path.
22630
+ const keyedSelectionIndex =
22631
+ ctx.autoMemo === true &&
22632
+ depEligible &&
22633
+ !requiresScope &&
22634
+ !itemMemo &&
22635
+ autoMemoDeps !== null &&
22636
+ !isDestructured &&
22637
+ !node.index &&
22638
+ !emptyStmts &&
22639
+ node.nativeArrayMap === undefined &&
22640
+ node.key != null
22641
+ ? keyedSelectionDepIndex(itemName, keyFn.body, subStmts, runtimeDepNames, ctx)
22642
+ : -1;
21568
22643
  let emptyHelperName = 'null';
21569
22644
  if (emptyStmts) {
21570
22645
  emptyHelperName = hoistBodyHelper(
@@ -21646,6 +22721,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21646
22721
  }
21647
22722
  }
21648
22723
 
22724
+ const ssrMarkerless = isSsrMarkerlessForItem(node);
22725
+ const hostRootTag = subStmts[0]?.id?.name ?? subStmts[0]?.openingElement?.name?.name;
22726
+ const hostMountSafe =
22727
+ ctx.autoMemo === true &&
22728
+ ctx._universalRuntimeUnit == null &&
22729
+ (parentNs === 'html' || (parentNs === 'opaque' && HTML_ONLY_TAGS.has(hostRootTag))) &&
22730
+ !emptyStmts &&
22731
+ !isDestructured &&
22732
+ !node.index &&
22733
+ subStmts.length === 1 &&
22734
+ singleRoot &&
22735
+ ssrMarkerless &&
22736
+ !containsComponentCallOrControlFlow(subStmts) &&
22737
+ !containsRenderCall(subStmts) &&
22738
+ !containsAutoMemoUnsafeStructure(subStmts) &&
22739
+ isHostMountSafeTree(subStmts[0]);
22740
+
21649
22741
  const mapCall = node.nativeArrayMap || null;
21650
22742
  return {
21651
22743
  id: ctx.nextHelperId++,
@@ -21679,18 +22771,21 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21679
22771
  pure,
21680
22772
  singleRoot,
21681
22773
  singleRootExpr,
21682
- ssrMarkerless: isSsrMarkerlessForItem(node),
22774
+ ssrMarkerless,
22775
+ hostMountSafe,
21683
22776
  // The env union doubles as the deps array: emitted whenever the helpers
21684
22777
  // capture anything (Phase 2 — the runtime stamps it as block.extra), and
21685
22778
  // ALSO compared for the dep-pure survivor short-circuit when depEligible.
21686
22779
  // Component-local entries remain the tuple prefix the helpers destructure;
21687
22780
  // any appended import witnesses are comparison-only.
21688
22781
  depEligible,
22782
+ requiresScope,
21689
22783
  itemMemoWitnesses,
21690
22784
  itemMemoFlags,
21691
22785
  autoMemoDeps,
21692
22786
  autoMemoWitnesses,
21693
22787
  autoMemoContextAware,
22788
+ keyedSelectionIndex,
21694
22789
  depNames: runtimeDepNames,
21695
22790
  // True only when the header binds NO `index <name>` — the body then can't
21696
22791
  // observe an item's position, so a pure reorder (same item ref, position
@@ -21894,10 +22989,41 @@ function bodyContainsJsx(node) {
21894
22989
 
21895
22990
  /** @param {TemplateIR} ast */
21896
22991
  function allocTemplate(ctx, ast, ns = 0, frag = 0) {
22992
+ const { html, origins } = serializeTemplateIr(ast);
22993
+ const intern = !ctx.hmr && !ctx.dev && !ctx.profile;
22994
+ const bucket = intern ? ctx.internedTemplates.get(html) : undefined;
22995
+ const existing = Array.isArray(bucket)
22996
+ ? bucket.find((template) => template.ns === ns && template.frag === frag)
22997
+ : bucket?.ns === ns && bucket.frag === frag
22998
+ ? bucket
22999
+ : undefined;
23000
+ if (existing !== undefined) {
23001
+ if (ctx.inspect && origins !== null && existing.origins !== null) {
23002
+ for (const origin of origins) {
23003
+ const canonical = existing.origins.find(
23004
+ (entry) =>
23005
+ entry.kind === origin.kind && entry.start === origin.start && entry.end === origin.end,
23006
+ );
23007
+ if (canonical !== undefined) {
23008
+ registerOriginAlias(
23009
+ ctx,
23010
+ { start: origin.srcStart, end: origin.srcEnd },
23011
+ { start: canonical.srcStart },
23012
+ );
23013
+ }
23014
+ }
23015
+ }
23016
+ return existing.name;
23017
+ }
21897
23018
  const id = ctx.nextTemplateId++;
21898
23019
  const name = `_t$${id}`;
21899
- const { html, origins } = serializeTemplateIr(ast);
21900
- ctx.hoistedTemplates.push({ name, ast, html, ns, frag, origins });
23020
+ const template = { name, ast, html, ns, frag, origins };
23021
+ ctx.hoistedTemplates.push(template);
23022
+ if (intern) {
23023
+ if (bucket === undefined) ctx.internedTemplates.set(html, template);
23024
+ else if (Array.isArray(bucket)) bucket.push(template);
23025
+ else ctx.internedTemplates.set(html, [bucket, template]);
23026
+ }
21901
23027
  return name;
21902
23028
  }
21903
23029