octane 0.1.38 → 0.1.40

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.
@@ -11553,7 +11553,8 @@ function childSlot(parentScope, slotKey, domParent, value, anchor, ownEnd, ownsH
11553
11553
  cachedDeps: null,
11554
11554
  emptyBlock: null,
11555
11555
  env: void 0,
11556
- adopt: null
11556
+ adopt: null,
11557
+ plainDeopt: false
11557
11558
  };
11558
11559
  if (upgradeArmed) {
11559
11560
  state.forSlot.adopt = buildDeoptAdoptQueue(upgradeChildren, state.start, state.end);
@@ -11624,8 +11625,10 @@ function childSlot(parentScope, slotKey, domParent, value, anchor, ownEnd, ownsH
11624
11625
  descriptorBody(item, scope);
11625
11626
  };
11626
11627
  }
11628
+ const plainDeopt = compiledMapBody === void 0 && mappedFallback !== true;
11629
+ state.forSlot.plainDeopt = plainDeopt;
11627
11630
  const fastFlags = compiledMapFlags || 0;
11628
- const ssrMarkerless = compiledMapBody === void 0 ? markerlessMappedFallback || body === deoptItemBody : (fastFlags & 16) !== 0;
11631
+ const ssrMarkerless = compiledMapBody === void 0 ? markerlessMappedFallback || plainDeopt : (fastFlags & 16) !== 0;
11629
11632
  let pure = (fastFlags & 1) !== 0;
11630
11633
  let lite = false;
