octane 0.1.29 → 0.1.31

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,90 @@ function containsComponentCallOrControlFlow(stmts) {
3343
3465
  return found;
3344
3466
  }
3345
3467
 
3468
+ /**
3469
+ * A host-only conditional can share its containing row's immutable
3470
+ * item/dependency proof, including through a narrowly proven nested keyed list.
3471
+ * Entering either construct still needs the row's active scope. Lists without a
3472
+ * conditional, components, opaque list expressions, and every other
3473
+ * control-flow boundary remain on their existing path.
3474
+ */
3475
+ function hasOnlyHostConditionalItemBodies(stmts) {
3476
+ let hasConditional = false;
3477
+ let disallowed = false;
3478
+ const seen = new WeakSet();
3479
+ function walk(node) {
3480
+ if (disallowed || !node) return;
3481
+ if (Array.isArray(node)) {
3482
+ for (const child of node) walk(child);
3483
+ return;
3484
+ }
3485
+ if (typeof node !== 'object') return;
3486
+ const type = node.type;
3487
+ if (!type || seen.has(node)) return;
3488
+ seen.add(node);
3489
+ if (
3490
+ type === 'ArrowFunctionExpression' ||
3491
+ type === 'FunctionExpression' ||
3492
+ type === 'FunctionDeclaration'
3493
+ ) {
3494
+ return;
3495
+ }
3496
+ if ((type === 'Element' || type === 'JSXElement') && isComponentTag(node)) {
3497
+ disallowed = true;
3498
+ return;
3499
+ }
3500
+ if (type === 'IfStatement' || type === 'JSXIfExpression') {
3501
+ hasConditional = true;
3502
+ } else if (type === 'JSXForExpression') {
3503
+ const declaration = node.left;
3504
+ const item = declaration?.declarations?.[0]?.id;
3505
+ const key = unwrapTsExpr(node.key);
3506
+ if (
3507
+ node.await === true ||
3508
+ node.index != null ||
3509
+ node.nativeArrayMap !== undefined ||
3510
+ declaration?.type !== 'VariableDeclaration' ||
3511
+ declaration.declarations?.length !== 1 ||
3512
+ item?.type !== 'Identifier' ||
3513
+ !isAutoMemoCalculationDependency(node.right) ||
3514
+ key?.type !== 'MemberExpression' ||
3515
+ key.computed === true ||
3516
+ key.optional === true ||
3517
+ key.object?.type !== 'Identifier' ||
3518
+ key.object.name !== item.name ||
3519
+ key.property?.type !== 'Identifier'
3520
+ ) {
3521
+ disallowed = true;
3522
+ return;
3523
+ }
3524
+ } else if (
3525
+ type === 'ForStatement' ||
3526
+ type === 'ForInStatement' ||
3527
+ type === 'ForOfStatement' ||
3528
+ type === 'WhileStatement' ||
3529
+ type === 'DoWhileStatement' ||
3530
+ type === 'TryStatement' ||
3531
+ type === 'SwitchStatement' ||
3532
+ type === 'ActivityStatement' ||
3533
+ type === 'JSXTryExpression' ||
3534
+ type === 'JSXSwitchExpression' ||
3535
+ type === 'JSXActivityExpression' ||
3536
+ ((type === 'TSRXExpression' || type === 'JSXExpressionContainer') &&
3537
+ node.expression &&
3538
+ isCreatePortalCall(node.expression))
3539
+ ) {
3540
+ disallowed = true;
3541
+ return;
3542
+ }
3543
+ for (const key in node) {
3544
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
3545
+ walk(node[key]);
3546
+ }
3547
+ }
3548
+ for (const statement of stmts) walk(statement);
3549
+ return hasConditional && !disallowed;
3550
+ }
3551
+
3346
3552
  /**
3347
3553
  * Classify the narrow opaque shape that an automatically memoized keyed item
3348
3554
  * may safely contain: ordinary JSX component calls, but no template control
@@ -3535,15 +3741,16 @@ function containsRenderCall(stmts, memoCtx = null) {
3535
3741
  }
3536
3742
 
3537
3743
  // 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.
3744
+ // React Compiler key on — plus the exact `unstable_` and `UNSTABLE_` staging
3745
+ // prefixes bindings expose (`unstable_useRouterState` in @octanejs/remix-router
3746
+ // and `UNSTABLE_useTreeGridState` in @octanejs/aria). Enumerate the prefixes
3747
+ // rather than matching "any `_use`" so ordinary helpers are not mistaken for
3748
+ // hooks by spelling alone.
3542
3749
  //
3543
3750
  // Getting this wrong in the permissive direction is not a staleness bug: a
3544
3751
  // cache wrapped around a hook call freezes its subscription and its state cell
3545
3752
  // for the life of the component.
3546
- const HOOK_NAME_CONVENTION_RE = /^(?:unstable_)?use(?:$|[A-Z])/;
3753
+ const HOOK_NAME_CONVENTION_RE = /^(?:(?:unstable|UNSTABLE)_)?use(?:$|[A-Z])/;
3547
3754
 
3548
3755
  function isHookCalleeName(name) {
3549
3756
  return HOOK_NAME_CONVENTION_RE.test(name);
@@ -3716,6 +3923,202 @@ function collectImmutableModuleFunctions(body) {
3716
3923
  return declared;
3717
3924
  }
3718
3925
 
3926
+ // A child warm plan is useful only if that child can reach an async creation.
3927
+ // Same-module declarations are the only closed call graph we can prove: an
3928
+ // imported/dynamic component, custom hook, helper call, or lazy state initializer
3929
+ // may suspend behind an opaque boundary, so each keeps its existing warm edge.
3930
+ // A useState call with a primitive literal initializer and publishing its stable
3931
+ // setter are synchronous, so neither turns a synchronous tree into a warm plan.
3932
+ function classifySameModuleWarmPotential(ctx) {
3933
+ for (const [, info] of ctx.componentInfo) {
3934
+ const component = info.node;
3935
+ const statements = component.body.body || [];
3936
+ const locals = collectComponentLocals(component);
3937
+ const invariant = computeInvariantLocals(statements, locals, false);
3938
+ const dependencies = new Set();
3939
+ const seen = new WeakSet();
3940
+ // A reassigned function binding can point at an async component by the
3941
+ // time its warm edge runs. Destructuring/default/rest parameters can also
3942
+ // invoke user code before the authored component body is reached.
3943
+ let opaque =
3944
+ !ctx.moduleFunctionDeclarations.has(component.id?.name) ||
3945
+ (component.params || []).some((parameter) => parameter.type !== 'Identifier');
3946
+
3947
+ function walk(node) {
3948
+ if (opaque || node === null || typeof node !== 'object') return;
3949
+ if (Array.isArray(node)) {
3950
+ for (const child of node) walk(child);
3951
+ return;
3952
+ }
3953
+ if (seen.has(node)) return;
3954
+ seen.add(node);
3955
+
3956
+ // Deferred handlers do not execute during this component's render. State
3957
+ // initializers are admitted only when proven primitive and non-callable.
3958
+ if (FN_TYPES.has(node.type)) return;
3959
+
3960
+ if ((node.type === 'Element' || node.type === 'JSXElement') && isComponentTag(node)) {
3961
+ const name = tagBindingName(node);
3962
+ if (
3963
+ name === null ||
3964
+ locals.has(name) ||
3965
+ ctx._octaneBoundaryNames.has(name) ||
3966
+ !ctx.componentInfo.has(name)
3967
+ ) {
3968
+ opaque = true;
3969
+ return;
3970
+ }
3971
+ dependencies.add(name);
3972
+ } else if (node.type === 'CallExpression' || node.type === 'NewExpression') {
3973
+ const hook = stableHookCallName(node);
3974
+ if (
3975
+ hook !== 'useState' ||
3976
+ node.arguments.length > 1 ||
3977
+ (node.arguments.length === 1 && !isInvariantLiteral(unwrapTsExpr(node.arguments[0])))
3978
+ ) {
3979
+ opaque = true;
3980
+ return;
3981
+ }
3982
+ } else if (node.type === 'VariableDeclarator' && node.id?.type === 'ArrayPattern') {
3983
+ // The built-in state tuple is the only iterator this proof owns. Any
3984
+ // other destructuring can execute a custom iterator or rest/default.
3985
+ if (
3986
+ stableHookCallName(unwrapTsExpr(node.init)) !== 'useState' ||
3987
+ node.id.elements.some((element) => element !== null && element.type !== 'Identifier')
3988
+ ) {
3989
+ opaque = true;
3990
+ return;
3991
+ }
3992
+ } else if (node.type === 'AssignmentExpression') {
3993
+ if (
3994
+ node.operator !== '=' ||
3995
+ node.left?.type !== 'Identifier' ||
3996
+ node.right?.type !== 'Identifier' ||
3997
+ !invariant.has(node.right.name)
3998
+ ) {
3999
+ opaque = true;
4000
+ return;
4001
+ }
4002
+ } else if (
4003
+ node.type === 'AwaitExpression' ||
4004
+ node.type === 'YieldExpression' ||
4005
+ node.type === 'MemberExpression' ||
4006
+ node.type === 'JSXMemberExpression' ||
4007
+ node.type === 'OptionalMemberExpression' ||
4008
+ node.type === 'OptionalCallExpression' ||
4009
+ node.type === 'SpreadElement' ||
4010
+ node.type === 'SpreadAttribute' ||
4011
+ node.type === 'JSXSpreadAttribute' ||
4012
+ node.type === 'ObjectPattern' ||
4013
+ node.type === 'AssignmentPattern' ||
4014
+ node.type === 'RestElement' ||
4015
+ node.type === 'ForOfStatement' ||
4016
+ node.type === 'JSXForExpression' ||
4017
+ node.type === 'ForInStatement' ||
4018
+ node.type === 'ForStatement' ||
4019
+ node.type === 'WhileStatement' ||
4020
+ node.type === 'DoWhileStatement' ||
4021
+ node.type === 'ThrowStatement' ||
4022
+ node.type === 'TryStatement' ||
4023
+ node.type === 'JSXTryExpression' ||
4024
+ node.type === 'ImportExpression' ||
4025
+ node.type === 'TaggedTemplateExpression' ||
4026
+ node.type === 'UpdateExpression'
4027
+ ) {
4028
+ opaque = true;
4029
+ return;
4030
+ }
4031
+
4032
+ for (const key in node) {
4033
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4034
+ walk(node[key]);
4035
+ }
4036
+ }
4037
+
4038
+ walk(statements);
4039
+ walk(component.body.render);
4040
+ info.warmPotential = opaque;
4041
+ info.warmDependencies = dependencies;
4042
+ }
4043
+
4044
+ // Propagate async reachability through forward references and recursive
4045
+ // same-module chains. An all-synchronous cycle stays false; one opaque or
4046
+ // async descendant makes every component that can reach it conservative.
4047
+ let changed = true;
4048
+ while (changed) {
4049
+ changed = false;
4050
+ for (const [, info] of ctx.componentInfo) {
4051
+ if (info.warmPotential) continue;
4052
+ for (const name of info.warmDependencies) {
4053
+ if (ctx.componentInfo.get(name)?.warmPotential !== false) {
4054
+ info.warmPotential = true;
4055
+ changed = true;
4056
+ break;
4057
+ }
4058
+ }
4059
+ }
4060
+ }
4061
+ }
4062
+
4063
+ // Omitting a JSX descriptor also omits its live `Component.defaultProps` read.
4064
+ // Only private declarations whose every reference is an immediately rendered,
4065
+ // attribute-free host child can therefore bypass descriptor construction. JSX
4066
+ // descriptors returned/stored/passed elsewhere expose their `.type`, so those
4067
+ // sites count as escapes even though their tag looks equally static.
4068
+ function collectPrivateUnescapedComponents(body, ctx) {
4069
+ const candidates = new Set();
4070
+ for (const statement of body) {
4071
+ if (
4072
+ statement.type === 'FunctionDeclaration' &&
4073
+ statement.id?.type === 'Identifier' &&
4074
+ ctx.componentInfo.get(statement.id.name)?.returnJsx === true &&
4075
+ ctx.moduleFunctionDeclarations.has(statement.id.name)
4076
+ ) {
4077
+ candidates.add(statement.id.name);
4078
+ }
4079
+ }
4080
+ if (candidates.size === 0) return candidates;
4081
+
4082
+ const allowed = new Set();
4083
+ const visitHost = (host) => {
4084
+ for (const child of host.children || []) {
4085
+ if (child?.type !== 'Element' && child?.type !== 'JSXElement') continue;
4086
+ if (!isComponentTag(child)) {
4087
+ visitHost(child);
4088
+ continue;
4089
+ }
4090
+ const name = tagBindingName(child);
4091
+ const attrs = child.attributes || child.openingElement?.attributes || [];
4092
+ if (name !== null && candidates.has(name) && attrs.length === 0 && !child.children?.length) {
4093
+ allowed.add(child);
4094
+ }
4095
+ }
4096
+ };
4097
+ for (const info of ctx.componentInfo.values()) {
4098
+ if (!info.returnJsx) continue;
4099
+ for (const statement of info.node.body.body || []) {
4100
+ if (statement.type === 'ReturnStatement' && isPlainHostRoot(statement.argument)) {
4101
+ visitHost(statement.argument);
4102
+ }
4103
+ }
4104
+ }
4105
+
4106
+ // An array deliberately does not introduce a synthetic module Block scope:
4107
+ // sibling function references must remain free for the escape proof.
4108
+ const escaped = collectFreeIdentifiers(body, [], allowed);
4109
+ for (const name of candidates) {
4110
+ if (escaped.has(name)) {
4111
+ candidates.delete(name);
4112
+ continue;
4113
+ }
4114
+ // A declaration binds its own name inside its body. Scan that disjoint
4115
+ // subtree once more so self-assigned properties cannot hide behind it.
4116
+ const declaration = ctx.moduleFunctionDeclarations.get(name);
4117
+ if (collectFreeIdentifiers(declaration.body, [], allowed).has(name)) candidates.delete(name);
4118
+ }
4119
+ return candidates;
4120
+ }
4121
+
3719
4122
  // Conservative semantic boundary for compiler-owned component-region memoization.
3720
4123
  // The cached region assumes React Compiler's pure-render / immutable-snapshot
3721
4124
  // contract, but still fails closed for constructs whose commit or retry behavior
@@ -3764,11 +4167,12 @@ function containsDeferredRefRead(root) {
3764
4167
  return found;
3765
4168
  }
3766
4169
 
3767
- // Both probes visit the identical node set, so one pass answers both. The ref
3768
- // probe loses its early exit; it finds nothing in 97% of components anyway.
4170
+ // Imported component tags rendered anywhere in the body — the auto-memo
4171
+ // region's runtime witnesses. (Deferred-ref reads are no longer collected
4172
+ // body-wide here: they gate per call site and per local through
4173
+ // collectAutoMemoLocalHazards.)
3769
4174
  function scanComponentBody(root, importedNames) {
3770
4175
  const importedComponents = new Set();
3771
- let readsDeferredRef = false;
3772
4176
  const seen = new WeakSet();
3773
4177
  function walk(node) {
3774
4178
  if (!node || typeof node !== 'object') return;
@@ -3778,7 +4182,6 @@ function scanComponentBody(root, importedNames) {
3778
4182
  }
3779
4183
  if (seen.has(node)) return;
3780
4184
  seen.add(node);
3781
- if (!readsDeferredRef && isDeferredRefRead(node)) readsDeferredRef = true;
3782
4185
  const imported = importedComponentTag(node, importedNames);
3783
4186
  if (imported !== null) importedComponents.add(imported);
3784
4187
  for (const key in node) {
@@ -3787,7 +4190,7 @@ function scanComponentBody(root, importedNames) {
3787
4190
  }
3788
4191
  }
3789
4192
  walk(root);
3790
- return { importedComponents, readsDeferredRef };
4193
+ return { importedComponents };
3791
4194
  }
3792
4195
 
3793
4196
  // A JSX member chain bottoms out at a JSXIdentifier, so the base test below
@@ -3823,9 +4226,137 @@ function containsImportedMemberRead(root, importedNames) {
3823
4226
  return found;
3824
4227
  }
3825
4228
 
4229
+ /**
4230
+ * Component locals whose values a region's dependency tuple cannot witness.
4231
+ *
4232
+ * A region dep on a LOCAL compares the local's per-render value. Locals
4233
+ * re-initialize on every body entry, so a plain value local — however it is
4234
+ * conditionally reassigned during render — is always an exact witness of
4235
+ * itself. What it CANNOT witness is a value drawn from mutable state whose
4236
+ * container identity hides the drift:
4237
+ *
4238
+ * - a ref read (`.current` / any computed member — `isDeferredRefRead`'s
4239
+ * definition): ref contents mutate outside render without scheduling one,
4240
+ * and a stable-identity closure (`useCallback(() => ref.current, [])`)
4241
+ * hands a skipped subtree a LIVE read whose changes nothing witnesses —
4242
+ * so the classification descends into nested function values;
4243
+ * - an imported binding's member read: a live import's property can move
4244
+ * while the import's identity (the only thing a dep can hold) stays
4245
+ * fixed.
4246
+ *
4247
+ * A name is tainted when such a read reaches its value through ANY feed —
4248
+ * declaration initializers at every nesting depth, destructuring patterns
4249
+ * that carry the read themselves (`const { current: el } = ref`, a default or
4250
+ * computed key reading a ref or live import), assignment right-hand sides, or
4251
+ * a for-of/for-in source — directly or through another tainted local (fixed
4252
+ * point, so declaration order and arrow-hoisted back-references cannot hide a
4253
+ * hazard). Nested and loop-scoped names are not component locals, so tainting
4254
+ * them is inert at call sites directly (unknown names already fail closed),
4255
+ * but it is what lets a hazard PROPAGATE through them into a top-level local
4256
+ * (`let out; for (const x of ref.current.items) out = x`). Shadowing is
4257
+ * folded by name — an inner binding's hazard taints its outer namesake —
4258
+ * which only ever declines more. Props params and same-module function
4259
+ * declarations never taint: the former are the witnessed snapshot itself, the
4260
+ * latter are immutable identities whose render-time CALLS the per-site
4261
+ * render-call gate already rejects.
4262
+ *
4263
+ * The direct forms of these reads inside a call site's own props are rejected
4264
+ * per site (containsAutoMemoUnsafeStructure / containsImportedMemberRead);
4265
+ * this set covers the same reads laundered through a local.
4266
+ */
4267
+ function collectAutoMemoLocalHazards(stmts, importedNames) {
4268
+ const hazards = new Set();
4269
+ // name -> union of free identifiers across its clean value sources; a name
4270
+ // with a directly hazardous source moves to `hazards` and leaves this map.
4271
+ const sourceFrees = new Map();
4272
+ const feed = (pattern, rhs) => {
4273
+ if (!rhs) return;
4274
+ const names = new Set();
4275
+ collectBindings(pattern, names);
4276
+ if (names.size === 0) return;
4277
+ // The pattern side can carry the read itself: a `current`/computed
4278
+ // binding key destructures ref contents off the source, and a default's
4279
+ // expression evaluates at destructure time. Both walks descend the whole
4280
+ // pattern, so nested shapes and defaults are covered together.
4281
+ const direct =
4282
+ containsDeferredRefRead(rhs) ||
4283
+ containsImportedMemberRead(rhs, importedNames) ||
4284
+ containsDeferredRefRead(pattern) ||
4285
+ containsImportedMemberRead(pattern, importedNames);
4286
+ // Default expressions inside the pattern read values too; their free
4287
+ // identifiers (which include the pattern's own bound names — inert
4288
+ // self-loops) join the clean-source union.
4289
+ const frees = direct
4290
+ ? null
4291
+ : [...collectFreeIdentifiers(rhs, []), ...collectFreeIdentifiers(pattern, [])];
4292
+ for (const name of names) {
4293
+ if (hazards.has(name)) continue;
4294
+ if (direct) {
4295
+ hazards.add(name);
4296
+ sourceFrees.delete(name);
4297
+ continue;
4298
+ }
4299
+ let set = sourceFrees.get(name);
4300
+ if (set === undefined) sourceFrees.set(name, (set = new Set()));
4301
+ for (const id of frees) set.add(id);
4302
+ }
4303
+ };
4304
+ // One walk feeds every binding form at every depth, including inside
4305
+ // nested function values: a deferred write targets a PREVIOUS render's
4306
+ // binding and can never leak into the next render's dep snapshot, but
4307
+ // walking uniformly is cheaper than proving which writes are deferred, and
4308
+ // only hazardous sources taint anyway.
4309
+ const seen = new WeakSet();
4310
+ (function walk(n) {
4311
+ if (!n) return;
4312
+ if (Array.isArray(n)) {
4313
+ for (const x of n) walk(x);
4314
+ return;
4315
+ }
4316
+ if (typeof n !== 'object' || !n.type || seen.has(n)) return;
4317
+ seen.add(n);
4318
+ if (n.type === 'VariableDeclaration') {
4319
+ for (const d of n.declarations || []) feed(d.id, d.init);
4320
+ } else if (n.type === 'AssignmentExpression' && n.left?.type !== 'MemberExpression') {
4321
+ feed(n.left, n.right);
4322
+ } else if ((n.type === 'ForOfStatement' || n.type === 'ForInStatement') && n.left) {
4323
+ // The loop source feeds the bound names whether the left declares
4324
+ // (`for (const x of src)` — declarator inits are null, so the
4325
+ // declaration arm above cannot see src) or reuses an outer binding.
4326
+ if (n.left.type === 'VariableDeclaration') {
4327
+ for (const d of n.left.declarations || []) feed(d.id, n.right);
4328
+ } else {
4329
+ feed(n.left, n.right);
4330
+ }
4331
+ }
4332
+ for (const key in n) {
4333
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
4334
+ walk(n[key]);
4335
+ }
4336
+ })(stmts);
4337
+ let changed = true;
4338
+ while (changed) {
4339
+ changed = false;
4340
+ for (const [name, free] of sourceFrees) {
4341
+ for (const id of free) {
4342
+ if (!hazards.has(id)) continue;
4343
+ hazards.add(name);
4344
+ sourceFrees.delete(name);
4345
+ changed = true;
4346
+ break;
4347
+ }
4348
+ }
4349
+ }
4350
+ return hazards;
4351
+ }
4352
+
3826
4353
  function containsAutoMemoUnsafeStructure(stmts) {
3827
4354
  let found = false;
3828
4355
  const seen = new WeakSet();
4356
+ // Spread bags on HOST elements, marked admissible by the owning element's
4357
+ // visit below. Parents walk before their attributes, so membership is
4358
+ // decided before the spread node itself is reached.
4359
+ const hostSpreads = new WeakSet();
3829
4360
  function walk(n) {
3830
4361
  if (found || !n) return;
3831
4362
  if (Array.isArray(n)) {
@@ -3857,13 +4388,29 @@ function containsAutoMemoUnsafeStructure(stmts) {
3857
4388
  t === 'YieldExpression' ||
3858
4389
  t === 'ThrowStatement' ||
3859
4390
  t === 'TryStatement' ||
3860
- t === 'JSXTryExpression' ||
3861
- t === 'SpreadAttribute' ||
3862
- t === 'JSXSpreadAttribute'
4391
+ t === 'JSXTryExpression'
3863
4392
  ) {
3864
4393
  found = true;
3865
4394
  return;
3866
4395
  }
4396
+ if (t === 'SpreadAttribute' || t === 'JSXSpreadAttribute') {
4397
+ // A HOST element's spread bag is one setSpread binding re-diffed on every
4398
+ // region entry. Its argument is composed of deps the region guard already
4399
+ // witnesses (unwitnessed names fail closed independently), so a skip is
4400
+ // the no-op a re-entry would have been — the same immutable-snapshot
4401
+ // argument that admits `class={e.cls}` member reads; runtime keys the
4402
+ // walk cannot see (ref/on*/value) diff per entry with full parity to
4403
+ // their direct-attribute forms. A COMPONENT tag's spread instead builds
4404
+ // the child's props snapshot (getter evaluation, hidden prop names) and
4405
+ // stays rejected, matching the callSiteOk exclusion — which
4406
+ // isAutoMemoPropsParam's rest admission relies on. The argument still
4407
+ // walks below: accessors, computed members, and ref reads inside it keep
4408
+ // failing closed.
4409
+ if (!hostSpreads.has(n)) {
4410
+ found = true;
4411
+ return;
4412
+ }
4413
+ }
3867
4414
  if (t === 'UnaryExpression' && n.operator === 'delete') {
3868
4415
  found = true;
3869
4416
  return;
@@ -3931,6 +4478,13 @@ function containsAutoMemoUnsafeStructure(stmts) {
3931
4478
  found = true;
3932
4479
  return;
3933
4480
  }
4481
+ if (!isComponentTag(n)) {
4482
+ for (const attr of n.attributes || n.openingElement?.attributes || []) {
4483
+ if (attr.type === 'SpreadAttribute' || attr.type === 'JSXSpreadAttribute') {
4484
+ hostSpreads.add(attr);
4485
+ }
4486
+ }
4487
+ }
3934
4488
  }
3935
4489
  for (const key in n) {
3936
4490
  if (AST_WALK_SKIP_KEYS.has(key)) continue;
@@ -3941,6 +4495,54 @@ function containsAutoMemoUnsafeStructure(stmts) {
3941
4495
  return found;
3942
4496
  }
3943
4497
 
4498
+ /**
4499
+ * Can this props parameter stand in for autoMemo's "one ordinary props
4500
+ * snapshot"? A bare Identifier always can. A destructuring ObjectPattern can
4501
+ * when destructuring it evaluates NO expressions and reads NOTHING mutable:
4502
+ * every binding it introduces is already a component local
4503
+ * (collectComponentLocals walks patterns), so the body's free-identifier and
4504
+ * capture analysis is complete for it — the pattern itself is the only part
4505
+ * those walks never see, because they take the body root, not the params.
4506
+ *
4507
+ * Fail closed on exactly the shapes whose evaluation escapes that analysis or
4508
+ * reads outside the snapshot:
4509
+ * - defaults (`{ a = expr }`): `expr` evaluates at destructure time, and its
4510
+ * reads are invisible to the body walks;
4511
+ * - computed keys (`{ [k]: v }`): same, for the key expression;
4512
+ * - a `current` binding at any depth: the same mutable-ref hazard the body
4513
+ * walk rejects for ObjectPattern declarations (a ref's identity is not a
4514
+ * complete witness for its contents);
4515
+ * - array patterns: destructuring runs the iterator protocol, which is
4516
+ * arbitrary user code on a non-Array prop;
4517
+ * - rest (`...rest`) with anything but a plain Identifier target.
4518
+ * Rest itself is admitted: object rest is an engine-level own-property copy of
4519
+ * the same snapshot, and spread-bearing call sites never receive a region
4520
+ * anyway (callSiteOk excludes them), so a getter-bearing props object cannot
4521
+ * reach a cached region.
4522
+ */
4523
+ function isAutoMemoPropsParam(param) {
4524
+ if (param == null) return false;
4525
+ if (param.type === 'Identifier') return true;
4526
+ if (param.type !== 'ObjectPattern') return false;
4527
+ const admissible = (pattern) => {
4528
+ if (pattern == null || pattern.type !== 'ObjectPattern') return false;
4529
+ for (const p of pattern.properties || []) {
4530
+ if (p.type === 'RestElement') {
4531
+ if (p.argument?.type !== 'Identifier') return false;
4532
+ continue;
4533
+ }
4534
+ if (p.type !== 'Property' || p.computed) return false;
4535
+ const key = p.key?.name ?? p.key?.value;
4536
+ if (key === 'current') return false;
4537
+ const value = p.value;
4538
+ if (value?.type === 'Identifier') continue;
4539
+ if (!admissible(value)) return false;
4540
+ }
4541
+ return true;
4542
+ };
4543
+ return admissible(param);
4544
+ }
4545
+
3944
4546
  /**
3945
4547
  * Bundling MOVES an argument's evaluation out of the arrow body and into the
3946
4548
  * component body, so it now runs on mount and on every update instead of once
@@ -4370,6 +4972,111 @@ function isSsrMarkerlessForItem(node) {
4370
4972
  return jsxChildren.length === 1 && isPlainHostRoot(jsxChildren[0]);
4371
4973
  }
4372
4974
 
4975
+ // Direct host-row mounting skips component-render bookkeeping. Keep the proof
4976
+ // narrower than markerless item rendering so opaque host lifecycles fail closed.
4977
+ const HOST_MOUNT_SAFE_TAGS = new Set([
4978
+ 'a',
4979
+ 'article',
4980
+ 'aside',
4981
+ 'b',
4982
+ 'br',
4983
+ 'div',
4984
+ 'em',
4985
+ 'footer',
4986
+ 'h1',
4987
+ 'h2',
4988
+ 'h3',
4989
+ 'h4',
4990
+ 'h5',
4991
+ 'h6',
4992
+ 'header',
4993
+ 'hr',
4994
+ 'i',
4995
+ 'li',
4996
+ 'main',
4997
+ 'nav',
4998
+ 'ol',
4999
+ 'p',
5000
+ 'section',
5001
+ 'small',
5002
+ 'span',
5003
+ 'strong',
5004
+ 'table',
5005
+ 'tbody',
5006
+ 'td',
5007
+ 'tfoot',
5008
+ 'th',
5009
+ 'thead',
5010
+ 'tr',
5011
+ 'ul',
5012
+ ]);
5013
+
5014
+ const HOST_MOUNT_UNSAFE_ATTRIBUTES = new Set([
5015
+ 'autofocus',
5016
+ 'checked',
5017
+ 'dangerouslysetinnerhtml',
5018
+ 'defaultchecked',
5019
+ 'defaultvalue',
5020
+ 'form',
5021
+ 'formaction',
5022
+ 'formenctype',
5023
+ 'formmethod',
5024
+ 'formnovalidate',
5025
+ 'formtarget',
5026
+ 'innerhtml',
5027
+ 'is',
5028
+ 'multiple',
5029
+ 'ref',
5030
+ 'selected',
5031
+ 'slot',
5032
+ 'src',
5033
+ 'srcset',
5034
+ 'style',
5035
+ 'value',
5036
+ ]);
5037
+
5038
+ function isHostMountSafeTree(root) {
5039
+ const seen = new WeakSet();
5040
+ function walk(node) {
5041
+ if (node == null || typeof node !== 'object') return true;
5042
+ if (Array.isArray(node)) return node.every(walk);
5043
+ if (seen.has(node)) return true;
5044
+ seen.add(node);
5045
+ const type = node.type;
5046
+ if (
5047
+ type === 'ArrowFunctionExpression' ||
5048
+ type === 'FunctionExpression' ||
5049
+ type === 'FunctionDeclaration'
5050
+ ) {
5051
+ // Delegated event callbacks run after the live row has mounted.
5052
+ return true;
5053
+ }
5054
+ if (type === 'JSXFragment' || type === 'Fragment') return false;
5055
+ if (type === 'Element' || type === 'JSXElement') {
5056
+ const tag = node.id || node.openingElement?.name;
5057
+ if (
5058
+ (tag?.type !== 'Identifier' && tag?.type !== 'JSXIdentifier') ||
5059
+ !HOST_MOUNT_SAFE_TAGS.has(tag.name)
5060
+ ) {
5061
+ return false;
5062
+ }
5063
+ for (const attribute of node.attributes || node.openingElement?.attributes || []) {
5064
+ if (attribute.type !== 'Attribute' && attribute.type !== 'JSXAttribute') return false;
5065
+ const name = jsxAttrRawName(attribute);
5066
+ if (typeof name !== 'string' || HOST_MOUNT_UNSAFE_ATTRIBUTES.has(name.toLowerCase())) {
5067
+ return false;
5068
+ }
5069
+ }
5070
+ }
5071
+ for (const key in node) {
5072
+ if (AST_WALK_SKIP_KEYS.has(key)) continue;
5073
+ if (!walk(node[key])) return false;
5074
+ }
5075
+ return true;
5076
+ }
5077
+ return walk(root);
5078
+ }
5079
+
4373
5080
  /**
4374
5081
  * Whether a function can return from its own body before reaching its compiled
4375
5082
  * output. Nested functions are separate execution scopes and do not affect the
@@ -6162,15 +6869,22 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6162
6869
  exactOrigins: new Map(), // see registerExactOrigin
6163
6870
  originAliases: [], // see registerOriginAlias
6164
6871
  hoistedTemplates: [], // { name, ast, html, ns, frag, origins }
6872
+ internedTemplates: new Map(), // HTML -> one template or namespace/fragment variants
6165
6873
  hoistedHelpers: [], // statement NODES (sub-components, hook Symbols, key fns) + hook-slot-base markers
6166
6874
  delegatedEvents: new Set(), // bubble event names seen in JSX — auto-emits delegateEvents(...)
6167
6875
  capturedEvents: new Set(), // capture-phase event names (onXxxCapture) — auto-emits delegateCaptureEvents(...)
6876
+ unownedDelegatedEvents: new Set(),
6877
+ unownedCapturedEvents: new Set(),
6168
6878
  cssInjections: [], // { hash, css } — one entry per component with a <style> block
6879
+ ownedCssInjections: new Set(),
6880
+ componentOwners: [],
6881
+ currentComponentOwner: null,
6169
6882
  currentComponentLocals: null, // Set<string> while compiling a component body; null otherwise
6170
6883
  currentMapTemps: null, // receiver/method temps owned by the current emitted function
6171
6884
  currentAutoMemoOffset: 0, // flat compiler-cache cell offset for the body being emitted
6172
6885
  currentAutoMemoCacheName: null, // collision-free local bound to the body's cache array
6173
6886
  currentAutoMemoCommittedName: null, // committed cache snapshot (copy-on-write source)
6887
+ currentAutoCalculatedRenderableRefs: null, // proven non-escaping calculation holes, inherited by lexical child bodies
6174
6888
  nextAutoMemoCacheId: 0, // unique non-index slots property per compiled render function
6175
6889
  inlineHookMemo: inlineHookMemoEnabled, // de-callbacked useMemo/useCallback + pu creations
6176
6890
  _puInlineLowering: false, // true only while a body pipeline ends in inlineHookMemoPass
@@ -6375,8 +7089,54 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6375
7089
  ctx.moduleFunctionDeclarations = collectImmutableModuleFunctions(ast.body);
6376
7090
  // M3 inherit-range exclusion set (see inheritSoleCompRoot).
6377
7091
  ctx._octaneBoundaryNames = collectOctaneBoundaryNames(ast.body);
6378
- // Client prelude `_$vtSeen()` module-load hint (view-transitions plan).
6379
- ctx._usesViewTransition = moduleImportsViewTransition(ast.body);
7092
+ const productionEffects = !ctx.hmr && !ctx.dev && !ctx.profile;
7093
+ const exportedComponents = ast.body.filter(
7094
+ (statement) =>
7095
+ (statement.type === 'ExportNamedDeclaration' ||
7096
+ statement.type === 'ExportDefaultDeclaration') &&
7097
+ isComponentFunction(statement.declaration),
7098
+ );
7099
+ // Prelude registration historically precedes every authored module effect.
7100
+ // An effect above any component could observe its stylesheet, delegated
7101
+ // listeners, or transition capability, so retain the entire module prelude.
7102
+ let effectBeforeComponent = false;
7103
+ let precedingModuleEffect = false;
7104
+ if (productionEffects && exportedComponents.length > 1) {
7105
+ for (const statement of ast.body) {
7106
+ const declaration =
7107
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
7108
+ ? statement.declaration
7109
+ : statement;
7110
+ if (isComponentFunction(declaration)) {
7111
+ if (precedingModuleEffect) {
7112
+ effectBeforeComponent = true;
7113
+ break;
7114
+ }
7115
+ continue;
7116
+ }
7117
+ if (
7118
+ statement.type === 'ImportDeclaration' ||
7119
+ statement.type === 'ExportAllDeclaration' ||
7120
+ (statement.type === 'ExportNamedDeclaration' && declaration == null) ||
7121
+ declaration?.type === 'FunctionDeclaration' ||
7122
+ declaration?.type === 'TSInterfaceDeclaration' ||
7123
+ declaration?.type === 'TSTypeAliasDeclaration' ||
7124
+ statement.type === 'EmptyStatement'
7125
+ ) {
7126
+ continue;
7127
+ }
7128
+ precedingModuleEffect = true;
7129
+ }
7130
+ }
7131
+ ctx.componentEffectOwnership =
7132
+ productionEffects && exportedComponents.length > 1 && !effectBeforeComponent;
7133
+ const viewTransitions = classifyViewTransitionOwnership(
7134
+ ast.body,
7135
+ productionEffects,
7136
+ ctx.componentEffectOwnership,
7137
+ );
7138
+ ctx._usesViewTransition = viewTransitions.global;
7139
+ ctx.viewTransitionOwners = viewTransitions.owners;
6380
7140
 
6381
7141
  // List of exported components needing HMR wrapping. Each entry: { name,
6382
7142
  // exportKind: 'default' | 'named' }. We emit the `Comp = hmr(Comp)` lines
@@ -6415,6 +7175,7 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6415
7175
  eligible: false,
6416
7176
  autoMemoSafe: false,
6417
7177
  autoMemoCallsitesSafe: true,
7178
+ autoMemoLocalHazards: null,
6418
7179
  autoMemoCaptures: [],
6419
7180
  autoMemoComponentDeps: [],
6420
7181
  autoMemoImportedComponents: [],
@@ -6425,6 +7186,19 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6425
7186
  });
6426
7187
  }
6427
7188
  }
7189
+ // Return-based JSX functions never attach fetch-tree warm plans, and a sole
7190
+ // same-module component already rejects a self-only warm edge below. Avoid a
7191
+ // whole-module graph walk unless a compiled-void body can actually reach a
7192
+ // distinct same-module declaration.
7193
+ if (
7194
+ ctx.componentInfo.size > 1 &&
7195
+ [...ctx.componentInfo.values()].some((info) => info.node.body?.type === 'JSXCodeBlock')
7196
+ ) {
7197
+ classifySameModuleWarmPotential(ctx);
7198
+ }
7199
+ ctx.privateUnescapedComponents = ctx.autoMemo
7200
+ ? collectPrivateUnescapedComponents(ast.body, ctx)
7201
+ : new Set();
6428
7202
  for (const [, info] of ctx.componentInfo) {
6429
7203
  const compNode = info.node;
6430
7204
  const locals = collectComponentLocals(compNode);
@@ -6434,16 +7208,26 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6434
7208
  if (compNode.body.render) stmts.push(compNode.body.render);
6435
7209
  const root = b.block(stmts);
6436
7210
  const free = collectFreeIdentifiers(root, locals);
6437
- const { importedComponents: autoMemoImportedComponents, readsDeferredRef } = scanComponentBody(
7211
+ const { importedComponents: autoMemoImportedComponents } = scanComponentBody(
6438
7212
  root,
6439
7213
  ctx.importedNames,
6440
7214
  );
6441
- // Both proofs below ask this of the same root. Kept lazy because the
6442
- // short-circuits ahead of each use skip it entirely for some components.
7215
+ // The callee-side proof below asks this of the same root. Kept lazy because
7216
+ // the short-circuits ahead of its use skip it entirely for some components.
6443
7217
  let importedMemberRead = null;
6444
7218
  const readsImportedMember = () =>
6445
7219
  (importedMemberRead ??= containsImportedMemberRead(root, ctx.importedNames));
6446
- let autoMemoCallsitesSafe = !readsDeferredRef && !readsImportedMember();
7220
+ // Call-site regions inside this body are gated per site: the site's own
7221
+ // props are walked directly (containsAutoMemoUnsafeStructure /
7222
+ // containsImportedMemberRead over the JSX node), and ref/imported-member
7223
+ // reads laundered through a body local are carried by this hazard set —
7224
+ // a site whose free identifiers touch a tainted local falls back. The
7225
+ // free-name loop below still vetoes the whole body for names no dep can
7226
+ // witness at all (boundary wrappers, namespaces, ambient module state).
7227
+ const autoMemoLocalHazards = ctx.autoMemo
7228
+ ? collectAutoMemoLocalHazards(stmts, ctx.importedNames)
7229
+ : null;
7230
+ let autoMemoCallsitesSafe = true;
6447
7231
  for (const name of free) {
6448
7232
  if (
6449
7233
  ctx._octaneBoundaryNames.has(name) ||
@@ -6457,13 +7241,14 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6457
7241
  break;
6458
7242
  }
6459
7243
  }
6460
- // autoMemo's first proof is intentionally narrower than lite eligibility.
6461
- // It models a pure component as a function of one ordinary props snapshot;
6462
- // destructuring/default/rest parameters can be admitted once their evaluation
6463
- // is included in the shared dependency/purity analysis.
7244
+ // autoMemo models a pure component as a function of one ordinary props
7245
+ // snapshot. A destructuring param is that same snapshot read once at
7246
+ // entry, admitted only while the pattern evaluates no expressions of its
7247
+ // own — see isAutoMemoPropsParam for the exact fail-closed shapes
7248
+ // (defaults, computed keys, `current`, array patterns).
6464
7249
  const ordinaryPropsParam =
6465
7250
  (compNode.params?.length ?? 0) <= 1 &&
6466
- (compNode.params?.length !== 1 || compNode.params[0]?.type === 'Identifier');
7251
+ (compNode.params?.length !== 1 || isAutoMemoPropsParam(compNode.params[0]));
6467
7252
  let autoMemoSafe =
6468
7253
  ordinaryPropsParam &&
6469
7254
  !containsRenderCall(stmts, ctx) &&
@@ -6501,6 +7286,7 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6501
7286
  }
6502
7287
  info.autoMemoSafe = autoMemoSafe;
6503
7288
  info.autoMemoCallsitesSafe = autoMemoCallsitesSafe;
7289
+ info.autoMemoLocalHazards = autoMemoLocalHazards;
6504
7290
  info.autoMemoCaptures = autoMemoSafe ? autoMemoCaptures.sort() : [];
6505
7291
  info.autoMemoComponentDeps = autoMemoSafe ? autoMemoComponentDeps.sort() : [];
6506
7292
  info.autoMemoImportedComponents = autoMemoSafe ? [...autoMemoImportedComponents].sort() : [];
@@ -6736,6 +7522,8 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6736
7522
  }
6737
7523
  }
6738
7524
 
7525
+ finalizeComponentInitializers(ctx, bodyNodes);
7526
+
6739
7527
  // Auto-emit delegateEvents([...]) / delegateCaptureEvents([...]) once at module
6740
7528
  // scope for every (bubble / capture) event seen.
6741
7529
  if (ctx.delegatedEvents.size > 0) {
@@ -6777,22 +7565,24 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
6777
7565
  ),
6778
7566
  );
6779
7567
  }
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)),
7568
+ const styleNodes = ctx.cssInjections
7569
+ .filter((i) => !ctx.ownedCssInjections.has(i))
7570
+ .map((i) => {
7571
+ // Point the authored `<style>` block at the CSS this injection carries.
7572
+ // Both sides are whole units — the block and the stylesheet it became —
7573
+ // which is the useful pairing for a scoped style.
7574
+ const anchor = claimCssOrigins(ctx, i);
7575
+ return inheritOriginLoc(
7576
+ b.stmt(
7577
+ b.call(
7578
+ '_$injectStyle',
7579
+ b.literal(i.hash, JSON.stringify(i.hash)),
7580
+ b.literal(i.css, JSON.stringify(i.css)),
7581
+ ),
6791
7582
  ),
6792
- ),
6793
- anchor ?? moduleOrigin,
6794
- );
6795
- });
7583
+ anchor ?? moduleOrigin,
7584
+ );
7585
+ });
6796
7586
  const templateNodes = ctx.hoistedTemplates.map((t) => {
6797
7587
  const args = [b.literal(t.html, JSON.stringify(t.html))];
6798
7588
  if (t.ns || t.frag) args.push(b.literal(t.ns | 0));
@@ -10152,6 +10942,103 @@ function singleRootInitializer(ctx, component) {
10152
10942
  return markPure(b.call('_$__s', component));
10153
10943
  }
10154
10944
 
10945
+ function finalizeComponentInitializers(ctx, bodyNodes) {
10946
+ if (!ctx.componentEffectOwnership || ctx.componentOwners.length === 0) return;
10947
+
10948
+ const delegatedOwners = new Map();
10949
+ const capturedOwners = new Map();
10950
+ const ownableStyles = new Set();
10951
+ for (const owner of ctx.componentOwners) {
10952
+ for (const event of owner.delegatedEvents) {
10953
+ delegatedOwners.set(event, (delegatedOwners.get(event) ?? 0) + 1);
10954
+ }
10955
+ for (const event of owner.capturedEvents) {
10956
+ capturedOwners.set(event, (capturedOwners.get(event) ?? 0) + 1);
10957
+ }
10958
+ for (const style of owner.styles) ownableStyles.add(style);
10959
+ }
10960
+ // The prelude emitted all stylesheets in authored order. Moving only owned
10961
+ // sheets behind an unowned style map or local component reverses that cascade.
10962
+ const preserveModuleStyleOrder = ctx.cssInjections.some((style) => !ownableStyles.has(style));
10963
+
10964
+ const replacements = new Map();
10965
+ for (const owner of ctx.componentOwners) {
10966
+ const calls = [];
10967
+ if (owner.transition && !ctx._usesViewTransition) {
10968
+ ctx.runtimeNeeded.add('__vtSeen');
10969
+ calls.push(b.call(rtAlias('__vtSeen')));
10970
+ }
10971
+
10972
+ const delegated = [...owner.delegatedEvents]
10973
+ .filter((event) => delegatedOwners.get(event) === 1 && !ctx.unownedDelegatedEvents.has(event))
10974
+ .sort();
10975
+ if (delegated.length !== 0) {
10976
+ for (const event of delegated) ctx.delegatedEvents.delete(event);
10977
+ ctx.runtimeNeeded.add('delegateEvents');
10978
+ calls.push(
10979
+ b.call(
10980
+ '_$delegateEvents',
10981
+ b.array(delegated.map((event) => b.literal(event, JSON.stringify(event)))),
10982
+ ),
10983
+ );
10984
+ }
10985
+
10986
+ const captured = [...owner.capturedEvents]
10987
+ .filter((event) => capturedOwners.get(event) === 1 && !ctx.unownedCapturedEvents.has(event))
10988
+ .sort();
10989
+ if (captured.length !== 0) {
10990
+ for (const event of captured) ctx.capturedEvents.delete(event);
10991
+ ctx.runtimeNeeded.add('delegateCaptureEvents');
10992
+ calls.push(
10993
+ b.call(
10994
+ '_$delegateCaptureEvents',
10995
+ b.array(captured.map((event) => b.literal(event, JSON.stringify(event)))),
10996
+ ),
10997
+ );
10998
+ }
10999
+
11000
+ if (!preserveModuleStyleOrder) {
11001
+ for (const style of owner.styles) {
11002
+ ctx.ownedCssInjections.add(style);
11003
+ const origin = claimCssOrigins(ctx, style);
11004
+ calls.push(
11005
+ inheritOriginLoc(
11006
+ b.call(
11007
+ '_$injectStyle',
11008
+ b.literal(style.hash, JSON.stringify(style.hash)),
11009
+ b.literal(style.css, JSON.stringify(style.css)),
11010
+ ),
11011
+ origin ?? owner.origin,
11012
+ ),
11013
+ );
11014
+ }
11015
+ }
11016
+ if (calls.length === 0) continue;
11017
+
11018
+ const declarator = owner.declaration.declarations[0];
11019
+ const initializer = inheritOriginLoc(
11020
+ markPure(b.call(b.arrow([], b.sequence([...calls, declarator.init])))),
11021
+ owner.origin,
11022
+ );
11023
+ replacements.set(owner.declaration, {
11024
+ ...owner.declaration,
11025
+ declarations: [{ ...declarator, init: initializer }],
11026
+ });
11027
+ }
11028
+
11029
+ if (replacements.size === 0) return;
11030
+ for (let index = 0; index < bodyNodes.length; index++) {
11031
+ const statement = bodyNodes[index];
11032
+ const replacement = replacements.get(statement);
11033
+ if (replacement !== undefined) {
11034
+ bodyNodes[index] = replacement;
11035
+ } else if (statement.type === 'ExportNamedDeclaration') {
11036
+ const declaration = replacements.get(statement.declaration);
11037
+ if (declaration !== undefined) bodyNodes[index] = { ...statement, declaration };
11038
+ }
11039
+ }
11040
+ }
11041
+
10155
11042
  function compileComponent(node, ctx, options) {
10156
11043
  const name = node.id.name;
10157
11044
  rejectAsyncOrGenerator(node, name);
@@ -10160,6 +11047,19 @@ function compileComponent(node, ctx, options) {
10160
11047
  const isDefault = !!node.default;
10161
11048
  const hmrWrap = !!(options && options.hmrWrap);
10162
11049
  const returnedOutput = node.body?.type === 'JSXCodeBlock' && hasOwnValueReturn(node);
11050
+ const owner =
11051
+ ctx.componentEffectOwnership && isExported
11052
+ ? {
11053
+ name,
11054
+ origin: node,
11055
+ delegatedEvents: new Set(),
11056
+ capturedEvents: new Set(),
11057
+ styles: [],
11058
+ transition: ctx.viewTransitionOwners.has(name),
11059
+ declaration: null,
11060
+ }
11061
+ : null;
11062
+ const firstStyle = ctx.cssInjections.length;
10163
11063
 
10164
11064
  // Scoped `<style>` block. New TSRX surfaces each style block as a
10165
11065
  // `JSXStyleElement` child of the rendered tree (parser pre-computes the
@@ -10189,11 +11089,15 @@ function compileComponent(node, ctx, options) {
10189
11089
  // locals.
10190
11090
  const prevLocals = ctx.currentComponentLocals;
10191
11091
  const prevAutoMemoCallsitesSafe = ctx.currentAutoMemoCallsitesSafe;
11092
+ const prevAutoMemoLocalHazards = ctx.currentAutoMemoLocalHazards;
10192
11093
  const prevKnownStr = ctx.knownStringLocals;
10193
11094
  const prevProfileComponentId = ctx.currentProfileComponentId;
11095
+ const previousComponentOwner = ctx.currentComponentOwner;
10194
11096
  ctx.currentComponentLocals = collectComponentLocals(node);
10195
11097
  ctx.currentAutoMemoCallsitesSafe = ctx.componentInfo.get(name)?.autoMemoCallsitesSafe !== false;
11098
+ ctx.currentAutoMemoLocalHazards = ctx.componentInfo.get(name)?.autoMemoLocalHazards ?? null;
10196
11099
  ctx.knownStringLocals = collectKnownStringLocals(node);
11100
+ ctx.currentComponentOwner = owner;
10197
11101
  if (ctx.profile) ctx.currentProfileComponentId = profileComponentId(ctx, name, node);
10198
11102
  let fnNode;
10199
11103
  try {
@@ -10209,9 +11113,12 @@ function compileComponent(node, ctx, options) {
10209
11113
  } finally {
10210
11114
  ctx.currentComponentLocals = prevLocals;
10211
11115
  ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
11116
+ ctx.currentAutoMemoLocalHazards = prevAutoMemoLocalHazards;
10212
11117
  ctx.knownStringLocals = prevKnownStr;
10213
11118
  ctx.currentProfileComponentId = prevProfileComponentId;
11119
+ ctx.currentComponentOwner = previousComponentOwner;
10214
11120
  }
11121
+ if (owner !== null) owner.styles = ctx.cssInjections.slice(firstStyle);
10215
11122
 
10216
11123
  // Parallel-use warm plan: attached to the INNER function object (not the
10217
11124
  // module const) so the component's own body — where the function-
@@ -10252,6 +11159,11 @@ function compileComponent(node, ctx, options) {
10252
11159
  sourceBeforeNode !== '' &&
10253
11160
  new RegExp(`\\b${name.replace(/\$/g, '\\$')}\\b`).test(sourceBeforeNode);
10254
11161
  if (referencedAboveDeclaration) {
11162
+ if (owner !== null) {
11163
+ for (const event of owner.delegatedEvents) ctx.unownedDelegatedEvents.add(event);
11164
+ for (const event of owner.capturedEvents) ctx.unownedCapturedEvents.add(event);
11165
+ if (owner.transition) ctx._usesViewTransition = true;
11166
+ }
10255
11167
  // Every stamp is `typeof`-guarded: route code-splitters (TanStack's) may
10256
11168
  // EXTRACT the declaration into its own module and leave these statements
10257
11169
  // behind — `typeof` on the then-undeclared identifier short-circuits
@@ -10341,6 +11253,10 @@ function compileComponent(node, ctx, options) {
10341
11253
  b.declaration(declKind, [b.declarator(b.id(name, node.id ?? node), valueExpr)]),
10342
11254
  node,
10343
11255
  );
11256
+ if (owner !== null) {
11257
+ owner.declaration = declNode;
11258
+ ctx.componentOwners.push(owner);
11259
+ }
10344
11260
  if (isDefault) {
10345
11261
  if (options && options.hmrMutable) {
10346
11262
  return {
@@ -10371,6 +11287,8 @@ function compileComponent(node, ctx, options) {
10371
11287
  */
10372
11288
  function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null, options = null) {
10373
11289
  const returnedOutput = options?.returnedOutput === true;
11290
+ const previousAutoCalculatedRenderableRefs = ctx.currentAutoCalculatedRenderableRefs;
11291
+ let autoCalculatedDeclarations = null;
10374
11292
  const prevMapTemps = ctx.currentMapTemps;
10375
11293
  const mapTemps = [];
10376
11294
  ctx.currentMapTemps = mapTemps;
@@ -10444,7 +11362,8 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
10444
11362
  );
10445
11363
  }
10446
11364
  workingStatements = removeMountEventCallbackDeclarations(statements, mountCallbackSinks).map(
10447
- (s) => rewriteAutoCallback(s, stableSet, ctx.currentComponentLocals, ctx),
11365
+ (s) =>
11366
+ rewriteAutoCallback(s, stableSet, ctx.currentComponentLocals, ctx, bodyInvariantLocals),
10448
11367
  );
10449
11368
  }
10450
11369
  if (returnedOutput) {
@@ -10496,9 +11415,21 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
10496
11415
  if (ctx.currentComponentLocals && ctx.mode !== 'server') {
10497
11416
  const renderReadNames = collectRenderReadNames(jsxNodes, ctx);
10498
11417
  if (renderReadNames.size > 0) {
10499
- workingStatements = workingStatements.map((statement) =>
10500
- rewriteAutoCalculation(statement, ctx.currentComponentLocals, renderReadNames, ctx),
10501
- );
11418
+ workingStatements = workingStatements.map((statement) => {
11419
+ const rewritten = rewriteAutoCalculation(
11420
+ statement,
11421
+ ctx.currentComponentLocals,
11422
+ renderReadNames,
11423
+ ctx,
11424
+ );
11425
+ if (ctx.autoMemo && rewritten !== statement) {
11426
+ (autoCalculatedDeclarations ??= new Map()).set(
11427
+ statement.declarations[0].id.name,
11428
+ statement,
11429
+ );
11430
+ }
11431
+ return rewritten;
11432
+ });
10502
11433
  }
10503
11434
  }
10504
11435
  workingStatements = parallelUseMemoizePass(workingStatements, ctx, name, creations, [], null);
@@ -10607,6 +11538,19 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
10607
11538
  // node itself always does.
10608
11539
  const prevFnOrigin = ctx._fnOrigin;
10609
11540
  ctx._fnOrigin = node.loc ? node : (node.id ?? prevFnOrigin);
11541
+ if (autoCalculatedDeclarations !== null) {
11542
+ const refs = collectAutoCalculatedRenderableRefs(
11543
+ statements,
11544
+ jsxNodes,
11545
+ autoCalculatedDeclarations,
11546
+ );
11547
+ if (refs !== null) {
11548
+ ctx.currentAutoCalculatedRenderableRefs = {
11549
+ nodes: refs,
11550
+ parent: previousAutoCalculatedRenderableRefs,
11551
+ };
11552
+ }
11553
+ }
10610
11554
  // Located origin for the function SHELL itself: synthetic helper shapes
10611
11555
  // (hoisted @for bodies, `__tsrx$N` sub-templates) carry no loc of their
10612
11556
  // own — their scaffolding inherits the nearest located enclosing origin.
@@ -10639,6 +11583,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
10639
11583
  ctx.currentBodyIsComponentScope = prevBodyIsComponentScope;
10640
11584
  ctx._inheritBody = prevInheritBody;
10641
11585
  ctx._fnOrigin = prevFnOrigin;
11586
+ ctx.currentAutoCalculatedRenderableRefs = previousAutoCalculatedRenderableRefs;
10642
11587
  ctx._foldedDirectiveCalls = prevFDC;
10643
11588
  ctx._valueDirectiveLowering = prevValueDirectiveLowering;
10644
11589
 
@@ -12390,6 +13335,12 @@ function buildWarmArtifacts(node, ctx, componentName, creations, warmChildren) {
12390
13335
  const warmKids = warmChildren.filter(
12391
13336
  (w) =>
12392
13337
  guardOk(w.guards, w.locals) &&
13338
+ // Server and universal-renderer warm plans have independent compilation
13339
+ // contexts. Only the DOM client's closed same-module graph can prove
13340
+ // that this edge and every descendant are synchronously render-only.
13341
+ (ctx.mode === 'server' ||
13342
+ ctx._universalRuntimeUnit != null ||
13343
+ ctx.componentInfo.get(w.compName)?.warmPotential !== false) &&
12393
13344
  !paramNames.has(w.compName) &&
12394
13345
  !(locals && locals.has(w.compName)) &&
12395
13346
  !(w.locals && w.locals.has(w.compName)) &&
@@ -13051,7 +14002,17 @@ function authoredHookMemoOf(stmt) {
13051
14002
  if (containsHookShapedCall(fn.body)) return null;
13052
14003
  if (fn.body.type === 'BlockStatement' && !blockBodyInlineSafe(fn.body)) return null;
13053
14004
  }
13054
- return { name, decl, kind: stmt.kind, fn, deps };
14005
+ return {
14006
+ name,
14007
+ decl,
14008
+ kind: stmt.kind,
14009
+ fn,
14010
+ deps,
14011
+ generatedInvariant:
14012
+ name === 'useCallback' &&
14013
+ callee._octaneGenerated === true &&
14014
+ callee._octaneLifetimeInvariant === true,
14015
+ };
13055
14016
  }
13056
14017
 
13057
14018
  // Compute statements writing the site's result into `target` (an expression
@@ -13127,7 +14088,7 @@ function lowerPuMemoDecl(stmt, ctx) {
13127
14088
  function lowerAuthoredHookMemo(stmt, ctx) {
13128
14089
  const entry = authoredHookMemoOf(stmt);
13129
14090
  if (entry === null) return null;
13130
- const { name, decl, kind, fn, deps } = entry;
14091
+ const { name, decl, kind, fn, deps, generatedInvariant } = entry;
13131
14092
  // Explicit `null` deps: recompute every render — no cache at all. A
13132
14093
  // useCallback degenerates to the factory itself; a useMemo evaluates
13133
14094
  // inline (via the shared block machinery when the body is a block).
@@ -13143,6 +14104,24 @@ function lowerAuthoredHookMemo(stmt, ctx) {
13143
14104
  }
13144
14105
  const names = hookMemoNames(ctx);
13145
14106
  const base = ctx.currentHookMemoOffset;
14107
+ if (generatedInvariant) {
14108
+ // A compiler-owned callback whose captures are lifetime-invariant cannot
14109
+ // miss after its first render. Its function value doubles as the init flag,
14110
+ // and publishing it immediately preserves identity across suspension.
14111
+ ctx.currentHookMemoOffset = base + 1;
14112
+ const valueCell = () => b.member(b.id(names.cache), hkNumLit(base), true);
14113
+ return [
14114
+ {
14115
+ ...stmt,
14116
+ declarations: [
14117
+ {
14118
+ ...decl,
14119
+ init: b.logical('??', valueCell(), b.assignment('=', valueCell(), fn)),
14120
+ },
14121
+ ],
14122
+ },
14123
+ ];
14124
+ }
13146
14125
  const k = deps.length;
13147
14126
  ctx.currentHookMemoOffset = base + k + 2;
13148
14127
  const cellRef = (i) => b.member(b.id(names.cache), hkNumLit(i), true);
@@ -13502,19 +14481,42 @@ function compileReturnJsxFunction(node, ctx, options) {
13502
14481
  const prevValueDirectiveLowering = ctx._valueDirectiveLowering;
13503
14482
  const prevLocals = ctx.currentComponentLocals;
13504
14483
  const prevAutoMemoCallsitesSafe = ctx.currentAutoMemoCallsitesSafe;
14484
+ const prevAutoMemoLocalHazards = ctx.currentAutoMemoLocalHazards;
13505
14485
  const prevMapTemps = ctx.currentMapTemps;
13506
14486
  const mapTemps = [];
13507
14487
  ctx._valueDirectiveLowering = lowerBodyValueDirective;
13508
14488
  ctx.currentComponentLocals = collectComponentLocals(node);
13509
14489
  ctx.currentAutoMemoCallsitesSafe = ctx.componentInfo.get(name)?.autoMemoCallsitesSafe !== false;
14490
+ ctx.currentAutoMemoLocalHazards = ctx.componentInfo.get(name)?.autoMemoLocalHazards ?? null;
13510
14491
  ctx.currentMapTemps = mapTemps;
13511
14492
  let newStatements;
13512
14493
  try {
13513
- newStatements = (node.body.body || []).map((sourceStatement) => {
14494
+ const authoredStatements = node.body.body || [];
14495
+ const renderedRoots = authoredStatements
14496
+ .filter((statement) => statement.type === 'ReturnStatement' && isJsxNode(statement.argument))
14497
+ .map((statement) => statement.argument);
14498
+ const renderReadNames = collectRenderReadNames(renderedRoots, ctx);
14499
+ let renderScopeEstablished = false;
14500
+ newStatements = authoredStatements.map((sourceStatement) => {
14501
+ // Return-JSX functions keep their ordinary callable ABI. Introducing a
14502
+ // cache into a hookless function would make an existing direct call
14503
+ // require a render scope, so only declarations following an authored,
14504
+ // unconditional Octane hook may use the existing calculation lowering.
14505
+ const calculated = renderScopeEstablished
14506
+ ? rewriteAutoCalculation(sourceStatement, ctx.currentComponentLocals, renderReadNames, ctx)
14507
+ : sourceStatement;
14508
+ if (!renderScopeEstablished && sourceStatement.type === 'VariableDeclaration') {
14509
+ renderScopeEstablished = (sourceStatement.declarations || []).some(
14510
+ (declaration) => stableHookCallName(unwrapTsExpr(declaration.init)) !== null,
14511
+ );
14512
+ } else if (!renderScopeEstablished && sourceStatement.type === 'ExpressionStatement') {
14513
+ renderScopeEstablished =
14514
+ stableHookCallName(unwrapTsExpr(sourceStatement.expression)) !== null;
14515
+ }
13514
14516
  // A return-based component's undefined output is ambiguous with the compiled
13515
14517
  // void-body signal at runtime. Preserve JSX roots for the specialized lowering
13516
14518
  // below, but normalize every other owned return to an explicit empty value.
13517
- const s = normalizeOwnRenderableReturns(sourceStatement, true);
14519
+ const s = normalizeOwnRenderableReturns(calculated, true);
13518
14520
  const prepared =
13519
14521
  s.type === 'ReturnStatement' && s.argument && isJsxNode(s.argument)
13520
14522
  ? s
@@ -13533,8 +14535,33 @@ function compileReturnJsxFunction(node, ctx, options) {
13533
14535
  ctx._valueDirectiveLowering = prevValueDirectiveLowering;
13534
14536
  ctx.currentComponentLocals = prevLocals;
13535
14537
  ctx.currentAutoMemoCallsitesSafe = prevAutoMemoCallsitesSafe;
14538
+ ctx.currentAutoMemoLocalHazards = prevAutoMemoLocalHazards;
13536
14539
  ctx.currentMapTemps = prevMapTemps;
13537
14540
  }
14541
+ if (
14542
+ ctx.autoMemo &&
14543
+ ctx.privateUnescapedComponents.has(name) &&
14544
+ (node.params?.length ?? 0) === 0 &&
14545
+ (node.body.body?.length ?? 0) === 1 &&
14546
+ isPlainHostRoot(node.body.body[0]?.argument) &&
14547
+ compInlinedSubs.length === 0 &&
14548
+ mapTemps.length === 0 &&
14549
+ cssHash === null
14550
+ ) {
14551
+ const descriptor = newStatements[0]?.argument;
14552
+ const renderer = descriptor?.arguments?.[0];
14553
+ const props = descriptor?.arguments?.[1];
14554
+ if (
14555
+ descriptor?.type === 'CallExpression' &&
14556
+ descriptor.callee?.type === 'Identifier' &&
14557
+ descriptor.callee.name === '_$createElement' &&
14558
+ descriptor.arguments.length === 2 &&
14559
+ renderer?.type === 'Identifier' &&
14560
+ isStaticFragmentRendererProps(props, ctx)
14561
+ ) {
14562
+ ctx.componentInfo.get(name).staticFragmentRenderer = { name: renderer.name, props };
14563
+ }
14564
+ }
13538
14565
  // The rebuilt function shell maps to the authored declaration. Hoisted
13539
14566
  // helper fns (compInlinedSubs — filled by the statement mapping above) are
13540
14567
  // function DECLARATION nodes embedded at the top of the body, matching the
@@ -13714,6 +14741,57 @@ function objectProp(hn, valNode) {
13714
14741
  return b.prop('init', b.id(hn), valNode);
13715
14742
  }
13716
14743
 
14744
+ // A bare, immutable same-module component needs no descriptor when its JSX is
14745
+ // consumed immediately by a returned host. Keep every value/props boundary on
14746
+ // the ordinary path: only this attribute-free call can remain in the template
14747
+ // without moving authored expression evaluation into its hoisted renderer.
14748
+ function isStaticFragmentRendererProps(props, ctx) {
14749
+ if (props?.type !== 'ObjectExpression') return false;
14750
+ for (const property of props.properties || []) {
14751
+ if (
14752
+ property.type !== 'Property' ||
14753
+ property.kind !== 'init' ||
14754
+ property.computed ||
14755
+ property.method ||
14756
+ property.shorthand
14757
+ ) {
14758
+ return false;
14759
+ }
14760
+ const descriptor = property.value;
14761
+ const component = descriptor?.arguments?.[0];
14762
+ const componentProps = descriptor?.arguments?.[1];
14763
+ if (
14764
+ descriptor?.type !== 'CallExpression' ||
14765
+ descriptor.callee?.type !== 'Identifier' ||
14766
+ descriptor.callee.name !== '_$createElement' ||
14767
+ descriptor.arguments.length !== 2 ||
14768
+ component?.type !== 'Identifier' ||
14769
+ !ctx.privateUnescapedComponents.has(component.name) ||
14770
+ componentProps?.type !== 'ObjectExpression' ||
14771
+ componentProps.properties.length !== 0
14772
+ ) {
14773
+ return false;
14774
+ }
14775
+ }
14776
+ return true;
14777
+ }
14778
+
14779
+ function isStaticReturnedFragmentComponent(node, ctx) {
14780
+ if (!ctx.autoMemo || ctx._foldCtx?.immediateRenderedOutput !== true) return false;
14781
+ const name = tagBindingName(node);
14782
+ if (
14783
+ name === null ||
14784
+ ctx.currentComponentLocals == null ||
14785
+ ctx.currentComponentLocals.has(name) ||
14786
+ !ctx.privateUnescapedComponents.has(name) ||
14787
+ ctx.componentInfo.get(name)?.staticFragmentRenderer === undefined
14788
+ ) {
14789
+ return false;
14790
+ }
14791
+ const attrs = node.attributes || node.openingElement?.attributes || [];
14792
+ return attrs.length === 0 && (node.children || []).length === 0;
14793
+ }
14794
+
13717
14795
  // Walk a host element or JSX fragment, replacing each DYNAMIC part (an
13718
14796
  // attribute/child expression) with `props.hN` and collecting
13719
14797
  // `{ hN: <originalExpr> }` into `holeProps`. Static structure (tag, literal attrs,
@@ -13831,6 +14909,22 @@ function extractFragment(node, ctx, holeProps, parentNs = 'html') {
13831
14909
  // a hole too, since it may be a local/member/dynamic tag that the hoisted
13832
14910
  // renderer cannot reference directly.
13833
14911
  newChildren.push(extractFragmentComponent(child, ctx, holeProps, childNs));
14912
+ } else if (isComponentTag(child) && isStaticReturnedFragmentComponent(child, ctx)) {
14913
+ // The original function remains callable and still returns its authored
14914
+ // descriptor. Only this nonescaping call can reuse its already-hoisted,
14915
+ // hookless fragment directly through the existing lite component ABI.
14916
+ const extracted = extractFragment(child, ctx, holeProps, childNs);
14917
+ newChildren.push({
14918
+ ...extracted,
14919
+ openingElement: {
14920
+ ...extracted.openingElement,
14921
+ metadata: {
14922
+ ...extracted.openingElement.metadata,
14923
+ staticFragmentRenderer: ctx.componentInfo.get(tagBindingName(child))
14924
+ .staticFragmentRenderer,
14925
+ },
14926
+ },
14927
+ });
13834
14928
  } else if (isComponentTag(child)) {
13835
14929
  const hn = `h${holeProps.length}`;
13836
14930
  holeProps.push(
@@ -16108,6 +17202,7 @@ function emitAutoMemoRegion(
16108
17202
  contextAware,
16109
17203
  depNode,
16110
17204
  initValue = null,
17205
+ restoreCachedContext = false,
16111
17206
  ) {
16112
17207
  const cell = allocAutoMemoCell(ctx, dependencies.length + (contextAware ? 1 : 0));
16113
17208
  const contextIndex = contextAware ? cell.base + dependencies.length : null;
@@ -16157,7 +17252,15 @@ function emitAutoMemoRegion(
16157
17252
  }
16158
17253
  ctx.runtimeNeeded.add('compilerCacheContext');
16159
17254
  const cacheContextCall = () =>
16160
- b.call('_$compilerCacheContext', b.id('__s'), b.literal(slotIndex), cacheAt(contextIndex));
17255
+ restoreCachedContext
17256
+ ? b.call(
17257
+ '_$compilerCacheContext',
17258
+ b.id('__s'),
17259
+ b.literal(slotIndex),
17260
+ cacheAt(contextIndex),
17261
+ b.literal(true),
17262
+ )
17263
+ : b.call('_$compilerCacheContext', b.id('__s'), b.literal(slotIndex), cacheAt(contextIndex));
16161
17264
  return b.block([
16162
17265
  ...depDecls,
16163
17266
  b.if(
@@ -16773,10 +17876,11 @@ function planJsx(
16773
17876
  const elVar = ensureVar(c.hostPath || []);
16774
17877
  c.elVar = elVar;
16775
17878
  const org = c.origin ?? planOrigin;
16776
- if (!noTemplate) {
17879
+ const key = `_${hostKey}$${c.id}`;
17880
+ if (!noTemplate && bag.host(key, elVar)) {
16777
17881
  mountLines.push(
16778
17882
  inheritOriginLoc(
16779
- b.stmt(b.assignment('=', b.id(bag.local(`_${hostKey}$${c.id}`)), hostVarNode(elVar))),
17883
+ b.stmt(b.assignment('=', b.id(bag.local(key)), hostVarNode(elVar))),
16780
17884
  org,
16781
17885
  ),
16782
17886
  );
@@ -17035,11 +18139,26 @@ function planJsx(
17035
18139
  const pushAfterStmt = (id, org, node) => pushAfter(id, inheritOriginLoc(node, org));
17036
18140
  for (const fc of forCalls) {
17037
18141
  const isMappedList = fc.mapMethodExpr !== null;
17038
- ctx.runtimeNeeded.add(isMappedList ? 'mapSlot' : 'forBlock');
18142
+ const forHelper = isMappedList
18143
+ ? fc.hostMountSafe
18144
+ ? 'fastMapSlot'
18145
+ : 'mapSlot'
18146
+ : fc.keyedSelectionIndex >= 0
18147
+ ? fc.hostMountSafe
18148
+ ? 'fastKeyedForBlock'
18149
+ : 'keyedForBlock'
18150
+ : fc.hostMountSafe
18151
+ ? 'fastForBlock'
18152
+ : 'forBlock';
18153
+ ctx.runtimeNeeded.add(forHelper);
18154
+ if (isMappedList && fc.hostMountSafe && fc.autoMemoDeps !== null) {
18155
+ // Memoized maps query the original native-array guard before dispatch.
18156
+ ctx.runtimeNeeded.add('mapSlot');
18157
+ }
17039
18158
  const slotIndex = fc.slotIndex;
17040
18159
  const org = fc.origin ?? planOrigin;
17041
18160
  registerDirectiveOrigin(ctx, org, [
17042
- isMappedList ? '_$mapSlot' : '_$forBlock',
18161
+ rtAlias(forHelper),
17043
18162
  fc.keyHelper,
17044
18163
  fc.bodyHelper,
17045
18164
  fc.emptyHelper,
@@ -17052,13 +18171,19 @@ function planJsx(
17052
18171
  // for survivors when deps unchanged this render),
17053
18172
  // bit 3 = indexIndependent (body binds no `index` → a pure reorder
17054
18173
  // 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.
18174
+ // bit 4 = SSR emitted markerless direct-host items; hydrate them by root,
18175
+ // bit 5 = conditional item bodies require their active scope,
18176
+ // bits 6+ = the zero-based keyedForBlock selection dependency index.
18177
+ // keyedForBlock itself identifies a selection, including dependency zero;
18178
+ // ordinary forBlock reuses bit 5 without an argument or tuple allocation.
17056
18179
  const flags =
17057
18180
  (fc.pure ? 1 : 0) |
17058
18181
  (fc.singleRoot ? 2 : 0) |
17059
18182
  (fc.depEligible ? 4 : 0) |
17060
18183
  (fc.indexIndependent ? 8 : 0) |
17061
- (fc.ssrMarkerless ? 16 : 0);
18184
+ (fc.ssrMarkerless ? 16 : 0) |
18185
+ (fc.requiresScope ? 32 : 0) |
18186
+ (fc.keyedSelectionIndex >= 0 ? fc.keyedSelectionIndex << 6 : 0);
17062
18187
  // Arg layout: forBlock(__s, slot, host, items, keyFn, body, flags?, deps?,
17063
18188
  // emptyBody?, anchor?, ownEnd?).
17064
18189
  // Optional args backfill positionally: `flags`/`deps` placeholders
@@ -17117,7 +18242,7 @@ function planJsx(
17117
18242
  if (fc.autoMemoDeps !== null) {
17118
18243
  const mappedCall = b.stmt(
17119
18244
  b.call(
17120
- '_$mapSlot',
18245
+ rtAlias(forHelper),
17121
18246
  b.id('__s'),
17122
18247
  b.literal(slotIndex),
17123
18248
  hostExpr(),
@@ -17159,7 +18284,7 @@ function planJsx(
17159
18284
  org,
17160
18285
  b.stmt(
17161
18286
  b.call(
17162
- '_$mapSlot',
18287
+ rtAlias(forHelper),
17163
18288
  b.id('__s'),
17164
18289
  b.literal(slotIndex),
17165
18290
  hostExpr(),
@@ -17188,7 +18313,7 @@ function planJsx(
17188
18313
  slotIndex,
17189
18314
  b.stmt(
17190
18315
  b.call(
17191
- '_$forBlock',
18316
+ rtAlias(forHelper),
17192
18317
  b.id('__s'),
17193
18318
  b.literal(slotIndex),
17194
18319
  hostExpr(),
@@ -17207,7 +18332,7 @@ function planJsx(
17207
18332
  org,
17208
18333
  b.stmt(
17209
18334
  b.call(
17210
- '_$forBlock',
18335
+ rtAlias(forHelper),
17211
18336
  b.id('__s'),
17212
18337
  b.literal(slotIndex),
17213
18338
  hostExpr(),
@@ -17327,6 +18452,59 @@ function planJsx(
17327
18452
  continue;
17328
18453
  }
17329
18454
  ctx.runtimeNeeded.add('setText');
18455
+ const updateHole = () =>
18456
+ b.if(
18457
+ b.logical('||', b.id('_o'), b.binary('!==', chp(), V())),
18458
+ b.block([
18459
+ b.const('_t', chv()),
18460
+ b.if(
18461
+ andChain([
18462
+ b.binary('!=', b.id('_t'), b.literal(null)),
18463
+ b.unary('!', b.id('_o')),
18464
+ b.binary('!==', V(), b.literal(null)),
18465
+ ]),
18466
+ b.stmt(b.call('_$setText', b.id('_t'), V())),
18467
+ b.stmt(
18468
+ b.assignment(
18469
+ '=',
18470
+ chv(),
18471
+ b.call(
18472
+ '_$childTextHole',
18473
+ b.id('__s'),
18474
+ b.literal(slotIndex),
18475
+ hostExpr(),
18476
+ V(),
18477
+ b.id('_t'),
18478
+ ),
18479
+ ),
18480
+ ),
18481
+ ),
18482
+ b.stmt(b.assignment('=', chp(), V())),
18483
+ ]),
18484
+ null,
18485
+ );
18486
+ const ordinary = updateHole();
18487
+ let update = ordinary;
18488
+ if (cc.autoMemoValue === true) {
18489
+ // Only a compiler-owned, non-escaping plain data array may skip
18490
+ // descriptor reconciliation. Accessor-backed, nested, or deferred
18491
+ // children must still be observed on every parent render; the
18492
+ // runtime caches eligibility by immutable snapshot identity.
18493
+ ctx.runtimeNeeded.add('compilerCacheArray');
18494
+ const cached = emitAutoMemoRegion(
18495
+ ctx,
18496
+ ['_v'],
18497
+ slotIndex,
18498
+ updateHole(),
18499
+ // Array → scalar → the same array must reconstruct the list.
18500
+ b.binary('!==', chp(), V()),
18501
+ true,
18502
+ undefined,
18503
+ null,
18504
+ true,
18505
+ );
18506
+ update = b.if(b.call('_$compilerCacheArray', V(), chp()), cached, ordinary);
18507
+ }
17330
18508
  pushAfterStmt(
17331
18509
  cc.id,
17332
18510
  org,
@@ -17344,36 +18522,7 @@ function planJsx(
17344
18522
  ),
17345
18523
  ),
17346
18524
  ),
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
- ),
18525
+ update,
17377
18526
  ]),
17378
18527
  );
17379
18528
  continue;
@@ -17809,6 +18958,7 @@ function bagLetter(i) {
17809
18958
  function makeBag() {
17810
18959
  const fields = [];
17811
18960
  const byKey = new Map();
18961
+ const byHost = new Map();
17812
18962
  const reg = (key, constExpr) => {
17813
18963
  let r = byKey.get(key);
17814
18964
  if (r === undefined) {
@@ -17831,6 +18981,16 @@ function makeBag() {
17831
18981
  return {
17832
18982
  /** Mount-write target for `key` — the pre-declared local. */
17833
18983
  local: (key) => reg(key, undefined).local,
18984
+ /** Register one immutable DOM-host field; aliases reuse its first mount write. */
18985
+ host: (key, host) => {
18986
+ const existing = byHost.get(host);
18987
+ if (existing !== undefined) {
18988
+ byKey.set(key, existing);
18989
+ return false;
18990
+ }
18991
+ byHost.set(host, reg(key, undefined));
18992
+ return true;
18993
+ },
17834
18994
  /** Seed `key` with a constant expression (no local, no mount write). */
17835
18995
  constField: (key, expr) => {
17836
18996
  reg(key, expr);
@@ -17975,8 +19135,10 @@ function emitDeferredMount(bind, elVar, bag) {
17975
19135
  if (!(bind.kind === 'class' && bind.fresh)) {
17976
19136
  bag.constField(bind.kind === 'style' ? `_sty$${bind.id}` : `_prev$${bind.id}`, 'undefined');
17977
19137
  }
19138
+ const key = `_el$${bind.id}`;
19139
+ if (!bag.host(key, elVar)) return null;
17978
19140
  return inheritOriginLoc(
17979
- b.stmt(b.assignment('=', b.id(bag.local(`_el$${bind.id}`)), hostVarNode(elVar))),
19141
+ b.stmt(b.assignment('=', b.id(bag.local(key)), hostVarNode(elVar))),
17980
19142
  bindingOrigin(bind),
17981
19143
  );
17982
19144
  }
@@ -17994,6 +19156,9 @@ function emitBindingMount(bind, elVar, bag) {
17994
19156
  const st = (node) => inheritOriginLoc(node, org);
17995
19157
  const el = () => hostVarNode(elVar);
17996
19158
  const local = (key) => b.id(bag.local(key));
19159
+ const hostKey = `_el$${bind.id}`;
19160
+ const mountHost = () =>
19161
+ bag.host(hostKey, elVar) ? [b.stmt(b.assignment('=', local(hostKey), el()))] : [];
17997
19162
  const V = () => b.id('_v');
17998
19163
  // The tokens that NAME this binding's lowering carry the authored attribute
17999
19164
  // name; everything else in the call maps to the value expression.
@@ -18011,10 +19176,7 @@ function emitBindingMount(bind, elVar, bag) {
18011
19176
  return st(b.stmt(b.call('_$markNativeChangeDiagnosticStatic', el())));
18012
19177
  }
18013
19178
  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
- ];
19179
+ return [...mountHost().map(st), st(b.stmt(b.call('_$queueNativeChangeDiagnostic', el())))];
18018
19180
  }
18019
19181
  switch (bind.kind) {
18020
19182
  case 'textOnlyChild': {
@@ -18036,7 +19198,7 @@ function emitBindingMount(bind, elVar, bag) {
18036
19198
  b.block([
18037
19199
  b.const('_v', bind.expr),
18038
19200
  b.stmt(b.call('_$setDangerouslySetInnerHTML', el(), V())),
18039
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
19201
+ ...mountHost(),
18040
19202
  b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
18041
19203
  ]),
18042
19204
  );
@@ -18065,7 +19227,7 @@ function emitBindingMount(bind, elVar, bag) {
18065
19227
  return st(
18066
19228
  b.block([
18067
19229
  b.stmt(b.call('_$setDangerouslySetInnerHTMLSources', el(), sources)),
18068
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
19230
+ ...mountHost(),
18069
19231
  ]),
18070
19232
  );
18071
19233
  }
@@ -18074,14 +19236,11 @@ function emitBindingMount(bind, elVar, bag) {
18074
19236
  b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
18075
19237
  );
18076
19238
  return st(
18077
- b.block([
18078
- b.stmt(b.call('_$setFormControlSources', el(), sources)),
18079
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
18080
- ]),
19239
+ b.block([b.stmt(b.call('_$setFormControlSources', el(), sources)), ...mountHost()]),
18081
19240
  );
18082
19241
  }
18083
19242
  case 'hostCommit': {
18084
- const elLocal = local(`_el$${bind.id}`);
19243
+ const hostMount = mountHost();
18085
19244
  const propsLocal = local(`_host$${bind.id}`);
18086
19245
  const sources = commitSourceRows(bind.sources, (binding, spread) =>
18087
19246
  b.id(bag.local(`${spread ? '_sp' : '_prev'}$${binding.id}`)),
@@ -18112,7 +19271,7 @@ function emitBindingMount(bind, elVar, bag) {
18112
19271
  );
18113
19272
  return st(
18114
19273
  b.block([
18115
- b.stmt(b.assignment('=', elLocal, el())),
19274
+ ...hostMount,
18116
19275
  b.stmt(
18117
19276
  b.assignment(
18118
19277
  '=',
@@ -18155,7 +19314,7 @@ function emitBindingMount(bind, elVar, bag) {
18155
19314
  b.block([
18156
19315
  b.const('_v', bind.expr),
18157
19316
  b.stmt(b.call(callee(), el(), nameLit(), V())),
18158
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
19317
+ ...mountHost(),
18159
19318
  b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
18160
19319
  ]),
18161
19320
  );
@@ -18172,12 +19331,7 @@ function emitBindingMount(bind, elVar, bag) {
18172
19331
  // update must re-run every render to reassert drift (React's
18173
19332
  // controlled contract). Not in DEFERRABLE_MOUNT_KINDS: the mount
18174
19333
  // 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
- );
19334
+ return st(b.block([b.stmt(b.call(callee(), el(), bind.expr)), ...mountHost()]));
18181
19335
  }
18182
19336
  case 'autoFocus': {
18183
19337
  // Mount-only (React ignores later autoFocus changes); the focus
@@ -18187,11 +19341,7 @@ function emitBindingMount(bind, elVar, bag) {
18187
19341
  case 'class': {
18188
19342
  // On SVG/MathML hosts the `className` property is read-only — fall back
18189
19343
  // 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
- ];
19344
+ const body = [b.const('_v', bind.expr), b.stmt(b.call(callee(), el(), V())), ...mountHost()];
18195
19345
  if (!bind.fresh) body.push(b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())));
18196
19346
  return st(b.block(body));
18197
19347
  }
@@ -18200,7 +19350,7 @@ function emitBindingMount(bind, elVar, bag) {
18200
19350
  b.block([
18201
19351
  b.const('_v', bind.expr),
18202
19352
  b.stmt(b.call(callee(), el(), V(), undefinedNode())),
18203
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
19353
+ ...mountHost(),
18204
19354
  b.stmt(b.assignment('=', local(`_sty$${bind.id}`), V())),
18205
19355
  ]),
18206
19356
  );
@@ -18223,7 +19373,7 @@ function emitBindingMount(bind, elVar, bag) {
18223
19373
  : [];
18224
19374
  // Register the mount locals BEFORE the cleanup closure resolves their
18225
19375
  // bag letters (letter() throws for a never-mounted field).
18226
- const elLocal = local(`_el$${bind.id}`);
19376
+ const hostMount = mountHost();
18227
19377
  const spLocal = local(`_sp$${bind.id}`);
18228
19378
  const cleanup = b.arrow(
18229
19379
  [],
@@ -18250,7 +19400,7 @@ function emitBindingMount(bind, elVar, bag) {
18250
19400
  b.block([
18251
19401
  b.const('_v', bind.expr),
18252
19402
  b.stmt(b.call('_$setSpread', el(), V(), undefinedNode(), b.id('__s'), ...flags)),
18253
- b.stmt(b.assignment('=', elLocal, el())),
19403
+ ...hostMount,
18254
19404
  b.stmt(b.assignment('=', spLocal, V())),
18255
19405
  cleanupsPush(cleanup),
18256
19406
  ]),
@@ -18264,7 +19414,7 @@ function emitBindingMount(bind, elVar, bag) {
18264
19414
  b.assignment('=', b.member(el(), slotKeyLiteral(bind), true), value),
18265
19415
  );
18266
19416
  if (bind.mountOnly) return st(slotAssign);
18267
- return [st(b.stmt(b.assignment('=', local(`_el$${bind.id}`), el()))), st(slotAssign)];
19417
+ return [...mountHost().map(st), st(slotAssign)];
18268
19418
  }
18269
19419
  case 'formAction': {
18270
19420
  // <form action={fn}> / <button formAction={fn}>: wire the submit handler
@@ -18274,7 +19424,7 @@ function emitBindingMount(bind, elVar, bag) {
18274
19424
  b.block([
18275
19425
  b.const('_v', bind.expr),
18276
19426
  b.stmt(b.call(callee(), el(), nameLit(), V(), undefinedNode())),
18277
- b.stmt(b.assignment('=', local(`_el$${bind.id}`), el())),
19427
+ ...mountHost(),
18278
19428
  b.stmt(b.assignment('=', local(`_prev$${bind.id}`), V())),
18279
19429
  ]),
18280
19430
  );
@@ -18314,6 +19464,7 @@ function emitBindingMount(bind, elVar, bag) {
18314
19464
  // cleanup read because updates re-point it. Assigning both bag locals inside
18315
19465
  // the queue call evaluates each mount value once while avoiding throwaway
18316
19466
  // temporaries; Suspense still receives the exact ref/target pair.
19467
+ const initializeHost = bag.host(hostKey, elVar);
18317
19468
  return st(
18318
19469
  b.block([
18319
19470
  b.stmt(
@@ -18321,7 +19472,7 @@ function emitBindingMount(bind, elVar, bag) {
18321
19472
  '_$queueRefAttach',
18322
19473
  b.id('__s'),
18323
19474
  b.assignment('=', local(`_ref$${bind.id}`), bind.expr),
18324
- b.assignment('=', local(`_el$${bind.id}`), el()),
19475
+ initializeHost ? b.assignment('=', local(hostKey), el()) : local(hostKey),
18325
19476
  ),
18326
19477
  ),
18327
19478
  cleanupsPush(
@@ -19616,8 +20767,21 @@ function emitElementHtml(
19616
20767
  // is unreachable from the output: the key's map position resolves to
19617
20768
  // the HANDLER expression, and the name itself emits nothing.
19618
20769
  registerExactOrigin(ctx, attr.name, attr.name?.end, [`'${slotKey}'`, `"${slotKey}"`]);
19619
- if (capture) ctx.capturedEvents.add(eventName);
19620
- else ctx.delegatedEvents.add(eventName);
20770
+ if (capture) {
20771
+ ctx.capturedEvents.add(eventName);
20772
+ if (ctx.currentComponentOwner !== null) {
20773
+ ctx.currentComponentOwner.capturedEvents.add(eventName);
20774
+ } else {
20775
+ ctx.unownedCapturedEvents.add(eventName);
20776
+ }
20777
+ } else {
20778
+ ctx.delegatedEvents.add(eventName);
20779
+ if (ctx.currentComponentOwner !== null) {
20780
+ ctx.currentComponentOwner.delegatedEvents.add(eventName);
20781
+ } else {
20782
+ ctx.unownedDelegatedEvents.add(eventName);
20783
+ }
20784
+ }
19621
20785
  // Hot-path optimisation: `() => fn(arg, …)` arrows with zero params get
19622
20786
  // compiled to a `{ fn, args }` bundle so the runtime can identity-diff
19623
20787
  // fn + each arg and skip the property reassignment when nothing
@@ -20613,7 +21777,7 @@ function soleRenderPropChild(children) {
20613
21777
  // expression remains AST throughout so a nested `() => @{…}` sub-template
20614
21778
  // hoists (and server-mode `use(thenable)` calls get their stable keys).
20615
21779
  function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash) {
20616
- return {
21780
+ const child = {
20617
21781
  id: ctx.nextHelperId++,
20618
21782
  loc: devLoc(ctx, expr),
20619
21783
  origin: expr,
@@ -20625,6 +21789,27 @@ function makeChildCall(expr, ctx, componentName, inlinedSubs, cssHash) {
20625
21789
  inlinedSubs,
20626
21790
  ),
20627
21791
  };
21792
+ if (
21793
+ ctx.autoMemo &&
21794
+ // Imported calculations do not expose their return type. Restrict the
21795
+ // array guard to deferred component-child bodies, where cached descriptor
21796
+ // lists otherwise cross an expensive value-position boundary; an ordinary
21797
+ // scalar directly in its owning component must not acquire this region.
21798
+ ctx.currentBodyIsComponentScope !== true &&
21799
+ expr?.type === 'Identifier'
21800
+ ) {
21801
+ for (
21802
+ let proof = ctx.currentAutoCalculatedRenderableRefs;
21803
+ proof !== null;
21804
+ proof = proof.parent
21805
+ ) {
21806
+ if (proof.nodes.has(expr)) {
21807
+ child.autoMemoValue = true;
21808
+ break;
21809
+ }
21810
+ }
21811
+ }
21812
+ return child;
20628
21813
  }
20629
21814
 
20630
21815
  const AST_STRUCTURAL_KEY_SKIP = new Set([
@@ -20761,7 +21946,10 @@ function makeCompCall(
20761
21946
  ) {
20762
21947
  const id = ctx.nextHelperId++;
20763
21948
  const compName = tagBindingName(node);
20764
- const compNode = tagExprNode(node);
21949
+ const staticFragmentRenderer = node.openingElement?.metadata?.staticFragmentRenderer;
21950
+ const compNode = staticFragmentRenderer
21951
+ ? inheritOriginLoc(b.id(staticFragmentRenderer.name), node.openingElement?.name || node.id)
21952
+ : tagExprNode(node);
20765
21953
  // `</Card>` emits nothing — lend it the opening name's generated ranges so a
20766
21954
  // closing tag is reachable for components the way it is for host elements.
20767
21955
  registerOriginAlias(ctx, node.closingElement?.name, node.id || node.openingElement?.name);
@@ -20878,7 +22066,7 @@ function makeCompCall(
20878
22066
  }
20879
22067
 
20880
22068
  // The props object as a node; the call-site emit embeds it directly.
20881
- const propsExpr = inheritOriginLoc(b.object(propNodes), node);
22069
+ const propsExpr = staticFragmentRenderer?.props ?? inheritOriginLoc(b.object(propNodes), node);
20882
22070
 
20883
22071
  // Design (c) v0: decide whether the call site can use componentSlotLite
20884
22072
  // (Scope-only, no Block / no Comment markers / no CompSlot wrapper).
@@ -20909,7 +22097,12 @@ function makeCompCall(
20909
22097
  // elides iff the callee carries the definition-site `$$singleRoot` stamp
20910
22098
  // (docs/comment-marker-elision-plan.md M1).
20911
22099
  let maybeSingleRoot = false;
20912
- if (ctx.componentInfo && compName !== null) {
22100
+ if (staticFragmentRenderer) {
22101
+ // The renderer is already a void, hookless component body. A memo boundary
22102
+ // would force a full Block and could hide a hookful descendant's update.
22103
+ liteEligible = true;
22104
+ voidComponent = true;
22105
+ } else if (ctx.componentInfo && compName !== null) {
20913
22106
  const callSiteOk = !hasSpreadProp && !hasChildrenProp;
20914
22107
  const calleeInfo = ctx.componentInfo.get(compName);
20915
22108
  if (calleeInfo) {
@@ -20946,7 +22139,13 @@ function makeCompCall(
20946
22139
  }
20947
22140
  for (const name of free) {
20948
22141
  if (name === compName) continue;
20949
- if (ctx.importNamespaceNames.has(name)) {
22142
+ if (
22143
+ ctx.importNamespaceNames.has(name) ||
22144
+ // A local laundering a ref or live-import member read (see
22145
+ // collectAutoMemoLocalHazards) — its per-render value is not a
22146
+ // complete witness, so the site keeps ordinary entry semantics.
22147
+ ctx.currentAutoMemoLocalHazards?.has(name) === true
22148
+ ) {
20950
22149
  depsSafe = false;
20951
22150
  } else if (ctx.currentComponentLocals.has(name) || ctx.importedNames.has(name)) {
20952
22151
  if (!callsiteDeps.coveredRoots.has(name)) deps.add(name);
@@ -21196,6 +22395,96 @@ function makeSwitchCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = nul
21196
22395
  // for-of inside element children → forBlock call
21197
22396
  // ===========================================================================
21198
22397
 
22398
+ /**
22399
+ * Prove that one parent dependency affects a keyed host row only through its
22400
+ * root class's strict comparison with the row key. The runtime can then update
22401
+ * only the old/new selected blocks when every other dependency and the iterable
22402
+ * snapshot remain unchanged.
22403
+ *
22404
+ * This recognizes `selected === item.id` (or its reverse) under `key item.id`,
22405
+ * and the same proof for any other explicit, noncomputed item property.
22406
+ * Dynamic ternary arms, extra references to
22407
+ * `selected` anywhere in the row (including deferred event callbacks), spreads,
22408
+ * and anything other than one direct host root all fail closed.
22409
+ */
22410
+ function keyedSelectionDepIndex(itemName, keyBody, subStmts, runtimeDepNames, ctx) {
22411
+ if (subStmts.length !== 1 || !isPlainHostRoot(subStmts[0])) return -1;
22412
+ const root = subStmts[0];
22413
+ const attrs = root.attributes || root.openingElement?.attributes || [];
22414
+ if (attrs.some((attr) => attr.type !== 'Attribute' && attr.type !== 'JSXAttribute')) {
22415
+ return -1;
22416
+ }
22417
+ const classAttrs = attrs.filter((attr) => {
22418
+ const name = attr.name?.name || attr.name;
22419
+ return name === 'class' || name === 'className';
22420
+ });
22421
+ if (classAttrs.length !== 1) return -1;
22422
+ const value = classAttrs[0].value;
22423
+ const conditional = unwrapTsExpr(
22424
+ value?.type === 'JSXExpressionContainer' ? value.expression : value,
22425
+ );
22426
+ if (conditional?.type !== 'ConditionalExpression') return -1;
22427
+ const consequent = unwrapTsExpr(conditional.consequent);
22428
+ const alternate = unwrapTsExpr(conditional.alternate);
22429
+ if (
22430
+ consequent?.type !== 'Literal' ||
22431
+ typeof consequent.value !== 'string' ||
22432
+ alternate?.type !== 'Literal' ||
22433
+ typeof alternate.value !== 'string'
22434
+ ) {
22435
+ return -1;
22436
+ }
22437
+
22438
+ const test = unwrapTsExpr(conditional.test);
22439
+ if (test?.type !== 'BinaryExpression' || test.operator !== '===') return -1;
22440
+ const key = unwrapTsExpr(keyBody);
22441
+ if (
22442
+ key?.type !== 'MemberExpression' ||
22443
+ key.computed === true ||
22444
+ key.optional === true ||
22445
+ key.object?.type !== 'Identifier' ||
22446
+ key.object.name !== itemName ||
22447
+ key.property?.type !== 'Identifier'
22448
+ ) {
22449
+ return -1;
22450
+ }
22451
+ const keyProperty = key.property.name;
22452
+ const isItemKey = (expression) => {
22453
+ const member = unwrapTsExpr(expression);
22454
+ return (
22455
+ member?.type === 'MemberExpression' &&
22456
+ member.computed !== true &&
22457
+ member.optional !== true &&
22458
+ member.object?.type === 'Identifier' &&
22459
+ member.object.name === itemName &&
22460
+ member.property?.type === 'Identifier' &&
22461
+ member.property.name === keyProperty
22462
+ );
22463
+ };
22464
+ const left = unwrapTsExpr(test.left);
22465
+ const right = unwrapTsExpr(test.right);
22466
+ const selected = isItemKey(left) ? right : isItemKey(right) ? left : null;
22467
+ if (
22468
+ selected?.type !== 'Identifier' ||
22469
+ selected.name === itemName ||
22470
+ !ctx.currentComponentLocals?.has(selected.name)
22471
+ ) {
22472
+ return -1;
22473
+ }
22474
+ // Ignore this exact comparison operand, then use the existing scope-aware
22475
+ // reference walker to detect any other capture of the same outer binding.
22476
+ // Shadowed callback parameters therefore stay harmless while a real event
22477
+ // closure over the selection correctly disables the specialization.
22478
+ if (
22479
+ collectFreeIdentifiers(b.block(subStmts), new Set([itemName]), new Set([selected])).has(
22480
+ selected.name,
22481
+ )
22482
+ ) {
22483
+ return -1;
22484
+ }
22485
+ return runtimeDepNames.indexOf(selected.name);
22486
+ }
22487
+
21199
22488
  function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null) {
21200
22489
  // `@for await (...)` (async iteration) has no meaning for the runtime's
21201
22490
  // synchronous keyed reconciler. The TSRX parser currently rejects the surface
@@ -21345,16 +22634,16 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21345
22634
  // - PURE: body closes over nothing parent-reactive, no hooks, no comps,
21346
22635
  // no control flow. Reconciler skips renderBlock when item ref + index
21347
22636
  // 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.
22637
+ // - DEP-PURE: body DOES close over parent locals but has no hooks or opaque
22638
+ // components. Proven host-only conditional content is permitted when the
22639
+ // runtime preserves the row's active scope. The compiler emits an explicit
22640
+ // deps array so the reconciler can do ONE equality check per parent render
22641
+ // and, if unchanged, skip item-ref-and-index-stable survivors entirely.
21354
22642
  // - NORMAL: anything else → body runs every render.
21355
22643
  let pure = false;
21356
22644
  const depNames = [];
21357
22645
  let depEligible = false;
22646
+ let requiresScope = false;
21358
22647
  let itemMemo = false;
21359
22648
  let itemMemoContextAware = false;
21360
22649
  let itemMemoWitnesses = [];
@@ -21528,8 +22817,33 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21528
22817
  // survivor shortcut has no compiler-cache epoch cell to consult).
21529
22818
  if (itemMemoContextAware && autoMemoDeps === null) itemMemo = false;
21530
22819
  const hostPure = !hasParentClosure && !hasHook && !hasNestedComp && !hasRenderCall;
22820
+ const structuredHostDepEligible =
22821
+ ctx.autoMemo === true &&
22822
+ hasNestedComp &&
22823
+ hasParentClosure &&
22824
+ !hasHook &&
22825
+ !hasRenderCall &&
22826
+ node.nativeArrayMap === undefined &&
22827
+ autoMemoDeps !== null &&
22828
+ autoMemoDeps.every((name) => seenDeps.has(name) || ctx.importedNames.has(name)) &&
22829
+ !containsAutoMemoContextRead(bodyAst, ctx) &&
22830
+ hasOnlyHostConditionalItemBodies(subStmts);
22831
+ if (structuredHostDepEligible) {
22832
+ // The whole-list cache already witnesses imported projections. A row
22833
+ // survivor now makes the same decision independently, so its existing
22834
+ // dependency tuple must witness those live bindings as well.
22835
+ for (const name of autoMemoDeps) {
22836
+ if (seenDeps.has(name)) continue;
22837
+ seenDeps.add(name);
22838
+ depNames.push(name);
22839
+ }
22840
+ }
21531
22841
  const hostDepEligible =
21532
- !hostPure && !hasHook && hasParentClosure && !hasNestedComp && !hasRenderCall;
22842
+ !hostPure &&
22843
+ !hasHook &&
22844
+ hasParentClosure &&
22845
+ (!hasNestedComp || structuredHostDepEligible) &&
22846
+ !hasRenderCall;
21533
22847
  if (itemMemo && itemMemoWitnesses.length > 0) {
21534
22848
  pure = hostPure;
21535
22849
  depEligible = hostDepEligible;
@@ -21538,6 +22852,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21538
22852
  pure = hostPure || (itemMemo && depNames.length === 0);
21539
22853
  depEligible = !pure && !hasHook && (hostDepEligible || (itemMemo && depNames.length > 0));
21540
22854
  }
22855
+ requiresScope = depEligible && structuredHostDepEligible;
21541
22856
  depNames.sort();
21542
22857
  }
21543
22858
 
@@ -21565,6 +22880,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21565
22880
  envNames === null
21566
22881
  ? depNames
21567
22882
  : [...envNames, ...depNames.filter((name) => !envNames.includes(name))];
22883
+ // Restrict targeted invalidation to the existing production-only pure-render
22884
+ // contract. The whole-list memo proof rules out render-time mutations and
22885
+ // opaque state, while DEP-PURE guarantees a hookless host-only item body.
22886
+ // Native `.map()` uses a distinct mapSlot ABI and stays on its current path.
22887
+ const keyedSelectionIndex =
22888
+ ctx.autoMemo === true &&
22889
+ depEligible &&
22890
+ !requiresScope &&
22891
+ !itemMemo &&
22892
+ autoMemoDeps !== null &&
22893
+ !isDestructured &&
22894
+ !node.index &&
22895
+ !emptyStmts &&
22896
+ node.nativeArrayMap === undefined &&
22897
+ node.key != null
22898
+ ? keyedSelectionDepIndex(itemName, keyFn.body, subStmts, runtimeDepNames, ctx)
22899
+ : -1;
21568
22900
  let emptyHelperName = 'null';
21569
22901
  if (emptyStmts) {
21570
22902
  emptyHelperName = hoistBodyHelper(
@@ -21646,6 +22978,23 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21646
22978
  }
21647
22979
  }
21648
22980
 
22981
+ const ssrMarkerless = isSsrMarkerlessForItem(node);
22982
+ const hostRootTag = subStmts[0]?.id?.name ?? subStmts[0]?.openingElement?.name?.name;
22983
+ const hostMountSafe =
22984
+ ctx.autoMemo === true &&
22985
+ ctx._universalRuntimeUnit == null &&
22986
+ (parentNs === 'html' || (parentNs === 'opaque' && HTML_ONLY_TAGS.has(hostRootTag))) &&
22987
+ !emptyStmts &&
22988
+ !isDestructured &&
22989
+ !node.index &&
22990
+ subStmts.length === 1 &&
22991
+ singleRoot &&
22992
+ ssrMarkerless &&
22993
+ !containsComponentCallOrControlFlow(subStmts) &&
22994
+ !containsRenderCall(subStmts) &&
22995
+ !containsAutoMemoUnsafeStructure(subStmts) &&
22996
+ isHostMountSafeTree(subStmts[0]);
22997
+
21649
22998
  const mapCall = node.nativeArrayMap || null;
21650
22999
  return {
21651
23000
  id: ctx.nextHelperId++,
@@ -21679,18 +23028,21 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
21679
23028
  pure,
21680
23029
  singleRoot,
21681
23030
  singleRootExpr,
21682
- ssrMarkerless: isSsrMarkerlessForItem(node),
23031
+ ssrMarkerless,
23032
+ hostMountSafe,
21683
23033
  // The env union doubles as the deps array: emitted whenever the helpers
21684
23034
  // capture anything (Phase 2 — the runtime stamps it as block.extra), and
21685
23035
  // ALSO compared for the dep-pure survivor short-circuit when depEligible.
21686
23036
  // Component-local entries remain the tuple prefix the helpers destructure;
21687
23037
  // any appended import witnesses are comparison-only.
21688
23038
  depEligible,
23039
+ requiresScope,
21689
23040
  itemMemoWitnesses,
21690
23041
  itemMemoFlags,
21691
23042
  autoMemoDeps,
21692
23043
  autoMemoWitnesses,
21693
23044
  autoMemoContextAware,
23045
+ keyedSelectionIndex,
21694
23046
  depNames: runtimeDepNames,
21695
23047
  // True only when the header binds NO `index <name>` — the body then can't
21696
23048
  // observe an item's position, so a pure reorder (same item ref, position
@@ -21894,10 +23246,41 @@ function bodyContainsJsx(node) {
21894
23246
 
21895
23247
  /** @param {TemplateIR} ast */
21896
23248
  function allocTemplate(ctx, ast, ns = 0, frag = 0) {
23249
+ const { html, origins } = serializeTemplateIr(ast);
23250
+ const intern = !ctx.hmr && !ctx.dev && !ctx.profile;
23251
+ const bucket = intern ? ctx.internedTemplates.get(html) : undefined;
23252
+ const existing = Array.isArray(bucket)
23253
+ ? bucket.find((template) => template.ns === ns && template.frag === frag)
23254
+ : bucket?.ns === ns && bucket.frag === frag
23255
+ ? bucket
23256
+ : undefined;
23257
+ if (existing !== undefined) {
23258
+ if (ctx.inspect && origins !== null && existing.origins !== null) {
23259
+ for (const origin of origins) {
23260
+ const canonical = existing.origins.find(
23261
+ (entry) =>
23262
+ entry.kind === origin.kind && entry.start === origin.start && entry.end === origin.end,
23263
+ );
23264
+ if (canonical !== undefined) {
23265
+ registerOriginAlias(
23266
+ ctx,
23267
+ { start: origin.srcStart, end: origin.srcEnd },
23268
+ { start: canonical.srcStart },
23269
+ );
23270
+ }
23271
+ }
23272
+ }
23273
+ return existing.name;
23274
+ }
21897
23275
  const id = ctx.nextTemplateId++;
21898
23276
  const name = `_t$${id}`;
21899
- const { html, origins } = serializeTemplateIr(ast);
21900
- ctx.hoistedTemplates.push({ name, ast, html, ns, frag, origins });
23277
+ const template = { name, ast, html, ns, frag, origins };
23278
+ ctx.hoistedTemplates.push(template);
23279
+ if (intern) {
23280
+ if (bucket === undefined) ctx.internedTemplates.set(html, template);
23281
+ else if (Array.isArray(bucket)) bucket.push(template);
23282
+ else ctx.internedTemplates.set(html, [bucket, template]);
23283
+ }
21901
23284
  return name;
21902
23285
  }
21903
23286