Scaffolding#
bun veap make:plugin my-pluginThis creates plugins/my-plugin-plugin from stubs, installs dependencies (unless --skip-install) and regenerates the plugin registry. The generated package.json contains the veap metadata block described in Plugins; adjust id, name and description there.
Registering#
The registry file lib/plugins.gen.ts is generated by scanning package.json dependencies (any dependency whose name ends with -plugin, or whose package.json has veap.type === "plugin", including packages inside plugins/ and modules/ folders). Keep it current:
bun veap register # rescan and regenerateveap add <npm-package> installs a plugin package and runs the same regeneration; veap eject <package> copies an installed plugin (or template) into your local workspace folder so you can modify it.
Declaring dependencies on other plugins#
Two levels:
- Package level: list the other plugin as a dependency in
package.jsonso its code is importable. - Manifest level:
veap.dependencies: ["@acme/other-plugin"]or["other-plugin"](the@scope/prefix is stripped when resolving). The registry topologically sorts by these: dependencies initialize first, enabling a plugin auto-enables its dependencies, disabling cascades to dependents.
Missing dependencies log a warning and are skipped; cycles throw.
A complete small plugin#
// plugins/announce-plugin/src/index.ts
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import {
createManifestFromPackageJson,
type IPlugin,
} from "@veap/core/plugins";
import { discoverRoutes } from "@veap/core/router";
import { eventBus } from "@veap/core/core";
import pkg from "../package.json with { type: 'json' }";
import { announceMigrations } from "./migrations";
import { navigation } from "./navigation";
import { AnnouncementBanner } from "./ui/banner";
const appDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "app");
const announcePlugin: IPlugin = {
manifest: createManifestFromPackageJson(pkg),
locales: {
en: () => import("./locales/en"),
pl: () => import("./locales/pl"),
},
migrations: announceMigrations,
widgets: [
{
id: "announcement-stats",
name: "Announcement stats",
area: "dashboard-stats",
component: AnnouncementBanner,
priority: 50,
},
],
navigation: {
admin: {
Content: {
title: "Content",
priority: 20,
items: [
{ title: "Announcements", url: "/announcements", icon: "megaphone" },
],
},
},
},
init: async () => {
eventBus.subscribe(
"model:created:announcements",
"announce-plugin",
async () => {
// example: react to new announcements
},
);
},
routeTree: async () =>
discoverRoutes(appDir, (relPath) => import(`./app/${relPath}`)),
};
export default announcePlugin;Conventions worth keeping#
- Models, actions and UI stay inside the plugin. Import them from the plugin's package entry when another plugin needs them; that export surface is the plugin's contract.
- Register
MorphMapaliases ininit()if your models participate in polymorphic relations (see the blog plugin for a full example). - Create permissions in
onEnableand clean them up inonDisable; the RBAC tables are the shared vocabulary across plugins. - Every user-visible string goes through the locales system, even if you ship only English; the intl loader merges plugin dictionaries automatically.
- Set
hasSetup: trueif the plugin needs a setup dialog; the panel plugin renders setup dialogs for plugins that declare extensions on theplugin-setup-dialogspoint.
System plugins#
veap.system: true marks a plugin as part of the platform: it is treated as enabled and installed on first sync, initializes before other plugins, and the toggle semantics protect it from casual deactivation.
Generating templates#
bun veap make:template <name> scaffolds a template package under templates/ (see Templates for the ITemplate contract).