Skip to content

Latest commit

Β 

History

History
420 lines (327 loc) Β· 19.3 KB

File metadata and controls

420 lines (327 loc) Β· 19.3 KB

Plugin system

ReactPress 4.0 plugins extend server-side business logic (hooks, content rules, integrations) β€” WordPress-style extensibility without touching core. Themes = presentation; plugins = logic.

Built-in: SEO Β· auto-summary Β· batch WebP optimization Β· install from Admin or CLI

Modeled after WordPress: install/enable from Admin, configure via plugin.json, integrate with register() and hook subscriptions.

Core model

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Two sources (how packages land in runtime)                  β”‚
β”‚    local  plugins/{id}/     β†’  .reactpress/plugins/{id}/    β”‚
β”‚    npm    npm pack spec     β†’  .reactpress/plugins/{id}/    β”‚  (phase 2)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Three layers                                                β”‚
β”‚    plugins/               Registry β€” what can be installed   β”‚
β”‚    .reactpress/plugins/   Materialized β€” installed + dist    β”‚
β”‚    DB globalSetting       Active β€” enabled list + per-plugin β”‚
β”‚                            config                            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              ↓ activate
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Runtime  HookService                                        β”‚
β”‚    require(server.module) β†’ register(hooks, ctx)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Aligns with the three-layer directory model in themes/README.md to reduce cognitive load for theme/plugin authors.

vs themes and webhooks

Type Extends Process Examples
Theme Visitor UI, SSR Next.js (:3001) Skins, article templates
Plugin Business rules, hooks NestJS API (:3002) Auto summary, SEO
Webhook Cross-system async notify Server outbound HTTP Slack, CI triggers

Boundaries

  • Plugins must not inject theme routes or modify Next build config.
  • Themes must not write to the database directly β€” only via toolkit β†’ API.
  • Hooks (in-process, inbound, can mutate fields) vs Webhooks (outbound HTTP) are separate concepts.

Hook + webhook flow on article publish:

article.service
  β”œβ”€ applyFilters('article.beforePublish')   ← plugin sync rewrite summary, etc.
  β”œβ”€ persist
  β”œβ”€ doAction('article.afterPublish')        ← plugin sync side effects
  └─ webhookService.dispatch('article.published')  ← external systems

Registry

plugins/package.json:

{
  "reactpress": {
    "local": ["hello-world"]
  }
}
Source Metadata Registry location
local plugins/{id}/plugin.json reactpress.local array
npm plugin.json inside package catalog anchor (phase 2, same rules as theme npm)

To add a local plugin: id matches directory name and plugin.json id; add to local array.

Manifest: plugin.json

Schema: plugin.manifest.schema.json. Parser: toolkit/src/plugin/extension/plugin.ts.

{
  "$schema": "../plugin.manifest.schema.json",
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "…",
  "requires": ">=3.5.0",
  "server": {
    "module": "./dist/index.js",
    "hooks": {
      "subscribe": ["article.beforePublish"]
    }
  },
  "settings": {
    "schema": {
      "type": "object",
      "properties": {
        "enabled": { "type": "boolean", "default": true }
      }
    }
  },
  "permissions": ["article:read"],
  "capabilities": { "headless": true }
}
Field Required Description
id βœ“ kebab-case, matches directory name
name / version βœ“ Display name and semver
requires Minimum ReactPress version
requiresPlugins Other plugin ids this depends on
server.module βœ“* Server entry (compiled JS)
server.hooks.subscribe Declared hook names
admin.slots.subscribe Built-in Admin slot ids (enum)
admin.menu Standalone plugin settings page menu
settings.schema JSON Schema for config form
src/locales/{locale}.json Admin strings
permissions Admin capability declarations
capabilities Security capabilities (see below)

* At least server.module, or one of admin.slots / admin.menu (Admin code convention: src/admin/index.ts).

WordPress plugin header ReactPress
Plugin Name name
Version version
Text Domain id
Requires at least requires
Requires Plugins requiresPlugins

Plugin package structure

Every plugin package uses the same layout: manifest and i18n at the root, all TypeScript under src/, split by runtime:

