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

@plumile/router

v0.1.38

Published

A modern, type-safe React router with code splitting, data preloading, and Suspense integration

Readme

@plumile/router

A modern, type-safe React router built with TypeScript that supports code splitting, data preloading, and React Suspense integration.

Features

  • Type-safe routing with full TypeScript support
  • Code splitting with dynamic imports and lazy loading
  • Data preloading for faster navigation
  • React Suspense integration for smooth loading states
  • Browser history management with push/replace state
  • Nested routing support
  • Route-based redirects
  • Active link detection with exact/partial matching

Installation

npm install @plumile/router

Peer Dependencies

This package expects react and react-dom to be available in your project (React 18 or 19).

npm install react react-dom

Module Entry Points

  • @plumile/router: primary ESM entry exporting runtime APIs and types.
  • @plumile/router/lib/esm/*: direct file imports when you need to tree-shake specific helpers.
  • @plumile/router/lib/types/*: TypeScript declaration files (consumed automatically via types field).

This package is ESM-only. If your tooling expects CommonJS, enable ESM support (e.g. Vite, Next.js, or webpack with type: 'module').

Documentation & Guides

Quick Start

1. Define Your Routes

import { Route, getResourcePage } from '@plumile/router';

const routes: Route<any, any>[] = [
  {
    path: '/',
    resourcePage: getResourcePage('Home', () => import('./pages/Home')),
  },
  {
    path: '/about',
    resourcePage: getResourcePage('About', () => import('./pages/About')),
  },
  {
    path: '/users',
    children: [
      {
        path: '/:id',
        resourcePage: getResourcePage(
          'UserProfile',
          () => import('./pages/UserProfile'),
        ),
        prepare: ({ variables }) => {
          // Preload user data
          return { userId: variables.id };
        },
      },
    ],
  },
];

2. Create the Router

import { createRouter } from '@plumile/router';

export const router = createRouter(routes);
const { context, cleanup } = router;
// Call cleanup() when tearing the app down (tests, SSR shell disposal, etc.).

3. Provide Router Context

import React from 'react';
import { RoutingContext, RouterRenderer } from '@plumile/router';
import { router } from './router';

const { context } = router;

function App() {
  return (
    <RoutingContext.Provider value={context}>
      <RouterRenderer fallback={<div>Loading...</div>} />
    </RoutingContext.Provider>
  );
}

4. Navigation with Links

import { Link } from '@plumile/router';

function Navigation() {
  return (
    <nav>
      <Link to="/" exact activeClassName="active">
        Home
      </Link>
      <Link to="/about" activeClassName="active">
        About
      </Link>
      <Link to="/users/123" activeClassName="active">
        User Profile
      </Link>
    </nav>
  );
}

Hooks Overview

| Hook | Signature | Purpose | Example | | ------------------------------ | ------------------------------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | useNavigate() | () => Navigate | Imperative navigation with type-safe params and filters. | const navigate = useNavigate(); navigate({ pathname: '/products', filters: { page: { eq: 2 } } }); | | useFilters(schema) | (schema) => [filters, actions] | Read and mutate filters inferred from a schema. | const [filters, actions] = useFilters(productFilters); actions.set('price', 'gt', 10); | | useQuery() | () => Record<string, string \| string[]> | Access the raw query aggregation (legacy/simple use). | const query = useQuery(); | | useQueryState(key, options?) | (key, options?) => [value, setValue] | Two-way binding for a single query parameter with default/replace options. | const [page, setPage] = useQueryState('page', { defaultValue: 1 }); | | useFilterDiagnostics() | () => Diagnostic[] | Surface parsing issues (unknown fields/operators) for UI or logging. | const diagnostics = useFilterDiagnostics(); | | useAllQuery(options?) | (options?) => QueryLike | Merge filters and raw query, helpful during migrations. | const all = useAllQuery(); |

API Reference

Core Components

createRouter(routes: Route[])

Creates a router instance with the given route configuration.

Parameters:

  • routes: Array of route definitions

Returns:

  • context: Router context object for the React Context Provider
  • cleanup: Function to clean up router listeners

RouterRenderer

Renders the matched route component with Suspense support.

Props:

  • fallback?: ReactNode - Fallback UI while loading components
  • enableTransition?: boolean - Enable React 18 transitions
  • pending?: ReactNode - UI to show during transitions

Link

Navigation component that handles client-side routing.

Props:

  • to: string - Destination path
  • exact?: boolean - Exact path matching for active state
  • activeClassName?: string - CSS class when link is active
  • className?: string - Base CSS class
  • preload?: boolean - Preload route on hover
  • replace?: boolean - Replace current history entry

RoutingContext

React context that provides router functionality to components.

Route Configuration

Route<TPrepared, TVariables>

Route definition interface.

Properties:

  • path?: string - URL path pattern
  • children?: Route[] | Redirect[] - Nested routes
  • resourcePage?: ResourcePage - Lazy-loaded component
  • prepare?: Function to preload data
  • render?: Custom render function

Redirect

Redirect configuration.

Properties:

  • path?: string - Source path
  • to: string - Destination path
  • status?: 301 | 302 - HTTP status code

Resource Management

getResourcePage(moduleId: string, loader: ResourcePageLoader)

Creates a resource for lazy-loading components.

Parameters:

  • moduleId: Unique identifier for caching
  • loader: Function that returns dynamic import

Returns:

  • ResourcePage instance

ResourcePage

Manages lazy-loaded components with Suspense integration.

Methods:

  • load(): Promise - Load the component
  • get(): Component | undefined - Get loaded component
  • read(): Component - Read with Suspense (throws Promise if loading)

History Management

BrowserHistory

Browser history implementation.

Methods:

  • push(location): Navigate to new location
  • set(location): Replace current location
  • subscribe(listener): Listen for navigation changes

Utilities

getMatchedRoute(routes, location)

Finds the matching route for a given location.

prepareMatch(match)

Prepares route data and components for rendering.

r<TPrepared, TVariables>(route)

Type helper for strongly-typed route definitions.

Unified Query & Filter Model

The router now unifies page-like query parameters and structured filters under a single model powered by @plumile/filter-query.

Key points:

  • A route can declare a querySchema (filter schema) on its deepest branch.
  • All URL parameters (simple key=value and filter operators like price.gt=10) are parsed into a single filters object.
  • Equality is implicit: field=value maps to internal operator eq.
  • The current active schema is exposed as entry.activeQuerySchema for tooling.
  • Serialization is centralized via buildCombinedSearch (used by both navigate and Link).

Defining a Schema

import { r } from '@plumile/router';
import { defineSchema, numberField, stringField } from '@plumile/filter-query';

const productFilters = defineSchema({
  page: numberField(),
  price: numberField(),
  title: stringField(),
});

const routes = [
  r({
    path: '/products',
    querySchema: productFilters,
    prepare: ({ filters }) => ({ page: filters.page?.eq ?? 1 }),
    render: () => null,
  }),
];

Accessing Filters (Strongly Typed)

useFilters now requires the schema as a mandatory argument (dynamic discovery was removed). It returns a tuple [filters, actions] where filters is fully inferred from the provided schema and actions are strongly typed mutation helpers.

import { useFilters } from '@plumile/router';
import { productFilters } from './schemas'; // assume you exported the schema above

function List() {
  const [filters, { set, remove, merge, clear }] = useFilters(productFilters);
  // All operators/values are type‑checked against the schema
  set('price', 'gt', 10); // => ?price.gt=10
  set('page', 'eq', 2); // => ?page=2 (page normalization will clamp <1 to 1)
  remove('price', 'gt'); // remove only that operator
  merge({ price: { between: [10, 20] }, title: { contains: 'ultra' } });
  clear(); // remove all active filters
  return null;
}

Programmatic Navigation

context.navigate({ pathname: '/products', filters: { page: { eq: 3 } } });

Page Normalization

The router clamps page.eq < 1 to 1 synchronously and performs a history replace so the back stack is not polluted.

Serialization Rules

buildCombinedSearch produces a leading ? (or empty string) by:

  1. Ordering simple query params (from schema order).
  2. Appending filter operator segments (field.operator=value).
  3. Using implicit equality (field=value).
  4. Maintaining reference stability (no unnecessary object churn).

Migration Note

The legacy query DSL (q.*, parseTypedQuery, buildSearch) has been removed. Replace any usage with a filter schema (defineSchema) and rely on useFilters, navigate({ filters }), and/or buildCombinedSearch.

Performance & Stability

Filters cache: repeated navigations with semantically identical search strings reuse the same filter object reference, enabling cheap React renders.

Example Link

<Link to="/products" filters={{ page: { eq: 1 }, price: { gt: 10 } }}>
  Deals
</Link>

Renders: /products?page=1&price.gt=10.

Manual Serialization Example

If you need to build a URL outside of navigation (e.g. constructing a sitemap entry):

import { buildCombinedSearch } from '@plumile/router';
// or: import buildCombinedSearch from '@plumile/router/lib/esm/routing/tools/buildCombinedSearch.js';

const search = buildCombinedSearch({
  filters: { page: { eq: 1 }, price: { gt: 10 } },
  querySchema: productFilters,
});
// => '?page=1&price.gt=10'

Guideline: If you need to derive lightweight projections (e.g. const { page } = typed), you can still destructure; but avoid spreading into a new object if you rely on reference equality downstream.

Inspection & Instrumentation

Activez l’instrumentation en développement pour exposer un bridge DevTools :

import {
  createRouter,
  createDevtoolsBridgeInstrumentation,
} from '@plumile/router';

const devtools = createDevtoolsBridgeInstrumentation();
const { context } = createRouter(routes, { instrumentations: [devtools] });

Aucun bridge n’est publié en production. Vous pouvez chaîner plusieurs instrumentations (par exemple la console) :

import {
  createConsoleLoggerInstrumentation,
  createDevtoolsBridgeInstrumentation,
} from '@plumile/router';

const instrumentations = [
  createDevtoolsBridgeInstrumentation(),
  createConsoleLoggerInstrumentation({ label: 'router' }),
];

createRouter(routes, { instrumentations });

Pour l’inspection visuelle (timeline, filtres, prepared data), installez la Plumile Router DevTools extension (voir docs/router-devtools-extension.md).

Unified Query Schema & useFilters (Integration with @plumile/filter-query)

The router uses a single unified schema (querySchema) defined with @plumile/filter-query to describe allowed key/operator pairs. Plain field=value maps to the implicit equality operator (eq). Each filter segment is encoded as field.operator=value (with eq omitted).

import { r, useFilters } from '@plumile/router';
import { defineSchema, numberField, stringField } from '@plumile/filter-query';

const querySchema = defineSchema({
  page: numberField(),      // page.eq=2 => ?page=2 (page normalization clamps <1 to 1)
  price: numberField(),     // price.gt=10 => ?price.gt=10
  name: stringField(),      // name.contains=ultra => ?name.contains=ultra
});

export const routes = [
  r({
    path: '/items',
    querySchema,
    render: () => <Items />,
  }),
];

function Items() {
  const [filters, { set, remove, clear, merge }] = useFilters(querySchema);
  set('price', 'gt', 10);                 // => ?price.gt=10
  set('page', 'eq', 2);                   // => ?page=2
  remove('price', 'gt');                  // remove only that operator
  merge({ price: { between: [10, 20] } }); // descriptor-array form removed; use object patch
  clear();
  return null;
}

navigate({ filters }) is available for programmatic updates. Serialization order: schema field keys first, then any extra non‑schema keys (if present). Operators follow deterministic ordering from @plumile/filter-query.

Typed Actions Cheat‑Sheet

const [filters, a] = useFilters(querySchema);
a.set('price', 'in', [10, 20, 30]); // array operators allowed when schema supports it
a.set('title', 'contains', 'ultra');
a.remove('price', 'in'); // remove only that operator
a.merge({ page: { eq: 3 }, price: { gt: 50 } }); // deep merge per field
a.clear(); // wipe everything (noop if already empty)

Navigation is skipped automatically if the structural filters object doesn't change (shallow compare) to avoid redundant history entries.

Diagnostics (unknown field/operator, invalid values, etc.) are accessible via useFilterDiagnostics().

ESLint Rule: no-direct-window-location-search

To encourage consistent usage of the query hooks, a custom rule is provided inside the router package to flag raw window.location.search access.

Add to your flat ESLint config:

import noDirectWindowLocationSearch from '@plumile/router/lib/eslint-rules/no-direct-window-location-search.js';

export default [
  {
    plugins: {
      '@plumile-router/dx': {
        rules: {
          'no-direct-window-location-search': noDirectWindowLocationSearch,
        },
      },
    },
    rules: {
      '@plumile-router/dx/no-direct-window-location-search': 'warn',
    },
  },
];

Optional configuration:

// allow some files (e.g. legacy bootstrap) to keep direct access
rules: {
  '@plumile-router/dx/no-direct-window-location-search': [
    'warn',
    { allowInFiles: ['legacy-entry.ts'] },
  ],
},

When triggered, replace patterns like:

const qs = window.location.search;

with:

const query = useQuery(); // raw key=value aggregation (simple); prefer useFilters() for structured access.

Migration Guide (Legacy DSL → Unified Filters + Typed Hook Update)

  1. Remove legacy imports of q, parseTypedQuery, buildSearch.
  2. Define a schema with defineSchema from @plumile/filter-query and attach as querySchema on the route needing filters.
  3. Replace (removed) useTypedQuery() usages with useFilters(querySchema) (schema argument now mandatory – breaking change vs earlier experimental auto‑discovery version).
  4. Update navigation: navigate({ filters: { page: { eq: 2 } } }) (unchanged pattern).
  5. Update links: <Link to="/x" filters={{ page: { eq: 2 } }} />.
  6. Replace any previous merge([{ field, op, value }]) descriptor array calls with merge({ field: { op: value } }) object patches.
  7. Page defaults / normalization: rely on built-in clamp (page < 1 -> 1).
  8. Remove any type tests referencing the DSL; rely on schema inference from defineSchema.
  9. If you manually used buildCombinedSearch({ schema }), rename the option to querySchema.

Removed & Changed APIs

Removed in favor of the unified model:

  • q.* descriptor DSL (use defineSchema)
  • parseTypedQuery
  • buildSearch (use buildCombinedSearch)
  • useTypedQuery

Changed (breaking):

  • useFilters()useFilters(schema) (schema argument required; no dynamic discovery)
  • merge action signature: descriptor array → object patch (merge({ field: { operator: value } }))
  • buildCombinedSearch({ schema })buildCombinedSearch({ querySchema })

Use @plumile/filter-query for schema definitions and useFilters(schema) / buildCombinedSearch for runtime access & serialization.

useQueryState Hook

useQueryState(key, opts?) creates a controlled binding between a single query parameter and component state.

const [page, setPage] = useQueryState<number>('page');
// Increment page without pushing a new history entry
setPage(page! + 1, { replace: true });

Behavior:

  • Reads from filters (implicit equality) and falls back to raw query key when not covered by schema.
  • Allows defaultValue override in options and omits key when value equals default (with omitIfDefault: true).
  • Pass { raw: true } to force raw (string) source for incremental migrations.
  • Uses unified serialization ordering (schema keys then extras; operators deterministic).

Options: { defaultValue?, omitIfDefault?: boolean = true, replace?: boolean, raw?: boolean }

Data Preloading

const route: Route<{ user: User }, { id: string }> = {
  path: '/users/:id',
  prepare: async ({ variables }) => {
    const user = await fetchUser(variables.id);
    return { user };
  },
  render: ({ prepared, children }) => {
    if (!prepared) return null;
    return <UserLayout user={prepared.user}>{children}</UserLayout>;
  },
};

Custom Route Rendering

const route: Route<any, any> = {
  path: '/protected',
  render: ({ children, prepared }) => {
    if (!userIsAuthenticated()) {
      return <Redirect to="/login" />;
    }
    return <ProtectedLayout>{children}</ProtectedLayout>;
  },
};

Programmatic Navigation

import { useContext } from 'react';
import { RoutingContext } from '@plumile/router';

function MyComponent() {
  const router = useContext(RoutingContext);

  const handleClick = () => {
    router.history.push({ pathname: '/new-path' });
  };

  return <button onClick={handleClick}>Navigate</button>;
}

Route Preloading

import { useContext } from 'react';
import { RoutingContext } from '@plumile/router';

function MyComponent() {
  const router = useContext(RoutingContext);

  const handleHover = () => {
    // Preload code only
    router.preloadCode({ pathname: '/users/123' });

    // Preload code and data
    router.preload({ pathname: '/users/123' });
  };

  return (
    <Link to="/users/123" onMouseEnter={handleHover}>
      User Profile
    </Link>
  );
}

TypeScript Support

The router is built with TypeScript and provides full type safety:

import { Route, r } from '@plumile/router';

interface UserPageData {
  user: User;
  posts: Post[];
}

interface UserPageParams {
  id: string;
}

const userRoute = r<UserPageData, UserPageParams>({
  path: '/users/:id',
  prepare: ({ variables }) => {
    // variables.id is typed as string
    return fetchUserData(variables.id);
  },
  render: ({ prepared }) => {
    // prepared is typed as UserPageData | undefined
    if (!prepared) return null;
    return <UserPage user={prepared.user} posts={prepared.posts} />;
  },
});

Browser Support

  • Modern browsers that support ES2021
  • React 18+
  • Node.js 21+

License

Licensed under the terms specified in the package's LICENSE file.