diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9d2099..bc2c7c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
+- React components declared with `forwardRef`, `memo`, or styled-components / emotion (`const Button = forwardRef(...)`, `const Card = memo(...)`, `const Box = styled.button\`…\``) are now recognized as components, so finding where they're used works. Before, they were indexed as plain constants, so `codegraph callers` and impact analysis reported "no callers found" even when the component was rendered across dozens of files — a dangerous false "safe to change" right before refactoring a shared component. Now every `` usage links back to the component, so callers and blast radius are complete. This is the standard shadcn/ui declaration style, so for typical React design systems the whole UI layer is no longer invisible to impact analysis. Thanks @Arlandaren for the report and @maxmilian for the root-cause. (#841)
+- React Router and Next.js routes defined in `.tsx` / `.jsx` files are now indexed. Routes written as JSX — `}/>`, `createBrowserRouter([...])`, and Next.js `app/`/`pages/` page files — were being skipped entirely (only routes that happened to live in plain `.ts`/`.js` were picked up), so "what renders at this path?" and the route → page-component link were missing for most React apps. Now those routes show up in `codegraph search`/`codegraph_explore` and connect to the component they render, just like the backend route → handler links on other frameworks.
- `codegraph index` now rebuilds the full graph from scratch, so it produces the same result as a fresh `codegraph init` instead of reporting "0 nodes, 0 edges" and looking like it wiped your index. Previously, re-running `index` on an unchanged project skipped every file (their contents hadn't changed) and showed an empty-looking summary; it now clears and re-indexes for an honest, complete rebuild every time. Use `codegraph sync` for fast incremental updates between full rebuilds. Thanks @Arc-univer. (#874)
- The file watcher that auto-syncs the graph now fails cleanly when live watching can no longer be trusted, instead of looking healthy while the index quietly goes stale. If the operating system runs out of file-watch resources, or another process holds the write lock far longer than a normal save, CodeGraph now disables auto-sync once — with a single clear message telling you to run `codegraph sync` (or rely on the git sync hooks) to refresh — rather than retrying forever or repeating the same error on a loop. And while auto-sync is disabled, CodeGraph's tool responses (and `codegraph status`) now say so plainly, so your AI agent knows to read files directly instead of trusting a frozen index. This mostly matters for long-running MCP/daemon sessions, which could otherwise keep serving stale results while appearing to work. Thanks @thismilktea. (#876)
- On Linux, hitting the kernel's inotify watch limit on a large project no longer silently leaves half the tree unwatched. CodeGraph now tells you once — naming the exact setting to raise (`fs.inotify.max_user_watches`, e.g. `sudo sysctl fs.inotify.max_user_watches=1048576`) — and keeps live-watching the directories it could register while `codegraph sync` (or the git sync hooks) covers the rest. (#876)
diff --git a/__tests__/frameworks-integration.test.ts b/__tests__/frameworks-integration.test.ts
index 344a0f6..2354604 100644
--- a/__tests__/frameworks-integration.test.ts
+++ b/__tests__/frameworks-integration.test.ts
@@ -908,3 +908,56 @@ describe('Go gRPC stub→impl synthesis', () => {
}
});
});
+
+describe('React Router end-to-end route extraction (.tsx/.jsx)', () => {
+ let tmpDir: string | undefined;
+ afterEach(() => {
+ if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
+ tmpDir = undefined;
+ });
+
+ // Regression for the resolver language-gate bug: the `react` resolver's
+ // `extract()` was filtered out of the .tsx/.jsx grammars, so `` routes
+ // — which only live in JSX files — were never indexed through the real
+ // indexing path (the unit tests call extract() directly and so missed this).
+ it('indexes }> routes from a .tsx file and links them to the component', async () => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rr-'));
+ fs.writeFileSync(
+ path.join(tmpDir, 'package.json'),
+ '{"dependencies":{"react":"^18.0.0","react-router-dom":"^6.0.0"}}'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'Home.tsx'),
+ 'export function Home() { return null; }\n'
+ );
+ fs.writeFileSync(
+ path.join(tmpDir, 'routes.tsx'),
+ `import { Routes, Route } from 'react-router-dom';
+import { Home } from './Home';
+export function AppRoutes() {
+ return (
+
+ } />
+
+ );
+}
+`
+ );
+
+ const cg = CodeGraph.initSync(tmpDir);
+ await cg.indexAll();
+ try {
+ // The route node from the .tsx file exists (the bug: it didn't).
+ const route = cg.getNodesByKind('route').find((n) => n.name === '/home');
+ expect(route, '/home route from .tsx should be indexed').toBeDefined();
+
+ // ...and it links to the Home component.
+ const home = cg.getNodesByName('Home').find((n) => n.kind === 'function');
+ expect(home).toBeDefined();
+ const toHome = cg.getOutgoingEdges(route!.id).find((e) => e.target === home!.id);
+ expect(toHome, 'route → Home component edge').toBeDefined();
+ } finally {
+ cg.close();
+ }
+ });
+});
diff --git a/__tests__/react-hoc-component.test.ts b/__tests__/react-hoc-component.test.ts
new file mode 100644
index 0000000..cac29b8
--- /dev/null
+++ b/__tests__/react-hoc-component.test.ts
@@ -0,0 +1,145 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import * as os from 'node:os';
+import { CodeGraph } from '../src';
+
+/**
+ * #841 — React components declared via an HOC wrapper
+ * (`const Button = forwardRef(...)`, `memo(...)`, `styled.x\`…\``) were indexed
+ * as plain `constant` nodes, so their JSX usages (``) got no render
+ * edge and `getCallers` / `getImpactRadius` returned empty — a dangerous silent
+ * false negative for every shadcn/ui-style design system. They must now be
+ * `component` nodes that receive jsx-render edges like function components do.
+ */
+describe('React HOC-wrapped component recognition (#841)', () => {
+ let dir: string;
+ let cg: any;
+
+ beforeEach(() => {
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'react-hoc-'));
+ fs.writeFileSync(path.join(dir, 'package.json'), '{"dependencies":{"react":"^18.0.0"}}');
+ });
+
+ afterEach(() => {
+ cg?.close?.();
+ fs.rmSync(dir, { recursive: true, force: true });
+ });
+
+ async function index() {
+ cg = await CodeGraph.init(dir, { silent: true });
+ await cg.indexAll();
+ return (cg as any).db.db;
+ }
+
+ const kindsOf = (db: any, name: string): string[] =>
+ db
+ .prepare('SELECT kind FROM nodes WHERE name=? ORDER BY kind')
+ .all(name)
+ .map((r: any) => r.kind);
+
+ it('classifies forwardRef / memo / styled consts as component nodes (not constant)', async () => {
+ fs.writeFileSync(
+ path.join(dir, 'ui.tsx'),
+ `import * as React from 'react';
+import styled from 'styled-components';
+export const Button = React.forwardRef((props, ref) => );
+export const Bare = forwardRef((props, ref) => );
+export const Card = memo((props: { t: string }) =>