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
- Architecture - Overall architecture overview
- Widgets - Understanding widgets
- Platform Guides - Platform-specific features