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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@epicabdou/linkr

v0.2.0

Published

linkr.js — File-based routing for React Router (v7) with Vite

Downloads

1,090

Readme

@epicabdou/linkr

File-based routing for React Router v7 in Vite + React SPA apps. Generates a route tree from a src/pages/ directory using import.meta.glob — the consumer passes the glob result; the library does not call it.

Install

pnpm add @epicabdou/linkr react react-dom react-router

Peer dependencies: react (≥18), react-dom (≥18), react-router (≥7).

Usage

Basic setup

import { createRoutes } from "@epicabdou/linkr";
import { createBrowserRouter, RouterProvider } from "react-router";

const pages = import.meta.glob("./pages/**/*.tsx");
const routes = createRoutes({ pagesGlob: pages, pagesDir: "pages" });
const router = createBrowserRouter(routes);

// Then render <RouterProvider router={router} />

One-line app bootstrap

Use createRootWithLinkr to create the root, build routes, and render in one call:

import { createRootWithLinkr } from "@epicabdou/linkr";

createRootWithLinkr(document.getElementById("root")!, {
  pagesGlob: import.meta.glob("./pages/**/*.tsx"),
  pagesDir: "pages",
});

This uses createBrowserRouter, StrictMode, and RouterProvider under the hood. No need to import React Router or call createRoutes yourself.

Using <LinkrApp> (with custom providers)

When you need to wrap the app with your own providers, use the <LinkrApp> component and pass the same options as createRoutes:

import { LinkrApp } from "@epicabdou/linkr";

const pages = import.meta.glob("./pages/**/*.tsx");

function App() {
  return (
    <YourProvider>
      <LinkrApp
        pagesGlob={pages}
        pagesDir="pages"
        defaultRedirectTo="/login"
      />
    </YourProvider>
  );
}

createRoot(document.getElementById("root")!).render(<App />);

Options are read once on mount; the router is created with useMemo.


createRoutes options

| Option | Type | Default | Description | |--------|------|---------|-------------| | pagesGlob | Record<string, () => Promise<unknown>> | required | Result of import.meta.glob("...") from your app. | | pagesDir | string | "src/pages" | Base path used to normalize file paths. Must match the prefix you strip from glob keys (e.g. "pages" when glob is ./pages/**/*.tsx from src/). | | layoutFileName | string | "_layout" | Filename (without extension) for layout routes. | | notFoundFileName | string | "404" | Filename for the catch-all not-found route. | | routeExtensions | string[] | [".tsx", ".ts", ".jsx", ".js"] | Extensions that define route files. | | defaultRedirectTo | string | "/" | Default redirect path when a route's protect check fails and no redirectTo is set. | | layoutsGlob | Record<string, () => Promise<unknown>> | — | When set with layoutMap, layouts are loaded from this glob and _layout files in pages are ignored. Example: import.meta.glob("./layouts/**/*.tsx"). | | layoutsDir | string | "src/layouts" | Base path for layout files when using layoutsGlob. | | layoutMap | Record<string, string> | — | Path prefix → layout name. Use "" or "/" for root; segment name (e.g. blog) for nested. Requires layoutsGlob. |


File conventions

