Routing component and framework for Ink - Bring Next.js-style routing to your CLI & TUI apps
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.
- 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
npm install @endernoke/wax
# or
yarn add @endernoke/wax
# or
pnpm add @endernoke/waxPeer Dependencies:
- React >= 18
- Ink >= 5
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>,
);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.
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 routinginitialRoute?: string- Initial route path (default:"/")initialHistory?: string[]- Initial history stack (default:["/"])onRouteChange?: (route: RouteInfo) => void- Called when route changesonError?: (error: Error) => void- Called when navigation error occurschildren?: React.ReactNode- Your application components (not required withbasePath)
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();Get the current pathname.
const pathname = usePathname();
// pathname = "/chat/123"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-projWax 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
When multiple routes could match a path, Wax uses this priority:
- Static routes (exact match):
/users/profile - Dynamic routes (single segment):
/users/:id - 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).
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
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)
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.tsxWax 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.
- Configuration
- Layouts and nested routes
- Loading states
- Error boundaries
- Route groups
- scaffolding CLI
- figure out how to be used in conjunction with Pastel
Contributions are welcome! Please open an issue for discussion before submitting a PR for big changes.
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Lint (eslint)
npm run lint
# Format (prettier)
npm run formatMIT, see LICENSE
Wax is inspired by:
I would also like to thank the contributors of history and path-to-regexp for making routing possible in a terminal environment.