Skip to content

endernoke/wax

Repository files navigation

Wax

Routing component and framework for Ink - Bring Next.js-style routing to your CLI & TUI apps

npm version

Wax is a routing library for Ink that brings the developer experience of Next.js App Router to CLI/TUI applications. It provides the router API, file-based routing, dynamic routes that you are already familiar with for building complex terminal applications.

Features

  • Routing system with in-memory history management
  • React Router-style hooks (useRouter, usePathname, useParams)
  • Programmatic navigation (push, replace, back, forward)
  • File-based route discovery like Next.js App Router
  • Dynamic and catch-all routes
  • Route change callbacks
  • Full TypeScript support

Installation

npm install @endernoke/wax
# or
yarn add @endernoke/wax
# or
pnpm add @endernoke/wax

Peer Dependencies:

  • React >= 18
  • Ink >= 5

Quick Start

Manual Routing

import React from "react";
import { render, Text, Box, useInput } from "ink";
import { Router, useRouter, usePathname } from "waxjs";

function App() {
  const pathname = usePathname();
  const router = useRouter();

  useInput((input, key) => {
    if (input === "h") {
      router.push("/");
    } else if (input === "a") {
      router.push("/about");
    } else if (input === "d") {
      router.push("/docs");
    } else if (key.leftArrow) {
      router.back();
    } else if (key.rightArrow) {
      router.forward();
    }
  });

  return (
    <Box flexDirection="column">
      <Text>Current: {pathname}</Text>
      <Box>
        {pathname === "/" && <Text>Home Page</Text>}
        {pathname === "/about" && <Text>About Page</Text>}
        {pathname === "/docs" && <Text>Docs Page</Text>}
      </Box>
    </Box>
  );
}

render(
  <Router>
    <App />
  </Router>,
);

File-Based Routing

Create route files in a routes/ directory (or whatever you prefer), e.g.:

routes/
├── index.tsx           # /
├── about.tsx           # /about
├── users/
│   └── [id].tsx        # /users/:id
└── docs/
    └── [...slug].tsx   # /docs/* (catch-all)

Then, in your entry file:

import React from "react";
import { render } from "ink";
import { Router } from "waxjs";
import { join } from "path";

const routesDir = join(__dirname, "routes"); // Adjust path as needed

render(<Router basePath={routesDir} />);

That's it! Wax will automatically discover and load your route files.

See the examples/ directory for complete working examples.

Important

Always wrap your app in a <Router> component to provide routing context.

API Reference

Components

<Router>

The main router component that provides routing context to your application.

File-Based Routing:

<Router basePath="/path/to/routes" />

Manual Routing:

<Router>{children}</Router>

Props:

  • basePath?: string - Path to routes directory for file-based routing
  • initialRoute?: string - Initial route path (default: "/")
  • initialHistory?: string[] - Initial history stack (default: ["/"])
  • onRouteChange?: (route: RouteInfo) => void - Called when route changes
  • onError?: (error: Error) => void - Called when navigation error occurs
  • children?: React.ReactNode - Your application components (not required with basePath)

Hooks

useRouter()

Access the router instance with navigation methods.

const router = useRouter();

// Navigate to a new route
router.push("/settings");
router.push("/settings", { state: { from: "home" } });

// Replace current route
router.replace("/login");

// Navigate history
router.back();
router.forward();
router.go(-2); // Go back 2 steps

// Check navigation state
router.canGoBack; // boolean
router.canGoForward; // boolean

// Current route info
router.pathname; // string
router.params; // Record<string, string | string[]>
router.state; // any

// Refresh current route
router.refresh();

usePathname()

Get the current pathname.

const pathname = usePathname();
// pathname = "/chat/123"

useParams<T>()

Get route parameters from dynamic route segments.

// In route file: routes/users/[id].tsx
const params = useParams<{ id: string }>();
console.log(params.id); // "123" from /users/123

// Catch-all route: routes/docs/[...slug].tsx
const params = useParams<{ slug: string[] }>();
console.log(params.slug); // ["getting", "started"] from /docs/getting/started

// Optional catch-all: routes/shop/[[...category]].tsx
const params = useParams<{ category?: string[] }>();
console.log(params.category); // undefined from /shop
console.log(params.category); // ["electronics"] from /shop/electronics

// Multiple dynamic route parameters: routes/[workspace]/[project].tsx
const params = useParams<{ workspace: string; project: string }>();
console.log(params.workspace); // "my-ws" from /my-ws/my-proj
console.log(params.project); // "my-proj" from /my-ws/my-proj

File-Based Routing

Route File Conventions

Wax automatically discovers routes from your routes/ directory:

File Route Pattern Matches
index.tsx / /
about.tsx /about /about
docs/installation/linux.tsx or docs/installation/linux/index.tsx /docs/installation/linux /docs/installation/linux
users/[id].tsx /users/:id /users/123
[workspace]/[project].tsx /:workspace/:project /my-ws/my-proj
docs/[...slug].tsx /docs/*slug /docs/a, /docs/a/b/c
shop/[[...category]].tsx /shop{/*category} /shop, /shop/electronics, /shop/a/b/c

Route Types:

  • Files without brackets create static routes
  • Use [param] for dynamic segments
    • the parameter name can be any valid JavaScript identifier
  • Use [...param] to match multiple segments
  • Use [[...param]] to match with or without segments

Route Priority

When multiple routes could match a path, Wax uses this priority:

  1. Static routes (exact match): /users/profile
  2. Dynamic routes (single segment): /users/:id
  3. Catch-all routes and optional catch-all routes (multiple segments): /users/*path

Tip

You must have only one of routes/path/index.tsx and routes/path.tsx, as both will match to the same route (/path).

Route Validation

Wax validates routes during initialization:

  • Catch-all segments must be at the end of a route
  • Parameter names must be valid JavaScript identifiers
  • Duplicate route patterns are detected and throw errors

Ignored Files

The following files are automatically ignored:

  • Files starting with an underscore _ (e.g., _utils.ts, _components.tsx)
  • Test files (e.g., *.test.tsx, *.spec.ts)
  • Type definition files (e.g., *.d.ts)

Examples

See the examples/ directory for complete working examples:

  • basic - Basic manual routing setup
  • file-routing - File-based routing with dynamic routes, catch-all routes, and navigation

To run an example:

npm install
npm run build
node --loader=ts-node/esm examples/basic/app.tsx
# node --loader=ts-node/esm examples/file-routing/app.tsx

Architecture

Wax is built on top of the history library and uses MemoryHistory for in-memory history management without URLs. It provides a React Context-based API similar to React Router, adapted for CLI/TUI applications.

Routes are discovered at run time and dynamically imported on request.

Todo

  • Configuration
  • Layouts and nested routes
  • Loading states
  • Error boundaries
  • Route groups
  • scaffolding CLI
  • figure out how to be used in conjunction with Pastel

Contributing

Contributions are welcome! Please open an issue for discussion before submitting a PR for big changes.

Development

# Install dependencies
npm install

# Build
npm run build

# Run tests
npm test

# Lint (eslint)
npm run lint

# Format (prettier)
npm run format

License

MIT, see LICENSE

Acknowledgements

Wax is inspired by:

  • Pastel - file-based CLI commands
  • Next.js - File-based routing patterns

I would also like to thank the contributors of history and path-to-regexp for making routing possible in a terminal environment.