plugins/my-plugin/
β”œβ”€β”€ plugin.json          # manifest (id, hooks, settings schema, admin slots)
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json        # extends ../tsconfig.base.json (server compile only)
β”œβ”€β”€ README.md
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ server/          # Node Hook logic β†’ tsc β†’ dist/
β”‚   β”‚   β”œβ”€β”€ index.ts     # export function register(hooks, ctx)
β”‚   β”‚   └── …
β”‚   β”œβ”€β”€ admin/           # optional React Admin UI (bundled by web via Vite)
β”‚   β”‚   β”œβ”€β”€ index.ts     # export function registerAdmin(registry, ctx)
β”‚   β”‚   └── …
β”‚   └── locales/         # Admin UI strings (en.json, zh.json, …)
└── dist/                # plugin.json β†’ server.module (./dist/index.js)
    └── index.js
Path Runtime Build
src/server/ NestJS require() at API startup pnpm run build β†’ tsc β†’ dist/
src/admin/ Admin SPA (Vite import.meta.glob) Compiled when building web/
src/locales/ Admin i18n API No build β€” served as JSON
plugin.json Install / enable / config No build

Shared server compiler defaults live in tsconfig.base.json. Admin TSX is excluded from plugin tsc and compiled by the Admin Vite app instead.

Admin i18n lives in src/locales/{locale}.json; API GET /extension/plugins/:id/locales/:locale reads from the installed plugin tree (themes keep locales/ at package root).

Dependency rules

plugins/{id}/src/server  β†’  @fecommunity/reactpress-toolkit/plugin/server only
plugins/{id}/src/admin   β†’  toolkit/plugin/admin + toolkit/plugin/react
server core              β†’  HookService only, no direct plugin imports
web / themes             β†’  load plugin admin via convention src/admin/index.ts

Admin UI slots

Symmetric with server.hooks.subscribe: manifest declares slot enum, code convention defines entry path.

Layer Declaration Notes
plugin.json admin.slots.subscribe: ["article.editor.meta.afterSummary"] Built-in slot id enum drives Admin load
Plugin package Fixed src/admin/index.ts β†’ registerAdmin() No path in manifest
Core pages <AdminSlot slot={AdminSlotIds.…} /> Host mount point

Manifest example:

"admin": {
  "slots": {
    "subscribe": ["article.editor.meta.afterSummary"]
  },
  "menu": {
    "title": "SEO Enhancement",
    "path": "/plugins/seo/settings",
    "permission": "extension:manage"
  }
}

Built-in slots (AdminSlotIds / JSON Schema enum):

Slot id Location
article.editor.meta.afterSummary Article editor Β· below summary
article.editor.sidebar.afterPublish Article editor Β· below publish box

Core page mount:

import { AdminSlot, AdminSlotIds } from '@fecommunity/reactpress-toolkit/plugin/react';

<AdminSlot slot={AdminSlotIds.ARTICLE_EDITOR_META_AFTER_SUMMARY} context={slotContext} />;

Plugin admin entry (plugins/{id}/src/admin/index.ts):

import {
  AdminSlotIds,
  type PluginAdminModule,
  type PluginAdminRegistry,
  type PluginAdminContext,
} from '@fecommunity/reactpress-toolkit/plugin/admin';

export function registerAdmin(registry: PluginAdminRegistry, ctx: PluginAdminContext): void {
  registry.registerSlot(AdminSlotIds.ARTICLE_EDITOR_META_AFTER_SUMMARY, MyPanel, {
    priority: 10,
  });
}

export default { registerAdmin } satisfies PluginAdminModule;

Slot component props: { context, pluginId, config }. context is defined by the host page (e.g. ArticleEditorAdminSlotContext with draft / patch / translate).

When enabled and manifest includes admin.slots or admin.menu, PluginAdminProvider loads plugins/{id}/src/admin/index by convention; enable/disable triggers re-bootstrap.

Admin dependency rules

plugins/{id}/src/admin β†’  @fecommunity/reactpress-toolkit/plugin/admin + plugin/react + react/antd

Server entry contract:

import type { HookService, PluginContext } from '@fecommunity/reactpress-toolkit/plugin/server';

export function register(hooks: HookService, ctx: PluginContext): void {
  hooks.addFilter(
    'article.beforePublish',
    async (article) => {
      return article;
    },
    { priority: 10, pluginId: ctx.id }
  );
}

// optional: cleanup timers on deactivate
export function deactivate(): void {}

Lifecycle

Action Effect
Install Materialize to .reactpress/plugins/; write installedPlugins[]
Enable Write activePlugins[]; Server hot-loads server.module
Disable Remove hooks; optionally call deactivate()
Uninstall Must disable first; delete runtime directory

Unlike themes: plugin enable/disable does not require API restart; theme enable requires Next restart (:3001).

Persistence (Setting.globalSetting.plugins):

