Plugin Integration

Learn how MediaProc plugins integrate with the core system, including registration, command handling, and lifecycle management.

Integration Architecture

Plugin Discovery

MediaProc discovers plugins through npm package naming convention:

@mediaproc/image     # Official plugin
@mediaproc/video     # Official plugin
@company/mediaproc-* # Third-party plugins

Plugin Loading

// Core loads plugins dynamically
async function loadPlugin(pluginName: string): Promise<Plugin> {
  const pluginPath = `@mediaproc/${pluginName}`;
  const plugin = await import(pluginPath);
  return plugin.default || plugin;
}

Registration System

Plugin Registration

Every plugin must export a default registration function:

// src/register.ts
import type { Command } from "commander";
import { imageCommand } from "./commands/index.js";

export default function register(program: Command): void {
  const pluginCmd = program
    .command("image")
    .description("Image processing commands");

  // Register all commands
  imageCommand(pluginCmd);
}

Command Registration

Commands are registered with the plugin's command group:

// src/commands/resize.ts
export function registerResize(pluginCmd: Command): void {
  pluginCmd
    .command("resize <input>")
    .description("Resize image to specified dimensions")
    .option("-w, --width <pixels>", "Target width")
    .option("-h, --height <pixels>", "Target height")
    .action(async (input: string, options: ResizeOptions) => {
      await resizeImage(input, options);
    });
}

Lifecycle Hooks

Initialization

export interface Plugin {
  name: string;
  version: string;
  init?: () => Promise<void>;
  register: (program: Command) => void;
  cleanup?: () => Promise<void>;
}

// Plugin with initialization
export default {
  name: "image",
  version: "1.0.0",

  async init() {
    // Initialize resources
    await loadNativeBindings();
    await validateDependencies();
  },

  register(program: Command) {
    // Register commands
  },

  async cleanup() {
    // Clean up resources
    await releaseNativeBindings();
  },
} as Plugin;

Command Execution

// Core handles command execution
async function executeCommand(
  command: string,
  args: string[],
  options: CommandOptions,
): Promise<void> {
  try {
    // Pre-execution hook
    await onBeforeExecute?.(command, args, options);

    // Execute command
    await runCommand(command, args, options);

    // Post-execution hook
    await onAfterExecute?.(command, args, options);
  } catch (error) {
    // Error handling hook
    await onError?.(error, command, args, options);
    throw error;
  }
}

Dependency Management

Plugin Dependencies

Declare dependencies in package.json:

{
  "name": "@mediaproc/image",
  "dependencies": {
    "sharp": "^0.32.0",
    "commander": "^11.0.0"
  },
  "peerDependencies": {
    "@mediaproc/core": "^1.0.0"
  }
}

Runtime Dependencies

Check dependencies at runtime:

export async function init(): Promise<void> {
  try {
    await import("sharp");
  } catch {
    throw new Error("sharp is required. Install with: npm install sharp");
  }
}

Command Sharing

Shared Utilities

Plugins can export utilities for other plugins:

// @mediaproc/core/utils
export { validatePath } from "./path-validator.js";
export { formatSize } from "./format-helper.js";
export { createProgress } from "./progress.js";

// Other plugins can import
import { validatePath } from "@mediaproc/core/utils";

Cross-Plugin Commands

Plugins can invoke commands from other plugins:

import { execCommand } from "@mediaproc/core";

async function processVideo(input: string): Promise<void> {
  // Extract frame using video plugin
  const frame = await execCommand("video", "extract-frame", {
    input,
    time: "00:00:05",
  });

  // Process frame using image plugin
  await execCommand("image", "resize", {
    input: frame,
    width: 800,
  });
}

Event System

Plugin Events

import { EventEmitter } from "events";

export const pluginEvents = new EventEmitter();

// Emit events
pluginEvents.emit("processing:start", { file: "image.jpg" });
pluginEvents.emit("processing:progress", { percent: 50 });
pluginEvents.emit("processing:complete", { file: "image.jpg" });

// Listen to events
pluginEvents.on("processing:start", (data) => {
  console.log(`Processing started: ${data.file}`);
});

Core Events

Core emits global events:

// Core events
core.on("plugin:loaded", (plugin) => {
  console.log(`Loaded plugin: ${plugin.name}`);
});

core.on("command:execute", (command, args) => {
  console.log(`Executing: ${command}`);
});

core.on("error", (error) => {
  console.error("Error:", error);
});

Configuration Integration

Plugin Configuration

interface PluginConfig {
  [key: string]: any;
}

export function loadConfig(): PluginConfig {
  const config = loadCoreConfig();
  return config.plugins?.image || {};
}

