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

@wavelengthusaf/client-router

v0.0.4

Published

Native TypeScript Router

Readme

Wavelength Router

A lightweight, native TypeScript web components router for modern web applications.

Table of Contents


Getting Started

Prerequisites

  • Node.js 18+ and npm
  • A modern browser with native web components support

Installation

Clone the repository and install dependencies:

git clone https://github.com/your-org/wavelength-router.git
cd wavelength-router
npm install

Husky Setup

This project uses Husky to run linting and tests before each commit. If you're setting up the project for the first time or if Husky hooks aren't working, initialize it with:

npm run prepare

Or manually initialize:

npx husky init

This ensures that before every commit, Husky will:

  1. Run unit tests via npm run test
  2. Run ESLint and Prettier via npm run lint

If either fails, the commit will be blocked.


Adding to Your Project

Option 1: npm Package (Published)

Once published to npm, install the package:

npm install @wavelengthusaf/client-router

Then import the elements in your entry point:

// main.ts or main.js
import '@wavelengthusaf/client-router';

Option 2: Local Development

For local development or testing changes:

npm run build

This compiles TypeScript to dist/. Then in your consuming project:

import '@wavelengthusaf/client-router';

Usage

You can implement routing using either the HTML template approach or pure TypeScript.

HTML Template Approach

Use the custom elements directly in your HTML. This is ideal for simpler applications or when you want declarative routing.

Basic Example

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My App</title>
  </head>
  <body>
    <!-- Navigation -->
    <nav>
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="/users/123">User Profile</a>
    </nav>

    <!-- Router Container - renders matched route component here -->
    <wavelength-router>
      <wavelength-route path="/" component="home-page"></wavelength-route>
      <wavelength-route path="/about" component="about-page"></wavelength-route>
      <wavelength-route
        path="/users/:userId"
        component="user-page"
      ></wavelength-route>
      <wavelength-route path="*" component="not-found-page"></wavelength-route>
    </wavelength-router>

    <script type="module">
      import '@wavelengthusaf/client-router';

      // Define your page components as custom elements
      customElements.define(
        'home-page',
        class extends HTMLElement {
          connectedCallback() {
            this.innerHTML = '<h1>Welcome Home</h1>';
          }
        },
      );

      customElements.define(
        'about-page',
        class extends HTMLElement {
          connectedCallback() {
            this.innerHTML = '<h1>About Us</h1><p>We are awesome.</p>';
          }
        },
      );

      customElements.define(
        'user-page',
        class extends HTMLElement {
          connectedCallback() {
            // Access route parameters via routeContext
            const userId = this.routeContext?.routeParams?.userId;
            this.innerHTML = `<h1>User Profile</h1><p>User ID: ${userId}</p>`;
          }
        },
      );

      customElements.define(
        'not-found-page',
        class extends HTMLElement {
          connectedCallback() {
            this.innerHTML = '<h1>404 - Page Not Found</h1>';
          }
        },
      );
    </script>
  </body>
</html>

How It Works

  1. <wavelength-router> acts as the router container and outlet
  2. It scans for <wavelength-route> children and extracts route definitions
  3. When the URL changes, the router matches the path and mounts the corresponding component
  4. The component is rendered inside the <wavelength-router> element

TypeScript Approach

For more control, you can use the Router class directly and define routes programmatically.

Basic Example

import { Router } from '@wavelengthusaf/client-router';
import type {
  RouteContext,
  RouteDefinition,
  RouteGuard,
} from '@wavelengthusaf/client-router';

// Define your page components
class HomePage extends HTMLElement {
  connectedCallback() {
    this.innerHTML = '<h1>Welcome Home</h1>';
  }
}

class AboutPage extends HTMLElement {
  connectedCallback() {
    this.innerHTML = '<h1>About Us</h1>';
  }
}

class UserPage extends HTMLElement {
  connectedCallback() {
    const userId = this.routeContext?.routeParams?.userId;
    this.innerHTML = `<h1>User Profile</h1><p>User ID: ${userId}</p>`;
  }
}

// Register components
customElements.define('home-page', HomePage);
customElements.define('about-page', AboutPage);
customElements.define('user-page', UserPage);

// Define routes using RouteDefinition objects
const routes: RouteDefinition[] = [
  {
    path: '/',
    routeComponent: {
      mount(container: HTMLElement, context: RouteContext) {
        const element = document.createElement('home-page') as HTMLElement & {
          routeContext?: RouteContext;
        };
        element.routeContext = context;
        container.appendChild(element);
      },
      unmount() {
        // Optional cleanup
      },
    },
  },
  {
    path: '/about',
    routeComponent: {
      mount(container: HTMLElement, context: RouteContext) {
        const element = document.createElement('about-page') as HTMLElement & {
          routeContext?: RouteContext;
        };
        element.routeContext = context;
        container.appendChild(element);
      },
    },
  },
  {
    path: '/users/:userId',
    routeComponent: {
      mount(container: HTMLElement, context: RouteContext) {
        const element = document.createElement('user-page') as HTMLElement & {
          routeContext?: RouteContext;
        };
        element.routeContext = context;
        container.appendChild(element);
      },
    },
  },
];

// Create and start the router
const router = new Router(routes, {
  outlet: document.getElementById('app')!, // Container element
  hashMode: false, // Use hash routing (optional)
});

router.start();

RouteDefinition Structure

Each route requires:

  • path: The URL path pattern (supports :param syntax for dynamic segments)
  • routeComponent: An object with a mount function (and optional unmount)
