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

singlepage-router

v1.0.4

Published

Micro client-side router with History API, hashbang, and middleware support. TypeScript rewrite of page.js with full type safety and dual ESM/CJS output.

Readme

singlepage-router

npm version npm bundle size TypeScript license

A micro client-side router with a clean TypeScript rewrite. Full History and hashbang support, zero runtime dependencies beyond path-to-regexp, and a modern dual ESM/CJS build system.


What's different from page

singlepage-router is a TypeScript rewrite of the classic page package by @visionmedia. The routing behaviour and API are intentionally kept familiar, but the internals have been modernised throughout:

  • Written in TypeScript — full type safety out of the box. No need for a separate @types/ package. All classes, options, and callbacks are fully typed.
  • Proper dual ESM/CJS build with an exports map — ships both import and require formats using a modern package.json exports field with conditional resolution.
  • Class-based internalsPageInstance, Context, and Route are proper ES classes, replacing the original's prototype chain manipulation and constructor functions.
  • No legacy code — all var declarations, bitwise ~indexOf tricks, IE-era guards, and the HTML5-History-API polyfill support have been removed. Targets modern browsers only.
  • Updated path-to-regexp — uses v6 versus the original's pinned v1.2.x, bringing improved pattern support and security fixes.
  • sideEffects: false — explicitly marked for bundler tree-shaking, so unused exports are dropped cleanly.
  • Proper named ES module exportsContext, Route, PageInstance, createPage, and all TypeScript types are individually importable as true ESM named exports, enabling tree-shaking and typed auto-imports in any modern editor.

The public API — page('/path', handler), page(), page.show(), page.back(), page.exit() etc. — works the same way as page, so migration is straightforward.


Credits

This package is a TypeScript rewrite of page, the original micro client-side router created by @visionmedia. The core routing concepts, middleware chain design, Context model, and overall API shape all originate from that project.

If you want the battle-tested original with a long history of production use and a large community, go use page. This package exists as a modernised alternative for projects that want TypeScript support and a native ESM build from the start.

Full credit and thanks to all contributors to the original page.js project.


Installation

npm install singlepage-router

Quick Start

import page from 'singlepage-router';

page('/', () => render('home'));
page('/users', () => render('users'));
page('/users/:id', (ctx) => render('user', ctx.params.id));
page('*', () => render('404'));

page(); // start the router

Usage

Basic routes

import page from 'singlepage-router';

page('/', (ctx, next) => {
  console.log('home', ctx.pathname);
});

page('/about', (ctx, next) => {
  console.log('about page');
});

page(); // start

Route parameters

page('/users/:id', (ctx) => {
  const { id } = ctx.params;
  console.log('user id:', id);
});

page('/posts/:year/:month/:slug', (ctx) => {
  const { year, month, slug } = ctx.params;
  console.log(`Post: ${slug} from ${month}/${year}`);
});

Middleware / chaining handlers

Handlers receive a next function. Call it to pass control to the next matching handler, just like Express middleware.

import page from 'singlepage-router';
import type { Callback } from 'singlepage-router';

const authenticate: Callback = (ctx, next) => {
  if (!isLoggedIn()) return page.show('/login');
  next();
};

const loadUser: Callback = (ctx, next) => {
  ctx.state.user = fetchUser(ctx.params.id);
  next();
};

const renderProfile: Callback = (ctx) => {
  render('profile', ctx.state.user);
};

page('/profile/:id', authenticate, loadUser, renderProfile);

Wildcard / catch-all

page('*', (ctx) => {
  console.log('no route matched:', ctx.path);
  render('404');
});

Redirects

// Declarative redirect — from one path to another
page.redirect('/old-path', '/new-path');

// Imperative redirect from inside a handler
page('/legacy', () => {
  page.redirect('/modern');
});

Navigate programmatically

// Push a new history entry
page.show('/users/42');

// Replace the current history entry (no new entry added)
page.replace('/users/42');

// Go back — falls back to a path if there is no history
page.back('/home');

Exit handlers

Exit handlers run when navigating away from a route. Useful for teardown, unsaved-change guards, or cancelling in-flight requests.

page('/editor', (ctx) => {
  startEditor();
});

page.exit('/editor', (ctx, next) => {
  if (hasUnsavedChanges()) {
    if (!confirm('Leave without saving?')) return;
  }
  stopEditor();
  next();
});

Hashbang mode

For environments that can't use the HTML5 History API (e.g. file:// protocol):

page.start({ hashbang: true });

// URLs will look like: /#!/users/42

Base path

If your app is not served from the root, set a base path:

page.base('/my-app');

page('/dashboard', () => render('dashboard'));
// Matches: /my-app/dashboard

page();

Multiple isolated instances

import { createPage } from 'singlepage-router';

const adminRouter = createPage();
const publicRouter = createPage();

adminRouter('/dashboard', () => { /* ... */ });
publicRouter('/home', () => { /* ... */ });

adminRouter();
publicRouter();

Accessing the Context object

Every handler receives a Context instance with the following properties:

| Property | Type | Description | |-----------------|---------------------------|-----------------------------------------| | path | string | The path without base | | canonicalPath | string | The full path including base | | pathname | string | Path without querystring or hash | | querystring | string | Querystring without the leading ? | | hash | string | Hash fragment without the # | | params | Record<string, string> | Matched route parameters | | state | Record<string, unknown> | Arbitrary state stored in history entry | | handled | boolean | Whether the context was handled | | routePath | string | The route pattern that matched |

page('/search', (ctx) => {
  console.log(ctx.querystring); // "q=typescript&page=2"
  console.log(ctx.hash);        // "results"
  console.log(ctx.state);       // any state passed via page.show()
});

API Reference

page(path, ...handlers)

Register a route.

page() / page.start(options?)

Start the router. Dispatches the current URL immediately.

| Option | Type | Default | Description | |-----------------------|-----------|---------|--------------------------------------------| | dispatch | boolean | true | Dispatch the current route on start | | click | boolean | true | Intercept link clicks automatically | | popstate | boolean | true | Listen to popstate events | | hashbang | boolean | false | Use #! URLs instead of History API | | decodeURLComponents | boolean | true | Decode URL params and querystrings | | window | Window | window| Use a custom window object |

page.stop()

Unbind all event listeners and stop the router.

page.show(path, state?, dispatch?, push?)

Navigate to path, pushing a new history entry.

page.replace(path, state?, init?, dispatch?)

Navigate to path, replacing the current history entry.

page.back(fallback?, state?)

Go back in history. If no history exists, navigates to fallback.

page.redirect(from, to?)

Register a redirect from one path to another, or immediately redirect to a path.

page.base(path?)

Get or set the base path.

page.strict(enable?)

Get or set strict trailing-slash matching.

page.exit(path, ...handlers)

Register exit handlers for a route.

page.configure(options)

Reconfigure the router after start.

createPage()

Create a new isolated router instance.


Project Structure

your-project/
├── src/
│   ├── index.ts        
│   └── singlepage.ts    
├── scripts/
│   └── fix-cjs-ext.mjs
├── dist/               
│   ├── esm/index.js
│   ├── cjs/index.cjs
│   └── types/index.d.ts
├── package.json
├── tsconfig.json
├── tsconfig.esm.json
├── tsconfig.cjs.json
└── tsconfig.types.json

Building

npm install
npm run build

Individual build steps:

npm run build:esm    # ESM output  → dist/esm/
npm run build:cjs    # CJS output  → dist/cjs/
npm run build:types  # Type declarations → dist/types/

License

MIT