npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@m5nv/rr-builder

v2.2.0

Published

Fluent API for seamless route & navigation authoring experience in React Router v7 framework mode

Readme

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 · Motivation

Modern web applications benefit from colocating navigation metadata with route configuration—keeping icons, labels, permissions, and UI state together with the routes that use them. This creates a single source of truth that prevents the drift and inconsistencies that can plague growing applications.

React Router v7's framework mode gives us much needed relief from handicapped file-system based routing mechanisms. The one downside is that it discards extra route properties, making colocation impossible. Even if React Router preserved extra metadata, storing complex navigation data in optional route arguments becomes brittle and unmanageable as applications scale.

@m5nv/rr-builder solves this by providing a fluent, type-safe DSL for authoring augmented routes with colocated navigational metadata. It provides a tool to generate both React Router configuration and a runtime navigation API that keeps everything perfectly synchronized. Now, you can author your routes and all navigation metadata in one fluent, type-safe DSL, and use the generated runtime‑safe module for your layout, menu, and navigational UI components.

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-builder

Peer 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,
  section,
} from "@m5nv/rr-builder";

export default build(
  [
    // Main section (default)
    layout("project/layout.tsx").children(
      route("overview", "project/overview.tsx").nav({
        label: "Overview",
        iconName: "ClipboardList",
      }),
      route("settings", "project/settings.tsx").nav({
        label: "Settings",
        iconName: "Settings",
        id: "project-settings",
      })
    ),

    // Dedicated admin section
    section("admin").children(
      layout("admin/layout.tsx").children(
        index("admin/dashboard.tsx").nav({
          label: "Admin Dashboard",
          iconName: "Shield",
        }),
        route("users", "admin/users.tsx").nav({
          label: "User Management",
          iconName: "Users",
        }),
        route("reports", "admin/reports.tsx").nav({
          label: "Reports",
          iconName: "BarChart",
          id: "admin-reports",
        })
      )
    ),

    // Documentation section with external links
    section("docs").children(
      external("https://docs.acme.dev").nav({
        label: "API Docs",
        iconName: "Book",
      }),
      external("https://guides.acme.dev").nav({
        label: "User Guides",
        iconName: "HelpCircle",
      })
    ),
  ],
  {
    globalActions: [
      {
        id: "search",
        label: "Search",
        iconName: "Search",
        sections: ["main", "admin"], // Available in main and admin sections
      },
      {
        id: "feedback",
        label: "Send Feedback",
        iconName: "MessageSquare",
        externalUrl: "https://feedback.acme.dev",
      },
    ],
    badgeTargets: ["project-settings", "admin-reports"],
  }
);

Generate the runtime helper and nav code:

# using npx (works everywhere)
npx rr-check routes.ts --out src/navigation.generated.ts

NOTE: to natively handle typescript files you do need the latest version of Node.js; or you could use typescript to convert routes.ts to JS and then use rr-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(...) and layout(...) return builders with .children() and .nav().
  • Both support all navigation metadata fields.
  • Only route and layout may have children.

Index & External Builders

  • index(...) and external(...) do not support .children() (type error).
  • Both support .nav(), with all fields allowed (except external() auto-sets external: true).
  • external(...) cannot be passed to prefix().

Section Builder

  • section(name) creates a navigation section for organizing routes into separate trees.
  • Sections can only be used at the top level of build() - they cannot be nested within other routes or sections.
  • Section builders support .nav() (without the section field) and .children().
  • A section's .children() can accept any non-section builder (route, layout, index, external).

Prefix Builder

  • prefix(path, [builders...]) nests several builders under a path segment.
  • Only route, layout, and index builders are allowed; external() and section() are disallowed by type.

Meta Shape (passed to .nav())

The .nav() method lets you attach navigation and UI metadata directly to your route definitions—making your navigation menus, sidebars, toolbars, and breadcrumbs entirely data-driven, consistent, and easy to extend.

Each field serves a specific purpose for your navigation or UI layer:

