A typesafe plugin system for React applications written in TypeScript. React PKL allows you to extend React applications from external sources through a robust, type-safe plugin architecture with advanced theming support.
React PKL is designed for SDK developers who want to create extensible React applications. It provides the foundation for building plugin systems where:
- Plugins can extend the UI through defined slots and layout overrides
- Plugins receive a typesafe context from the host application
- Theme plugins can override entire layout components with custom styling
- Static plugins work without lifecycle management (perfect for themes)
- Resources are automatically cleaned up when plugins are disabled
- Plugins can be managed locally or fetched from a remote source
- Style context provides type-safe access to theme variables
The main plugin management system.
npm install @react-pkl/coreFeatures:
PluginManager- Standalone mode with full plugin lifecycle controlPluginClient- Client mode for remote plugin manifestsResourceTracker- Automatic resource cleanup- React integration with hooks and components
Build tools for creating and bundling plugins.
npm install @react-pkl/sdk --save-devFeatures:
buildPlugin()- Bundle plugins with esbuild- Metadata generation
- Multiple output formats (ESM, CJS)
First, define your application context and create an SDK for your plugin developers:
// my-sdk/src/app-context.ts
export interface AppContext {
notifications: {
show(message: string, type?: 'info' | 'success' | 'error'): void;
};
router: {
navigate(path: string): void;
};
user: { id: string; name: string } | null;
}
// my-sdk/src/index.ts
import { PluginManager } from '@react-pkl/core';
import type { AppContext } from './app-context.js';
export function createAppManager(context: AppContext) {
return new PluginManager<AppContext>(context);
}
export { AppContext };// app/src/App.tsx
import { PluginProvider, PluginSlot } from '@react-pkl/core/react';
import { createAppManager } from 'my-sdk';
function App() {
const manager = createAppManager({
notifications: {
show: (msg, type) => console.log(`[${type}] ${msg}`),
},
router: {
navigate: (path) => window.location.href = path,
},
user: { id: '1', name: 'John Doe' },
});
// Load plugins
useEffect(() => {
manager.add(() => import('./plugins/hello-plugin.js'), { enabled: true });
}, []);
return (
<PluginProvider registry={manager.registry}>
<header>
<h1>My App</h1>
{/* Plugins can add components to this slot */}
<PluginSlot name="toolbar" />
</header>
<main>
<h2>Content</h2>
<PluginSlot name="content" fallback={<p>No plugins loaded</p>} />
</main>
</PluginProvider>
);
}// plugins/hello-plugin.tsx
import type { AppContext } from 'my-sdk';
export default {
meta: {
id: 'com.example.hello',
name: 'Hello Plugin',
version: '1.0.0',
description: 'A simple greeting plugin',
},
activate(context: AppContext) {
context.notifications.show('Hello Plugin activated!', 'success');
},
deactivate() {
console.log('Goodbye!');
},
components: {
toolbar: () => <button>Hello!</button>,
content: () => <div>Hello from plugin!</div>,
},
};The application manages plugins directly with full control:
import { PluginManager } from '@react-pkl/core';
const manager = new PluginManager(context);
// Add plugins
await manager.add(() => import('./my-plugin.js'), { enabled: true });
// Control lifecycle
await manager.enable('plugin-id');
await manager.disable('plugin-id');
await manager.remove('plugin-id');The application fetches plugins from a remote manifest:
import { PluginClient } from '@react-pkl/core';
const client = new PluginClient({
manifestUrl: 'https://api.example.com/plugins',
context: myAppContext,
});
// Sync plugins from server
await client.sync();The manifest should return an array of plugin descriptors:
[
{
"meta": {
"id": "com.example.plugin",
"name": "My Plugin",
"version": "1.0.0"
},
"url": "https://cdn.example.com/plugins/my-plugin/index.js"
}
]The ResourceTracker automatically cleans up plugin resources when they're disabled:
// In your plugin's activate function
export default {
activate(context: AppContext) {
// Register a route
context.router.registerRoute({
path: '/my-page',
component: MyPage,
});
// This will be automatically cleaned up when the plugin is disabled!
// No need to manually track it in deactivate()
},
};To support this in your SDK:
export interface AppContext {
router: {
registerRoute(route: Route): void;
};
_resources?: ResourceTracker;
_currentPluginId?: string;
}
// In your SDK implementation
function registerRoute(route: Route) {
routes.set(route.path, route);
// Register cleanup function
if (context._resources && context._currentPluginId) {
context._resources.register(context._currentPluginId, () => {
routes.delete(route.path);
});
}
}Get all registered plugins:
import { usePlugins } from '@react-pkl/core/react';
function PluginList() {
const plugins = usePlugins();
return (
<ul>
{plugins.map(entry => (
<li key={entry.module.meta.id}>
{entry.module.meta.name} - {entry.status}
</li>
))}
</ul>
);
}Get only enabled plugins:
const enabledPlugins = useEnabledPlugins();Get a specific plugin by ID:
const plugin = usePlugin('com.example.hello');Get metadata for all plugins:
const metaList = usePluginMeta();Get all components registered for a slot:
const toolbarComponents = useSlotComponents('toolbar');Theme plugins use onThemeEnable() and onThemeDisable() to manage theme lifecycle:
import { definePlugin, AppHeader, AppSidebar, StyleProvider } from 'my-sdk';
const darkThemePlugin = definePlugin({
meta: {
id: 'com.example.dark-theme',
name: 'Dark Theme',
version: '1.0.0',
},
// Theme plugins don't need activate/deactivate (static plugins)
// They only activate when set as the active theme
onThemeEnable(slots) {
// Apply CSS variables
document.documentElement.style.setProperty('--bg-primary', '#1a1a1a');
document.documentElement.style.setProperty('--text-primary', '#e4e4e7');
// Override layout slots with themed components
slots.set(AppHeader, DarkHeader);
slots.set(AppSidebar, DarkSidebar);
// Return cleanup function
return () => {
document.documentElement.style.removeProperty('--bg-primary');
document.documentElement.style.removeProperty('--text-primary');
};
},
onThemeDisable() {
console.log('Theme disabled - additional cleanup');
},
});
function DarkHeader({ toolbar }) {
return (
<StyleProvider variables={{
bgPrimary: '#1a1a1a',
textPrimary: '#e4e4e7',
accentColor: '#60a5fa',
}}>
<header style={{ background: 'linear-gradient(135deg, #18181b 0%, #27272a 100%)' }}>
{toolbar}
</header>
</StyleProvider>
);
}Provide type-safe style variables to components:
import { StyleProvider, useStyles } from 'my-sdk';
function MyComponent() {
const styles = useStyles();
return (
<div style={{
background: styles.bgPrimary,
color: styles.textPrimary,
borderColor: styles.borderColor,
}}>
Themed content
</div>
);
}import { isThemePlugin } from '@react-pkl/core';
// Check if a plugin is a theme plugin
if (isThemePlugin(plugin)) {
pluginHost.setThemePlugin(plugin);
}
// Get current theme
const currentTheme = pluginHost.getThemePlugin();
// Remove theme (back to default)
pluginHost.setThemePlugin(null);
// Persist theme in localStorage
localStorage.setItem('active-theme', plugin.meta.id);React PKL supports two plugin types:
import { isStaticPlugin, isThemePlugin } from '@react-pkl/core';
// Static plugins - no activate/deactivate lifecycle
// Perfect for theme plugins that only need theme lifecycle
const themePlugin = {
meta: { id: 'theme', name: 'Theme', version: '1.0.0' },
onThemeEnable(slots) { /* ... */ },
onThemeDisable() { /* ... */ },
};
isStaticPlugin(themePlugin); // true
isThemePlugin(themePlugin); // true
// Dynamic plugins - full lifecycle management
const dataPlugin = {
meta: { id: 'data', name: 'Data', version: '1.0.0' },
async activate(context) { /* ... */ },
async deactivate() { /* ... */ },
};
isStaticPlugin(dataPlugin); // falseWraps your application to provide plugin context:
import { PluginProvider } from '@react-pkl/core/react';
<PluginProvider registry={manager.registry}>
<App />
</PluginProvider>Renders plugin components in a specific slot:
import { PluginSlot } from '@react-pkl/core/react';
// Basic usage
<PluginSlot name="toolbar" />
// With fallback
<PluginSlot name="sidebar" fallback={<p>No plugins</p>} />
// With props passed to plugin components
<PluginSlot name="dashboard" componentProps={{ theme: 'dark' }} />Use the SDK package to bundle your plugins:
// build.ts
import { buildPlugin } from '@react-pkl/sdk';
await buildPlugin({
entry: './src/index.tsx',
outDir: './dist',
meta: {
id: 'com.example.plugin',
name: 'My Plugin',
version: '1.0.0',
},
formats: ['esm'],
minify: true,
sourcemap: true,
external: ['react', 'react-dom'],
});Optionally generate custom metadata:
await buildPlugin({
entry: './src/index.tsx',
outDir: './dist',
meta: { id: 'my-plugin', name: 'My Plugin', version: '1.0.0' },
generateMetadata: async (meta, outDir) => {
return {
...meta,
buildTime: new Date().toISOString(),
hash: await computeHash(outDir),
};
},
metadataFileName: 'plugin.json',
});interface PluginModule<TContext> {
// Required metadata
meta: {
id: string;
name: string;
version: string;
description?: string;
};
// Optional lifecycle hooks
activate?(context: TContext): void | Promise<void>;
deactivate?(): void | Promise<void>;
// Optional React entrypoint
entrypoint?(): ReactNode;
// Optional theme lifecycle hooks
onThemeEnable?(slots: Map<Function, Function>): void | (() => void);
onThemeDisable?(): void;
}Plugin Types:
- Dynamic Plugins: Have
activate/deactivate- Full lifecycle management - Static Plugins: No
activate/deactivate- Always available, perfect for themes - Theme Plugins: Have
onThemeEnable/onThemeDisable- Can be set as active theme
### TypeScript Plugin Helper
Create a helper for better type inference:
```typescript
// my-sdk/src/plugin.ts
import type { PluginModule } from '@react-pkl/core';
import type { AppContext } from './app-context.js';
export type AppPlugin = PluginModule<AppContext>;
export function definePlugin(plugin: AppPlugin): AppPlugin {
return plugin;
}
// Usage in plugins
export default definePlugin({
meta: { /* ... */ },
activate(context) {
// `context` is properly typed as AppContext!
context.notifications.show('Hello!');
},
});
Slots are named extension points where plugins can inject components. Define slots in your SDK:
// my-sdk/src/slots.ts
export const APP_SLOTS = {
TOOLBAR: 'toolbar',
SIDEBAR: 'sidebar',
CONTENT: 'content',
SETTINGS: 'settings',
} as const;
export type AppSlot = typeof APP_SLOTS[keyof typeof APP_SLOTS];Then use them in your app:
import { APP_SLOTS } from 'my-sdk';
<PluginSlot name={APP_SLOTS.TOOLBAR} />React PKL is fully type-safe. Define your context once and get type checking everywhere:
// SDK defines the context
export interface AppContext {
api: {
fetch<T>(path: string): Promise<T>;
};
}
// Plugins get full type checking
export default definePlugin({
async activate(context) {
// TypeScript knows about `context.api.fetch`
const data = await context.api.fetch<User[]>('/users');
// ^? User[]
},
});The repository includes complete examples:
examples/app- Host application with plugin integrationexamples/sdk- Custom SDK built on React PKLexamples/plugins- Sample plugins demonstrating various features:hello-plugin- Basic plugin with notificationuser-greeting-plugin- Accesses app contexttheme-toggle-plugin- State management with toolbar buttoncustom-page-plugin- Route registration with cleanupdark-theme-plugin- Complete theme with layout overrides and style context
- Indirect Dependency - Plugin developers use your SDK, not React PKL directly
- Type Safety First - Everything is typed through generics
- Automatic Cleanup - Resources are tracked and cleaned up automatically
- Flexibility - Works in both standalone and client-server architectures
- React Native - Provider/hook patterns for seamless integration
- Add comprehensive test suite
- Error boundary integration
- Plugin sandboxing/isolation
- Hot module replacement support
- Performance monitoring
- Bundle size optimization
- CLI for scaffolding plugins
- Plugin marketplace template
Contributions are welcome! This is a monorepo using npm workspaces.
# Install dependencies
npm install
# Build all packages
npm run build
# Run example app
cd examples/app
npm run dev[Insert your license here]
Created for building extensible React applications with type safety and proper resource management.