unplugin-react-fine-grained-signals 0.1.0 → 0.2.0

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.
@@ -4,8 +4,8 @@ import { declare } from "@babel/helper-plugin-utils";
4
4
  import { transformSync } from "@babel/core";
5
5
  import * as t from "@babel/types";
6
6
  //#region src/internal/transform.ts
7
- const useSignalsComment = /(^|\s)@useSignals(\s|$)/;
8
- const noUseSignalsComment = /(^|\s)@noUseSignals(\s|$)/;
7
+ const useSignalTrackingComment = /(^|\s)@signalTracking(\s|$)/;
8
+ const noSignalTrackingComment = /(^|\s)@noSignalTracking(\s|$)/;
9
9
  const transformedMetadataKey = "reactFineGrainedSignalsTransformed";
10
10
  const noMemoDirective = "use no memo";
11
11
  const memoizationDirectives = /* @__PURE__ */ new Set([
@@ -46,7 +46,7 @@ function isReactSource(source, reactImportSource) {
46
46
  *
47
47
  * Every import-recognition predicate in this file -- React's `memo`/
48
48
  * `forwardRef` wrappers, a default-or-namespace React import, a named or
49
- * namespace `useSignals` import -- reduces to "does this resolved binding
49
+ * namespace `useSignalTracking` import -- reduces to "does this resolved binding
50
50
  * have the right specifier kind and source", so they all build on this one
51
51
  * binding walk instead of each repeating
52
52
  * `getBinding` -> specifier-kind -> `importKind` -> parent-declaration.
@@ -88,7 +88,7 @@ function isKnownComponentWrapper(path, reactImportSource) {
88
88
  return object.isIdentifier() && isReactDefaultOrNamespaceImport(path, object.node.name, reactImportSource);
89
89
  }
90
90
  /**
91
- * The static name of an object property's or class field's key. A computed key
91
+ * The static name of an object property's key. A computed key
92
92
  * is only readable when it is a string literal (`{ ["Home"]: ... }`), exactly as
93
93
  * `getReadPropertyName` reads `items["map"]`; anything else is decided at
94
94
  * runtime and stays unnamed, which leaves the function untransformed.
@@ -100,15 +100,39 @@ function getPropertyKeyName(node) {
100
100
  return t.isStringLiteral(key) ? key.value : void 0;
101
101
  }
102
102
  /**
103
+ * The identity an object-literal method takes from its key -- the shorthand
104
+ * twin of a keyed `ObjectProperty`:
105
+ *
106
+ * ```js
107
+ * export const ns = { Home() { return <p>{count.value}</p>; } }; // this
108
+ * export const ns = { Home: () => <p>{count.value}</p> }; // == this
109
+ * ```
110
+ *
111
+ * The two spellings put the same function at the same `ns.Home` slot, and the
112
+ * rest of the module reaches both through `<ns.Home />`, so they have to resolve
113
+ * to the same identity -- while only the property form did, the method form was
114
+ * passed over in silence. Unlike a property, the method *is* the function node,
115
+ * so there is no enclosing slot to climb to: its own key is read directly.
116
+ *
117
+ * Only a plain `kind: "method"` qualifies. A getter or setter is an accessor
118
+ * React never calls as a component, and an async or generator method can never
119
+ * carry a boundary (`decideTransform` refuses them anyway), so all four resolve
120
+ * to no identity at all rather than to a name that could only ever be rejected
121
+ * later or, under an annotation, turned into a build error.
122
+ */
123
+ function getObjectMethodIdentityName(node) {
124
+ if (node.kind !== "method" || node.async || node.generator) return void 0;
125
+ return getPropertyKeyName(node);
126
+ }
127
+ /**
103
128
  * The component/hook name a function held in a *keyed slot* takes from its key:
104
129
  *
105
130
  * ```js
106
131
  * Card.Header = () => <h1>{count.value}</h1>; // a compound component
107
132
  * export const ns = { Home: () => <p>{count.value}</p> }; // an object namespace
108
- * class Holder { Row = () => <li>{count.value}</li>; } // a class field
109
133
  * ```
110
134
  *
111
- * All three are ordinary ways to hold a component that no *binding* names, so
135
+ * Both are ordinary ways to hold a component that no *binding* names, so
112
136
  * without this they resolved to no identity at all and `auto` mode passed over
113
137
  * them in silence. The key is the name the rest of the module reaches them by
114
138
  * (`<ns.Home />`, `<Card.Header />`), which is precisely what the PascalCase /
@@ -135,8 +159,10 @@ function getPropertyKeyName(node) {
135
159
  * disqualifies every other keyed slot (`isKeyedRenderCallback`) cannot see those
136
160
  * uses at all -- there would be no way to take the boundary back once given. So
137
161
  * the ambiguous case resolves the way this file always resolves one: name
138
- * nothing, transform nothing. Class components stay a manual `useSignals()` /
139
- * `@useSignals` opt-in.
162
+ * nothing, transform nothing. Class fields (`Row = () => ...`) are excluded the
163
+ * same way. Such a renderer is left to its author: one rendered as an element
164
+ * (`<this.Row />`) can call `useSignalTracking()` itself; one called as a
165
+ * function or passed per item cannot hold a hook at all.
140
166
  */
141
167
  function getKeyedIdentityName(parent) {
142
168
  if (parent.isAssignmentExpression()) {
@@ -144,7 +170,7 @@ function getKeyedIdentityName(parent) {
144
170
  if (!t.isMemberExpression(left)) return void 0;
145
171
  return getStaticMemberPath(left)?.keys.at(-1);
146
172
  }
147
- if (parent.isObjectProperty() || parent.isClassProperty()) return getPropertyKeyName(parent.node);
173
+ if (parent.isObjectProperty()) return getPropertyKeyName(parent.node);
148
174
  }
149
175
  /**
150
176
  * A member expression read as a root binding plus the chain of static keys
@@ -157,22 +183,34 @@ function getKeyedIdentityName(parent) {
157
183
  * match a use site against the definition site: a one-level walk saw
158
184
  * `const a = { b: { Row } }` handed to `items.map(a.b.Row)` as an unrelated
159
185
  * reference and let the per-item callback keep a hook boundary of its own.
186
+ *
187
+ * An optional link reads the same key: `ns?.group.Home`, `ns.group?.Home` and
188
+ * `ns.group.Home` all name one slot, so each link may be either member form.
160
189
  */
161
190
  function getStaticMemberPath(node) {
191
+ const chain = getStaticMemberChain(node);
192
+ return chain !== void 0 && t.isIdentifier(chain.root) ? {
193
+ root: chain.root.name,
194
+ keys: chain.keys
195
+ } : void 0;
196
+ }
197
+ /** `getStaticMemberPath` without the root restriction: the innermost object node and the static keys. */
198
+ function getStaticMemberChain(node) {
162
199
  const keys = [];
163
200
  let current = node;
164
- while (t.isMemberExpression(current)) {
201
+ while (t.isMemberExpression(current) || t.isOptionalMemberExpression(current)) {
165
202
  const key = getReadPropertyName(current);
166
203
  if (key === void 0) return void 0;
167
204
  keys.unshift(key);
168
205
  current = current.object;
169
206
  }
170
- return t.isIdentifier(current) ? {
171
- root: current.name,
207
+ return {
208
+ root: current,
172
209
  keys
173
- } : void 0;
210
+ };
174
211
  }
175
212
  function getBindingIdentityName(path, reactImportSource, climbedParent = climbComponentWrappers(path, reactImportSource).parentPath) {
213
+ if (path.isObjectMethod()) return getObjectMethodIdentityName(path.node);
176
214
  const parent = climbedParent;
177
215
  if (parent !== null) {
178
216
  if (parent.isVariableDeclarator() && t.isIdentifier(parent.node.id)) return parent.node.id.name;
@@ -254,7 +292,7 @@ function rendersJsx(path) {
254
292
  * the returned component's JSX and `.value` reads credited to the factory's own
255
293
  * body. `auto` mode injected the boundary into the factory, `all` mode did so
256
294
  * with no `.value` read needed at all, and since `WithCount(Foo)` is normally
257
- * called at module scope the injected `useSignals()` ran with no React
295
+ * called at module scope the injected `useSignalTracking()` ran with no React
258
296
  * dispatcher and threw at import time -- the unrecoverable direction this file
259
297
  * steers away from everywhere else.
260
298
  */
@@ -291,7 +329,7 @@ function getSimpleParameterName(param) {
291
329
  * The inner function is the real component in both, but it has no name of its
292
330
  * own: its only identity comes from being what `withCount` returns. Left
293
331
  * unresolved it is not a component to `auto` mode and not an annotatable
294
- * function to `@useSignals`, so a signal write silently stops re-rendering it
332
+ * function to `@signalTracking`, so a signal write silently stops re-rendering it
295
333
  * -- no error, just a stale UI.
296
334
  *
297
335
  * Three things have to hold, tested cheapest-first:
@@ -326,8 +364,8 @@ function getSimpleParameterName(param) {
326
364
  * is always a real module binding. A factory that returns a factory
327
365
  * (`(a) => (b) => (props) => <p />`) deliberately leaves the innermost
328
366
  * function unresolved rather than inventing a name for a middle function that
329
- * has none -- an `@useSignals` annotation there still warns
330
- * (`warnUnnamedUseSignalsAnnotation`) instead of silently doing nothing.
367
+ * has none -- an `@signalTracking` annotation there still warns
368
+ * (`warnUnnamedSignalTrackingAnnotation`) instead of silently doing nothing.
331
369
  */
332
370
  function getIdentityFactory(path, reactImportSource, climbedParent) {
333
371
  if (climbedParent === null) return void 0;
@@ -408,24 +446,32 @@ function getDefaultExportIdentity(climbedParent, filename) {
408
446
  name
409
447
  };
410
448
  }
411
- function isVerifiedUseSignalsSource(source, importSource) {
412
- return source === importSource || source === `${importSource}/runtime`;
449
+ function isVerifiedSignalTrackingSource(source, importSource) {
450
+ return source === importSource;
451
+ }
452
+ function isNamedUseManagedSignalsImport(functionPath, name, runtimeSource) {
453
+ const resolved = resolveImportedBinding(functionPath, name);
454
+ return resolved !== void 0 && resolved.specifier.isImportSpecifier() && resolved.source === runtimeSource && t.isIdentifier(resolved.specifier.node.imported, { name: "useManagedSignals" });
455
+ }
456
+ function isNamespaceUseManagedSignalsImport(functionPath, name, runtimeSource) {
457
+ const resolved = resolveImportedBinding(functionPath, name);
458
+ return resolved !== void 0 && resolved.specifier.isImportNamespaceSpecifier() && resolved.source === runtimeSource;
413
459
  }
414
- function isNamedUseSignalsImport(functionPath, name, importSource) {
460
+ function isNamedSignalTrackingImport(functionPath, name, importSource) {
415
461
  const resolved = resolveImportedBinding(functionPath, name);
416
462
  if (resolved === void 0 || !resolved.specifier.isImportSpecifier()) return false;
417
- return t.isIdentifier(resolved.specifier.node.imported, { name: "useSignals" }) && (importSource === void 0 || isVerifiedUseSignalsSource(resolved.source, importSource));
463
+ return t.isIdentifier(resolved.specifier.node.imported, { name: "useSignalTracking" }) && (importSource === void 0 || isVerifiedSignalTrackingSource(resolved.source, importSource));
418
464
  }
419
- function isNamespaceUseSignalsImport(functionPath, name, importSource) {
465
+ function isNamespaceSignalTrackingImport(functionPath, name, importSource) {
420
466
  const resolved = resolveImportedBinding(functionPath, name);
421
- return resolved !== void 0 && resolved.specifier.isImportNamespaceSpecifier() && (importSource === void 0 || isVerifiedUseSignalsSource(resolved.source, importSource));
467
+ return resolved !== void 0 && resolved.specifier.isImportNamespaceSpecifier() && (importSource === void 0 || isVerifiedSignalTrackingSource(resolved.source, importSource));
422
468
  }
423
- function isUseSignalsCallee(functionPath, callee, importSource, allowBarrel = true) {
424
- if (callee.isIdentifier()) return isNamedUseSignalsImport(functionPath, callee.node.name, allowBarrel ? void 0 : importSource);
469
+ function isSignalTrackingCallee(functionPath, callee, importSource, allowBarrel = true) {
470
+ if (callee.isIdentifier()) return isNamedSignalTrackingImport(functionPath, callee.node.name, allowBarrel ? void 0 : importSource);
425
471
  if (!callee.isMemberExpression()) return false;
426
472
  const object = callee.get("object");
427
473
  if (!object.isIdentifier()) return false;
428
- return getReadPropertyName(callee.node) === "useSignals" && isNamespaceUseSignalsImport(functionPath, object.node.name, allowBarrel ? void 0 : importSource);
474
+ return getReadPropertyName(callee.node) === "useSignalTracking" && isNamespaceSignalTrackingImport(functionPath, object.node.name, allowBarrel ? void 0 : importSource);
429
475
  }
430
476
  const renderCallbackMethods = /* @__PURE__ */ new Set([
431
477
  "map",
@@ -527,9 +573,24 @@ function isRenderCallbackCallee(callee) {
527
573
  * reach the former from the latter.
528
574
  */
529
575
  function isRenderCallbackInvocation(call, callback) {
576
+ const argument = getRenderCallbackArgument(call);
577
+ return argument !== void 0 && argument === callback;
578
+ }
579
+ /**
580
+ * The argument slot of `call` that a known iteration method invokes once per
581
+ * item: argument 0 of `items.map` / `flatMap` / `forEach`, and argument 1 --
582
+ * the `mapFn` -- of `Array.from(arrayLike, mapFn)`, which builds an element per
583
+ * item exactly as `map` does (`Array.from({ length: 5 }, Star)`). Only a
584
+ * plain `Array` identifier counts as the callee's object; a shadowing local
585
+ * `Array` with its own `from` is not worth a scope lookup here, and matching it
586
+ * errs toward the crash-safe side (no hook in the callback).
587
+ */
588
+ function getRenderCallbackArgument(call) {
530
589
  const parts = getCallParts(call);
531
- if (parts === void 0 || !isRenderCallbackCallee(parts.callee)) return false;
532
- return parts.arguments[0] === callback;
590
+ if (parts === void 0) return void 0;
591
+ if (isRenderCallbackCallee(parts.callee)) return parts.arguments[0];
592
+ const callee = unwrapTransparent(parts.callee);
593
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.object, { name: "Array" }) && getReadPropertyName(callee) === "from") return parts.arguments[1];
533
594
  }
534
595
  /**
535
596
  * The name other statements can reach this function's own binding by. The
@@ -567,6 +628,10 @@ function getOwnBindingName(path) {
567
628
  *
568
629
  * A class field is deliberately absent -- it is reached through an instance
569
630
  * (`new Holder().Row`), which no binding walk can follow.
631
+ *
632
+ * An object *method* (`{ Row() {...} }`) is passed in as its own `parent`: it is
633
+ * the function and the slot at once, and it sits in its object literal exactly
634
+ * where the equivalent `{ Row: ... }` property would, so the same climb applies.
570
635
  */
571
636
  function getKeyedAccessPath(parent) {
572
637
  if (parent.isAssignmentExpression()) {
@@ -578,7 +643,7 @@ function getKeyedAccessPath(parent) {
578
643
  ...path
579
644
  };
580
645
  }
581
- if (!parent.isObjectProperty()) return void 0;
646
+ if (!parent.isObjectProperty() && !parent.isObjectMethod()) return void 0;
582
647
  const key = getPropertyKeyName(parent.node);
583
648
  if (key === void 0) return void 0;
584
649
  const keys = [key];
@@ -625,20 +690,293 @@ function getKeyedAccessPath(parent) {
625
690
  * genuine compound component keeps its boundary.
626
691
  */
627
692
  function isKeyedRenderCallback(parent) {
628
- const access = getKeyedAccessPath(parent);
629
- if (access === void 0) return false;
693
+ return getKeyedSafetyUses(parent).some((slot) => slot.parentPath !== null && isRenderCallbackInvocation(slot.parentPath.node, slot.node));
694
+ }
695
+ /**
696
+ * Every place the module reads the exact keyed slot `access` describes, as the
697
+ * path of the syntactic slot that read occupies (past transparent TypeScript
698
+ * wrappers, so `ns.Row!` hands back the wrapper that actually sits in an
699
+ * argument list or callee position).
700
+ *
701
+ * The walk goes through the *root* binding, then follows the same chain of keys
702
+ * back down from each reference; a reference that turns off the chain anywhere
703
+ * (`ns.Other`, `ns` passed whole -- or `ns[k]` unless `includeDynamic` is set)
704
+ * is not a use of this slot. Both
705
+ * by-key exclusions -- the render-callback one above and the plain-call one in
706
+ * `isPlainCalledComponent` -- ask their question of this list (joined with the
707
+ * `this` uses in `getKeyedSafetyUses`), so they cannot disagree about which
708
+ * reads count as reaching the slot.
709
+ */
710
+ function getKeyedSlotUses(access, includeDynamic = false) {
630
711
  const binding = access.origin.scope.getBinding(access.root);
631
- if (binding === void 0) return false;
632
- return binding.referencePaths.some((rootPath) => {
633
- let current = rootPath;
634
- for (const key of access.keys) {
635
- const member = current.parentPath;
636
- if (member === null || !member.isMemberExpression() || member.node.object !== current.node || getReadPropertyName(member.node) !== key) return false;
637
- current = member;
712
+ if (binding === void 0) return [];
713
+ const uses = [];
714
+ for (const rootPath of binding.referencePaths) followKeyedChain(rootPath, access.keys, includeDynamic, uses);
715
+ return uses;
716
+ }
717
+ /**
718
+ * Follows `keys` down from `start` -- a reference to the root binding, or a
719
+ * `this` that stands for the object holding the slot -- and records the use it
720
+ * reaches in `uses`. A chain that turns off the path records nothing.
721
+ */
722
+ function followKeyedChain(start, keys, includeDynamic, uses) {
723
+ let current = start;
724
+ for (const [index, key] of keys.entries()) {
725
+ const member = current.parentPath;
726
+ if (member === null || !matchesKeyedLink(member, current, key, includeDynamic)) {
727
+ if (index === keys.length - 1) uses.push(...getDestructuredSlotUses(current, key));
728
+ return;
729
+ }
730
+ current = member;
731
+ }
732
+ uses.push(climbTransparentWrappers(current));
733
+ }
734
+ /**
735
+ * Every place a sibling reaches the object-literal slot `holder` through
736
+ * `this`:
737
+ *
738
+ * ```jsx
739
+ * const renderers = {
740
+ * Leaf(i) { return <li>{suffix.value}</li>; },
741
+ * Tree(count) { return <ul>{Array.from({ length: count }, (_, i) => this.Leaf(i))}</ul>; },
742
+ * };
743
+ * ```
744
+ *
745
+ * `this.Leaf(i)` runs `Leaf` once per item inside whatever render called
746
+ * `Tree`, exactly as `renderers.Leaf(i)` would, but `this` is not a binding,
747
+ * so the root-reference walk in `getKeyedSlotUses` never sees it -- and a
748
+ * boundary in `Leaf` was then a variable number of hooks in the caller.
749
+ *
750
+ * `this` is only followed where its identity is syntactic: inside a function
751
+ * that is itself a non-arrow member of the object literal (a method, accessor,
752
+ * or `function` property value), and inside arrows nested in it, which inherit
753
+ * that `this`. A nested `function` or class rebinds `this` and is skipped. Each
754
+ * enclosing object literal is its own level, so `this.Leaf` in a method of the
755
+ * object holding `Leaf` reaches it, `this.group.Leaf` in a method of the outer
756
+ * object reaches it through `group`, and a same-named slot in another object
757
+ * literal is never touched. A computed `this[key]` reaches every slot at its
758
+ * depth, like `renderers[key]` does.
759
+ *
760
+ * Only the object literals around this one slot are walked, never the
761
+ * program, so the cost stays with the object that holds the slot.
762
+ */
763
+ function getThisSlotUses(holder) {
764
+ if (!holder.isObjectProperty() && !holder.isObjectMethod()) return [];
765
+ const key = getPropertyKeyName(holder.node);
766
+ if (key === void 0) return [];
767
+ const keys = [key];
768
+ const uses = [];
769
+ for (let container = holder.parentPath; container?.isObjectExpression();) {
770
+ for (const reference of getObjectThisReferences(container)) followKeyedChain(reference, keys, true, uses);
771
+ const outer = container.parentPath;
772
+ if (outer === null || !outer.isObjectProperty() || outer.node.value !== container.node) break;
773
+ const outerKey = getPropertyKeyName(outer.node);
774
+ if (outerKey === void 0) break;
775
+ keys.unshift(outerKey);
776
+ container = outer.parentPath;
777
+ }
778
+ return uses;
779
+ }
780
+ /**
781
+ * `getObjectThisReferences` results, per object literal. One function's
782
+ * decision asks for the same object's references many times over -- every
783
+ * candidacy check of every nested function lands here -- so they are collected
784
+ * once. Keyed by node, so nothing outlives the file being transformed.
785
+ *
786
+ * A rewrite moves the rewritten function's statements under new parents, and a
787
+ * cached path inside it would then describe a tree that no longer exists, so
788
+ * `invalidateThisReferences` drops every entry whose references could lie
789
+ * inside that function: the object literals within it, and -- only when it
790
+ * contains a `this` at all -- the object literals around it. Every other entry
791
+ * stays valid, which matters in a module where most object members are
792
+ * transformed one after another and only a few of them use `this`.
793
+ */
794
+ const thisReferenceCache = /* @__PURE__ */ new WeakMap();
795
+ function invalidateThisReferences(rewritten) {
796
+ let containsThis = false;
797
+ rewritten.traverse({
798
+ ObjectExpression(object) {
799
+ thisReferenceCache.delete(object.node);
800
+ },
801
+ ThisExpression() {
802
+ containsThis = true;
638
803
  }
639
- const slot = climbTransparentWrappers(current);
640
- return slot.parentPath !== null && isRenderCallbackInvocation(slot.parentPath.node, slot.node);
641
804
  });
805
+ if (!containsThis) return;
806
+ for (let parent = rewritten.parentPath; parent !== null; parent = parent.parentPath) if (parent.isObjectExpression()) thisReferenceCache.delete(parent.node);
807
+ }
808
+ /** The `this` expressions that stand for `object` itself, inside its own non-arrow members. */
809
+ function getObjectThisReferences(object) {
810
+ const cached = thisReferenceCache.get(object.node);
811
+ if (cached !== void 0) return cached;
812
+ const references = [];
813
+ thisReferenceCache.set(object.node, references);
814
+ for (const property of object.get("properties")) {
815
+ let member;
816
+ if (property.isObjectMethod()) member = property;
817
+ else if (property.isObjectProperty()) {
818
+ const value = unwrapTransparentPath(property.get("value"));
819
+ if (value.isFunctionExpression()) member = value;
820
+ }
821
+ member?.traverse({
822
+ Function(nested) {
823
+ if (!nested.isArrowFunctionExpression()) nested.skip();
824
+ },
825
+ Class(nested) {
826
+ nested.skip();
827
+ },
828
+ ThisExpression(reference) {
829
+ references.push(reference);
830
+ }
831
+ });
832
+ }
833
+ return references;
834
+ }
835
+ /**
836
+ * Every use that can run the keyed slot `holder` inline: reads through its root
837
+ * binding, computed keys included (`getKeyedSlotUses`), and reads through
838
+ * `this` from a sibling member of the same object literal (`getThisSlotUses`).
839
+ * Both safety exclusions -- per-item callback and plain call -- ask this list.
840
+ */
841
+ function getKeyedSafetyUses(holder, access = getKeyedAccessPath(holder)) {
842
+ const uses = access === void 0 ? [] : getKeyedSlotUses(access, true);
843
+ uses.push(...getThisSlotUses(holder));
844
+ return uses;
845
+ }
846
+ /**
847
+ * Does `member` read `key` off `object` -- one link of a keyed-slot path?
848
+ *
849
+ * A static link has to name the key exactly. With `includeDynamic`, a computed
850
+ * link whose key is decided at runtime (`renderers[node.type]`,
851
+ * `renderers?.[key]`) matches too, because nothing rules out that it names this
852
+ * slot. That is what keeps an object-literal component off a boundary of its
853
+ * own when the module dispatches to it dynamically:
854
+ *
855
+ * ```jsx
856
+ * const renderers = { User(node) { return <li>{suffix.value}</li>; } };
857
+ * nodes.map((node) => renderers[node.__typename](node));
858
+ * ```
859
+ *
860
+ * `User` is called once per item inside the caller's render, exactly like
861
+ * `renderers.User(node)` would be, so a boundary in it is a variable number of
862
+ * hooks in the caller -- "Rendered more hooks than during the previous render"
863
+ * as soon as the list grows. The static spelling of that call was already
864
+ * excluded; the dynamic one could not be named, so the slot kept its boundary.
865
+ *
866
+ * Only a link at the right depth counts, so `renderers[key](node)` reaches
867
+ * `renderers.User` but not `renderers.group.User`, and `registry.users[key]`
868
+ * reaches only the slots under `registry.users`.
869
+ *
870
+ * An optional link (`ns?.Home`, `renderers?.[key]`) is matched exactly like
871
+ * the plain one, static or dynamic: it reaches the very same slot, and missing
872
+ * it costs a crash -- `{show && ns?.Home()}` is as much a conditional call
873
+ * inside the caller as `{show && ns.Home()}` is.
874
+ */
875
+ function matchesKeyedLink(member, object, key, includeDynamic) {
876
+ if (!member.isMemberExpression() && !member.isOptionalMemberExpression()) return false;
877
+ if (member.node.object !== object.node) return false;
878
+ const name = getReadPropertyName(member.node);
879
+ return name === key || includeDynamic && name === void 0;
880
+ }
881
+ /**
882
+ * The references of the binding that `const { key } = object` or
883
+ * `const { key: Alias } = object` creates, when `object` is the node that
884
+ * initializes that declarator; nothing otherwise.
885
+ */
886
+ function getDestructuredSlotUses(object, key) {
887
+ const declarator = object.parentPath;
888
+ if (declarator === null || !declarator.isVariableDeclarator() || declarator.node.init !== object.node || !t.isObjectPattern(declarator.node.id)) return [];
889
+ for (const property of declarator.node.id.properties) {
890
+ if (!t.isObjectProperty(property) || !t.isIdentifier(property.value)) continue;
891
+ if (getPropertyKeyName(property) !== key) continue;
892
+ const alias = declarator.scope.getBinding(property.value.name);
893
+ return alias === void 0 ? [] : alias.referencePaths.map((reference) => climbTransparentWrappers(reference));
894
+ }
895
+ return [];
896
+ }
897
+ /** Is `slot` the callee of a plain or optional-chained call -- `Row()`, `Row?.()`, `ns.Row!()`? */
898
+ function isPlainCallee(slot) {
899
+ const call = slot.parentPath;
900
+ if (call === null) return false;
901
+ const parts = getCallParts(call.node);
902
+ return parts !== void 0 && parts.callee === slot.node;
903
+ }
904
+ /**
905
+ * Is `path` a component-named function that the module invokes as a plain
906
+ * function -- `Title()`, `Title?.()`, `Title!()`, `ns.Home()` -- rather than
907
+ * (only) mounting it through JSX?
908
+ *
909
+ * ```jsx
910
+ * function Title() { return <h1>{count.value}</h1>; }
911
+ * function Outer({ show }) { return <div>{show && Title()}</div>; }
912
+ * ```
913
+ *
914
+ * `Title` reads exactly like a component, but a plain call never mounts a
915
+ * fiber: React sees only `Outer`, and `Title`'s body simply runs inside
916
+ * `Outer`'s render. A boundary injected into it is therefore three more hooks
917
+ * *in `Outer`*, and here they run only when `show` is true -- the conditional
918
+ * hook call the Rules of Hooks forbid, which React reports as "Rendered more
919
+ * hooks than during the previous render" (or "Expected static flag was
920
+ * missing") the first time `show` flips. The render-callback exclusion
921
+ * (`isRenderCallback`) already handled the looped variant of this -- `Row`
922
+ * called once per item from inside `items.map(...)` -- and this is the same
923
+ * fact stated for every call site, not just the ones inside a loop: a function
924
+ * that is called is not a component, whatever it is named.
925
+ *
926
+ * The exclusion is decided from the module as a whole -- *any* plain call of the
927
+ * function's own binding (or of its exact keyed slot) disqualifies it -- because
928
+ * a boundary is a property of the function, not of one call site: once one
929
+ * caller runs the body inline, a boundary inside it is wrong for that caller no
930
+ * matter what the others do. The reads are not lost; `inspectFunction` folds
931
+ * them into the function that makes the call (`foldDirectlyCalledRenderCallbacks`),
932
+ * so `Outer` is the one that gets transformed and subscribes -- which is right,
933
+ * since `Outer` is the fiber that re-renders and re-runs `Title()`.
934
+ *
935
+ * Deliberately narrow, in both directions:
936
+ *
937
+ * - Only a *component* name is affected. A `useX` hook is called as a function
938
+ * by definition, and its boundary is valid precisely because React runs it
939
+ * inside a render already in progress; hooks keep their handling unchanged.
940
+ * - Only the function's *own* binding counts, as in `isRenderCallback`: a
941
+ * same-named binding in another scope cannot disqualify it, and an alias
942
+ * (`const T = Title; T()`) is not followed. Neither is `Title.call(...)` or
943
+ * `new Title()`, which are not plain calls of the binding.
944
+ */
945
+ function isPlainCalledComponent(path) {
946
+ const slot = climbTransparentWrappers(path);
947
+ const holder = path.isObjectMethod() ? path : slot.parentPath;
948
+ if (holder === null) return false;
949
+ const access = getKeyedAccessPath(holder);
950
+ const key = access?.keys.at(-1) ?? (holder.isObjectProperty() || holder.isObjectMethod() ? getPropertyKeyName(holder.node) : void 0);
951
+ if (key !== void 0) return isComponentName(key) && getKeyedSafetyUses(holder, access).some(isPlainCallee);
952
+ return getOwnBindingReferences(path, holder)?.some((reference) => isPlainCallee(climbTransparentWrappers(reference))) ?? false;
953
+ }
954
+ /**
955
+ * The reference paths of `path`'s own component-named binding, or `undefined`
956
+ * when it has none -- no own binding, a non-component name, or a same-named
957
+ * binding that belongs to some other function. `holder` is the climbed parent
958
+ * the binding is looked up from, matching `isRenderCallback`'s own lookup.
959
+ */
960
+ function getOwnBindingReferences(path, holder) {
961
+ const name = getOwnBindingName(path);
962
+ if (name === void 0 || !isComponentName(name)) return void 0;
963
+ const binding = holder.scope.getBinding(name);
964
+ if (binding === void 0) return void 0;
965
+ if (binding.path.node !== path.node && binding.path.node !== holder.node) return void 0;
966
+ return binding.referencePaths;
967
+ }
968
+ /**
969
+ * Is `path` -- already known to be plain-called -- *also* mounted through JSX
970
+ * by its own binding (`<Title />`)? That instance loses its own boundary along
971
+ * with the plain-called one, so `decideTransform` reports the mix rather than
972
+ * leaving the `<Title />` instance to go stale in silence. Babel records a JSX
973
+ * tag as an ordinary reference to the binding it names, which is what makes
974
+ * this a plain reference walk.
975
+ */
976
+ function isAlsoMountedThroughJsx(path) {
977
+ const holder = climbTransparentWrappers(path).parentPath;
978
+ if (holder === null || path.isObjectMethod()) return false;
979
+ return getOwnBindingReferences(path, holder)?.some((reference) => reference.isJSXIdentifier()) ?? false;
642
980
  }
643
981
  function isRenderCallback(path, visited = /* @__PURE__ */ new Set()) {
644
982
  if (visited.has(path.node)) return false;
@@ -646,7 +984,8 @@ function isRenderCallback(path, visited = /* @__PURE__ */ new Set()) {
646
984
  const argument = climbTransparentWrappers(path);
647
985
  const enclosing = argument.parentPath;
648
986
  if (enclosing !== null && isRenderCallbackInvocation(enclosing.node, argument.node)) return true;
649
- if (enclosing !== null && isKeyedRenderCallback(enclosing)) return true;
987
+ const keyedHolder = path.isObjectMethod() ? path : enclosing;
988
+ if (keyedHolder !== null && isKeyedRenderCallback(keyedHolder)) return true;
650
989
  const name = getOwnBindingName(path);
651
990
  if (name === void 0 || enclosing === null) return false;
652
991
  const binding = enclosing.scope.getBinding(name);
@@ -692,6 +1031,87 @@ function resolveReferencedFunction(origin, name) {
692
1031
  }
693
1032
  }
694
1033
  /**
1034
+ * The keyed-slot twin of `resolveReferencedFunction`: the function a static
1035
+ * member callee such as `ns.Home` / `deep.a.Home` / `Card.Header` reaches, when
1036
+ * this module defines it in one of the keyed shapes `getKeyedAccessPath`
1037
+ * names -- an object literal bound to the root (`const ns = { Home() {} }`,
1038
+ * `{ Home: () => ... }`, nested to any depth) or a member assignment
1039
+ * (`Card.Header = () => ...`).
1040
+ *
1041
+ * Needed because `isPlainCalledComponent` takes the boundary away from such a
1042
+ * slot the moment it is called (`ns.Home()`), and without resolving it here its
1043
+ * reads would reach nobody: the slot no longer subscribes, and the caller never
1044
+ * learns what it read. Anything this cannot resolve unambiguously -- a spread,
1045
+ * a computed runtime key, an import -- resolves to nothing, which leaves the
1046
+ * caller exactly as it would otherwise have been.
1047
+ */
1048
+ function resolveKeyedFunction(origin, callee) {
1049
+ const chain = getStaticMemberChain(callee);
1050
+ if (chain !== void 0 && t.isThisExpression(chain.root)) {
1051
+ const object = getThisObject(origin);
1052
+ return object === void 0 ? void 0 : resolveObjectLiteralSlot(object, chain.keys);
1053
+ }
1054
+ const access = getStaticMemberPath(callee);
1055
+ if (access === void 0) return void 0;
1056
+ const binding = origin.scope.getBinding(access.root);
1057
+ if (binding === void 0) return void 0;
1058
+ if (binding.path.isVariableDeclarator()) {
1059
+ const found = resolveObjectLiteralSlot(unwrapTransparentPath(binding.path.get("init")), access.keys);
1060
+ if (found !== void 0) return found;
1061
+ }
1062
+ const assigned = [];
1063
+ for (const slot of getKeyedSlotUses({
1064
+ origin,
1065
+ ...access
1066
+ })) {
1067
+ const parent = slot.parentPath;
1068
+ if (parent !== null && parent.isAssignmentExpression({ operator: "=" }) && parent.node.left === slot.node) assigned.push(parent);
1069
+ }
1070
+ const only = assigned.length === 1 ? assigned[0] : void 0;
1071
+ if (only === void 0) return void 0;
1072
+ const value = unwrapTransparentPath(only.get("right"));
1073
+ return value.isArrowFunctionExpression() || value.isFunctionExpression() ? value : void 0;
1074
+ }
1075
+ /**
1076
+ * The function at `keys` inside the object literal `object`, walking down
1077
+ * through nested literals. The *last* matching property wins, as it does at
1078
+ * runtime; anything that is not a literal or not a function resolves to nothing.
1079
+ */
1080
+ function resolveObjectLiteralSlot(object, keys) {
1081
+ let container = object;
1082
+ for (const key of keys) {
1083
+ if (container === void 0 || !container.isObjectExpression()) return void 0;
1084
+ const match = container.get("properties").filter((property) => (property.isObjectProperty() || property.isObjectMethod()) && getPropertyKeyName(property.node) === key).at(-1);
1085
+ container = match?.isObjectProperty() ? unwrapTransparentPath(match.get("value")) : match;
1086
+ }
1087
+ if (container === void 0) return void 0;
1088
+ if (container.isObjectMethod()) return container;
1089
+ if (container.isArrowFunctionExpression() || container.isFunctionExpression()) return container;
1090
+ }
1091
+ /**
1092
+ * The object literal a `this` written at `origin` stands for, when that is
1093
+ * syntactic: the nearest non-arrow function is a member of an object literal
1094
+ * (a method, accessor, or `function` property value) and no class sits in
1095
+ * between. The inverse of the walk `getObjectThisReferences` does.
1096
+ */
1097
+ function getThisObject(origin) {
1098
+ let owner = origin.getFunctionParent();
1099
+ while (owner !== null && owner.isArrowFunctionExpression()) owner = owner.getFunctionParent();
1100
+ if (owner === null) return void 0;
1101
+ const blocker = origin.findParent((parent) => parent.node === owner.node || parent.isClass());
1102
+ if (blocker === null || blocker.node !== owner.node) return void 0;
1103
+ if (owner.isObjectMethod()) {
1104
+ const object = owner.parentPath;
1105
+ return object.isObjectExpression() ? object : void 0;
1106
+ }
1107
+ if (!owner.isFunctionExpression()) return void 0;
1108
+ const property = climbTransparentWrappers(owner);
1109
+ const holder = property.parentPath;
1110
+ if (holder === null || !holder.isObjectProperty() || holder.node.value !== property.node) return void 0;
1111
+ const object = holder.parentPath;
1112
+ return object.isObjectExpression() ? object : void 0;
1113
+ }
1114
+ /**
695
1115
  * Is `path` a component defined inline as a call argument --
696
1116
  * `observer(function App() { return <p>{count.value}</p>; })`?
697
1117
  *
@@ -731,9 +1151,10 @@ function isInlineNamedComponentArgument(path, call, reactImportSource) {
731
1151
  }
732
1152
  function isAutomaticTransformCandidate(path, reactImportSource, climbedParent) {
733
1153
  if (isRenderCallback(path)) return false;
734
- if (path.isFunctionDeclaration()) return true;
1154
+ if (isPlainCalledComponent(path)) return false;
1155
+ if (path.isFunctionDeclaration() || path.isObjectMethod()) return true;
735
1156
  const parent = climbedParent === void 0 ? climbComponentWrappers(path, reactImportSource).parentPath : climbedParent;
736
- return parent !== null && (parent.isVariableDeclarator() || parent.isReturnStatement() || parent.isArrowFunctionExpression() || parent.isExportDefaultDeclaration() || parent.isObjectProperty() || parent.isClassProperty() || parent.isAssignmentExpression() && t.isMemberExpression(parent.node.left) && getStaticMemberPath(parent.node.left) !== void 0 || parent.isCallExpression() && isInlineNamedComponentArgument(path, parent, reactImportSource));
1157
+ return parent !== null && (parent.isVariableDeclarator() || parent.isReturnStatement() || parent.isArrowFunctionExpression() || parent.isExportDefaultDeclaration() || parent.isObjectProperty() || parent.isAssignmentExpression() && t.isMemberExpression(parent.node.left) && getStaticMemberPath(parent.node.left) !== void 0 || parent.isCallExpression() && isInlineNamedComponentArgument(path, parent, reactImportSource));
737
1158
  }
738
1159
  function isNestedTrackingBoundary(path, reactImportSource) {
739
1160
  const climbedParent = climbComponentWrappers(path, reactImportSource).parentPath;
@@ -818,14 +1239,27 @@ function isDeferredReadContext(path, functionPath) {
818
1239
  }
819
1240
  return false;
820
1241
  }
821
- function inspectFunction(functionPath, importSource, reactImportSource, visited = /* @__PURE__ */ new Set([functionPath.node])) {
1242
+ /**
1243
+ * Is `node` (at `path`) a `.value` read that counts as part of `functionPath`'s
1244
+ * render -- the one rule both `inspectFunction` (does this function subscribe
1245
+ * at all?) and `relocateTrackedParameters` (does this parameter have to move
1246
+ * behind the boundary?) apply, so the two can never disagree about what a
1247
+ * tracked read is.
1248
+ */
1249
+ function isTrackedValueRead(node, path, functionPath) {
1250
+ if (getReadPropertyName(node) !== "value") return false;
1251
+ if (isNonSignalValueReceiver(node.object)) return false;
1252
+ return !isDeferredReadContext(path, functionPath);
1253
+ }
1254
+ function inspectFunction(functionPath, importSource, managedRuntimeSource, reactImportSource, visited = /* @__PURE__ */ new Set([functionPath.node])) {
822
1255
  const inspection = {
823
1256
  containsJSX: false,
824
1257
  readsValue: false,
825
- hasUseSignalsCall: false
1258
+ hasSignalTrackingCall: false,
1259
+ hasUseManagedSignalsCall: false
826
1260
  };
827
1261
  const foldReferencedRenderCallbacks = (call) => {
828
- const argument = call.node.arguments[0];
1262
+ const argument = getRenderCallbackArgument(call.node);
829
1263
  if (argument === void 0) return;
830
1264
  const reference = unwrapTransparent(argument);
831
1265
  if (!t.isIdentifier(reference)) return;
@@ -834,28 +1268,33 @@ function inspectFunction(functionPath, importSource, reactImportSource, visited
834
1268
  if (target === void 0 || target.isDescendant(functionPath)) return;
835
1269
  if (visited.has(target.node)) return;
836
1270
  visited.add(target.node);
837
- const nested = inspectFunction(target, importSource, reactImportSource, visited);
1271
+ const nested = inspectFunction(target, importSource, managedRuntimeSource, reactImportSource, visited);
838
1272
  if (nested.containsJSX) inspection.containsJSX = true;
839
1273
  if (nested.readsValue) inspection.readsValue = true;
840
1274
  };
841
1275
  const foldDirectlyCalledRenderCallbacks = (call) => {
842
1276
  const callee = unwrapTransparent(call.node.callee);
843
- if (!t.isIdentifier(callee) || !isComponentName(callee.name) && !isHookName(callee.name)) return;
1277
+ const member = t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee) ? callee : void 0;
1278
+ const chain = member === void 0 ? void 0 : getStaticMemberChain(member);
1279
+ const name = t.isIdentifier(callee) ? callee.name : chain !== void 0 && (t.isIdentifier(chain.root) || t.isThisExpression(chain.root)) ? chain.keys.at(-1) : void 0;
1280
+ if (name === void 0 || !isComponentName(name) && !isHookName(name)) return;
1281
+ if (!t.isIdentifier(callee) && !isComponentName(name)) return;
844
1282
  const owner = call.getFunctionParent();
845
- if (owner === null || owner.node === functionPath.node || !isRenderCallback(owner)) return;
846
- const target = resolveReferencedFunction(call, callee.name);
1283
+ if (owner === null) return;
1284
+ if (owner.node === functionPath.node || !isRenderCallback(owner)) {
1285
+ if (!isComponentName(name)) return;
1286
+ if (isDeferredReadContext(call, functionPath)) return;
1287
+ }
1288
+ const target = t.isIdentifier(callee) ? resolveReferencedFunction(call, callee.name) : member !== void 0 ? resolveKeyedFunction(call, member) : void 0;
847
1289
  if (target === void 0 || target.isDescendant(functionPath)) return;
848
1290
  if (visited.has(target.node)) return;
849
1291
  visited.add(target.node);
850
- const nested = inspectFunction(target, importSource, reactImportSource, visited);
1292
+ const nested = inspectFunction(target, importSource, managedRuntimeSource, reactImportSource, visited);
851
1293
  if (nested.containsJSX) inspection.containsJSX = true;
852
1294
  if (nested.readsValue) inspection.readsValue = true;
853
1295
  };
854
1296
  const recordValueRead = (node, path) => {
855
- if (getReadPropertyName(node) !== "value") return;
856
- if (isNonSignalValueReceiver(node.object)) return;
857
- if (isDeferredReadContext(path, functionPath)) return;
858
- inspection.readsValue = true;
1297
+ if (isTrackedValueRead(node, path, functionPath)) inspection.readsValue = true;
859
1298
  };
860
1299
  functionPath.traverse({
861
1300
  Function(path) {
@@ -877,7 +1316,9 @@ function inspectFunction(functionPath, importSource, reactImportSource, visited
877
1316
  foldReferencedRenderCallbacks(path);
878
1317
  foldDirectlyCalledRenderCallbacks(path);
879
1318
  if (path.getFunctionParent()?.node !== functionPath.node) return;
880
- if (isUseSignalsCallee(functionPath, path.get("callee"), importSource)) inspection.hasUseSignalsCall = true;
1319
+ const callee = path.get("callee");
1320
+ if (isSignalTrackingCallee(functionPath, callee, importSource)) inspection.hasSignalTrackingCall = true;
1321
+ if (callee.isIdentifier() && isNamedUseManagedSignalsImport(functionPath, callee.node.name, managedRuntimeSource) || callee.isMemberExpression() && getReadPropertyName(callee.node) === "useManagedSignals" && callee.get("object").isIdentifier() && isNamespaceUseManagedSignalsImport(functionPath, callee.get("object").node.name, managedRuntimeSource)) inspection.hasUseManagedSignalsCall = true;
881
1322
  },
882
1323
  OptionalCallExpression(path) {
883
1324
  foldReferencedRenderCallbacks(path);
@@ -886,26 +1327,26 @@ function inspectFunction(functionPath, importSource, reactImportSource, visited
886
1327
  });
887
1328
  return inspection;
888
1329
  }
889
- function findRuntimeImports(programPath, runtimeSource) {
1330
+ function findRuntimeImports(programPath, runtimeSource, importedName) {
890
1331
  const imports = [];
891
1332
  for (const statement of programPath.get("body")) {
892
1333
  if (!statement.isImportDeclaration() || statement.node.importKind === "type" || statement.node.source.value !== runtimeSource) continue;
893
- for (const specifier of statement.get("specifiers")) if (specifier.isImportSpecifier() && specifier.node.importKind !== "type" && t.isIdentifier(specifier.node.imported, { name: "useSignals" })) imports.push({
1334
+ for (const specifier of statement.get("specifiers")) if (specifier.isImportSpecifier() && specifier.node.importKind !== "type" && t.isIdentifier(specifier.node.imported, { name: importedName })) imports.push({
894
1335
  identifier: t.cloneNode(specifier.node.local),
895
1336
  bindingPath: specifier
896
1337
  });
897
1338
  }
898
1339
  return imports;
899
1340
  }
900
- function addRuntimeImport(programPath, runtimeSource, functionPath) {
901
- const local = functionPath.scope.generateUidIdentifier("useSignals");
902
- const declaration = t.importDeclaration([t.importSpecifier(t.cloneNode(local), t.identifier("useSignals"))], t.stringLiteral(runtimeSource));
1341
+ function addRuntimeImport(programPath, runtimeSource, importedName, functionPath) {
1342
+ const local = functionPath.scope.generateUidIdentifier(importedName);
1343
+ const declaration = t.importDeclaration([t.importSpecifier(t.cloneNode(local), t.identifier(importedName))], t.stringLiteral(runtimeSource));
903
1344
  const imports = programPath.get("body").filter((path) => path.isImportDeclaration());
904
1345
  const inserted = (imports.length === 0 ? programPath.unshiftContainer("body", declaration) : imports.at(-1).insertAfter(declaration))[0];
905
- if (inserted === void 0 || !inserted.isImportDeclaration()) throw new Error("Failed to insert the useSignals import");
1346
+ if (inserted === void 0 || !inserted.isImportDeclaration()) throw new Error(`Failed to insert the ${importedName} import`);
906
1347
  programPath.scope.registerDeclaration(inserted);
907
1348
  const specifier = inserted.get("specifiers")[0];
908
- if (specifier === void 0 || !specifier.isImportSpecifier()) throw new Error("Failed to register the useSignals import");
1349
+ if (specifier === void 0 || !specifier.isImportSpecifier()) throw new Error(`Failed to register the ${importedName} import`);
909
1350
  return {
910
1351
  identifier: local,
911
1352
  bindingPath: specifier
@@ -913,31 +1354,31 @@ function addRuntimeImport(programPath, runtimeSource, functionPath) {
913
1354
  }
914
1355
  /**
915
1356
  * Is `statements`' first entry a bare zero-argument call whose callee resolves
916
- * to `useSignals`, with `allowBarrel` controlling whether a barrel/re-export
917
- * chain counts (see `isUseSignalsCallee`)? Shared by `isExplicitUseSignals`
1357
+ * to `useSignalTracking`, with `allowBarrel` controlling whether a barrel/re-export
1358
+ * chain counts (see `isSignalTrackingCallee`)? Shared by `isExplicitSignalTracking`
918
1359
  * (`allowBarrel: false`, the verified boundary) and
919
- * `isUnverifiableBarrelUseSignals` (`allowBarrel: true`, to detect the exact
1360
+ * `isUnverifiableBarrelSignalTracking` (`allowBarrel: true`, to detect the exact
920
1361
  * call the former rejects).
921
1362
  */
922
- function isFirstStatementUseSignalsCall(functionPath, statements, importSource, allowBarrel) {
1363
+ function isFirstStatementSignalTrackingCall(functionPath, statements, importSource, allowBarrel) {
923
1364
  const first = statements[0];
924
1365
  if (first === void 0 || !first.isExpressionStatement()) return false;
925
1366
  const expression = first.get("expression");
926
1367
  if (!expression.isCallExpression() || expression.node.arguments.length !== 0) return false;
927
- return isUseSignalsCallee(functionPath, expression.get("callee"), importSource, allowBarrel);
1368
+ return isSignalTrackingCallee(functionPath, expression.get("callee"), importSource, allowBarrel);
928
1369
  }
929
- function isExplicitUseSignals(functionPath, statements, importSource) {
930
- return isFirstStatementUseSignalsCall(functionPath, statements, importSource, false);
1370
+ function isExplicitSignalTracking(functionPath, statements, importSource) {
1371
+ return isFirstStatementSignalTrackingCall(functionPath, statements, importSource, false);
931
1372
  }
932
1373
  /**
933
- * The named import specifier the absorbed explicit `useSignals()` call resolves
1374
+ * The named import specifier the absorbed explicit `useSignalTracking()` call resolves
934
1375
  * to, when the call is a bare identifier bound by one. `applyManaged` replaces
935
1376
  * that call with its own store declaration, which can leave the import behind
936
1377
  * with nothing referencing it -- so `Program.exit` needs the specifier to check
937
1378
  * and drop. A namespace import is deliberately not returned: the namespace
938
1379
  * object may be used for other exports of the same module.
939
1380
  */
940
- function getAbsorbedUseSignalsImport(functionPath, statements) {
1381
+ function getAbsorbedSignalTrackingImport(functionPath, statements) {
941
1382
  const first = statements[0];
942
1383
  if (first === void 0 || !first.isExpressionStatement()) return void 0;
943
1384
  const expression = first.get("expression");
@@ -948,22 +1389,26 @@ function getAbsorbedUseSignalsImport(functionPath, statements) {
948
1389
  if (resolved === void 0 || !resolved.specifier.isImportSpecifier()) return void 0;
949
1390
  return resolved.specifier;
950
1391
  }
951
- function isUnverifiableBarrelUseSignals(functionPath, statements, importSource, explicit) {
1392
+ function isUnverifiableBarrelSignalTracking(functionPath, statements, importSource, explicit) {
952
1393
  if (explicit) return false;
953
- return isFirstStatementUseSignalsCall(functionPath, statements, importSource, true);
1394
+ return isFirstStatementSignalTrackingCall(functionPath, statements, importSource, true);
954
1395
  }
955
- function warnUnverifiableBarrelUseSignals(path, importSource) {
956
- const warning = path.buildCodeFrameError(`This useSignals() call cannot be verified as "${importSource}"'s own export: it resolves only through a barrel/re-export module, and a single-file transform cannot follow that chain to confirm the target. The component stays on the bare, best-effort useSignals() boundary. Import useSignals directly from "${importSource}" or "${importSource}/runtime" instead to get the verified boundary.`);
1396
+ function warnUnverifiableBarrelSignalTracking(path, importSource) {
1397
+ const warning = path.buildCodeFrameError(`This useSignalTracking() call cannot be verified as "${importSource}"'s own export: it resolves only through a barrel/re-export module, and a single-file transform cannot follow that chain to confirm the target. The component stays on the bare, best-effort useSignalTracking() boundary. Import useSignalTracking directly from "${importSource}" instead to get the verified boundary.`);
957
1398
  console.warn(warning.message);
958
1399
  }
959
- function warnUnnamedUseSignalsAnnotation(path) {
960
- const warning = path.buildCodeFrameError("This @useSignals annotation is ignored: the transform could not resolve a component or hook identity for the annotated function, and the boundary is attached to that identity. Give it a PascalCase (component) or useX (hook) name -- a binding, a named function declaration, or an object/class property key -- to opt it in.");
1400
+ function warnUnnamedSignalTrackingAnnotation(path) {
1401
+ const warning = path.buildCodeFrameError("This @signalTracking annotation is ignored: the transform could not resolve a component or hook identity for the annotated function, and the boundary is attached to that identity. Give it a PascalCase (component) or useX (hook) name -- a binding, a named function declaration, or an object property key -- to opt it in.");
961
1402
  console.warn(warning.message);
962
1403
  }
963
1404
  function warnUnnamedDefaultExport(path) {
964
1405
  const warning = path.buildCodeFrameError("This anonymous default export is left untransformed: its component identity is derived from the module's file name, and this module's name does not yield a PascalCase identifier. Name the function (`export default function App() { ... }`), or assign it to a PascalCase binding and export that binding, to opt it in.");
965
1406
  console.warn(warning.message);
966
1407
  }
1408
+ function warnPlainCalledComponentAlsoMounted(path, name) {
1409
+ const warning = path.buildCodeFrameError(`${name} is called as a plain function (${name}()) and also rendered as <${name} />. A plain call runs its body inside the caller's render, so it gets no tracking boundary of its own (its signal reads are tracked by the calling component instead), and the <${name} /> instance therefore only updates when its parent re-renders. Use one form consistently -- render it only as <${name} />, or only call it -- to remove the ambiguity.`);
1410
+ console.warn(warning.message);
1411
+ }
967
1412
  function shouldAutomaticallyTransform(mode, inspection, identity) {
968
1413
  if (isHookIdentity(identity)) return mode !== "manual" && inspection.readsValue;
969
1414
  if (!isComponentIdentity(identity) || !inspection.containsJSX) return false;
@@ -975,10 +1420,10 @@ function markTransformed(state) {
975
1420
  }
976
1421
  /**
977
1422
  * Resolves what, if anything, `path` needs done to it: the opt-out check,
978
- * explicit/annotated/automatic eligibility (plus the barrel-useSignals
1423
+ * explicit/annotated/automatic eligibility (plus the barrel-useSignalTracking
979
1424
  * warning that eligibility check surfaces along the way), the
980
1425
  * async/generator guard, and -- once a function is confirmed eligible for
981
- * real codegen -- acquiring the runtime `useSignals` import it will call.
1426
+ * real codegen -- acquiring the corresponding runtime hook import it will call.
982
1427
  * Pure decision-making: no AST mutation happens here, so every early return is
983
1428
  * just a `return`, not a `return` guarding mutations already made.
984
1429
  */
@@ -987,38 +1432,39 @@ function decideTransform(path, state, options, reactImportSource) {
987
1432
  const identity = resolveComponentIdentity(path, reactImportSource, climbedParent) ?? getDefaultExportIdentity(climbedParent, state.filename);
988
1433
  const factory = identity?.kind === "hoc-return" ? identity.factory : void 0;
989
1434
  const ownsLeadingComment = (pattern) => hasOwnedLeadingComment(path, pattern) || factory !== void 0 && hasOwnedLeadingComment(factory, pattern);
990
- if (ownsLeadingComment(noUseSignalsComment)) return { kind: "skip" };
1435
+ if (ownsLeadingComment(noSignalTrackingComment)) return { kind: "skip" };
991
1436
  const body = path.get("body");
992
1437
  const statements = body.isBlockStatement() ? body.get("body") : [];
993
- const explicit = isExplicitUseSignals(path, statements, options.importSource);
994
- if (isUnverifiableBarrelUseSignals(path, statements, options.importSource, explicit)) warnUnverifiableBarrelUseSignals(path, options.importSource);
995
- const inspection = inspectFunction(path, options.importSource, reactImportSource);
996
- const annotation = ownsLeadingComment(useSignalsComment);
1438
+ const explicit = isExplicitSignalTracking(path, statements, options.importSource);
1439
+ if (isUnverifiableBarrelSignalTracking(path, statements, options.importSource, explicit)) warnUnverifiableBarrelSignalTracking(path, options.importSource);
1440
+ const managedRuntimeSource = `${options.importSource}/runtime`;
1441
+ const inspection = inspectFunction(path, options.importSource, managedRuntimeSource, reactImportSource);
1442
+ const annotation = ownsLeadingComment(useSignalTrackingComment);
997
1443
  const isFactory = isHigherOrderComponentFactory(path) && !isHookIdentity(identity);
998
1444
  const annotated = annotation && !isFactory && (isComponentIdentity(identity) || isHookIdentity(identity));
999
1445
  const candidate = isAutomaticTransformCandidate(path, reactImportSource, climbedParent);
1000
1446
  if (annotation && !annotated && rendersJsx(path)) {
1001
1447
  const enclosing = path.getFunctionParent();
1002
- if (enclosing === null || !hasOwnedLeadingComment(enclosing, useSignalsComment)) warnUnnamedUseSignalsAnnotation(path);
1448
+ if (enclosing === null || !hasOwnedLeadingComment(enclosing, useSignalTrackingComment)) warnUnnamedSignalTrackingAnnotation(path);
1003
1449
  }
1004
1450
  if (options.mode !== "manual" && identity === void 0 && climbedParent !== null && climbedParent.isExportDefaultDeclaration() && rendersJsx(path)) warnUnnamedDefaultExport(path);
1451
+ if (!candidate && !explicit && !annotated && !isFactory && shouldAutomaticallyTransform(options.mode, inspection, identity) && isComponentIdentity(identity) && isPlainCalledComponent(path) && isAlsoMountedThroughJsx(path)) warnPlainCalledComponentAlsoMounted(path, identity?.name ?? "component");
1005
1452
  const automatic = !isFactory && candidate && shouldAutomaticallyTransform(options.mode, inspection, identity);
1006
1453
  if (!explicit && !annotated && !automatic) return { kind: "skip" };
1007
- if (!explicit && inspection.hasUseSignalsCall) return { kind: "skip" };
1454
+ if (!explicit && (inspection.hasSignalTrackingCall || inspection.hasUseManagedSignalsCall)) return { kind: "skip" };
1008
1455
  if (options.transform === "inject" && explicit) return {
1009
1456
  kind: "directive-only",
1010
1457
  body
1011
1458
  };
1012
1459
  if (path.node.async || path.node.generator) {
1013
1460
  if (!explicit && !annotated) return { kind: "skip" };
1014
- throw path.buildCodeFrameError("useSignals transform only supports synchronous, non-generator functions");
1461
+ throw path.buildCodeFrameError("useSignalTracking transform only supports synchronous, non-generator functions");
1015
1462
  }
1016
- const managedRuntimeSource = `${options.importSource}/runtime`;
1017
1463
  const importSource = options.transform === "managed" ? managedRuntimeSource : options.importSource;
1018
1464
  const imports = options.transform === "managed" ? state.managedRuntimeImports : state.directImports;
1019
1465
  let runtimeImport = imports.find(({ identifier, bindingPath }) => path.scope.getBinding(identifier.name)?.path === bindingPath);
1020
1466
  if (runtimeImport === void 0) {
1021
- runtimeImport = addRuntimeImport(state.programPath, importSource, path);
1467
+ runtimeImport = addRuntimeImport(state.programPath, importSource, options.transform === "managed" ? "useManagedSignals" : "useSignalTracking", path);
1022
1468
  imports.push(runtimeImport);
1023
1469
  }
1024
1470
  return {
@@ -1027,17 +1473,186 @@ function decideTransform(path, state, options, reactImportSource) {
1027
1473
  statements,
1028
1474
  explicit,
1029
1475
  runtimeImport,
1030
- absorbedImport: explicit ? getAbsorbedUseSignalsImport(path, statements) : void 0
1476
+ absorbedImport: explicit ? getAbsorbedSignalTrackingImport(path, statements) : void 0
1031
1477
  };
1032
1478
  }
1033
- /** Codegen: inject a bare `useSignals()` call at the top of the function body. */
1479
+ /**
1480
+ * Does `param` itself evaluate a tracked `.value` read when the function is
1481
+ * called -- i.e. in a default value (`{ v = count.value }`, `n = count.value`)?
1482
+ *
1483
+ * Reads inside a nested function are skipped: a closure created by a default
1484
+ * (`format = () => count.value`) does not run during parameter binding, and if
1485
+ * the body calls it during render, it runs after the boundary is open anyway.
1486
+ */
1487
+ function parameterReadsTrackedValue(param, functionPath) {
1488
+ if (param.isIdentifier()) return false;
1489
+ let found = false;
1490
+ const visit = (path) => {
1491
+ if (isTrackedValueRead(path.node, path, functionPath)) {
1492
+ found = true;
1493
+ path.stop();
1494
+ }
1495
+ };
1496
+ param.traverse({
1497
+ Function(nested) {
1498
+ nested.skip();
1499
+ },
1500
+ MemberExpression: visit,
1501
+ OptionalMemberExpression: visit
1502
+ });
1503
+ return found;
1504
+ }
1505
+ /**
1506
+ * Would moving `moved` from the parameter list into the body change what any
1507
+ * name in them means? Two shapes do, and both are rare enough that the honest
1508
+ * answer is to leave the parameters exactly where they were:
1509
+ *
1510
+ * - A default refers to a name the body itself declares
1511
+ * (`({ v = count.value }) => { const count = ...; }`). In the parameter list
1512
+ * that name is the *outer* binding, because parameters get a scope of their
1513
+ * own; moved into the body it would hit the body's declaration instead (and
1514
+ * its temporal dead zone). Babel models parameters and body-level
1515
+ * declarations as one function scope, which is precisely what makes this
1516
+ * detectable: such a name resolves to an own binding that is not a parameter.
1517
+ * - A moved parameter is re-declared in the body (`var v;`, `function v() {}`),
1518
+ * which is legal next to a parameter but a redeclaration error next to the
1519
+ * `let` the move would emit. Babel records those as constant violations of
1520
+ * the parameter binding.
1521
+ */
1522
+ function hasRelocationConflict(path, moved) {
1523
+ for (const param of moved) {
1524
+ for (const name of Object.keys(t.getBindingIdentifiers(param.node))) if (path.scope.getOwnBinding(name)?.constantViolations.some((violation) => violation.isVariableDeclarator() || violation.isFunctionDeclaration() || violation.isClassDeclaration())) return true;
1525
+ let conflict = false;
1526
+ param.traverse({ ReferencedIdentifier(reference) {
1527
+ if (!reference.isIdentifier()) return;
1528
+ const binding = path.scope.getOwnBinding(reference.node.name);
1529
+ if (binding !== void 0 && binding.kind !== "param") {
1530
+ conflict = true;
1531
+ reference.stop();
1532
+ }
1533
+ } });
1534
+ if (conflict) return true;
1535
+ }
1536
+ return false;
1537
+ }
1538
+ /**
1539
+ * Moves the parameters whose defaults read a signal behind the boundary,
1540
+ * returning the statements that bind them again, for the caller to place as
1541
+ * the first statements *after* the hook call.
1542
+ *
1543
+ * Parameter defaults are evaluated before the first statement of the body runs,
1544
+ * so in `function C({ v = count.value })` the read happens before the injected
1545
+ * `useManagedSignals()` / `useSignalTracking()` call has opened the render
1546
+ * collection -- the read is real, `inspectFunction` rightly counts it and
1547
+ * transforms the component, and yet the boundary never sees it, so the
1548
+ * component does not re-render when `count` changes. Moving the pattern into
1549
+ * the body is the only placement that runs it after the hook:
1550
+ *
1551
+ * ```js
1552
+ * function C(_param) {
1553
+ * const _signals = _useManagedSignals();
1554
+ * try {
1555
+ * let { v = count.value } = _param;
1556
+ * ...
1557
+ * ```
1558
+ *
1559
+ * The rewrite is kept as small as it can be while staying faithful:
1560
+ *
1561
+ * - Nothing happens unless a parameter actually reads a signal; every other
1562
+ * function keeps its parameter list untouched.
1563
+ * - From the first such parameter *onward*, every parameter moves, in order. A
1564
+ * later default may refer to an earlier parameter (`(a = count.value, b =
1565
+ * a)`), and it may have side effects whose order matters, so moving one
1566
+ * parameter alone could change what the others see.
1567
+ * - Each moved parameter is replaced one-for-one by a generated identifier
1568
+ * rather than removed, so the parameter *count* is unchanged and a
1569
+ * `forwardRef` render function keeps its two-parameter `(props, ref)` shape
1570
+ * (`length` can change where a default used to end the count, which React
1571
+ * only inspects to warn about a `forwardRef` arity other than 0 or 2).
1572
+ * - A default still applies only on `undefined`: `v = count.value` becomes
1573
+ * `let v = _param === undefined ? count.value : _param`, which is the exact
1574
+ * rule a parameter default follows; a destructuring pattern's own nested
1575
+ * defaults keep their meaning by moving verbatim.
1576
+ * - A type annotation travels to the generated parameter, so the signature
1577
+ * still says what it accepts; `?` optional markers have no meaning on a
1578
+ * `let` and are dropped from the moved pattern. In TypeScript the generated
1579
+ * parameter is marked `?` wherever the original was optional.
1580
+ *
1581
+ * When the move cannot be done without changing meaning
1582
+ * (`hasRelocationConflict`), or a parameter is a TypeScript parameter property
1583
+ * (constructor-only, never a component), nothing moves and the read stays
1584
+ * untracked exactly as before -- an edge this declines rather than miscompiles.
1585
+ */
1586
+ function relocateTrackedParameters(path, typeScript) {
1587
+ const params = path.get("params");
1588
+ const first = params.findIndex((param) => parameterReadsTrackedValue(param, path));
1589
+ if (first === -1) return [];
1590
+ const moved = params.slice(first);
1591
+ const plan = [];
1592
+ for (const param of moved) {
1593
+ const node = param.node;
1594
+ if (t.isRestElement(node)) {
1595
+ if (!isBindablePattern(node.argument)) return [];
1596
+ plan.push({
1597
+ param,
1598
+ pattern: node.argument,
1599
+ rest: node
1600
+ });
1601
+ } else if (t.isAssignmentPattern(node)) {
1602
+ if (!isBindablePattern(node.left)) return [];
1603
+ plan.push({
1604
+ param,
1605
+ pattern: node.left,
1606
+ fallback: node.right
1607
+ });
1608
+ } else if (isBindablePattern(node)) plan.push({
1609
+ param,
1610
+ pattern: node
1611
+ });
1612
+ else return [];
1613
+ }
1614
+ if (hasRelocationConflict(path, moved)) return [];
1615
+ const optional = [];
1616
+ let laterOptional = true;
1617
+ for (let index = plan.length - 1; index >= 0; index -= 1) {
1618
+ const { pattern, fallback, rest } = plan[index];
1619
+ optional[index] = typeScript && rest === void 0 && (pattern.optional === true || fallback !== void 0 && laterOptional);
1620
+ laterOptional &&= rest !== void 0 || optional[index];
1621
+ }
1622
+ const declarations = [];
1623
+ for (const [index, { param, pattern, fallback, rest }] of plan.entries()) {
1624
+ const local = path.scope.generateUidIdentifier("param");
1625
+ const init = fallback === void 0 ? t.cloneNode(local) : t.conditionalExpression(t.binaryExpression("===", t.cloneNode(local), t.identifier("undefined")), fallback, t.cloneNode(local));
1626
+ let replacement = local;
1627
+ if (rest !== void 0) {
1628
+ replacement = t.restElement(t.cloneNode(local));
1629
+ replacement.typeAnnotation = rest.typeAnnotation ?? null;
1630
+ } else if (pattern.typeAnnotation !== null && pattern.typeAnnotation !== void 0) local.typeAnnotation = pattern.typeAnnotation;
1631
+ if (optional[index]) local.optional = true;
1632
+ pattern.typeAnnotation = null;
1633
+ pattern.optional = null;
1634
+ declarations.push(t.variableDeclaration("let", [t.variableDeclarator(pattern, init)]));
1635
+ param.replaceWith(replacement);
1636
+ }
1637
+ return declarations;
1638
+ }
1639
+ function isBindablePattern(node) {
1640
+ return t.isIdentifier(node) || t.isObjectPattern(node) || t.isArrayPattern(node);
1641
+ }
1642
+ /** Codegen: inject a bare `useSignalTracking()` call at the top of the function body. */
1034
1643
  function applyInject(path, { body, runtimeImport }, options, state) {
1035
1644
  const call = t.expressionStatement(t.callExpression(t.cloneNode(runtimeImport.identifier), []));
1036
- if (body.isBlockStatement()) body.unshiftContainer("body", call);
1037
- else body.replaceWith(t.blockStatement([call, t.returnStatement(body.node)]));
1645
+ const relocated = relocateTrackedParameters(path, state.typeScript);
1646
+ if (body.isBlockStatement()) body.unshiftContainer("body", [call, ...relocated]);
1647
+ else body.replaceWith(t.blockStatement([
1648
+ call,
1649
+ ...relocated,
1650
+ t.returnStatement(body.node)
1651
+ ]));
1038
1652
  const injectedBody = path.node.body;
1039
1653
  if (options.reactCompiler === "auto" && t.isBlockStatement(injectedBody)) addNoMemoDirective(injectedBody);
1040
1654
  path.scope.crawl();
1655
+ invalidateThisReferences(path);
1041
1656
  markTransformed(state);
1042
1657
  }
1043
1658
  /** Codegen: wrap the function body in the managed try/finally render-tracking scope. */
@@ -1047,16 +1662,18 @@ function applyManaged(path, { body, statements, explicit, runtimeImport, absorbe
1047
1662
  const first = statements[0];
1048
1663
  if (explicit && first !== void 0) t.inheritsComments(declaration, first.node);
1049
1664
  const originalStatements = body.isBlockStatement() ? statements.slice(explicit ? 1 : 0).map((statement) => statement.node) : [t.returnStatement(body.node)];
1050
- const transformedBody = t.blockStatement([declaration, t.tryStatement(t.blockStatement(originalStatements), null, t.blockStatement([t.expressionStatement(t.callExpression(t.memberExpression(t.cloneNode(store), t.identifier("f")), []))]))]);
1665
+ const relocated = relocateTrackedParameters(path, state.typeScript);
1666
+ const transformedBody = t.blockStatement([declaration, t.tryStatement(t.blockStatement([...relocated, ...originalStatements]), null, t.blockStatement([t.expressionStatement(t.callExpression(t.memberExpression(t.cloneNode(store), t.identifier("finish")), []))]))]);
1051
1667
  if (body.isBlockStatement()) transformedBody.directives = body.node.directives;
1052
1668
  if (options.reactCompiler === "auto") addNoMemoDirective(transformedBody);
1053
1669
  body.replaceWith(transformedBody);
1054
1670
  path.scope.crawl();
1671
+ invalidateThisReferences(path);
1055
1672
  if (absorbedImport !== void 0) state.absorbedImports.push(absorbedImport);
1056
1673
  markTransformed(state);
1057
1674
  }
1058
1675
  /**
1059
- * Drops an `import { useSignals } from "<importSource>"` the managed transform
1676
+ * Drops an `import { useSignalTracking } from "<importSource>"` the managed transform
1060
1677
  * absorbed and nothing else refers to any more, along with the whole
1061
1678
  * declaration once its last specifier goes. A reused `/runtime` import survives
1062
1679
  * this untouched: the emitted store declaration still calls it, so the binding
@@ -1075,6 +1692,45 @@ function removeAbsorbedImports(programPath, state) {
1075
1692
  }
1076
1693
  state.absorbedImports = [];
1077
1694
  }
1695
+ const jsxPragmaComment = /@jsx(?:ImportSource|Runtime|Frag)?(?![\w$])/;
1696
+ /**
1697
+ * Puts a per-file JSX pragma back in front of the code once a generated import
1698
+ * has been placed ahead of it.
1699
+ *
1700
+ * In a file with no import of its own, `addRuntimeImport` has to put the
1701
+ * runtime import first, and Babel keeps the author's leading comments attached
1702
+ * to the statement they were written above -- so a first-line
1703
+ * `/** @jsxImportSource react-fine-grained-signals *\/` ends up printed after
1704
+ * that import. Oxc (and so Vite) only honors a JSX pragma that comes before the
1705
+ * first statement, so the pragma was silently ignored: the file compiled against
1706
+ * `react/jsx-runtime`, and a signal child then reached React as an object and
1707
+ * threw on mount.
1708
+ *
1709
+ * Every leading comment up to and including the last pragma moves onto the
1710
+ * first generated import, in order, so a license header above the pragma stays
1711
+ * above it; comments after the last pragma -- typically the first statement's
1712
+ * own doc comment -- stay where they were. A file without a pragma is left
1713
+ * exactly as it was. This runs at `Program.exit`, after every function has been
1714
+ * decided, because `@signalTracking` / `@noSignalTracking` are read from those
1715
+ * same leading comments.
1716
+ */
1717
+ function keepJsxPragmasFirst(programPath, state) {
1718
+ if (!state.importFree) return;
1719
+ const body = programPath.node.body;
1720
+ const authored = body.findIndex((statement) => !t.isImportDeclaration(statement));
1721
+ if (authored <= 0) return;
1722
+ const comments = body[authored].leadingComments;
1723
+ if (comments === null || comments === void 0) return;
1724
+ let lastPragma = -1;
1725
+ for (const [index, comment] of comments.entries()) if (jsxPragmaComment.test(comment.value)) lastPragma = index;
1726
+ if (lastPragma === -1) return;
1727
+ const generated = body[0];
1728
+ const staying = comments.slice(lastPragma + 1);
1729
+ generated.leadingComments = [...generated.leadingComments ?? [], ...comments.slice(0, lastPragma + 1)];
1730
+ body[authored].leadingComments = staying;
1731
+ const interpreter = programPath.node.interpreter;
1732
+ if (interpreter?.trailingComments) interpreter.trailingComments = interpreter.trailingComments.filter((comment) => !staying.includes(comment));
1733
+ }
1078
1734
  const babelTransform = declare((api, options) => {
1079
1735
  api.assertVersion(8);
1080
1736
  const managedRuntimeSource = `${options.importSource}/runtime`;
@@ -1085,13 +1741,16 @@ const babelTransform = declare((api, options) => {
1085
1741
  Program: {
1086
1742
  enter(path, state) {
1087
1743
  state.programPath = path;
1088
- state.managedRuntimeImports = findRuntimeImports(path, managedRuntimeSource);
1089
- state.directImports = findRuntimeImports(path, options.importSource);
1744
+ state.managedRuntimeImports = findRuntimeImports(path, managedRuntimeSource, "useManagedSignals");
1745
+ state.directImports = findRuntimeImports(path, options.importSource, "useSignalTracking");
1090
1746
  state.absorbedImports = [];
1747
+ state.typeScript = state.file.opts.parserOpts?.plugins?.includes("typescript") === true;
1748
+ state.importFree = !path.node.body.some((statement) => t.isImportDeclaration(statement));
1091
1749
  state.file.metadata[transformedMetadataKey] = false;
1092
1750
  },
1093
1751
  exit(path, state) {
1094
1752
  removeAbsorbedImports(path, state);
1753
+ keepJsxPragmasFirst(path, state);
1095
1754
  }
1096
1755
  },
1097
1756
  Function(path, state) {
@@ -1118,9 +1777,8 @@ const babelTransform = declare((api, options) => {
1118
1777
  * it is discarded outright for every file none of the three opt-in routes can
1119
1778
  * possibly apply to:
1120
1779
  *
1121
- * - explicit and annotated opt-in both need the text `useSignals` (an aliased
1122
- * import, a namespace call and the `@useSignals` comment all still contain
1123
- * it);
1780
+ * - explicit opt-in needs `useSignalTracking`; annotation opt-in and opt-out
1781
+ * need `@signalTracking` or `@noSignalTracking`;
1124
1782
  * - `auto` needs a `.value` read, so it needs the text `value`;
1125
1783
  * - `all` additionally wraps JSX components, so it needs a `<` -- but its
1126
1784
  * custom-hook rule still needs a `.value` read.
@@ -1130,7 +1788,7 @@ const babelTransform = declare((api, options) => {
1130
1788
  * would have rejected anyway.
1131
1789
  */
1132
1790
  function mightTransform(code, mode) {
1133
- if (code.includes("useSignals")) return true;
1791
+ if (code.includes("useSignalTracking") || code.includes("@signalTracking") || code.includes("@noSignalTracking")) return true;
1134
1792
  if (mode === "manual") return false;
1135
1793
  if (code.includes("value")) return true;
1136
1794
  return mode === "all" && code.includes("<");
@@ -1168,7 +1826,7 @@ function transformReactFineGrainedSignals(code, id, options) {
1168
1826
  }
1169
1827
  //#endregion
1170
1828
  //#region src/unplugin.ts
1171
- const SCRIPT_MODULE = /\.m?[jt]sx?$/;
1829
+ const SCRIPT_MODULE = /\.m?[jt]sx?$/i;
1172
1830
  const pluginName = "unplugin-react-fine-grained-signals";
1173
1831
  /**
1174
1832
  * `id` with the `?query`/`#fragment` a bundler appends taken off, which is
@@ -1300,21 +1958,26 @@ function useOwnTransformLoader(compiler) {
1300
1958
  }
1301
1959
  const reactFineGrainedSignals = createUnplugin((options = {}) => {
1302
1960
  let configuredLoaders;
1961
+ const transform = (code, id) => transformReactFineGrainedSignals(code, id, {
1962
+ importSource: options.importSource ?? "react-fine-grained-signals",
1963
+ reactImportSource: options.reactImportSource ?? "react",
1964
+ mode: options.mode ?? "auto",
1965
+ transform: options.transform ?? "managed",
1966
+ reactCompiler: options.reactCompiler ?? "auto"
1967
+ });
1303
1968
  return {
1304
1969
  name: pluginName,
1305
1970
  enforce: "pre",
1306
1971
  transformInclude(id) {
1307
1972
  return canTransform(id, options);
1308
1973
  },
1309
- transform(code, id) {
1310
- return transformReactFineGrainedSignals(code, id, {
1311
- importSource: options.importSource ?? "react-fine-grained-signals",
1312
- reactImportSource: options.reactImportSource ?? "react",
1313
- mode: options.mode ?? "auto",
1314
- transform: options.transform ?? "managed",
1315
- reactCompiler: options.reactCompiler ?? "auto"
1316
- });
1317
- },
1974
+ transform,
1975
+ vite: { transform: {
1976
+ order: "pre",
1977
+ handler(code, id) {
1978
+ return canTransform(id, options) ? transform(code, id) : null;
1979
+ }
1980
+ } },
1318
1981
  esbuild: {
1319
1982
  config(buildOptions) {
1320
1983
  configuredLoaders = buildOptions.loader;