Skip to content

Latest commit

 

History

191 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Virtual Console

DevTools-style console for the places that don't have DevTools — iOS WebViews, Android WebViews, any page where console.log goes nowhere. Logs, object inspector, and a REPL that reaches your app's live globals. Injects before your app boots — and runs whether your app boots, crashes, or never starts at all.

Use it through an explicit runtime import, or through the Vite plugin when you want it injected before your app bundle runs.

Why this exists

A WKWebView inside a native app can't be opened in Safari Web Inspector unless the app that hosts it sets isInspectable, which defaults to false — so if you don't own the native shell, there is no console to open. Even when you do own it, an inspector you attach after the fact can't show you what already happened during boot. This console is rendered by the page itself, so it's attached from the first line of script, to everyone looking at the screen.

Try the live demo → — no install required. Or open the website source in StackBlitz to run and edit it in-browser.

Features

  • 📱 Mobile Friendly: Toggle with 2-finger long press or Shift+C.
  • 🚀 Independent of Your App: Rendered by the page, sharing no lifecycle with your framework — it's there even if your app never mounts.
  • 🔍 Object Inspector: Interactive viewer for Objects, Arrays, Maps, Sets, and more.
  • ⌨️ Live REPL: Evaluate expressions against your app's real globals, with a preview of the result as you type.
  • 🧯 Catches What You'd Miss: Intercepts console.log/error/warn/info/debug, plus uncaught errors and unhandled promise rejections.
  • 🎨 Themable: Comes with multiple themes (VSCode, Chrome Light, Dracula, Nord, Tokyo).
  • ⚡ Vite Plugin: Optional injection before your app bundle runs, so boot-time logs aren't lost.
  • 📦 Zero Runtime Dependencies: Nothing pulled in at runtime; Vite is an optional peer.
  • 🔒 No Telemetry: Fully local and self-contained — see Privacy.

Installation

pnpm add @codehacks/virtual-console
# or
npm install @codehacks/virtual-console

Usage

Explicit Import

import { installVirtualConsole } from '@codehacks/virtual-console';
import '@codehacks/virtual-console/styles.css';

const virtualConsole = installVirtualConsole({
  maxLogs: 100
});

// Later, from your framework cleanup lifecycle:
virtualConsole.destroy();

Vite Plugin

Add the plugin to your vite.config.ts when you want the console injected before your app bundle runs:

import { defineConfig } from 'vite';
import { virtualConsoleVitePlugin } from '@codehacks/virtual-console/plugins/vite';

export default defineConfig({
  plugins: [
    virtualConsoleVitePlugin({
      // Specify available themes. The first one will be the default.
      themes: ['vscode', 'chrome-light', 'dracula'],
      options: {
        maxLogs: 100
      }
    })
  ]
});

Configuration

The console supports the following themes:

  • vscode (Default Dark)
  • chrome-light (Chrome DevTools Light)
  • dracula
  • nord
  • tokyo

installVirtualConsole() accepts a partial VirtualConsoleConfig:

installVirtualConsole({
  maxLogs: 100,               // logs kept before the oldest is dropped
  minHeight: 100,              // dock bounds when docked top/bottom
  maxHeight: window.innerHeight * 0.8,
  defaultHeight: 200,
  minWidth: 200,                // dock bounds when docked left/right
  maxWidth: window.innerWidth * 0.8,
  defaultWidth: 400,
  keyboardShortcut: { code: 'KeyC', shiftKey: true }, // see below
  longPressFingers: 2,          // mobile activation gesture
  longPressDuration: 500,       // ms
  replEnabled: true,            // set false to remove the eval-based REPL entirely
  replHistoryLimit: 50,         // REPL commands kept in localStorage
  targetElement: undefined      // mount into a specific element instead of <body>
});

Keyboard shortcut

keyboardShortcut takes a KeyboardEvent.code plus the exact modifiers that must be held - every modifier not listed is required to be up, so the match is exact and won't fire on an unrelated combination that happens to share a key:

// Ctrl+Shift+D instead of the default Shift+C
installVirtualConsole({ keyboardShortcut: { code: 'KeyD', ctrlKey: true, shiftKey: true } });

// Disable the keyboard shortcut entirely (e.g. only use the long-press gesture,
// or wire your own trigger to the exported `toggleConsole()`)
installVirtualConsole({ keyboardShortcut: null });

