octane 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cjs/context-identity.cjs +29 -0
  2. package/dist/cjs/dom-stage.cjs +24 -18
  3. package/dist/cjs/error-codes.server.generated.cjs +5 -0
  4. package/dist/cjs/hydration-markers.cjs +12 -0
  5. package/dist/cjs/index.cjs +2 -0
  6. package/dist/cjs/runtime.cjs +253 -393
  7. package/dist/cjs/runtime.server.cjs +137 -35
  8. package/dist/cjs/server/index.cjs +2 -0
  9. package/dist/cjs/server/streamed-signals.cjs +41 -9
  10. package/dist/cjs/signals/client.cjs +1 -6
  11. package/dist/cjs/signals/engine.cjs +1 -12
  12. package/dist/cjs/version.cjs +1 -1
  13. package/dist/compiler/client-only-server.js +1 -28
  14. package/dist/compiler/compile-renderer-boundaries.js +1 -22
  15. package/dist/compiler/compile-universal.js +1 -22
  16. package/dist/compiler/compile.js +782 -359
  17. package/dist/compiler/dom-binding-fixed-props.js +11 -1
  18. package/dist/compiler/generated-origin.js +46 -0
  19. package/dist/compiler/hydrate-boundaries.js +88 -52
  20. package/dist/compiler/plain-hook-memo.js +5 -1
  21. package/dist/compiler/pure-factories.js +66 -0
  22. package/dist/compiler/register.js +4 -1
  23. package/dist/compiler/slot-hooks.js +113 -186
  24. package/dist/compiler/typescript-lowering.js +763 -0
  25. package/dist/compiler/void-roots.js +239 -0
  26. package/dist/compiler/volar.js +91 -38
  27. package/dist/context-identity.d.ts +14 -0
  28. package/dist/context-identity.js +28 -0
  29. package/dist/dom-stage.js +24 -18
  30. package/dist/error-codes.server.generated.d.ts +1 -0
  31. package/dist/error-codes.server.generated.js +5 -0
  32. package/dist/hydration/idle.js +1 -0
  33. package/dist/hydration/independent-island.js +53 -2
  34. package/dist/hydration/media.js +1 -0
  35. package/dist/hydration/types.d.ts +5 -0
  36. package/dist/hydration/visible.js +1 -0
  37. package/dist/hydration-markers.d.ts +8 -0
  38. package/dist/hydration-markers.js +8 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +2 -0
  41. package/dist/runtime.d.ts +25 -40
  42. package/dist/runtime.js +254 -398
  43. package/dist/runtime.server.d.ts +23 -2
  44. package/dist/runtime.server.js +142 -36
  45. package/dist/server/index.d.ts +1 -1
  46. package/dist/server/index.js +2 -0
  47. package/dist/server/streamed-signals.js +41 -9
  48. package/dist/signals/client.js +3 -8
  49. package/dist/signals/engine.d.ts +0 -2
  50. package/dist/signals/engine.js +1 -13
  51. package/dist/universal-core.js +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +1 -1
  54. package/dist/cjs/signals/diagnostic-local-hook.cjs +0 -37
  55. package/dist/compiler/local-void-roots.js +0 -154
  56. package/dist/signals/diagnostic-local-hook.d.ts +0 -8
  57. package/dist/signals/diagnostic-local-hook.js +0 -12
@@ -127,7 +127,12 @@ import { collectProvenContextBindings, isProvenContextUse } from './context-use.
127
127
  import { assertNoLegacyContextProviders } from './context-provider.js';
128
128
  import { applyCssModuleConstants } from './css-module-constants.js';
129
129
  import { assertUniversalRuntimeTarget, normalizeUniversalRuntime } from './universal-runtime.js';
130
- import { findLocalVoidRootCallees } from './local-void-roots.js';
130
+ import { findRootFactoryImports, proveVoidRoots } from './void-roots.js';
131
+ import {
132
+ lowerParameterProperties,
133
+ lowerTypeScriptStatements,
134
+ needsTypeScriptStatementLowering,
135
+ } from './typescript-lowering.js';
131
136
  import { findPrivateCompiledContexts } from './private-context.js';
132
137
 
133
138
  // DOM truth tables shared with the client/server runtimes (via constants.ts) —
@@ -148,6 +153,11 @@ import {
148
153
  hyphenateStyleName,
149
154
  } from '../dom-tables.js';
150
155
  import { isDelegatedEventProp } from '../event-names.js';
156
+ import {
157
+ collectPureFactoryLocals,
158
+ isPureFactoryCall,
159
+ octanePureFactoryNames,
160
+ } from './pure-factories.js';
151
161
  import { sanitizeURLAttribute, shouldSanitizeURLAttribute } from '../sanitize-url.js';
