Web Development

Setup

Prerequisites

  • Node.js 18+
  • Bun 1.0+
  • pnpm 8+

Install & Run

cd web
pnpm install
pnpm dev          # Development server
pnpm build        # Production build
pnpm test         # Run tests

Project Structure

web/
├── src/
│   ├── server/           # Node.js backend
│   │   ├── server.ts     # HTTP/WebSocket server
│   │   ├── pty/          # Terminal management
│   │   ├── services/     # Business logic
│   │   └── routes/       # API endpoints
│   ├── client/           # Web frontend
│   │   ├── app.ts        # Main application
│   │   ├── components/   # Lit components
│   │   └── services/     # Client services
│   └── shared/           # Shared types
├── dist/                 # Build output
└── tests/                # Test files

Server Development

Core Services

ServiceFilePurpose
TerminalManagerservices/terminal-manager.tsPTY lifecycle
SessionManagerservices/session-manager.tsSession state
BufferAggregatorservices/buffer-aggregator.tsOutput batching
AuthServiceservices/auth.tsAuthentication

API Routes

// routes/api.ts
router.post('/api/sessions', createSession);
router.get('/api/sessions', listSessions);
router.get('/api/sessions/:id', getSession);
router.delete('/api/sessions/:id', deleteSession);
router.ws('/api/sessions/:id/ws', handleWebSocket);

WebSocket Handler

// services/websocket-handler.ts
export async function handleWebSocket(ws: WebSocket, sessionId: string) {
  const session = await sessionManager.get(sessionId);
  
  // Binary protocol for terminal data
  session.onData((data: Buffer) => {
    ws.send(encodeBuffer(data));
  });
  
  // Handle client messages
  ws.on('message', (msg: Buffer) => {
    const data = JSON.parse(msg.toString());
    if (data.type === 'input') {
      session.write(data.data);
    }
  });
}

PTY Management

// pty/pty-manager.ts
import * as pty from 'node-pty';

export class PTYManager {
  create(options: PTYOptions): IPty {
    return pty.spawn(options.shell || '/bin/zsh', options.args, {
      cols: options.cols || 80,
      rows: options.rows || 24,
      cwd: options.cwd || process.env.HOME,
      env: { ...process.env, ...options.env }
    });
  }
}

Client Development

Lit Components

// components/terminal-view.ts
@customElement('terminal-view')
export class TerminalView extends LitElement {
  @property({ type: String }) sessionId = '';
  
  private terminal?: Terminal;
  private ws?: WebSocket;
  
  createRenderRoot() {
    return this; // No shadow DOM for Tailwind
  }
  
  firstUpdated() {
    this.initTerminal();
    this.connectWebSocket();
  }
  
  render() {
    return html`
      <div id="terminal" class="h-full w-full"></div>
    `;
  }
}

WebSocket Client

// services/websocket-client.ts
export class WebSocketClient {
  private ws?: WebSocket;
  
  connect(sessionId: string): void {
    this.ws = new WebSocket(`ws://localhost:4020/api/sessions/${sessionId}/ws`);
    this.ws.binaryType = 'arraybuffer';
    
    this.ws.onmessage = (event) => {
      if (event.data instanceof ArrayBuffer) {
        const text = this.decodeBuffer(event.data);
        this.onData?.(text);
      }
    };
  }
  
  send(data: string): void {
    this.ws?.send(JSON.stringify({ type: 'input', data }));
  }
}

Terminal Integration

// services/terminal-service.ts
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';

export class TerminalService {
  private terminal: Terminal;
  private fitAddon: FitAddon;
  
  initialize(container: HTMLElement): void {
    this.terminal = new Terminal({
      theme: {
        background: '#1e1e1e',
        foreground: '#ffffff'
      }
    });
    
    this.fitAddon = new FitAddon();
    this.terminal.loadAddon(this.fitAddon);
    this.terminal.open(container);
    this.fitAddon.fit();
  }
}

Build System

Development Build

// package.json scripts
{
  "dev": "concurrently \"npm:dev:*\"",
  "dev:server": "tsx watch src/server/server.ts",
  "dev:client": "vite",
  "dev:tailwind": "tailwindcss -w"
}

Production Build

# Build everything
pnpm build

# Outputs:
# dist/server/   - Compiled server
# dist/client/   - Static web assets
# dist/bun       - Standalone executable

Bun Compilation

// scripts/build-bun.ts
await Bun.build({
  entrypoints: ['src/server/server.ts'],
  outdir: 'dist',
  target: 'bun',
  minify: true,
  sourcemap: 'external'
});

Testing

Unit Tests

// tests/terminal-manager.test.ts
describe('TerminalManager', () => {
  it('creates session', async () => {
    const manager = new TerminalManager();
    const session = await manager.create({ shell: '/bin/bash' });
    expect(session.id).toBeDefined();
  });
});

E2E Tests

// tests/e2e/session.test.ts
test('create and connect to session', async ({ page }) => {
  await page.goto('http://localhost:4020');
  await page.click('button:text("New Terminal")');
  await expect(page.locator('.terminal')).toBeVisible();
});

Performance

Optimization Techniques

TechniqueImplementationImpact
Buffer aggregationBatch every 16ms90% fewer messages
Binary protocolMagic byte encoding50% smaller payload
Virtual scrollingxterm.js built-inHandles 100K+ lines
Service workerCache static assetsInstant load

Benchmarks

// Measure WebSocket throughput
const start = performance.now();
let bytes = 0;

ws.onmessage = (event) => {
  bytes += event.data.byteLength;
  if (performance.now() - start > 1000) {
    console.log(`Throughput: ${bytes / 1024}KB/s`);
  }
};

Debugging

Server Debugging

# Run with inspector
node --inspect dist/server/server.js

# With source maps
NODE_OPTIONS='--enable-source-maps' node dist/server/server.js

# Verbose logging
DEBUG=vt:* pnpm dev:server

Client Debugging

// Enable xterm.js debug mode
terminal.options.logLevel = 'debug';

// WebSocket debugging
ws.addEventListener('message', (e) => {
  console.log('WS received:', e.data);
});

Common Issues

IssueSolution
CORS errorsCheck server CORS config
WebSocket failsVerify port/firewall
Terminal garbledCheck encoding (UTF-8)
Build failsClear node_modules

See Also