/** * Vue Router — navigation written as markup. * * Sign in * * // Nuxt * * * A template attribute is not a call, so the extractor records no reference * for it and the resolver in `frameworks/vue-router.ts` — which binds * `router.push` and `navigateTo` — never sees it. This pass reads every `to` * out of the source, attributes it to the component (the innermost function) * it is written in, matches it against the Vue route table by NAME or by * path, and synthesizes one `navigates` edge from the component to the route. * * The bound form (`:to`) is what carries an object or a template, and it is * the common one in a Vue template — so both spellings are read, and both a * `{ name: … }` and a `{ path: … }` destination resolve, exactly as they do * from a `router.push`. * * Edges are `provenance:'heuristic'`, `synthesizedBy:'vue-router-link'`, with * the destination as written and `registeredAt` = the template site. A * computed `:to="target"` is nothing; a name or path nothing declares is * nothing. Nothing here runs on a project with no Vue routes. */ import type { Edge, Node } from '../types'; import type { ResolutionContext } from './types'; import type { MaybeYield } from './cooperative-yield'; import { isTestPath } from '../search/query-utils'; import { readStringAt, routesForFile, toHref } from './frameworks/expo-router'; import { destinationsForHref } from './frameworks/nextjs'; import { parseVuePathObject, routeNameInExpression, vueRouteTable } from './frameworks/vue-router'; import { enclosingFn, makeLineAt } from './synth-utils'; const TEMPLATE_FILE = /\.(?:vue|[cm]?[jt]sx?)$/; /** `]*?)\s:?to\s*=\s*(?:"([^"]*)"|'([^']*)')/g; /** A tag this pass could possibly match — the cheap prefilter. */ const HAS_LINK_TAG = /<(?:router-link|RouterLink|NuxtLink|nuxt-link)\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 vueRouterLinkEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise { const table = vueRouteTable(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 (!TEMPLATE_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 nodes = ctx.getNodesInFile(file); const lineOf = makeLineAt(source, 1); LINK_TAG.lastIndex = 0; let m: RegExpExecArray | null; while ((m = LINK_TAG.exec(source)) !== null) { // A bound `:to` holds an expression; a plain `to` holds a literal path. const value = (m[3] ?? m[4] ?? '').trim(); if (value.length === 0) continue; const line = lineOf(m.index); const component = enclosingFn(nodes, line); if (!component) continue; const bound = m[0].includes(':to'); const named = bound ? routeNameInExpression(value) : null; const byName = named === null ? undefined : routes.byName.get(named); // A `{ name }` destination names exactly one route; a path may be // written as a choice, and then every arm is drawn. let destinations: { node: Node; display: string }[]; if (byName && named !== null) destinations = [{ node: byName, display: named }]; else { const href = bound ? (parseVuePathObject(value) ?? toHref(readStringAt(value, 0))) : toHref(value); if (!href || !href.path.startsWith('/')) continue; destinations = destinationsForHref(href, routes).map((d) => ({ node: d.node, display: d.href.display })); } for (const { node: target, display } of destinations) { const key = `${component.id}>${target.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: target.id, kind: 'navigates', line, provenance: 'heuristic', metadata: { synthesizedBy: 'vue-router-link', href: display, navMethod: 'link', ...(named !== null && routes.byName.has(named) ? { by: 'name' } : {}), registeredAt: `${file}:${line}`, }, }); } } } return edges; }