Plugin Development
How to create plugins for the EreoJS ecosystem.
Plugin Structure
An EreoJS plugin is a function that returns a plugin object with lifecycle hooks:
import type { EreoPlugin } from '@ereo/core'
export function myPlugin(options?: MyPluginOptions): EreoPlugin {
return {
name: 'my-plugin',
setup(app) {
// Called once when the plugin is registered
},
configResolved(config) {
// Called after all config is resolved
},
buildStart() {
// Called when the build starts
},
buildEnd() {
// Called when the build finishes
},
devServerStart(server) {
// Called when the dev server starts
},
}
}Register the plugin in ereo.config.ts:
import { defineConfig } from '@ereo/core'
import { myPlugin } from './plugins/my-plugin'
export default defineConfig({
plugins: [
myPlugin({ /* options */ }),
],
})Lifecycle Hooks
Hooks are called in this order during development:
setup(app)--- Register services, set initial stateconfigResolved(config)--- Read the final merged configurationdevServerStart(server)--- Access the Bun HTTP server instancebuildStart()--- Called before bundling (dev or production)transform(code, id)--- Transform source files during bundlingbuildEnd()--- Called after bundling completes
During production builds, devServerStart is skipped.
Creating a Middleware Plugin
A common plugin pattern is adding middleware to the request pipeline:
import type { EreoPlugin } from '@ereo/core'
export function rateLimitPlugin(maxRequests = 100): EreoPlugin {
const requestCounts = new Map<string, number>()
return {
name: 'rate-limit',
setup(app) {
app.middleware('rateLimit', async (request, context, next) => {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const count = requestCounts.get(ip) || 0
if (count >= maxRequests) {
return new Response('Too Many Requests', { status: 429 })
}
requestCounts.set(ip, count + 1)
return next()
})
},
}
}Routes can then reference this middleware by name:
// routes/api/_middleware.ts
export const config = {
middleware: ['rateLimit'],
}Creating a Virtual Module Plugin
Virtual modules let plugins provide imports that do not correspond to actual files on disk:
import type { EreoPlugin } from '@ereo/core'
export function buildInfoPlugin(): EreoPlugin {
return {
name: 'build-info',
setup(app) {
app.virtualModule('virtual:build-info', () => {
return `
export const buildTime = ${JSON.stringify(new Date().toISOString())};
export const version = ${JSON.stringify(process.env.npm_package_version)};
export const nodeEnv = ${JSON.stringify(process.env.NODE_ENV)};
`
})
},
}
}Import the virtual module in your application:
import { buildTime, version } from 'virtual:build-info'
export default function Footer() {
return <footer>v{version} - Built {buildTime}</footer>
}For TypeScript support, create a declaration file:
// types/virtual-build-info.d.ts
declare module 'virtual:build-info' {
export const buildTime: string
export const version: string
export const nodeEnv: string
}Transforming Source Files
Use the transform hook to modify source code during bundling:
import type { EreoPlugin } from '@ereo/core'
export function autoImportPlugin(): EreoPlugin {
return {
name: 'auto-import',
transform(code, id) {
// Only process .tsx route files
if (!id.endsWith('.tsx') || !id.includes('/routes/')) return
// Add automatic imports
if (code.includes('useLoaderData') && !code.includes("from '@ereo/client'")) {
return `import { useLoaderData } from '@ereo/client'\n${code}`
}
},
}
}Return undefined or null to skip transformation. Return a string to replace the file contents.
Testing Plugins
Test plugins using Bun's test runner with a mock application context:
// my-plugin.test.ts
import { test, expect } from 'bun:test'
import { createTestApp } from '@ereo/core/test'
import { myPlugin } from './my-plugin'
test('plugin registers middleware', async () => {
const app = createTestApp({
plugins: [myPlugin()],
})
await app.init()
expect(app.hasMiddleware('myMiddleware')).toBe(true)
})
test('plugin transforms route files', async () => {
const app = createTestApp({
plugins: [myPlugin()],
})
await app.init()
const result = app.transform('const x = 1', '/routes/index.tsx')
expect(result).toContain('import')
})Publishing to npm
- Follow the standard package conventions (ESM, TypeScript declarations)
- Name your package
ereo-plugin-*or@yourscope/ereo-plugin-* - Include
ereo-pluginin thekeywordsarray inpackage.json - Export the plugin factory function as the default export or a named export
- Include a
README.mdwith installation and configuration instructions
{
"name": "ereo-plugin-analytics",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"keywords": ["ereo", "ereo-plugin", "analytics"],
"peerDependencies": {
"@ereo/core": "^0.1.0"
}
}See the Plugins guide for how users consume plugins.