@visulima/ono
Version:
Ono is an error-parsing library that pretty prints JavaScript errors on a web page or the terminal.
618 lines (468 loc) • 19.3 kB
Markdown
<div align="center">
<h3>Visulima ono (Oh No!)</h3>
<p>
A modern, delightful error overlay and inspector for Node.js servers and dev tooling.
</p>
</div>
<br />
<div align="center">
[![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url]
</div>
---
<div align="center">
<p>
<sup>
Daniel Bannert's open source work is supported by the community on <a href="https://github.com/sponsors/prisis">GitHub Sponsors</a>
</sup>
</p>
</div>
---
| Light Mode | Dark Mode |
| -------------------------------- | ------------------------------ |
|  |  |
## Install
```sh
pnpm add /ono
```
```sh
npm i /ono
```
```sh
yarn add /ono
```
## Features
- Pretty, theme‑aware error page
- Sticky header (shows error name/message while scrolling)
- One‑click copy for error title (icon feedback)
- Stack trace viewer
- Shiki‑powered syntax highlighting (singleton highlighter)
- Tabs for frames; grouping for internal/node_modules/application frames
- Tooltips and labels to guide usage
- Optional "Open in editor" button per frame
- Error causes viewer (nested causes, each with its own viewer)
- Solutions panel
- Default open; smooth expand/collapse without layout jump
- Animated height/opacity; icon toggles open/close
- Built-in rule-based Markdown hints for common issues (ESM/CJS interop, export mismatch, port in use, missing files/case, TS path mapping, DNS/connection, React hydration mismatch, undefined property access)
- Custom solution finders support
- Raw stack trace panel
- Theme toggle (auto/dark/light) with persistence
- **Copy to Clipboard** - One-click copying for all data sections
- **Responsive Design** - Sticky sidebar navigation with smooth scrolling
- Consistent tooltips (one global script; components only output HTML)
**New in latest version:**
- **Tabbed Interface** - Switch between Stack and Context views
- **Request Context Panel** - Detailed HTTP request debugging information
- cURL command with proper formatting and copy functionality
- Headers, cookies, body, and session data
- App routing, client info, Git status, and version details
- Smart data sanitization and masking for sensitive information
- **Flexible Context API** - Add any custom context data via `createRequestContextPage()`
- **Modern Ono Class API** - Simple, consistent interface for both HTML and ANSI rendering
- **Solution Finders** - Extensible system for custom error solutions
Accessibility and keyboard UX
- ARIA-correct tabs and panels for stack frames; improved labeling
- Focus trap within the overlay; restores focus on close
- Keyboard shortcuts help dialog (press Shift+/ or “?” button)
- Buttons/controls are keyboard-activatable (Enter/Space)
Editor integration
- Editor selector is always visible; selection persists (localStorage)
- Uses server endpoint when configured; otherwise opens via editor URL scheme (defaults to VS Code)
### Using the Ono class (recommended)
The new Ono class provides a simple, consistent API for both HTML and ANSI error rendering:
```ts
import { Ono } from "@visulima/ono";
const ono = new Ono();
// HTML error page
const html = await ono.toHTML(error, {
cspNonce: "your-nonce",
theme: "dark",
solutionFinders: [
/* custom finders */
],
});
// ANSI terminal output
const { errorAnsi, solutionBox } = await ono.toANSI(error, {
solutionFinders: [
/* custom finders */
],
});
```
### Node.js HTTP Server Example
```ts
import { createServer } from "node:http";
import { Ono } from "@visulima/ono";
import createRequestContextPage from "@visulima/ono/page/context";
import { createNodeHttpHandler } from "@visulima/ono/server/open-in-editor";
const ono = new Ono();
const openInEditorHandler = createNodeHttpHandler();
const server = createServer(async (request, response) => {
const url = new URL(request.url || "/", `http://localhost:3000`);
// Open-in-editor endpoint
if (url.pathname === "/__open-in-editor") {
return openInEditorHandler(request, response);
}
try {
// Your app logic here
throw new Error("Something went wrong!");
} catch (error) {
// Create context page with request information
const contextPage = await createRequestContextPage(request, {
context: {
request: {
method: request.method,
url: request.url,
headers: request.headers,
},
user: {
client: {
ip: request.socket?.remoteAddress,
userAgent: request.headers["user-agent"],
},
},
},
});
// Generate HTML error page
const html = await ono.toHTML(error, {
content: [contextPage],
openInEditorUrl: "__open-in-editor",
cspNonce: "nonce-" + Date.now(),
theme: "auto",
});
response.writeHead(500, {
"Content-Type": "text/html",
"Content-Length": Buffer.byteLength(html, "utf8"),
});
response.end(html);
}
});
server.listen(3000);
```
### Hono Framework Example
```ts
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { Ono } from "@visulima/ono";
import createRequestContextPage from "@visulima/ono/page/context";
const app = new Hono();
const ono = new Ono();
app.get("/", (c) => c.text("OK"));
app.get("/error", () => {
throw new Error("Boom from Hono");
});
app.onError(async (err, c) => {
const contextPage = await createRequestContextPage(c.req.raw, {
context: {
request: {
method: c.req.method,
url: c.req.url,
headers: Object.fromEntries(c.req.raw.headers.entries()),
},
},
});
const html = await ono.toHTML(err, {
content: [contextPage],
cspNonce: "hono-nonce-" + Date.now(),
theme: "dark",
});
return c.html(html, 500);
});
serve({ fetch: app.fetch, port: 3000 });
```
## API
### Ono Class
The main API for rendering errors in both HTML and ANSI formats.
#### Constructor
```ts
const ono = new Ono();
```
#### Methods
##### `toHTML(error, options?)` => `Promise<string>`
Renders an error as an HTML page.
- **error**: `unknown` - The error to render
- **options**: `TemplateOptions` (optional)
- `content?: ContentPage[]` - Additional pages to display as tabs
- `cspNonce?: string` - CSP nonce for inline scripts/styles
- `editor?: Editors` - Default editor for "Open in editor" functionality
- `openInEditorUrl?: string` - Server endpoint for opening files in editor
- `solutionFinders?: SolutionFinder[]` - Custom solution finders
- `theme?: 'dark' | 'light' | 'auto'` - Theme preference
Returns the complete HTML string for the error page.
##### `toANSI(error, options?)` => `Promise<{ errorAnsi: string; solutionBox?: string }>`
Renders an error as ANSI terminal output.
- **error**: `unknown` - The error to render
- **options**: `CliOptions` (optional)
- `solutionFinders?: SolutionFinder[]` - Custom solution finders
- All other options from `/error` renderError options
Returns an object with `errorAnsi` (the formatted error) and optional `solutionBox` (suggested solutions).
### createRequestContextPage(request, options) => `Promise<ContentPage | undefined>`
Creates a context page with detailed request debugging information.
- **request**: `Request` - The HTTP request object
- **options**: `ContextContentOptions`
- `context?: Record<string, unknown>` - Additional context data
- `headerAllowlist?: string[]` - Headers to include (default: all)
- `headerDenylist?: string[]` - Headers to exclude
- `maskValue?: string` - Mask for sensitive values (default: "[masked]")
### createNodeHttpHandler(options) => `(req, res) => void`
Creates an HTTP handler for opening files in editors.
- **options**: `OpenInEditorOptions` (optional)
- `projectRoot?: string` - Project root directory
- `allowOutsideProject?: boolean` - Allow opening files outside project
Returns an Express/Node.js compatible request handler.
### CLI Example
```ts
import { Ono } from "@visulima/ono";
const ono = new Ono();
try {
throw new Error("Something went wrong!");
} catch (error) {
// Basic ANSI output
const result = await ono.toANSI(error);
console.log(result.errorAnsi);
if (result.solutionBox) {
console.log("\n" + result.solutionBox);
}
// With custom solution finder
const resultWithCustom = await ono.toANSI(error, {
solutionFinders: [
{
name: "custom-finder",
priority: 100,
handle: async (err, context) => ({
header: "Custom Solution",
body: "Try checking your configuration.",
}),
},
],
});
}
```
### Request Context Panel
Use `createRequestContextPage()` to create a "Context" tab with comprehensive debugging information:
- **Request Overview** - cURL command with proper formatting and copy functionality
- **Headers** - HTTP headers with smart masking for sensitive data
- **Body** - Request body content with proper formatting
- **Session** - Session data in organized key-value tables
- **Cookies** - Cookie information in readable format
- **Dynamic Context Sections** - Any additional context keys you provide are automatically rendered as sections with:
- Proper titles (capitalized)
- Copy buttons for JSON data
- Organized key-value tables
- Sticky sidebar navigation
**Built-in sections** (when data is provided):
- `app` - Application routing details (route, params, query)
- `user` - Client information (IP, User-Agent, geo)
- `git` - Repository status (branch, commit, tag, dirty state)
- `versions` - Package versions and dependencies
**Custom sections** - Add any context data you want:
- `database` - Database connection info, queries, etc.
- `cache` - Cache status and keys
- `environment` - Environment variables
- `performance` - Performance metrics
- And more!
**Deep Object & Array Support** - The context panel intelligently renders:
- Nested objects with proper indentation and visual hierarchy
- Arrays with indexed items and collapsible structure
- Complex data types (strings, numbers, booleans, null, undefined)
- Performance-optimized rendering with depth limits (max 3 levels)
- Smart truncation for large datasets (shows first 10 items/keys)
All sections include copy buttons for easy data extraction and debugging.
### Custom Solution Finders
Create custom solution finders to provide specific guidance for your application's errors:
```ts
import { Ono } from "@visulima/ono";
const customFinder = {
name: "my-app-finder",
priority: 100, // Higher priority = checked first
handle: async (error, context) => {
if (error.message.includes("database connection")) {
return {
header: "Database Connection Issue",
body: "Check your database configuration and ensure the server is running.",
};
}
if (error.message.includes("authentication")) {
return {
header: "Authentication Error",
body: "Verify your API keys and authentication tokens are valid.",
};
}
return undefined; // No solution found
},
};
const ono = new Ono();
const html = await ono.toHTML(error, {
solutionFinders: [customFinder],
});
```
### Copy to Clipboard
All data sections in the Request Context Panel include copy buttons that:
- Copy data in JSON format for easy debugging
- Provide visual feedback (button changes to "Copied!" with green styling)
- Support both modern `navigator.clipboard` API and fallback methods
- Work across all browsers and environments
### Adding custom pages/tabs via `options.content`
You can add any number of custom pages using the `content` option:
```ts
import { Ono } from "@visulima/ono";
import createRequestContextPage from "@visulima/ono/page/context";
const ono = new Ono();
// Create a context page with request information
const contextPage = await createRequestContextPage(request, {
context: {
request: request,
app: { routing: { route: "/api/users", params: {}, query: {} } },
user: { client: { ip: "127.0.0.1", userAgent: "Mozilla/5.0..." } },
database: { connection: "active", queries: ["SELECT * FROM users"] },
},
});
// Add custom pages
const customPages = [
contextPage, // Context page with request info
{
id: "performance",
name: "Performance",
code: {
html: "<div><h3>Performance Metrics</h3><p>Custom performance data here...</p></div>",
},
},
{
id: "debug",
name: "Debug Info",
code: {
html: "<div><h3>Debug Information</h3><pre>" + JSON.stringify(debugData, null, 2) + "</pre></div>",
},
},
];
const html = await ono.toHTML(error, {
content: customPages,
cspNonce: "your-nonce",
});
```
## Examples
The `examples/` directory contains working examples for different use cases:
### CLI Example (`examples/cli/`)
Demonstrates basic ANSI output and custom solution finders:
```bash
cd examples/cli
node index.js
```
### Node.js HTTP Server (`examples/node/`)
Complete HTTP server example with rich context pages:
```bash
cd examples/node
node index.js
```
Try these routes:
- `/error` - Basic error with context
- `/esm-cjs` - ESM/CJS interop error
- `/export-mismatch` - Export mismatch error
- `/custom-solution` - Custom solution finder demo
### Hono Framework (`examples/hono/`)
Hono framework integration example:
```bash
cd examples/hono
node index.js
```
Try these routes:
- `/error` - Basic error handling
- `/error-html` - HTML error page
- `/api/error-json` - JSON error response
### Server helpers
From `/ono/server/open-in-editor`:
- `openInEditor(request, options)` — core function (uses `open-editor` under the hood)
- `createNodeHttpHandler(options)` — returns `(req, res) => void` for Node http servers
- `createExpressHandler(options)` — returns `(req, res) => void` for Express/Connect
Options:
- `projectRoot?: string` — defaults to `process.cwd()`
- `allowOutsideProject?: boolean` — defaults to `false`
### Editor selector
- Always visible
- Persists user choice in `localStorage` (`ono:editor`)
- Used for both the server opener (sent as `editor` in the POST body) and the client-side fallback
### Client-side fallback editor links
- If `openInEditorUrl` is not set, clicking “Open in editor” uses editor URL schemes on the client. The default editor is VS Code. The selected editor in the header is respected.
- Supported editors and templates (placeholders: `%f` = file, `%l` = line, `%c` = column when supported):
- textmate: `txmt://open?url=file://%f&line=%l`
- macvim: `mvim://open?url=file://%f&line=%l`
- emacs: `emacs://open?url=file://%f&line=%l`
- sublime: `subl://open?url=file://%f&line=%l`
- phpstorm: `phpstorm://open?file=%f&line=%l`
- atom: `atom://core/open/file?filename=%f&line=%l`
- atom-beta: `atom-beta://core/open/file?filename=%f&line=%l`
- brackets: `brackets://open?url=file://%f&line=%l`
- clion: `clion://open?file=%f&line=%l`
- code (VS Code): `vscode://file/%f:%l:%c`
- code-insiders: `vscode-insiders://file/%f:%l:%c`
- codium (VSCodium): `vscodium://file/%f:%l:%c`
- cursor: `cursor://file/%f:%l:%c`
- emacs: `emacs://open?url=file://%f&line=%l`
- idea: `idea://open?file=%f&line=%l`
- intellij: `idea://open?file=%f&line=%l`
- macvim: `mvim://open?url=file://%f&line=%l`
- notepad++: `notepad-plus-plus://open?file=%f&line=%l`
- phpstorm: `phpstorm://open?file=%f&line=%l`
- pycharm: `pycharm://open?file=%f&line=%l`
- rider: `rider://open?file=%f&line=%l`
- rubymine: `rubymine://open?file=%f&line=%l`
- sublime: `subl://open?url=file://%f&line=%l`
- textmate: `txmt://open?url=file://%f&line=%l`
- vim: `vim://open?url=file://%f&line=%l`
- visualstudio: `visualstudio://open?file=%f&line=%l`
- vscode: `vscode://file/%f:%l:%c`
- vscodium: `vscodium://file/%f:%l:%c`
- webstorm: `webstorm://open?file=%f&line=%l`
- xcode: `xcode://open?file=%f&line=%l`
- zed: `zed://open?file=%f&line=%l&column=%c`
- android-studio: `idea://open?file=%f&line=%l`
### Keyboard Shortcuts
- **Shift+/ (or ?)** — Open shortcuts help dialog
- **Esc** — Close dialogs
- **Enter/Space** — Activate focused control (e.g., toggles, tabs)
### Tooltips
Components emit HTML with `data-tooltip-trigger`; a single script exported by the tooltip module is imported once by the layout (so there's no duplication).
### Extend the VisulimaError
```ts
import { VisulimaError } from "@visulima/error";
class MyError extends VisulimaError {
constructor(message: string) {
super({
name: "MyError",
message,
});
}
}
throw new MyError("My error message");
// or
const error = new MyError("My error message");
error.hint = "My error hint";
throw error;
```
### Pretty code frame
```ts
import { codeFrame } from "@visulima/error";
const source = "const x = 10;\nconst error = x.y;\n";
const loc = { column: 16, line: 2 };
const frame = codeFrame(source, loc);
console.log(frame);
// 1 | const x = 10;
// > 2 | const error = x.y;
// | ^
```
## Supported Node.js Versions
Libraries in this ecosystem make the best effort to track [Node.js’ release schedule](https://github.com/nodejs/release#release-schedule).
Here’s [a post on why we think this is important](https://medium.com/the-node-js-collection/maintainers-should-consider-following-node-js-release-schedule-ab08ed4de71a).
## Contributing
If you would like to help take a look at the [list of issues](https://github.com/visulima/visulima/issues) and check our [Contributing](.github/CONTRIBUTING.md) guild.
> **Note:** please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
## Credits
- [Daniel Bannert](https://github.com/prisis)
- [All Contributors](https://github.com/visulima/visulima/graphs/contributors)
## License
The visulima error is open-sourced software licensed under the [MIT][license-url]
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
[typescript-url]: "typescript"
[license-image]: https://img.shields.io/npm/l/@visulima/ono?color=blueviolet&style=for-the-badge
[license-url]: LICENSE.md "license"
[npm-image]: https://img.shields.io/npm/v/@visulima/ono/latest.svg?style=for-the-badge&logo=npm
[npm-url]: https://www.npmjs.com/package/@visulima/ono/v/latest "npm"