Overview
Bunli’s plugin system is fully typed with TypeScript. This page documents all plugin-related types, interfaces, and type utilities.
Core Types
BunliPlugin
The main plugin interface with optional store type parameter:
interface BunliPlugin<TStore = {}> {
/** Unique plugin name */
name: string
/** Optional plugin version */
version?: string
/** Plugin store schema/initial state */
store?: TStore
/** Setup hook - Called during CLI initialization */
setup?(context: PluginContext): void | Promise<void>
/** Config resolved hook - Called after all configuration is finalized */
configResolved?(config: ResolvedConfig): void | Promise<void>
/** Before command hook - Called before command execution */
beforeCommand?(context: CommandContext<any>): void | Promise<void>
/** After command hook - Called after command execution */
afterCommand?(context: CommandContext<any> & CommandResult): void | Promise<void>
}
Store type parameter for type-safe store access
Unique identifier for the plugin. Use namespaced names like @scope/plugin-name
Plugin version, typically following semver
Initial store state. Store is merged across all plugins.
setup
(context: PluginContext) => void | Promise<void>
Lifecycle hook called during CLI initialization. Can modify config and register commands.
configResolved
(config: ResolvedConfig) => void | Promise<void>
Lifecycle hook called after configuration is finalized. Config is immutable at this point.
beforeCommand
(context: CommandContext) => void | Promise<void>
Lifecycle hook called before command execution. Can inject context and validate.
afterCommand
(context: CommandContext & CommandResult) => void | Promise<void>
Lifecycle hook called after command execution. Receives command result or error.
PluginFactory
Type for plugin factory functions that accept configuration:
type PluginFactory<TOptions = unknown, TStore = {}> =
(options?: TOptions) => BunliPlugin<TStore>
Type of options the factory accepts
Type of the plugin’s store
Example:
interface MyPluginOptions {
enabled: boolean
threshold: number
}
interface MyPluginStore {
count: number
}
const myPlugin: PluginFactory<MyPluginOptions, MyPluginStore> = (options) => ({
name: 'my-plugin',
store: { count: 0 },
beforeCommand(context) {
if (options.enabled) {
context.store.count++
}
}
})
PluginConfig
Union type for different ways to configure plugins:
type PluginConfig =
| string // Path to plugin module
| BunliPlugin // Plugin object
| PluginFactory // Plugin factory function
| [PluginFactory, unknown] // Plugin factory with options tuple
Example:
export default {
plugins: [
// String path
'./plugins/my-plugin.ts',
// Plugin object
{ name: 'inline-plugin', setup: () => {} },
// Factory without options
myPlugin,
// Factory with options
myPlugin({ enabled: true, threshold: 10 }),
// Tuple syntax
[myPlugin, { enabled: true }]
]
}
Store Types
StoreOf
Extract the store type from a plugin:
type StoreOf<P> = P extends BunliPlugin<infer S> ? S : {}
Example:
const myPlugin = createPlugin<{ count: number }>({
name: 'my-plugin',
store: { count: 0 }
})
type MyStore = StoreOf<typeof myPlugin>
// Result: { count: number }
MergeStores
Merge multiple plugin stores into one type:
type MergeStores<Plugins extends readonly BunliPlugin[]> =
Plugins extends readonly []
? {}
: Plugins extends readonly [infer First, ...infer Rest]
? First extends BunliPlugin
? Rest extends readonly BunliPlugin[]
? StoreOf<First> & MergeStores<Rest>
: StoreOf<First>
: {}
: {}
Example:
const plugin1 = createPlugin<{ count: number }>({
name: 'plugin1',
store: { count: 0 }
})
const plugin2 = createPlugin<{ message: string }>({
name: 'plugin2',
store: { message: '' }
})
type Combined = MergeStores<[typeof plugin1, typeof plugin2]>
// Result: { count: number; message: string }
Context Types
PluginContext
Context available during the setup hook:
interface PluginContext {
/** Current configuration (being built) */
readonly config: BunliConfigInput
/** Update configuration */
updateConfig(partial: Partial<BunliConfigInput>): void
/** Register a new command */
registerCommand(command: CommandDefinition): void
/** Add global middleware */
use(middleware: Middleware): void
/** Shared storage between plugins */
readonly store: Map<string, unknown>
/** Plugin logger */
readonly logger: Logger
/** System paths */
readonly paths: PathInfo
}
See Plugin Context for detailed documentation.
CommandContext
Context available during command execution hooks:
interface CommandContext<TStore = {}> {
/** Command name being executed */
readonly command: string
/** The Command object being executed */
readonly commandDef: Command<any, TStore>
/** Positional arguments */
readonly args: string[]
/** Parsed flags/options */
readonly flags: Record<string, unknown>
/** Environment information */
readonly env: EnvironmentInfo
/** Type-safe context store */
readonly store: TStore
/** Type-safe store value access */
getStoreValue<K extends keyof TStore>(key: K): TStore[K]
getStoreValue(key: string | number | symbol): unknown
/** Type-safe store value update */
setStoreValue<K extends keyof TStore>(key: K, value: TStore[K]): void
setStoreValue(key: string | number | symbol, value: unknown): void
/** Check if a store property exists */
hasStoreValue<K extends keyof TStore>(key: K): boolean
hasStoreValue(key: string | number | symbol): boolean
}
See Plugin Context for detailed documentation.
CommandResult
Result information passed to afterCommand hook:
interface CommandResult {
/** Command return value */
result?: unknown
/** Error if command failed */
error?: unknown
/** Exit code */
exitCode?: number
}
The value returned by the command handler
Error object if the command threw an exception
Process exit code (0 for success, non-zero for failure)
Supporting Types
PathInfo
System path information:
interface PathInfo {
/** Current working directory */
cwd: string
/** User home directory */
home: string
/** Config directory path */
config: string
}
EnvironmentInfo
Environment information:
interface EnvironmentInfo {
/** Running in CI environment */
isCI: boolean
}
Detects CI environments including:
- GitHub Actions
- GitLab CI
- CircleCI
- Jenkins
- Generic CI (CI=true)
Middleware
Middleware function type:
type Middleware = (
context: CommandContext,
next: () => Promise<unknown>
) => Promise<unknown>
Example:
const timingMiddleware: Middleware = async (context, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
console.log(`Command took ${duration}ms`)
}
// In plugin setup:
setup(context) {
context.use(timingMiddleware)
}
CommandDefinition
Type alias for command objects:
type CommandDefinition = Command<any>
Module Augmentation
Plugins can extend global types using module augmentation:
declare module '@bunli/core' {
interface PluginStore {
// Add your custom store properties
myPlugin: {
customData: string
}
}
interface CommandContext {
// Add custom context methods
myCustomMethod(): void
}
}
Example:
// my-plugin.ts
import type {} from '@bunli/core'
declare module '@bunli/core' {
interface PluginStore {
auth: {
token: string | null
user: string | null
}
}
}
export const authPlugin = createPlugin<{ token: string }>(options => ({
name: 'auth-plugin',
store: {
auth: {
token: options.token,
user: null
}
},
beforeCommand(context) {
// TypeScript knows about auth property
if (!context.store.auth.token) {
throw new Error('Not authenticated')
}
}
}))
Type Utilities
InferPluginOptions
Infer options type from a plugin factory:
type InferPluginOptions<T> = T extends PluginFactory<infer O, any> ? O : never
// Example
const myPlugin = createPlugin((options: { enabled: boolean }) => ({
name: 'my-plugin'
}))
type Options = InferPluginOptions<typeof myPlugin>
// Result: { enabled: boolean }
InferPluginStore
Infer store type from a plugin or plugin factory:
type InferPluginStore<T> =
T extends BunliPlugin<infer S> ? S
: T extends PluginFactory<any, infer S> ? S
: {}
// Example
const myPlugin = createPlugin<{}, { count: number }>(options => ({
name: 'my-plugin',
store: { count: 0 }
}))
type Store = InferPluginStore<ReturnType<typeof myPlugin>>
// Result: { count: number }
Best Practices
- Always type your store: Use the
TStore generic parameter for type-safe store access
- Use satisfies: Add
satisfies BunliPlugin<TStore> to catch type errors early
- Avoid any: Use proper generics instead of
any in plugin definitions
- Module augmentation: Use sparingly and document well
- Export types: Export your plugin’s option and store types for consumers