@flotrace/runtime-core 2.2.1 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sameer Sitre
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,33 +1,76 @@
1
1
  # @flotrace/runtime-core
2
2
 
3
- Platform-agnostic core for the FloTrace runtime — fiber walker, hook/effect inspectors, state-store trackers, serializer, and WebSocket client. Shared by [`@flotrace/runtime`](https://www.npmjs.com/package/@flotrace/runtime) (web) and [`@flotrace/runtime-native`](https://www.npmjs.com/package/@flotrace/runtime-native) (React Native).
3
+ Platform-agnostic core for [FloTrace](https://flotrace.dev) — fiber walker, hook/effect inspectors, state-store trackers (Zustand / Redux / TanStack Query), serializer, and WebSocket client. Shared between [`@flotrace/runtime`](https://www.npmjs.com/package/@flotrace/runtime) (web) and [`@flotrace/runtime-native`](https://www.npmjs.com/package/@flotrace/runtime-native) (React Native).
4
4
 
5
- > **You probably don't want this package directly.**
6
- > Install `@flotrace/runtime` for a web React app or `@flotrace/runtime-native` for React Native. Those adapters depend on `runtime-core` and provide the wiring (provider, network tracker, platform-specific hooks) you need.
5
+ > **You almost certainly want one of the adapter packages instead.**
6
+ >
7
+ > - **Web React app?** → [`@flotrace/runtime`](https://www.npmjs.com/package/@flotrace/runtime)
8
+ > - **React Native (Expo / bare)?** → [`@flotrace/runtime-native`](https://www.npmjs.com/package/@flotrace/runtime-native)
9
+ >
10
+ > The adapters depend on `runtime-core` and provide the wiring (provider component, network tracker, platform-specific hooks) you actually need. This package on its own does nothing useful at runtime.
7
11
 
8
- `runtime-core` is published as a public package so adapters can pin a compatible version. It has no runtime dependency on `window` / `document` / `XMLHttpRequest` — all platform-specific features live in the adapters.
12
+ `runtime-core` is published publicly so adapters can pin a compatible version and so users can audit the open-source half of FloTrace. It has zero runtime dependency on `window` / `document` / `XMLHttpRequest` — all platform-specific features live in the adapters.
13
+
14
+ ---
15
+
16
+ ## About FloTrace Desktop
17
+
18
+ [**FloTrace Desktop**](https://flotrace.dev) is a free Electron app (macOS / Windows / Linux) that visualizes a running React app's component hierarchy in real time. It pairs with this runtime over a local WebSocket on port `3457` — the runtime sits inside your app and emits metadata; the desktop app renders the live tree, props, hooks, effects, and state. **Source code never leaves your machine.**
19
+
20
+ What you get when this runtime is paired with the desktop:
21
+
22
+ - **Live component tree** — React Flow graph, render-flash animation, frequency-based heatmap, breadcrumb navigation.
23
+ - **Per-node inspection** — props (with diff history), hooks (14 classified types + dep diffs), effects (willRun + dep diffs), component timeline.
24
+ - **State tracking** — Zustand (per-store), Redux (with change highlighting), Router, TanStack Query (with health warnings + wasted-refetch detection), Context.
25
+ - **Render cascade tracing** — trigger log, cascade tree, flame chart, cascade compare modal.
26
+ - **Prop drilling detection** — chain detection (≥3 levels deep), severity badges, heatmap overlay, refactor recommendations.
27
+ - **Network health** — fetch / XHR tracking, method badges, status dots, duplicate detection, API → store causal correlation.
28
+ - **Watch expressions** — pin values from 8 sources (Zustand / Redux / Router / Context / Props / Hooks / TanStack Query / API).
29
+ - **AI Code Review Dashboard** — 6-tab review (Re-renders, Memo, Drilling, Effects, Compiler, Network) with Lighthouse-style scores.
30
+ - **Copy-as-Prompt** — turn any panel into an AI-ready prompt for Cursor / Claude / ChatGPT in one click.
31
+
32
+ How it fits together:
33
+
34
+ ```
35
+ your React app ←→ @flotrace/runtime[-native] ←→ ws://localhost:3457 ←→ FloTrace Desktop
36
+ (this stack — open source, MIT) (closed-source commercial)
37
+ ```
38
+
39
+ [**Download FloTrace Desktop →**](https://flotrace.dev) · [Docs](https://flotrace.dev/docs) · [Security model](https://flotrace.dev/security)
9
40
 
10
41
  ## What's inside
11
42
 
12
43
  | Module | Purpose |
13
44
  |---|---|
14
- | `fiberTreeWalker` | Incremental fiber walk, diffed tree emission, pluggable `pruneSubtree` / `frameworkComponentNames` / `hostComponentSkipPrefixes` options for platform adapters. |
15
- | `hookInspector` / `effectInspector` | Classify hooks and effects from a fiber; diff deps between commits. |
16
- | `zustandTracker` / `reduxTracker` / `tanstackQueryTracker` | Duck-typed subscribers for the major state libraries. |
17
- | `timelineTracker` | Per-component lifecycle events. |
18
- | `cascadeAnalyzer` / `propDrillingAnalyzer` | Render-cascade tracing + prop-drilling chain detection. |
19
- | `serializer` | Safe JSON serialization (depth 5, circular refs, truncation). |
20
- | `websocketClient` | Singleton WS client with reconnect, batching, auth-token support. |
45
+ | `fiberTreeWalker` | Incremental fiber walk, diffed tree emission. Pluggable `pruneSubtree` / `frameworkComponentNames` / `hostComponentSkipPrefixes` for platform adapters. |
46
+ | `hookInspector` / `effectInspector` | Classify hooks (14 types) and effects from a fiber; diff deps between commits. |
47
+ | `zustandTracker` / `reduxTracker` / `tanstackQueryTracker` | Duck-typed subscribers for the major state libraries — no peer-dep bloat. |
48
+ | `timelineTracker` | Per-component lifecycle events (mount, unmount, update, prop diff). |
49
+ | `cascadeAnalyzer` / `propDrillingAnalyzer` | Render-cascade tracing + prop-drilling DFS chain detection with severity scoring. |
50
+ | `serializer` | Safe JSON serialization (depth 5, circular-ref guard, truncation). |
51
+ | `websocketClient` | Singleton WS client with exponential backoff reconnect, message batching, optional auth token. |
21
52
 
22
53
  ## Version compatibility
23
54
 
24
- `@flotrace/runtime-core@0.1.x` is the companion release for:
55
+ `@flotrace/runtime-core@2.x` is the companion release for:
56
+
57
+ - `@flotrace/runtime@2.x`
58
+ - `@flotrace/runtime-native@2.x`
59
+
60
+ All three are released in lockstep — pin the same major.minor across the trio. The desktop app and runtime versions are independent (the WebSocket protocol is versioned).
61
+
62
+ ## Why open?
25
63
 
26
- - `@flotrace/runtime@0.2.x`
27
- - `@flotrace/runtime-native@0.1.x`
64
+ The runtime is what lives inside your app. Open-source means you can read every byte of the code that touches your fibers, audit the WebSocket payloads, and fork if FloTrace ever disappears. The desktop app is closed-source commercial — that's the bit we charge for. See [flotrace.dev/security](https://flotrace.dev/security) for the full threat model.
28
65
 
29
- Use matching minor versions when pinning across all three.
66
+ ## Contributing
67
+
68
+ Issues and PRs welcome at [github.com/sameersitre/runtime-core](https://github.com/sameersitre/runtime-core). The runtime packages target Hermes, V8 (Chromium), and JavaScriptCore — please test against all three when changing fiber-walker or serializer code.
30
69
 
31
70
  ## License
32
71
 
33
- MIT
72
+ MIT — see [LICENSE](./LICENSE).
73
+
74
+ ---
75
+
76
+ > **Mirrored from the [flotrace-desktop](https://github.com/sameersitre/flotrace-desktop) monorepo.** This repo is read-only — every release is regenerated by the lockstep publisher in the desktop monorepo. Issues filed here are tracked, but PRs are best opened against the upstream monorepo where the canonical source lives.
package/dist/index.d.mts CHANGED
@@ -48,6 +48,13 @@ interface RuntimeReadyMessage {
48
48
  /** React Native version from `Platform.constants.reactNativeVersion`, formatted
49
49
  * as "major.minor.patch". Native-only; web adapter leaves this undefined. */
50
50
  reactNativeVersion?: string;
51
+ /** Version of the @flotrace/runtime or @flotrace/runtime-native package the
52
+ * user installed in their app. Lets the desktop diagnose runtime/desktop
53
+ * drift (e.g., user pinned an older runtime that lacks a new feature).
54
+ * Read from the adapter's own package.json at build time. The lockstep
55
+ * release script keeps runtime-core pinned identically, so a separate
56
+ * core-version field would be redundant. */
57
+ runtimeVersion?: string;
51
58
  }
52
59
  interface RuntimeRenderMessage {
53
60
  type: 'runtime:render';
@@ -867,10 +874,14 @@ interface FloTraceConfig {
867
874
  /** React Native version from `Platform.constants.reactNativeVersion`, formatted
868
875
  * "major.minor.patch". Native adapter only. */
869
876
  reactNativeVersion?: string;
877
+ /** Version of the active runtime adapter (`@flotrace/runtime` on web,
878
+ * `@flotrace/runtime-native` on RN). Adapters auto-populate from their
879
+ * own package.json. Surfaced on `runtime:ready` for diagnostics. */
880
+ runtimeVersion?: string;
870
881
  }
871
882
  /** Keys that stay optional in DEFAULT_CONFIG. These are populated by adapters (web/native)
872
883
  * at call-time — the default object should not pretend to know a platform or LAN token. */
873
- type OptionalConfigKeys = 'getAppUrl' | 'platform' | 'appId' | 'appVersion' | 'host' | 'authToken' | 'userOnlyStrict' | 'userAllowPatterns' | 'frameworkName' | 'frameworkVersion' | 'reactNativeVersion';
884
+ type OptionalConfigKeys = 'getAppUrl' | 'platform' | 'appId' | 'appVersion' | 'host' | 'authToken' | 'userOnlyStrict' | 'userAllowPatterns' | 'frameworkName' | 'frameworkVersion' | 'reactNativeVersion' | 'runtimeVersion';
874
885
  type ResolvedFloTraceConfig = Required<Omit<FloTraceConfig, OptionalConfigKeys>> & Pick<FloTraceConfig, OptionalConfigKeys>;
875
886
  /**
876
887
  * Default configuration
@@ -1589,7 +1600,7 @@ declare function getChangedKeys(prev: Record<string, unknown> | undefined, next:
1589
1600
  * State lives here — in platform-agnostic core — so both the web and native
1590
1601
  * network trackers can write to the same registry that the analyzers read.
1591
1602
  */
1592
- /** Tag an object and its nested children (depth ≤ 2) with the requestId. */
1603
+ /** Tag an object and its nested children (depth ≤ FETCH_ORIGIN_SCAN_DEPTH) with the requestId. */
1593
1604
  declare function tagFetchData(obj: unknown, requestId: string, depth?: number): void;
1594
1605
  /** Returns true if any API request's response data is currently tagged (within TTL window). */
1595
1606
  declare function hasActiveTags(): boolean;
@@ -1600,13 +1611,23 @@ declare function hasActiveTags(): boolean;
1600
1611
  */
1601
1612
  declare function clearFetchOriginTags(): void;
1602
1613
  /**
1603
- * Scan an object (and nested children up to depth 2) for a WeakMap-tagged fetch origin.
1604
- * Called by Zustand/Redux trackers synchronously in their subscribe callbacks.
1605
- * Returns the requestId if this object was the result of a tracked fetch within the TTL
1606
- * window, else undefined. TTL prevents stale entries from matching on later store updates
1607
- * that reuse the same object references (immutable store pattern).
1614
+ * Scan an object (and nested children up to FETCH_ORIGIN_SCAN_DEPTH) for a
1615
+ * WeakMap-tagged fetch origin. Returns the requestId if this object was the
1616
+ * result of a tracked fetch, else undefined.
1617
+ *
1618
+ * By default the result must be within FETCH_ORIGIN_TTL_MS of the original
1619
+ * fetch. Network-panel features (live API → Store correlation in
1620
+ * `storeUtils.buildCorrelatedRequests`, `tanstackQueryTracker.updateQueryTracking`,
1621
+ * `fiberTreeWalker.scanFiberStateForOrigin`) rely on this gate to avoid
1622
+ * mis-attributing later mutations.
1623
+ *
1624
+ * Pass `{ ignoreTTL: true }` to bypass the gate. Used by the value-lineage
1625
+ * resolver, which wants the *causal* origin regardless of how long ago the
1626
+ * fetch resolved (the trace UI displays an "expired" hint for old origins).
1608
1627
  */
1609
- declare function findFetchOrigin(obj: unknown, depth?: number): string | undefined;
1628
+ declare function findFetchOrigin(obj: unknown, options?: {
1629
+ ignoreTTL?: boolean;
1630
+ }): string | undefined;
1610
1631
 
1611
1632
  /**
1612
1633
  * Next.js App Router client-side detection.
package/dist/index.d.ts CHANGED
@@ -48,6 +48,13 @@ interface RuntimeReadyMessage {
48
48
  /** React Native version from `Platform.constants.reactNativeVersion`, formatted
49
49
  * as "major.minor.patch". Native-only; web adapter leaves this undefined. */
50
50
  reactNativeVersion?: string;
51
+ /** Version of the @flotrace/runtime or @flotrace/runtime-native package the
52
+ * user installed in their app. Lets the desktop diagnose runtime/desktop
53
+ * drift (e.g., user pinned an older runtime that lacks a new feature).
54
+ * Read from the adapter's own package.json at build time. The lockstep
55
+ * release script keeps runtime-core pinned identically, so a separate
56
+ * core-version field would be redundant. */
57
+ runtimeVersion?: string;
51
58
  }
52
59
  interface RuntimeRenderMessage {
53
60
  type: 'runtime:render';
@@ -867,10 +874,14 @@ interface FloTraceConfig {
867
874
  /** React Native version from `Platform.constants.reactNativeVersion`, formatted
868
875
  * "major.minor.patch". Native adapter only. */
869
876
  reactNativeVersion?: string;
877
+ /** Version of the active runtime adapter (`@flotrace/runtime` on web,
878
+ * `@flotrace/runtime-native` on RN). Adapters auto-populate from their
879
+ * own package.json. Surfaced on `runtime:ready` for diagnostics. */
880
+ runtimeVersion?: string;
870
881
  }
871
882
  /** Keys that stay optional in DEFAULT_CONFIG. These are populated by adapters (web/native)
872
883
  * at call-time — the default object should not pretend to know a platform or LAN token. */
873
- type OptionalConfigKeys = 'getAppUrl' | 'platform' | 'appId' | 'appVersion' | 'host' | 'authToken' | 'userOnlyStrict' | 'userAllowPatterns' | 'frameworkName' | 'frameworkVersion' | 'reactNativeVersion';
884
+ type OptionalConfigKeys = 'getAppUrl' | 'platform' | 'appId' | 'appVersion' | 'host' | 'authToken' | 'userOnlyStrict' | 'userAllowPatterns' | 'frameworkName' | 'frameworkVersion' | 'reactNativeVersion' | 'runtimeVersion';
874
885
  type ResolvedFloTraceConfig = Required<Omit<FloTraceConfig, OptionalConfigKeys>> & Pick<FloTraceConfig, OptionalConfigKeys>;
875
886
  /**
876
887
  * Default configuration
@@ -1589,7 +1600,7 @@ declare function getChangedKeys(prev: Record<string, unknown> | undefined, next:
1589
1600
  * State lives here — in platform-agnostic core — so both the web and native
1590
1601
  * network trackers can write to the same registry that the analyzers read.
1591
1602
  */
1592
- /** Tag an object and its nested children (depth ≤ 2) with the requestId. */
1603
+ /** Tag an object and its nested children (depth ≤ FETCH_ORIGIN_SCAN_DEPTH) with the requestId. */
1593
1604
  declare function tagFetchData(obj: unknown, requestId: string, depth?: number): void;
1594
1605
  /** Returns true if any API request's response data is currently tagged (within TTL window). */
1595
1606
  declare function hasActiveTags(): boolean;
@@ -1600,13 +1611,23 @@ declare function hasActiveTags(): boolean;
1600
1611
  */
1601
1612
  declare function clearFetchOriginTags(): void;
1602
1613
  /**
1603
- * Scan an object (and nested children up to depth 2) for a WeakMap-tagged fetch origin.
1604
- * Called by Zustand/Redux trackers synchronously in their subscribe callbacks.
1605
- * Returns the requestId if this object was the result of a tracked fetch within the TTL
1606
- * window, else undefined. TTL prevents stale entries from matching on later store updates
1607
- * that reuse the same object references (immutable store pattern).
1614
+ * Scan an object (and nested children up to FETCH_ORIGIN_SCAN_DEPTH) for a
1615
+ * WeakMap-tagged fetch origin. Returns the requestId if this object was the
1616
+ * result of a tracked fetch, else undefined.
1617
+ *
1618
+ * By default the result must be within FETCH_ORIGIN_TTL_MS of the original
1619
+ * fetch. Network-panel features (live API → Store correlation in
1620
+ * `storeUtils.buildCorrelatedRequests`, `tanstackQueryTracker.updateQueryTracking`,
1621
+ * `fiberTreeWalker.scanFiberStateForOrigin`) rely on this gate to avoid
1622
+ * mis-attributing later mutations.
1623
+ *
1624
+ * Pass `{ ignoreTTL: true }` to bypass the gate. Used by the value-lineage
1625
+ * resolver, which wants the *causal* origin regardless of how long ago the
1626
+ * fetch resolved (the trace UI displays an "expired" hint for old origins).
1608
1627
  */
1609
- declare function findFetchOrigin(obj: unknown, depth?: number): string | undefined;
1628
+ declare function findFetchOrigin(obj: unknown, options?: {
1629
+ ignoreTTL?: boolean;
1630
+ }): string | undefined;
1610
1631
 
1611
1632
  /**
1612
1633
  * Next.js App Router client-side detection.
package/dist/index.js CHANGED
@@ -84,7 +84,13 @@ module.exports = __toCommonJS(index_exports);
84
84
  var DEFAULT_CONFIG = {
85
85
  port: 3457,
86
86
  appName: "React App",
87
- enabled: globalThis.process?.env?.NODE_ENV === "development",
87
+ // Default-on unless an explicit `process.env.NODE_ENV === 'production'` is
88
+ // detected. The previous heuristic (`=== 'development'`) silently disabled
89
+ // the runtime in any browser context that doesn't shim `process` (e.g. Vite,
90
+ // Webpack 5 with `node: false`, Rsbuild) — making the README quickstart fail
91
+ // for everyone. Production safety is handled by users gating the import via
92
+ // the dynamic-import pattern documented in the runtime READMEs.
93
+ enabled: globalThis.process?.env?.NODE_ENV !== "production",
88
94
  autoReconnect: true,
89
95
  reconnectInterval: 2e3,
90
96
  trackAllRenders: true,
@@ -309,7 +315,8 @@ var _FloTraceWebSocketClient = class _FloTraceWebSocketClient {
309
315
  appVersion: this.config.appVersion,
310
316
  frameworkName: this.config.frameworkName,
311
317
  frameworkVersion: this.config.frameworkVersion,
312
- reactNativeVersion: this.config.reactNativeVersion
318
+ reactNativeVersion: this.config.reactNativeVersion,
319
+ runtimeVersion: this.config.runtimeVersion
313
320
  });
314
321
  this.flush();
315
322
  };
@@ -1840,20 +1847,23 @@ function extractRoute(url) {
1840
1847
  var originalFetch = null;
1841
1848
  var interceptorClient = null;
1842
1849
  var isInstalled2 = false;
1850
+ var patchedFetchRef = null;
1843
1851
  function installRscPayloadInterceptor(client2) {
1844
1852
  if (isInstalled2 || typeof globalThis.fetch !== "function") return;
1845
1853
  isInstalled2 = true;
1846
1854
  interceptorClient = client2;
1847
1855
  originalFetch = globalThis.fetch;
1848
- globalThis.fetch = async function patchedFetch(input, init) {
1856
+ const capturedOriginalFetch = originalFetch;
1857
+ const capturedClient = client2;
1858
+ const patchedFetch = async function patchedFetch2(input, init) {
1849
1859
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1850
1860
  const isRscRequest = RSC_URL_PATTERNS.some((p) => p.test(url));
1851
- const response = await originalFetch.call(globalThis, input, init);
1852
- if (isRscRequest && interceptorClient?.connected) {
1861
+ const response = await capturedOriginalFetch.call(globalThis, input, init);
1862
+ if (isRscRequest && interceptorClient === capturedClient && capturedClient.connected) {
1853
1863
  try {
1854
1864
  const sizeHeader = response.headers.get("content-length");
1855
1865
  const payloadSizeBytes = sizeHeader ? parseInt(sizeHeader, 10) : 0;
1856
- interceptorClient.send({
1866
+ capturedClient.send({
1857
1867
  type: "runtime:rscPayload",
1858
1868
  route: extractRoute(url),
1859
1869
  payloadSizeBytes: isNaN(payloadSizeBytes) ? 0 : payloadSizeBytes,
@@ -1865,11 +1875,16 @@ function installRscPayloadInterceptor(client2) {
1865
1875
  }
1866
1876
  return response;
1867
1877
  };
1878
+ patchedFetchRef = patchedFetch;
1879
+ globalThis.fetch = patchedFetch;
1868
1880
  }
1869
1881
  function uninstallRscPayloadInterceptor() {
1870
1882
  if (!isInstalled2 || !originalFetch) return;
1871
- globalThis.fetch = originalFetch;
1883
+ if (globalThis.fetch === patchedFetchRef) {
1884
+ globalThis.fetch = originalFetch;
1885
+ }
1872
1886
  originalFetch = null;
1887
+ patchedFetchRef = null;
1873
1888
  interceptorClient = null;
1874
1889
  isInstalled2 = false;
1875
1890
  }
@@ -1878,12 +1893,16 @@ function uninstallRscPayloadInterceptor() {
1878
1893
  var fetchDataOrigin = /* @__PURE__ */ new WeakMap();
1879
1894
  var requestTagTimestamps = /* @__PURE__ */ new Map();
1880
1895
  var FETCH_ORIGIN_TTL_MS = 3e3;
1896
+ var FETCH_ORIGIN_SCAN_DEPTH = 4;
1897
+ var FETCH_ORIGIN_TAG_ARRAY_LIMIT = 50;
1898
+ var FETCH_ORIGIN_SCAN_ARRAY_LIMIT = 20;
1881
1899
  function tagFetchData(obj, requestId, depth = 0) {
1882
- if (depth > 2 || obj === null || typeof obj !== "object") return;
1900
+ if (depth > FETCH_ORIGIN_SCAN_DEPTH || obj === null || typeof obj !== "object") return;
1883
1901
  fetchDataOrigin.set(obj, requestId);
1884
1902
  if (depth === 0) requestTagTimestamps.set(requestId, Date.now());
1885
1903
  if (Array.isArray(obj)) {
1886
- for (let i = 0; i < Math.min(obj.length, 50); i++) tagFetchData(obj[i], requestId, depth + 1);
1904
+ const limit = Math.min(obj.length, FETCH_ORIGIN_TAG_ARRAY_LIMIT);
1905
+ for (let i = 0; i < limit; i++) tagFetchData(obj[i], requestId, depth + 1);
1887
1906
  } else {
1888
1907
  for (const val of Object.values(obj)) tagFetchData(val, requestId, depth + 1);
1889
1908
  }
@@ -1894,24 +1913,29 @@ function hasActiveTags() {
1894
1913
  function clearFetchOriginTags() {
1895
1914
  requestTagTimestamps.clear();
1896
1915
  }
1897
- function findFetchOrigin(obj, depth = 0) {
1898
- if (depth > 2 || obj === null || typeof obj !== "object") return void 0;
1916
+ function findFetchOrigin(obj, options) {
1917
+ return scanForOrigin(obj, 0, options?.ignoreTTL === true);
1918
+ }
1919
+ function scanForOrigin(obj, depth, ignoreTTL) {
1920
+ if (depth > FETCH_ORIGIN_SCAN_DEPTH || obj === null || typeof obj !== "object") return void 0;
1899
1921
  const rid = fetchDataOrigin.get(obj);
1900
1922
  if (rid) {
1923
+ if (ignoreTTL) return rid;
1901
1924
  const tagTime = requestTagTimestamps.get(rid);
1902
1925
  if (tagTime && Date.now() - tagTime <= FETCH_ORIGIN_TTL_MS) return rid;
1903
1926
  requestTagTimestamps.delete(rid);
1904
1927
  }
1905
1928
  if (Array.isArray(obj)) {
1906
- for (let i = 0; i < Math.min(obj.length, 20); i++) {
1907
- const found = findFetchOrigin(obj[i], depth + 1);
1908
- if (found) return found;
1909
- }
1910
- } else {
1911
- for (const val of Object.values(obj)) {
1912
- const found = findFetchOrigin(val, depth + 1);
1929
+ const limit = Math.min(obj.length, FETCH_ORIGIN_SCAN_ARRAY_LIMIT);
1930
+ for (let i = 0; i < limit; i++) {
1931
+ const found = scanForOrigin(obj[i], depth + 1, ignoreTTL);
1913
1932
  if (found) return found;
1914
1933
  }
1934
+ return void 0;
1935
+ }
1936
+ for (const val of Object.values(obj)) {
1937
+ const found = scanForOrigin(val, depth + 1, ignoreTTL);
1938
+ if (found) return found;
1915
1939
  }
1916
1940
  return void 0;
1917
1941
  }
@@ -3706,7 +3730,7 @@ function safeCall(fn, fallback) {
3706
3730
 
3707
3731
  // src/valueTraceResolver.ts
3708
3732
  var FIBER_TAG_CONTEXT_PROVIDER = 10;
3709
- var BUDGET_MS = 50;
3733
+ var BUDGET_MS = 100;
3710
3734
  var SCAN_DEPTH = 3;
3711
3735
  var MAX_PROP_CHAIN_DEPTH = 30;
3712
3736
  function now() {
@@ -3730,34 +3754,49 @@ function getHookValueAt(fiber, hookIndex) {
3730
3754
  if (!hook) return void 0;
3731
3755
  return hook.memoizedState;
3732
3756
  }
3733
- function valuesMatch(target, targetFp, candidate) {
3757
+ function cachedFp(value, cache) {
3758
+ if (value === null || typeof value !== "object") return valueFingerprint(value);
3759
+ const cached = cache.get(value);
3760
+ if (cached !== void 0) return cached;
3761
+ const fp = valueFingerprint(value);
3762
+ cache.set(value, fp);
3763
+ return fp;
3764
+ }
3765
+ function valuesMatch(target, targetFp, candidate, cache) {
3734
3766
  const targetIsObject = target !== null && typeof target === "object";
3735
3767
  const candidateIsObject = candidate !== null && typeof candidate === "object";
3736
3768
  if (targetIsObject && candidateIsObject && target === candidate) return "exact";
3737
3769
  if (!shouldFlagRename(target) || !shouldFlagRename(candidate)) return null;
3738
- if (valueFingerprint(candidate) === targetFp) return "fingerprint-match";
3770
+ if (cachedFp(candidate, cache) === targetFp) return "fingerprint-match";
3739
3771
  return null;
3740
3772
  }
3741
- function findMatchingPathInObject(target, targetFp, container, currentPath, depth, deadline) {
3773
+ function findReferenceMatchAtTopLevel(target, container) {
3774
+ if (target === null || typeof target !== "object") return null;
3775
+ for (const key of Object.keys(container)) {
3776
+ if (container[key] === target) return key;
3777
+ }
3778
+ return null;
3779
+ }
3780
+ function findMatchingPathInObject(target, targetFp, container, currentPath, depth, deadline, cache) {
3742
3781
  if (now() > deadline) return null;
3743
3782
  if (depth > SCAN_DEPTH) return null;
3744
3783
  if (container === null || typeof container !== "object") return null;
3745
- const selfMatch = valuesMatch(target, targetFp, container);
3784
+ const selfMatch = valuesMatch(target, targetFp, container, cache);
3746
3785
  if (selfMatch) return { path: [...currentPath], confidence: selfMatch };
3747
3786
  if (Array.isArray(container)) {
3748
3787
  for (let i = 0; i < Math.min(container.length, 50); i++) {
3749
3788
  const child = container[i];
3750
- const directMatch = valuesMatch(target, targetFp, child);
3789
+ const directMatch = valuesMatch(target, targetFp, child, cache);
3751
3790
  if (directMatch) return { path: [...currentPath, String(i)], confidence: directMatch };
3752
- const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline);
3791
+ const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline, cache);
3753
3792
  if (nested) return nested;
3754
3793
  }
3755
3794
  } else {
3756
3795
  for (const key of Object.keys(container)) {
3757
3796
  const child = container[key];
3758
- const directMatch = valuesMatch(target, targetFp, child);
3797
+ const directMatch = valuesMatch(target, targetFp, child, cache);
3759
3798
  if (directMatch) return { path: [...currentPath, key], confidence: directMatch };
3760
- const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline);
3799
+ const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline, cache);
3761
3800
  if (nested) return nested;
3762
3801
  }
3763
3802
  }
@@ -3770,6 +3809,59 @@ function buildFiberToNodeIdMap() {
3770
3809
  }
3771
3810
  return reverse;
3772
3811
  }
3812
+ function retreatToEnclosingObject(fiber, propPath, rootValue) {
3813
+ const isObject = rootValue !== null && typeof rootValue === "object";
3814
+ if (isObject || propPath.length <= 1 || !fiber.memoizedProps) {
3815
+ return { rootValue, trailingSubPath: [] };
3816
+ }
3817
+ let path = propPath.slice();
3818
+ let value = rootValue;
3819
+ const trail = [];
3820
+ while (path.length > 1 && (value === null || typeof value !== "object")) {
3821
+ trail.unshift(path[path.length - 1]);
3822
+ path = path.slice(0, -1);
3823
+ value = walkPath(fiber.memoizedProps, path);
3824
+ }
3825
+ if (value === null || typeof value !== "object") {
3826
+ return { rootValue, trailingSubPath: [] };
3827
+ }
3828
+ return { rootValue: value, trailingSubPath: trail };
3829
+ }
3830
+ function isDebugEnabled() {
3831
+ try {
3832
+ return !!globalThis.__FLOTRACE_DEBUG__;
3833
+ } catch {
3834
+ return false;
3835
+ }
3836
+ }
3837
+ function findFetchOriginUpKeyPath(stateRoot, keyPath) {
3838
+ const ancestors = [stateRoot];
3839
+ let cursor = stateRoot;
3840
+ for (const segment of keyPath) {
3841
+ if (cursor === null || typeof cursor !== "object") break;
3842
+ cursor = cursor[segment];
3843
+ ancestors.push(cursor);
3844
+ }
3845
+ for (let i = ancestors.length - 1; i >= 0; i--) {
3846
+ const value = ancestors[i];
3847
+ if (value === null || typeof value !== "object") continue;
3848
+ const rid = findFetchOrigin(value, { ignoreTTL: true });
3849
+ if (rid) {
3850
+ if (isDebugEnabled()) {
3851
+ console.debug("[FloTrace] origin via keyPath retreat", {
3852
+ keyPath,
3853
+ depthHit: i,
3854
+ requestId: rid
3855
+ });
3856
+ }
3857
+ return rid;
3858
+ }
3859
+ }
3860
+ return void 0;
3861
+ }
3862
+ function resolveOriginViaTagOrKeyPath(matchedValue, stateRoot, keyPath) {
3863
+ return findFetchOrigin(matchedValue, { ignoreTTL: true }) ?? findFetchOriginUpKeyPath(stateRoot, keyPath);
3864
+ }
3773
3865
  function resolveValueTrace(input) {
3774
3866
  const startedAt = now();
3775
3867
  const deadline = startedAt + BUDGET_MS;
@@ -3798,9 +3890,13 @@ function resolveValueTrace(input) {
3798
3890
  if (rootValue === void 0) {
3799
3891
  return { ...base, error: "value-not-found", resolvedAtMs: now() };
3800
3892
  }
3893
+ const retreated = input.propPath ? retreatToEnclosingObject(fiber, input.propPath, rootValue) : { rootValue, trailingSubPath: [] };
3894
+ rootValue = retreated.rootValue;
3895
+ const trailingSubPath = retreated.trailingSubPath;
3801
3896
  const rootFp = valueFingerprint(rootValue);
3802
3897
  const fiberToNodeId = buildFiberToNodeIdMap();
3803
3898
  const rootComponentName = getComponentNameFromFiber(fiber) ?? "Unknown";
3899
+ const fpCache = /* @__PURE__ */ new WeakMap();
3804
3900
  if (input.propPath) {
3805
3901
  steps.push({
3806
3902
  kind: "prop",
@@ -3829,8 +3925,17 @@ function resolveValueTrace(input) {
3829
3925
  if (current.tag !== FIBER_TAG_CONTEXT_PROVIDER) {
3830
3926
  const props = current.memoizedProps;
3831
3927
  if (props) {
3832
- const match = findMatchingPathInObject(rootValue, rootFp, props, [], 0, deadline);
3833
- if (match) {
3928
+ const refKey = findReferenceMatchAtTopLevel(rootValue, props);
3929
+ let matchPath = refKey !== null ? [refKey] : null;
3930
+ let matchConfidence = "exact";
3931
+ if (matchPath === null) {
3932
+ const match = findMatchingPathInObject(rootValue, rootFp, props, [], 0, deadline, fpCache);
3933
+ if (match) {
3934
+ matchPath = match.path;
3935
+ matchConfidence = match.confidence;
3936
+ }
3937
+ }
3938
+ if (matchPath !== null) {
3834
3939
  const ancestorNodeId = fiberToNodeId.get(current);
3835
3940
  const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
3836
3941
  if (ancestorNodeId) {
@@ -3838,10 +3943,28 @@ function resolveValueTrace(input) {
3838
3943
  kind: "prop",
3839
3944
  nodeId: ancestorNodeId,
3840
3945
  componentName: ancestorName,
3841
- propPath: match.path,
3842
- confidence: match.confidence
3946
+ propPath: trailingSubPath.length > 0 ? [...matchPath, ...trailingSubPath] : matchPath,
3947
+ confidence: matchConfidence
3843
3948
  });
3844
3949
  }
3950
+ } else {
3951
+ const hookMatch = findMatchingHookState(current, rootValue, rootFp, fpCache);
3952
+ if (hookMatch) {
3953
+ const ancestorNodeId = fiberToNodeId.get(current);
3954
+ const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
3955
+ if (ancestorNodeId) {
3956
+ steps.push({
3957
+ kind: "hook-state",
3958
+ nodeId: ancestorNodeId,
3959
+ componentName: ancestorName,
3960
+ hookIndex: hookMatch.hookIndex,
3961
+ hookType: hookMatch.hookType,
3962
+ subPath: trailingSubPath.length > 0 ? trailingSubPath : void 0,
3963
+ confidence: hookMatch.confidence
3964
+ });
3965
+ return { ...base, steps, resolvedAtMs: now() };
3966
+ }
3967
+ }
3845
3968
  }
3846
3969
  }
3847
3970
  }
@@ -3850,7 +3973,7 @@ function resolveValueTrace(input) {
3850
3973
  }
3851
3974
  }
3852
3975
  if (input.hookPath) {
3853
- const origin = findFetchOrigin(rootValue);
3976
+ const origin = findFetchOrigin(rootValue, { ignoreTTL: true });
3854
3977
  if (origin) {
3855
3978
  steps.push({
3856
3979
  kind: "api",
@@ -3862,15 +3985,15 @@ function resolveValueTrace(input) {
3862
3985
  return { ...base, steps, resolvedAtMs: now() };
3863
3986
  }
3864
3987
  }
3865
- const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName);
3988
+ const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName, fpCache);
3866
3989
  if (derivedMatch) {
3867
3990
  steps.push({ ...derivedMatch, nodeId: input.nodeId });
3868
3991
  return { ...base, steps, resolvedAtMs: now() };
3869
3992
  }
3870
- const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId);
3993
+ const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId, fpCache);
3871
3994
  if (contextMatch) {
3872
3995
  steps.push(contextMatch.step);
3873
- const providerStoreMatch = findStoreMatch(contextMatch.providerValue, valueFingerprint(contextMatch.providerValue), deadline);
3996
+ const providerStoreMatch = findStoreMatch(contextMatch.providerValue, cachedFp(contextMatch.providerValue, fpCache), deadline, fpCache);
3874
3997
  if (providerStoreMatch) {
3875
3998
  steps.push({
3876
3999
  kind: "store",
@@ -3879,19 +4002,23 @@ function resolveValueTrace(input) {
3879
4002
  keyPath: providerStoreMatch.keyPath,
3880
4003
  confidence: providerStoreMatch.confidence
3881
4004
  });
3882
- const origin = findFetchOrigin(providerStoreMatch.matchedValue);
4005
+ const origin = resolveOriginViaTagOrKeyPath(
4006
+ providerStoreMatch.matchedValue,
4007
+ providerStoreMatch.stateRoot,
4008
+ providerStoreMatch.keyPath
4009
+ );
3883
4010
  if (origin) {
3884
4011
  steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
3885
4012
  }
3886
4013
  } else {
3887
- const origin = findFetchOrigin(contextMatch.providerValue);
4014
+ const origin = findFetchOrigin(contextMatch.providerValue, { ignoreTTL: true });
3888
4015
  if (origin) {
3889
4016
  steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
3890
4017
  }
3891
4018
  }
3892
4019
  return { ...base, steps, resolvedAtMs: now() };
3893
4020
  }
3894
- const storeMatch = findStoreMatch(rootValue, rootFp, deadline);
4021
+ const storeMatch = findStoreMatch(rootValue, rootFp, deadline, fpCache);
3895
4022
  if (storeMatch) {
3896
4023
  steps.push({
3897
4024
  kind: "store",
@@ -3900,7 +4027,11 @@ function resolveValueTrace(input) {
3900
4027
  keyPath: storeMatch.keyPath,
3901
4028
  confidence: storeMatch.confidence
3902
4029
  });
3903
- const origin = findFetchOrigin(storeMatch.matchedValue);
4030
+ const origin = resolveOriginViaTagOrKeyPath(
4031
+ storeMatch.matchedValue,
4032
+ storeMatch.stateRoot,
4033
+ storeMatch.keyPath
4034
+ );
3904
4035
  if (origin) {
3905
4036
  steps.push({
3906
4037
  kind: "api",
@@ -3913,12 +4044,12 @@ function resolveValueTrace(input) {
3913
4044
  }
3914
4045
  return { ...base, steps, resolvedAtMs: now() };
3915
4046
  }
3916
- function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
4047
+ function findContextMatch(consumer, target, targetFp, fiberToNodeId, cache) {
3917
4048
  const deps = consumer.dependencies?.firstContext;
3918
4049
  if (!deps) return null;
3919
4050
  let dep = deps;
3920
4051
  while (dep) {
3921
- const match = valuesMatch(target, targetFp, dep.memoizedValue);
4052
+ const match = valuesMatch(target, targetFp, dep.memoizedValue, cache);
3922
4053
  if (match) {
3923
4054
  const provider = findNearestProvider(consumer, dep.context);
3924
4055
  const step = {
@@ -3934,14 +4065,14 @@ function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
3934
4065
  }
3935
4066
  return null;
3936
4067
  }
3937
- function findDerivationMatch(fiber, target, targetFp, componentName) {
4068
+ function findDerivationMatch(fiber, target, targetFp, componentName, cache) {
3938
4069
  let hook = fiber.memoizedState;
3939
4070
  let index = 0;
3940
4071
  while (hook) {
3941
4072
  const ms = hook.memoizedState;
3942
4073
  if (Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1])) {
3943
4074
  const [computed, deps] = ms;
3944
- const match = valuesMatch(target, targetFp, computed);
4075
+ const match = valuesMatch(target, targetFp, computed, cache);
3945
4076
  if (match) {
3946
4077
  const hookType = typeof computed === "function" ? "useCallback" : "useMemo";
3947
4078
  return {
@@ -3960,6 +4091,25 @@ function findDerivationMatch(fiber, target, targetFp, componentName) {
3960
4091
  }
3961
4092
  return null;
3962
4093
  }
4094
+ function findMatchingHookState(fiber, target, targetFp, cache) {
4095
+ let hook = fiber.memoizedState;
4096
+ let index = 0;
4097
+ while (hook) {
4098
+ const ms = hook.memoizedState;
4099
+ const isMemoTuple = Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1]);
4100
+ const isEffectShape2 = ms !== null && typeof ms === "object" && "create" in ms && "deps" in ms;
4101
+ const isRefShape = ms !== null && typeof ms === "object" && Object.keys(ms).length === 1 && "current" in ms;
4102
+ if (!isMemoTuple && !isEffectShape2 && !isRefShape) {
4103
+ const match = valuesMatch(target, targetFp, ms, cache);
4104
+ if (match) {
4105
+ return { hookIndex: index, hookType: "useState", confidence: match };
4106
+ }
4107
+ }
4108
+ hook = hook.next;
4109
+ index++;
4110
+ }
4111
+ return null;
4112
+ }
3963
4113
  function findNearestProvider(consumer, contextObj) {
3964
4114
  let current = consumer.return;
3965
4115
  let hops = 0;
@@ -3973,44 +4123,47 @@ function findNearestProvider(consumer, contextObj) {
3973
4123
  }
3974
4124
  return null;
3975
4125
  }
3976
- function findStoreMatch(target, targetFp, deadline) {
4126
+ function findStoreMatch(target, targetFp, deadline, cache) {
3977
4127
  for (const [storeName, state] of getZustandSnapshot()) {
3978
4128
  if (now() > deadline) return null;
3979
- const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline);
4129
+ const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline, cache);
3980
4130
  if (hit) {
3981
4131
  return {
3982
4132
  source: "zustand",
3983
4133
  storeName,
3984
4134
  keyPath: hit.path,
3985
4135
  confidence: hit.confidence,
3986
- matchedValue: walkPath(state, hit.path)
4136
+ matchedValue: walkPath(state, hit.path),
4137
+ stateRoot: state
3987
4138
  };
3988
4139
  }
3989
4140
  }
3990
4141
  const redux = getReduxSnapshot();
3991
4142
  if (redux) {
3992
4143
  if (now() > deadline) return null;
3993
- const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline);
4144
+ const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline, cache);
3994
4145
  if (hit) {
3995
4146
  return {
3996
4147
  source: "redux",
3997
4148
  storeName: "redux",
3998
4149
  keyPath: hit.path,
3999
4150
  confidence: hit.confidence,
4000
- matchedValue: walkPath(redux, hit.path)
4151
+ matchedValue: walkPath(redux, hit.path),
4152
+ stateRoot: redux
4001
4153
  };
4002
4154
  }
4003
4155
  }
4004
4156
  for (const [queryHash, entry] of getTanstackSnapshot()) {
4005
4157
  if (now() > deadline) return null;
4006
- const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline);
4158
+ const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline, cache);
4007
4159
  if (hit) {
4008
4160
  return {
4009
4161
  source: "tanstack-query",
4010
4162
  storeName: queryHash,
4011
4163
  keyPath: hit.path,
4012
4164
  confidence: hit.confidence,
4013
- matchedValue: walkPath(entry.data, hit.path)
4165
+ matchedValue: walkPath(entry.data, hit.path),
4166
+ stateRoot: entry.data
4014
4167
  };
4015
4168
  }
4016
4169
  }
package/dist/index.mjs CHANGED
@@ -2,7 +2,13 @@
2
2
  var DEFAULT_CONFIG = {
3
3
  port: 3457,
4
4
  appName: "React App",
5
- enabled: globalThis.process?.env?.NODE_ENV === "development",
5
+ // Default-on unless an explicit `process.env.NODE_ENV === 'production'` is
6
+ // detected. The previous heuristic (`=== 'development'`) silently disabled
7
+ // the runtime in any browser context that doesn't shim `process` (e.g. Vite,
8
+ // Webpack 5 with `node: false`, Rsbuild) — making the README quickstart fail
9
+ // for everyone. Production safety is handled by users gating the import via
10
+ // the dynamic-import pattern documented in the runtime READMEs.
11
+ enabled: globalThis.process?.env?.NODE_ENV !== "production",
6
12
  autoReconnect: true,
7
13
  reconnectInterval: 2e3,
8
14
  trackAllRenders: true,
@@ -227,7 +233,8 @@ var _FloTraceWebSocketClient = class _FloTraceWebSocketClient {
227
233
  appVersion: this.config.appVersion,
228
234
  frameworkName: this.config.frameworkName,
229
235
  frameworkVersion: this.config.frameworkVersion,
230
- reactNativeVersion: this.config.reactNativeVersion
236
+ reactNativeVersion: this.config.reactNativeVersion,
237
+ runtimeVersion: this.config.runtimeVersion
231
238
  });
232
239
  this.flush();
233
240
  };
@@ -1758,20 +1765,23 @@ function extractRoute(url) {
1758
1765
  var originalFetch = null;
1759
1766
  var interceptorClient = null;
1760
1767
  var isInstalled2 = false;
1768
+ var patchedFetchRef = null;
1761
1769
  function installRscPayloadInterceptor(client2) {
1762
1770
  if (isInstalled2 || typeof globalThis.fetch !== "function") return;
1763
1771
  isInstalled2 = true;
1764
1772
  interceptorClient = client2;
1765
1773
  originalFetch = globalThis.fetch;
1766
- globalThis.fetch = async function patchedFetch(input, init) {
1774
+ const capturedOriginalFetch = originalFetch;
1775
+ const capturedClient = client2;
1776
+ const patchedFetch = async function patchedFetch2(input, init) {
1767
1777
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1768
1778
  const isRscRequest = RSC_URL_PATTERNS.some((p) => p.test(url));
1769
- const response = await originalFetch.call(globalThis, input, init);
1770
- if (isRscRequest && interceptorClient?.connected) {
1779
+ const response = await capturedOriginalFetch.call(globalThis, input, init);
1780
+ if (isRscRequest && interceptorClient === capturedClient && capturedClient.connected) {
1771
1781
  try {
1772
1782
  const sizeHeader = response.headers.get("content-length");
1773
1783
  const payloadSizeBytes = sizeHeader ? parseInt(sizeHeader, 10) : 0;
1774
- interceptorClient.send({
1784
+ capturedClient.send({
1775
1785
  type: "runtime:rscPayload",
1776
1786
  route: extractRoute(url),
1777
1787
  payloadSizeBytes: isNaN(payloadSizeBytes) ? 0 : payloadSizeBytes,
@@ -1783,11 +1793,16 @@ function installRscPayloadInterceptor(client2) {
1783
1793
  }
1784
1794
  return response;
1785
1795
  };
1796
+ patchedFetchRef = patchedFetch;
1797
+ globalThis.fetch = patchedFetch;
1786
1798
  }
1787
1799
  function uninstallRscPayloadInterceptor() {
1788
1800
  if (!isInstalled2 || !originalFetch) return;
1789
- globalThis.fetch = originalFetch;
1801
+ if (globalThis.fetch === patchedFetchRef) {
1802
+ globalThis.fetch = originalFetch;
1803
+ }
1790
1804
  originalFetch = null;
1805
+ patchedFetchRef = null;
1791
1806
  interceptorClient = null;
1792
1807
  isInstalled2 = false;
1793
1808
  }
@@ -1796,12 +1811,16 @@ function uninstallRscPayloadInterceptor() {
1796
1811
  var fetchDataOrigin = /* @__PURE__ */ new WeakMap();
1797
1812
  var requestTagTimestamps = /* @__PURE__ */ new Map();
1798
1813
  var FETCH_ORIGIN_TTL_MS = 3e3;
1814
+ var FETCH_ORIGIN_SCAN_DEPTH = 4;
1815
+ var FETCH_ORIGIN_TAG_ARRAY_LIMIT = 50;
1816
+ var FETCH_ORIGIN_SCAN_ARRAY_LIMIT = 20;
1799
1817
  function tagFetchData(obj, requestId, depth = 0) {
1800
- if (depth > 2 || obj === null || typeof obj !== "object") return;
1818
+ if (depth > FETCH_ORIGIN_SCAN_DEPTH || obj === null || typeof obj !== "object") return;
1801
1819
  fetchDataOrigin.set(obj, requestId);
1802
1820
  if (depth === 0) requestTagTimestamps.set(requestId, Date.now());
1803
1821
  if (Array.isArray(obj)) {
1804
- for (let i = 0; i < Math.min(obj.length, 50); i++) tagFetchData(obj[i], requestId, depth + 1);
1822
+ const limit = Math.min(obj.length, FETCH_ORIGIN_TAG_ARRAY_LIMIT);
1823
+ for (let i = 0; i < limit; i++) tagFetchData(obj[i], requestId, depth + 1);
1805
1824
  } else {
1806
1825
  for (const val of Object.values(obj)) tagFetchData(val, requestId, depth + 1);
1807
1826
  }
@@ -1812,24 +1831,29 @@ function hasActiveTags() {
1812
1831
  function clearFetchOriginTags() {
1813
1832
  requestTagTimestamps.clear();
1814
1833
  }
1815
- function findFetchOrigin(obj, depth = 0) {
1816
- if (depth > 2 || obj === null || typeof obj !== "object") return void 0;
1834
+ function findFetchOrigin(obj, options) {
1835
+ return scanForOrigin(obj, 0, options?.ignoreTTL === true);
1836
+ }
1837
+ function scanForOrigin(obj, depth, ignoreTTL) {
1838
+ if (depth > FETCH_ORIGIN_SCAN_DEPTH || obj === null || typeof obj !== "object") return void 0;
1817
1839
  const rid = fetchDataOrigin.get(obj);
1818
1840
  if (rid) {
1841
+ if (ignoreTTL) return rid;
1819
1842
  const tagTime = requestTagTimestamps.get(rid);
1820
1843
  if (tagTime && Date.now() - tagTime <= FETCH_ORIGIN_TTL_MS) return rid;
1821
1844
  requestTagTimestamps.delete(rid);
1822
1845
  }
1823
1846
  if (Array.isArray(obj)) {
1824
- for (let i = 0; i < Math.min(obj.length, 20); i++) {
1825
- const found = findFetchOrigin(obj[i], depth + 1);
1826
- if (found) return found;
1827
- }
1828
- } else {
1829
- for (const val of Object.values(obj)) {
1830
- const found = findFetchOrigin(val, depth + 1);
1847
+ const limit = Math.min(obj.length, FETCH_ORIGIN_SCAN_ARRAY_LIMIT);
1848
+ for (let i = 0; i < limit; i++) {
1849
+ const found = scanForOrigin(obj[i], depth + 1, ignoreTTL);
1831
1850
  if (found) return found;
1832
1851
  }
1852
+ return void 0;
1853
+ }
1854
+ for (const val of Object.values(obj)) {
1855
+ const found = scanForOrigin(val, depth + 1, ignoreTTL);
1856
+ if (found) return found;
1833
1857
  }
1834
1858
  return void 0;
1835
1859
  }
@@ -3624,7 +3648,7 @@ function safeCall(fn, fallback) {
3624
3648
 
3625
3649
  // src/valueTraceResolver.ts
3626
3650
  var FIBER_TAG_CONTEXT_PROVIDER = 10;
3627
- var BUDGET_MS = 50;
3651
+ var BUDGET_MS = 100;
3628
3652
  var SCAN_DEPTH = 3;
3629
3653
  var MAX_PROP_CHAIN_DEPTH = 30;
3630
3654
  function now() {
@@ -3648,34 +3672,49 @@ function getHookValueAt(fiber, hookIndex) {
3648
3672
  if (!hook) return void 0;
3649
3673
  return hook.memoizedState;
3650
3674
  }
3651
- function valuesMatch(target, targetFp, candidate) {
3675
+ function cachedFp(value, cache) {
3676
+ if (value === null || typeof value !== "object") return valueFingerprint(value);
3677
+ const cached = cache.get(value);
3678
+ if (cached !== void 0) return cached;
3679
+ const fp = valueFingerprint(value);
3680
+ cache.set(value, fp);
3681
+ return fp;
3682
+ }
3683
+ function valuesMatch(target, targetFp, candidate, cache) {
3652
3684
  const targetIsObject = target !== null && typeof target === "object";
3653
3685
  const candidateIsObject = candidate !== null && typeof candidate === "object";
3654
3686
  if (targetIsObject && candidateIsObject && target === candidate) return "exact";
3655
3687
  if (!shouldFlagRename(target) || !shouldFlagRename(candidate)) return null;
3656
- if (valueFingerprint(candidate) === targetFp) return "fingerprint-match";
3688
+ if (cachedFp(candidate, cache) === targetFp) return "fingerprint-match";
3657
3689
  return null;
3658
3690
  }
3659
- function findMatchingPathInObject(target, targetFp, container, currentPath, depth, deadline) {
3691
+ function findReferenceMatchAtTopLevel(target, container) {
3692
+ if (target === null || typeof target !== "object") return null;
3693
+ for (const key of Object.keys(container)) {
3694
+ if (container[key] === target) return key;
3695
+ }
3696
+ return null;
3697
+ }
3698
+ function findMatchingPathInObject(target, targetFp, container, currentPath, depth, deadline, cache) {
3660
3699
  if (now() > deadline) return null;
3661
3700
  if (depth > SCAN_DEPTH) return null;
3662
3701
  if (container === null || typeof container !== "object") return null;
3663
- const selfMatch = valuesMatch(target, targetFp, container);
3702
+ const selfMatch = valuesMatch(target, targetFp, container, cache);
3664
3703
  if (selfMatch) return { path: [...currentPath], confidence: selfMatch };
3665
3704
  if (Array.isArray(container)) {
3666
3705
  for (let i = 0; i < Math.min(container.length, 50); i++) {
3667
3706
  const child = container[i];
3668
- const directMatch = valuesMatch(target, targetFp, child);
3707
+ const directMatch = valuesMatch(target, targetFp, child, cache);
3669
3708
  if (directMatch) return { path: [...currentPath, String(i)], confidence: directMatch };
3670
- const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline);
3709
+ const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, String(i)], depth + 1, deadline, cache);
3671
3710
  if (nested) return nested;
3672
3711
  }
3673
3712
  } else {
3674
3713
  for (const key of Object.keys(container)) {
3675
3714
  const child = container[key];
3676
- const directMatch = valuesMatch(target, targetFp, child);
3715
+ const directMatch = valuesMatch(target, targetFp, child, cache);
3677
3716
  if (directMatch) return { path: [...currentPath, key], confidence: directMatch };
3678
- const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline);
3717
+ const nested = findMatchingPathInObject(target, targetFp, child, [...currentPath, key], depth + 1, deadline, cache);
3679
3718
  if (nested) return nested;
3680
3719
  }
3681
3720
  }
@@ -3688,6 +3727,59 @@ function buildFiberToNodeIdMap() {
3688
3727
  }
3689
3728
  return reverse;
3690
3729
  }
3730
+ function retreatToEnclosingObject(fiber, propPath, rootValue) {
3731
+ const isObject = rootValue !== null && typeof rootValue === "object";
3732
+ if (isObject || propPath.length <= 1 || !fiber.memoizedProps) {
3733
+ return { rootValue, trailingSubPath: [] };
3734
+ }
3735
+ let path = propPath.slice();
3736
+ let value = rootValue;
3737
+ const trail = [];
3738
+ while (path.length > 1 && (value === null || typeof value !== "object")) {
3739
+ trail.unshift(path[path.length - 1]);
3740
+ path = path.slice(0, -1);
3741
+ value = walkPath(fiber.memoizedProps, path);
3742
+ }
3743
+ if (value === null || typeof value !== "object") {
3744
+ return { rootValue, trailingSubPath: [] };
3745
+ }
3746
+ return { rootValue: value, trailingSubPath: trail };
3747
+ }
3748
+ function isDebugEnabled() {
3749
+ try {
3750
+ return !!globalThis.__FLOTRACE_DEBUG__;
3751
+ } catch {
3752
+ return false;
3753
+ }
3754
+ }
3755
+ function findFetchOriginUpKeyPath(stateRoot, keyPath) {
3756
+ const ancestors = [stateRoot];
3757
+ let cursor = stateRoot;
3758
+ for (const segment of keyPath) {
3759
+ if (cursor === null || typeof cursor !== "object") break;
3760
+ cursor = cursor[segment];
3761
+ ancestors.push(cursor);
3762
+ }
3763
+ for (let i = ancestors.length - 1; i >= 0; i--) {
3764
+ const value = ancestors[i];
3765
+ if (value === null || typeof value !== "object") continue;
3766
+ const rid = findFetchOrigin(value, { ignoreTTL: true });
3767
+ if (rid) {
3768
+ if (isDebugEnabled()) {
3769
+ console.debug("[FloTrace] origin via keyPath retreat", {
3770
+ keyPath,
3771
+ depthHit: i,
3772
+ requestId: rid
3773
+ });
3774
+ }
3775
+ return rid;
3776
+ }
3777
+ }
3778
+ return void 0;
3779
+ }
3780
+ function resolveOriginViaTagOrKeyPath(matchedValue, stateRoot, keyPath) {
3781
+ return findFetchOrigin(matchedValue, { ignoreTTL: true }) ?? findFetchOriginUpKeyPath(stateRoot, keyPath);
3782
+ }
3691
3783
  function resolveValueTrace(input) {
3692
3784
  const startedAt = now();
3693
3785
  const deadline = startedAt + BUDGET_MS;
@@ -3716,9 +3808,13 @@ function resolveValueTrace(input) {
3716
3808
  if (rootValue === void 0) {
3717
3809
  return { ...base, error: "value-not-found", resolvedAtMs: now() };
3718
3810
  }
3811
+ const retreated = input.propPath ? retreatToEnclosingObject(fiber, input.propPath, rootValue) : { rootValue, trailingSubPath: [] };
3812
+ rootValue = retreated.rootValue;
3813
+ const trailingSubPath = retreated.trailingSubPath;
3719
3814
  const rootFp = valueFingerprint(rootValue);
3720
3815
  const fiberToNodeId = buildFiberToNodeIdMap();
3721
3816
  const rootComponentName = getComponentNameFromFiber(fiber) ?? "Unknown";
3817
+ const fpCache = /* @__PURE__ */ new WeakMap();
3722
3818
  if (input.propPath) {
3723
3819
  steps.push({
3724
3820
  kind: "prop",
@@ -3747,8 +3843,17 @@ function resolveValueTrace(input) {
3747
3843
  if (current.tag !== FIBER_TAG_CONTEXT_PROVIDER) {
3748
3844
  const props = current.memoizedProps;
3749
3845
  if (props) {
3750
- const match = findMatchingPathInObject(rootValue, rootFp, props, [], 0, deadline);
3751
- if (match) {
3846
+ const refKey = findReferenceMatchAtTopLevel(rootValue, props);
3847
+ let matchPath = refKey !== null ? [refKey] : null;
3848
+ let matchConfidence = "exact";
3849
+ if (matchPath === null) {
3850
+ const match = findMatchingPathInObject(rootValue, rootFp, props, [], 0, deadline, fpCache);
3851
+ if (match) {
3852
+ matchPath = match.path;
3853
+ matchConfidence = match.confidence;
3854
+ }
3855
+ }
3856
+ if (matchPath !== null) {
3752
3857
  const ancestorNodeId = fiberToNodeId.get(current);
3753
3858
  const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
3754
3859
  if (ancestorNodeId) {
@@ -3756,10 +3861,28 @@ function resolveValueTrace(input) {
3756
3861
  kind: "prop",
3757
3862
  nodeId: ancestorNodeId,
3758
3863
  componentName: ancestorName,
3759
- propPath: match.path,
3760
- confidence: match.confidence
3864
+ propPath: trailingSubPath.length > 0 ? [...matchPath, ...trailingSubPath] : matchPath,
3865
+ confidence: matchConfidence
3761
3866
  });
3762
3867
  }
3868
+ } else {
3869
+ const hookMatch = findMatchingHookState(current, rootValue, rootFp, fpCache);
3870
+ if (hookMatch) {
3871
+ const ancestorNodeId = fiberToNodeId.get(current);
3872
+ const ancestorName = getComponentNameFromFiber(current) ?? "Unknown";
3873
+ if (ancestorNodeId) {
3874
+ steps.push({
3875
+ kind: "hook-state",
3876
+ nodeId: ancestorNodeId,
3877
+ componentName: ancestorName,
3878
+ hookIndex: hookMatch.hookIndex,
3879
+ hookType: hookMatch.hookType,
3880
+ subPath: trailingSubPath.length > 0 ? trailingSubPath : void 0,
3881
+ confidence: hookMatch.confidence
3882
+ });
3883
+ return { ...base, steps, resolvedAtMs: now() };
3884
+ }
3885
+ }
3763
3886
  }
3764
3887
  }
3765
3888
  }
@@ -3768,7 +3891,7 @@ function resolveValueTrace(input) {
3768
3891
  }
3769
3892
  }
3770
3893
  if (input.hookPath) {
3771
- const origin = findFetchOrigin(rootValue);
3894
+ const origin = findFetchOrigin(rootValue, { ignoreTTL: true });
3772
3895
  if (origin) {
3773
3896
  steps.push({
3774
3897
  kind: "api",
@@ -3780,15 +3903,15 @@ function resolveValueTrace(input) {
3780
3903
  return { ...base, steps, resolvedAtMs: now() };
3781
3904
  }
3782
3905
  }
3783
- const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName);
3906
+ const derivedMatch = findDerivationMatch(fiber, rootValue, rootFp, rootComponentName, fpCache);
3784
3907
  if (derivedMatch) {
3785
3908
  steps.push({ ...derivedMatch, nodeId: input.nodeId });
3786
3909
  return { ...base, steps, resolvedAtMs: now() };
3787
3910
  }
3788
- const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId);
3911
+ const contextMatch = findContextMatch(fiber, rootValue, rootFp, fiberToNodeId, fpCache);
3789
3912
  if (contextMatch) {
3790
3913
  steps.push(contextMatch.step);
3791
- const providerStoreMatch = findStoreMatch(contextMatch.providerValue, valueFingerprint(contextMatch.providerValue), deadline);
3914
+ const providerStoreMatch = findStoreMatch(contextMatch.providerValue, cachedFp(contextMatch.providerValue, fpCache), deadline, fpCache);
3792
3915
  if (providerStoreMatch) {
3793
3916
  steps.push({
3794
3917
  kind: "store",
@@ -3797,19 +3920,23 @@ function resolveValueTrace(input) {
3797
3920
  keyPath: providerStoreMatch.keyPath,
3798
3921
  confidence: providerStoreMatch.confidence
3799
3922
  });
3800
- const origin = findFetchOrigin(providerStoreMatch.matchedValue);
3923
+ const origin = resolveOriginViaTagOrKeyPath(
3924
+ providerStoreMatch.matchedValue,
3925
+ providerStoreMatch.stateRoot,
3926
+ providerStoreMatch.keyPath
3927
+ );
3801
3928
  if (origin) {
3802
3929
  steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
3803
3930
  }
3804
3931
  } else {
3805
- const origin = findFetchOrigin(contextMatch.providerValue);
3932
+ const origin = findFetchOrigin(contextMatch.providerValue, { ignoreTTL: true });
3806
3933
  if (origin) {
3807
3934
  steps.push({ kind: "api", requestId: origin, method: "UNKNOWN", urlPath: "", ageMs: 0 });
3808
3935
  }
3809
3936
  }
3810
3937
  return { ...base, steps, resolvedAtMs: now() };
3811
3938
  }
3812
- const storeMatch = findStoreMatch(rootValue, rootFp, deadline);
3939
+ const storeMatch = findStoreMatch(rootValue, rootFp, deadline, fpCache);
3813
3940
  if (storeMatch) {
3814
3941
  steps.push({
3815
3942
  kind: "store",
@@ -3818,7 +3945,11 @@ function resolveValueTrace(input) {
3818
3945
  keyPath: storeMatch.keyPath,
3819
3946
  confidence: storeMatch.confidence
3820
3947
  });
3821
- const origin = findFetchOrigin(storeMatch.matchedValue);
3948
+ const origin = resolveOriginViaTagOrKeyPath(
3949
+ storeMatch.matchedValue,
3950
+ storeMatch.stateRoot,
3951
+ storeMatch.keyPath
3952
+ );
3822
3953
  if (origin) {
3823
3954
  steps.push({
3824
3955
  kind: "api",
@@ -3831,12 +3962,12 @@ function resolveValueTrace(input) {
3831
3962
  }
3832
3963
  return { ...base, steps, resolvedAtMs: now() };
3833
3964
  }
3834
- function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
3965
+ function findContextMatch(consumer, target, targetFp, fiberToNodeId, cache) {
3835
3966
  const deps = consumer.dependencies?.firstContext;
3836
3967
  if (!deps) return null;
3837
3968
  let dep = deps;
3838
3969
  while (dep) {
3839
- const match = valuesMatch(target, targetFp, dep.memoizedValue);
3970
+ const match = valuesMatch(target, targetFp, dep.memoizedValue, cache);
3840
3971
  if (match) {
3841
3972
  const provider = findNearestProvider(consumer, dep.context);
3842
3973
  const step = {
@@ -3852,14 +3983,14 @@ function findContextMatch(consumer, target, targetFp, fiberToNodeId) {
3852
3983
  }
3853
3984
  return null;
3854
3985
  }
3855
- function findDerivationMatch(fiber, target, targetFp, componentName) {
3986
+ function findDerivationMatch(fiber, target, targetFp, componentName, cache) {
3856
3987
  let hook = fiber.memoizedState;
3857
3988
  let index = 0;
3858
3989
  while (hook) {
3859
3990
  const ms = hook.memoizedState;
3860
3991
  if (Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1])) {
3861
3992
  const [computed, deps] = ms;
3862
- const match = valuesMatch(target, targetFp, computed);
3993
+ const match = valuesMatch(target, targetFp, computed, cache);
3863
3994
  if (match) {
3864
3995
  const hookType = typeof computed === "function" ? "useCallback" : "useMemo";
3865
3996
  return {
@@ -3878,6 +4009,25 @@ function findDerivationMatch(fiber, target, targetFp, componentName) {
3878
4009
  }
3879
4010
  return null;
3880
4011
  }
4012
+ function findMatchingHookState(fiber, target, targetFp, cache) {
4013
+ let hook = fiber.memoizedState;
4014
+ let index = 0;
4015
+ while (hook) {
4016
+ const ms = hook.memoizedState;
4017
+ const isMemoTuple = Array.isArray(ms) && ms.length === 2 && Array.isArray(ms[1]);
4018
+ const isEffectShape2 = ms !== null && typeof ms === "object" && "create" in ms && "deps" in ms;
4019
+ const isRefShape = ms !== null && typeof ms === "object" && Object.keys(ms).length === 1 && "current" in ms;
4020
+ if (!isMemoTuple && !isEffectShape2 && !isRefShape) {
4021
+ const match = valuesMatch(target, targetFp, ms, cache);
4022
+ if (match) {
4023
+ return { hookIndex: index, hookType: "useState", confidence: match };
4024
+ }
4025
+ }
4026
+ hook = hook.next;
4027
+ index++;
4028
+ }
4029
+ return null;
4030
+ }
3881
4031
  function findNearestProvider(consumer, contextObj) {
3882
4032
  let current = consumer.return;
3883
4033
  let hops = 0;
@@ -3891,44 +4041,47 @@ function findNearestProvider(consumer, contextObj) {
3891
4041
  }
3892
4042
  return null;
3893
4043
  }
3894
- function findStoreMatch(target, targetFp, deadline) {
4044
+ function findStoreMatch(target, targetFp, deadline, cache) {
3895
4045
  for (const [storeName, state] of getZustandSnapshot()) {
3896
4046
  if (now() > deadline) return null;
3897
- const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline);
4047
+ const hit = findMatchingPathInObject(target, targetFp, state, [], 0, deadline, cache);
3898
4048
  if (hit) {
3899
4049
  return {
3900
4050
  source: "zustand",
3901
4051
  storeName,
3902
4052
  keyPath: hit.path,
3903
4053
  confidence: hit.confidence,
3904
- matchedValue: walkPath(state, hit.path)
4054
+ matchedValue: walkPath(state, hit.path),
4055
+ stateRoot: state
3905
4056
  };
3906
4057
  }
3907
4058
  }
3908
4059
  const redux = getReduxSnapshot();
3909
4060
  if (redux) {
3910
4061
  if (now() > deadline) return null;
3911
- const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline);
4062
+ const hit = findMatchingPathInObject(target, targetFp, redux, [], 0, deadline, cache);
3912
4063
  if (hit) {
3913
4064
  return {
3914
4065
  source: "redux",
3915
4066
  storeName: "redux",
3916
4067
  keyPath: hit.path,
3917
4068
  confidence: hit.confidence,
3918
- matchedValue: walkPath(redux, hit.path)
4069
+ matchedValue: walkPath(redux, hit.path),
4070
+ stateRoot: redux
3919
4071
  };
3920
4072
  }
3921
4073
  }
3922
4074
  for (const [queryHash, entry] of getTanstackSnapshot()) {
3923
4075
  if (now() > deadline) return null;
3924
- const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline);
4076
+ const hit = findMatchingPathInObject(target, targetFp, entry.data, [], 0, deadline, cache);
3925
4077
  if (hit) {
3926
4078
  return {
3927
4079
  source: "tanstack-query",
3928
4080
  storeName: queryHash,
3929
4081
  keyPath: hit.path,
3930
4082
  confidence: hit.confidence,
3931
- matchedValue: walkPath(entry.data, hit.path)
4083
+ matchedValue: walkPath(entry.data, hit.path),
4084
+ stateRoot: entry.data
3932
4085
  };
3933
4086
  }
3934
4087
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flotrace/runtime-core",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Platform-agnostic core for FloTrace runtime — fiber walker, analyzers, trackers. Shared by @flotrace/runtime (web) and @flotrace/runtime-native (React Native).",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -13,7 +13,9 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "LICENSE",
18
+ "README.md"
17
19
  ],
18
20
  "scripts": {
19
21
  "build": "tsup",
@@ -46,10 +48,13 @@
46
48
  "flotrace"
47
49
  ],
48
50
  "license": "MIT",
51
+ "homepage": "https://flotrace.dev",
49
52
  "repository": {
50
53
  "type": "git",
51
- "url": "https://github.com/flotrace/flotrace.git",
52
- "directory": "packages/runtime-core"
54
+ "url": "https://github.com/sameersitre/runtime-core.git"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/sameersitre/runtime-core/issues"
53
58
  },
54
59
  "publishConfig": {
55
60
  "access": "public"