Cross-Runtime Compatibility
Comprehensive research and implementation guide for building Verb applications that work seamlessly across Bun, Node.js, and other JavaScript runtimes.
Overview
Cross-runtime compatibility ensures your Verb applications can run on multiple JavaScript runtimes without modification. This guide covers:
- Runtime Detection: Identifying the current runtime environment
- API Abstraction: Creating unified interfaces across runtimes
- Feature Polyfills: Filling runtime-specific gaps
- Build Strategies: Packaging for multiple targets
- Testing Approaches: Validating across all supported runtimes
Runtime Detection and Abstraction
Universal Runtime Detector
typescript
// runtime-detector.ts - Comprehensive runtime detection
export interface RuntimeInfo {
name: "bun" | "node" | "deno" | "browser" | "unknown";
version: string;
capabilities: {
nativeModules: boolean;
workers: boolean;
sqlite: boolean;
ffi: boolean;
typescript: boolean;
};
}
export class RuntimeDetector {
static detect(): RuntimeInfo {
// Bun detection
if (typeof Bun !== "undefined") {
return {
name: "bun",
version: Bun.version,
capabilities: {
nativeModules: true,
workers: true,
sqlite: true,
ffi: true,
typescript: true
}
};
}
// Deno detection
if (typeof Deno !== "undefined") {
return {
name: "deno",
version: Deno.version.deno,
capabilities: {
nativeModules: false,
workers: true,
sqlite: false,
ffi: true,
typescript: true
}
};
}
// Node.js detection
if (typeof process !== "undefined" && process.versions?.node) {
return {
name: "node",
version: process.version,
capabilities: {
nativeModules: true,
workers: true,
sqlite: false,
ffi: false,
typescript: false
}
};
}
// Browser detection
if (typeof window !== "undefined") {
return {
name: "browser",
version: navigator.userAgent,
capabilities: {
nativeModules: false,
workers: true,
sqlite: false,
ffi: false,
typescript: false
}
};
}
return {
name: "unknown",
version: "unknown",
capabilities: {
nativeModules: false,
workers: false,
sqlite: false,
ffi: false,
typescript: false
}
};
}
static isBun(): boolean {
return this.detect().name === "bun";
}
static isNode(): boolean {
return this.detect().name === "node";
}
static isDeno(): boolean {
return this.detect().name === "deno";
}
static isBrowser(): boolean {
return this.detect().name === "browser";
}
static hasCapability(capability: keyof RuntimeInfo["capabilities"]): boolean {
return this.detect().capabilities[capability];
}
}
Universal File System API
typescript
// fs-universal.ts - Cross-runtime file system abstraction
export interface UniversalFS {
readFile(path: string): Promise<string>;
writeFile(path: string, content: string): Promise<void>;
exists(path: string): Promise<boolean>;
mkdir(path: string): Promise<void>;
readdir(path: string): Promise<string[]>;
}
class BunFS implements UniversalFS {
async readFile(path: string): Promise<string> {
const file = Bun.file(path);
return await file.text();
}
async writeFile(path: string, content: string): Promise<void> {
await Bun.write(path, content);
}
async exists(path: string): Promise<boolean> {
const file = Bun.file(path);
return await file.exists();
}
async mkdir(path: string): Promise<void> {
await Bun.write(path + "/.gitkeep", "");
}
async readdir(path: string): Promise<string[]> {
const glob = new Bun.Glob("*");
return Array.from(glob.scanSync(path));
}
}
class NodeFS implements UniversalFS {
private fs = require("fs").promises;
async readFile(path: string): Promise<string> {
return await this.fs.readFile(path, "utf-8");
}
async writeFile(path: string, content: string): Promise<void> {
await this.fs.writeFile(path, content);
}
async exists(path: string): Promise<boolean> {
try {
await this.fs.access(path);
return true;
} catch {
return false;
}
}
async mkdir(path: string): Promise<void> {
await this.fs.mkdir(path, { recursive: true });
}
async readdir(path: string): Promise<string[]> {
return await this.fs.readdir(path);
}
}
class DenoFS implements UniversalFS {
async readFile(path: string): Promise<string> {
return await Deno.readTextFile(path);
}
async writeFile(path: string, content: string): Promise<void> {
await Deno.writeTextFile(path, content);
}
async exists(path: string): Promise<boolean> {
try {
await Deno.stat(path);
return true;
} catch {
return false;
}
}
async mkdir(path: string): Promise<void> {
await Deno.mkdir(path, { recursive: true });
}
async readdir(path: string): Promise<string[]> {
const entries = [];
for await (const entry of Deno.readDir(path)) {
entries.push(entry.name);
}
return entries;
}
}
export function createFS(): UniversalFS {
const runtime = RuntimeDetector.detect();
switch (runtime.name) {
case "bun":
return new BunFS();
case "node":
return new NodeFS();
case "deno":
return new DenoFS();
default:
throw new Error(`Unsupported runtime: ${runtime.name}`);
}
}
Universal HTTP Client
typescript
// http-universal.ts - Cross-runtime HTTP client
export interface HTTPResponse {
status: number;
headers: Record<string, string>;
text(): Promise<string>;
json<T = any>(): Promise<T>;
}
export interface HTTPClient {
get(url: string, options?: RequestInit): Promise<HTTPResponse>;
post(url: string, body?: any, options?: RequestInit): Promise<HTTPResponse>;
put(url: string, body?: any, options?: RequestInit): Promise<HTTPResponse>;
delete(url: string, options?: RequestInit): Promise<HTTPResponse>;
}
class UniversalHTTPResponse implements HTTPResponse {
constructor(
public status: number,
public headers: Record<string, string>,
private response: Response
) {}
async text(): Promise<string> {
return await this.response.text();
}
async json<T = any>(): Promise<T> {
return await this.response.json();
}
}
class BunHTTPClient implements HTTPClient {
async get(url: string, options?: RequestInit): Promise<HTTPResponse> {
const response = await fetch(url, { ...options, method: "GET" });
return this.createResponse(response);
}
async post(url: string, body?: any, options?: RequestInit): Promise<HTTPResponse> {
const response = await fetch(url, {
...options,
method: "POST",
body: typeof body === "string" ? body : JSON.stringify(body),
headers: {
"Content-Type": "application/json",
...options?.headers
}
});
return this.createResponse(response);
}
async put(url: string, body?: any, options?: RequestInit): Promise<HTTPResponse> {
const response = await fetch(url, {
...options,
method: "PUT",
body: typeof body === "string" ? body : JSON.stringify(body),
headers: {
"Content-Type": "application/json",
...options?.headers
}
});
return this.createResponse(response);
}
async delete(url: string, options?: RequestInit): Promise<HTTPResponse> {
const response = await fetch(url, { ...options, method: "DELETE" });
return this.createResponse(response);
}
private createResponse(response: Response): HTTPResponse {
const headers: Record<string, string> = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
return new UniversalHTTPResponse(response.status, headers, response);
}
}
// Node.js and Deno can use the same implementation since they support fetch
export function createHTTPClient(): HTTPClient {
// All modern runtimes support fetch API
return new BunHTTPClient();
}
Database Abstraction Layer
Universal Database Interface
typescript
// database-universal.ts - Cross-runtime database abstraction
export interface DatabaseRow {
[key: string]: any;
}
export interface Database {
query(sql: string, params?: any[]): Promise<DatabaseRow[]>;
execute(sql: string, params?: any[]): Promise<{ changes: number; lastInsertRowid?: number }>;
close(): Promise<void>;
transaction<T>(fn: (db: Database) => Promise<T>): Promise<T>;
}
class BunDatabase implements Database {
private db: any;
constructor(path: string) {
const { Database } = require("bun:sqlite");
this.db = new Database(path);
}
async query(sql: string, params: any[] = []): Promise<DatabaseRow[]> {
const stmt = this.db.query(sql);
return stmt.all(...params);
}
async execute(sql: string, params: any[] = []): Promise<{ changes: number; lastInsertRowid?: number }> {
const stmt = this.db.query(sql);
const result = stmt.run(...params);
return {
changes: result.changes,
lastInsertRowid: result.lastInsertRowid
};
}
async close(): Promise<void> {
this.db.close();
}
async transaction<T>(fn: (db: Database) => Promise<T>): Promise<T> {
const transaction = this.db.transaction(() => {
return fn(this);
});
return transaction();
}
}
class NodeDatabase implements Database {
private db: any;
constructor(path: string) {
const Database = require("better-sqlite3");
this.db = new Database(path);
}
async query(sql: string, params: any[] = []): Promise<DatabaseRow[]> {
const stmt = this.db.prepare(sql);
return stmt.all(params);
}
async execute(sql: string, params: any[] = []): Promise<{ changes: number; lastInsertRowid?: number }> {
const stmt = this.db.prepare(sql);
const result = stmt.run(params);
return {
changes: result.changes,
lastInsertRowid: result.lastInsertRowid
};
}
async close(): Promise<void> {
this.db.close();
}
async transaction<T>(fn: (db: Database) => Promise<T>): Promise<T> {
const transaction = this.db.transaction(() => {
return fn(this);
});
return transaction();
}
}
export function createDatabase(path: string): Database {
const runtime = RuntimeDetector.detect();
switch (runtime.name) {
case "bun":
return new BunDatabase(path);
case "node":
return new NodeDatabase(path);
case "deno":
// Deno doesn't have native SQLite, would need WASM version
throw new Error("SQLite not supported in Deno runtime");
default:
throw new Error(`Database not supported in ${runtime.name} runtime`);
}
}
Universal Environment Management
typescript
// env-universal.ts - Cross-runtime environment variables
export interface EnvironmentConfig {
get(key: string): string | undefined;
get(key: string, defaultValue: string): string;
set(key: string, value: string): void;
has(key: string): boolean;
getAll(): Record<string, string>;
}
class UniversalEnvironment implements EnvironmentConfig {
private env: Record<string, string>;
constructor() {
const runtime = RuntimeDetector.detect();
switch (runtime.name) {
case "bun":
case "node":
this.env = process.env as Record<string, string>;
break;
case "deno":
this.env = Object.fromEntries(Object.entries(Deno.env.toObject()));
break;
case "browser":
// In browser, use a predefined config or localStorage
this.env = this.loadBrowserEnv();
break;
default:
this.env = {};
}
}
get(key: string): string | undefined;
get(key: string, defaultValue: string): string;
get(key: string, defaultValue?: string): string | undefined {
return this.env[key] ?? defaultValue;
}
set(key: string, value: string): void {
this.env[key] = value;
const runtime = RuntimeDetector.detect();
if (runtime.name === "deno") {
Deno.env.set(key, value);
} else if (runtime.name === "node" || runtime.name === "bun") {
process.env[key] = value;
}
}
has(key: string): boolean {
return key in this.env;
}
getAll(): Record<string, string> {
return { ...this.env };
}
private loadBrowserEnv(): Record<string, string> {
// In browser, load from window object or localStorage
if (typeof window !== "undefined") {
return (window as any).__ENV__ || {};
}
return {};
}
}
export const env = new UniversalEnvironment();
Build and Packaging Strategies
Universal Build Configuration
typescript
// build-config.ts - Multi-runtime build configuration
export interface BuildTarget {
runtime: "bun" | "node" | "deno" | "browser";
format: "esm" | "cjs" | "iife";
platform: "server" | "browser" | "universal";
minify: boolean;
sourcemap: boolean;
}
export const buildTargets: BuildTarget[] = [
{
runtime: "bun",
format: "esm",
platform: "server",
minify: false,
sourcemap: true
},
{
runtime: "node",
format: "cjs",
platform: "server",
minify: false,
sourcemap: true
},
{
runtime: "deno",
format: "esm",
platform: "server",
minify: false,
sourcemap: true
},
{
runtime: "browser",
format: "esm",
platform: "browser",
minify: true,
sourcemap: true
}
];
export async function buildForTarget(target: BuildTarget) {
const runtime = RuntimeDetector.detect();
if (runtime.name === "bun") {
return await buildWithBun(target);
} else if (runtime.name === "node") {
return await buildWithEsbuild(target);
} else if (runtime.name === "deno") {
return await buildWithDeno(target);
}
throw new Error(`Build not supported for ${runtime.name}`);
}
async function buildWithBun(target: BuildTarget) {
await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: `./dist/${target.runtime}`,
format: target.format as any,
minify: target.minify,
sourcemap: target.sourcemap ? "external" : "none",
target: target.platform === "browser" ? "browser" : "bun"
});
}
async function buildWithEsbuild(target: BuildTarget) {
const esbuild = require("esbuild");
await esbuild.build({
entryPoints: ["./src/index.ts"],
outdir: `./dist/${target.runtime}`,
format: target.format,
minify: target.minify,
sourcemap: target.sourcemap,
platform: target.platform === "browser" ? "browser" : "node",
target: target.runtime === "node" ? "node18" : "es2022"
});
}
async function buildWithDeno(target: BuildTarget) {
// Deno uses native bundling
const command = new Deno.Command("deno", {
args: [
"bundle",
"./src/index.ts",
`./dist/${target.runtime}/index.js`
]
});
await command.output();
}
Package.json for Multi-Runtime Support
json
{
"name": "verb-universal-app",
"version": "1.0.0",
"type": "module",
"main": "./dist/node/index.js",
"module": "./dist/bun/index.js",
"browser": "./dist/browser/index.js",
"deno": "./dist/deno/index.js",
"exports": {
".": {
"bun": "./dist/bun/index.js",
"node": "./dist/node/index.js",
"deno": "./dist/deno/index.js",
"browser": "./dist/browser/index.js",
"default": "./dist/node/index.js"
}
},
"engines": {
"node": ">=18.0.0",
"bun": ">=1.0.0"
},
"scripts": {
"build": "npm run build:all",
"build:all": "npm run build:bun && npm run build:node && npm run build:deno",
"build:bun": "bun run build-script.ts bun",
"build:node": "node build-script.js node",
"build:deno": "deno run --allow-all build-script.ts deno",
"test": "npm run test:all",
"test:all": "npm run test:bun && npm run test:node && npm run test:deno",
"test:bun": "bun test",
"test:node": "node --test",
"test:deno": "deno test"
},
"dependencies": {
"verb": "latest"
},
"devDependencies": {
"esbuild": "^0.19.0",
"typescript": "^5.0.0"
}
}
Testing Across Runtimes
Universal Test Runner
typescript
// test-runner.ts - Cross-runtime test execution
export interface TestCase {
name: string;
fn: () => Promise<void> | void;
skip?: boolean;
timeout?: number;
}
export class UniversalTestRunner {
private tests: TestCase[] = [];
private runtime = RuntimeDetector.detect();
test(name: string, fn: () => Promise<void> | void, options?: { skip?: boolean; timeout?: number }) {
this.tests.push({
name,
fn,
skip: options?.skip,
timeout: options?.timeout || 5000
});
}
async run(): Promise<{ passed: number; failed: number; skipped: number }> {
let passed = 0;
let failed = 0;
let skipped = 0;
console.log(`Running tests on ${this.runtime.name} ${this.runtime.version}`);
console.log("=".repeat(50));
for (const test of this.tests) {
if (test.skip) {
console.log(`⏭️ SKIP: ${test.name}`);
skipped++;
continue;
}
try {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Test timeout")), test.timeout);
});
await Promise.race([
Promise.resolve(test.fn()),
timeoutPromise
]);
console.log(`✅ PASS: ${test.name}`);
passed++;
} catch (error) {
console.log(`❌ FAIL: ${test.name}`);
console.log(` Error: ${error.message}`);
failed++;
}
}
console.log("=".repeat(50));
console.log(`Results: ${passed} passed, ${failed} failed, ${skipped} skipped`);
return { passed, failed, skipped };
}
}
// Usage example
const runner = new UniversalTestRunner();
runner.test("should detect runtime correctly", () => {
const runtime = RuntimeDetector.detect();
if (runtime.name === "unknown") {
throw new Error("Could not detect runtime");
}
});
runner.test("should read files universally", async () => {
const fs = createFS();
const content = await fs.readFile("./package.json");
if (!content.includes("verb")) {
throw new Error("Could not read package.json correctly");
}
});
runner.test("should make HTTP requests", async () => {
const http = createHTTPClient();
const response = await http.get("https://httpbin.org/json");
const data = await response.json();
if (!data.slideshow) {
throw new Error("Invalid response from httpbin");
}
});
// Run tests
if (import.meta.main) {
await runner.run();
}
Runtime-Specific Test Configuration
typescript
// test-config.ts - Runtime-specific test settings
export interface TestConfig {
timeout: number;
retries: number;
parallel: boolean;
coverage: boolean;
reporters: string[];
}
export function getTestConfig(): TestConfig {
const runtime = RuntimeDetector.detect();
const baseConfig: TestConfig = {
timeout: 5000,
retries: 0,
parallel: true,
coverage: true,
reporters: ["default"]
};
switch (runtime.name) {
case "bun":
return {
...baseConfig,
timeout: 3000, // Bun is faster
parallel: true,
reporters: ["default", "json"]
};
case "node":
return {
...baseConfig,
timeout: 5000,
parallel: true,
reporters: ["default", "junit"]
};
case "deno":
return {
...baseConfig,
timeout: 4000,
parallel: false, // Deno test runner limitations
coverage: false // Different coverage system
};
default:
return baseConfig;
}
}
Performance Optimization Across Runtimes
Runtime-Specific Optimizations
typescript
// optimizations.ts - Runtime-specific performance optimizations
export class PerformanceOptimizer {
static async optimizeForRuntime<T>(
implementations: {
bun?: () => Promise<T>;
node?: () => Promise<T>;
deno?: () => Promise<T>;
fallback: () => Promise<T>;
}
): Promise<T> {
const runtime = RuntimeDetector.detect();
try {
switch (runtime.name) {
case "bun":
return implementations.bun ? await implementations.bun() : await implementations.fallback();
case "node":
return implementations.node ? await implementations.node() : await implementations.fallback();
case "deno":
return implementations.deno ? await implementations.deno() : await implementations.fallback();
default:
return await implementations.fallback();
}
} catch (error) {
console.warn(`Runtime-specific implementation failed, using fallback: ${error.message}`);
return await implementations.fallback();
}
}
static createOptimizedBuffer(size: number): ArrayBuffer | Buffer {
const runtime = RuntimeDetector.detect();
if (runtime.name === "bun" || runtime.name === "node") {
return Buffer.allocUnsafe(size);
} else {
return new ArrayBuffer(size);
}
}
static async readFileOptimized(path: string): Promise<string> {
return await this.optimizeForRuntime({
bun: async () => {
const file = Bun.file(path);
return await file.text();
},
node: async () => {
const fs = require("fs").promises;
return await fs.readFile(path, "utf-8");
},
deno: async () => {
return await Deno.readTextFile(path);
},
fallback: async () => {
throw new Error("File reading not supported in this runtime");
}
});
}
}
Error Handling and Compatibility
Universal Error Types
typescript
// errors-universal.ts - Cross-runtime error handling
export class UniversalError extends Error {
public readonly code: string;
public readonly runtime: string;
public readonly originalError?: Error;
constructor(message: string, code: string, originalError?: Error) {
super(message);
this.name = "UniversalError";
this.code = code;
this.runtime = RuntimeDetector.detect().name;
this.originalError = originalError;
}
toJSON() {
return {
name: this.name,
message: this.message,
code: this.code,
runtime: this.runtime,
stack: this.stack,
originalError: this.originalError?.message
};
}
}
export class RuntimeCompatibilityError extends UniversalError {
constructor(feature: string, runtime: string) {
super(
`Feature '${feature}' is not supported in ${runtime} runtime`,
"RUNTIME_INCOMPATIBLE"
);
}
}
export function handleRuntimeError(error: unknown): UniversalError {
if (error instanceof UniversalError) {
return error;
}
if (error instanceof Error) {
return new UniversalError(error.message, "GENERIC_ERROR", error);
}
return new UniversalError(
`Unknown error: ${String(error)}`,
"UNKNOWN_ERROR"
);
}
Documentation and Best Practices
Cross-Runtime Development Guidelines
markdown
# Cross-Runtime Development Best Practices
## 1. Always Use Runtime Detection
```typescript
const runtime = RuntimeDetector.detect();
if (!runtime.capabilities.sqlite) {
throw new RuntimeCompatibilityError("SQLite", runtime.name);
}
2. Prefer Universal APIs
- Use
fetch()
instead of runtime-specific HTTP clients - Use
ReadableStream
instead of Node.js streams - Use
URL
andURLSearchParams
for URL manipulation
3. Graceful Degradation
- Always provide fallback implementations
- Test each runtime-specific code path
- Document runtime requirements clearly
4. Environment Consistency
- Use the universal environment abstraction
- Validate required environment variables at startup
- Provide sensible defaults for optional configuration
5. Error Handling
- Use universal error types
- Include runtime information in error messages
- Test error scenarios across all runtimes
This comprehensive cross-runtime compatibility guide ensures Verb applications can run seamlessly across Bun, Node.js, Deno, and even browser environments with appropriate abstractions and fallbacks.