// Usage
const config = loadConfig();
const defaultQuality = config.defaultQuality || 90;

Configuration Schema

export const configSchema = {
  type: "object",
  properties: {
    defaultQuality: {
      type: "number",
      minimum: 1,
      maximum: 100,
      default: 90,
    },
    defaultFormat: {
      type: "string",
      enum: ["jpg", "png", "webp"],
      default: "jpg",
    },
  },
};

Error Handling

Custom Error Types

export class PluginError extends Error {
  constructor(
    message: string,
    public plugin: string,
    public code?: string,
  ) {
    super(message);
    this.name = "PluginError";
  }
}

export class CommandError extends PluginError {
  constructor(
    message: string,
    plugin: string,
    public command: string,
  ) {
    super(message, plugin);
    this.name = "CommandError";
  }
}

Error Propagation

async function executeCommand(cmd: string): Promise<void> {
  try {
    await runCommand(cmd);
  } catch (error) {
    if (error instanceof PluginError) {
      // Plugin-specific error handling
      console.error(`Plugin error in ${error.plugin}: ${error.message}`);
    } else {
      // Generic error handling
      console.error("Unexpected error:", error);
    }
    throw error;
  }
}

Performance Optimization

Lazy Loading

// Load commands only when needed
export default function register(program: Command): void {
  program
    .command("image")
    .description("Image processing")
    .action(async () => {
      // Lazy load command implementation
      const { imageCommand } = await import("./commands/image.js");
      await imageCommand();
    });
}

Resource Pooling

import { Pool } from "generic-pool";

// Create resource pool
const processorPool = Pool({
  create: async () => await createProcessor(),
  destroy: async (processor) => await processor.cleanup(),
  max: 4,
  min: 1,
});

// Use pooled resources
async function processImage(input: string): Promise<void> {
  const processor = await processorPool.acquire();
  try {
    await processor.process(input);
  } finally {
    await processorPool.release(processor);
  }
}

Testing Integration

Plugin Testing

import { describe, it, expect, beforeAll } from "vitest";
import { loadPlugin } from "@mediaproc/core";

describe("Plugin Integration", () => {
  let plugin: Plugin;

  beforeAll(async () => {
    plugin = await loadPlugin("image");
    await plugin.init?.();
  });

  it("should register commands", () => {
    const program = createProgram();
    plugin.register(program);
    expect(program.commands).toHaveLength(1);
  });

  it("should execute commands", async () => {
    await execCommand("image", "resize", {
      input: "test.jpg",
      width: 800,
    });
    // Assert results
  });
});

Mock Dependencies

import { vi } from "vitest";

vi.mock("sharp", () => ({
  default: vi.fn(() => ({
    resize: vi.fn().mockReturnThis(),
    toFile: vi.fn().mockResolvedValue(undefined),
  })),
}));

Distribution

Package Structure

{
  "name": "@mediaproc/image",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "bin": {
    "mediaproc-image": "bin/cli.js"
  },
  "exports": {
    ".": "./dist/index.js",
    "./commands": "./dist/commands/index.js",
    "./utils": "./dist/utils/index.js"
  },
  "files": ["dist", "bin", "README.md", "LICENSE"]
}

Publishing

# Build plugin
pnpm build

# Test locally
pnpm link --global

# Publish to npm
pnpm publish --access public

Versioning Strategy

Semantic Versioning

MAJOR.MINOR.PATCH 1.0.0 → 1.0.1 # Bug fix 1.0.0 → 1.1.0 # New feature 1.0.0 → 2.0.0 # Breaking change

Compatibility

// Check version compatibility
import semver from "semver";

function checkCompatibility(
  coreVersion: string,
  pluginVersion: string,
): boolean {
  return semver.satisfies(coreVersion, pluginVersion);
}

Best Practices

1. Minimal Dependencies

Keep dependencies minimal:

{
  "dependencies": {
    "sharp": "^0.32.0", // Essential
    "commander": "^11.0.0" // Essential
  },
  "devDependencies": {
    "typescript": "^5.0.0", // Development only
    "@types/node": "^20.0.0" // Development only
  }
}

2. Error Recovery

Implement graceful degradation:

async function processWithFallback(input: string): Promise<void> {
  try {
    await processWithNativeLib(input);
  } catch (error) {
    console.warn("Native processing failed, using fallback");
    await processWithJavaScript(input);
  }
}

3. Resource Cleanup

Always clean up:

async function process(input: string): Promise<void> {
  const tempFile = await createTempFile();
  try {
    await processFile(tempFile);
  } finally {
    await fs.unlink(tempFile).catch(() => {});
  }
}

Resources

Found an issue? Help us improve this page.

Edit on GitHub →