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

@miurajs/miura-router

v0.1.6

Published

Modern, declarative routing for miura applications. Built for Web Components, the router handles hash/history/memory navigation modes, async guards, data loaders, redirects, and DOM rendering hooks.

Readme

@miurajs/miura-router

Modern, declarative routing for miura applications. Built for Web Components, the router handles hash/history/memory navigation modes, async guards, data loaders, redirects, and DOM rendering hooks.

✨ Features

  • Multiple navigation modes: hash, history, or in-memory for tests.
  • Guards & loaders: resolve access/gate data before components render.
  • Nested routes & redirects: declarative tree definitions.
  • Type-safe route params: defineRoute<TParams>() with typed buildPath() and navigate().
  • Runtime param validation: optional Zod / Valibot / ArkType schema on any route.
  • Event-driven: emits lifecycle events through the framework EventBus.
  • Performance hooks: timing integration via PerformanceMonitor.

🚦 Quick Start

import { createRouter } from '@miurajs/miura-router';

const router = createRouter({
  mode: 'history',
  base: '/',
  fallback: '/login',
  routes: [
    { path: '/', component: 'app-home' },
    {
      path: '/admin',
      component: 'app-admin',
      renderZone: '#primary',
      guards: [async ({ data }) => (data.user?.isAdmin ? true : '/login')],
      loaders: [async () => ({ stats: await fetchStats() })],
    },
  ],
  render: (context) => mountIntoDom(context),
  eventBus,
  performance,
});

await router.start();

🛡️ Route Guards

Guards run before loaders/rendering. They may:

  • return true/void to continue
  • return false to block (router:blocked event)
  • return a string path (sync/async) to redirect
const routes = [
  {
    path: '/dashboard',
    component: 'app-dashboard',
    guards: [async ({ data }) => (data.session ? true : '/login')],
  },
];

📦 Route Loaders

Loaders run after guards and before rendering. Existing function loaders still work as before and their returned objects are shallow-merged into context.data.

const routes = [
  {
    path: '/profile/:id',
    component: 'app-profile',
    loaders: [
      ({ params }) => ({ profile: fetchProfile(params.id) }),
      async ({ params }) => ({ permissions: await fetchPermissions(params.id) }),
    ],
  },
];

Access loader results inside the render callback (or components via routeContext) through context.data.

For richer route state, you can also use named loaders:

const routes = [
  {
    path: '/profile/:id',
    component: 'app-profile',
    loaders: [
      {
        key: 'profile',
        load: ({ params }) => fetchProfile(params.id),
      },
      {
        key: 'permissions',
        optional: true,
        load: async ({ params }) => fetchPermissions(params.id),
      },
    ],
  },
];

Named loader results are exposed under context.data.<key>, and the full loader lifecycle is available on context.loaders:

  • context.loaders.status
  • context.loaders.entries.profile
  • context.loaders.entries.permissions
  • context.loaders.error

Optional loaders may fail without aborting navigation, which makes it easier to render partial route data.

🗂️ Nested Routes & Layout Outlets

Define a children array on any route to create a parent/child hierarchy. The parent route acts as a layout shell; the matched child fills the <miura-router-outlet> inside it.

const routes = [
  {
    path: '/app',
    component: 'app-layout',      // renders the shell + <miura-router-outlet>
    children: [
      { path: 'dashboard', component: 'app-dashboard' },
      { path: 'settings',  component: 'app-settings'  },
      { path: 'profile/:id', component: 'app-profile' },
    ],
  },
];
// app-layout component
template() {
  return html`
    <nav>...</nav>
    <main>
      <miura-router-outlet></miura-router-outlet>   <!-- child component mounts here -->
    </main>
  `;
}

context.matched contains the full chain from root to leaf, so nested outlets at any depth receive the correct slice of the matched array.

🔗 <miura-router-outlet>

The <miura-router-outlet> custom element is a passive mount-point. The router's render callback uses context.matched to determine which components to mount at each level.

