feat(expo-router): add Expo Router support for screens and navigates

Introduce Expo Router integration: a new framework resolver, route-based screen nodes, and navigates edges, plus a /api/screens endpoint and a Screens UI view. Adds branch-guard-driven labeling of edges, resolution logic, and tests to cover extraction, resolution, and end-to-end flow. This enables CodeGraph UI to surface screens and transitions from Expo Router apps.
This commit is contained in:
Colby McHenry
2026-08-27 22:52:18 -05:00
parent ac9580544b
commit 70fd5fefc2
42 changed files with 4257 additions and 33 deletions
+14 -4
View File
@@ -193,8 +193,16 @@ export const FLOW_CALLABLE_KINDS: ReadonlySet<string> = new Set([
'function',
'component',
'constructor',
'route',
]);
/**
* Edge kinds a flow may ride. `navigates` is a screen transition (Expo Router
* `router.push('/x')` → the route node) — a hop in the user's flow exactly as
* a call is a hop in the program's.
*/
export const FLOW_EDGE_KINDS: ReadonlySet<string> = new Set(['calls', 'navigates']);
/**
* Node kinds that can be an endpoint of a SYNTHESIZED edge without being
* callable. An RTK thunk is `const X = createAsyncThunk(...)`, so a thunk →
@@ -458,8 +466,10 @@ function walkCalls(
if (id !== seed.id && named.has(id)) reached.push(id);
if (depth >= maxHops - 1) continue;
for (const c of cg.getCallees(id)) {
if (c.edge.kind !== 'calls' || parent.has(c.node.id)) continue;
const newStreak = named.has(c.node.id) ? 0 : streak + 1;
if (!FLOW_EDGE_KINDS.has(c.edge.kind) || parent.has(c.node.id)) continue;
// A route node is a connector, not a symbol the reader would have named:
// crossing one costs no bridge budget.
const newStreak = named.has(c.node.id) ? 0 : c.node.kind === 'route' ? streak : streak + 1;
if (newStreak > maxBridge) continue;
parent.set(c.node.id, { prev: id, edge: c.edge, node: c.node });
queue.push({ id: c.node.id, depth: depth + 1, streak: newStreak });
@@ -530,7 +540,7 @@ function walkBidirectional(
const next: Node[] = [];
for (const node of frontF) {
for (const c of cg.getCallees(node.id)) {
if (c.edge.kind !== 'calls' || forward.has(c.node.id)) continue;
if (!FLOW_EDGE_KINDS.has(c.edge.kind) || forward.has(c.node.id)) continue;
forward.set(c.node.id, { prev: node.id, edge: c.edge, node: c.node });
next.push(c.node);
}
@@ -542,7 +552,7 @@ function walkBidirectional(
const next: Node[] = [];
for (const node of frontB) {
for (const c of cg.getCallers(node.id)) {
if (c.edge.kind !== 'calls' || backward.has(c.node.id)) continue;
if (!FLOW_EDGE_KINDS.has(c.edge.kind) || backward.has(c.node.id)) continue;
backward.set(c.node.id, { next: node.id, edge: c.edge });
backNodes.set(c.node.id, c.node);
next.push(c.node);