Files
inkreach-teable-cli/skills/inkreach-teable-plugin/dynamic-plugins/overview.md
T
yeuimu e09ed1bc0f feat: 收录 inkreach-teable-plugin skill;install.sh 安装全部技能
skills/ 现包含 inkreach-teable + inkreach-teable-plugin,
npx skills add 一并安装两个;install.sh 改为遍历安装 skills/*。
2026-09-23 06:23:46 +00:00

6.8 KiB

Plugin system architecture

The host Next.js app (:3002) injects a global namespace window.__TEABLE_HOST__ during startup. Plugins are plain JS IIFE bundles that read this namespace at runtime — no ES imports, no iframes, no penpal bridge.

Why this system exists

The previous iframe/ZIP plugin system isolated plugins in sandboxed iframes, limiting API access to a penpal postMessage bridge. Adding or changing a plugin required rebuilding and re-deploying the host Next.js app. This system solves both problems:

  • Full host API access: Plugins run in the same React tree as the host, sharing the same React singleton. They can use any SDK hook, UI component, or OpenAPI helper directly.
  • No host rebuild: Upload a new IIFE bundle via upload-bundle and it's immediately available — the host loads it at runtime through PluginLoader.
  • Shared dependencies: React, ReactDOM, and all @teable/* packages are provided by the host, so plugin bundles are small (only contain the plugin's own code).

The trade-off is that plugins are fully trusted (no sandboxing) — upload is gated by a developer whitelist and bundles are validated for security.

Runtime flow

1. Host app mounts
   HostInitializer (<AppProvider>):
     window.__TEABLE_HOST__ = {
       React, ReactDOM,
       SDK:     { useFields, useRecords, useTableId, useViewId, ... },
       UILib:   { Button, Dialog, Select, ... },
       OpenAPI: { getRecords, updateRecords, ... },
       events:  EventBus instance,
       PluginSDK: { definePlugin, registerComponent, useSelection, getSelectionRecords },
       // Native embeddable components (same React tree as the host app):
       openFormModal,      // modal wrapper around the native form
       NativeGrid,         // full native grid with providers
       NativeForm,         // full native form (Tier-1 injection props supported)
       NativeFormScope,    // Tier-2: state scope for custom form layouts
       NativeFormField,    // Tier-2: single native form row
       useNativeForm,      // Tier-2: headless hook { values, setValue, verify, submit, ... }
     }

2. Plugin loads (PluginLoader component)
   → fetch /api/plugin/:id/runtime/plugin.json (manifest)
   → check minAppVersion compatibility
   → <link> inject CSS (if manifest.style exists)
   → <script src="/api/plugin/:id/runtime/main.js"> (IIFE executes)
   → script calls definePlugin({ id, name, onLoad, ... })
     → window.__TEABLE_PLUGINS__[pluginId] = config
   → PluginLoader reads registration, creates PluginContext, calls onLoad(ctx)
   → ctx.addContextMenuAction(...) etc. register React components to componentRegistry
   → PluginSlot renders registered components at the right mount point

3. Plugin unloads
   → onUnload(ctx) called
   → ctx auto-disposes all registered resources (components, event listeners)
   → script/link elements removed
   → window.__TEABLE_PLUGINS__[pluginId] deleted

Key host modules

HostInitializer

Mounts inside <AppProvider> (after SDK init). Runs once — writes all host APIs to window.__TEABLE_HOST__. The object is immutable after initialization.

Location: apps/nextjs-app/src/features/app/components/plugin/host/host-initializer.tsx

PluginLoader

A React component that loads a single plugin. Handles the full lifecycle: manifest fetch → compatibility check → CSS injection → script load → definePlugin → onLoad → onUnload → cleanup.

Each installed plugin gets its own <PluginLoader> instance. The loader shows a "Loading plugin..." message during fetch/load, and an error message on failure.

Location: apps/nextjs-app/src/features/app/components/plugin/host/plugin-loader.tsx

PluginSlot

Queries the component registry for all plugins registered at a given position and renders their components.

<PluginSlot position="contextMenu" tableId={tableId} />
<PluginSlot position="panel" tableId={tableId} viewId={viewId} />
<PluginSlot position="dashboard" baseId={baseId} />

Location: apps/nextjs-app/src/features/app/components/plugin/host/plugin-slot.tsx

ComponentRegistry

An in-memory registry mapping (position, pluginId, componentId) → React.FC. Plugins register components during onLoad and the registry auto-cleans up on plugin unload.

Location: apps/nextjs-app/src/features/app/components/plugin/host/component-registry.ts

EventBus

A simple pub/sub event bus shared between the host and all plugins. Plugins subscribe via ctx.on(...) and emit via ctx.emit(...).

Events include: record.created, record.updated, record.deleted, view.changed, selection.changed, plugin.activated.

Location: apps/nextjs-app/src/features/app/components/plugin/host/event-bus.ts

Plugin bundle structure

dist/
├── main.js       # IIFE bundle (webpack output)
└── styles.css    # optional stylesheet
plugin.json       # manifest

Shared dependency bridging

Plugin webpack config marks these as externals, mapping to host globals:

externals: {
  'react':              'window.__TEABLE_HOST__.React',
  'react-dom':          'window.__TEABLE_HOST__.ReactDOM',
  '@teable/sdk':        'window.__TEABLE_HOST__.SDK',
  '@teable/ui-lib':     'window.__TEABLE_HOST__.UILib',
  '@teable/openapi':     'window.__TEABLE_HOST__.OpenAPI',
  '@teable/plugin-sdk': 'window.__TEABLE_HOST__.PluginSDK',
}

This means the plugin does NOT bundle React or host packages — it uses the host's singleton instances. The bundle only contains the plugin's own code.

Two ways to access host APIs

const host = window.__TEABLE_HOST__;
const React = host.React;
const { useFields, useTableId } = host.SDK;
const { Button } = host.UILib;
const { getRecords } = host.OpenAPI;
const { definePlugin, useSelection } = host.PluginSDK;

This is what plugins/batch-edit-pro/ uses — the plugin bundle has no @teable/* imports, everything resolves through the host global.

2. Through @teable/plugin-sdk (also works because webpack externals)

import { definePlugin } from '@teable/plugin-sdk';

At build time, webpack resolves this to window.__TEABLE_HOST__.PluginSDK.definePlugin. Both styles work — the externals config handles the mapping.

Positions and registration

Position Mount point Registration method
ContextMenu Right-click → "Plugins" group in context menu ctx.addContextMenuAction(id, component)
Panel Right-side drawer of a table view ctx.addPanel(id, component)
View View-type dropdown (alongside grid/form/gallery) ctx.addView(id, component)
Dashboard Grid card on space dashboard ctx.addDashboardWidget(id, component)
Route Full-page route (e.g., /plugin/:pluginId/my-page) ctx.addRoute(path, component)