Files
agent/.agent/skills/tech-stack/elysiajs/references/plugin.md

208 lines
4.9 KiB
Markdown
Raw Normal View History

# Plugins
## Plugin = Decoupled Elysia Instance
```ts
const plugin = new Elysia()
.decorate('plugin', 'hi')
.get('/plugin', ({ plugin }) => plugin)
const app = new Elysia()
.use(plugin) // inherit properties
.get('/', ({ plugin }) => plugin)
```
**Inherits**: state, decorate
**Does NOT inherit**: lifecycle (isolated by default)
## Dependency
Each instance runs independently like microservice. **Must explicitly declare dependencies**.
```ts
const auth = new Elysia()
.decorate('Auth', Auth)
// ❌ Missing dependency
const main = new Elysia()
.get('/', ({ Auth }) => Auth.getProfile())
// ✅ Declare dependency
const main = new Elysia()
.use(auth) // required for Auth
.get('/', ({ Auth }) => Auth.getProfile())
```
## Deduplication
**Every plugin re-executes by default**. Use `name` + optional `seed` to deduplicate:
```ts
const ip = new Elysia({ name: 'ip' }) // unique identifier
.derive({ as: 'global' }, ({ server, request }) => ({
ip: server?.requestIP(request)
}))
const router1 = new Elysia().use(ip)
const router2 = new Elysia().use(ip)
const server = new Elysia().use(router1).use(router2)
// `ip` only executes once due to deduplication
```
## Global vs Explicit Dependency
**Global plugin** (rare, apply everywhere):
- Doesn't add types - cors, compress, helmet
- Global lifecycle no instance controls - tracing, logging
- Examples: OpenAPI docs, OpenTelemetry, logging
**Explicit dependency** (default, recommended):
- Adds types - macro, state, model
- Business logic instances interact with - Auth, DB
- Examples: state management, ORM, auth, features
## Scope
**Lifecycle isolated by default**. Must specify scope to export.
```ts
// ❌ NOT inherited by app
const profile = new Elysia()
.onBeforeHandle(({ cookie }) => throwIfNotSignIn(cookie))
.get('/profile', () => 'Hi')
const app = new Elysia()
.use(profile)
.patch('/rename', ({ body }) => updateProfile(body)) // No sign-in check
// ✅ Exported to app
const profile = new Elysia()
.onBeforeHandle({ as: 'global' }, ({ cookie }) => throwIfNotSignIn(cookie))
.get('/profile', () => 'Hi')
```
## Scope Levels
1. **local** (default) - current + descendants only
2. **scoped** - parent + current + descendants
3. **global** - all instances (all parents, current, descendants)
Example with `.onBeforeHandle({ as: 'local' }, ...)`:
| type | child | current | parent | main |
|------|-------|---------|--------|------|
| local | ✅ | ✅ | ❌ | ❌ |
| scoped | ✅ | ✅ | ✅ | ❌ |
| global | ✅ | ✅ | ✅ | ✅ |
## Config
```ts
// Instance factory with config
const version = (v = 1) => new Elysia()
.get('/version', v)
const app = new Elysia()
.use(version(1))
```
## Functional Callback (not recommended)
```ts
// Harder to handle scope/encapsulation
const plugin = (app: Elysia) => app
.state('counter', 0)
.get('/plugin', () => 'Hi')
// Prefer new instance (better type inference, no perf diff)
```
## Guard (Apply to Multiple Routes)
```ts
.guard(
{ body: t.Object({ username: t.String(), password: t.String() }) },
(app) =>
app.post('/sign-up', ({ body }) => signUp(body))
.post('/sign-in', ({ body }) => signIn(body))
)
```
**Grouped guard** (merge group + guard):
```ts
.group(
'/v1',
{ body: t.Literal('Rikuhachima Aru') }, // guard here
(app) => app.post('/student', ({ body }) => body)
)
```
## Scope Casting
**3 methods to apply hook to parent**:
1. **Inline as** (single hook):
```ts
.derive({ as: 'scoped' }, () => ({ hi: 'ok' }))
```
2. **Guard as** (multiple hooks, no derive/resolve):
```ts
.guard({
as: 'scoped',
response: t.String(),
beforeHandle() { console.log('ok') }
})
```
3. **Instance as** (all hooks + schema):
```ts
const plugin = new Elysia()
.derive(() => ({ hi: 'ok' }))
.get('/child', ({ hi }) => hi)
.as('scoped') // lift scope up
```
`.as()` lifts scope: local → scoped → global
## Lazy Load
**Deferred module** (async plugin, non-blocking startup):
```ts
// plugin.ts
export const loadStatic = async (app: Elysia) => {
const files = await loadAllFiles()
files.forEach((asset) => app.get(asset, file(asset)))
return app
}
// main.ts
const app = new Elysia().use(loadStatic)
```
**Lazy-load module** (dynamic import):
```ts
const app = new Elysia()
.use(import('./plugin')) // loaded after startup
```
**Testing** (wait for modules):
```ts
await app.modules // ensure all deferred/lazy modules loaded
```
## Notes
[Inference] Based on docs patterns:
- Use inline values for static resources (performance optimization)
- Group routes by prefix for organization
- Extend context minimally (separation of concerns)
- Use `status()` over `set.status` for type safety
- Prefer `resolve()` over `derive()` when type integrity matters
- Plugins isolated by default (must declare scope explicitly)
- Use `name` for deduplication when plugin used multiple times
- Prefer explicit dependency over global (better modularity/tracking)