Filesystem Router
Scan app/routes/ with Bun.Glob and produce a sorted, deterministic list of RouteEntry records consumed by 02-router.
Purpose
Scan app/routes/ with Bun.Glob and produce a sorted, deterministic list of RouteEntry records consumed by 02-router.
Public surface
export interface RouteEntry {
filePath: string // absolute path
bunPattern: string // Bun.serve({ routes }) pattern, e.g. "/hotels/:city" or "/files/*"
kind: "page" | "api"
segments: Segment[] // parsed form, useful for sorting
}
export async function scanRoutes(appDir: string): Promise<RouteEntry[]>
Behavior
- Use
new Bun.Glob("**/*.{ts,tsx}").scan({ cwd: appDir + "/routes" }).- If
app/routes/does not exist (fresh scaffold, no routes yet), return[]. The scanner catches theENOENT-style failure fromscan()and treats a missing routes dir identically to an empty one. Any other glob failure must propagate.
- If
- Skip files starting with
_(private) or matching*.test.ts(x). - Classify: paths under
routes/api/areapi;.tsxelsewhere arepage;.tsoutsideapi/is an error. - Translate to a
Bun.serve({ routes })pattern:index→""(joined with parent path).[name]→:name.[...name]→*(Bun's catch-all; the matched suffix is exposed viareq.params["*"]).
- Sort entries so static segments outrank dynamic, and shorter patterns outrank catch-alls. Determinism matters for HMR diffing.
Dependencies
Bun.Globonly — nofast-glob, noglobby.- All file existence/metadata checks use
Bun.file(path).exists()andBun.file(path).size. Nevernode:fs.statorfs.access.
Build-time vs runtime
In dev (patties dev), scanRoutes runs at boot and on bun --watch restarts.
In production builds, the scanner runs once at build time (04-build) and its output is inlined into the server bundle via a Bun macro (import { ROUTES } from "./routes.macro" with { type: "macro" }). The runtime server reads ROUTES directly — no filesystem scan happens after Bun.serve boots. On the edge target this eliminates a ~30–80ms cold-start penalty per Worker isolate.
Non-goals
- Watching the filesystem (that's 05-dev-hmr).
- Loading the modules — scanner returns paths; the router decides when to
import().
Acceptance criteria
- Empty
routes/returns[]. - Missing
routes/directory entirely (noapp/routes/at all) also returns[]— no thrown error. - Two files producing the same pattern raise a clear conflict error naming both paths.
- The output is stable across runs for the same input tree.