11631
11634
  if (compiledMapBody !== void 0) {
@@ -14722,7 +14725,11 @@ function forBlock(parentScope, slotKey, domParent, items, getKey, itemBody, flag
14722
14725
  cachedDeps: null,
14723
14726
  emptyBlock: null,
14724
14727
  env: void 0,
14725
- adopt: null
14728
+ adopt: null,
14729
+ // Compiled `@for` slots never read this — they never pass the de-opt
14730
+ // sentinel — but both ForSlot literals declare it so every slot shares
14731
+ // one hidden class and the stamp in childSlot transitions nothing.
14732
+ plainDeopt: false
14726
14733
  };
14727
14734
  parentScope.slots[slotKey] = state;
14728
14735
  registerSlot(parentScope, state);
@@ -15532,8 +15539,8 @@ function batchClearItems(state, oldItems) {
15532
15539
  function mountItem(parentBlock, parentNode, anchor, item, index, body, forSlot, singleRoot, ssrMarkerless, adoptNode = null) {
15533
15540
  const hydration = activeHydration();
15534
15541
  if (hydration !== null) {
15535
- if (ssrMarkerless && !hydration.isOpen(hydration.node) && (singleRoot !== 2 || body !== deoptItemBody || isHostDescriptor(item) && !descNeedsBlocks(item))) {
15536
- if (hydration.node !== null && hydration.node !== forSlot.end && (singleRoot !== 2 || body !== deoptItemBody || hydration.node.nodeType === 1 && hydration.node.parentNode === parentNode)) {
15542
+ if (ssrMarkerless && !hydration.isOpen(hydration.node) && (singleRoot !== 2 || forSlot.plainDeopt !== true || isHostDescriptor(item) && !descNeedsBlocks(item))) {
15543
+ if (hydration.node !== null && hydration.node !== forSlot.end && (singleRoot !== 2 || forSlot.plainDeopt !== true || hydration.node.nodeType === 1 && hydration.node.parentNode === parentNode)) {
15537
15544
  const root = hydration.node;
15538
15545
  const block3 = createBlock(
15539
15546
  "control-flow",
@@ -15547,7 +15554,7 @@ function mountItem(parentBlock, parentNode, anchor, item, index, body, forSlot,
15547
15554
  );
15548
15555
  block3.forSlot = forSlot;
15549
15556
  block3.itemIndex = index;
15550
- if (singleRoot === 2 && body === deoptItemBody) block3.deoptNode = root;
15557
+ if (singleRoot === 2 && forSlot.plainDeopt === true) block3.deoptNode = root;
15551
15558
  renderBlock(block3);
15552
15559
  hydration.node = block3.endMarker?.nextSibling ?? root.nextSibling;
15553
15560
  return block3;
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  version: () => version
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const version = "0.1.38";
24
+ const version = "0.1.40";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  version
@@ -272,6 +272,7 @@ const UNIVERSAL_RUNTIME_IMPORTS = new Set([
272
272
  // ordinary renderers while allowing native first-screen programs to erase
273
273
  // background-owned callbacks from their render-only specialization.
274
274
  const MAIN_THREAD_RENDER_ONLY_CAPABILITY = 'main-thread-render-only';
275
+ const COMPONENT_SCOPE_FOR_CAPABILITY = 'component-scope-for';
275
276
  const THREAD_FUNCTION_CAPABILITY = 'thread-functions';
276
277
  const THREAD_DIRECTIVES = new Map([
277
278
  ['main thread', 'main-thread'],
@@ -2453,7 +2454,8 @@ function templateProgramForHost(node, state) {
2453
2454
  function templateProgramForComponent(node, state) {
2454
2455
  if (
2455
2456
  node.empty != null ||
2456
- !rendererHasCapability(state, 'template-program-mount') ||
2457
+ (!rendererHasCapability(state, 'template-program-mount') &&
2458
+ !rendererHasCapability(state, COMPONENT_SCOPE_FOR_CAPABILITY)) ||
2457
2459
  !isOwnerFreeForExpression(node.right) ||
2458
2460
  !isOwnerFreeForExpression(node.key)
2459
2461
  ) {
@@ -3,8 +3,9 @@
3
3
  * runtime.
4
4
  *
5
5
  * Architecture:
6
- * 1. Parse TSRX via @tsrx/core's parseModule and run its target-neutral
7
- * semantic analysis on the authored module.
6
+ * 1. Parse TSRX through the environment-selected @tsrx/core-compatible
7
+ * parser (native oxc-tsrx in Node, pure JavaScript elsewhere), then run
8
+ * @tsrx/core's target-neutral semantic analysis on the authored module.
8
9
  * 2. For each top-level node:
9
10
  * - Component (`@{ … }` body or a return-JSX function) → compile to a
10
11
  * function taking the props-first ABI `(…userParams, __s, __extra)`.
@@ -30,7 +31,6 @@
30
31
  import {
31
32
  analyzeCss,
32
33
  analyzeTsrx,
33
- parseModule,
34
34
  prepareStylesheetForRender,
35
35
  renderStylesheets,
36
36
  builders as b,
@@ -38,6 +38,7 @@ import {
38
38
  clone_ast_node as cloneAstNode,
39
39
  strongHash,
40
40
  } from '@tsrx/core';
41
+ import { parseModule } from '#octane/compiler-parser';
41
42
  import { print as esrapPrint } from 'esrap';
42
43
  import esrapTsx from 'esrap/languages/tsx';
43
44
  import { buildFatSegments } from './fat-segments.js';
@@ -8640,6 +8641,29 @@ function compileInternal(source, filename, options, analyzedAst, mode, bundlerMe
8640
8641
  return true;
8641
8642
  }
8642
8643
  }
8644
+ // METHOD-style custom hooks — `route.useLoaderData()`, `api.useSearch()`,
8645
+ // `SomeContext.useSelector(fn)`. The free-identifier sweep above only sees
8646
+ // bare names, and the unknown-call rule above only inspects Identifier
8647
+ // callees, so without this a component whose ONLY hook is member-form
8648
+ // looks hookless: it takes the lite path, gets no block of its own, and
8649
+ // its hook state lands on the parent — an update from that hook then
8650
+ // re-renders the parent and every sibling.
8651
+ //
8652
+ // Same `use[A-Z]` convention, applied to the PROPERTY name, that the
8653
+ // slot-injection pass already uses for these calls. `useContext` is
8654
+ // included rather than excluded: it is not lite-safe either (the sweep
8655
+ // above rejects the bare name for the same reason).
8656
+ if (
8657
+ t === 'CallExpression' &&
8658
+ n.callee &&
8659
+ n.callee.type === 'MemberExpression' &&
8660
+ !n.callee.computed &&
8661
+ n.callee.property &&
8662
+ n.callee.property.type === 'Identifier' &&
8663
+ /^use[A-Z]/.test(n.callee.property.name)
8664
+ ) {
8665
+ return true;
8666
+ }
8643
8667
  for (const k in n) {
8644
8668
  if (AST_WALK_SKIP_KEYS.has(k)) continue;
8645
8669
  if (walk(n[k])) return true;
@@ -118,12 +118,14 @@ function unwrapExport(node) {
118
118
  return node;
119
119
  }
120
120
 
121
- function predeclareDirect(statements, scope, hookRuntimeModules) {
121
+ function predeclareDirect(statements, scope, hookRuntimeModules, bindingsOnly = false) {
122
122
  for (const original of statements || []) {
123
123
  if (original.type === 'ImportDeclaration') {
124
+ if (bindingsOnly && original.importKind === 'type') continue;
124
125
  const isHookRuntime = hookRuntimeModules.has(original.source?.value);
125
126
  const isOctane = original.source?.value === 'octane';
126
127
  for (const specifier of original.specifiers || []) {
128
+ if (bindingsOnly && specifier.importKind === 'type') continue;
127
129
  const imported = specifier.imported?.name;
128
130
  declareName(scope, specifier.local.name, {
129
131
  imported: true,
@@ -156,19 +158,29 @@ function predeclareDirect(statements, scope, hookRuntimeModules) {
156
158
  }
157
159
  }
158
160
 
159
- function collectHoistedVars(node, functionScope, root = true) {
161
+ function collectHoistedVars(node, functionScope, root = true, bindingsOnly = false) {
160
162
  if (!node || typeof node !== 'object') return;
161
163
  if (Array.isArray(node)) {
162
- for (const child of node) collectHoistedVars(child, functionScope, false);
164
+ for (const child of node) collectHoistedVars(child, functionScope, false, bindingsOnly);
163
165
  return;
164
166
  }
165
167
  if (!root && isFunction(node)) return;
168
+ if (
169
+ bindingsOnly &&
170
+ (isFunction(node) ||
171
+ (!root && node.type === 'StaticBlock') ||
172
+ node.type === 'ClassDeclaration' ||
173
+ node.type === 'ClassExpression' ||
174
+ node.type === 'TSModuleDeclaration')
175
+ ) {
176
+ return;
177
+ }
166
178
  if (node.type === 'VariableDeclaration' && node.kind === 'var') {
167
179
  for (const decl of node.declarations || []) declarePattern(decl.id, functionScope);
168
180
  }
169
181
  for (const key in node) {
170
182
  if (AST_META_KEYS.has(key)) continue;
171
- collectHoistedVars(node[key], functionScope, false);
183
+ collectHoistedVars(node[key], functionScope, false, bindingsOnly);
172
184
  }
173
185
  }
174
186
 
@@ -277,7 +289,7 @@ function markReassignedPattern(pattern, scope) {
277
289
  }
278
290
  }
279
291
 
280
- function buildScopes(ast, onlyImported, hookRuntimeModules) {
292
+ function buildScopes(ast, onlyImported, hookRuntimeModules, bindingsOnly = false) {
281
293
  nextBindingId = 0;
282
294
  const moduleScope = createScope(null, 'module');
283
295
  const nodeScopes = new WeakMap();
@@ -289,8 +301,35 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
289
301
  const functionRecords = new WeakMap();
290
302
  const trustedHookNames = new WeakMap();
291
303
  const callAnnotations = new Map();
292
- predeclareDirect(ast.body, moduleScope, hookRuntimeModules);
293
- collectHoistedVars(ast, moduleScope);
304
+ const declarationBindings = bindingsOnly ? new Map() : null;
305
+ const firstDefinitions = bindingsOnly ? new Map() : null;
306
+ predeclareDirect(ast.body, moduleScope, hookRuntimeModules, bindingsOnly);
307
+ collectHoistedVars(ast, moduleScope, true, bindingsOnly);
308
+
309
+ function rememberBindings(bindings, definesValue = true) {
310
+ if (declarationBindings === null || firstDefinitions === null) return;
311
+ for (const { pattern, binding } of bindings) {
312
+ declarationBindings.set(pattern, binding);
313
+ if (!definesValue) continue;
314
+ const previous = firstDefinitions.get(binding);
315
+ if (previous !== undefined && previous !== pattern) binding.reassigned = true;
316
+ else firstDefinitions.set(binding, pattern);
317
+ }
318
+ }
319
+
320
+ function rememberPattern(pattern, scope, definesValue = true) {
321
+ if (!bindingsOnly) return;
322
+ if (pattern?.type === 'TSParameterProperty') pattern = pattern.parameter;
323
+ const bindings = [];
324
+ collectPatternBindings(pattern, scope, bindings);
325
+ rememberBindings(bindings, definesValue);
326
+ }
327
+
328
+ function invalidateVisibleBindings(scope) {
329
+ for (let current = scope; current !== null; current = current.parent) {
330
+ for (const binding of current.bindings.values()) binding.reassigned = true;
331
+ }
332
+ }
294
333
 
295
334
  function walk(node, scope) {
296
335
  if (!node || typeof node !== 'object') return;
@@ -298,34 +337,103 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
298
337
  for (const child of node) walk(child, scope);
299
338
  return;
300
339
  }
301
- nodeScopes.set(node, scope);
340
+ if (!bindingsOnly) nodeScopes.set(node, scope);
341
+ if (
342
+ bindingsOnly &&
343
+ (node.type === 'TSInstantiationExpression' || node.type === 'TSExportAssignment')
344
+ ) {
345
+ walk(node.expression, scope);
346
+ return;
347
+ }
302
348
 
303
349
  if (isFunction(node)) {
304
350
  const fnScope = createScope(scope, 'function');
305
- functionScopes.set(node, fnScope);
306
- const record = {
307
- node,
308
- scope: fnScope,
309
- binding:
310
- node.type === 'FunctionDeclaration' && node.id
311
- ? resolveBinding(scope, node.id.name)
312
- : null,
313
- parameters: null,
314
- stableDefinition: node.type === 'FunctionDeclaration',
315
- };
316
- functions.push(record);
317
- functionRecords.set(node, record);
351
+ const record = bindingsOnly
352
+ ? null
353
+ : {
354
+ node,
355
+ scope: fnScope,
356
+ binding:
357
+ node.type === 'FunctionDeclaration' && node.id
358
+ ? resolveBinding(scope, node.id.name)
359
+ : null,
360
+ parameters: null,
361
+ stableDefinition: node.type === 'FunctionDeclaration',
362
+ };
363
+ if (record !== null) {
364
+ functionScopes.set(node, fnScope);
365
+ functions.push(record);
366
+ functionRecords.set(node, record);
367
+ }
368
+ if (bindingsOnly && node.type === 'FunctionDeclaration' && node.id) {
369
+ rememberPattern(node.id, scope);
370
+ }
318
371
  if (node.type === 'FunctionExpression' && node.id) {
319
372
  declareName(fnScope, node.id.name);
373
+ if (bindingsOnly) rememberPattern(node.id, fnScope);
320
374
  }
321
375
  if (node.type !== 'ArrowFunctionExpression') declareName(fnScope, 'arguments');
322
- for (const param of node.params || []) declarePattern(param, fnScope);
323
- record.parameters = (node.params || []).map((param) =>
324
- directParameterBinding(param, fnScope),
325
- );
326
- collectHoistedVars(node.body, fnScope);
327
- for (const param of node.params || []) walkPatternDefaults(param, fnScope);
328
- walk(node.body, fnScope);
376
+ for (const param of node.params || []) {
377
+ declarePattern(
378
+ bindingsOnly && param.type === 'TSParameterProperty' ? param.parameter : param,
379
+ fnScope,
380
+ );
381
+ }
382
+ if (record !== null) {
383
+ record.parameters = (node.params || []).map((param) =>
384
+ directParameterBinding(param, fnScope),
385
+ );
386
+ }
387
+ if (bindingsOnly) {
388
+ for (const param of node.params || []) rememberPattern(param, fnScope);
389
+ // Parameter defaults cannot see declarations in the function body.
390
+ // Afterwards, share the direct body with the parameters so legal var
391
+ // and function redeclarations count as competing value definitions.
392
+ for (const param of node.params || []) walkPatternDefaults(param, fnScope);
393
+ collectHoistedVars(node.body, fnScope, true, true);
394
+ if (node.body?.type === 'BlockStatement' || node.body?.type === 'JSXCodeBlock') {
395
+ predeclareDirect(node.body.body, fnScope, hookRuntimeModules, true);
396
+ for (const statement of node.body.body || []) walk(statement, fnScope);
397
+ if (node.body.type === 'JSXCodeBlock') walk(node.body.render, fnScope);
398
+ } else {
399
+ walk(node.body, fnScope);
400
+ }
401
+ } else {
402
+ collectHoistedVars(node.body, fnScope);
403
+ for (const param of node.params || []) walkPatternDefaults(param, fnScope);
404
+ walk(node.body, fnScope);
405
+ }
406
+ return;
407
+ }
408
+
409
+ if (bindingsOnly && node.type === 'ImportDeclaration') {
410
+ if (node.importKind !== 'type') {
411
+ for (const specifier of node.specifiers || []) {
412
+ if (specifier.importKind !== 'type') rememberPattern(specifier.local, scope);
413
+ }
414
+ }
415
+ return;
416
+ }
417
+
418
+ if (
419
+ bindingsOnly &&
420
+ node.declare !== true &&
421
+ ((node.type === 'TSModuleDeclaration' && node.kind !== 'global') ||
422
+ node.type === 'TSEnumDeclaration')
423
+ ) {
424
+ // The dependency walk deliberately skips TypeScript-only syntax. Runtime
425
+ // namespaces and enum initializers can still write their enclosing scope.
426
+ invalidateVisibleBindings(scope);
427
+ return;
428
+ }
429
+
430
+ if (bindingsOnly && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression')) {
431
+ if (node.type === 'ClassDeclaration' && node.id) rememberPattern(node.id, scope);
432
+ const classScope = createScope(scope, 'block');
433
+ if (node.id) declarePattern(node.id, classScope);
434
+ for (const decorator of node.decorators || []) walk(decorator, scope);
435
+ walk(node.superClass, classScope);
436
+ walk(node.body, classScope);
329
437
  return;
330
438
  }
331
439
 
@@ -334,8 +442,14 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
334
442
  node.type === 'StaticBlock' ||
335
443
  node.type === 'JSXCodeBlock'
336
444
  ) {
337
- const blockScope = createScope(scope, 'block');
338
- predeclareDirect(node.body, blockScope, hookRuntimeModules);
445
+ const blockScope = createScope(
446
+ scope,
447
+ bindingsOnly && node.type === 'StaticBlock' ? 'function' : 'block',
448
+ );
449
+ if (bindingsOnly && node.type === 'StaticBlock') {
450
+ collectHoistedVars(node, blockScope, true, true);
451
+ }
452
+ predeclareDirect(node.body, blockScope, hookRuntimeModules, bindingsOnly);
339
453
  for (const statement of node.body || []) walk(statement, blockScope);
340
454
  // TSRX's final render node lives beside the setup-statement list.
341
455
  if (node.type === 'JSXCodeBlock') walk(node.render, blockScope);
@@ -345,12 +459,17 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
345
459
  if (node.type === 'CatchClause') {
346
460
  const catchScope = createScope(scope, 'block');
347
461
  declarePattern(node.param, catchScope);
462
+ if (bindingsOnly) rememberPattern(node.param, catchScope);
463
+ if (bindingsOnly && node.resetParam) {
464
+ declarePattern(node.resetParam, catchScope);
465
+ rememberPattern(node.resetParam, catchScope);
466
+ }
348
467
  walkPatternDefaults(node.param, catchScope);
349
468
  walk(node.body, catchScope);
350
469
  return;
351
470
  }
352
471
 
353
- if (node.type === 'SwitchStatement') {
472
+ if (node.type === 'SwitchStatement' || (bindingsOnly && node.type === 'JSXSwitchExpression')) {
354
473
  // A switch body is one lexical scope shared by every unbraced case.
355
474
  // Predeclare the direct case statements together so let/const/function
356
475
  // captures resolve even when their declaration appears in a case list
@@ -360,10 +479,10 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
360
479
  for (const switchCase of node.cases || []) {
361
480
  statements.push(...(switchCase.consequent || []));
362
481
  }
363
- predeclareDirect(statements, switchScope, hookRuntimeModules);
482
+ predeclareDirect(statements, switchScope, hookRuntimeModules, bindingsOnly);
364
483
  walk(node.discriminant, scope);
365
484
  for (const switchCase of node.cases || []) {
366
- nodeScopes.set(switchCase, switchScope);
485
+ if (!bindingsOnly) nodeScopes.set(switchCase, switchScope);
367
486
  walk(switchCase.test, switchScope);
368
487
  for (const statement of switchCase.consequent || []) walk(statement, switchScope);
369
488
  }
@@ -373,32 +492,45 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
373
492
  if (
374
493
  node.type === 'ForStatement' ||
375
494
  node.type === 'ForInStatement' ||
376
- node.type === 'ForOfStatement'
495
+ node.type === 'ForOfStatement' ||
496
+ (bindingsOnly && node.type === 'JSXForExpression')
377
497
  ) {
378
498
  const loopScope = createScope(scope, 'block');
379
- const declaration = node.type === 'ForStatement' ? node.init : node.left;
499
+ const loopType = node.type === 'JSXForExpression' ? node.statementType : node.type;
500
+ const declaration = loopType === 'ForStatement' ? node.init : node.left;
380
501
  if (declaration?.type === 'VariableDeclaration' && declaration.kind !== 'var') {
381
502
  for (const decl of declaration.declarations || []) declarePattern(decl.id, loopScope);
382
503
  }
383
- if (node.type === 'ForStatement') {
504
+ if (loopType === 'ForStatement') {
384
505
  walk(node.init, loopScope);
385
506
  walk(node.test, loopScope);
386
507
  walk(node.update, loopScope);
387
508
  } else {
388
509
  walk(node.left, loopScope);
389
510
  if (node.left?.type !== 'VariableDeclaration') markReassignedPattern(node.left, loopScope);
390
- walk(node.right, loopScope);
511
+ else if (bindingsOnly && node.left.kind === 'var') {
512
+ for (const decl of node.left.declarations || [])
513
+ markReassignedPattern(decl.id, loopScope);
514
+ }
515
+ walk(node.right, node.type === 'JSXForExpression' ? scope : loopScope);
516
+ }
517
+ if (bindingsOnly && node.type === 'JSXForExpression') {
518
+ declarePattern(node.index, loopScope);
519
+ rememberPattern(node.index, loopScope);
520
+ walk(node.key, loopScope);
391
521
  }
392
522
  walk(node.body, loopScope);
523
+ if (bindingsOnly && node.type === 'JSXForExpression') walk(node.empty, scope);
393
524
  return;
394
525
  }
395
526
 
396
527
  if (node.type === 'VariableDeclaration') {
397
528
  for (const decl of node.declarations || []) {
398
- nodeScopes.set(decl, scope);
529
+ if (!bindingsOnly) nodeScopes.set(decl, scope);
399
530
  const bindings = [];
400
531
  collectPatternBindings(decl.id, scope, bindings);
401
- declarators.push({ decl, bindings, kind: node.kind });
532
+ if (bindingsOnly) rememberBindings(bindings, node.kind !== 'var' || decl.init != null);
533
+ else declarators.push({ decl, bindings, kind: node.kind });
402
534
  walkPatternDefaults(decl.id, scope);
403
535
  walk(decl.init, scope);
404
536
  }
@@ -411,7 +543,21 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
411
543
  markReassignedPattern(node.argument, scope);
412
544
  }
413
545
 
414
- if (node.type === 'CallExpression') {
546
+ if (bindingsOnly && node.type === 'CallExpression') {
547
+ let callee = unwrapValue(node.callee);
548
+ while (callee?.type === 'TSInstantiationExpression') {
549
+ callee = unwrapValue(callee.expression);
550
+ }
551
+ if (
552
+ node.optional !== true &&
553
+ callee?.type === 'Identifier' &&
554
+ callee.name === 'eval' &&
555
+ resolveBinding(scope, callee.name) === null
556
+ ) {
557
+ // Direct eval can reassign any lexically reachable writable binding.
558
+ invalidateVisibleBindings(scope);
559
+ }
560
+ } else if (node.type === 'CallExpression') {
415
561
  // Preserve lexical import identity for the later slotting pass. A module-
416
562
  // level name map is insufficient: a component can shadow either a named
417
563
  // alias or an Octane namespace inside any nested scope. The annotation is
@@ -471,6 +617,9 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
471
617
  function walkPatternDefaults(pattern, scope) {
472
618
  if (!pattern) return;
473
619
  switch (pattern.type) {
620
+ case 'TSParameterProperty':
621
+ if (bindingsOnly) walkPatternDefaults(pattern.parameter, scope);
622
+ return;
474
623
  case 'AssignmentPattern':
475
624
  walkPatternDefaults(pattern.left, scope);
476
625
  walk(pattern.right, scope);
@@ -513,7 +662,9 @@ function buildScopes(ast, onlyImported, hookRuntimeModules) {
513
662
  functions,
514
663
  trustedHookNames,
515
664
  callAnnotations,
665
+ declarationBindings,
516
666
  };
667
+ if (bindingsOnly) return analysis;
517
668
  // The surgical plain-TS pass slots base hooks only; without a custom-hook
518
669
  // withSlot boundary, two local wrapper calls would share their inner slots.
519
670
  // Restrict custom-call inference to the full TSRX/TSX compiler, which emits
@@ -1252,6 +1403,25 @@ function analyzeInternal(ast, options) {
1252
1403
  return { analysis, inferred };
1253
1404
  }
1254
1405
 
1406
+ /**
1407
+ * Return authored declaration identifiers whose bindings have a write or a
1408
+ * competing value definition. This is a read-only, scope-aware proof for callers
1409
+ * that recreate lexical scopes per invocation. Absence from the set does not
1410
+ * make a mutable declaration kind immutable by itself.
1411
+ *
1412
+ * @param {any} ast
1413
+ * @returns {WeakSet<import('estree').Identifier>}
1414
+ */
1415
+ export function collectReassignedBindings(ast) {
1416
+ const { declarationBindings } = buildScopes(ast, true, new Set(), true);
1417
+ /** @type {WeakSet<import('estree').Identifier>} */
1418
+ const reassigned = new WeakSet();
1419
+ for (const [node, binding] of declarationBindings ?? []) {
1420
+ if (binding.reassigned) reassigned.add(node);
1421
+ }
1422
+ return reassigned;
1423
+ }
1424
+
1255
1425
  /**
1256
1426
  * Return inferred dependency expressions for every supported hook call whose
1257
1427
  * dependency argument is omitted. Explicit arrays, `null`, and any other
@@ -0,0 +1 @@
1
+ export { parseModule } from '@tsrx/core';
@@ -0,0 +1 @@
1
+ export { parseModule } from 'oxc-tsrx/tsrx-core-compat';