@m5nv/rr-builder v2.0.1
Fluent Route Configuration
A tiny, fluent builder API to configure React Router v7 framework mode routes, for a seamless, unified route & navigation authoring experience.
Single‑source of truth for routes + navigation
1 · Why this package exists
React‑Router v7’s framework‑mode is awesome, but:
- It discards extra route props, making it impossible to keep navigation/UI meta in sync with your route config.
- As apps grow, maintaining and extending nested RR configs by hand becomes tedious and error‑prone.
@m5nv/rr-builder lets you author your routes and all navigation metadata in
one fluent, type-safe DSL, and then generates a runtime‑safe module for your
layout, menus, and navigation UIs.
2 · Installation & Platform Support
# npm or pnpm (dev-only; nothing leaks to runtime)
npm install -D @m5nv/rr-builder
# or
pnpm add -D @m5nv/rr-builderPeer dependency: Make sure you have
@react-router/dev(v7) installed as a peer dependency:npm install @react-router/dev
3 · Quick Start & Example
// routes.ts
import {
build,
external,
index,
layout,
prefix,
route,
} from "@m5nv/rr-builder";
export default build([
layout("project/layout.tsx").children(
route("overview", "project/overview.tsx")
.nav({
label: "Overview",
iconName: "ClipboardList",
section: "project",
}),
route("settings", "project/settings.tsx")
.nav({ label: "Settings", iconName: "Settings" }),
external("https://docs.acme.dev")
.nav({ label: "Docs", iconName: "Book" }),
prefix("account", [
index("project/account/home.tsx")
.nav({ label: "Account Home", iconName: "User" }),
route("settings", "project/account/settings.tsx")
.nav({ label: "Account Settings", iconName: "Settings" }),
]),
),
]);Generate the runtime helper and nav code:
# using npx (works everywhere)
npx rr-check routes.ts --out src/navigation.generated.tsNOTE: to natively handle
typescriptfiles you do need the latest version ofNode.js; or you could use typescript to convertroutes.tsto JS and then userr-check. More details below.
4 · Fluent Authoring API (with Type Safety)
@m5nv/rr-builder provides a type-safe, fluent builder DSL so only legal route/navigation structures are possible:
Route & Layout Builders
route(...)andlayout(...)return builders with.children()and.nav().layout(...).nav()cannot set asection(type error).- Only
routeandlayoutmay have children.
Index & External Builders
index(...)andexternal(...)do not support.children()(type error).- Both support
.nav(), with all fields allowed (exceptexternal()auto-setsexternal: true). external(...)cannot be passed toprefix().
Prefix Builder
prefix(path, [builders...])nests several builders under a path segment.- Only
route,layout, andindexbuilders are allowed;external()is disallowed by type.
Here’s a more descriptive, context-rich replacement for your Meta Shape
(passed to .nav()) section:
Meta Shape (passed to .nav())
The .nav() method lets you annotate each route with extra navigation/UI
metadata. These attributes drive how your Navigator, menu, sidebar, or
breadcrumbs are rendered and allow you to embed business context directly in
your route config. Here’s what each field means:
// For route(), index(), external()
interface NavMeta {
label: string; // **Human‑readable name** for UI elements (menus, tabs, breadcrumbs).
iconName?: string; // **Icon key** (typically from lucide-react or your icon set) to show beside the label.
order?: number; // **Sorting hint**—lower numbers appear earlier in the section/group.
section?: string; // **High-level menu partition** (e.g. "main", "admin", "support"). If omitted, defaults to "main". Used to group unrelated branches.
group?: string; // **Cluster key**—for dividing a section into tabs, panels, or submenus (e.g. “Profile” vs “Security” tabs in Account).
tags?: string[]; // **Arbitrary search/filter keywords** (for power search, badges, or smart menus).
hidden?: boolean; // **Hide from all navigation UIs**—route remains valid, but isn’t shown in menu/sidebar.
end?: boolean; // **Exact path match only** for highlighting in nav (like React Router’s “end”). Useful for index/home routes.
external?: true; // **Automatically set by external()**. Marks this as an external/off-site link.
abac?: Record<string, string>; // **Access control**—attributes for runtime ABAC checks (if your app supports them).
actions?: Array<{ id: string; label: string; iconName?: string }>;
// **Route-specific actions** to show as contextual buttons or menus (e.g. “Create”, “Export”).
}Best practices for NavMeta:
- specify
labelon every route from the start to avoid drift - specify
sectionif you want to split your product into distinct areas each with its own navigation style; e.g.docs,dashboard,shop,news. — remember layouts can set UI meta, but not section partitioning. - use
groupfor tabs,orderfor sorting, andtagsfor search or context-aware navigation. - Actions let you surface per-route commands (like “Create new post” or “Invite user”) right from the navigation model.
Collectively by using .nav(), globalActions and badgeTargets, your
routes.[tj]s file can become not only the single source of truth but also a
planning tool! For example, you could creatively use tags to mark in which
sprint a route will be delivered or was introduced.
Type Safety at a Glance
.children(...)only available onroute()/layout()..nav({ section: ... })disallowed onlayout().prefix()only acceptsroute,layout,indexbuilders..children(...)onindex()orexternal()is a compile-time error.
Example Misuse (now type errors)
index("home.tsx").children(route("foo", "foo.tsx")); // ❌ error
external("http://foo").children(route("foo", "foo.tsx")); // ❌ error
layout("layout.tsx").nav({ section: "main" }); // ❌ error
prefix("docs", [external("https://foo.dev")]); // ❌ error5 · CLI & Code Generation (rr-check)
Check your routes, visualize trees, and generate navigation helpers for runtime.
Usage
npx rr-check <routes-file> [--print:<flags>] [--out <file>] [--watch]Flags:
| Flag | Effect |
|---|---|
--out <file> | Where to emit the navigation helper module |
--force | Code-gen even if duplicate IDs or missing files are found |
--print:<flags> | Comma-list: route-tree, nav-tree, include-id, include-path |
--watch | Watch for file changes and regenerate automatically |
The generated module exports:
// Wire up RR
export function registerRouter(adapter: RouterAdapter): void;
// Data model and utility functions
export interface NavigationApi {
/** list of section names present in the `forest of trees` */
sections(): string[];
/* pure selectors – NO runtime context */
routes(section?: string): NavTreeNode[];
routesByTags(section: string, tags: string[]): NavTreeNode[];
routesByGroup(section: string, group: string): NavTreeNode[];
/* convenience hook that hydrates results returned by adapter.useMatches */
useHydratedMatches: <T = unknown>() => Array<{ handle: NavMeta }>;
/* static extras */
globalActions: GlobalActionSpec[];
badgeTargets: string[];
/* router adapter injected at factory time */
router: RouterAdapter;
}Typescript support
You can run the codegen/CLI tool with Deno for .ts route files:
deno run --unstable-sloppy-imports --allow-read ./node_modules/@m5nv/rr-builder/src/rr-check.js routes.tsOr, use the latest Node.js (> v23.6.0):
node ./node_modules/@m5nv/rr-builder/src/rr-check.js routes.tsTypical error and output
npx rr-check routes.ts --print:nav-tree,include-id
⚠️ Found 1 duplicate route ID
⚠️ Found 6 missing component files
...
├── Home(*!) [id: foo]
├── Settings(!) [id: routes/settings/page]
└── Overview(*!) [id: foo]
└── Annual(!) [id: routes/dashboard/reports/annual]6 · Using the Generated Navigation Module in Your UI
Register the Router Adapter
import { Link, matchPath, useLocation, useMatches } from "react-router-dom";
import nav, { registerRouter } from "@/navigation.generated";
/// one-time registration
registerRouter({ Link, useLocation, useMatches, matchPath });Rendering navigation/menus
function Sidebar({ section }) {
const items = nav.routes(section);
return (
<ul>
{items.map((n) => (
<li key={n.id}>
<nav.router.Link to={n.path}>{n.label}</nav.router.Link>
</li>
))}
</ul>
);
}Dynamic Layouts with Hydrated Matches
import { Outlet } from "react-router";
import { useHydratedMatches } from "./navigation.generated";
export default function ContentLayout() {
const matches = useHydratedMatches();
const match = matches.at(-1);
let { label, iconName } = match?.handle ??
{ label: "Unknown", iconName: "Help" };
return (
<article>
<h2>
<Icon name={iconName} /> {label}
</h2>
<section>
<Outlet />
</section>
</article>
);
}7 · Concepts & Best Practices
Route vs Layout
route(): Owns a URL segment (path), can render its file and children.layout(): Pure wrapper with children, no path.
Index Routes
- Defined with
index(file); rendered at the parent path.
Prefixing
- Use
prefix(path, builders[])to DRY up grouped path segments.
Unique IDs
If multiple routes use the same file, provide a unique ID:
index("Page.tsx", { id: "users-all" });
route("active", "Page.tsx", { id: "users-active" });Now you can switch on match.id in your component.
8 · Troubleshooting & Migration
- Duplicate IDs: Only the first is used in navigation trees; fix to avoid ambiguity.
- Missing files: Shown by the CLI; check spelling or ensure the file exists.
- Type errors in your editor: Confirm
.children()and.nav({ section })are only used where allowed. - Switching from manual routes: Replace your raw array with a single
build([...])call and migrate metadata to.nav().
9 · Design notes
- Sections vs. Groups – section splits the full
forestbytree; group clusters within a section. - External links – Stay in your menu, but are filtered out before hitting RR config.
- No dev-only deps leak to runtime – Only the generated module is imported at runtime.
- Type-safety – The API statically prevents misuse.
- First-class codegen – We do codegen so your runtime bundle is tree-shakable and never ships builder helpers.
10 · Roadmap
- Adapter gallery – ship
@m5nv/rr-adapter-preact,…-solid, etc. - Schema plug‑ins – allow custom meta keys via generic parameter.
- ID collision auto‑resolve – suggestion prompt instead of hard error.
- VS Code plugin – live tree preview + jump‑to‑route.
- Docs site – interactive playground, recipes, FAQ.
- Validate iconName - iconName against icon libraries such
lucide‑reactat build time and create aicons.tsready for import and use by UI menus and layouts.
License
© 2025 Million Views, LLC – Distributed under the MIT License. See LICENSE for details.