interface RouteDefinition {
  path: string; // e.g., '/users/:id' or '/products/:category/:id'
  routeComponent: {
    mount(container: HTMLElement, context: RouteContext): void;
    unmount?(): void; // Optional cleanup function
  };
}

API Reference

<wavelength-router> Attributes

| Attribute | Type | Description | | ----------- | ------ | ------------------------------------------------------- | | hash-mode | flag | Enable hash-based routing (#/path) instead of history | | guard | string | Name of a global guard function on window |

Example with Hash Mode

<wavelength-router hash-mode>
  <wavelength-route path="/" component="home-page"></wavelength-route>
  <wavelength-route path="/profile" component="profile-page"></wavelength-route>
</wavelength-router>

URL becomes: https://example.com/#/profile

<wavelength-route> Attributes

| Attribute | Type | Description | | ----------- | ------ | ------------------------------------------------ | | path | string | URL path pattern (supports :param syntax) | | component | string | Custom element tag name to render for this route |

Path Patterns

  • Static: /about, /dashboard
  • Dynamic: /users/:userId, /posts/:category/:postId
  • Wildcard: * (catches all unmatched routes)

RouteContext

Passed to your component's routeContext property and the mount function:

interface RouteContext {
  path: string; // The matched path, e.g., '/users/123'
  routeParams: {
    // Extracted URL parameters
    [key: string]: string;
  };
  query: URLSearchParams; // Query string parameters
}

Accessing RouteContext in Components

class MyPage extends HTMLElement {
  static get routeContext(): RouteContext | undefined {
    return (this as unknown as { routeContext?: RouteContext }).routeContext;
  }

  connectedCallback() {
    const ctx = MyPage.routeContext;
    if (ctx) {
      console.log('Current path:', ctx.path);
      console.log('User ID:', ctx.routeParams.userId);
      console.log('Search query:', ctx.query.get('search'));
    }
  }
}

RouteGuard

Navigation guards allow you to intercept and redirect navigation. Define a guard function and reference it via the guard attribute.

// Define guard on window (required for HTML approach)
type NextCallback = (redirect?: string) => void;

(window as unknown as Record<string, unknown>).authGuard = (
  to: RouteContext,
  next: NextCallback,
) => {
  const isAuthenticated = checkAuth(); // Your auth logic

  if (to.path.startsWith('/protected') && !isAuthenticated) {
    next('/login'); // Redirect to login
  } else {
    next(); // Allow navigation
  }
};
<wavelength-router guard="authGuard">
  <wavelength-route path="/" component="home-page"></wavelength-route>
  <wavelength-route
    path="/protected"
    component="protected-page"
  ></wavelength-route>
  <wavelength-route path="/login" component="login-page"></wavelength-route>
</wavelength-router>

Guard Flow:

  1. Guard receives RouteContext with destination path
  2. Guard calls next() to allow navigation or next('/redirect-path') to redirect

Programmatic Navigation

The router provides navigation methods:

const routerElement = document.querySelector('wavelength-router');

// Navigate to a new path (adds to history)
routerElement.navigate('/about');

// Navigate and replace current entry (no history)
routerElement.replace('/new-page');

// Go back in history
routerElement.back();

Best Practices

1. Always Define a Wildcard Route

Provide a catch-all route for 404 handling:

<wavelength-route path="*" component="not-found-page"></wavelength-route>

2. Access RouteContext Correctly

The routeContext is injected into your custom element. Always check for its existence:

class UserPage extends HTMLElement {
  connectedCallback() {
    const ctx = (this as unknown as { routeContext?: RouteContext })
      .routeContext;
    if (ctx) {
      this.innerHTML = `<p>User: ${ctx.routeParams.userId}</p>`;
    }
  }
}

3. Use Hash Mode for Static Hosts

If deploying to static hosting without server configuration, use hash mode:

<wavelength-router hash-mode> ... </wavelength-router>

This avoids the need for server-side routing rules.

4. Clean Up in Route Components

Implement cleanup logic in the unmount function if needed:

const routes: RouteDefinition[] = [
  {
    path: '/chat',
    routeComponent: {
      mount(container, context) {
        const chat = document.createElement('chat-widget');
        container.appendChild(chat);
      },
      unmount() {
        // Close WebSocket connections, remove event listeners, etc.
      },
    },
  },
];

5. Route Guards for Authentication

Always protect authenticated routes:

(window as unknown as Record<string, unknown>).requireAuth = (ctx, next) => {
  if (isProtectedPath(ctx.path) && !getCurrentUser()) {
    next('/login');
  } else {
    next();
  }
};

6. URL Encoding

Route parameters are automatically URL-decoded. If you pass special characters, they will be properly handled.

7. Order Routes by Specificity

More specific routes should be defined before general ones:

<!-- Correct order -->
<wavelength-route path="/users/:id" component="user-page"></wavelength-route>
<wavelength-route path="/users" component="users-list"></wavelength-route>
<wavelength-route path="*" component="not-found"></wavelength-route>

8. Avoid Memory Leaks

Always clean up when routes unmount:

routeComponent: {
  let interval: number;

  mount(container) {
    const page = document.createElement('my-page');
    container.appendChild(page);

    // Store reference for cleanup
    interval = setInterval(() => updateTime(), 1000);
  },
  unmount() {
    clearInterval(interval);
  },
}

Development

# Run tests
npm run test

# Run tests with coverage
npm run test -- --coverage

# Lint and format
npm run lint
npm run prettier

# Build for production
npm run build

# Run Storybook for component development
npm run storybook

License

ISC - Wavelength