Bun Plugins Ecosystem
The Stacks.js reference implementation leverages Bun's powerful plugin architecture to extend bundling, module resolution, and runtime capabilities. It includes several Bun plugins out of the box and provides patterns for creating custom plugins.
Protocol context — This page describes Bun-specific tooling of the Stacks.js reference implementation. Bun plugins are reference-implementation-specific and are not part of the language-agnostic Stacks Protocol; the capabilities they enable map to contracts in the Stacks Protocol white paper.
Plugin Architecture
Bun plugins hook into the bundler and module resolver to transform, load, or generate code during build time:
import type { BunPlugin } from 'bun'
const myPlugin: BunPlugin = {
name: 'my-plugin',
setup(build) {
// Called once when plugin is registered
// Handle module resolution
build.onResolve({ filter: /\.custom$/ }, (args) => {
return { path: args.path, namespace: 'custom' }
})
// Handle module loading
build.onLoad({ filter: /.*/, namespace: 'custom' }, async (args) => {
const source = await Bun.file(args.path).text()
return {
contents: transformCustomFormat(source),
loader: 'ts',
}
})
},
}
Built-in Stacks Plugins
bun-plugin-stx
Compiles STX templates to optimized JavaScript:
import { stxPlugin } from 'bun-plugin-stx'
await Bun.build({
entrypoints: ['./app.ts'],
plugins: [
stxPlugin({
componentDir: './components',
layoutDir: './layouts',
minify: true,
sourceMap: true,
}),
],
})
The plugin transforms STX syntax:
@component('UserCard'
<divclass="
<h2{{ name }}</h2
@if(verified
<spanclass="Verified</span
</div
Into optimized runtime code:
export function UserCard(props: { name: string; verified: boolean }) {
return h('div', { class: 'card' }, [
h('h2', null, props.name),
props.verified && h('span', { class: 'badge' }, 'Verified'),
])
}
bun-plugin-crosswind
Processes and optimizes Crosswind CSS:
import { crosswindPlugin } from 'bun-plugin-crosswind'
await Bun.build({
entrypoints: ['./app.ts'],
plugins: [
crosswindPlugin({
config: './crosswind.config.ts',
purge: true,
minify: true,
sourceMap: false,
}),
],
})
Features:
- JIT Compilation: Generates only used utility classes
- Custom Theme: Extends default design tokens
- Variants: Responsive, state, and custom variants
- Plugins: Extend with typography, forms, aspect-ratio
bun-plugin-dts
Generates TypeScript declarations during build:
import { dtsPlugin } from 'bun-plugin-dts'
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
plugins: [
dtsPlugin({
outDir: './dist',
bundle: true,
respectExternal: true,
}),
],
})
bun-plugin-auto-imports
Enables auto-imports without explicit import statements. In Stacks, everything is auto-importable by default — functions, composables, components, and utilities are all available without manual imports:
import { autoImportsPlugin } from 'bun-plugin-auto-imports'
await Bun.build({
entrypoints: ['./app.ts'],
plugins: [
autoImportsPlugin({
// Import from packages
imports: [
{ from: '@stacksjs/utils', names: ['log', 'debug'] },
{ from: 'vue', names: ['ref', 'computed', 'watch'] },
],
// Scan directories for auto-imports
directories: ['./composables', './utils'],
// Use presets (vue, solid-js, react, etc.)
presets: ['vue'],
// Generate TypeScript declaration file
dts: './auto-imports.d.ts',
}),
],
})
How Stacks Uses Auto-Imports
Stacks configures auto-imports out of the box, making all your code instantly available:
// In Stacks, these directories are automatically scanned:
// - ./app/Actions → Actions are auto-imported
// - ./app/Composables → Composables are auto-imported
// - ./app/Utils → Utility functions are auto-imported
// - ./components → Components are auto-imported
// - ./resources/functions → Helper functions are auto-imported
// Example: No imports needed!
// useAuth() is auto-imported from ./app/Composables/useAuth.ts
const { user, login, logout } = useAuth()
// formatCurrency() is auto-imported from ./app/Utils/formatCurrency.ts
const price = formatCurrency(99.99)
// CreateUser action is auto-imported from ./app/Actions/CreateUser.ts
const result = await CreateUser.handle({ name: 'John', email: 'john@example.com' })
Configuration
Configure auto-imports in config/app.ts:
// config/app.ts
export default {
autoImports: {
// Directories to scan for auto-imports
dirs: [
'./app/Actions',
'./app/Composables',
'./app/Utils',
'./components',
'./resources/functions',
],
// Package presets to include
presets: [
'vue', // ref, computed, watch, etc.
'@vueuse/core', // useStorage, useFetch, etc.
],
// Explicit imports from packages
imports: [
{ from: 'zod', names: ['z'] },
{ from: '@stacksjs/validation', names: ['v'] },
],
// Generate TypeScript declarations
dts: './storage/framework/types/auto-imports.d.ts',
},
}
Generated TypeScript Declarations
Auto-imports automatically generates a .d.ts file so TypeScript understands all available globals:
// storage/framework/types/auto-imports.d.ts (auto-generated)
export {}
declare global {
// From ./app/Composables
const useAuth: typeof import('./app/Composables/useAuth')['default']
const useCart: typeof import('./app/Composables/useCart')['default']
// From ./app/Utils
const formatCurrency: typeof import('./app/Utils/formatCurrency')['formatCurrency']
const formatDate: typeof import('./app/Utils/formatDate')['formatDate']
// From vue preset
const ref: typeof import('vue')['ref']
const computed: typeof import('vue')['computed']
const watch: typeof import('vue')['watch']
// From zod
const z: typeof import('zod')['z']
}
Pickier Integration
Stacks uses Pickier for linting and formatting. Auto-imports work seamlessly with Pickier because:
- TypeScript declarations - The generated
.d.tsfile tells TypeScript about all auto-imported globals - No globals configuration needed - Pickier relies on TypeScript for type checking, so the declarations are sufficient
// The generated auto-imports.d.ts handles everything
// storage/framework/types/auto-imports.d.ts
declare global {
const useAuth: typeof import('./app/Composables/useAuth')['default']
const ref: typeof import('vue')['ref']
// ... all auto-imported functions
}
For edge cases where you need to explicitly declare globals (e.g., runtime-injected variables), you can add them to your pickier.config.ts:
// pickier.config.ts
import type { PickierConfig } from 'pickier'
const config: PickierConfig = {
// ... other config
pluginRules: {
// Ignore specific patterns in no-unused-vars
'pickier/no-unused-vars': ['error', {
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
}],
},
}
export default config
bun-plugin-env
Handles environment variable injection and validation:
import { envPlugin } from 'bun-plugin-env'
await Bun.build({
entrypoints: ['./app.ts'],
plugins: [
envPlugin({
envFile: '.env',
prefix: 'VITE_',
validate: {
API_URL: { required: true, type: 'url' },
DEBUG: { type: 'boolean', default: 'false' },
},
}),
],
})
Creating Custom Plugins
Basic Plugin Structure
import type { BunPlugin } from 'bun'
export function createMyPlugin(options: MyPluginOptions = {}): BunPlugin {
return {
name: 'my-plugin',
setup(build) {
const { transform = true, minify = false } = options
build.onLoad({ filter: /\.myext$/ }, async (args) => {
const source = await Bun.file(args.path).text()
let result = source
if (transform) {
result = await transformSource(result)
}
if (minify) {
result = await minifySource(result)
}
return {
contents: result,
loader: 'js',
}
})
},
}
}
Virtual Module Plugin
Create virtual modules that don't exist on disk:
export function virtualModulePlugin(modules: Record<string, string>): BunPlugin {
return {
name: 'virtual-modules',
setup(build) {
build.onResolve({ filter: /^virtual:/ }, (args) => ({
path: args.path,
namespace: 'virtual',
}))
build.onLoad({ filter: /.*/, namespace: 'virtual' }, (args) => {
const moduleName = args.path.replace('virtual:', '')
const contents = modules[moduleName]
if (!contents) {
throw new Error(`Virtual module "${moduleName}" not found`)
}
return { contents, loader: 'ts' }
})
},
}
}
// Usage
await Bun.build({
plugins: [
virtualModulePlugin({
'virtual:build-info': `export const buildTime = "${new Date().toISOString()}"`,
'virtual:version': `export const version = "${pkg.version}"`,
}),
],
})
Asset Transformation Plugin
Transform non-JS assets during build:
export function svgPlugin(): BunPlugin {
return {
name: 'svg-component',
setup(build) {
build.onLoad({ filter: /\.svg$/ }, async (args) => {
const svg = await Bun.file(args.path).text()
const optimized = await optimizeSvg(svg)
return {
contents: `
export default function SvgComponent(props) {
return \`.replace(/<svg/,'<svg {...props}')\`
}
`,
loader: 'js',
}
})
},
}
}
Plugin Composition
Stacks provides utilities for composing multiple plugins:
import { composePlugins, withDefaults } from '@stacksjs/bun-plugins'
const stacksPlugins = composePlugins([
stxPlugin(),
crosswindPlugin(),
dtsPlugin(),
autoImportsPlugin({
directories: ['./composables'],
}),
])
await Bun.build({
entrypoints: ['./src/index.ts'],
plugins: stacksPlugins,
})
Runtime Plugins
Some plugins work at runtime rather than build time:
import { plugin } from 'bun'
// Register runtime loader for custom file types
plugin({
name: 'yaml-loader',
setup(build) {
build.onLoad({ filter: /\.ya?ml$/ }, async (args) => {
const text = await Bun.file(args.path).text()
const data = parseYaml(text)
return {
contents: `export default ${JSON.stringify(data)}`,
loader: 'js',
}
})
},
})
// Now you can import YAML files
import config from './config.yaml'
Plugin Best Practices
- Use Specific Filters: Narrow file filters improve performance
- Cache Results: Store transformed results for repeated builds
- Handle Errors Gracefully: Provide helpful error messages
- Support Source Maps: Maintain debugging capability
- Document Options: Clear configuration documentation
- Test Thoroughly: Unit test transformation logic
// Example with caching and error handling
export function robustPlugin(options: PluginOptions): BunPlugin {
const cache = new Map<string, TransformResult>()
return {
name: 'robust-plugin',
setup(build) {
build.onLoad({ filter: options.filter }, async (args) => {
const cacheKey = `${args.path}:${Bun.file(args.path).lastModified}`
if (cache.has(cacheKey)) {
return cache.get(cacheKey)!
}
try {
const source = await Bun.file(args.path).text()
const result = await transform(source, options)
cache.set(cacheKey, result)
return result
} catch (error) {
throw new Error(
`[robust-plugin] Failed to transform ${args.path}: ${error.message}`
)
}
})
},
}
}