| File / pattern | Route | |----------------|-------| | index.tsx | / (index route) | | about.tsx | /about | | blog/index.tsx | /blog (index under blog) | | blog/[id].tsx | /blog/:id | | blog/[id]/index.tsx | /blog/:id (index under dynamic segment) | | blog/[id]/post.tsx | /blog/:id/post | | docs/[...slug].tsx | /docs/* (splat) | | 404.tsx | path: "*" (catch-all, last) | | _layout.tsx (in any folder) | Layout route; children render in <Outlet />. Ignored when using layoutsGlob + layoutMap. |

  • Nested routes: folder nesting = nested routes. Dynamic segments work in both file names (e.g. [id].tsx) and folder names (e.g. [id]/index.tsx, [id]/post.tsx).
  • Layouts: Either put _layout.tsx in a folder (folder-based), or use a separate layouts folder with layoutsGlob and layoutMap so layouts are reusable components.
  • Sort order: among siblings — static segments first, then dynamic (:id), then splat (*).
  • Lazy loading: every route uses React Router's lazy() for code splitting.

Page module exports

| Export | Description | |--------|-------------| | default | React component used as the route element (required). | | ErrorBoundary | Optional; used as the route's errorElement. | | handle | Optional; attached to the route's handle. | | protect | Optional; runs before rendering. If it returns false, the user is redirected. See Route protection. |


Layouts in a separate folder

Keep layouts as reusable components in a dedicated folder and wire them by path with layoutMap (no _layout.tsx in pages).

Example layouts:

// src/layouts/Root.tsx
import { Link, Outlet } from "react-router";
export default function RootLayout() {
  return (
    <div>
      <nav><Link to="/">Home</Link> <Link to="/blog">Blog</Link></nav>
      <Outlet />
    </div>
  );
}
// src/layouts/Blog.tsx
import { Outlet } from "react-router";
export default function BlogLayout() {
  return <div><h2>Blog</h2><Outlet /></div>;
}

Wire in router setup:

const pages = import.meta.glob("./pages/**/*.tsx");
const layouts = import.meta.glob("./layouts/**/*.tsx");
const routes = createRoutes({
  pagesGlob: pages,
  pagesDir: "pages",
  layoutsGlob: layouts,
  layoutsDir: "layouts",
  layoutMap: { "/": "Root", "": "Root", blog: "Blog" },
});
  • layoutMap keys: "" or "/" = root layout; "blog" = layout for /blog and its children. Value = layout filename without extension (e.g. Root, Blog).
  • The same layout component can be reused by mapping multiple keys to the same name.

Route protection

You can guard a route or an entire layout in two ways.

1. protect export on a page or layout

Export protect from any page or _layout module. The check runs in the route loader; if it returns false, the user is redirected before the page renders.

Shorthand (function): redirect uses defaultRedirectTo from createRoutes options (or "/").

// pages/dashboard.tsx
export const protect = () => !!getAuthToken();

export default function Dashboard() {
  return <div>Dashboard</div>;
}

Full form (object): specify redirect and optional fallback for async checks.

// pages/settings/_layout.tsx
export const protect = {
  check: async () => (await fetchUser())?.role === "admin",
  redirectTo: "/login",
};

export default function SettingsLayout() {
  return (
    <div>
      <nav>...</nav>
      <Outlet />
    </div>
  );
}

Set a default redirect when creating routes:

createRoutes({
  pagesGlob: pages,
  pagesDir: "pages",
  defaultRedirectTo: "/login",
});

2. <Protect> component

Use <Protect> when you want to guard content inside a layout (e.g. wrap <Outlet />) or need a stable condition with a fallback UI.

Predefined config (recommended): define condition, redirectTo, and fallback once, then reuse with <Protect {...config}>.

// src/config/protect.tsx
import type { ProtectConfig } from "@epicabdou/linkr";

export const authProtect: ProtectConfig = {
  condition: () => !!localStorage.getItem("token"),
  redirectTo: "/login",
  fallback: <div>Checking access…</div>,
};
// In any layout
import { Protect } from "@epicabdou/linkr";
import { Outlet } from "react-router";
import { authProtect } from "../config/protect";

export default function DashboardLayout() {
  return (
    <Protect {...authProtect}>
      <Outlet />
    </Protect>
  );
}

Props:

  • condition: sync or async function; return true to allow, false to redirect.
  • redirectTo: path to redirect to when the condition fails.
  • fallback: optional React node shown while an async condition is pending.
  • children: content to render when the condition is true.

You can define multiple configs (e.g. authProtect, adminProtect) and import the one you need.


API exports

| Export | Description | |--------|-------------| | createRoutes | Builds React Router route array from pages glob and options. | | LinkrApp | Component that renders RouterProvider with routes from options. | | createRootWithLinkr | One-shot: createRoot, createRoutes, StrictMode + RouterProvider. | | Protect | Component for conditional redirect with fallback UI. | | normalizePath, parseSegment, compareSegments | Path utilities (for advanced use). |

Types: CreateRoutesOptions, LayoutMap, LayoutsGlob, PagesGlob, RouteObject, RouteProtect, ProtectProps, ProtectConfig, LinkrAppOptions, ParsedSegment, SegmentKind.


Quick start with CLI

npx create-linkrjs-app my-app
cd my-app && pnpm dev

Build & test

From the monorepo root:

pnpm --filter @epicabdou/linkr build
pnpm --filter @epicabdou/linkr test

Or from packages/linkr:

pnpm build
pnpm test