Sharing Code Between Plugins

Overview

UrsaMU resolves plugin load order through the dependencies array in
the plugin definition (see Dependencies
below), but there is no app.plugins.get() API for cross-plugin code
access at runtime. Plugins share code the same way any TypeScript modules
do: direct imports.

This keeps things simple and type-safe. If plugin B needs something from
plugin A, it declares plugin A in dependencies so the loader orders them
correctly, then imports from it directly.

Sharing Utilities

The cleanest pattern is a shared utility module that both plugins import.
Put it in a neutral location like src/plugins/shared/ or a dedicated
src/lib/ folder:

// src/plugins/shared/format.ts
export function formatTimestamp(ms: number): string {
  return new Date(ms).toLocaleString("en-US", {
    month: "short", day: "numeric", year: "numeric",
    hour: "numeric", minute: "2-digit",
  });
}

export function slugify(name: string): string {
  return name.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]/g, "");
}

Any plugin can import from it:

import { formatTimestamp } from "../shared/format.ts";

Importing from Another Plugin

If plugin B genuinely depends on plugin A’s types or database collections,
import directly from plugin A:

// src/plugins/event-rsvp-exporter/commands.ts
import { gameEvents, eventRsvps } from "../events/db.ts";
import type { IGameEvent } from "../../@types/IGameEvent.ts";

// Now this plugin can read event and RSVP data
const ev = await gameEvents.queryOne({ number: 1 });

Things to keep in mind:

  • If plugin A is not installed, the import will fail at startup. Document this
    requirement clearly in your plugin’s ursamu.plugin.json description and
    README.
  • Prefer importing types and DBO collections rather than importing
    commands.ts or index.ts, since those have side effects (command
    registration, plugin init).

Shared Database Collections

If two plugins need to read or write the same data, the cleanest approach is
to extract the DBO<T> definition into a shared module that both import:

// src/plugins/shared/jobsDb.ts
import { DBO } from "../../services/Database/database.ts";

export interface IJob {
  id: string;
  title: string;
  status: "open" | "in-progress" | "closed";
  createdAt: number;
}

export const jobs = new DBO<IJob>("server.jobs");

Both the jobs plugin and a hypothetical jobs-reporter plugin import from
shared/jobsDb.ts. The KV store is shared automatically since both collections
use the same key ("server.jobs").

Coordination via Events

When plugins need loose coupling — where plugin B reacts to what plugin A
does without importing from it directly — use EventsService:

// Plugin A emits an event
import { EventsService } from "../../services/Events/index.ts";

await EventsService.getInstance().emit(
  "jobs.status-changed",
  { jobId: job.id, newStatus: job.status },
);
// Plugin B subscribes without knowing about Plugin A's internals
import { EventsService } from "../../services/Events/index.ts";

const svc = EventsService.getInstance();
await svc.subscribe(
  "jobs.status-changed",
  `u.send("Job " + event.jobId + " is now " + event.newStatus);`,
  "world",
);

This pattern is preferred when the dependency is optional or when you want
Plugin B to work even if Plugin A is not installed.

Dependencies

Declare transitive plugin dependencies in the dependencies array of your
plugin’s IPlugin. The loader (loadPlugins) topologically sorts plugins
and calls init() in dependency order:

import { registerPlugin } from "@ursamu/mush";

registerPlugin({
  name: "jobs-reporter",
  version: "1.0.0",
  description: "Generates reports from the jobs plugin",
  dependencies: [
    { name: "jobs", version: ">=1.0.0" },
  ],
  async init() { /* ... */ },
  async remove() { /* ... */ },
});

Each entry needs name and a version semver range (e.g. ">=1.0.0").
At load time the loader checks that the declared dependency is installed and
that its version satisfies the range, aborting with an error if not.

See dependencies for full load-order semantics.