Skip to main content

Runtime System

Deep dive into unblessed's runtime dependency injection system.

Overview

The runtime system is the key to unblessed's cross-platform capability. It allows the same widget code to run in Node.js, browsers, and potentially other platforms like Deno or Bun.

The Runtime Interface

All platform-specific APIs are abstracted behind the Runtime interface:

export interface Runtime {
fs: FileSystemAPI;
process: ProcessAPI;
tty: TtyAPI;
buffer: BufferAPI;
stream: StreamAPI;
colors: ColorsAPI;
unicode: UnicodeAPI;
nextTick: (callback: () => void) => void;
}

API Categories

FileSystemAPI

Abstracts file system operations:

interface FileSystemAPI {
readFileSync(path: string, encoding?: string): string | Buffer;
existsSync(path: string): boolean;
statSync(path: string): { isDirectory(): boolean; isFile(): boolean };
// ... other fs methods
}

ProcessAPI

Abstracts process information and control:

interface ProcessAPI {
platform: string;
env: Record<string, string | undefined>;
cwd(): string;
exit(code?: number): never;
on(event: string, listener: Function): void;
// ... other process methods
}

TtyAPI

Abstracts terminal control:

interface TtyAPI {
isatty(fd: number): boolean;
getWindowSize(): [number, number];
setRawMode(mode: boolean): void;
write(data: string | Buffer): void;
// ... other TTY methods
}

BufferAPI

Abstracts buffer handling:

interface BufferAPI {
Buffer: typeof Buffer;
from(data: string | ArrayBuffer, encoding?: string): Buffer;
alloc(size: number): Buffer;
concat(buffers: Buffer[]): Buffer;
// ... other buffer methods
}

Using the Runtime

In Core Code

All core code accesses platform APIs through the runtime:

import { getRuntime } from '@unblessed/core/runtime-context';

export class MyWidget extends Element {
loadData(filename: string) {
const runtime = getRuntime();

// Use runtime instead of direct `fs` import
if (runtime.fs.existsSync(filename)) {
const data = runtime.fs.readFileSync(filename, 'utf8');
this.setContent(data);
}
}
}

Why: This allows the same widget to work in Node.js (using real fs) and browsers (using polyfills).

Runtime Context

The runtime is stored in a global context:

// runtime-context.ts
let globalRuntime: Runtime | null = null;

export function setRuntime(runtime: Runtime): void {
globalRuntime = runtime;
}

export function getRuntime(): Runtime {
if (!globalRuntime) {
throw new Error('Runtime not initialized');
}
return globalRuntime;
}

Platform Implementations

Node.js Runtime

@unblessed/node provides a Node.js implementation:

// node-runtime.ts
import fs from 'fs';
import process from 'process';
import tty from 'tty';

export class NodeRuntime implements Runtime {
fs = {
readFileSync: fs.readFileSync.bind(fs),
existsSync: fs.existsSync.bind(fs),
statSync: fs.statSync.bind(fs),
// ... other fs methods
};

process = {
platform: process.platform,
env: process.env,
cwd: process.cwd.bind(process),
exit: process.exit.bind(process),
on: process.on.bind(process),
// ... other process methods
};

tty = {
isatty: tty.isatty.bind(tty),
// ... other TTY methods
};

// ... other runtime APIs
}

Browser Runtime

@unblessed/browser provides polyfills:

// browser-runtime.ts
import { Buffer } from 'buffer';

export class BrowserRuntime implements Runtime {
fs = {
readFileSync: (path: string) => {
// Use bundled terminfo data or throw error
const data = BUNDLED_FILES[path];
if (!data) {
throw new Error(`File not found: ${path}`);
}
return data;
},
existsSync: (path: string) => {
return path in BUNDLED_FILES;
},
// ... other polyfilled fs methods
};

process = {
platform: 'browser',
env: {},
cwd: () => '/',
exit: (code?: number) => {
throw new Error('process.exit() not supported in browser');
},
on: (event: string, listener: Function) => {
// Browser event handling
},
// ... other polyfilled process methods
};

buffer = {
Buffer: Buffer,
from: Buffer.from.bind(Buffer),
alloc: Buffer.alloc.bind(Buffer),
// ... other buffer methods
};

// ... other runtime APIs
}

Auto-Initialization

Each platform package auto-initializes the runtime on import:

// @unblessed/node/src/auto-init.ts
import { setRuntime } from '@unblessed/core';
import { NodeRuntime } from './node-runtime';

// Initialize runtime when package is imported
setRuntime(new NodeRuntime());
// @unblessed/node/src/index.ts
import './auto-init'; // Runs first
export * from '@unblessed/core'; // Then export widgets

When you import from @unblessed/node, the runtime is set up before any widgets are created.

Testing with Mock Runtimes

The runtime system makes testing easy with mock implementations:

import { setRuntime } from '@unblessed/core';

const mockRuntime: Runtime = {
fs: {
readFileSync: vi.fn(() => 'mock data'),
existsSync: vi.fn(() => true),
// ... other mocked methods
},
process: {
platform: 'test',
env: {},
// ... other mocked methods
},
// ... other mock APIs
};

beforeEach(() => {
setRuntime(mockRuntime);
});

Adding New Platforms

To support a new platform (e.g., Deno):

1. Create Runtime Implementation

// @unblessed/deno/src/deno-runtime.ts
import { Runtime } from '@unblessed/core';

export class DenoRuntime implements Runtime {
fs = {
readFileSync: (path: string, encoding?: string) => {
return Deno.readTextFileSync(path);
},
existsSync: (path: string) => {
try {
Deno.statSync(path);
return true;
} catch {
return false;
}
},
// ... implement other fs methods
};

process = {
platform: Deno.build.os,
env: Deno.env.toObject(),
cwd: Deno.cwd.bind(Deno),
// ... implement other process methods
};

// ... implement other runtime APIs
}

2. Auto-Initialize

// @unblessed/deno/src/auto-init.ts
import { setRuntime } from '@unblessed/core';
import { DenoRuntime } from './deno-runtime';

setRuntime(new DenoRuntime());

3. Export Widgets

// @unblessed/deno/src/index.ts
import './auto-init';
export * from '@unblessed/core';

Now unblessed works in Deno!

Runtime Environment Detection

Sometimes you need to detect the current platform:

import { getRuntime } from '@unblessed/core/runtime-context';

const runtime = getRuntime();

if (runtime.process.platform === 'browser') {
// Browser-specific code
} else if (runtime.process.platform === 'darwin') {
// macOS-specific code
}

Advanced: Runtime Capabilities

Check if specific features are available:

function supportsColor(): boolean {
const runtime = getRuntime();
return runtime.tty?.isatty?.(1) ?? false;
}

function hasFileSystem(): boolean {
const runtime = getRuntime();
try {
runtime.fs.existsSync('/');
return true;
} catch {
return false;
}
}

Benefits

Cross-Platform Code

Write once, run anywhere:

// This widget works in Node.js AND browsers
export class FileViewer extends Box {
loadFile(path: string) {
const runtime = getRuntime();
const content = runtime.fs.readFileSync(path, 'utf8');
this.setContent(content);
}
}

Testability

Easy to mock and test:

test('FileViewer loads file', () => {
const mockFs = {
readFileSync: vi.fn(() => 'test content'),
};
setRuntime({ ...mockRuntime, fs: mockFs });

const viewer = new FileViewer();
viewer.loadFile('/test.txt');

expect(mockFs.readFileSync).toHaveBeenCalledWith('/test.txt', 'utf8');
expect(viewer.content).toBe('test content');
});

Future-Proof

Easy to add new platforms without changing core code.

Next Steps