208 lines
4.9 KiB
Markdown
208 lines
4.9 KiB
Markdown
# 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)
|