The shortcut is automatically ignored while focus is inside a text input, textarea, select, or contenteditable element (including the console's own REPL input) so it can't hijack a keystroke the app being debugged relies on.

Disabling the REPL

The REPL evaluates whatever you type via eval. If you want to ship a read-only log viewer with no eval surface at all (e.g. a build that might reach production), set replEnabled: false.

Styling overrides

The console's stacking order can be adjusted from your own CSS without a JS config option, in case your app already has a very-high-z-index overlay of its own:

:root {
  --vc-z-index: 2147483000;
}

Activation

  • Desktop: Press Shift + C (or Escape to close while the console is focused), configurable via keyboardShortcut
  • Mobile: Long press with 2 fingers for 0.5s, configurable via longPressFingers / longPressDuration

Privacy

Virtual Console makes zero outbound network or telemetry calls of its own. It only reads/writes localStorage on the page it's installed on (theme choice, dock position/size, REPL history - all under keys prefixed virtual-console:) and renders everything locally in the DOM it creates. Nothing you log or throw is ever sent anywhere by this library.

Development

# Install dependencies
pnpm install

# Build the library
pnpm build

# Lint
pnpm lint

# Typecheck
pnpm typecheck

# Run the test suite
pnpm test

# Run local workspace examples
pnpm dev:import:local
pnpm dev:vite-plugin:local

# Build local workspace examples
pnpm build:local

# Run the public website
pnpm dev:website

# Build the public website
pnpm build:website

# Build the public website and serve the real static output (what CI deploys,
# prerendered HTML included) - no hot reload, refresh manually after a change
pnpm preview:website

# Same, but rebuilds automatically on every change under website/src or
# website/index.html - still no hot reload, just refresh the browser yourself
pnpm preview:website:watch

# Pull a @codehacks/virtual-console release into the website (see Releasing below)
pnpm bump:website:latest

See examples/README.md for the full layout (local workspace examples vs. standalone examples that install the real published package) and the *:published commands.

Releasing

Versions are managed by vump (vump.toml), which declares two independently-versioned projects - main (this package, tagged v{version}) and website (the website, tagged website-v{version}). Pushing either tag shape triggers its own workflow; see DECISIONS.md for why they're split and what each one does.

Release the package - publishes to npm, under the alpha/beta/rc dist-tag for a pre-release or latest for a stable one:

vump patch --project main --tag --push   # or minor / major

Release the website - for a site-only change (copy, layout, a new section); never touches npm or the package's version:

vump patch --project website --tag --push   # or minor / major

Pick up a new package version in the website. website/package.json pins @codehacks/virtual-console to an exact version rather than a latest alias, so the file itself always shows exactly what the live website runs - installed from its own committed lockfile, not re-resolved at deploy time. Bump it explicitly for whichever channel you want live, then release the website to deploy it - this is a deliberate follow-up, not automatic:

pnpm bump:website:latest   # or bump:website:alpha / :beta / :rc to preview a pre-release
git add website/package.json website/pnpm-lock.yaml
git commit -m "chore(website): bump @codehacks/virtual-console"
vump patch --project website --tag --push

Common vump commands, run from the repo root:

vump status                                                 # every project's version, and whether its files agree
vump check <tag>                                            # verify a pushed tag against its project (what CI runs; infers the project from the tag's shape)
vump patch|minor|major --project <main|website>             # bump a stable version
vump alpha|beta|rc --project <main|website> --from <bump>    # start/advance a pre-release, e.g. --from patch
vump release --project <main|website>                       # drop a pre-release suffix

Add --dry-run to preview a bump without writing anything. --tag implies --commit, and --push implies --commit too - each flag is independent otherwise, so pushing a tag needs both --tag --push together, as in the examples above.

See website/README.md for the public website (website/) - it's not one of the examples above; it's the landing page at virtual-console.codehacks.io, deployed on its own release line independent of the package's.

See DECISIONS.md for the rationale behind non-obvious choices (REPL evaluation safety, DevTools-parity scope, known/accepted limitations) before proposing a change in those areas.

See CLAUDE.md for the engineering standards contributors (human or AI) are expected to hold to in this repo.

License

MIT

About

DevTools-style console for the places that don't have DevTools — iOS WebViews, Android WebViews, any page where console.log goes nowhere. Logs, object inspector, and a REPL that reaches your app's live globals. Injects before your app boots — and runs whether your app boots, crashes, or never starts at all.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages