API Reference
Complete API documentation for React PKL v0.2.0.
Core Package (@pkl.js/react)
PluginHost
Central controller for plugin lifecycle, theme management, and layout slots (new in v0.2.0).
Constructor
constructor(context?: TContext)
Creates a new plugin host with an optional host context.
Parameters:
context- The application context passed to plugins during activation
Example:
const host = new PluginHost<AppContext>({
notifications: myNotificationService,
router: myRouter,
});
Theme Management Methods
setThemePlugin(plugin: PluginModule<TContext> | null): void
Set the active theme plugin. Only one theme can be active at a time.
Parameters:
plugin- Theme plugin module withonThemeEnablemethod, ornullto reset to default
Throws: Error if plugin doesn't have onThemeEnable method
Behavior:
- Disables current theme (calls cleanup function and
onThemeDisable) - Clears layout slot overrides
- Enables new theme (calls
onThemeEnablewith layout slots map) - Stores cleanup function
- Notifies React components to re-render
Example:
import darkTheme from './dark-theme-plugin';
// Enable dark theme
host.setThemePlugin(darkTheme);
// Reset to default theme
host.setThemePlugin(null);
getThemePlugin(): PluginModule<TContext> | null
Get the currently active theme plugin.
const currentTheme = host.getThemePlugin();
if (currentTheme) {
console.log(`Active theme: ${currentTheme.meta.name}`);
}
getLayoutSlotOverride(defaultComponent: Function): Function | null
Get the layout override for a slot component. Returns null if no override is set.
Parameters:
defaultComponent- The default layout component class/function
Returns: Override component or null
const override = host.getLayoutSlotOverride(AppHeader);
const HeaderComponent = override || AppHeader;
Plugin Lifecycle Methods
These methods delegate to the internal PluginManager:
setContext(context: TContext): void
Set or update the host context.
host.setContext(newContext);
add(loader: PluginLoader<TContext>, options?: { enabled?: boolean }): Promise<void>
Register a plugin and optionally enable it immediately.
Parameters:
loader- Plugin module or async function that returns a plugin moduleoptions.enabled- Iftrue, activates the plugin immediately (default:false)
Examples:
// From a local module
await host.add(() => import('./my-plugin.js'), { enabled: true });
// From a plugin object
await host.add({
meta: { id: 'my-plugin', name: 'My Plugin', version: '1.0.0' },
activate: (ctx) => { /* ... */ },
entrypoint: () => <MyComponent />,
});
// Lazy loading
await host.add(async () => {
const module = await fetch('/plugins/my-plugin.js');
return module.default;
});
enable(id: string): Promise<void>
Enable a plugin by ID. Calls the plugin's activate hook with the context.
Throws: Error if plugin not found
await host.enable('com.example.plugin');
disable(id: string): Promise<void>
Disable a plugin without removing it. Cleans up resources and calls deactivate hook.
Throws: Error if plugin not found
await host.disable('com.example.plugin');
remove(id: string): Promise<void>
Deactivate and completely remove a plugin from the registry.
await host.remove('com.example.plugin');
getAll(): ReadonlyArray<PluginEntry<TContext>>
Get all registered plugins regardless of status.
const allPlugins = host.getAll();
getEnabled(): ReadonlyArray<PluginEntry<TContext>>
Get only enabled plugins.
const enabledPlugins = host.getEnabled();
subscribe(listener: PluginEventListener): () => void
Subscribe to plugin lifecycle events. Returns an unsubscribe function.
Events:
{ type: 'added', pluginId: string }{ type: 'removed', pluginId: string }{ type: 'enabled', pluginId: string }{ type: 'disabled', pluginId: string }
const unsubscribe = host.subscribe((event) => {
console.log(`Plugin ${event.pluginId} was ${event.type}`);
});
// Later...
unsubscribe();
Internal Methods
setCurrentPlugin(plugin: PluginModule<TContext> | null): void
Internal - Set the currently executing plugin for resource tracking.
getCurrentPlugin(): PluginModule<TContext> | null
Internal - Get the currently executing plugin.
trackResource(cleanup: () => void): void
Internal - Register a cleanup function for the current plugin.
PluginModule Interface
The structure that all plugins must conform to.
interface PluginModule<TContext = unknown> {
// Required metadata
meta: PluginMeta;
// Standard plugin lifecycle hooks
activate?(context: TContext): void | Promise<void>;
deactivate?(): void | Promise<void>;
entrypoint?(): ReactNode;
// Theme plugin hooks (new in v0.2.0)
onThemeEnable?(slots: Map<Function, Function>): void | (() => void);
onThemeDisable?(): void;
}
Standard Lifecycle Hooks
has(id: string): boolean
Check if a plugin is registered.
get(id: string): PluginEntry<TContext> | undefined
Get a plugin entry by ID.
getAll(): ReadonlyArray<PluginEntry<TContext>>
Get all plugin entries.
getEnabled(): ReadonlyArray<PluginEntry<TContext>>
Get enabled plugin entries.
add(module: PluginModule<TContext>, status?: PluginStatus): void
Add a plugin module.
remove(id: string): void
Remove a plugin by ID.
setStatus(id: string, status: PluginStatus): void
Standard Lifecycle Hooks
activate(context: TContext): void | Promise<void>
Called when the plugin is enabled. Receives the host application context.
activate(context) {
// Register routes, event listeners, etc.
const cleanup = context.router.registerRoute({
path: '/my-page',
component: MyPage
});
// Track resources for automatic cleanup
context.trackResource(cleanup);
}
deactivate(): void | Promise<void>
Called when the plugin is disabled. Use for cleanup (though automatic resource tracking is preferred).
deactivate() {
console.log('Plugin is being disabled');
}
entrypoint(): ReactNode
React entry point for the plugin. Returns components that register with slots.
entrypoint() {
return (
<>
<ToolbarItem>My Button</ToolbarItem>
<SidebarItem>My Link</SidebarItem>
</>
);
}
Theme Lifecycle Hooks (New in v0.2.0)
onThemeEnable(slots: Map<Function, Function>): void | (() => void)
Called when this plugin is set as the active theme. Register layout component overrides in the slots map.
Parameters:
slots- Map where key is default component, value is override component
Returns: Optional cleanup function that will be called on theme disable
onThemeEnable(slots) {
// Register layout overrides
slots.set(AppHeader, DarkHeader);
slots.set(AppSidebar, DarkSidebar);
slots.set(AppDashboard, DarkDashboard);
// Inject global styles
const style = document.createElement('style');
style.textContent = ':root { --bg: #000; }';
document.head.appendChild(style);
// Return cleanup function
return () => {
document.head.removeChild(style);
};
}
onThemeDisable(): void
Called when this plugin is no longer the active theme. Use for additional cleanup (cleanup function from onThemeEnable is called automatically).
onThemeDisable() {
console.log('Theme is being disabled');
}
PluginMeta Interface
Plugin metadata.
interface PluginMeta {
id: string; // Unique identifier (e.g., 'com.example.plugin')
name: string; // Human-readable name
version: string; // Semver version
description?: string; // Optional description
}
Utility Functions
isStaticPlugin<TContext>(plugin: PluginModule<TContext>): boolean
Check if a plugin is "static" (doesn't have lifecycle methods). Static plugins are always available and cannot be enabled/disabled.
import { isStaticPlugin } from '@pkl.js/react';
if (isStaticPlugin(plugin)) {
console.log('This is a static plugin');
}
isThemePlugin<TContext>(plugin: PluginModule<TContext>): boolean
Check if a plugin is a theme plugin (has onThemeEnable method).
import { isThemePlugin } from '@pkl.js/react';
if (isThemePlugin(plugin)) {
host.setThemePlugin(plugin);
}
React Integration (@pkl.js/react/react)
Components
<PluginProvider>
Provides plugin context to the React tree (required as root component).
Props:
host: PluginHost<TContext>- The plugin host instancechildren: ReactNode- Child components
const host = new PluginHost(context);
<PluginProvider host={host}>
<App />
</PluginProvider>
<LayoutProvider>
Provides layout context for slot management (usually wrapped by your SDK).
Props:
children: ReactNode- Child components
<PluginProvider host={host}>
<LayoutProvider>
<App />
</LayoutProvider>
</PluginProvider>
<Slot>
Renders components registered for a named slot.
Props:
name: string- The slot namefallback?: ReactNode- Rendered when no components registered
<Slot name="toolbar" fallback={<p>No toolbar plugins</p>} />
<LayoutSlot>
Renders a layout component, checking for theme overrides.
Props:
default: ComponentType- The default layout component
<LayoutSlot default={AppHeader} />
Equivalent to:
const HeaderComponent = useAppLayoutSlot(AppHeader);
<HeaderComponent />
<PluginEntrypoints>
Renders all plugin entrypoints. Usually placed near the root of your app.
<PluginProvider host={host}>
<LayoutProvider>
<PluginEntrypoints />
<App />
</LayoutProvider>
</PluginProvider>
Hooks
All hooks must be used inside appropriate providers.
usePluginHost<TContext>(): PluginHost<TContext>
Access the PluginHost instance.
Requires: <PluginProvider>
function MyComponent() {
const host = usePluginHost();
const handleEnableDarkMode = () => {
host.setThemePlugin(darkTheme);
};
return <button onClick={handleEnableDarkMode}>Dark Mode</button>;
}
usePlugins<TContext>(): ReadonlyArray<PluginModule<TContext>>
Get all registered plugin modules.
Requires: <PluginProvider>
function PluginList() {
const plugins = usePlugins();
return (
<ul>
{plugins.map(plugin => (
<li key={plugin.meta.id}>{plugin.meta.name}</li>
))}
</ul>
);
}
useAppContext<TContext>(): TContext
Access the host application context (defined by your SDK).
Requires: Custom SDK provider (usually wraps <PluginProvider>)
function MyComponent() {
const { router, notifications } = useAppContext();
const navigate = () => {
router.navigate('/settings');
};
return <button onClick={navigate}>Settings</button>;
}
useAppLayout(): LayoutSlotContent
Access layout slot content (toolbar, sidebar, dashboard items, etc.).
Requires: <LayoutProvider>
function AppHeader() {
const { toolbar } = useAppLayout();
return (
<header>
<h1>My App</h1>
<div className="toolbar">{toolbar}</div>
</header>
);
}
useAppLayoutSlot(defaultComponent: ComponentType): ComponentType
Get the layout component for a slot, checking for theme overrides.
Requires: <LayoutProvider> and <PluginProvider>
Parameters:
defaultComponent- The default layout component
Returns: Override component if theme is active, otherwise default
function MyPage() {
const HeaderComponent = useAppLayoutSlot(AppHeader);
return (
<div>
<HeaderComponent />
<main>Content</main>
</div>
);
}
useSlotItems(name: string): ReactNode[]
Get all components registered for a named slot.
Requires: <LayoutProvider>
function CustomToolbar() {
const toolbarItems = useSlotItems('toolbar');
return (
<div className="toolbar">
{toolbarItems.map((item, i) => (
<div key={i}>{item}</div>
))}
</div>
);
}
Style Context
Type-safe theming with CSS variables (usually provided by your SDK).
<StyleProvider>
Provides style variables to child components.
Props:
variables: Partial<StyleVariables>- Style variables to providechildren: ReactNode- Child components
StyleVariables Interface:
interface StyleVariables {
bgPrimary: string;
textPrimary: string;
textSecondary: string;
textMuted: string;
accentColor: string;
linkColor: string;
borderColor: string;
cardBg: string;
cardBgSecondary: string;
toolbarBg: string;
sidebarBg: string;
borderAccent: string;
}
Usage:
<StyleProvider variables={{
bgPrimary: '#1a1a1a',
textPrimary: '#fff',
accentColor: '#60a5fa'
}}>
<MyComponent />
</StyleProvider>
Cascading: Nested providers override parent values.
<StyleProvider variables={{ bgPrimary: '#000', textPrimary: '#fff' }}>
<Header />
<StyleProvider variables={{ accentColor: '#60a5fa' }}>
<Toolbar /> {/* Inherits bgPrimary, textPrimary, overrides accent */}
</StyleProvider>
</StyleProvider>
useStyles(): StyleVariables
Access style variables from the nearest <StyleProvider>.
function MyComponent() {
const styles = useStyles();
return (
<div style={{
background: styles.bgPrimary,
color: styles.textPrimary,
border: `1px solid ${styles.borderColor}`
}}>
Content
</div>
);
}
getCSSVariable(name: string, fallback?: string): string
Read a CSS variable from the document root.
import { getCSSVariable } from 'my-sdk';
const bgColor = getCSSVariable('--color-bg', '#ffffff');
readStyleVariablesFromCSS(): Partial<StyleVariables>
Read all style variables from CSS custom properties.
import { readStyleVariablesFromCSS } from 'my-sdk';
const cssVars = readStyleVariablesFromCSS();
SDK Helpers
These utilities help create custom slot components (usually defined in your SDK).
createSlot<TProps = { children: ReactNode }>(name: string): ComponentType<TProps>
Create a slot component that plugins can register items to.
Type Parameter:
TProps- Props that the slot component accepts (defaults to{ children: ReactNode })
Parameters:
name: string- Unique identifier for the slot
Returns: React component type
Examples:
import { createSlot } from '@pkl.js/react/react';
// Simple slots (use default children prop)
export const ToolbarItem = createSlot('toolbar');
export const SidebarItem = createSlot('sidebar');
// Slot with custom props
export const SettingsSection = createSlot<{
title: string;
icon?: string;
children: ReactNode;
}>('settings');
Plugin usage:
entrypoint() {
return (
<>
<ToolbarItem>My Button</ToolbarItem>
<SettingsSection title="Advanced" icon="gear">
<MySettings />
</SettingsSection>
</>
);
}
createLayoutSlot<TProps>(defaultComponent: ComponentType<TProps>): ComponentType<TProps>
Create a layout slot that theme plugins can override.
import { createLayoutSlot } from '@pkl.js/react/react';
export const AppHeader = createLayoutSlot(() => {
const { toolbar } = useAppLayout();
return <header>{toolbar}</header>;
});
Theme plugin usage:
onThemeEnable(slots) {
function DarkHeader() {
const { toolbar } = useAppLayout();
return <header style={{ background: '#000' }}>{toolbar}</header>;
}
slots.set(AppHeader, DarkHeader);
}
Type Definitions
PluginEntry<TContext>
Internal plugin entry with status.
interface PluginEntry<TContext> {
module: PluginModule<TContext>;
status: 'enabled' | 'disabled';
}
PluginLoader<TContext>
Plugin loader - either a module or a function that returns one.
type PluginLoader<TContext> =
| PluginModule<TContext>
| (() => PluginModule<TContext> | Promise<PluginModule<TContext>>);
PluginEvent
Plugin lifecycle events.
type PluginEvent =
| { type: 'added'; pluginId: string }
| { type: 'removed'; pluginId: string }
| { type: 'enabled'; pluginId: string }
| { type: 'disabled'; pluginId: string };
PluginEventListener
Event listener function.
type PluginEventListener = (event: PluginEvent) => void;
SDK Package (@pkl.js/react-sdk)
Build tools for creating and bundling plugins (not changed in v0.2.0).
buildPlugin(options: BuildOptions): Promise<void>
Bundle a plugin using esbuild.
Options:
interface BuildOptions {
entryPoint: string; // Plugin entry file
outDir: string; // Output directory
external?: string[]; // External dependencies
minify?: boolean; // Minify output (default: true)
sourcemap?: boolean; // Generate sourcemaps (default: true)
target?: string; // ES target (default: 'es2020')
}
Example:
import { buildPlugin } from '@pkl.js/react-sdk';
await buildPlugin({
entryPoint: 'src/plugin.tsx',
outDir: 'dist',
external: ['react', 'react-dom', 'my-sdk'],
minify: true,
sourcemap: true
});
Migration from v0.1.0
Breaking Changes
-
PluginManager replaced by PluginHost
// Old (v0.1.0)
const manager = new PluginManager(context);
<PluginProvider registry={manager.registry}>
// New (v0.2.0)
const host = new PluginHost(context);
<PluginProvider host={host}> -
Layout components use hooks instead of props
// Old (v0.1.0)
function AppHeader({ toolbar }) {
return <header>{toolbar}</header>;
}
// New (v0.2.0)
function AppHeader() {
const { toolbar } = useAppLayout();
return <header>{toolbar}</header>;
} -
Theme plugins use new lifecycle hooks
// New (v0.2.0)
export const darkTheme = {
meta: { id: 'dark', name: 'Dark', version: '1.0.0' },
onThemeEnable(slots) {
slots.set(AppHeader, DarkHeader);
},
onThemeDisable() {
// Cleanup
}
}; -
Static plugins supported
// New (v0.2.0) - no activate/deactivate needed
export const staticPlugin = {
meta: { id: 'static', name: 'Static', version: '1.0.0' },
entrypoint: () => <MyComponent />
};
Deprecated APIs
PluginManager- UsePluginHostinsteadPluginClient- Removed (use custom loading logic withPluginHost)ResourceTracker(standalone) - Now integrated intoPluginHost<PluginSlot>withcomponentProps- Use context hooks instead
Best Practices
- Use TypeScript for full type safety
- Track resources with
context.trackResource()for automatic cleanup - Use hooks instead of prop drilling in layout components
- Define slots in SDK for consistent plugin extension points
- Use StyleProvider for theme variables instead of hardcoded colors
- Create theme plugins with
onThemeEnable/onThemeDisablefor consistent styling - Make static plugins when no lifecycle management is needed
- External SDK dependencies in plugin builds to avoid version conflicts
Examples
See the examples on GitHub for complete working examples:
- examples/app - Host application with plugin system
- examples/plugins - Various plugin types (standard, theme, static)
- examples/sdk - Custom SDK implementation
For more information, see:
const plugins = usePlugins<AppContext>();
Returns: ReadonlyArray<PluginEntry<TContext>>
useEnabledPlugins<TContext>()
Returns only enabled plugin entries.
const enabledPlugins = useEnabledPlugins<AppContext>();
Returns: ReadonlyArray<PluginEntry<TContext>>
usePlugin<TContext>(id: string)
Returns a specific plugin entry by ID.
const plugin = usePlugin<AppContext>('com.example.hello');
Returns: PluginEntry<TContext> | undefined
usePluginMeta()
Returns metadata for all plugins (lighter weight than usePlugins).
const metaList = usePluginMeta();
// [{ id: '...', name: '...', version: '...' }, ...]
Returns: ReadonlyArray<PluginMeta>
useSlotComponents(slot: string)
Returns all components registered for a slot.
const toolbarComponents = useSlotComponents('toolbar');
Returns: ReadonlyArray<ComponentType<unknown>>
SDK Package (@pkl.js/react-sdk)
buildPlugin(config: PluginBuildConfig): Promise<PluginBuildResult>
Bundle a plugin using esbuild.
Config:
interface PluginBuildConfig<TMeta = unknown> {
// Required
entry: string; // Entry point file path
outDir: string; // Output directory
meta: TMeta; // Plugin metadata
// Optional
formats?: Array<'esm' | 'cjs'>; // Output formats (default: ['esm'])
minify?: boolean; // Minify output (default: false)
sourcemap?: boolean; // Generate sourcemaps (default: true)
external?: string[]; // External dependencies (react/react-dom always external)
esbuildPlugins?: Plugin[]; // Custom esbuild plugins
generateMetadata?: MetadataGenerator<TMeta>; // Generate custom metadata
metadataFileName?: string; // Metadata file name (default: 'meta.json')
}
Result:
interface PluginBuildResult<TMeta = unknown> {
outDir: string; // Resolved output directory
outputFiles: string[]; // Array of generated file paths
metadata?: TMeta; // Generated metadata (if generateMetadata provided)
}
Example:
import { buildPlugin } from '@pkl.js/react-sdk';
const result = await buildPlugin({
entry: './src/index.tsx',
outDir: './dist',
meta: {
id: 'com.example.plugin',
name: 'My Plugin',
version: '1.0.0',
description: 'A sample plugin',
},
formats: ['esm'],
minify: true,
generateMetadata: async (meta, outDir) => {
const stats = await getDirectoryStats(outDir);
return {
...meta,
buildTime: new Date().toISOString(),
size: stats.totalSize,
};
},
});
console.log('Built:', result.outputFiles);
Type Definitions
PluginMeta
Static metadata about a plugin.
interface PluginMeta {
readonly id: string; // Unique identifier
readonly name: string; // Human-readable name
readonly version: string; // Semver version
readonly description?: string; // Optional description
}
PluginModule<TContext>
The runtime export of a plugin bundle.
interface PluginModule<TContext = unknown> {
readonly meta: PluginMeta;
activate?(context: TContext): void | Promise<void>;
deactivate?(): void | Promise<void>;
components?: Readonly<Record<string, ComponentType<any>>>;
}
PluginEntry<TContext>
Internal representation of a registered plugin.
interface PluginEntry<TContext = unknown> {
readonly module: PluginModule<TContext>;
status: PluginStatus; // 'enabled' | 'disabled'
}
PluginLoader<TContext>
Plugin module or factory function.
type PluginLoader<TContext = unknown> =
| PluginModule<TContext>
| (() => PluginModule<TContext> | Promise<PluginModule<TContext>>);
RemotePluginDescriptor
Server-side plugin manifest entry.
interface RemotePluginDescriptor {
readonly meta: PluginMeta;
readonly url: string; // URL to plugin bundle
}
PluginEvent
Plugin lifecycle event.
type PluginEvent =
| { type: 'added'; pluginId: string }
| { type: 'removed'; pluginId: string }
| { type: 'enabled'; pluginId: string }
| { type: 'disabled'; pluginId: string };
CleanupFunction
Function called to clean up plugin resources.
type CleanupFunction = () => void;