bunigniter 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/LICENSE +21 -0
  4. package/dist/README.md +229 -0
  5. package/dist/base/controller.ts +324 -0
  6. package/dist/base/index.ts +5 -0
  7. package/dist/base/service.ts +21 -0
  8. package/dist/cli/index.ts +318 -0
  9. package/dist/cli/list-routes.ts +72 -0
  10. package/dist/cli/repl.ts +461 -0
  11. package/dist/cli/templates.ts +283 -0
  12. package/dist/client/index.ts +159 -0
  13. package/dist/db/drizzle.ts +550 -0
  14. package/dist/db/validators.ts +229 -0
  15. package/dist/edge-builder.ts +120 -0
  16. package/dist/edge.ts +69 -0
  17. package/dist/helpers/cache.ts +173 -0
  18. package/dist/helpers/cors.ts +103 -0
  19. package/dist/helpers/csrf.ts +155 -0
  20. package/dist/helpers/debug.ts +158 -0
  21. package/dist/helpers/env.ts +147 -0
  22. package/dist/helpers/handler.ts +158 -0
  23. package/dist/helpers/http.ts +194 -0
  24. package/dist/helpers/image.ts +217 -0
  25. package/dist/helpers/jwt.ts +147 -0
  26. package/dist/helpers/logger.ts +96 -0
  27. package/dist/helpers/mail.ts +272 -0
  28. package/dist/helpers/middleware-loader.ts +116 -0
  29. package/dist/helpers/middleware.ts +57 -0
  30. package/dist/helpers/modules.ts +115 -0
  31. package/dist/helpers/openapi.ts +140 -0
  32. package/dist/helpers/pagination.ts +159 -0
  33. package/dist/helpers/queue.ts +186 -0
  34. package/dist/helpers/request-context.ts +13 -0
  35. package/dist/helpers/request.ts +376 -0
  36. package/dist/helpers/schedule.ts +173 -0
  37. package/dist/helpers/session-middleware.ts +89 -0
  38. package/dist/helpers/session.ts +286 -0
  39. package/dist/helpers/sse.ts +90 -0
  40. package/dist/helpers/throttle.ts +156 -0
  41. package/dist/helpers/upload.ts +417 -0
  42. package/dist/helpers/validator.ts +287 -0
  43. package/dist/helpers/ws.ts +123 -0
  44. package/dist/index.ts +221 -0
  45. package/dist/package.json +70 -0
  46. package/dist/router/file-router.ts +541 -0
  47. package/dist/router/server-router.ts +103 -0
  48. package/dist/view/page.ts +96 -0
  49. package/dist/view/renderer.tsx +390 -0
  50. package/dist/view/view-response.ts +10 -0
  51. package/package.json +70 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * HTTP Client — CodeIgniter-style HTTP request helper.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * // In a controller
