Platform Differences
Key differences between Node.js and Browser platforms.
Quick Comparison
| Feature | Node.js | Browser |
|---|---|---|
| Runtime | Native Node.js APIs | Polyfills + XTerm.js |
| Terminal | Direct TTY | XTerm.js emulation |
| File System | Full access | Bundled terminfo only |
| Process | Full process control | Limited polyfill |
| Exit | process.exit() | Destroy screen + terminal |
| Streams | Native streams | stream-browserify |
| Buffer | Native Buffer | buffer package |
| Performance | Faster (native) | Slower (emulation) |
| Bundle Size | N/A | ~150KB gzipped |
| Mouse | Terminal-dependent | Always available |
| Resize | SIGWINCH signal | Window resize events |
Installation
Node.js
pnpm add @unblessed/node
Includes:
- Node.js runtime implementation
- All core widgets
- Direct TTY support
Browser
pnpm add @unblessed/browser xterm
Includes:
- Browser runtime with polyfills
- All core widgets
- XTerm.js integration
Initialization
Node.js
Automatic runtime initialization:
import { Screen, Box } from '@unblessed/node';
// Runtime auto-initialized with Node.js APIs
const screen = new Screen();
Browser
Requires XTerm.js terminal:
import { Terminal } from 'xterm';
import { Screen, Box } from '@unblessed/browser';
// Create and mount terminal
const term = new Terminal();
term.open(document.getElementById('terminal'));
// Runtime auto-initialized with polyfills
const screen = new Screen({ terminal: term });
API Differences
Screen Creation
Node.js:
const screen = new Screen({
input: process.stdin, // Node.js stream
output: process.stdout, // Node.js stream
terminal: 'xterm-256color'
});
Browser:
const screen = new Screen({
terminal: term // XTerm.js Terminal instance
});
Exiting
Node.js:
screen.key(['q', 'C-c'], () => {
process.exit(0); // Works in Node.js
});
Browser:
screen.key(['q', 'C-c'], () => {
screen.destroy();
term.dispose(); // Can't actually exit browser
});
File Access
Node.js:
import { readFileSync } from 'fs';
const data = readFileSync('./config.json', 'utf8');
box.setContent(data);
Browser:
// Use fetch API instead
const response = await fetch('/api/config.json');
const data = await response.text();
box.setContent(data);
Environment Variables
Node.js:
const apiKey = process.env.API_KEY;
const isDev = process.env.NODE_ENV === 'development';
Browser:
// process.env is empty object
// Use build-time env variables instead
const apiKey = import.meta.env.VITE_API_KEY;
const isDev = import.meta.env.DEV;
Feature Availability
Available in Both
✅ All widgets (Box, List, Table, Form, etc.) ✅ Event system ✅ Keyboard input ✅ Mouse support ✅ Rendering pipeline ✅ Styling and colors ✅ Content tags ✅ Focus management
Node.js Only
✅ Direct TTY control
✅ File system access
✅ Child processes
✅ Process signals (SIGINT, SIGTERM)
✅ Native streams
✅ process.exit()
Browser Only
✅ XTerm.js integration ✅ DOM integration ✅ Full-screen mode ✅ Browser DevTools ✅ requestAnimationFrame ✅ ResizeObserver ✅ WebWorkers (for async tasks)
Performance Differences
Node.js
Advantages:
- Direct TTY access (no emulation)
- Faster rendering (~6.5ms empty screen)
- Lower memory usage
- Native Buffer operations
Use cases:
- CLI tools
- Server monitoring
- Build tools
- System utilities
Browser
Advantages:
- Works in web browsers
- No installation required
- Cross-platform web apps
- Better for demos/documentation
Trade-offs:
- Slower rendering (~10-15ms empty screen)
- Higher memory usage
- Larger bundle size (~150KB)
- XTerm.js emulation overhead
Use cases:
- Web-based terminals
- Interactive documentation
- Browser DevTools
- Remote terminals
Code Compatibility
Write Once, Run Anywhere
Most code works identically:
// Works in both Node.js and Browser!
import { Screen, Box, List } from '@unblessed/node'; // or browser
const screen = new Screen(/* platform-specific options */);
const box = new Box({
parent: screen,
top: 'center',
left: 'center',
width: '50%',
height: '50%',
content: 'Hello World!',
border: { type: 'line' }
});
screen.key('q', () => {
/* platform-specific cleanup */
});
screen.render();
Platform Detection
Detect current platform:
import { getRuntime } from '@unblessed/core/runtime-context';
const runtime = getRuntime();
if (runtime.process.platform === 'browser') {
// Browser-specific code
console.log('Running in browser');
} else {
// Node.js-specific code
console.log('Running in Node.js');
}
Conditional Imports
Use dynamic imports for platform-specific code:
async function initApp() {
if (typeof window !== 'undefined') {
// Browser
const { Terminal } = await import('xterm');
const { Screen } = await import('@unblessed/browser');
const term = new Terminal();
term.open(document.getElementById('terminal'));
return new Screen({ terminal: term });
} else {
// Node.js
const { Screen } = await import('@unblessed/node');
return new Screen();
}
}
Migration Guide
Node.js to Browser
- Add XTerm.js:
pnpm add xterm @xterm/addon-fit
- Change imports:
// Before
import { Screen } from '@unblessed/node';
// After
import { Terminal } from 'xterm';
import { Screen } from '@unblessed/browser';
- Create terminal:
// Add this
const term = new Terminal();
term.open(document.getElementById('terminal'));
// Update screen creation
const screen = new Screen({ terminal: term });
- Update file access:
// Before
import { readFileSync } from 'fs';
const data = readFileSync('./data.txt', 'utf8');
// After
const response = await fetch('/data.txt');
const data = await response.text();
- Update exit handling:
// Before
screen.key('q', () => process.exit(0));
// After
screen.key('q', () => {
screen.destroy();
term.dispose();
});
Browser to Node.js
- Remove XTerm.js:
pnpm remove xterm
- Change imports:
// Before
import { Terminal } from 'xterm';
import { Screen } from '@unblessed/browser';
// After
import { Screen } from '@unblessed/node';
- Simplify screen creation:
// Before
const term = new Terminal();
term.open(element);
const screen = new Screen({ terminal: term });
// After
const screen = new Screen();
- Use Node.js APIs:
// Before
const response = await fetch('/data.txt');
const data = await response.text();
// After
import { readFileSync } from 'fs';
const data = readFileSync('./data.txt', 'utf8');
Best Practices
1. Abstract Platform Differences
Create platform adapters:
// platform.ts
export interface PlatformAdapter {
createScreen(): Screen;
readFile(path: string): Promise<string>;
exit(): void;
}
// node-platform.ts
export class NodePlatform implements PlatformAdapter {
createScreen() {
return new Screen();
}
async readFile(path: string) {
return readFileSync(path, 'utf8');
}
exit() {
process.exit(0);
}
}
// browser-platform.ts
export class BrowserPlatform implements PlatformAdapter {
constructor(private term: Terminal) {}
createScreen() {
return new Screen({ terminal: this.term });
}
async readFile(path: string) {
const response = await fetch(path);
return response.text();
}
exit() {
// Can't exit browser
console.log('Application closed');
}
}
2. Feature Detection
Check for capabilities:
function hasFileSystem(): boolean {
try {
const runtime = getRuntime();
runtime.fs.existsSync('/');
return true;
} catch {
return false;
}
}
function canExit(): boolean {
const runtime = getRuntime();
return runtime.process.platform !== 'browser';
}
3. Progressive Enhancement
Provide fallbacks:
async function loadData(path: string) {
const runtime = getRuntime();
if (runtime.process.platform === 'browser') {
// Browser: fetch from server
const response = await fetch(path);
return response.json();
} else {
// Node.js: read from file system
const data = readFileSync(path, 'utf8');
return JSON.parse(data);
}
}
4. Shared Widgets
Write platform-agnostic widgets:
export class StatusBar extends Box {
constructor(options: BoxOptions) {
super({
...options,
height: 3,
bottom: 0,
left: 0,
right: 0,
style: { bg: 'blue', fg: 'white' }
});
}
setStatus(message: string) {
this.setContent(` ${message}`);
this.screen?.render();
}
}
// Works in both platforms!
const status = new StatusBar({ parent: screen });
status.setStatus('Ready');
Common Pitfalls
❌ Using process.exit() in Browser
// DON'T
screen.key('q', () => process.exit(0)); // Throws error in browser
// DO
screen.key('q', () => {
screen.destroy();
if (typeof window === 'undefined') {
process.exit(0);
}
});
❌ Assuming File System Exists
// DON'T
const config = readFileSync('./config.json'); // Fails in browser
// DO
async function loadConfig() {
if (hasFileSystem()) {
return readFileSync('./config.json', 'utf8');
} else {
const response = await fetch('/config.json');
return response.text();
}
}
❌ Forgetting XTerm.js in Browser
// DON'T
const screen = new Screen(); // Missing terminal in browser
// DO
const term = new Terminal();
term.open(element);
const screen = new Screen({ terminal: term });
Next Steps
- Node.js Platform - Node.js-specific features
- Browser Platform - Browser-specific features
- Examples - Platform-specific examples