// Navigation metadata for all builders (external flag is set automatically)
type NavMeta = {
  /**
   * Human‑friendly display name for this route/menu entry.
   * Always provide a label for anything visible in navigation.
   */
  label: string;

  /**
   * Icon to show beside label in menus and sidebars.
   * Typically the string name from lucide-react or your icon set.
   */
  iconName?: string;

  /**
   * Used to sort menu items within a group or section.
   * Lower numbers appear first. Defaults to 0 if omitted.
   */
  order?: number;

  /**
   * Used for grouping related routes into submenus, tab panels,
   * or logical clusters within a section (e.g. "Settings", "Billing" tabs).
   */
  group?: string;

  /**
   * List of keywords for search/filtering or for applying styles/logic
   * to groups of routes (e.g. tags: ["admin", "beta"]).
   */
  tags?: string[];

  /**
   * If true, hides this item from all navigation UI,
   * but leaves the route itself active and linkable.
   * Useful for "secret"/unlinked pages, or wizard steps.
   */
  hidden?: boolean;

  /**
   * If true, menu highlighting should only occur on an exact path match,
   * not for descendant routes. Useful for "Home" or main dashboard links.
   */
  end?: boolean;

  /**
   * Arbitrary access control claims for runtime ABAC or RBAC.
   * Can be used to hide/show nav entries based on permissions.
   */
  abac?: string | string[];

  /**
   * List of per-route/contextual actions. These surface as menu items,
   * action buttons, or FABs—e.g. "Create", "Export", "Refresh".
   * UI code can render them as dropdowns, icon buttons, or toolbars.
   */
  actions?: Array<{ id: string; label: string; iconName?: string }>;

  /**
   * Marks this as an external (off-site) link.
   * This flag is set automatically by `external()` and cannot be set manually.
   * UI code can use this to render with special icons or open in a new tab.
   */
  external?: true; // Set automatically, not manually
};

// All builders use: Omit<NavMeta, 'external'> for their .nav() method
// The external flag is only set automatically by the external() builder

Global Actions & Badge Targets

Beyond per-route metadata, you can define global actions and badge targets that apply across your entire application:

// In your build() call
export default build(
  [
    // ... your routes
  ],
  {
    globalActions: [
      {
        id: "search",
        label: "Global Search",
        iconName: "Search",
        sections: ["main", "admin"], // Only show in these sections
      },
      {
        id: "help",
        label: "Help Center",
        iconName: "HelpCircle",
        externalUrl: "https://help.acme.dev", // External link
      },
      {
        id: "create-project",
        label: "New Project",
        iconName: "Plus",
        // No sections = available everywhere
      },
    ],
    badgeTargets: [
      "notifications", // Route IDs that can show badges
      "admin-users",
      "reports-monthly",
      "global-search", // Action IDs that can show badges
    ],
  }
);

Global Actions are app-wide commands that appear in your navigation UI regardless of the current route. They can:

  • Be scoped to specific sections via the sections array
  • Link to external URLs via externalUrl
  • Trigger application logic when clicked (handled by your UI code)

Badge Targets are route IDs or action IDs that can display notification badges, counters, or status indicators in your navigation UI.

Type Safety at a Glance

  • .children(...) only available on route(), layout(), and section().
  • section() builders can only be used at the top level of build().
  • prefix() only accepts route, layout, index builders (not external or section).
  • .children(...) on index() or external() is a compile-time error.
Example Misuse (flagged for type errors)
index("home.tsx").children(route("foo", "foo.tsx")); // ❌ error
external("http://foo").children(route("foo", "foo.tsx")); // ❌ error
section("admin").children(section("nested")); // ❌ error
prefix("docs", [external("https://foo.dev")]); // ❌ error
prefix("api", [section("admin")]); // ❌ error

// This is also an error - sections can only be at top level
route("admin", "admin.tsx").children(
  section("users") // ❌ error
);

5 · Section-Based Navigation Architecture

The section() builder enables you to organize your application into distinct navigation trees, perfect for:

  • Monorepo applications: A single repo serving multiple webapps (docs, dashboard, ...)
  • Role-based interfaces: Different navigation for different user types
  • Feature modules: Isolate documentation, settings, or tool sections
  • Progressive disclosure: Show simpler navigation to new users

Section Examples

export default build([
  // Main application routes
  layout("app/layout.tsx").children(
    index("app/dashboard.tsx").nav({ label: "Dashboard" }),
    route("projects", "app/projects.tsx").nav({ label: "Projects" })
  ),

  // Admin section with its own navigation tree
  section("admin")
    .nav({ label: "Administration", iconName: "Shield" })
    .children(
      layout("admin/layout.tsx").children(
        index("admin/overview.tsx").nav({ label: "Admin Overview" }),
        route("users", "admin/users.tsx").nav({ label: "Users" }),
        route("settings", "admin/settings.tsx").nav({ label: "Settings" })
      )
    ),

  // Documentation section
  section("docs")
    .nav({ label: "Documentation", iconName: "Book" })
    .children(
      external("https://api-docs.acme.dev").nav({ label: "API Reference" }),
      external("https://guides.acme.dev").nav({ label: "User Guides" })
    ),
]);

Your generated navigation API will provide section-aware methods:

// Get all available sections
const sections = nav.sections(); // ["main", "admin", "docs"]

// Get routes for a specific section
const adminRoutes = nav.routes("admin");
const mainRoutes = nav.routes("main"); // or nav.routes() for default

6 · 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 type 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.ts

Or, use the latest Node.js (> v23.6.0):

node ./node_modules/@m5nv/rr-builder/src/rr-check.js routes.ts

Typical 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]

7 · Using the Generated Navigation Module in Your UI