import { RouterOutlet } from '@miurajs/miura-router';
// RouterOutlet registers itself as <miura-router-outlet> when imported

� Redirects

{ path: '/old-path', redirect: '/new-path' }
{ path: '/dynamic', redirect: (ctx) => `/target/${ctx.params.id}` }

�📢 Router Events

| Event | When | |-------|------| | router:setup | Router initialised | | router:navigating | Navigation started | | router:navigated | Navigation committed | | router:blocked | Guard returned false | | router:not-found | No matching route | | router:error | Unhandled navigation error | | router:rendered | Render callback completed |

🛠️ Router API

| Method | Description | |--------|-------------| | router.navigate(path, opts?) | Push a new entry and navigate | | router.replace(path, opts?) | Replace current entry and navigate | | router.back() | Go back in history | | router.forward() | Go forward in history | | router.current | Current RouteContext | | router.previous | Previous RouteContext | | router.start() | Start listening to navigation events | | router.stop() | Stop listening (keeps state) | | router.destroy() | Full teardown |

navigate() and replace() both return Promise<NavigationResult>:

const result = await router.navigate('/dashboard');
if (!result.ok) console.log('blocked:', result.reason);

🔷 Type-Safe Route Params

Use defineRoute<TParams>() to get typed buildPath() and navigate() helpers with compile-time safety on route params.

import { defineRoute, createRouter } from '@miurajs/miura-router';

// No params
const homeRoute  = defineRoute({ path: '/', component: 'app-home' });

// Single param — TypeScript enforces { id: string }
const userRoute  = defineRoute<{ id: string }>({
  path: '/users/:id',
  component: 'user-page',
});

// Multiple params
const postRoute  = defineRoute<{ userId: string; postId: string }>({
  path: '/users/:userId/posts/:postId',
  component: 'post-page',
});

// Pass records to createRouter
const router = createRouter({
  mode: 'history',
  routes: [homeRoute.record, userRoute.record, postRoute.record],
  render: (ctx) => mountComponent(ctx),
});

// Typed navigation — TS error if a param is missing or wrong type
await userRoute.navigate(router, { id: '42' });
await postRoute.navigate(router, { userId: '1', postId: '99' });

// Build path without navigating
userRoute.buildPath({ id: '42' });      // → '/users/42'
postRoute.buildPath({ userId: '1', postId: '99' }); // → '/users/1/posts/99'

Runtime validation with Zod

Pass any Zod-compatible schema as a second argument. Params are validated (and coerced) after every match, before guards run.

import { z } from 'zod';

const UserParams = z.object({ id: z.string().regex(/^\d+$/) });

const userRoute = defineRoute(
  { path: '/users/:id', component: 'user-page' },
  UserParams, // ← schema
);

// Navigation to /users/abc → NavigationResult { ok: false, reason: 'error' }
// Navigation to /users/42  → proceeds normally, ctx.params.id is '42'

The ParamsSchema interface is minimal — anything with safeParse() works (Zod, Valibot, ArkType, custom).

🧪 Testing

Use mode: 'memory' to avoid touching the real browser location. Provide a spy render callback to inspect contexts:

const renders: RouteRenderContext[] = [];
const router = createRouter({
  mode: 'memory',
  routes,
  render: (ctx) => { renders.push(ctx); },
});
await router.start();
await router.navigate('/dashboard');
assert.equal(renders.at(-1)?.route.component, 'app-dashboard');

The repository contains test/router.guards-loaders.test.ts covering redirects, blocks, and loader merges.

📚 Framework Integration

MiuraFramework wires this router automatically. Define a static router config in your framework subclass, and the framework handles instantiation, DOM zones, and navigation helpers (navigate, replaceRoute, goBack, goForward).

When a route defines meta.title, MiuraFramework also updates document.title automatically:

{
  path: '/dashboard',
  component: 'app-dashboard',
  meta: {
    title: ({ data }) => `Dashboard (${data.stats?.total ?? 0})`,
  },
}