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

@zomme/bun-plugin-tsr

v0.2.1

Published

Bun plugin for TanStack Router route generation with file-based routing

Readme

@zomme/bun-plugin-tsr

Bun plugin for TanStack Router route generation with file-based routing.

Features

  • Automatic route tree generation from file structure
  • Watch mode for development (auto-regenerates on file changes)
  • Configurable via bunfig.toml
  • Supports multiple root directories
  • Full TypeScript support

Installation

bun add -d @zomme/bun-plugin-tsr

Install peer dependencies:

bun add @tanstack/react-router
bun add -d @tanstack/router-generator

Usage

Basic Setup

Add the plugin to your bunfig.toml:

[serve.static]
plugins = ["@zomme/bun-plugin-tsr"]

Or use it programmatically:

import tsr from "@zomme/bun-plugin-tsr";

Bun.build({
  entrypoints: ["./src/index.tsx"],
  plugins: [tsr],
});

Configuration

Configure via bunfig.toml:

[plugins.tsr]
rootDirectory = "src"
routesDirectory = "./routes"
generatedRouteTree = "routeTree.gen.ts"
quoteStyle = "double"
routeFileIgnorePattern = ".test."

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | rootDirectory | string \| string[] | "src" | Root directory for routes | | routesDirectory | string | "./routes" | Routes directory relative to root | | generatedRouteTree | string | "routeTree.gen.ts" | Output file name | | quoteStyle | "single" \| "double" | "double" | Quote style in generated code | | routeFileIgnorePattern | string | ".test." | Pattern to ignore route files |

Directory Structure

Create routes using file-based routing:

src/
├── routes/
│   ├── __root.tsx         # Root layout
│   ├── index.tsx          # / (home page)
│   ├── about.tsx          # /about
│   ├── dashboard/
│   │   ├── index.tsx      # /dashboard
│   │   ├── settings.tsx   # /dashboard/settings
│   │   └── $userId.tsx    # /dashboard/:userId (dynamic)
│   └── posts/
│       ├── index.tsx      # /posts
│       └── $postId.tsx    # /posts/:postId (dynamic)
├── routeTree.gen.ts       # Generated route tree (auto)
└── main.tsx

Route File Naming Conventions

| File Name | Route Path | Description | |-----------|------------|-------------| | index.tsx | / | Index route | | about.tsx | /about | Static route | | $userId.tsx | /:userId | Dynamic parameter | | posts.$postId.tsx | /posts/:postId | Nested dynamic | | __root.tsx | - | Root layout | | -components/ | - | Private folder (ignored) |

Example

Root Layout

// src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";

export const Route = createRootRoute({
  component: RootComponent,
});

function RootComponent() {
  return (
    <div>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/dashboard">Dashboard</Link>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

Index Route

// src/routes/index.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
  component: HomePage,
});

function HomePage() {
  return <h1>Welcome Home</h1>;
}

Dynamic Route

// src/routes/posts/$postId.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/posts/$postId")({
  component: PostPage,
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId);
    return { post };
  },
});

function PostPage() {
  const { post } = Route.useLoaderData();
  return <article>{post.title}</article>;
}

App Entry Point

// src/main.tsx
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { createRoot } from "react-dom/client";
import { routeTree } from "./routeTree.gen";

const router = (import.meta.hot.data.router ??= createRouter({ routeTree }));
router.update({ routeTree });

declare module "@tanstack/react-router" {
  interface Register {
    router: typeof router;
  }
}

const root = (import.meta.hot.data.root ??= createRoot(document.getElementById("root")!));
root.render(<RouterProvider router={router} />);

import.meta.hot.accept();

Important: The plugin appends import.meta.hot.accept() to the generated routeTree.gen.ts so it acts as an HMR boundary. When any route file changes, Bun re-evaluates the route tree, which bubbles up to your entry point (also self-accepting). On re-evaluation, ??= returns the existing router and router.update({ routeTree }) patches it with the new route tree. You must:

  1. Persist the router with import.meta.hot.data.router so it survives re-evaluations.
  2. Call router.update({ routeTree }) right after — on first load this is a no-op, on HMR it patches the existing router with the re-imported route tree.

Without this, editing route files will cause the page to crash or full-reload.

HMR Caveats

__root.tsx and module-level constructors

Because the generated routeTree.gen.ts self-accepts HMR, editing any route file causes Bun to re-evaluate the route tree, which re-imports every route module — including __root.tsx.

If __root.tsx (or a module it imports) calls a constructor at the top level (e.g. new QueryClient()), the re-evaluation cascade can leave that dependency in an inconsistent state, producing errors like:

TypeError: QueryClient is not a constructor

How to fix:

  1. Persist instances with import.meta.hot.data — the same pattern used for the router. This skips the constructor on re-evaluation:

    // helpers/query-client.ts
    import { QueryClient } from "@tanstack/react-query";
    
    export const queryClient = (import.meta.hot.data.queryClient ??=
      new QueryClient({ defaultOptions: { queries: { staleTime: 60_000 } } }));
  2. Move providers to the entry point — keep __root.tsx free of imports from libraries that export classes/constructors used at module scope. Wrapping RouterProvider with QueryClientProvider in your entry point removes the problematic import from __root.tsx entirely:

    // main.tsx
    import { QueryClientProvider } from "@tanstack/react-query";
    import { queryClient } from "./helpers/query-client";
    
    root.render(
      <QueryClientProvider client={queryClient}>
        <RouterProvider router={router} />
      </QueryClientProvider>,
    );

Rule of thumb: any singleton created with new that __root.tsx depends on should be persisted via import.meta.hot.data.xxx ??= to survive HMR re-evaluations.

Watch Mode

In development (NODE_ENV !== "production"), the plugin automatically watches for file changes:

  • Create/Delete: Route files are detected and the route tree is regenerated
  • Update: Content changes are handled by Bun HMR — the route tree re-evaluates automatically via its import.meta.hot.accept() boundary
# Development - watch mode enabled
bun dev

# Production - no watch mode
NODE_ENV=production bun build

Multiple Root Directories

Support multiple entry points:

[plugins.tsr]
rootDirectory = ["apps/web/src", "apps/admin/src"]

Generated Route Tree

The plugin generates a routeTree.gen.ts file:

// This file is auto-generated by @zomme/bun-plugin-tsr

import { Route as rootRoute } from "./routes/__root";
import { Route as IndexRoute } from "./routes/index";
import { Route as AboutRoute } from "./routes/about";
import { Route as DashboardIndexRoute } from "./routes/dashboard/index";

const routeTree = rootRoute.addChildren([
  IndexRoute,
  AboutRoute,
  DashboardIndexRoute,
]);

export { routeTree };

How It Works

  1. Scan: Plugin scans routesDirectory for route files
  2. Parse: Analyzes file names for route patterns
  3. Generate: Creates route tree with proper nesting
  4. Watch: In dev mode, watches for changes and regenerates

Requirements

  • Bun >= 1.0.0
  • @tanstack/react-router >= 1.0.0
  • @tanstack/router-generator >= 1.0.0

License

MIT