Overview
The plugin store provides type-safe shared state that persists across the plugin lifecycle. Each plugin can define its own store schema, and TypeScript enforces type safety throughout.Basic Store
Define a store with initial state:import { createPlugin } from '@bunli/core/plugin'
interface MyStore {
count: number
user?: string
}
const plugin = createPlugin<MyStore>({
name: 'my-plugin',
// Initial store state
store: {
count: 0
},
beforeCommand(context) {
// TypeScript knows store.count is a number!
context.store.count++
console.log(`Count: ${context.store.count}`)
}
})
Type Safety
The store is fully typed with generics:interface CounterStore {
count: number
lastCommand?: string
}
// Type parameter provides compile-time safety
const counterPlugin = createPlugin<CounterStore>({
name: 'counter',
store: {
count: 0
},
beforeCommand(context) {
// ✅ Valid - count is defined as number
context.store.count++
// ✅ Valid - lastCommand is optional string
context.store.lastCommand = context.command
// ❌ Type error - count must be a number
// context.store.count = 'invalid'
// ❌ Type error - unknown property
// context.store.unknown = 123
}
})
Store Access Methods
Access store values with type-safe methods:interface UserStore {
userId?: string
preferences: {
theme: 'light' | 'dark'
}
}
const plugin = createPlugin<UserStore>({
name: 'user',
store: {
preferences: { theme: 'light' }
},
beforeCommand(context) {
// Get value (type-safe)
const theme = context.getStoreValue('preferences')
console.log(theme.theme) // TypeScript knows the structure
// Set value (type-checked)
context.setStoreValue('userId', 'user-123')
// Check existence
if (context.hasStoreValue('userId')) {
console.log('User is authenticated')
}
}
})
Store Patterns
Counter Pattern
interface CounterStore {
count: number
}
const counterPlugin = createPlugin<CounterStore>({
name: 'counter',
store: {
count: 0
},
beforeCommand(context) {
context.store.count++
},
afterCommand(context) {
console.log(`Total commands: ${context.store.count}`)
}
})
Cache Pattern
interface CacheStore {
cache: Map<string, any>
}
const cachePlugin = createPlugin<CacheStore>({
name: 'cache',
store: {
cache: new Map()
},
beforeCommand(context) {
const key = `${context.command}:${JSON.stringify(context.args)}`
if (context.store.cache.has(key)) {
console.log('Cache hit!')
return context.store.cache.get(key)
}
},
afterCommand(context) {
const key = `${context.command}:${JSON.stringify(context.args)}`
context.store.cache.set(key, context.result)
}
})
Event Log Pattern
interface LogStore {
events: Array<{
type: string
timestamp: Date
data: any
}>
}
const logPlugin = createPlugin<LogStore>({
name: 'logger',
store: {
events: []
},
beforeCommand(context) {
context.store.events.push({
type: 'command_start',
timestamp: new Date(),
data: { command: context.command }
})
},
afterCommand(context) {
context.store.events.push({
type: 'command_end',
timestamp: new Date(),
data: {
command: context.command,
exitCode: context.exitCode
}
})
// Keep only last 100 events
if (context.store.events.length > 100) {
context.store.events = context.store.events.slice(-100)
}
}
})
Methods in Store
You can include methods in your store:interface MetricsStore {
metrics: {
events: Event[]
recordEvent: (name: string, data?: any) => void
getEvents: (name?: string) => Event[]
clearEvents: () => void
}
}
const metricsPlugin = createPlugin<MetricsStore>({
name: 'metrics',
store: {
metrics: {
events: [],
recordEvent(name, data = {}) {
this.events.push({
name,
timestamp: new Date(),
data
})
},
getEvents(name) {
return name
? this.events.filter(e => e.name === name)
: [...this.events]
},
clearEvents() {
this.events = []
}
}
},
beforeCommand(context) {
context.store.metrics.recordEvent('command_started', {
command: context.command
})
}
})
Store Persistence
The store persists across commands but resets on CLI restart:interface PersistentStore {
sessionId: string
commandCount: number
}
const sessionPlugin = createPlugin<PersistentStore>({
name: 'session',
store: {
sessionId: crypto.randomUUID(),
commandCount: 0
},
beforeCommand(context) {
console.log(`Session: ${context.store.sessionId}`)
console.log(`Command #${++context.store.commandCount}`)
// Same session ID across all commands in this CLI run
}
})
Store Merging
When multiple plugins define stores, they are merged:interface StoreA {
countA: number
}
interface StoreB {
countB: number
}
const pluginA = createPlugin<StoreA>({
name: 'plugin-a',
store: { countA: 0 }
})
const pluginB = createPlugin<StoreB>({
name: 'plugin-b',
store: { countB: 0 }
})
// Both stores are available
const cli = await createCLI({
name: 'my-cli',
plugins: [pluginA, pluginB]
})
// In commands, access merged store:
// context.store.countA
// context.store.countB
Shared Plugin Store
Plugins can share data via the plugin context store:const writerPlugin = createPlugin({
name: 'writer',
setup(context) {
// Write to shared store
context.store.set('shared-data', { value: 123 })
}
})
const readerPlugin = createPlugin({
name: 'reader',
setup(context) {
// Read from shared store
const data = context.store.get('shared-data')
console.log(data) // { value: 123 }
}
})
Real-World Example
Complete metrics plugin from Bunli examples:examples/dev-server/plugins/metrics.ts
import { createPlugin } from '@bunli/core/plugin'
interface MetricsStore {
metrics: {
events: Array<{
name: string
timestamp: Date
data: Record<string, any>
}>
recordEvent: (name: string, data?: Record<string, any>) => void
getEvents: (name?: string) => Array<any>
clearEvents: () => void
}
}
export const metricsPlugin = createPlugin<MetricsStore>({
name: 'metrics',
store: {
metrics: {
events: [],
recordEvent(name: string, data: Record<string, any> = {}) {
this.events.push({
name,
timestamp: new Date(),
data
})
// Keep only last 100 events to prevent memory leaks
if (this.events.length > 100) {
this.events = this.events.slice(-100)
}
},
getEvents(name?: string) {
if (name) {
return this.events.filter(event => event.name === name)
}
return [...this.events]
},
clearEvents() {
this.events = []
}
}
},
beforeCommand({ store, command }) {
// Record command start
store.metrics.recordEvent('command_started', {
command: command,
timestamp: new Date().toISOString()
})
},
afterCommand({ store, command }) {
// Record command completion
store.metrics.recordEvent('command_completed', {
command: command,
timestamp: new Date().toISOString()
})
}
})
Best Practices
Keep Stores Lightweight
// ✅ Good - lightweight state
interface GoodStore {
count: number
lastCommand: string
}
// ❌ Bad - heavy objects
interface BadStore {
cache: Map<string, LargeObject>
history: LargeArray[]
}
Use Methods for Logic
// ✅ Good - encapsulate logic in methods
interface GoodStore {
items: string[]
addItem: (item: string) => void
removeItem: (item: string) => void
}
// ❌ Bad - manipulate arrays directly
beforeCommand(context) {
context.store.items.push('new-item')
context.store.items = context.store.items.filter(i => i !== 'old')
}
Never Use Object.freeze()
// ❌ BAD - breaks Zod validation!
store: Object.freeze({
count: 0
})
// ✅ Good - plain objects
store: {
count: 0
}
Prevent Memory Leaks
interface SafeStore {
events: Event[]
}
const safePlugin = createPlugin<SafeStore>({
name: 'safe',
store: { events: [] },
afterCommand(context) {
context.store.events.push(/* ... */)
// Keep only last N items
if (context.store.events.length > 100) {
context.store.events = context.store.events.slice(-100)
}
}
})
Type Inference
Leverage TypeScript’s type inference:// Store type is inferred from generic
const plugin = createPlugin<{ count: number }>({
name: 'inferred',
store: { count: 0 },
beforeCommand(context) {
// TypeScript knows context.store.count is number
context.store.count++
}
})
// Extract store type
type PluginStore = InferPluginStore<typeof plugin>
// { count: number }
Testing Store Behavior
import { testPluginHooks } from '@bunli/core/plugin'
const results = await testPluginHooks(myPlugin, {
store: { count: 0 }
})
if (results.beforeCommand?.success) {
const ctx = results.beforeCommand.context
console.log('Store after beforeCommand:', ctx.store)
}
Next Steps
Creating Plugins
Build plugins with stores
Plugin Hooks
Learn about lifecycle hooks
Official Plugins
Study real store implementations
TypeScript
Master type-safe patterns