Sharing Code Between Plugins
Overview
UrsaMU does not have a formal runtime dependency resolver — there is no
dependencies array on IPlugin and no app.plugins.get() API. Instead,
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 imports it.
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’sursamu.plugin.jsondescription and
README. - Prefer importing types and DBO collections rather than importing
commands.tsorindex.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.
ursamu.plugin.json Dependencies
While there is no runtime dependency resolver, you can document required
plugins in ursamu.plugin.json for human readers and future tooling:
{
"name": "jobs-reporter",
"version": "1.0.0",
"description": "Generates reports from the jobs plugin",
"ursamu": ">=1.0.0",
"author": "Your Name",
"license": "MIT",
"requires": ["jobs"]
}
The requires field is informational only — ursamu plugin install displays
it so operators know what to install first. It does not affect load order.