152
162
  import {
153
163
  invalidHtmlNestingWithAncestor,
@@ -1610,7 +1620,7 @@ function collectOctaneImportBindings(astBody) {
1610
1620
  return { locals, namespaces, foreignLocals };
1611
1621
  }
1612
1622
 
1613
- function collectNestedBindingNames(root) {
1623
+ export function collectNestedBindingNames(root) {
1614
1624
  const names = new Set();
1615
1625
  const seen = new WeakSet();
1616
1626
  const walk = (value) => {
@@ -1721,40 +1731,25 @@ function errorBoundaryFallback(node, fallbackAttribute) {
1721
1731
  }
1722
1732
 
1723
1733
  /**
1724
- * Annotate direct calls to octane's `lazy` with `@__PURE__` so bundlers can
1725
- * tree-shake unused lazy-component declarations. Creating a lazy descriptor
1726
- * has no side effects, and React builds get the same effect (an unused
1727
- * `React.lazy(() => import('./x.client'))` const disappears from the server
1728
- * bundle, which Start's import-protection tree-shake verification relies on
1729
- * for `*.client.*` modules). Locals shadowed anywhere below module scope are
1730
- * skipped wholesale — same conservatism as lowerImportedErrorBoundaries.
1734
+ * Annotate direct calls to octane's side-effect-free factories with
1735
+ * `@__PURE__` so bundlers can tree-shake unused declarations (see
1736
+ * pure-factories.js for why the declaration-side `@__NO_SIDE_EFFECTS__` is not
1737
+ * enough under esbuild). Creating a lazy descriptor has no side effects, and
1738
+ * React builds get the same effect (an unused `React.lazy(() =>
1739
+ * import('./x.client'))` const disappears from the server bundle, which Start's
1740
+ * import-protection tree-shake verification relies on for `*.client.*`
1741
+ * modules). Locals shadowed anywhere below module scope are skipped wholesale —
1742
+ * same conservatism as lowerImportedErrorBoundaries.
1731
1743
  */
1732
- function annotatePureLazyCalls(ast) {
1733
- const lazyLocals = new Set();
1734
- for (const node of ast.body || []) {
1735
- if (node.type !== 'ImportDeclaration' || node.source?.value !== 'octane') continue;
1736
- for (const specifier of node.specifiers || []) {
1737
- if (
1738
- specifier.type === 'ImportSpecifier' &&
1739
- (specifier.imported?.name ?? specifier.imported?.value) === 'lazy' &&
1740
- specifier.local?.name
1741
- ) {
1742
- lazyLocals.add(specifier.local.name);
1743
- }
1744
- }
1745
- }
1746
- if (lazyLocals.size === 0) return ast;
1747
- for (const name of collectNestedBindingNames(ast.body)) lazyLocals.delete(name);
1748
- if (lazyLocals.size === 0) return ast;
1744
+ function annotatePureFactoryCalls(ast, names) {
1745
+ const pureLocals = collectPureFactoryLocals(ast.body, names);
1746
+ if (pureLocals.size === 0) return ast;
1747
+ for (const name of collectNestedBindingNames(ast.body)) pureLocals.delete(name);
1748
+ if (pureLocals.size === 0) return ast;
1749
1749
  // COW: the @__PURE__ mark rides on a shallow copy of the call node (marker
1750
1750
  // props are compiler-owned annotations and may only live on rebuilt nodes).
1751
1751
  const body = mapAst(ast.body, (node) => {
1752
- if (
1753
- node.type === 'CallExpression' &&
1754
- node.callee?.type === 'Identifier' &&
1755
- lazyLocals.has(node.callee.name) &&
1756
- node.__octanePure !== true
1757
- ) {
1752
+ if (isPureFactoryCall(node, pureLocals) && node.__octanePure !== true) {
1758
1753
  return { ...node, __octanePure: true };
1759
1754
  }
1760
1755
  return null;
@@ -2427,6 +2422,71 @@ function styleObjectNeedsNativeBinding(node) {
2427
2422
  });
2428
2423
  }
2429
2424
 
2425
+ const STATIC_HOST_COMPONENT_NODES = new Set([
2426
+ 'JSXElement',
2427
+ 'JSXOpeningElement',
2428
+ 'JSXClosingElement',
2429
+ 'JSXFragment',
2430
+ 'JSXOpeningFragment',
2431
+ 'JSXClosingFragment',
2432
+ 'JSXAttribute',
2433
+ 'JSXIdentifier',
2434
+ 'JSXNamespacedName',
2435
+ 'JSXText',
2436
+ 'JSXExpressionContainer',
2437
+ 'JSXEmptyExpression',
2438
+ 'Literal',
2439
+ 'TemplateLiteral',
2440
+ 'TemplateElement',
2441
+ ]);
2442
+
2443
+ /**
2444
+ * A component that evaluates no authored expression cannot perform a native
2445
+ * read, so a native-read module need not bracket its render. The proof is an
2446
+ * allowlist: no setup statements, no destructured or defaulted parameters, only
2447
+ * lowercase host elements (component tags invoke authored code), and only
2448
+ * literal attribute and child values. Any other node keeps the bracket.
2449
+ */
2450
+ function isStaticHostComponent(node) {
2451
+ if (node?.body?.type !== 'JSXCodeBlock' || (node.body.body?.length ?? 0) !== 0) return false;
2452
+ if (!(node.params ?? []).every((param) => param.type === 'Identifier')) return false;
2453
+ const visit = (value) => {
2454
+ if (value === null || typeof value !== 'object') return true;
2455
+ if (Array.isArray(value)) return value.every(visit);
2456
+ // Plain records such as a template quasi's { raw, cooked } hold no code.
2457
+ if (value.type === undefined)
2458
+ return Object.values(value).every((item) => item === null || typeof item !== 'object');
2459
+ if (!STATIC_HOST_COMPONENT_NODES.has(value.type)) return false;
2460
+ if (value.type === 'JSXOpeningElement' && !/^[a-z]/.test(value.name?.name ?? '')) return false;
2461
+ if (value.type === 'Literal' && value.regex !== undefined) return false;
2462
+ if (value.type === 'TemplateLiteral' && value.expressions.length !== 0) return false;
2463
+ for (const key in value) {
2464
+ if (key === 'loc' || key === 'metadata' || key === 'parent' || key === 'range') continue;
2465
+ const child = value[key];
2466
+ if (child !== null && typeof child === 'object' && !visit(child)) return false;
2467
+ }
2468
+ return true;
2469
+ };
2470
+ return node.body.render != null && visit(node.body.render);
2471
+ }
2472
+
2473
+ // A fresh object literal whose properties are all plain data: enumerating it
2474
+ // runs no accessor, and nothing else can mutate it. nativeStyleBinding can then
2475
+ // check its values at runtime and skip the owning Block while all are scalar.
2476
+ function isPlainStyleObjectLiteral(node) {
2477
+ return (
2478
+ node?.type === 'ObjectExpression' &&
2479
+ (node.properties ?? []).every(
2480
+ (property) =>
2481
+ (property.type === 'Property' || property.type === 'ObjectProperty') &&
2482
+ !property.computed &&
2483
+ !property.method &&
2484
+ (property.kind === undefined || property.kind === 'init') &&
2485
+ (property.key?.name ?? property.key?.value) !== '__proto__',
2486
+ )
2487
+ );
2488
+ }
2489
+
2430
2490
  // DEV must send invalid literal declarations through the same style helpers as
2431
2491
  // dynamic objects. Keep valid literals and all production output fully baked.
2432
2492
  function staticStyleObjectNeedsDevValidation(obj) {
@@ -5570,6 +5630,99 @@ function collectImmutableModuleFunctions(body) {
5570
5630
  return declared;
5571
5631
  }
5572
5632
 
5633
+ /**
5634
+ * Top-level module bindings, split by whether their identity can change after
5635
+ * evaluation. `mutable` holds `let`/`var` declarations, class declarations, and
5636
+ * function declarations that collectImmutableModuleFunctions demoted: an
5637
+ * identity-only survivor skip cannot witness them, so a region reading one
5638
+ * must re-render. `all` adds consts, unreassigned functions, enums, and
5639
+ * imports; a free name outside it (and outside every enclosing local) is a
5640
+ * global. Only top-level declarations count — a same-named parameter or local
5641
+ * elsewhere in the module never shadows the global a region actually reads.
5642
+ */
5643
+ function collectModuleTopLevelBindings(body, immutableFunctions) {
5644
+ const all = new Set();
5645
+ const mutable = new Set();
5646
+ for (const statement of body) {
5647
+ if (statement.type === 'ImportDeclaration') {
5648
+ for (const specifier of statement.specifiers || []) {
5649
+ if (specifier.local) all.add(specifier.local.name);
5650
+ }
5651
+ continue;
5652
+ }
5653
+ const declaration =
5654
+ statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
5655
+ ? statement.declaration
5656
+ : statement;
5657
+ if (!declaration) continue;
5658
+ if (declaration.type === 'VariableDeclaration') {
5659
+ const names = new Set();
5660
+ for (const d of declaration.declarations || []) collectBindings(d.id, names);
5661
+ for (const name of names) {
5662
+ all.add(name);
5663
+ if (declaration.kind !== 'const') mutable.add(name);
5664
+ }
5665
+ } else if (declaration.type === 'ClassDeclaration' && declaration.id) {
5666
+ all.add(declaration.id.name);
5667
+ mutable.add(declaration.id.name);
5668
+ } else if (
5669
+ declaration.type === 'FunctionDeclaration' &&
5670
+ declaration.id?.type === 'Identifier'
5671
+ ) {
5672
+ all.add(declaration.id.name);
5673
+ if (!immutableFunctions.has(declaration.id.name)) mutable.add(declaration.id.name);
5674
+ } else if (
5675
+ (declaration.type === 'TSEnumDeclaration' ||
5676
+ declaration.type === 'TSModuleDeclaration' ||
5677
+ declaration.type === 'TSImportEqualsDeclaration') &&
5678
+ declaration.id?.type === 'Identifier'
5679
+ ) {
5680
+ all.add(declaration.id.name);
5681
+ }
5682
+ }
5683
+ return { all, mutable };
5684
+ }
5685
+
5686
+ // Globals a render-time read may treat as fixed: the value constants and the
5687
+ // standard ECMAScript namespaces and constructors. Any other unbound name
5688
+ // (`location`, `window`, `document`, `globalThis`, an app-installed global) is
5689
+ // host or application state that no compiler witness observes.
5690
+ const IMMUTABLE_AMBIENT_GLOBALS = new Set([
5691
+ 'undefined',
5692
+ 'NaN',
5693
+ 'Infinity',
5694
+ ...SETUP_PURE_GLOBAL_CALLEES,
5695
+ ...SETUP_PURE_NAMESPACE_MEMBERS.keys(),
5696
+ 'Error',
5697
+ 'TypeError',
5698
+ 'RangeError',
5699
+ 'RegExp',
5700
+ 'Map',
5701
+ 'Set',
5702
+ 'WeakMap',
5703
+ 'WeakSet',
5704
+ 'Promise',
5705
+ 'Reflect',
5706
+ 'Intl',
5707
+ ]);
5708
+
5709
+ /**
5710
+ * Does `name`, a free read of a memo region that is neither an enclosing local
5711
+ * (ctx.currentComponentLocals, extended through every hoisted helper) nor an
5712
+ * import, resolve to state that can change without any witness the compiler
5713
+ * compares? True for mutable top-level module bindings and for non-intrinsic
5714
+ * globals. An enclosing binding the local set misses reads as a global and
5715
+ * fails closed — a lost fast path, never a stale row.
5716
+ */
5717
+ function isUnwitnessedAmbientRead(name, ctx) {
5718
+ // `_$…` names are runtime helpers the compiler itself imports.
5719
+ if (name.startsWith('_$')) return false;
5720
+ const module = ctx.moduleTopLevelBindings;
5721
+ if (module === undefined) return false;
5722
+ if (module.mutable.has(name)) return true;
5723
+ return !module.all.has(name) && !IMMUTABLE_AMBIENT_GLOBALS.has(name);
5724
+ }
5725
+
5573
5726
  // A component whose synchronous proof reads its props is safe only at JSX
5574
5727
  // edges that construct every read property as an own data field. Missing keys
5575
5728
  // can reach an inherited getter, and `__proto__` changes an object literal's
@@ -8153,7 +8306,8 @@ function normalizeArrowComponents(ast) {
8153
8306
  // `.tsrx` module. This is RUNTIME-ONLY: the Volar/TS-server path (volar.js) is a
8154
8307
  // separate pipeline that intentionally PRESERVES all types for the language
8155
8308
  // service, so it never calls this. Enums and value namespaces have runtime
8156
- // semantics and are deliberately NOT treated as type-only.
8309
+ // semantics and are deliberately NOT treated as type-only: the print lowers
8310
+ // them to plain JavaScript instead (typescript-lowering.js).
8157
8311
  function isTypeOnlyStatement(node) {
8158
8312
  if (node == null) return false;
8159
8313
  if (
@@ -8177,8 +8331,13 @@ function isTypeOnlyStatement(node) {
8177
8331
  );
8178
8332
  }
8179
8333
  }
8180
- // `import type { … } from …`
8181
- if (node.type === 'ImportDeclaration' && node.importKind === 'type') return true;
8334
+ // `import type { … } from …` / `import type X = require('…')`
8335
+ if (
8336
+ (node.type === 'ImportDeclaration' || node.type === 'TSImportEqualsDeclaration') &&
8337
+ node.importKind === 'type'
8338
+ ) {
8339
+ return true;
8340
+ }
8182
8341
  // `export type { … }`, `export type X = …`, `export interface I {}`
8183
8342
  if (node.type === 'ExportNamedDeclaration') {
8184
8343
  if (node.exportKind === 'type') return true;
@@ -9976,6 +10135,15 @@ function compileInternal(
9976
10135
  // rebuilt with builders/spreads (locations carried via setLocation) and
9977
10136
  // untouched subtrees stay shared with the parse by reference.
9978
10137
  let ast = parsedAst;
10138
+ // Void roots, private compiled contexts and compiled split Hydrate are
10139
+ // same-module proofs over the authored DOM tree. Renderer participation is
10140
+ // decided per module: `rendererBoundaryPreparation` is non-null exactly when
10141
+ // this module renders a configured boundary tag, and nested renderer compiles
10142
+ // carry `__rendererBoundariesLowered`/`__universal`. The project-wide
10143
+ // `rendererBoundaries`/`rendererRegistry` options alone do not disqualify a
10144
+ // module — bundlers pass them to every module once any boundary exists, and
10145
+ // the specialized runtime entries share the public Context/root shapes that
10146
+ // renderer bridges read.
9979
10147
  const localVoidRootsEnabled =
9980
10148
  !options?.dev &&
9981
10149
  !options?.hmr &&
@@ -9984,9 +10152,8 @@ function compileInternal(
9984
10152
  options?.universalRuntime == null &&
9985
10153
  options?.__universal == null &&
9986
10154
  !options?.__rendererBoundariesLowered &&
9987
- rendererBoundaryPreparation === null &&
9988
- options?.rendererBoundaries == null &&
9989
- options?.rendererRegistry == null;
10155
+ rendererBoundaryPreparation === null;
10156
+ const rootFactories = localVoidRootsEnabled ? findRootFactoryImports(ast) : new Map();
9990
10157
  const authoredVoidRootIds = new Set();
9991
10158
  const privateCompiledContexts = localVoidRootsEnabled
9992
10159
  ? findPrivateCompiledContexts(ast)
@@ -9994,21 +10161,7 @@ function compileInternal(
9994
10161
  const splitPrivateContexts = localVoidRootsEnabled
9995
10162
  ? privateCompiledContextsForHydrateAst(parsedAst)
9996
10163
  : null;
9997
- if (
9998
- localVoidRootsEnabled &&
9999
- ast.body.some(
10000
- (node) =>
10001
- node.type === 'ImportDeclaration' &&
10002
- node.source?.value === 'octane' &&
10003
- node.specifiers.some(
10004
- (specifier) =>
10005
- specifier.type === 'ImportSpecifier' &&
10006
- ['createRoot', 'hydrateRoot'].includes(
10007
- specifier.imported.name ?? specifier.imported.value,
10008
- ),
10009
- ),
10010
- )
10011
- ) {
10164
+ if (rootFactories.size > 0) {
10012
10165
  for (const statement of ast.body) {
10013
10166
  const node =
10014
10167
  statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration'
@@ -10027,7 +10180,15 @@ function compileInternal(
10027
10180
  // Normalize arrow-function components (`const X = () => @{…}`) to
10028
10181
  // FunctionDeclaration form so the component pipeline recognizes them.
10029
10182
  ast = normalizeArrowComponents(ast);
10030
- ast = annotatePureLazyCalls(ast);
10183
+ ast = annotatePureFactoryCalls(
10184
+ ast,
10185
+ octanePureFactoryNames({
10186
+ clientDom:
10187
+ (options?.renderer?.target ?? 'dom') === 'dom' &&
10188
+ universalRuntime === undefined &&
10189
+ options?.__universal == null,
10190
+ }),
10191
+ );
10031
10192
  const errorBoundaryLowering = lowerImportedErrorBoundaries(ast);
10032
10193
  ast = errorBoundaryLowering.ast;
10033
10194
  const consumedRuntimeLocals = errorBoundaryLowering.consumed;
@@ -10402,6 +10563,13 @@ function compileInternal(
10402
10563
  // that are ever assigned (`foo = bar`) are excluded: their identity is then
10403
10564
  // as mutable as a module `let`, which fails closed.
10404
10565
  ctx.moduleFunctionDeclarations = collectImmutableModuleFunctions(ast.body);
10566
+ // Top-level module bindings (and the mutable subset): identity-only survivor
10567
+ // skips (forBlock PURE / DEP-PURE) fail closed when a row reads a mutable
10568
+ // one or a global (see isUnwitnessedAmbientRead).
10569
+ ctx.moduleTopLevelBindings = collectModuleTopLevelBindings(
10570
+ ast.body,
10571
+ ctx.moduleFunctionDeclarations,
10572
+ );
10405
10573
  // Global namespaces are only trusted by the setup checkpoint analysis when
10406
10574
  // no module binding shadows them (see setupCanScheduleSelfUpdate).
10407
10575
  ctx._moduleBoundNames = collectModuleBoundNames(ast);
@@ -10505,25 +10673,71 @@ function compileInternal(
10505
10673
  });
10506
10674
  }
10507
10675
  }
10508
- if (authoredVoidRootIds.size > 0) {
10509
- const definitions = new Set(
10510
- [...ctx.componentInfo.values()]
10511
- .filter((info) => info.voidOutput && authoredVoidRootIds.has(info.node.id))
10512
- .map((info) => info.node.id),
10513
- );
10676
+ if (
10677
+ rootFactories.size > 0 &&
10678
+ (authoredVoidRootIds.size > 0 ||
10679
+ (ctx.isVoidComponentImport !== null && ctx.importedComponentBindings.size > 0))
10680
+ ) {
10681
+ // Definition IDs come from the authored, exact shorthand declarations:
10682
+ // arrow normalization and return-JSX lowering cannot manufacture this
10683
+ // evidence. Imports rely on the adapter's proof of the loaded export ABI.
10684
+ const writes = authoredVoidRootIds.size > 0 ? collectReassignedBindings(ast) : null;
10685
+ const stableComponents = new Set();
10686
+ for (const info of ctx.componentInfo.values())
10687
+ if (info.voidOutput && authoredVoidRootIds.has(info.node.id) && !writes.has(info.node.id))
10688
+ stableComponents.add(info.node.id.name);
10514
10689
  const components = new Set([...ctx.componentInfo.values()].map((info) => info.node));
10515
- const callees = findLocalVoidRootCallees(ast, definitions, components);
10690
+ const callees = new Map();
10691
+ let loweredElements = null;
10692
+ for (const root of proveVoidRoots(ast, {
10693
+ factories: rootFactories,
10694
+ component(name) {
10695
+ if (stableComponents.has(name)) return true;
10696
+ const imported = ctx.importedComponentBindings.get(name);
10697
+ return imported !== undefined &&
10698
+ ctx.isVoidComponentImport?.(imported.request, imported.imported) === true
10699
+ ? true
10700
+ : undefined;
10701
+ },
10702
+ // Roots created while a component renders stay on the generic path.
10703
+ skip: (node) => node.type === 'JSXCodeBlock' || components.has(node),
10704
+ })) {
10705
+ callees.set(root.callee, root.helper);
10706
+ // Native reads keep their scoped element resolver around `$` reads.
10707
+ for (const { call, index } of ctx.nativeReads ? [] : root.elements) {
10708
+ const props = voidRootElementProps(call.arguments[index]);
10709
+ if (props !== null) (loweredElements ??= new Map()).set(call, { index, props });
10710
+ }
10711
+ }
10516
10712
  if (callees.size > 0) {
10517
10713
  const aliases = new Map();
10518
- for (const helper of new Set(callees.values())) {
10714
+ const helpers = new Set(callees.values());
10715
+ if (loweredElements !== null) helpers.add('__voidRootProps');
10716
+ for (const helper of helpers) {
10519
10717
  const alias = allocCompilerName(ctx, rtAlias(helper));
10520
10718
  (ctx.privateRuntimeAliases ??= new Map()).set(helper, alias);
10521
10719
  ctx.runtimeNeeded.add(helper);
10522
10720
  aliases.set(helper, alias);
10523
10721
  }
10524
- ast = mapAst(ast, (node) =>
10525
- callees.has(node) ? { ...node, name: aliases.get(callees.get(node)) } : null,
10526
- );
10722
+ const rewrite = (node) => {
10723
+ if (callees.has(node)) return { ...node, name: aliases.get(callees.get(node)) };
10724
+ const lowered = loweredElements?.get(node);
10725
+ if (lowered === undefined) return null;
10726
+ // `root.render(<C .../>)` → `root.render(C, props)`: the root unwraps
10727
+ // the element to exactly this body/props pair, with a null key.
10728
+ const element = node.arguments[lowered.index];
10729
+ const name = element.openingElement.name;
10730
+ const tag = () => inheritOriginLoc(b.id(name.name), name);
10731
+ const args = [...node.arguments];
10732
+ args.splice(
10733
+ lowered.index,
10734
+ 1,
10735
+ tag(),
10736
+ inheritOriginLoc(b.call(aliases.get('__voidRootProps'), tag(), lowered.props), element),
10737
+ );
10738
+ return mapAst({ ...node, arguments: args }, rewrite);
10739
+ };
10740
+ ast = mapAst(ast, rewrite);
10527
10741
  }
10528
10742
  }
10529
10743
  if (privateCompiledContexts.size > 0 || splitPrivateContexts?.callees.size > 0) {
@@ -11494,7 +11708,7 @@ function compileServer(
11494
11708
  // Normalize arrow-function components (`const X = () => @{…}`) to
11495
11709
  // FunctionDeclaration form so the component pipeline recognizes them.
11496
11710
  ast = normalizeArrowComponents(ast);
11497
- ast = annotatePureLazyCalls(ast);
11711
+ ast = annotatePureFactoryCalls(ast, octanePureFactoryNames({ clientDom: false }));
11498
11712
  const errorBoundaryLowering = lowerImportedErrorBoundaries(ast);
11499
11713
  ast = errorBoundaryLowering.ast;
11500
11714
  const consumedRuntimeLocals = errorBoundaryLowering.consumed;
@@ -12504,12 +12718,22 @@ function ssrEmitNodes(
12504
12718
  // adjacency. The client counterpart is the hole-aware `sibling()` walk in
12505
12719
  // runtime.ts, which treats separators as protocol nodes. Keep the comment
12506
12720
  // payload in sync with HYDRATION_TEXT_SEP (constants.ts).
12721
+ //
12722
+ // A dynamic hole with siblings is a `<!>` position the client walks from
12723
+ // (htextSwap), so an empty value must still serialize ONE node: wrap it in
12724
+ // ssrTextSlot, which emits an empty anchor comment for ''. A sole child
12725
+ // needs no stand-in: the client mounts it with htext (element) or it ends
12726
+ // its block range (root/arm), and no later walk starts from it.
12507
12727
  let prevText = null; // 'static' | 'dyn' | null — last emitted part's text kind
12508
12728
  for (const n of nodes) {
12509
12729
  const kind = textAdjacencyKind(n, ctx);
12510
12730
  if (kind === 'empty') continue; // serializes nothing — skip, adjacency-transparent
12511
12731
  const nlGuard = nlGuardFirst && parts.length === 0;
12512
- const p = ssrEmitNode(n, ctx, name, inlinedSubs, parentNs, cssHash, componentNs, nlGuard);
12732
+ let p = ssrEmitNode(n, ctx, name, inlinedSubs, parentNs, cssHash, componentNs, nlGuard);
12733
+ if (p && kind === 'dyn' && nodes.length > 1) {
12734
+ ctx.runtimeNeeded.add('ssrTextSlot');
12735
+ p = ssrCall('ssrTextSlot', [p], n);
12736
+ }
12513
12737
  if (p) {
12514
12738
  if (kind !== 'other' && prevText !== null && (kind === 'dyn' || prevText === 'dyn')) {
12515
12739
  parts.push('<!-- -->');
@@ -14465,6 +14689,10 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
14465
14689
  containsComponentCallOrControlFlow(itemBody) ||
14466
14690
  containsRenderCall(itemBody) ||
14467
14691
  ssrAstCallsAny(itemSub.fn, ['_$ssrChild', '_$ssrComponent']);
14692
+ const signalSite =
14693
+ mapCall === null && (ctx.signalBindingsUsed || ctx.nativeReads) && containsRenderCall(itemBody)
14694
+ ? ssrControlKey('for', node)
14695
+ : null;
14468
14696
  const itemCall = ssrSubCall(
14469
14697
  itemSub.fnName,
14470
14698
  node.index ? [b.id('__it'), b.id('__i')] : [b.id('__it')],
@@ -14484,7 +14712,11 @@ function ssrEmitFor(node, ctx, name, inlinedSubs, parentNs, cssHash, componentNs
14484
14712
  node.index ? b.id('__i') : ssrVoid(node),
14485
14713
  b.id('__s'),
14486
14714
  b.literal(!sharedItemRange && !bindingSite),
14487
- ...(mapCall === null ? [] : [b.literal(true)]),
14715
+ ...(signalSite !== null
14716
+ ? [b.literal(false), b.literal(signalSite)]
14717
+ : mapCall === null
14718
+ ? []
14719
+ : [b.literal(true)]),
14488
14720
  ],
14489
14721
  node,
14490
14722
  )
@@ -16255,7 +16487,7 @@ function compileFunctionBody(node, ctx, name, parentNs = 'html', cssHash = null,
16255
16487
  b.id(name, node.id ?? node),
16256
16488
  fnParams,
16257
16489
  b.block(
16258
- ctx.nativeReads
16490
+ ctx.nativeReads && !isStaticHostComponent(node)
16259
16491
  ? wrapNativeReadScope(presentationBody, b.id('__s'), nativeReadNames(ctx))
16260
16492
  : presentationBody,
16261
16493
  ),
@@ -16811,15 +17043,30 @@ function depPathKey(node) {
16811
17043
  // props OBJECT with unchanged fields doesn't refetch), bare identifiers
16812
17044
  // otherwise. Only outer runtime references become deps; locally declared
16813
17045
  // bindings and erased syntax do not.
16814
- function collectDepPaths(expr, coarsenDepRoots = null) {
17046
+ //
17047
+ // Deps evaluate eagerly on every render, so they may only perform reads the
17048
+ // authored render performs. A read inside a nested function (which may never
17049
+ // run during render) or behind a condition that cannot be replayed is
17050
+ // deferred: a module-bound root keeps its precise path as `root?.prop`, and an
17051
+ // ambient global (not bound in the enclosing scope chain) contributes no dep at
17052
+ // all, because it may not exist in this environment (`window` under SSR).
17053
+ // `isModuleBound` is a scope-aware `(name) => boolean` from
17054
+ // `moduleBoundCheckForDeps`, resolving through the expression's enclosing scope
17055
+ // chain so that sibling-function parameters never leak into the decision.
17056
+ function collectDepPaths(expr, coarsenDepRoots = null, isModuleBound = null) {
16815
17057
  const deps = [];
16816
17058
  const seen = new Set();
16817
17059
  const lexical = createLexicalAnalysis(expr);
16818
17060
  let guards = null;
16819
17061
  let nextGuard = 0;
17062
+ let deferred = 0;
17063
+ let ownArguments = 0;
16820
17064
  const isFree = (node, parent, key) =>
16821
17065
  isIdentifierReference(node, parent, key, lexical) &&
16822
- !lexical.isBound(lexical.nodeScopes.get(node) ?? lexical.rootScope, node.name);
17066
+ !lexical.isBound(lexical.nodeScopes.get(node) ?? lexical.rootScope, node.name) &&
17067
+ !(ownArguments > 0 && node.name === 'arguments');
17068
+ const deferredAmbient = (name) =>
17069
+ deferred > 0 && (isModuleBound === null || !isModuleBound(name));
16823
17070
  const push = (node, key) => {
16824
17071
  if (coarsenDepRoots !== null) {
16825
17072
  const member = depPathMember(node);
@@ -16892,10 +17139,16 @@ function collectDepPaths(expr, coarsenDepRoots = null) {
16892
17139
  return null;
16893
17140
  }
16894
17141
 
17142
+ function walkDeferred(node, parent, key) {
17143
+ deferred++;
17144
+ walk(node, parent, key);
17145
+ deferred--;
17146
+ }
17147
+
16895
17148
  function walkGuarded(node, test, truthy) {
16896
17149
  const guard = necessaryTypeofGuard(test, truthy);
16897
17150
  if (guard === null) {
16898
- walk(node, null, null);
17151
+ walkDeferred(node, null, null);
16899
17152
  return;
16900
17153
  }
16901
17154
  const previous = guards;
@@ -16944,15 +17197,33 @@ function collectDepPaths(expr, coarsenDepRoots = null) {
16944
17197
  if (n.operator === '&&' || n.operator === '||') {
16945
17198
  walkGuarded(n.right, n.left, n.operator === '&&');
16946
17199
  } else {
16947
- walk(n.right, n, 'right');
17200
+ walkDeferred(n.right, n, 'right');
16948
17201
  }
16949
17202
  return;
17203
+ case 'FunctionExpression':
17204
+ case 'FunctionDeclaration':
17205
+ case 'ArrowFunctionExpression': {
17206
+ // A non-arrow function owns `arguments`; it is never the component's.
17207
+ const own = n.type !== 'ArrowFunctionExpression';
17208
+ if (own) ownArguments++;
17209
+ deferred++;
17210
+ forEachRuntimeAstChild(n, (child, childKey) => walk(child, n, childKey));
17211
+ deferred--;
17212
+ if (own) ownArguments--;
17213
+ return;
17214
+ }
17215
+ case 'PropertyDefinition':
17216
+ // Instance field initializers run per construction, not at render.
17217
+ if (n.computed) walk(n.key, n, 'key');
17218
+ if (n.static) walk(n.value, n, 'value');
17219
+ else walkDeferred(n.value, n, 'value');
17220
+ return;
16950
17221
  case 'MetaProperty':
16951
17222
  // `import.meta` and `new.target` contain syntax tokens, not free
16952
17223
  // bindings. Visiting their Identifier children creates invalid deps.
16953
17224
  return;
16954
17225
  case 'Identifier':
16955
- if (isFree(n, parent, key)) push(b.id(n.name), depPathKey(n));
17226
+ if (isFree(n, parent, key) && !deferredAmbient(n.name)) push(b.id(n.name), depPathKey(n));
16956
17227
  return;
16957
17228
  case 'UnaryExpression': {
16958
17229
  const argument = unwrapTsExpr(n.argument);
@@ -16969,14 +17240,14 @@ function collectDepPaths(expr, coarsenDepRoots = null) {
16969
17240
  case 'MemberExpression': {
16970
17241
  const propertyName = staticDepMemberName(n);
16971
17242
  if (n.object.type === 'Identifier' && propertyName !== null) {
16972
- if (isFree(n.object, n, 'object')) {
17243
+ if (isFree(n.object, n, 'object') && !deferredAmbient(n.object.name)) {
16973
17244
  const member = b.member(
16974
17245
  b.id(n.object.name),
16975
17246
  n.computed
16976
17247
  ? b.literal(propertyName, JSON.stringify(propertyName))
16977
17248
  : b.id(propertyName),
16978
17249
  n.computed,
16979
- n.optional === true,
17250
+ n.optional === true || deferred > 0,
16980
17251
  );
16981
17252
  // Optional MemberExpressions must remain inside a ChainExpression.
16982
17253
  // Besides keeping the synthesized tree valid ESTree, the wrapper
@@ -16993,6 +17264,19 @@ function collectDepPaths(expr, coarsenDepRoots = null) {
16993
17264
  }
16994
17265
  }
16995
17266
 
17267
+ // Scope-aware module-bound check: returns a function `(name) => boolean` that
17268
+ // resolves through the scope chain at `exprNode`'s position in the module AST.
17269
+ // Nested parameters and catch bindings from unrelated functions are invisible,
17270
+ // so a shadowed global (e.g. `function fmt(document)`) does not trick
17271
+ // deferred-ambient into emitting an eager dep for an undeclared name.
17272
+ function moduleBoundCheckForDeps(ctx, exprNode) {
17273
+ if (ctx.activityModuleAst == null) return null;
17274
+ const lexical = (ctx.activityLexical ??= createLexicalAnalysis(ctx.activityModuleAst));
17275
+ const scope = lexical.nodeScopes.get(exprNode);
17276
+ if (scope === undefined) return null;
17277
+ return (name) => lexical.isBound(scope, name);
17278
+ }
17279
+
16996
17280
  // A `use()` argument that needs no memoization: already-stable references
16997
17281
  // (identifiers, static member chains) and literals.
16998
17282
  function isTrivialUseArg(n) {
@@ -17228,7 +17512,7 @@ function makeCreationMemoCall(
17228
17512
  // promise — and the derived creation would never refresh when its upstream
17229
17513
  // promise does. Coarsen member deps rooted at render-created locals to the
17230
17514
  // bare identifier (dedup follows).
17231
- const deps = collectDepPaths(expr, coarsenDepRoots);
17515
+ const deps = collectDepPaths(expr, coarsenDepRoots, moduleBoundCheckForDeps(ctx, expr));
17232
17516
  // Server mirror: `puMemo` — keyed CROSS-PASS creation cache (a fresh
17233
17517
  // SSRScope per pass makes client useMemo semantics useless there).
17234
17518
  const memoHelper = ctx.nativeReads
@@ -17938,7 +18222,7 @@ function parallelUseWalkJsx(nodes, ctx, componentName, creations, warmChildren,
17938
18222
  kind: 'useMemo',
17939
18223
  node: expr,
17940
18224
  });
17941
- const deps = collectDepPaths(expr);
18225
+ const deps = collectDepPaths(expr, null, moduleBoundCheckForDeps(ctx, expr));
17942
18226
  const memoAlias = requireRuntimeForContext(ctx, ctx.nativeReads ? 'nativePuMemo' : 'puMemo');
17943
18227
  changed = true;
17944
18228
  // The minted prop-memo wrapper maps to the authored prop expression.
@@ -23512,9 +23796,10 @@ function applyStringChildProofs(ast, source, filename, facts) {
23512
23796
  const primitiveProofs = new Set();
23513
23797
  const intrinsicCalls = [];
23514
23798
  const writes = [];
23515
- let intrinsicMutationReference = false;
23516
- const intrinsicMutationReferences = [];
23517
- const intrinsicAliases = [];
23799
+ const mutatorReferences = [];
23800
+ const destructuredMutators = new WeakSet();
23801
+ const globalValueUses = [];
23802
+ let globalValueDestructured = false;
23518
23803
  const ambientIntrinsicValues = new Set();
23519
23804
  const constantDeclarations = [];
23520
23805
  const references = [];
@@ -23557,38 +23842,38 @@ function applyStringChildProofs(ast, source, filename, facts) {
23557
23842
  if (primitiveRanges?.delete(range)) primitiveProofs.add(expr);
23558
23843
  }
23559
23844
  if (inspectIntrinsicCalls) {
23560
- if (
23561
- node.type === 'MemberExpression' ||
23562
- (node.type === 'Property' && parent?.type === 'ObjectPattern')
23563
- ) {
23564
- const property = node.type === 'MemberExpression' ? node.property : node.key;
23565
- const method = node.computed ? property?.value : (property?.name ?? property?.value);
23566
- if (
23567
- INTRINSIC_MUTATION_METHODS.has(method) ||
23568
- (node.computed && property?.type !== 'Literal')
23569
- ) {
23570
- // Unknown computed references may extract a mutator or sit under a
23571
- // TypeScript/optional-chain wrapper before invocation. Decline the new
23572
- // intrinsic-local proof even for unrelated dynamic property reads;
23573
- // this is rejection only, not built-in name admission.
23574
- intrinsicMutationReference = true;
23575
- }
23576
- }
23577
- if (
23578
- node.type === 'VariableDeclarator' &&
23579
- parent?.type === 'VariableDeclaration' &&
23580
- parent.kind === 'const' &&
23581
- node.id?.type === 'Identifier' &&
23582
- node.init
23583
- ) {
23584
- intrinsicAliases.push(node);
23585
- }
23845
+ // Mutator references: `Reflect.set`, `obj[key]`, `const { set } = R`.
23846
+ // The receiver decides later whether one can reach a text intrinsic.
23586
23847
  if (node.type === 'MemberExpression' && isPossibleIntrinsicMutator(node)) {
23587
- intrinsicMutationReferences.push([node.object, node, parent]);
23848
+ mutatorReferences.push({ reference: node, receiver: node.object, parent });
23588
23849
  } else if (node.type === 'VariableDeclarator' && node.id?.type === 'ObjectPattern') {
23589
23850
  for (const property of node.id.properties || []) {
23590
23851
  if (property.type === 'Property' && isPossibleIntrinsicMutator(property)) {
23591
- intrinsicMutationReferences.push([node.init, property]);
23852
+ destructuredMutators.add(property);
23853
+ mutatorReferences.push({ reference: property, receiver: node.init, parent: null });
23854
+ }
23855
+ }
23856
+ } else if (
23857
+ node.type === 'Property' &&
23858
+ parent?.type === 'ObjectPattern' &&
23859
+ !destructuredMutators.has(node)
23860
+ ) {
23861
+ // Parameter/assignment patterns hide their source: fail closed.
23862
+ if (isPossibleIntrinsicMutator(node)) {
23863
+ mutatorReferences.push({ reference: node, receiver: null, parent: null });
23864
+ }
23865
+ if (GLOBAL_VALUE_MEMBERS.has(staticPropertyKeyName(node))) globalValueDestructured = true;
23866
+ }
23867
+ // Where a global object, Object, or Reflect flows as a value, a later
23868
+ // alias can reach it; wrapper parents defer to the outermost wrapper.
23869
+ if (!TRANSPARENT_VALUE_WRAPPERS.has(parent?.type)) {
23870
+ const value = unwrapValueExpression(node);
23871
+ if (
23872
+ (value?.type === 'Identifier' && GLOBAL_VALUE_NAMES.has(value.name)) ||
23873
+ (value?.type === 'MemberExpression' && GLOBAL_VALUE_NAMES.has(staticMemberName(value)))
23874
+ ) {
23875
+ if (!isNonEscapingValueUse(parent, key)) {
23876
+ globalValueUses.push({ node, value, parent, key });
23592
23877
  }
23593
23878
  }
23594
23879
  }
@@ -23637,165 +23922,52 @@ function applyStringChildProofs(ast, source, filename, facts) {
23637
23922
  );
23638
23923
  }
23639
23924
  let lexical = null;
23925
+ // One decision guards every intrinsic-derived proof: inline calls, supplied
23926
+ // type facts, and primitive locals. TypeScript keeps a built-in's declared
23927
+ // primitive return type after an asserted replacement, so once a mutation
23928
+ // is visible none of those proofs survive. A write naming a text intrinsic
23929
+ // is always visible; mutator and computed references are visible when
23930
+ // their receiver is a global object, Object, or Reflect, or whenever such a
23931
+ // value escapes into a binding, argument, return, or property where an
23932
+ // alias could reach it. Values from other modules stay outside this proof.
23933
+ let mutationVisible = false;
23640
23934
  if (
23641
- intrinsicCalls.length > 0 ||
23642
- ((stringProofs.size > 0 || primitiveProofs.size > 0) &&
23643
- (writes.length > 0 || intrinsicMutationReferences.length > 0))
23935
+ (intrinsicCalls.length > 0 || stringProofs.size > 0 || primitiveProofs.size > 0) &&
23936
+ (writes.length > 0 || mutatorReferences.length > 0)
23644
23937
  ) {
23645
- // The existing scope analysis understands var hoisting, parameter defaults,
23646
- // imports, catch bindings, and TSRX @for/@try scopes. Pay for it only when
23647
- // an intrinsic candidate or a possible proof-invalidating write needs it.
23648
23938
  lexical = createLexicalAnalysis(ast);
23649
- if (writes.some((target) => writesGlobalTextIntrinsic(target, lexical))) {
23650
- // TypeScript still uses a built-in constructor's declared primitive
23651
- // return type after an asserted replacement. Decline all new proofs in this
23652
- // rare module rather than attempting incomplete value-taint analysis.
23653
- // Supplied ranges were still validated above; explicit text syntax keeps
23654
- // its existing coercion meaning through isKnownStringExpression.
23655
- return ast;
23656
- }
23657
- }
23658
- if (lexical !== null) {
23659
- const aliases = new WeakMap();
23660
- for (const declaration of intrinsicAliases) {
23661
- const scope = lexical.resolveBinding(
23662
- lexical.nodeScopes.get(declaration.id),
23663
- declaration.id.name,
23664
- )?.scope;
23665
- if (scope === undefined) continue;
23666
- let values = aliases.get(scope);
23667
- if (values === undefined) aliases.set(scope, (values = new Map()));
23668
- values.set(declaration.id.name, declaration.init);
23669
- }
23670
- const initializer = (node) => {
23671
- const scope = lexical.resolveBinding(lexical.nodeScopes.get(node), node.name)?.scope;
23672
- return scope === undefined ? undefined : aliases.get(scope)?.get(node.name);
23673
- };
23674
- const intrinsicMemberName = (node) => {
23675
- const property = node.computed ? unwrapTsExpr(node.property) : node.property;
23676
- return node.computed
23677
- ? property?.type === 'Literal' && typeof property.value === 'string'
23678
- ? property.value
23679
- : null
23680
- : property?.type === 'Identifier'
23681
- ? property.name
23682
- : null;
23683
- };
23684
- const globalOrigin = (expression, seen = new Set(), constructors = false) => {
23685
- const node = unwrapTsExpr(expression);
23686
- if (!node || seen.has(node)) return null;
23687
- seen.add(node);
23688
- if (node.type === 'Identifier') {
23689
- const scope = lexical.nodeScopes.get(node);
23690
- if (scope === undefined) return null;
23691
- if (!lexical.isBound(scope, node.name)) {
23692
- return INLINE_INTRINSIC_MUTATION_GLOBALS.has(node.name) ||
23693
- (constructors && TEXT_INTRINSICS.has(node.name))
23694
- ? node.name
23695
- : null;
23696
- }
23697
- return globalOrigin(initializer(node), seen, constructors);
23698
- }
23699
- if (node.type === 'ChainExpression') return globalOrigin(node.expression, seen, constructors);
23700
- if (node.type === 'MemberExpression') {
23701
- const name = intrinsicMemberName(node);
23702
- if (name !== 'Object' && name !== 'Reflect' && !(constructors && TEXT_INTRINSICS.has(name)))
23703
- return null;
23704
- const origin = globalOrigin(node.object, seen);
23705
- return origin !== null && INLINE_INTRINSIC_GLOBAL_RECEIVERS.has(origin) ? name : null;
23706
- }
23707
- return null;
23708
- };
23709
- const freshTarget = (expression, seen = new Set()) => {
23710
- const node = unwrapTsExpr(expression);
23711
- if (!node || seen.has(node)) return false;
23712
- seen.add(node);
23713
- if (node.type === 'ObjectExpression' || node.type === 'ArrayExpression') return true;
23714
- return node.type === 'Identifier' && freshTarget(initializer(node), seen);
23715
- };
23716
- const visibleMutation =
23717
- intrinsicMutationReferences.some(([receiver, reference, parent]) => {
23718
- const origin = globalOrigin(receiver);
23719
- const key = reference.type === 'Property' ? reference.key : reference.property;
23720
- const property = reference.computed ? unwrapTsExpr(key) : key;
23721
- const method = reference.computed ? property?.value : (property?.name ?? property?.value);
23722
- if (origin === 'Object' || origin === 'Reflect') {
23723
- // A normal native call targeting a fresh literal cannot directly write
23724
- // a global constructor. Unknown getter/callback effects retain the
23725
- // existing builtin assumptions; wrapped/extracted calls fail closed.
23726
- const target = parent?.arguments?.[0];
23727
- return !(
23728
- parent?.type === 'CallExpression' &&
23729
- parent.optional !== true &&
23730
- parent.callee === reference &&
23731
- reference.optional !== true &&
23732
- INLINE_INTRINSIC_MUTATION_METHODS.has(method) &&
23733
- freshTarget(target)
23734
- );
23735
- }
23736
- return (
23737
- origin !== null &&
23738
- INLINE_INTRINSIC_GLOBAL_RECEIVERS.has(origin) &&
23739
- (method === '__defineGetter__' ||
23740
- method === '__defineSetter__' ||
23741
- (reference.computed && property?.type !== 'Literal'))
23742
- );
23743
- }) ||
23744
- writes.some((target) => {
23745
- const node = unwrapTsExpr(target);
23746
- if (node?.type !== 'MemberExpression') return false;
23747
- const name = node.computed ? node.property?.value : node.property?.name;
23748
- if (!TEXT_INTRINSICS.has(name) && !(node.computed && node.property?.type !== 'Literal'))
23749
- return false;
23750
- const origin = globalOrigin(node.object);
23751
- return origin !== null && INLINE_INTRINSIC_GLOBAL_RECEIVERS.has(origin);
23752
- });
23753
- const unsafeCalls = new WeakSet();
23939
+ mutationVisible = isTextIntrinsicMutationVisible(
23940
+ writes,
23941
+ mutatorReferences,
23942
+ globalValueDestructured ||
23943
+ globalValueUses.some(
23944
+ ({ node, value, parent, key }) =>
23945
+ isGlobalValue(value, lexical) &&
23946
+ (value !== node ||
23947
+ value.type !== 'Identifier' ||
23948
+ isIdentifierReference(node, parent, key, lexical)),
23949
+ ),
23950
+ lexical,
23951
+ );
23952
+ }
23953
+ if (mutationVisible) {
23954
+ // Supplied ranges were still validated above; explicit text syntax keeps
23955
+ // its existing coercion meaning through isKnownStringExpression.
23956
+ stringProofs.clear();
23957
+ primitiveProofs.clear();
23958
+ } else if (intrinsicCalls.length > 0) {
23959
+ // The existing scope analysis understands var hoisting, parameter defaults,
23960
+ // imports, catch bindings, and TSRX @for/@try scopes.
23961
+ lexical ??= createLexicalAnalysis(ast);
23754
23962
  for (const call of intrinsicCalls) {
23755
23963
  const name = call.callee.name;
23756
- if (ambientIntrinsicValues.has(name)) continue;
23964
+ if (ambientIntrinsicValues.has(name) || call.optional === true) continue;
23757
23965
  const scope = lexical.nodeScopes.get(call);
23758
23966
  if (scope !== undefined && !lexical.isBound(scope, name)) {
23759
- if (visibleMutation) {
23760
- unsafeCalls.add(call);
23761
- continue;
23762
- }
23763
- if (call.optional === true) continue;
23764
23967
  if (name === 'String' || name === 'Date') stringProofs.add(call);
23765
23968
  else primitiveProofs.add(call);
23766
23969
  }
23767
23970
  }
23768
- if (visibleMutation) {
23769
- // TypeScript still describes the built-in return type after an asserted
23770
- // replacement. Drop only supplied proofs that depend on these calls;
23771
- // unrelated typed scalars and JavaScript's own coercion guarantees survive.
23772
- const unsafeProof = (node, seen = new WeakSet()) => {
23773
- if (!node || typeof node !== 'object' || seen.has(node)) return false;
23774
- seen.add(node);
23775
- if (unsafeCalls.has(node)) return true;
23776
- if (node.type === 'CallExpression') {
23777
- let callee = unwrapTsExpr(node.callee);
23778
- while (callee?.type === 'ChainExpression') callee = unwrapTsExpr(callee.expression);
23779
- if (callee?.type === 'MemberExpression') {
23780
- const method = intrinsicMemberName(callee);
23781
- if (method === 'call' || method === 'apply') callee = callee.object;
23782
- }
23783
- // Supplied facts may describe a saved constructor or its Function
23784
- // call/apply result. Invalidation grants no new primitive admission.
23785
- if (TEXT_INTRINSICS.has(globalOrigin(callee, new Set(), true))) return true;
23786
- }
23787
- if (node.type === 'Identifier' && unsafeProof(initializer(node), seen)) return true;
23788
- if (Array.isArray(node)) return node.some((child) => unsafeProof(child, seen));
23789
- for (const key in node) {
23790
- if (AST_WALK_SKIP_KEYS.has(key)) continue;
23791
- if (unsafeProof(node[key], seen)) return true;
23792
- }
23793
- return false;
23794
- };
23795
- for (const proofs of [stringProofs, primitiveProofs]) {
23796
- for (const proof of proofs) if (unsafeProof(proof)) proofs.delete(proof);
23797
- }
23798
- }
23799
23971
  }
23800
23972
  const valueProofs = new Set();
23801
23973
  if (
@@ -23805,11 +23977,8 @@ function applyStringChildProofs(ast, source, filename, facts) {
23805
23977
  constantDeclarations.some((declaration) => isPrimitiveValueExpression(declaration.init)))
23806
23978
  ) {
23807
23979
  lexical ??= createLexicalAnalysis(ast);
23808
- // A local alias can mutate a global constructor without spelling its
23809
- // global receiver. This check only declines new local intrinsic facts;
23810
- // operator/template primitives and existing child proofs remain.
23811
- const intrinsicLocalResultsSafe =
23812
- !intrinsicMutationReference && !writes.some(mayWriteTextIntrinsicMember);
23980
+ // Operator/template primitives stay provable after a visible mutation.
23981
+ const intrinsicLocalResultsSafe = !mutationVisible;
23813
23982
  const reassigned = collectReassignedBindings(ast);
23814
23983
  const declarationsByScope = new Map();
23815
23984
  for (const declaration of constantDeclarations) {
@@ -23915,94 +24084,151 @@ const INTRINSIC_MUTATION_METHODS = new Set([
23915
24084
  'setPrototypeOf',
23916
24085
  'deleteProperty',
23917
24086
  ]);
24087
+ // Unbound names that may hold the global object, Object, or Reflect. Member
24088
+ // reads of these names reach the same values only through a global receiver
24089
+ // (`window.top`), except for the unambiguous names below (`x.Reflect`).
24090
+ const GLOBAL_OBJECT_NAMES = new Set([
24091
+ 'globalThis',
24092
+ 'window',
24093
+ 'self',
24094
+ 'global',
24095
+ 'frames',
24096
+ 'parent',
24097
+ 'top',
24098
+ ]);
24099
+ const GLOBAL_VALUE_NAMES = new Set([...GLOBAL_OBJECT_NAMES, 'Object', 'Reflect']);
24100
+ const GLOBAL_VALUE_MEMBERS = new Set(['globalThis', 'window', 'Object', 'Reflect']);
24101
+ const TRANSPARENT_VALUE_WRAPPERS = new Set([
24102
+ 'TSAsExpression',
24103
+ 'TSNonNullExpression',
24104
+ 'TSTypeAssertion',
24105
+ 'TSSatisfiesExpression',
24106
+ 'ParenthesizedExpression',
24107
+ 'ChainExpression',
24108
+ ]);
23918
24109
 
23919
- function mayWriteTextIntrinsicMember(target) {
23920
- target = unwrapTsExpr(target);
23921
- if (!target || typeof target !== 'object') return false;
23922
- if (target.type === 'AssignmentPattern') return mayWriteTextIntrinsicMember(target.left);
23923
- if (target.type === 'MemberExpression') {
23924
- return target.computed
23925
- ? target.property?.type !== 'Literal' || TEXT_INTRINSICS.has(target.property.value)
23926
- : TEXT_INTRINSICS.has(target.property?.name);
23927
- }
23928
- if (target.type === 'RestElement') return mayWriteTextIntrinsicMember(target.argument);
23929
- if (target.type === 'ArrayPattern') {
23930
- return (target.elements || []).some(mayWriteTextIntrinsicMember);
23931
- }
23932
- if (target.type === 'ObjectPattern') {
23933
- return (target.properties || []).some((property) =>
23934
- mayWriteTextIntrinsicMember(property.argument ?? property.value),
23935
- );
24110
+ function unwrapValueExpression(node) {
24111
+ while (node && TRANSPARENT_VALUE_WRAPPERS.has(node.type)) node = node.expression;
24112
+ return node;
24113
+ }
24114
+
24115
+ function staticMemberName(node) {
24116
+ const property = node.computed ? unwrapTsExpr(node.property) : node.property;
24117
+ if (node.computed) {
24118
+ return property?.type === 'Literal' && typeof property.value === 'string'
24119
+ ? property.value
24120
+ : null;
23936
24121
  }
23937
- return false;
24122
+ return property?.type === 'Identifier' ? property.name : null;
23938
24123
  }
23939
24124
 
23940
- const INLINE_INTRINSIC_MUTATION_METHODS = new Set([
23941
- 'assign',
23942
- 'defineProperty',
23943
- 'defineProperties',
23944
- 'set',
23945
- '__defineGetter__',
23946
- '__defineSetter__',
23947
- 'setPrototypeOf',
23948
- 'deleteProperty',
23949
- ]);
23950
- const INLINE_INTRINSIC_GLOBAL_RECEIVERS = new Set(['globalThis', 'window', 'self', 'global']);
23951
- const INLINE_INTRINSIC_MUTATION_GLOBALS = new Set([
23952
- 'Object',
23953
- 'Reflect',
23954
- ...INLINE_INTRINSIC_GLOBAL_RECEIVERS,
23955
- ]);
24125
+ function staticPropertyKeyName(property) {
24126
+ const key = property.computed ? unwrapTsExpr(property.key) : property.key;
24127
+ if (key?.type === 'Identifier' && !property.computed) return key.name;
24128
+ return key?.type === 'Literal' && typeof key.value === 'string' ? key.value : null;
24129
+ }
24130
+
24131
+ // A use that reads through, tests, or discards a value cannot alias it.
24132
+ function isNonEscapingValueUse(parent, key) {
24133
+ switch (parent?.type) {
24134
+ case 'MemberExpression':
24135
+ return key === 'object';
24136
+ case 'UnaryExpression':
24137
+ case 'BinaryExpression':
24138
+ case 'ExpressionStatement':
24139
+ return true;
24140
+ case 'CallExpression':
24141
+ case 'NewExpression':
24142
+ return key === 'callee';
24143
+ case 'IfStatement':
24144
+ case 'ConditionalExpression':
24145
+ case 'WhileStatement':
24146
+ case 'DoWhileStatement':
24147
+ case 'ForStatement':
24148
+ return key === 'test';
24149
+ default:
24150
+ return false;
24151
+ }
24152
+ }
24153
+
24154
+ function isGlobalValue(expression, lexical) {
24155
+ const node = unwrapValueExpression(expression);
24156
+ if (node?.type === 'Identifier') {
24157
+ if (!GLOBAL_VALUE_NAMES.has(node.name)) return false;
24158
+ const scope = lexical.nodeScopes.get(node);
24159
+ return scope !== undefined && !lexical.isBound(scope, node.name);
24160
+ }
24161
+ if (node?.type !== 'MemberExpression') return false;
24162
+ const name = staticMemberName(node);
24163
+ return (
24164
+ GLOBAL_VALUE_MEMBERS.has(name) ||
24165
+ (GLOBAL_VALUE_NAMES.has(name) && isGlobalValue(node.object, lexical))
24166
+ );
24167
+ }
23956
24168
 
23957
24169
  // Inspect the reference itself: TS/optional wrappers and extracted methods may
23958
- // hide its eventual call. Lexical receiver checks keep ordinary computed reads
23959
- // and application .set methods from invalidating intrinsic-result proofs.
24170
+ // hide its eventual call.
23960
24171
  function isPossibleIntrinsicMutator(node) {
23961
24172
  const key = node.type === 'Property' ? node.key : node.property;
23962
24173
  const property = node.computed ? unwrapTsExpr(key) : key;
23963
24174
  const name = node.computed ? property?.value : (property?.name ?? property?.value);
23964
- return (
23965
- INLINE_INTRINSIC_MUTATION_METHODS.has(name) || (node.computed && property?.type !== 'Literal')
23966
- );
24175
+ return INTRINSIC_MUTATION_METHODS.has(name) || (node.computed && property?.type !== 'Literal');
23967
24176
  }
23968
24177
 
23969
- // A visible replacement of a global constructor invalidates inferred text
23970
- // proofs for the module. Calls still evaluate the authored callee: a replacement
23971
- // may return an element rather than the built-in's primitive result.
23972
- function writesGlobalTextIntrinsic(target, lexical) {
23973
- target = unwrapTsExpr(target);
23974
- if (!target || typeof target !== 'object') return false;
23975
- const unbound = (node, name) => {
24178
+ function isTextIntrinsicMutationVisible(writes, mutatorReferences, globalValueEscapes, lexical) {
24179
+ const unboundIntrinsic = (node) => {
23976
24180
  const scope = lexical.nodeScopes.get(node);
23977
- return scope !== undefined && !lexical.isBound(scope, name);
23978
- };
23979
- if (target.type === 'Identifier') {
23980
- return TEXT_INTRINSICS.has(target.name) && unbound(target, target.name);
23981
- }
23982
- if (target.type === 'MemberExpression') {
23983
- const name = target.computed ? target.property?.value : target.property?.name;
23984
- const object = target.object;
23985
24181
  return (
23986
- TEXT_INTRINSICS.has(name) &&
23987
- object?.type === 'Identifier' &&
23988
- (object.name === 'globalThis' ||
23989
- object.name === 'window' ||
23990
- object.name === 'self' ||
23991
- object.name === 'global') &&
23992
- unbound(object, object.name)
24182
+ TEXT_INTRINSICS.has(node.name) && scope !== undefined && !lexical.isBound(scope, node.name)
23993
24183
  );
23994
- }
23995
- if (target.type === 'AssignmentPattern') return writesGlobalTextIntrinsic(target.left, lexical);
23996
- if (target.type === 'RestElement') return writesGlobalTextIntrinsic(target.argument, lexical);
23997
- if (target.type === 'ArrayPattern') {
23998
- return (target.elements || []).some((element) => writesGlobalTextIntrinsic(element, lexical));
23999
- }
24000
- if (target.type === 'ObjectPattern') {
24001
- return (target.properties || []).some((property) =>
24002
- writesGlobalTextIntrinsic(property.argument ?? property.value, lexical),
24184
+ };
24185
+ const mayReachIntrinsic = (receiver) =>
24186
+ receiver == null || globalValueEscapes || isGlobalValue(receiver, lexical);
24187
+ const writesIntrinsic = (target) => {
24188
+ target = unwrapTsExpr(target);
24189
+ if (!target || typeof target !== 'object') return false;
24190
+ if (target.type === 'Identifier') return unboundIntrinsic(target);
24191
+ if (target.type === 'MemberExpression') {
24192
+ // A named write is visible on any receiver; a computed key only when
24193
+ // its receiver can be a global object.
24194
+ const property = target.computed ? unwrapTsExpr(target.property) : target.property;
24195
+ if (!target.computed) return TEXT_INTRINSICS.has(property?.name);
24196
+ if (property?.type === 'Literal') return TEXT_INTRINSICS.has(property.value);
24197
+ return mayReachIntrinsic(target.object);
24198
+ }
24199
+ if (target.type === 'AssignmentPattern') return writesIntrinsic(target.left);
24200
+ if (target.type === 'RestElement') return writesIntrinsic(target.argument);
24201
+ if (target.type === 'ArrayPattern') return (target.elements || []).some(writesIntrinsic);
24202
+ if (target.type === 'ObjectPattern') {
24203
+ return (target.properties || []).some((property) =>
24204
+ writesIntrinsic(property.argument ?? property.value),
24205
+ );
24206
+ }
24207
+ return false;
24208
+ };
24209
+ if (writes.some(writesIntrinsic)) return true;
24210
+ return mutatorReferences.some(({ reference, receiver, parent }) => {
24211
+ const key = reference.type === 'Property' ? reference.key : reference.property;
24212
+ const property = reference.computed ? unwrapTsExpr(key) : key;
24213
+ const method = reference.computed ? property?.value : (property?.name ?? property?.value);
24214
+ // These install accessors on whichever object receives them.
24215
+ if (method === '__defineGetter__' || method === '__defineSetter__') return true;
24216
+ if (!mayReachIntrinsic(receiver)) return false;
24217
+ if (globalValueEscapes || receiver == null) return true;
24218
+ const target = unwrapValueExpression(receiver);
24219
+ const receiverName = target.type === 'Identifier' ? target.name : staticMemberName(target);
24220
+ if (receiverName !== 'Object' && receiverName !== 'Reflect') return true;
24221
+ // A direct native call whose arguments hold no global value (any such
24222
+ // argument escapes) cannot target a global object. Wrapped, optional,
24223
+ // and extracted methods hide their eventual target.
24224
+ return !(
24225
+ parent?.type === 'CallExpression' &&
24226
+ parent.callee === reference &&
24227
+ parent.optional !== true &&
24228
+ reference.optional !== true &&
24229
+ INTRINSIC_MUTATION_METHODS.has(method)
24003
24230
  );
24004
- }
24005
- return false;
24231
+ });
24006
24232
  }
24007
24233
 
24008
24234
  // Syntax/annotation proof shared by text bindings and non-text optimizations.
@@ -24321,18 +24547,62 @@ const TS_TYPE_PROPS = [
24321
24547
  'declare', // `declare` modifier
24322
24548
  'override', // class member `override`
24323
24549
  'implements', // `class X implements I` list
24550
+ 'abstract', // `abstract class X`
24324
24551
  ];
24325
24552
 
24553
+ // Class members with no runtime existence: index signatures, abstract members,
24554
+ // and method/constructor overload signatures (a body-less function value).
24555
+ // The native parser has TSAbstract* node types and TSEmptyBodyFunctionExpression
24556
+ // values; the JavaScript parser flags ordinary members `abstract` and gives
24557
+ // overloads a TSDeclareMethod value.
24558
+ function isTypeOnlyClassElement(node) {
24559
+ if (node == null) return false;
24560
+ switch (node.type) {
24561
+ case 'TSIndexSignature':
24562
+ case 'TSAbstractMethodDefinition':
24563
+ case 'TSAbstractPropertyDefinition':
24564
+ case 'TSAbstractAccessorProperty':
24565
+ return true;
24566
+ case 'MethodDefinition':
24567
+ return (
24568
+ node.abstract === true ||
24569
+ node.value?.type === 'TSEmptyBodyFunctionExpression' ||
24570
+ node.value?.type === 'TSDeclareMethod'
24571
+ );
24572
+ case 'PropertyDefinition':
24573
+ case 'AccessorProperty':
24574
+ return node.abstract === true;
24575
+ }
24576
+ return false;
24577
+ }
24578
+
24326
24579
  // Copy-on-write: stripped shapes are shallow copies; nodes with no TS-only
24327
24580
  // surface are returned by reference, so printing an already-plain subtree
24328
24581
  // allocates nothing and the (possibly parser-owned, frozen-under-tests) input
24329
- // is never written to.
24330
- function stripTsOnlyWrappers(node) {
24582
+ // is never written to. TypeScript declarations with runtime semantics (enums,
24583
+ // value namespaces, import aliases, parameter properties) are lowered here too,
24584
+ // so every emitted module is plain JavaScript (see typescript-lowering.js).
24585
+ // `env` is `{ filename, enums }`: the filename for diagnostics and the enum
24586
+ // values visible from enclosing statement lists (for constant folding);
24587
+ // `topLevel` marks the Program body.
24588
+ function stripTsOnlyWrappers(node, env, topLevel = false) {
24331
24589
  if (node === null || typeof node !== 'object') return node;
24332
24590
  if (Array.isArray(node)) {
24333
24591
  let out = null;
24334
24592
  for (let i = 0; i < node.length; i++) {
24335
24593
  const item = node[i];
24594
+ // A statement list holding an enum/namespace/import alias is lowered as
24595
+ // a whole: declaration merging and namespace exports span its items.
24596
+ if (needsTypeScriptStatementLowering(item)) {
24597
+ return lowerTypeScriptStatements(node, {
24598
+ topLevel,
24599
+ filename: env.filename,
24600
+ enums: env.enums,
24601
+ isTypeOnlyStatement,
24602
+ strip: (statement, enums) =>
24603
+ stripTsOnlyWrappers(statement, { filename: env.filename, enums }),
24604
+ });
24605
+ }
24336
24606
  // Type-only STATEMENTS nested below module scope (a `type X = …` or
24337
24607
  // `interface I {}` inside a function body) never hit the top-level
24338
24608
  // `ast.body` filter, and stripping their annotations below would
@@ -24341,11 +24611,11 @@ function stripTsOnlyWrappers(node) {
24341
24611
  // statement-only, so pruning from any AST array is safe (sparse
24342
24612
  // ArrayExpression holes are `null` and isTypeOnlyStatement keeps
24343
24613
  // them). Checked BEFORE the per-node strip so `declare` is intact.
24344
- if (isTypeOnlyStatement(item)) {
24614
+ if (isTypeOnlyStatement(item) || isTypeOnlyClassElement(item)) {
24345
24615
  if (out === null) out = node.slice(0, i);
24346
24616
  continue;
24347
24617
  }
24348
- const mapped = stripTsOnlyWrappers(item);
24618
+ const mapped = stripTsOnlyWrappers(item, env);
24349
24619
  if (out === null && mapped !== item) out = node.slice(0, i);
24350
24620
  if (out !== null) out.push(mapped);
24351
24621
  }
@@ -24358,7 +24628,10 @@ function stripTsOnlyWrappers(node) {
24358
24628
  node.type === 'TSSatisfiesExpression' ||
24359
24629
  node.type === 'TSInstantiationExpression'
24360
24630
  ) {
24361
- return stripTsOnlyWrappers(node.expression);
24631
+ return stripTsOnlyWrappers(node.expression, env);
24632
+ }
24633
+ if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
24634
+ node = lowerParameterProperties(node, env.filename);
24362
24635
  }
24363
24636
  let out = null;
24364
24637
  // A TS `this` parameter (`function f(this: Foo, …)`) is type-only and is
@@ -24405,7 +24678,7 @@ function stripTsOnlyWrappers(node) {
24405
24678
  continue;
24406
24679
  const child = (out ?? node)[key];
24407
24680
  if (child === null || typeof child !== 'object') continue;
24408
- const mapped = stripTsOnlyWrappers(child);
24681
+ const mapped = stripTsOnlyWrappers(child, env, key === 'body' && node.type === 'Program');
24409
24682
  if (mapped !== child) {
24410
24683
  if (out === null) out = { ...node };
24411
24684
  out[key] = mapped;
@@ -25237,6 +25510,8 @@ function planJsx(
25237
25510
  if (signalHelper !== null) {
25238
25511
  ctx.runtimeNeeded.add(signalHelper);
25239
25512
  registerAttrLoweringOrigin(ctx, b.nameOrigin, signalHelper, b.name);
25513
+ if (signalHelper === 'bindSignalAttribute' && b.kind !== 'class')
25514
+ ctx.runtimeNeeded.add(attrBindingHelper(b));
25240
25515
  }
25241
25516
  if (!b.signalDirect && (b.kind === 'text' || b.kind === 'textOnlyChild')) {
25242
25517
  ctx.runtimeNeeded.add('setText');
@@ -25718,10 +25993,10 @@ function planJsx(
25718
25993
  ? 'fastMapSlot'
25719
25994
  : 'mapSlot'
25720
25995
  : fc.keyedSelectionIndex >= 0
25721
- ? fc.hostMountSafe
25996
+ ? fc.hostMountSafe && fc.signalSite === null
25722
25997
  ? 'fastKeyedForBlock'
25723
25998
  : 'keyedForBlock'
25724
- : fc.hostMountSafe
25999
+ : fc.hostMountSafe && fc.signalSite === null
25725
26000
  ? 'fastForBlock'
25726
26001
  : 'forBlock';
25727
26002
  ctx.runtimeNeeded.add(forHelper);
@@ -25832,6 +26107,12 @@ function planJsx(
25832
26107
  while (tailArgs.length < 7) tailArgs.push(undefinedNode());
25833
26108
  tailArgs.push(helperRefNode(fc.selectionHelper));
25834
26109
  }
26110
+ if (fc.signalSite !== null) {
26111
+ while (tailArgs.length < (fc.keyedSelectionIndex >= 0 ? 8 : 7)) {
26112
+ tailArgs.push(undefinedNode());
26113
+ }
26114
+ tailArgs.push(b.literal(fc.signalSite));
26115
+ }
25835
26116
  if (isMappedList) {
25836
26117
  const nativeName = allocCompilerName(ctx, '__mapNative');
25837
26118
  if (fc.autoMemoDeps !== null) {
@@ -26951,10 +27232,19 @@ function directSignalBindingArgs(bind, host, previous, value = bind.expr, previo
26951
27232
  attrLoweringToken(b.literal(bind.name), bind),
26952
27233
  bind.expr,
26953
27234
  b.literal(bind.signalSite, JSON.stringify(bind.signalSite)),
26954
- b.literal(bind.kind, JSON.stringify(bind.kind)),
27235
+ directSignalAttributeWriter(bind),
26955
27236
  ];
26956
27237
  }
26957
27238
 
27239
+ // Class composition selects its writer from the element's namespace at runtime.
27240
+ // Every other attribute passes the writer its ordinary binding would call, so a
27241
+ // statically admitted name never retains the generic setAttribute route.
27242
+ function directSignalAttributeWriter(bind) {
27243
+ return bind.kind === 'class'
27244
+ ? b.literal('class', JSON.stringify('class'))
27245
+ : b.id(`_$${attrBindingHelper(bind)}`);
27246
+ }
27247
+
26958
27248
  function emitBindingMount(bind, elVar, bag) {
26959
27249
  const knownExpression = resolveKnownAttributeSource(bind.expr, (source) =>
26960
27250
  b.id(bag.local(`_prev$${source.binding.id}`)),
@@ -27073,7 +27363,16 @@ function emitBindingMount(bind, elVar, bag) {
27073
27363
  return st(
27074
27364
  b.block([
27075
27365
  ...mountHost(),
27076
- b.stmt(b.call(bind.helper, b.id('__s'), b.literal(bind.slotIndex), el(), bind.expr)),
27366
+ b.stmt(
27367
+ b.call(
27368
+ bind.helper,
27369
+ b.id('__s'),
27370
+ b.literal(bind.slotIndex),
27371
+ el(),
27372
+ bind.expr,
27373
+ ...(bind.literal ? [b.literal(1)] : []),
27374
+ ),
27375
+ ),
27077
27376
  ]),
27078
27377
  );
27079
27378
  }
@@ -27571,7 +27870,16 @@ function emitBindingUpdate(bind, bag, inlineBindingGuards = false) {
27571
27870
  }
27572
27871
  case 'nativeStyle': {
27573
27872
  return st(
27574
- b.stmt(b.call(bind.helper, b.id('__s'), b.literal(bind.slotIndex), F('_el'), bind.expr)),
27873
+ b.stmt(
27874
+ b.call(
27875
+ bind.helper,
27876
+ b.id('__s'),
27877
+ b.literal(bind.slotIndex),
27878
+ F('_el'),
27879
+ bind.expr,
27880
+ ...(bind.literal ? [b.literal(1)] : []),
27881
+ ),
27882
+ ),
27575
27883
  );
27576
27884
  }
27577
27885
  case 'nativeChangeRuntime': {
@@ -29033,6 +29341,7 @@ function emitElementHtml(
29033
29341
  bindings.push({
29034
29342
  id: bindings.length,
29035
29343
  kind: 'nativeStyle',
29344
+ literal: isPlainStyleObjectLiteral(inner),
29036
29345
  expr: tsrxExprNode(inner, ctx, componentName, inlinedSubs),
29037
29346
  path,
29038
29347
  ns: hostNs,
@@ -30831,8 +31140,8 @@ function requireCompiledHydrateAlias(ctx) {
30831
31140
  return alias;
30832
31141
  }
30833
31142
 
30834
- function hydrateDiagnosticChildrenCaptures(node, children, ctx) {
30835
- if (!ctx.dev || ctx.mode === 'server') return undefined;
31143
+ function hydrateChildrenCaptures(node, children, ctx) {
31144
+ if (ctx.mode === 'server') return undefined;
30836
31145
  const tag = node.openingElement?.name ?? node.id;
30837
31146
  if (tag?.type !== 'JSXIdentifier' || ctx.octaneImportLocals?.get(tag.name) !== 'Hydrate')
30838
31147
  return undefined;
@@ -30858,7 +31167,7 @@ function hydrateDiagnosticChildrenCaptures(node, children, ctx) {
30858
31167
  for (const name of roots) covered.add(name);
30859
31168
  }
30860
31169
  for (const name of free) if (!covered.has(name)) captures.push(b.id(name));
30861
- // Property reads stay deferred until activation; collecting diagnostic data
31170
+ // Property reads stay deferred until activation; comparing captures
30862
31171
  // must not evaluate a dormant child's getters or call its render function.
30863
31172
  return inheritOriginLoc(b.arrow([], b.array(captures)), node);
30864
31173
  }
@@ -31052,8 +31361,8 @@ function makeCompCall(
31052
31361
  ctx.runtimeNeeded.add('markChildrenBlock');
31053
31362
  hasChildrenProp = true;
31054
31363
  const childrenArgs = [b.id(childrenHelperName)];
31055
- const diagnosticCaptures = hydrateDiagnosticChildrenCaptures(node, children, ctx);
31056
- if ((ctx.autoMemo && ctx.mode !== 'server') || diagnosticCaptures !== undefined) {
31364
+ const hydrateCaptures = hydrateChildrenCaptures(node, children, ctx);
31365
+ if ((ctx.autoMemo && ctx.mode !== 'server') || hydrateCaptures !== undefined) {
31057
31366
  // These functions close over parent locals and are recreated on every
31058
31367
  // render. A module-owned token distinguishes a real Provider body
31059
31368
  // handoff from fresh captures without invalidating ordinary cache hits.
@@ -31061,8 +31370,7 @@ function makeCompCall(
31061
31370
  ctx.hoistedHelpers.push(inheritOriginLoc(b.const(bodyIdentity, b.object([])), node));
31062
31371
  childrenArgs.push(b.id(bodyIdentity));
31063
31372
  }
31064
- if (diagnosticCaptures !== undefined)
31065
- childrenArgs.push(diagnosticCaptures ?? b.literal(null));
31373
+ if (hydrateCaptures !== undefined) childrenArgs.push(hydrateCaptures ?? b.literal(null));
31066
31374
  let childrenValue = b.call('_$markChildrenBlock', ...childrenArgs);
31067
31375
  if (ctx.presentationHydration?.structural && node._octaneBindingSite)
31068
31376
  childrenValue = b.call(
@@ -31835,10 +32143,21 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
31835
32143
  if (ctx.currentComponentLocals) {
31836
32144
  const bodyScope = new Set([itemName]);
31837
32145
  if (node.index) bodyScope.add(node.index.name);
31838
- const bodyAst = b.block(subStmts);
32146
+ // A destructured header runs in the item helper's synthesized prologue, so
32147
+ // its defaults and computed keys are row reads too, and its fields bind
32148
+ // there rather than reading as free.
32149
+ const analyzedStmts =
32150
+ destructureInjection.length > 0 ? [...destructureInjection, ...subStmts] : subStmts;
32151
+ const bodyAst = b.block(analyzedStmts);
31839
32152
  const free = collectFreeIdentifiers(bodyAst, bodyScope);
31840
32153
  let hasParentClosure = false;
31841
32154
  let hasHook = false;
32155
+ // PURE and DEP-PURE skip a survivor on item identity (plus the deps
32156
+ // tuple). A module `let` or a mutable global (`location.pathname`) can
32157
+ // change between parent renders with neither moving, so a row reading one
32158
+ // must re-render, exactly as the item-memo and whole-list proofs fail closed.
32159
+ let hasAmbientRead = false;
32160
+ let ambientCandidates = null;
31842
32161
  const seenDeps = new Set();
31843
32162
  for (const name of free) {
31844
32163
  if (HOOK_NAMES.has(name) || name === 'use' || name === 'useContext') {
@@ -31850,8 +32169,20 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
31850
32169
  seenDeps.add(name);
31851
32170
  depNames.push(name);
31852
32171
  }
32172
+ } else if (!ctx.importedNames.has(name) && isUnwitnessedAmbientRead(name, ctx)) {
32173
+ (ambientCandidates ??= []).push(name);
31853
32174
  }
31854
32175
  }
32176
+ if (ambientCandidates !== null) {
32177
+ // An inline event handler runs after commit, so a read confined to one
32178
+ // (`onClick={() => window.open(item.url)}`) never reaches row output.
32179
+ const renderFree = collectFreeIdentifiers(
32180
+ bodyAst,
32181
+ bodyScope,
32182
+ collectEventHandlerClosures(bodyAst),
32183
+ );
32184
+ hasAmbientRead = ambientCandidates.some((name) => renderFree.has(name));
32185
+ }
31855
32186
  const hasNestedComp = containsComponentCallOrControlFlow(subStmts);
31856
32187
  // Compatibility mode keeps method calls live: an unchanged receiver can
31857
32188
  // hide mutable state (`header.column.getIsSorted()` flips while `header`
@@ -31859,7 +32190,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
31859
32190
  // and admit every user-authored call shape. Both modes still witness every
31860
32191
  // capture, including callbacks that need the latest parent state. Actual
31861
32192
  // setup hooks remain outside this item-region proof.
31862
- const hasRenderCall = containsRenderCall(subStmts, ctx);
32193
+ const hasRenderCall = containsRenderCall(analyzedStmts, ctx);
31863
32194
  itemMemo =
31864
32195
  ctx.autoMemo === true &&
31865
32196
  hasNestedComp &&
@@ -32008,7 +32339,11 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32008
32339
  !containsAutoMemoUnsafeStructure(subStmts, ctx) &&
32009
32340
  !containsImportedMemberRead(bodyAst, ctx.importedNames);
32010
32341
  const hostPure =
32011
- !hasParentClosure && !hasHook && !hasRenderCall && (!hasNestedComp || hostConditionalPure);
32342
+ !hasParentClosure &&
32343
+ !hasHook &&
32344
+ !hasRenderCall &&
32345
+ !hasAmbientRead &&
32346
+ (!hasNestedComp || hostConditionalPure);
32012
32347
  const structuredHostDepEligible =
32013
32348
  ctx.autoMemo === true &&
32014
32349
  hasNestedComp &&
@@ -32033,6 +32368,7 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32033
32368
  const hostDepEligible =
32034
32369
  !hostPure &&
32035
32370
  !hasHook &&
32371
+ !hasAmbientRead &&
32036
32372
  hasParentClosure &&
32037
32373
  (!hasNestedComp || structuredHostDepEligible) &&
32038
32374
  !hasRenderCall;
@@ -32210,6 +32546,10 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32210
32546
  );
32211
32547
 
32212
32548
  const mapCall = node.nativeArrayMap || null;
32549
+ const signalSite =
32550
+ mapCall === null && (ctx.signalBindingsUsed || ctx.nativeReads) && containsRenderCall(subStmts)
32551
+ ? ssrControlKey('for', node)
32552
+ : null;
32213
32553
  if (keyCapturesParent) autoMemoDeps = null;
32214
32554
  return {
32215
32555
  id: ctx.nextHelperId++,
@@ -32240,7 +32580,8 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32240
32580
  keyHelper,
32241
32581
  inlineKey: inlineKey ? keyFn : null,
32242
32582
  bodyHelper: itemHelperName,
32243
- selectionHelper: keyedSelection?.helper ?? null,
32583
+ signalSite,
32584
+ selectionHelper: signalSite === null ? (keyedSelection?.helper ?? null) : null,
32244
32585
  hasPerItemEventClosure,
32245
32586
  pure,
32246
32587
  singleRoot,
@@ -32253,13 +32594,15 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32253
32594
  // Component-local entries remain the tuple prefix the helpers destructure;
32254
32595
  // any appended import witnesses are comparison-only.
32255
32596
  depEligible,
32256
- requiresScope,
32597
+ // Inline signals need their row owner on every survivor update, including
32598
+ // dependency changes that could otherwise use the lite selection path.
32599
+ requiresScope: requiresScope || signalSite !== null,
32257
32600
  itemMemoWitnesses,
32258
32601
  itemMemoFlags,
32259
32602
  autoMemoDeps,
32260
32603
  autoMemoWitnesses,
32261
32604
  autoMemoContextAware,
32262
- keyedSelectionIndex,
32605
+ keyedSelectionIndex: signalSite === null ? keyedSelectionIndex : -1,
32263
32606
  depNames: runtimeDepNames,
32264
32607
  // True only when the header binds NO `index <name>` — the body then can't
32265
32608
  // observe an item's position, so a pure reorder (same item ref, position
@@ -32277,6 +32620,39 @@ function makeForCall(node, ctx, inlinedSubs, parentNs = 'html', cssHash = null)
32277
32620
  };
32278
32621
  }
32279
32622
 
32623
+ // Inline `onXxx={() => …}` / `onXxx={function () {…}}` handler closures under
32624
+ // `root`, as a node set for collectFreeIdentifiers' ignoreNodes.
32625
+ function collectEventHandlerClosures(root) {
32626
+ const closures = new Set();
32627
+ const stack = [root];
32628
+ while (stack.length > 0) {
32629
+ const node = stack.pop();
32630
+ if (!node || typeof node !== 'object') continue;
32631
+ if (Array.isArray(node)) {
32632
+ stack.push(...node);
32633
+ continue;
32634
+ }
32635
+ if (node.type === 'JSXAttribute' || node.type === 'Attribute') {
32636
+ const name = node.name?.name || node.name;
32637
+ const expression =
32638
+ node.value?.type === 'JSXExpressionContainer' ? node.value.expression : node.value;
32639
+ if (
32640
+ typeof name === 'string' &&
32641
+ /^on[A-Z]/.test(name) &&
32642
+ (expression?.type === 'ArrowFunctionExpression' ||
32643
+ expression?.type === 'FunctionExpression')
32644
+ ) {
32645
+ closures.add(expression);
32646
+ continue;
32647
+ }
32648
+ }
32649
+ for (const key in node) {
32650
+ if (!AST_WALK_SKIP_KEYS.has(key)) stack.push(node[key]);
32651
+ }
32652
+ }
32653
+ return closures;
32654
+ }
32655
+
32280
32656
  function mapCallbackHasEventClosure(callback) {
32281
32657
  const stack = [callback.body];
32282
32658
  while (stack.length > 0) {
@@ -33002,7 +33378,10 @@ const esrapCommentOptions = {
33002
33378
  * literal raws are re-derived centrally before the print.
33003
33379
  */
33004
33380
  function printNodeWithMap(node, ctx) {
33005
- const printable = stripTsOnlyWrappers(escapeMultilineStringLiterals(node));
33381
+ const printable = stripTsOnlyWrappers(escapeMultilineStringLiterals(node), {
33382
+ filename: ctx.mapSourceName,
33383
+ enums: null,
33384
+ });
33006
33385
  if (assertPrintedLocs()) assertNodeLocs(printable);
33007
33386
  const { code, map } = esrapPrint(printable, withDeferredImports(esrapTsx(esrapCommentOptions)), {
33008
33387
  sourceMapSource: ctx.mapSourceName,
@@ -33031,6 +33410,50 @@ function escapeMultilineStringLiterals(node) {
33031
33410
  });
33032
33411
  }
33033
33412
 
33413
+ // Props for a proven void root's `<C ... />` target, or null when the element
33414
+ // needs the general descriptor path: a `key` (or a spread that may carry one)
33415
+ // changes root identity, children need descriptor lowering, and a value that
33416
+ // can run user code must keep its deferred render-scope evaluation. The literal
33417
+ // is fresh, so it already has the descriptor's own-props copy shape.
33418
+ function voidRootElementProps(element) {
33419
+ if (element.children?.length > 0) return null;
33420
+ const properties = [];
33421
+ for (const attribute of element.openingElement.attributes) {
33422
+ if (
33423
+ attribute.type !== 'JSXAttribute' ||
33424
+ attribute.name?.type !== 'JSXIdentifier' ||
33425
+ attribute.name.name === 'key' ||
33426
+ // In a literal, `__proto__` sets the prototype, which the element path
33427
+ // then drops by copying own keys only. Keep that path's semantics.
33428
+ attribute.name.name === '__proto__'
33429
+ )
33430
+ return null;
33431
+ const value = attribute.value;
33432
+ let expression;
33433
+ if (value == null) expression = inheritOriginLoc(b.literal(true), attribute);
33434
+ else if (
33435
+ (value.type === 'Literal' || value.type === 'StringLiteral') &&
33436
+ typeof value.value === 'string'
33437
+ )
33438
+ expression = inheritOriginLoc(b.literal(value.value), value);
33439
+ else if (
33440
+ value.type === 'JSXExpressionContainer' &&
33441
+ (isStaticJsxValueExpression(value.expression) ||
33442
+ unwrapTsExpr(value.expression)?.type === 'Identifier')
33443
+ )
33444
+ expression = value.expression;
33445
+ else return null;
33446
+ const name = attribute.name.name;
33447
+ properties.push(
33448
+ inheritOriginLoc(
33449
+ b.prop('init', /^[A-Za-z_$][\w$]*$/.test(name) ? b.id(name) : b.literal(name), expression),
33450
+ attribute,
33451
+ ),
33452
+ );
33453
+ }
33454
+ return inheritOriginLoc(b.object(properties), element);
33455
+ }
33456
+
33034
33457
  // Identity-preserving copy-on-write map: `mutate` returns a replacement node
33035
33458
  // (or null to keep descending). Untouched subtrees are returned BY REFERENCE —
33036
33459
  // only spines above a replacement are rebuilt (as shallow copies), so mapping