Register the Router Adapter

import { Link, matchPath, useLocation, useMatches } from "react-router";
import nav, { registerRouter } from "@/navigation.generated";

/// one-time registration
registerRouter({ Link, useLocation, useMatches, matchPath });

Rendering Section-Based Navigation

function AppNavigation() {
  const sections = nav.sections();

  return (
    <nav>
      {sections.map((sectionName) => (
        <section key={sectionName}>
          <h3>{sectionName}</h3>
          <NavigationSection section={sectionName} />
        </section>
      ))}
    </nav>
  );
}

function NavigationSection({ section }) {
  const items = nav.routes(section);
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <nav.router.Link to={item.path}>
            {item.iconName && <Icon name={item.iconName} />}
            {item.label}
          </nav.router.Link>
        </li>
      ))}
    </ul>
  );
}

Global Actions & Badges

function GlobalActionBar() {
  const actions = nav.globalActions;
  const currentSection = getCurrentSection(); // Your logic

  const availableActions = actions.filter(
    (action) => !action.sections || action.sections.includes(currentSection)
  );

  return (
    <div className="action-bar">
      {availableActions.map((action) => (
        <ActionButton
          key={action.id}
          action={action}
          hasBadge={nav.badgeTargets.includes(action.id)}
        />
      ))}
    </div>
  );
}

function ActionButton({ action, hasBadge }) {
  const handleClick = () => {
    if (action.externalUrl) {
      window.open(action.externalUrl, "_blank");
    } else {
      // Dispatch your custom action
      dispatchAction(action.id);
    }
  };

  return (
    <button onClick={handleClick} className="action-button">
      {action.iconName && <Icon name={action.iconName} />}
      {action.label}
      {hasBadge && <Badge />}
    </button>
  );
}

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, actions } = match?.handle ?? {
    label: "Unknown",
    iconName: "Help",
    actions: [],
  };

  return (
    <article>
      <header>
        <h2>
          <Icon name={iconName} /> {label}
        </h2>
        {actions && (
          <div className="route-actions">
            {actions.map((action) => (
              <button key={action.id} onClick={() => handleAction(action.id)}>
                {action.iconName && <Icon name={action.iconName} />}
                {action.label}
              </button>
            ))}
          </div>
        )}
      </header>
      <section>
        <Outlet />
      </section>
    </article>
  );
}

8 · Concepts & Best Practices

Route vs Layout vs Section

  • route(): Owns a URL segment (path), can render its file and children.
  • layout(): Pure wrapper with children, no path.
  • section(): Top-level container that creates separate navigation trees.

Index Routes

  • Defined with index(file); rendered at the parent path.

Prefixing

  • Use prefix(path, builders[]) to DRY up grouped path segments.
  • Cannot include section() or external() builders.

Section Organization

  • Use sections to create distinct areas of your application
  • Each section gets its own navigation tree
  • Perfect for admin areas, documentation, or feature modules
  • Sections cannot be nested - they're always top-level

Shared Layouts vs Individual Sections

Use sharedLayout() when:

  • Multiple sections share the same root layout (app shell, navigation, header)
  • You want to eliminate repetitive layout nesting
  • You have a common UI structure across different app areas

Use individual section() when:

  • Each section has completely different layout requirements
  • Sections are truly independent with no shared UI
  • You need maximum flexibility in structure
// Shared layout - common app shell
...sharedLayout("layouts/app-shell.tsx", {
  "dashboard": dashboardRoutes,
  "admin": adminRoutes,
})

// vs Individual sections - different layouts
section("public").children(
  layout("layouts/marketing.tsx").children(...)
),
section("app").children(
  layout("layouts/authenticated.tsx").children(...)
),

Actions & Global Actions

  • Route actions: Contextual commands specific to a route (Create, Edit, Delete)
  • Global actions: App-wide commands available across sections (Search, Help, New)
  • Actions are defined in metadata but wired up by your UI code
  • Use badgeTargets to show notifications on specific routes or actions

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.

9 · 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() usage follows the constraints (no sections except at top level).
  • Section nesting errors: Remember sections can only be used at the top level of build().
  • Switching from manual routes: Replace your raw array with a single build([...]) call and migrate metadata to .nav().

10 · Design notes

  • Sections vs. Groupssection splits the full forest by tree; 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.
  • Actions are declarative – Define them in route metadata, wire them up in UI code.

11 · Roadmap

  • ID collision auto‑resolve – suggestion prompt instead of hard error.
  • Docs site – interactive playground, recipes, FAQ.
  • Validate iconName - iconName against icon libraries such lucide‑react at build time and create a icons.ts ready for import and use by UI menus and layouts.
  • Action validation - Validate action IDs and provide TypeScript autocompletion.

License

© 2025 Million Views, LLC – Distributed under the MIT License. See LICENSE for details.