UNPKG

skeleton-crew-runtime

Version:

A minimal, plugin-based application runtime for building internal tools and modular applications

727 lines (547 loc) 19.1 kB
# Skeleton Crew Runtime **Modernize your legacy app without rewriting it.** A minimal runtime that lets you add new features to existing applications using a clean plugin architecture — while keeping your old code running. ## The Problem You have a working application, but: - Adding features means touching fragile legacy code - Different parts use different patterns (callbacks, promises, globals) - Testing is hard because everything is tightly coupled - You want to use modern patterns but can't justify a full rewrite **Sound familiar?** ## The Solution Skeleton Crew lets you write new features as isolated plugins that can access your existing serviceswithout touching legacy code. ```typescript // Your existing app (unchanged) const legacyApp = { database: new DatabaseConnection(), logger: new Logger(), userService: new UserService() }; // Add Skeleton Crew alongside it const runtime = new Runtime({ hostContext: { db: legacyApp.database, logger: legacyApp.logger, users: legacyApp.userService } }); // Write new features as clean, testable plugins const newFeaturePlugin = { name: "analytics", version: "1.0.0", setup(ctx) { // Access legacy services through context const { db, logger } = ctx.host; ctx.actions.registerAction({ id: "analytics:track", handler: async (event) => { logger.info("Tracking event:", event); await db.insert("analytics", event); ctx.events.emit("analytics:tracked", event); } }); } }; await runtime.initialize(); runtime.getContext().plugins.registerPlugin(newFeaturePlugin); ``` **Result:** New code is clean, testable, and isolated. Old code keeps working. ## Why This Approach Works ### ✅ Zero Risk Your existing code doesn't change. New features run alongside it. ### ✅ Incremental Migration Migrate one feature at a time. No big-bang rewrites. ### ✅ Immediate Value Start writing better code today. See benefits immediately. ### ✅ Team Friendly New developers work in clean plugin code. Legacy experts maintain old code. ### ✅ Future Proof When you're ready, gradually replace legacy services. Or don't — both work fine. ## Quick Start: Add Your First Feature ### 1. Install ```bash npm install skeleton-crew-runtime ``` ### 2. Create Runtime with Your Existing Services ```typescript import { Runtime } from "skeleton-crew-runtime"; // Inject your existing services const runtime = new Runtime({ hostContext: { db: yourDatabase, api: yourApiClient, logger: yourLogger, config: yourConfig } }); await runtime.initialize(); ``` ### 3. Write a Plugin for Your New Feature ```typescript // plugins/notifications.ts export const NotificationsPlugin = { name: "notifications", version: "1.0.0", setup(ctx) { // Access your existing services const { db, logger } = ctx.host; // Register an action ctx.actions.registerAction({ id: "notifications:send", handler: async ({ userId, message }) => { logger.info(`Sending notification to user ${userId}`); // Use your existing database await db.insert("notifications", { userId, message, createdAt: new Date() }); // Emit event for other plugins ctx.events.emit("notification:sent", { userId, message }); return { success: true }; } }); // Listen to events from other parts of your app ctx.events.on("user:registered", async (user) => { await ctx.actions.runAction("notifications:send", { userId: user.id, message: "Welcome to our app!" }); }); } }; ``` ### 4. Register and Use ```typescript const ctx = runtime.getContext(); // Register your plugin ctx.plugins.registerPlugin(NotificationsPlugin); // Call from anywhere in your app await ctx.actions.runAction("notifications:send", { userId: 123, message: "Your order has shipped!" }); ``` **That's it.** Your new feature is isolated, testable, and doesn't touch legacy code. ## Core Concepts (5 Minutes to Learn) ### 1. Host Context: Bridge to Legacy Code Inject your existing services so plugins can use them: ```typescript const runtime = new Runtime({ hostContext: { db: legacyDatabase, // Your existing DB connection cache: redisClient, // Your existing cache auth: authService // Your existing auth } }); // Plugins access via ctx.host const { db, cache, auth } = ctx.host; ``` ### 2. Actions: Business Logic Encapsulate operations as named, testable actions: ```typescript ctx.actions.registerAction({ id: "orders:create", handler: async (orderData) => { const { db } = ctx.host; const order = await db.insert("orders", orderData); ctx.events.emit("order:created", order); return order; } }); // Call from anywhere const order = await ctx.actions.runAction("orders:create", data); ``` ### 3. Events: Decouple Features Let features communicate without direct dependencies: ```typescript // Feature A: Emit event ctx.events.emit("order:created", order); // Feature B: React to event (doesn't know about Feature A) ctx.events.on("order:created", (order) => { ctx.actions.runAction("email:send", { to: order.customerEmail, template: "order-confirmation" }); }); ``` ### 4. Plugins: Isolated Features Group related functionality into plugins: ```typescript export const OrdersPlugin = { name: "orders", version: "1.0.0", setup(ctx) { // Register actions, screens, event handlers // Everything for "orders" feature in one place } }; ``` ### 5. Screens (Optional): UI Definitions Define screens that any UI framework can render: ```typescript ctx.screens.registerScreen({ id: "orders:list", title: "Orders", component: OrderListComponent // React, Vue, or anything }); ``` ## Real-World Migration Patterns ### Pattern 1: Feature Flags (Safest) Gradually switch features from legacy to plugins: ```typescript const features = { notifications: 'plugin', // Using Skeleton Crew payments: 'legacy', // Still using old code reports: 'plugin' // Using Skeleton Crew }; class App { async sendNotification(userId, message) { if (features.notifications === 'plugin') { return this.runtime.getContext().actions.runAction( 'notifications:send', { userId, message } ); } else { return this.legacyNotifications.send(userId, message); } } } ``` **Benefits:** Roll back instantly if issues arise. Test in production with small user percentage. ### Pattern 2: New Features Only Keep legacy code frozen. All new features are plugins: ```typescript // Legacy code (frozen, no changes) class LegacyApp { constructor() { this.db = new Database(); this.users = new UserService(this.db); } } // New features as plugins const runtime = new Runtime({ hostContext: { db: legacyApp.db, users: legacyApp.users } }); // New "analytics" feature - clean plugin code const AnalyticsPlugin = { name: "analytics", version: "1.0.0", setup(ctx) { const { db, users } = ctx.host; ctx.actions.registerAction({ id: "analytics:track", handler: async (event) => { await db.insert("analytics", event); } }); } }; ``` **Benefits:** Legacy code stays stable. New code is clean and testable. No rewrite needed. ### Pattern 3: Strangler Fig Gradually replace legacy services with plugin-based ones: ```typescript // Phase 1: Legacy service const legacyAuth = new LegacyAuthService(); // Phase 2: New auth plugin (same interface) const AuthPlugin = { name: "auth", version: "2.0.0", setup(ctx) { ctx.actions.registerAction({ id: "auth:login", handler: async (credentials) => { // New implementation } }); } }; // Phase 3: Switch gradually const useNewAuth = process.env.NEW_AUTH === 'true'; async function login(credentials) { if (useNewAuth) { return runtime.getContext().actions.runAction('auth:login', credentials); } else { return legacyAuth.login(credentials); } } ``` **Benefits:** Replace services one at a time. Run both versions in parallel. Validate before full switch. ### Pattern 4: Event-Driven Integration Let legacy code emit events that plugins handle: ```typescript // Legacy code (minimal change - just emit events) class LegacyUserService { async createUser(userData) { const user = await this.db.insert('users', userData); // Add this one line eventBus.emit('user:created', user); return user; } } // New plugin reacts to legacy events const WelcomeEmailPlugin = { name: "welcome-emails", version: "1.0.0", setup(ctx) { ctx.events.on('user:created', async (user) => { await ctx.actions.runAction('email:send', { to: user.email, template: 'welcome' }); }); } }; ``` **Benefits:** Minimal legacy changes. New features are completely isolated. Easy to add/remove features. ## When to Use Skeleton Crew ### ✅ Perfect For - **Legacy modernization** - Add features without touching old code - **Internal tools** - Admin panels, dashboards, dev tools - **Browser extensions** - Background scripts with plugin architecture - **Modular applications** - Features that can be enabled/disabled - **Multi-team codebases** - Teams work on isolated plugins - **Gradual rewrites** - Migrate piece by piece ### ❌ Not Ideal For - **Greenfield apps with simple needs** - Might be overkill - **Static websites** - No need for runtime architecture - **Apps with single feature** - Plugin system adds unnecessary complexity --- ## Advanced Features ### Introspection API Debug and monitor your runtime: ```typescript const ctx = runtime.getContext(); // List all registered resources const actions = ctx.introspect.listActions(); const plugins = ctx.introspect.listPlugins(); const screens = ctx.introspect.listScreens(); // Get metadata const stats = ctx.introspect.getMetadata(); // { runtimeVersion: "0.1.0", totalActions: 15, totalPlugins: 5 } ``` **Use cases:** Admin dashboards, debugging tools, runtime monitoring. ### Action Timeouts Prevent hanging operations: ```typescript ctx.actions.registerAction({ id: "api:fetch", timeout: 5000, // 5 seconds max handler: async () => { return await fetch('/api/data'); } }); ``` ### Event Patterns **Fire-and-forget:** ```typescript ctx.events.emit('user:created', user); // Synchronous ``` **Wait for handlers:** ```typescript await ctx.events.emitAsync('order:processed', order); // Asynchronous ``` --- ## Learning Resources ### Complete Migration Guide See [Migration Guide](docs/guides/migration-guide.md) for: - Step-by-step migration strategies - Real-world examples - Common pitfalls and solutions - Testing strategies ### Tutorial: Build a Task Manager Learn by building a complete app from scratch: ```bash npm run build npm run tutorial:01 # Start with step 1 ``` See [example/tutorial/README.md](example/tutorial/README.md). ### Example Applications Run focused examples: ```bash npm run example:01 # Plugin System npm run example:02 # Screen Registry npm run example:03 # Action Engine npm run example:04 # Event Bus npm run example:05 # Runtime Context npm run example # Complete playground ``` ## How It Works ``` ┌─────────────────────────────────────────────────┐ │ Your Legacy Application │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Database │ │ API │ │ Logger │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ └─────────────┴──────────────┘ │ │ │ │ │ Host Context (injected) │ └─────────────────────┬───────────────────────────┘ │ ┌─────────────────────▼───────────────────────────┐ │ Skeleton Crew Runtime │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ Plugin 1 Plugin 2 Plugin 3 │ │ │ │ (new code) (new code) (new code) │ │ │ └──────────────────────────────────────────┘ │ │ │ │ │ │ │ ┌────▼────┐ ┌───▼────┐ ┌───▼────┐ │ │ │ Actions │ │ Events │ │Screens │ │ │ └─────────┘ └────────┘ └────────┘ │ └─────────────────────────────────────────────────┘ ``` **Key insight:** Legacy services flow up through host context. New features are isolated plugins that use those services. ## Testing Your Plugins One of the biggest wins: plugins are easy to test. ```typescript import { describe, it, expect, beforeEach } from 'vitest'; import { Runtime } from 'skeleton-crew-runtime'; import { NotificationsPlugin } from './notifications.js'; describe('NotificationsPlugin', () => { let runtime; let mockDb; beforeEach(async () => { // Mock your legacy services mockDb = { insert: vi.fn().mockResolvedValue({ id: 1 }) }; // Create isolated runtime for testing runtime = new Runtime({ hostContext: { db: mockDb } }); await runtime.initialize(); runtime.getContext().plugins.registerPlugin(NotificationsPlugin); }); it('sends notification', async () => { const ctx = runtime.getContext(); const result = await ctx.actions.runAction('notifications:send', { userId: 123, message: 'Test' }); expect(result.success).toBe(true); expect(mockDb.insert).toHaveBeenCalledWith('notifications', { userId: 123, message: 'Test', createdAt: expect.any(Date) }); }); }); ``` **Benefits:** - No need to set up entire legacy app - Mock only what you need - Fast, isolated tests - Easy to test edge cases ## API Quick Reference ### Runtime Setup ```typescript import { Runtime } from 'skeleton-crew-runtime'; const runtime = new Runtime({ hostContext: { // Your existing services db: yourDatabase, logger: yourLogger } }); await runtime.initialize(); const ctx = runtime.getContext(); ``` ### Working with Actions ```typescript // Register ctx.actions.registerAction({ id: 'feature:action', timeout: 5000, // optional handler: async (params) => { const { db } = ctx.host; return await db.query(params); } }); // Execute const result = await ctx.actions.runAction('feature:action', params); ``` ### Working with Events ```typescript // Subscribe ctx.events.on('entity:changed', (data) => { console.log('Changed:', data); }); // Emit (fire-and-forget) ctx.events.emit('entity:changed', { id: 123 }); // Emit (wait for handlers) await ctx.events.emitAsync('entity:changed', { id: 123 }); ``` ### Working with Plugins ```typescript // Define export const MyPlugin = { name: 'my-plugin', version: '1.0.0', setup(ctx) { // Register actions, screens, events }, dispose(ctx) { // Optional cleanup } }; // Register ctx.plugins.registerPlugin(MyPlugin); ``` ### Accessing Host Context ```typescript // In any plugin setup(ctx) { const { db, logger, cache } = ctx.host; // Use your existing services await db.query('SELECT * FROM users'); logger.info('Plugin initialized'); } ``` ### Introspection ```typescript // List resources const actions = ctx.introspect.listActions(); const plugins = ctx.introspect.listPlugins(); // Get metadata const actionMeta = ctx.introspect.getActionDefinition('feature:action'); const stats = ctx.introspect.getMetadata(); ``` **Full API documentation:** [API.md](docs/api/API.md) ## FAQ ### Do I need to rewrite my app? No. Skeleton Crew runs alongside your existing code. Write new features as plugins, keep old code unchanged. ### What if I want to migrate existing features later? You can gradually replace legacy code with plugins using feature flags. Or don't — both approaches work fine. ### Does this work with my UI framework? Yes. Skeleton Crew is UI-agnostic. Use React, Vue, Svelte, or no UI at all. The runtime doesn't care. ### Is this overkill for small apps? Possibly. If you have a simple app with no legacy code and no plans to grow, you might not need this. But if you're dealing with technical debt or planning for modularity, it's a good fit. ### How big is the runtime? Less than 5KB gzipped. Minimal overhead. ### Can I use this in production? Yes. The runtime is stable and tested. Start with non-critical features, then expand. --- ## Documentation - **[Migration Guide](docs/guides/migration-guide.md)** - Step-by-step migration strategies - **[API Reference](docs/api/API.md)** - Complete TypeScript API - **[Examples Guide](docs/guides/EXAMPLES_GUIDE.md)** - Learn through examples - **[Browser Extensions](docs/use-cases/BROWSER_TOOLS.md)** - Building browser tools - **[Project Overview](docs/PROJECT_OVERVIEW.md)** - Architecture deep dive ## Get Started ```bash npm install skeleton-crew-runtime ``` Then follow the [Quick Start](#quick-start-add-your-first-feature) above. --- ## Contributing We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ## License MIT --- **Built for developers who need to modernize legacy apps without the risk of a full rewrite.**