import { resolveFileRoutePath } from './file-route-path.js'; /** * @fileoverview THE route enumerator. There is exactly one in this repo. * * It used to have a twin: `parseStaticRoutes` in the builder's * `crawlRoutes.ts`, a regex that dropped `:param` routes and could not resolve * nested `` composition. Both are now this file, reached through two * adapters: * * - {@link collectRoutes} takes a `Map` — what the * temporal-worker has after untarring `POST /files/archive`, and what a * unit test can build in three lines. * - {@link collectRoutesFromDir} is a thin `readdir`/`readFile` wrapper for * the Vite dev server, which has a directory and no file map. * * Enumeration deliberately runs OUTSIDE the app's pod: the publish path must * survive a build that never produced its page list, and a pod that cannot run * the build would otherwise take the sitemap and the route manifest down with * it. * * ## Two modes, chosen by PRESENCE — never by a template version * * The fleet runs two template generations at once and will for a long time: * * - **v5 (TanStack Start file routes)** — one file per route under * `src/routes/`, with no central registry. `src/routes/menu/$slug.tsx` IS * the declaration of `/menu/$slug`; there is no `` element anywhere * to scan. * - **v4 (react-router JSX)** — `` elements inside * `src/App.tsx`, or a `createBrowserRouter([…])` config. * * {@link collectRoutes} picks its mode from the file map itself: **any source * file under `src/routes/` ⇒ file-route mode, otherwise the JSX scanner** * (plan decision D-17 — no behaviour anywhere in this migration branches on a * template version number). Both modes share everything downstream: the same * `` cross-check, the same exclusion denylist, the same * {@link RouteDiscovery} verdict, and the same exported types — the two * consumers (`crawlRoutes.ts` in the builder, `routeManifest.ts` in the * publish path) cannot tell which mode ran. * * File-route mode deliberately does **not** run the JSX scanner as well: a v5 * app still vendors the CMS admin console, which is react-router-based and * full of `` elements. Scanning both would mint dozens of * phantom `/admin/**` pages out of vendored code. */ /** * Re-exported so this module stays the one specifier every server-side consumer * imports the mapper from, while the mapper ITSELF lives in a node-free file the * browser can bundle (`./file-route-path.ts`, published as * `@proyecta-ai/vite/file-route-path`). One implementation, two reachable * specifiers — never a second copy. */ export { resolveFileRoutePath }; /** Where a discovered path came from. */ export type RouteKind = /** A real `` / `createBrowserRouter` entry. */ 'route' /** * No route declares this path, but the app links to it with a string * literal. Emitted so the URL is not a hard 404, but never advertised in * `sitemap.xml`: a stale `` would otherwise mint a thin * 200 that we then invite crawlers to index — a manufactured soft-404 farm * on exactly the zero-authority domains this pipeline exists to protect. */ | 'linkOnly'; export interface CollectedRoute { path: string; component?: string; isDynamic: boolean; kind: RouteKind; /** * True when the route must never be published. Two reasons converge here: * * 1. it matches the one exclusion denylist (`./routeExclusions.ts`), or * 2. **file-route mode only** — the route module does not pass a literal * `component:` key to `createFileRoute`. TanStack's prerender * auto-discovery silently skips such a route (`if * (!route.createFileRouteProps?.has('component')) continue`), so the * build never writes an HTML file for it. Advertising it in * `sitemap.xml` — or listing it in the page list a publish guard then * asserts against — would be a lie. (Plan decision D-14.) * * Excluded routes stay in the list — the Vite dev server's route picker * must still offer `/cart` and `/admin` for navigation — and every * publish-path consumer filters them out via {@link publishableRoutes}. */ excluded: boolean; /** * The collection apiId this route module reads ONE ROW from — the answer to * "which collection's entries fill this route's `$param`?", stated by the * route itself. * * File-route mode only, and read from the module's ENTRY reads exclusively * (`loadEntry` / `useEntry` / `contentGet`). It is `bindRouteToCollection`'s * rung 0 (`core/ai/cms/src/seo/routeSeo.ts`), which exists because every * other rung compares the URL SEGMENT against the collection apiId as a * string — and those two names are written by different agents in different * languages. `catalogItems` at `/tienda/$slug` and `services` at * `/servicios/$slug` defeat rungs 1–3 outright; measured 2026-08-11, 8 of the * 15 current-era published apps were left `partial` by exactly that mismatch, * so no detail page was prerendered for any of them. * * ⚠️ **A LIST read must never land here, and that is the whole subtlety.** * `_site.tienda.categoria.$categoria.tsx` reads `loadCollection({ collection: * 'catalogItems', filter: { category } })` — its `$param` is a CATEGORY, not * an entry slug. Binding it would expand `catalogItems`' slugs against it and * mint `/tienda/categoria/pantalon-cargo-negro`: a URL the router never * matches, advertised in `sitemap.xml` as a real page. That is the exact * failure `detailRoutes.ts` and rung 4 both already warn about, arriving from * a new direction. A filter route naming no entry read stays unbound, which * is the correct outcome. * * `undefined` when the module names no entry read, or names more than one * distinct collection — ambiguity is not evidence, so nothing is claimed. */ entryCollection?: string; } /** * How much of the app's route surface this pass can vouch for. Two downstream * decisions — whether the SPA fallback may carry `noindex`, and whether orphan * objects may be retracted — are only safe on `complete`, because `noindex` is * a faster and stronger deindex signal than a 404 and aiming it at a path * family we merely failed to parse would remove live pages. * * This is the ENUMERATION half of the verdict. Callers that also bind * `:param` routes to collections downgrade it further (an unbound family or a * truncated collection makes an otherwise-`complete` pass `partial`); they may * never upgrade it. */ export type RouteDiscovery = 'complete' | 'partial' | 'degraded'; export interface RouteCollection { /** Every discovered path, deduplicated and deterministically sorted. */ routes: CollectedRoute[]; /** String-literal link targets that match no declared route. */ linkOnly: string[]; /** * The leading static chunk of every template-literal link target — the * `/blog/` in ````. This is the single most * common AI-authored link pattern (a `.map()` over a collection) and a * literal-only cross-check is completely blind to it. */ linkPrefixes: string[]; /** `linkPrefixes` entries no enumerated route family covers. */ uncoveredLinkPrefixes: string[]; /** Source files read without a parse failure. */ filesParsed: number; /** Source files whose parse failed or threw. */ filesFailed: number; routeDiscovery: RouteDiscovery; } /** * Enumerate every route declared across `files`, in whichever of the two * modes the file map calls for (see the file header): TanStack Start file * routes when anything lives under `src/routes/`, react-router JSX otherwise. * * Synchronous by design: `oxc-parser` is synchronous, and a synchronous * function is trivially callable from both a Temporal activity and a Vite * middleware. * * @param files `path → source`. Keys must keep their real extension; the * parser picks its TSX/JSX dialect from the filename, and file-route mode * reads the URL out of the filename itself. */ export declare function collectRoutes(files: ReadonlyMap): RouteCollection; /** * Directory adapter for {@link collectRoutes}. Walks `/src` and hands * the resulting file map to the one implementation. */ export declare function collectRoutesFromDir(rootDir: string): Promise; /** * The routes a publish path may act on: declared routes only, denylist * applied. The ONE place that filter lives, so no caller re-invents it and * gets a subset of it wrong. */ export declare function publishableRoutes(collection: RouteCollection): CollectedRoute[]; /** * The route-ID segments a module contributes, or `null` when the module is not * a route at all. * * ## Why this is not derivable from {@link resolveFileRoutePath} * * That function answers "which URL does this module serve?". Nesting is a * different question answered in a different vocabulary, and the two * deliberately disagree on exactly the filenames that exist to make them * disagree: * * - `shop_/$id.tsx` and `shop/$id.tsx` both SERVE `/shop/$id`, but only the * second renders inside `shop.tsx`. The trailing `_` is the opt-out marker, * so it is STRIPPED from the URL and KEPT in the id. * - `_public.tsx` serves no URL at all (`resolveFileRoutePath` → `null`) and * is nonetheless the PARENT of `_public/about.tsx`. A pathless layout is * invisible in the URL and load-bearing in the tree. * - `blog/index.tsx` serves `/blog`, the same URL as `blog.tsx` — and is that * module's CHILD. `index` therefore survives here as its own id segment. * - `blog/route.tsx` serves `/blog` and IS the directory's node, so `route` * is dropped rather than kept. * * Everything the two DO share — `.` and `/` are both separators, `(group)` * folders are invisible, `.lazy` is a code-split half, a `-` prefix is not a * route, a co-located spec is not a route — is parsed once, right here, beside * the URL mapper. A second copy of these conventions anywhere else is the * failure this file's header already records. * * `__root.tsx` answers `[]` — the empty id, which is a proper prefix of every * other module's and therefore makes it the ancestor of the whole tree, exactly * as TanStack renders it. */ export declare function fileRouteIdSegments(filePath: string): string[] | null; /** * For every route module in `filePaths`, the module that RENDERS IT — i.e. the * one whose `` it appears in — or `null` when it is a top-level route * with no layout above it but the document shell. * * The parent is the module whose id is the LONGEST proper prefix of the child's. * Longest-prefix rather than "drop one segment" because a tree may skip a level: * with `a.tsx` and `a/b/c.tsx` and no `a/b.tsx`, `c` renders inside `a`. * * Returned as a `Map`; a module with no parent in the set * is simply absent from it. `__root.tsx`, when present, is the parent of every * top-level module — which is what makes "a parent must render ``" * one rule with no root-shaped exception carved out of it. */ export declare function resolveFileRouteParents(filePaths: readonly string[]): Map; //# sourceMappingURL=route-collector.d.ts.map