/** * SvelteKit — navigation written as markup. * * Sign in * * * * SvelteKit has no link component: an ordinary `` IS the navigation, * intercepted by the router. So a page's outgoing links are plain markup, the * extractor records no reference for them, and the resolver in * `frameworks/sveltekit-router.ts` — which binds `goto` and `redirect` — * never sees them. This pass reads every internal `` out of the * source, attributes it to the component (the innermost function) it is * written in, matches it against the SvelteKit route table, and synthesizes * one `navigates` edge from the component to the page. * * Edges are `provenance:'heuristic'`, `synthesizedBy:'sveltekit-link'`, with * the href as written and `registeredAt` = the markup site. An external href * is a link out of the site, not a transition; a computed one is nothing; a * path no page serves is nothing. Nothing here runs on a project with no * SvelteKit pages. * * This is `next-router-synthesizer.ts`'s twin over `` alone — Next * reads `` too, and Svelte has no such component. * * A second pass here binds a route to the `+page.svelte` that serves it * (`svelteKitPageComponentEdges`): the route node and the component sit in the * same file, but nothing joined them, so a SvelteKit page had no body for the * Steps picture to walk and opened as a lone box. * * The other half of the join — a page and the `+page.server.js` beside it — is * `callback-synthesizer.ts`'s `svelteKitLoadEdges`, which already existed: a * SvelteKit page and its loader are two halves of one route joined by the file * system rather than by a call, and without that join a page's own auth guard * (`redirect(302, '/login')` in its loader) belongs to no screen at all. */ import type { Edge, Node } from '../types'; import type { ResolutionContext } from './types'; import type { MaybeYield } from './cooperative-yield'; import { isTestPath } from '../search/query-utils'; import { HOLE, readStringAt, routesForFile, toHref } from './frameworks/expo-router'; import { destinationsForHref } from './frameworks/nextjs'; import { svelteKitTable } from './frameworks/sveltekit-router'; import { enclosingFn, makeLineAt } from './synth-utils'; const MARKUP_FILE = /\.svelte$/; /** `]*?)\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*)/g; /** Links a single component may carry before it is a navigation menu, not a decision. */ const MAX_LINKS_PER_COMPONENT = 24; export async function svelteKitLinkEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise { const table = svelteKitTable(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 (!MARKUP_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 || !source.includes('href')) 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) { let literal: string | null = m[2] ?? m[3] ?? null; if (literal === null) { // `href={…}`: a string or a template with holes. const at = m.index + m[0].length; const ch = source[at]; if (ch === '"' || ch === "'" || ch === '`') literal = readStringAt(source, at); } if (literal === null) continue; // An external href is a link out of the site, not a transition. A // Svelte `{expr}` inside a quoted attribute is an interpolation, so it // becomes the same hole a template literal's `${…}` does — which is how // `/profile/@{user.username}` reaches the `/profile/@:user` page. if (!literal.startsWith('/')) continue; const href = toHref(literal.replace(/\{[^}]*\}/g, HOLE)); if (!href) 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: page, href: arm } of destinationsForHref(href, routes)) { const key = `${component.id}>${page.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: page.id, kind: 'navigates', line, provenance: 'heuristic', metadata: { synthesizedBy: 'sveltekit-link', href: arm.display, navMethod: 'a', registeredAt: `${file}:${line}`, }, }); } } } return edges; } // ============================================================================= // A route and the page that serves it // ============================================================================= /** * One `calls` edge from each `+page.svelte` route to the component in its own * file — the page that renders when a navigation lands there. * * Every other framework's resolver names this at extraction: a Next page route * points at the file's default export, a React Router route at the component * the markup named. SvelteKit's route is derived from the file's PATH, and its * component has no name of its own to reference (every page file's component * is called `+page`), so the two are joined here, where both are already in * hand and the match is the file itself rather than a name. * * With it, `route-roots.ts` reads the page as the route's root: the Steps * picture starts at the page instead of at an empty box, and the Screens walk * attributes a navigation to the screen whose component holds it rather than * falling back to the file it was written in. */ export async function svelteKitPageComponentEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise { const table = svelteKitTable(ctx); if (table.byRoot.size === 0) return []; const edges: Edge[] = []; let scanned = 0; const pages = new Set(); for (const routes of table.byRoot.values()) for (const page of routes.exact.values()) pages.add(page); for (const page of pages) { if ((++scanned & 31) === 0) await onYield(); const component = ctx.getNodesInFile(page.filePath).find((n) => n.kind === 'component'); if (!component) continue; edges.push({ source: page.id, target: component.id, kind: 'calls', line: component.startLine, provenance: 'heuristic', metadata: { synthesizedBy: 'sveltekit-page', registeredAt: page.filePath }, }); } return edges; }