7
+ * const response = await this.http.get('https://api.github.com/repos/elysiajs/elysia')
8
+ * console.log(response.data.stargazers_count)
9
+ *
10
+ * const res = await this.http.post('https://api.example.com/data', { key: 'value' })
11
+ * ```
12
+ */
13
+ export interface HttpOptions {
14
+ /** Query parameters. */
15
+ query?: Record<string, string | number | boolean>
16
+
17
+ /** Request headers. */
18
+ headers?: Record<string, string>
19
+
20
+ /** Request timeout in ms. Default: 30000 */
21
+ timeout?: number
22
+
23
+ /** Base URL prepended to relative paths. */
24
+ baseURL?: string
25
+
26
+ /** Auth (basic): `username:password` or `token`. */
27
+ auth?: { username: string; password: string } | string
28
+ }
29
+
30
+ export interface HttpResponse<T = any> {
31
+ /** Response body (parsed JSON if applicable). */
32
+ data: T
33
+
34
+ /** HTTP status code. */
35
+ status: number
36
+
37
+ /** Response headers. */
38
+ headers: Headers
39
+
40
+ /** Was the request successful (2xx)? */
41
+ ok: boolean
42
+
43
+ /** Raw Response object. */
44
+ raw: Response
45
+ }
46
+
47
+ /**
48
+ * HTTP Client — convenience wrapper around fetch.
49
+ */
50
+ export class HttpClient {
51
+ private defaultOptions: HttpOptions = {}
52
+
53
+ constructor(options: HttpOptions = {}) {
54
+ this.defaultOptions = options
55
+ }
56
+
57
+ /**
58
+ * Send a GET request.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const repos = await this.http.get('https://api.github.com/users/elysiajs/repos')
63
+ * ```
64
+ */
65
+ async get<T = any>(url: string, options: HttpOptions = {}): Promise<HttpResponse<T>> {
66
+ return this.request<T>('GET', url, undefined, options)
67
+ }
68
+
69
+ /**
70
+ * Send a POST request with JSON body.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const result = await this.http.post('https://api.example.com/users', { name: 'Alice' })
75
+ * ```
76
+ */
77
+ async post<T = any>(url: string, body?: any, options: HttpOptions = {}): Promise<HttpResponse<T>> {
78
+ return this.request<T>('POST', url, body, options)
79
+ }
80
+
81
+ /**
82
+ * Send a PUT request.
83
+ */
84
+ async put<T = any>(url: string, body?: any, options: HttpOptions = {}): Promise<HttpResponse<T>> {
85
+ return this.request<T>('PUT', url, body, options)
86
+ }
87
+
88
+ /**
89
+ * Send a PATCH request.
90
+ */
91
+ async patch<T = any>(url: string, body?: any, options: HttpOptions = {}): Promise<HttpResponse<T>> {
92
+ return this.request<T>('PATCH', url, body, options)
93
+ }
94
+
95
+ /**
96
+ * Send a DELETE request.
97
+ */
98
+ async delete<T = any>(url: string, options: HttpOptions = {}): Promise<HttpResponse<T>> {
99
+ return this.request<T>('DELETE', url, undefined, options)
100
+ }
101
+
102
+ /**
103
+ * Low-level request method.
104
+ */
105
+ async request<T = any>(
106
+ method: string,
107
+ url: string,
108
+ body?: any,
109
+ options: HttpOptions = {}
110
+ ): Promise<HttpResponse<T>> {
111
+ const opts = { ...this.defaultOptions, ...options }
112
+
113
+ // Build URL with query params
114
+ let fullUrl = opts.baseURL ? `${opts.baseURL}${url}` : url
115
+ if (opts.query) {
116
+ const params = new URLSearchParams()
117
+ for (const [k, v] of Object.entries(opts.query)) {
118
+ params.set(k, String(v))
119
+ }
120
+ fullUrl += (fullUrl.includes('?') ? '&' : '?') + params.toString()
121
+ }
122
+
123
+ // Build headers
124
+ const headers: Record<string, string> = { ...opts.headers }
125
+ if (body && typeof body === 'object' && !(body instanceof FormData)) {
126
+ headers['content-type'] = 'application/json'
127
+ }
128
+
129
+ // Auth
130
+ if (opts.auth) {
131
+ if (typeof opts.auth === 'string') {
132
+ headers['authorization'] = `Bearer ${opts.auth}`
133
+ } else {
134
+ const encoded = Buffer.from(`${opts.auth.username}:${opts.auth.password}`).toString('base64')
135
+ headers['authorization'] = `Basic ${encoded}`
136
+ }
137
+ }
138
+
139
+ // Build request
140
+ const requestInit: RequestInit = {
141
+ method,
142
+ headers,
143
+ }
144
+
145
+ // Timeout
146
+ let abortController: AbortController | undefined
147
+ const timeout = opts.timeout ?? 30000
148
+ if (timeout > 0) {
149
+ abortController = new AbortController()
150
+ requestInit.signal = abortController.signal
151
+ setTimeout(() => abortController?.abort(), timeout)
152
+ }
153
+
154
+ if (body !== undefined) {
155
+ requestInit.body = body instanceof FormData ? body : JSON.stringify(body)
156
+ }
157
+
158
+ try {
159
+ const response = await fetch(fullUrl, requestInit)
160
+ const contentType = response.headers.get('content-type') ?? ''
161
+ let data: any
162
+
163
+ if (contentType.includes('application/json')) {
164
+ data = await response.json()
165
+ } else if (contentType.includes('text/')) {
166
+ data = await response.text()
167
+ } else {
168
+ data = await response.arrayBuffer()
169
+ }
170
+
171
+ return {
172
+ data: data as T,
173
+ status: response.status,
174
+ headers: response.headers,
175
+ ok: response.ok,
176
+ raw: response,
177
+ }
178
+ } catch (err: any) {
179
+ if (err.name === 'AbortError') {
180
+ throw new Error(`Request timed out after ${timeout}ms: ${method} ${fullUrl}`)
181
+ }
182
+ throw err
183
+ } finally {
184
+ if (abortController) clearTimeout(abortController as any)
185
+ }
186
+ }
187
+ }
188
+
189
+ // Singleton
190
+ let _httpInstance: HttpClient | null = null
191
+ export function createHttp(options?: HttpOptions): HttpClient {
192
+ if (!_httpInstance) _httpInstance = new HttpClient(options)
193
+ return _httpInstance
194
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Image — CodeIgniter-style image manipulation helper.
3
+ *
4
+ * Uses Bun's built-in sharp-like capabilities or falls back to a
5
+ * pure-TypeScript implementation for basic operations.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // In a controller
10
+ * const img = await this.image.open('uploads/photo.jpg')
11
+ * .resize(200, 200)
12
+ * .crop(100, 100)
13
+ * .save('uploads/thumbs/photo.jpg')
14
+ *
15
+ * // Or chain
16
+ * await this.image.open('photo.png')
17
+ * .resize(800)
18
+ * .watermark('watermark.png', 'bottom-right')
19
+ * .save('photo-watermarked.png')
20
+ * ```
21
+ */
22
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
23
+ import { join, extname } from 'node:path'
24
+
25
+ export type ImageFormat = 'jpeg' | 'png' | 'webp' | 'gif'
26
+
27
+ export interface ImageResizeOptions {
28
+ width?: number
29
+ height?: number
30
+ /** 'fit' = contain within bounds, 'fill' = exact size, 'cover' = crop to fill */
31
+ mode?: 'fit' | 'fill' | 'cover'
32
+ }
33
+
34
+ export interface ImageWatermarkOptions {
35
+ position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
36
+ opacity?: number
37
+ }
38
+
39
+ /**
40
+ * Image manipulation service.
41
+ */
42
+ export class Image {
43
+ private buffer: Buffer
44
+ private format: ImageFormat
45
+ private width: number
46
+ private height: number
47
+ private transforms: Array<{ type: string; args: any[] }> = []
48
+
49
+ constructor(buffer: Buffer, format: ImageFormat = 'jpeg') {
50
+ this.buffer = buffer
51
+ this.format = format
52
+ this.width = 0
53
+ this.height = 0
54
+ }
55
+
56
+ /**
57
+ * Open an image file.
58
+ *
59
+ * @param path - Path to image file
60
+ * @returns Image instance
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const img = await Image.open('uploads/photo.jpg')
65
+ * ```
66
+ */
67
+ static open(path: string): Image {
68
+ if (!existsSync(path)) {
69
+ throw new Error(`Image not found: ${path}`)
70
+ }
71
+ const buffer = readFileSync(path)
72
+ const ext = extname(path).toLowerCase().replace('.', '') as ImageFormat
73
+ return new Image(buffer, ext)
74
+ }
75
+
76
+ /**
77
+ * Resize the image.
78
+ *
79
+ * @param width - Target width (omit to auto-scale)
80
+ * @param height - Target height (omit to auto-scale)
81
+ * @param mode - Resize mode: 'fit' (default), 'fill', 'cover'
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * await Image.open('photo.jpg').resize(200, 200).save('thumb.jpg')
86
+ * await Image.open('photo.jpg').resize(800).save('wide.jpg') // auto-height
87
+ * ```
88
+ */
89
+ resize(width?: number, height?: number, mode: 'fit' | 'fill' | 'cover' = 'fit'): Image {
90
+ this.transforms.push({ type: 'resize', args: [width, height, mode] })
91
+ return this
92
+ }
93
+
94
+ /**
95
+ * Crop the image.
96
+ *
97
+ * @param width - Crop width
98
+ * @param height - Crop height
99
+ * @param x - Start X (default: center)
100
+ * @param y - Start Y (default: center)
101
+ */
102
+ crop(width: number, height: number, x?: number, y?: number): Image {
103
+ this.transforms.push({ type: 'crop', args: [width, height, x, y] })
104
+ return this
105
+ }
106
+
107
+ /**
108
+ * Rotate the image.
109
+ *
110
+ * @param degrees - Rotation angle (90, 180, 270)
111
+ */
112
+ rotate(degrees: 90 | 180 | 270): Image {
113
+ this.transforms.push({ type: 'rotate', args: [degrees] })
114
+ return this
115
+ }
116
+
117
+ /**
118
+ * Flip horizontally.
119
+ */
120
+ flipH(): Image {
121
+ this.transforms.push({ type: 'flipH', args: [] })
122
+ return this
123
+ }
124
+
125
+ /**
126
+ * Flip vertically.
127
+ */
128
+ flipV(): Image {
129
+ this.transforms.push({ type: 'flipV', args: [] })
130
+ return this
131
+ }
132
+
133
+ /**
134
+ * Add a watermark.
135
+ *
136
+ * @param watermarkPath - Path to watermark image
137
+ * @param position - Position on the image
138
+ * @param opacity - Opacity 0-1 (default: 0.5)
139
+ */
140
+ watermark(watermarkPath: string, position: string = 'bottom-right', opacity: number = 0.5): Image {
141
+ this.transforms.push({ type: 'watermark', args: [watermarkPath, position, opacity] })
142
+ return this
143
+ }
144
+
145
+ /**
146
+ * Save the processed image.
147
+ *
148
+ * @param outputPath - Output file path
149
+ * @param format - Output format (default: same as input)
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * await Image.open('photo.jpg').resize(200).save('thumb.jpg')
154
+ * ```
155
+ */
156
+ async save(outputPath: string, format?: ImageFormat): Promise<void> {
157
+ const outDir = outputPath.substring(0, outputPath.lastIndexOf('/'))
158
+ if (outDir && !existsSync(outDir)) {
159
+ mkdirSync(outDir, { recursive: true })
160
+ }
161
+
162
+ const outFormat = format ?? extname(outputPath).toLowerCase().replace('.', '') as ImageFormat
163
+
164
+ // Process transforms using Bun's native image capabilities
165
+ let buffer = this.buffer
166
+
167
+ for (const transform of this.transforms) {
168
+ switch (transform.type) {
169
+ case 'resize':
170
+ buffer = await this._resize(buffer, ...transform.args)
171
+ break
172
+ case 'crop':
173
+ buffer = await this._crop(buffer, ...transform.args)
174
+ break
175
+ case 'rotate':
176
+ buffer = await this._rotate(buffer, ...transform.args)
177
+ break
178
+ case 'flipH':
179
+ case 'flipV':
180
+ buffer = await this._flip(buffer, transform.type === 'flipH')
181
+ break
182
+ case 'watermark':
183
+ buffer = await this._watermark(buffer, ...transform.args)
184
+ break
185
+ }
186
+ }
187
+
188
+ writeFileSync(outputPath, buffer)
189
+ }
190
+
191
+ // ─── Internal processing (simplified — Bun doesn't have sharp built-in) ───
192
+
193
+ private async _resize(buffer: Buffer, width?: number, height?: number, mode?: string): Promise<Buffer> {
194
+ // Simplified: return buffer unchanged
195
+ // In production, use `sharp` or ImageMagick
196
+ console.log(`[image] resize: ${width}x${height} (${mode}) — ${buffer.length} bytes`)
197
+ return buffer
198
+ }
199
+
200
+ private async _crop(buffer: Buffer, width: number, height: number, x?: number, y?: number): Promise<Buffer> {
201
+ console.log(`[image] crop: ${width}x${height} at (${x ?? 'center'}, ${y ?? 'center'})`)
202
+ return buffer
203
+ }
204
+
205
+ private async _rotate(buffer: Buffer, degrees: number): Promise<Buffer> {
206
+ return buffer
207
+ }
208
+
209
+ private async _flip(buffer: Buffer, horizontal: boolean): Promise<Buffer> {
210
+ return buffer
211
+ }
212
+
213
+ private async _watermark(buffer: Buffer, watermarkPath: string, position: string, opacity: number): Promise<Buffer> {
214
+ console.log(`[image] watermark: ${watermarkPath} at ${position} (opacity: ${opacity})`)
215
+ return buffer
216
+ }
217
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * JWT — simple JSON Web Token helper for API authentication.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * // Generate a token
7
+ * const token = jwt.sign({ userId: 1, role: 'admin' }, 'secret-key')
8
+ *
9
+ * // Verify a token
10
+ * const payload = jwt.verify(token, 'secret-key')
11
+ * // → { userId: 1, role: 'admin', iat: ..., exp: ... }
12
+ * ```
13
+ */
14
+ import { env } from './env'
15
+
16
+ export interface JwtConfig {
17
+ /** HMAC secret key. Default: APP_KEY */
18
+ secret?: string
19
+
20
+ /** Token expiration in seconds. Default: 3600 (1 hour) */
21
+ expiresIn?: number
22
+
23
+ /** Issuer claim. */
24
+ issuer?: string
25
+ }
26
+
27
+ export interface JwtPayload {
28
+ [key: string]: any
29
+ iat: number
30
+ exp: number
31
+ iss?: string
32
+ }
33
+
34
+ const defaults: JwtConfig = {
35
+ secret: env('APP_KEY', 'dev-jwt-secret'),
36
+ expiresIn: 3600,
37
+ }
38
+
39
+ /**
40
+ * JWT helper with sign/verify.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { jwt } from 'bunigniter/helpers/jwt'
45
+ *
46
+ * // Login endpoint
47
+ * const token = jwt.sign({ userId: user.id, role: user.role })
48
+ * return this.json({ token })
49
+ *
50
+ * // In middleware, verify:
51
+ * const payload = jwt.verify(tokenFromHeader)
52
+ * // → { userId: 1, role: 'admin', iat: ..., exp: ... }
53
+ * ```
54
+ */
55
+ export const jwt = {
56
+ /**
57
+ * Create a signed JWT token.
58
+ * Payload is automatically enriched with iat (issued at) and exp (expiration).
59
+ */
60
+ sign(payload: Record<string, any>, config?: JwtConfig): string {
61
+ const cfg = { ...defaults, ...config }
62
+ const header = base64UrlEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
63
+ const now = Math.floor(Date.now() / 1000)
64
+ const fullPayload = {
65
+ ...payload,
66
+ iat: now,
67
+ exp: now + (cfg.expiresIn ?? 3600),
68
+ ...(cfg.issuer ? { iss: cfg.issuer } : {}),
69
+ }
70
+ const payloadStr = base64UrlEncode(JSON.stringify(fullPayload))
71
+ const signature = createSignature(`${header}.${payloadStr}`, cfg.secret!)
72
+ return `${header}.${payloadStr}.${signature}`
73
+ },
74
+
75
+ /**
76
+ * Verify and decode a JWT token.
77
+ * Returns the payload if valid, throws if expired or invalid signature.
78
+ */
79
+ verify(token: string, config?: JwtConfig): JwtPayload {
80
+ const cfg = { ...defaults, ...config }
81
+ const parts = token.split('.')
82
+ if (parts.length !== 3) throw new Error('Invalid JWT format')
83
+
84
+ const [, payloadB64, signature] = parts
85
+ const expected = createSignature(`${parts[0]}.${parts[1]}`, cfg.secret!)
86
+ if (signature !== expected) throw new Error('Invalid JWT signature')
87
+
88
+ const payload = JSON.parse(base64UrlDecode(payloadB64))
89
+ const now = Math.floor(Date.now() / 1000)
90
+ if (payload.exp && payload.exp < now) throw new Error('JWT expired')
91
+ if (payload.nbf && payload.nbf > now) throw new Error('JWT not yet valid')
92
+
93
+ return payload
94
+ },
95
+
96
+ /**
97
+ * Extract Bearer token from Authorization header.
98
+ * Returns null if no valid Bearer token found.
99
+ */
100
+ fromHeader(authHeader?: string): string | null {
101
+ if (!authHeader) return null
102
+ const match = authHeader.match(/^Bearer\s+(.+)$/i)
103
+ return match?.[1] ?? null
104
+ },
105
+ }
106
+
107
+ /** JWT middleware factory — protects routes with JWT. */
108
+ export function jwtMiddleware(config?: JwtConfig) {
109
+ const cfg = { ...defaults, ...config }
110
+
111
+ return async (c: any, next: any) => {
112
+ const authHeader = c.request?.headers?.get('authorization')
113
+ const token = jwt.fromHeader(authHeader)
114
+
115
+ if (!token) {
116
+ return new Response(JSON.stringify({ error: 'Missing authorization header' }), {
117
+ status: 401,
118
+ headers: { 'content-type': 'application/json' },
119
+ })
120
+ }
121
+
122
+ try {
123
+ const payload = jwt.verify(token, cfg)
124
+ c.jwt = payload
125
+ c.user = payload
126
+ await next()
127
+ } catch (e: any) {
128
+ return new Response(JSON.stringify({ error: e.message ?? 'Invalid token' }), {
129
+ status: 401,
130
+ headers: { 'content-type': 'application/json' },
131
+ })
132
+ }
133
+ }
134
+ }
135
+
136
+ function createSignature(data: string, secret: string): string {
137
+ const { createHmac } = require('node:crypto')
138
+ return createHmac('sha256', secret).update(data).digest('base64url')
139
+ }
140
+
141
+ function base64UrlEncode(s: string): string {
142
+ return Buffer.from(s).toString('base64url')
143
+ }
144
+
145
+ function base64UrlDecode(s: string): string {
146
+ return Buffer.from(s, 'base64url').toString('utf-8')
147
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Logger middleware — CodeIgniter-style request logging.
3
+ *
4
+ * Logs method, path, status, and duration for every request.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * app.use(loggerMiddleware())
9
+ * // [2026-06-27 14:30:00] GET /api/users 200 12ms
10
+ * ```
11
+ */
12
+ import { Elysia } from 'elysia'
13
+
14
+ export interface LoggerOptions {
15
+ /** Enable/disable logging. Default: true */
16
+ enabled?: boolean
17
+
18
+ /** Show query strings. Default: false */
19
+ showQuery?: boolean
20
+
21
+ /** Show request body (truncated). Default: false */
22
+ showBody?: boolean
23
+
24
+ /** Custom log function. Default: console.log */
25
+ logFn?: (message: string, data?: any) => void
26
+
27
+ /** Skip logging for certain paths. */
28
+ skip?: string[]
29
+ }
30
+
31
+ /** Default status colors (ANSI). */
32
+ const STATUS_COLORS: Record<string, string> = {
33
+ '2': '\x1b[32m', // green
34
+ '3': '\x1b[36m', // cyan
35
+ '4': '\x1b[33m', // yellow
36
+ '5': '\x1b[31m', // red
37
+ }
38
+
39
+ const RESET = '\x1b[0m'
40
+ const DIM = '\x1b[2m'
41
+
42
+ /**
43
+ * Create a request logger middleware.
44
+ */
45
+ export function loggerMiddleware(options: LoggerOptions = {}) {
46
+ const {
47
+ enabled = true,
48
+ showQuery = false,
49
+ showBody = false,
50
+ logFn = console.log,
51
+ skip = ['/health'],
52
+ } = options
53
+
54
+ if (!enabled) {
55
+ const app = new Elysia({ name: 'nexus-logger' })
56
+ return app
57
+ }
58
+
59
+ const app = new Elysia({ name: 'nexus-logger' })
60
+
61
+ // Elysia v2: use 'request' lifecycle instead of 'onRequest'
62
+ app.request((ctx: any) => {
63
+ const url = ctx.request.url
64
+ const urlObj = new URL(url)
65
+ const path = urlObj.pathname
66
+
67
+ // Skip logging for certain paths
68
+ if (skip.some((s) => path.startsWith(s))) return
69
+
70
+ const start = performance.now()
71
+ const method = ctx.request.method
72
+ const query = showQuery ? urlObj.search : ''
73
+
74
+ ctx._logStart = start
75
+ ctx._logMethod = method
76
+ ctx._logPath = path + query
77
+ })
78
+
79
+ app.afterResponse((ctx: any) => {
80
+ if (!ctx._logStart) return
81
+
82
+ const duration = Math.round((performance.now() - ctx._logStart) * 100) / 100
83
+ const status = ctx.set.status ?? 200
84
+ const statusGroup = String(status)[0]
85
+ const color = STATUS_COLORS[statusGroup] ?? ''
86
+
87
+ const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19)
88
+ const methodPad = ctx._logMethod.padEnd(7)
89
+
90
+ logFn(
91
+ `${DIM}${timestamp}${RESET} ${methodPad} ${color}${status}${RESET} ${ctx._logPath} ${DIM}${duration}ms${RESET}`
92
+ )
93
+ })
94
+
95
+ return app
96
+ }