Docs/Plugins & Templates

Creating plugins

3 min read

Scaffolding#

bash
bun veap make:plugin my-plugin

This 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:

bash
bun veap register          # rescan and regenerate

veap 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.json so 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#

ts
// 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 MorphMap aliases in init() if your models participate in polymorphic relations (see the blog plugin for a full example).
  • Create permissions in onEnable and clean them up in onDisable; 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: true if the plugin needs a setup dialog; the panel plugin renders setup dialogs for plugins that declare extensions on the plugin-setup-dialogs point.

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).

veap
Preparing0%