/** * React Router — navigation written as markup. * * Continue * Profile * * // react-router-bootstrap * … // v5's object form * * A JSX attribute is not a call, so the extractor records no reference for it * and the resolver in `frameworks/react-router.ts` — which binds * `history.push` and `navigate` — never sees it. This pass reads every `to` * attribute out of the source, attributes it to the component (the innermost * function) it is written in, matches it against the React Router route * table, and synthesizes one `navigates` edge from the component to the * route. That is the edge the Screens view walks back from, so a screen's * links are its transitions exactly as its pushes are. * * Edges are `provenance:'heuristic'`, `synthesizedBy:'react-router-link'`, * with the path as written and `registeredAt` = the JSX site. A computed * target (`to={next}`) is nothing; a path no route serves is nothing; a * relative `to` is nothing, because it is resolved against a nesting this * scan does not read. Nothing here runs on a project with no React Router * routes. * * This is `next-router-synthesizer.ts`'s twin — the same shape over the other * attribute (`to`, not `href`) and the other table. */ import type { Edge } from '../types'; import type { ResolutionContext } from './types'; import type { MaybeYield } from './cooperative-yield'; import { stripCommentsForRegex } from './strip-comments'; import { isTestPath } from '../search/query-utils'; import { parseHrefExpression, routesForFile, toHref, type HrefLiteral } from './frameworks/expo-router'; import { matchBracket } from './frameworks/object-literal'; import { destinationsForHref } from './frameworks/nextjs'; import { reactRouterTable } from './frameworks/react-router'; import { enclosingFn, makeLineAt } from './synth-utils'; const JSX_FILE = /\.(?:[cm]?[jt]sx?|mdx)$/; /** The tags that carry a route as a `to` attribute, the attribute anywhere in the tag. */ const LINK_TAG = /<(Link|NavLink|Navigate|LinkContainer|IndexLinkContainer)\b([^>]*?)\bto\s*=\s*(?:"([^"]*)"|'([^']*)'|(?=\{))/g; /** A tag this pass could possibly match — the cheap prefilter before stripping comments. */ const HAS_LINK_TAG = /<(?:Link|NavLink|Navigate|LinkContainer|IndexLinkContainer)\b/; /** Links a single component may carry before it is a navigation menu, not a decision. */ const MAX_LINKS_PER_COMPONENT = 24; export async function reactRouterLinkEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise { const table = reactRouterTable(ctx); if (table.byRoot.size === 0) return []; const edges: Edge[] = []; const seen = new Set(); const perComponent = new Map(); let scanned = 0; for (const file of ctx.getAllFiles()) { if (!JSX_FILE.test(file) || isTestPath(file)) continue; const routes = routesForFile(table, file); if (!routes || routes.exact.size === 0) continue; if ((++scanned & 63) === 0) await onYield(); const source = ctx.readFile(file); if (!source || !HAS_LINK_TAG.test(source)) continue; const safe = stripCommentsForRegex(source, 'typescript'); const nodes = ctx.getNodesInFile(file); const lineOf = makeLineAt(safe, 1); LINK_TAG.lastIndex = 0; let m: RegExpExecArray | null; while ((m = LINK_TAG.exec(safe)) !== null) { const tag = m[1]!; const quoted: string | null = m[3] ?? m[4] ?? null; let href: HrefLiteral | null; if (quoted !== null) href = toHref(quoted); else { // `to={…}` holds an EXPRESSION, and it is read with the same reader // the `history.push(…)` path uses — a string, a template, a // `{ pathname }` object, or a conditional whose arms agree // (`to={redirect ? `/register?redirect=${redirect}` : '/register'}`, // which is how react-router apps write a link that carries state). // Peeking at the first character instead missed every one of those. const at = m.index + m[0].length; const close = matchBracket(safe, at); if (close < 0) continue; href = parseHrefExpression(safe.slice(at + 1, close)); } // A relative `to` is resolved against the route this markup renders // under — a nesting this scan does not read, so it is not a destination. if (!href || !href.path.startsWith('/')) continue; const line = lineOf(m.index); const component = enclosingFn(nodes, line); if (!component) continue; // A destination written as a choice names one route per arm, and the // user reaches every one of them — each is drawn. for (const { node: route, href: arm } of destinationsForHref(href, routes)) { const key = `${component.id}>${route.id}`; if (seen.has(key)) continue; const count = (perComponent.get(component.id) ?? 0) + 1; perComponent.set(component.id, count); if (count > MAX_LINKS_PER_COMPONENT) continue; seen.add(key); edges.push({ source: component.id, target: route.id, kind: 'navigates', line, provenance: 'heuristic', metadata: { synthesizedBy: 'react-router-link', href: arm.display, navMethod: tag === 'Navigate' ? 'navigate' : 'link', registeredAt: `${file}:${line}`, }, }); } } } return edges; }