{
  installedPlugins: string[];
  activePlugins: string[];              // ordered β€” affects load order
  entries: Record<string, {
    version: string;
    source: 'local' | 'npm';
    config?: Record<string, unknown>;
    activatedAt?: string;
    loadError?: string;
  }>;
}

Hook system

Hook Type Timing Payload
article.beforeCreate filter Before create Partial<Article>
article.beforePublish filter Before publish Mutate fields (e.g. summary)
article.afterPublish action After publish { article, isNew }
comment.beforeCreate filter Before comment Can __hookReject
comment.afterCreate action After comment { comment, createByAdmin }

Reject a filter (toolkit createHookRejectValue / getHookReject):

import { createHookRejectValue } from '@fecommunity/reactpress-toolkit/plugin/server';

return createHookRejectValue(comment, { message: 'Comment rejected', statusCode: 400 });

Same hook name sorted by priority (default 10); failed plugins write loadError without affecting others.

Build a new plugin

Standalone repo (recommended for third-party plugins): use the reactpress-plugin-starter template β€” hooks, Admin slots, settings page, and build scripts out of the box. Develop outside the monorepo, then install via npm (phase 2) or copy into plugins/{id}/.

In this monorepo:

  1. Copy hello-world/ β†’ plugins/my-plugin/
  2. Edit plugin.json, src/server/index.ts
  3. Add to plugins/package.json β†’ local
  4. Build and enable:
pnpm --filter @reactpress/plugin-my-plugin run build
# or
pnpm run build:plugins
pnpm dev   # compiles local plugins on demand before start
  1. Admin Plugins β†’ Install β†’ Enable

Install & API

Action HTTP
List GET /api/extension/plugins
State GET /api/extension/plugins/state
Install POST /api/extension/plugins/:id/install
Enable POST /api/extension/plugins/:id/activate
Disable POST /api/extension/plugins/:id/deactivate
Uninstall DELETE /api/extension/plugins/:id
Config PUT /api/extension/plugins/:id/config (auto reload after save)
Admin locales GET /api/extension/plugins/:id/locales/:locale

Plugins with settings.schema show a Settings link opening a WordPress-style config page (/plugins/{id}/settings).

reactpress plugin list
reactpress plugin install hello-world

Permissions & security

  • Install/enable/config requires admin JWT + extension:manage (web route layer).
  • Enabled plugins = trusted code: require() in Node process, same model as WordPress; only admins can install/enable.
  • server.module path constraint: relative paths inside plugin dir only (no .., no absolute paths); extensions limited to .js/.cjs/.mjs.
  • Config writes: server validates against manifest settings.schema (Ajv); rejects dangerous keys like __proto__.
  • Materialize copy: production skips symlinks when copying plugin files.
  • Plugin id: global kebab-case validation; invalid ids in globalSetting.plugins are ignored.
capability Meaning Runtime enforcement
headless Server only, no Admin UI Documented convention
network Outbound HTTP Phase 2
filesystem Read/write outside project Phase 2
database Register entities Phase 2

Manifest permissions[] declares Admin UI capabilities; server API boundary is currently admin role.

Typical use cases

Scenario Mount
Mutate fields before publish article.beforePublish filter
Comment filtering comment.beforeCreate filter
Post-publish analytics article.afterPublish action
Server-only, no UI server.module + headless: true

Not suitable for plugins: visitor page components (use themes), large DB schema changes (core migration PR).

Build commands

pnpm run build:plugins
pnpm --filter @reactpress/plugin-hello-world run build
pnpm --filter @reactpress/plugin-hello-world run typecheck

Full pnpm build includes plugins after toolkit, before server.

Built-in plugins

id Name Description
hello-world Auto Summary Generate summary from body/title when empty on publish
seo SEO Enhancement Slug, keywords, meta description with auto-fill
image-optimizer Image Optimization Analyze legacy assets, batch WebP variants

Related code

Module Role
server/src/modules/extension/plugin.service.ts Install, enable, config
server/src/modules/extension/plugin-loader.service.ts Dynamic load + register()
server/src/modules/hook/hook.service.ts Hook registry
toolkit/src/plugin/server/ Plugin SDK
toolkit/src/plugin/extension/plugin.ts Manifest parsing and state types
cli/out/lib/plugin-build.js Dev on-demand compile
web/src/modules/plugins/ Admin plugin list

Roadmap (not implemented)

  • npm catalog and reactpress plugin add
  • Dynamic plugin Admin admin.entry loading (custom React settings UI)
  • reactpress plugin create CLI (scaffold: reactpress-plugin-starter), plugin marketplace

References