@tyno/tyno 2.1.3

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 (45) hide show
  1. package/README-zh.md +572 -0
  2. package/README.md +555 -0
  3. package/example/app.ts +173 -0
  4. package/example/public/index.html +1 -0
  5. package/example/public/test.json +1 -0
  6. package/package.json +63 -0
  7. package/scripts/build.mjs +97 -0
  8. package/scripts/rename-cjs.mjs +23 -0
  9. package/src/application.ts +304 -0
  10. package/src/cache/drivers/file.ts +79 -0
  11. package/src/cache/drivers/memory.ts +72 -0
  12. package/src/cache/drivers/redis.ts +72 -0
  13. package/src/cache/index.ts +5 -0
  14. package/src/cache/manager.ts +106 -0
  15. package/src/cache/types.ts +24 -0
  16. package/src/cache-facade.ts +64 -0
  17. package/src/compose.ts +139 -0
  18. package/src/context.ts +5 -0
  19. package/src/errors/app-error.ts +37 -0
  20. package/src/errors/http-error.ts +34 -0
  21. package/src/errors/index.ts +4 -0
  22. package/src/errors/runtime-error.ts +19 -0
  23. package/src/facade/index.ts +3 -0
  24. package/src/index.ts +29 -0
  25. package/src/middlewares/compress.ts +101 -0
  26. package/src/middlewares/cors.ts +57 -0
  27. package/src/middlewares/error-page.ts +89 -0
  28. package/src/middlewares/index.ts +9 -0
  29. package/src/middlewares/request-id.ts +47 -0
  30. package/src/middlewares/static.ts +138 -0
  31. package/src/mime.ts +38 -0
  32. package/src/request/body-parser.ts +61 -0
  33. package/src/request/index.ts +273 -0
  34. package/src/request/multipart-parser.ts +360 -0
  35. package/src/request-global.ts +31 -0
  36. package/src/response/sse.ts +54 -0
  37. package/src/response.ts +177 -0
  38. package/src/router/index.ts +290 -0
  39. package/src/router/node.ts +15 -0
  40. package/src/router/parse-path.ts +18 -0
  41. package/src/types.ts +109 -0
  42. package/test/functional.test.ts +614 -0
  43. package/tsconfig.build.json +13 -0
  44. package/tsconfig.cjs.json +9 -0
  45. package/tsconfig.json +21 -0
package/README.md ADDED
@@ -0,0 +1,555 @@
1
+ # tyno
2
+
3
+ A zero-dependency lightweight Node.js HTTP framework written in TypeScript, built on native `http` module with ES Modules. Combines Koa-style onion middleware with ThinkPHP/Laravel-style request accessors.
4
+
5
+ ```ts
6
+ import { Tyno } from '@tyno/tyno'
7
+
8
+ const app = new Tyno()
9
+ app.use((req) => `Hello ${req.query('name') || 'World'}`)
10
+ app.listen(3000)
11
+ ```
12
+
13
+ ```ts
14
+ import { Tyno } from '@tyno/tyno'
15
+ import { Router } from '@tyno/tyno/router'
16
+
17
+ const app = new Tyno()
18
+ const r = new Router()
19
+ r.get('/users/:id', (req) => ({ id: req.params.id }))
20
+ app.use(r.routes()).listen(3000)
21
+ ```
22
+
23
+ ## Features
24
+
25
+ - **Onion middleware**: auto-normalized return values, async compression
26
+ - **Trie router**: static/param `:id`/regex `:id(\d+)`/wildcard `*`, HEAD, `group()`, `fallback()`
27
+ - **Request accessors**: `query()` / `post()` / `param()` / `input()`
28
+ - **File uploads**: streaming multipart state-machine parser, buffer/disk dual mode
29
+ - **Response builder**: static factories `json/text/empty/redirect/image`, chainable `set/type/setStatus/setCookie/attachment`
30
+ - **Global facades**: `req/res` (aliases) / `request` (read inbound) / `Cache` (cache ops)
31
+ - **Error system**: `HttpError` / `RuntimeError` + error middleware + Symbol marker
32
+ - **Event system**: `request` / `response` / `response:sent` / `error` / `ready`
33
+ - **Cache**: memory/file/Redis drivers, `get/set/has/delete/clear/remember`
34
+ - **Built-in middleware**: CORS, async compression, Range static files, request ID
35
+ - **Testing**: `app.inject()` without starting a server
36
+
37
+ ## Module Structure
38
+
39
+ Package exports namespace-style, akin to PHP namespaces:
40
+
41
+ ```ts
42
+ // Main entry
43
+ import { Tyno, Response, res, Request, req } from '@tyno/tyno'
44
+
45
+ // Facades
46
+ import { request, Cache } from '@tyno/tyno/facade'
47
+
48
+ // Middleware
49
+ import { cors, compress, serveStatic, requestId } from '@tyno/tyno/middleware'
50
+
51
+ // Router
52
+ import { Router } from '@tyno/tyno/router'
53
+
54
+ // Errors
55
+ import { HttpError, NotFound, RuntimeError } from '@tyno/tyno/errors'
56
+
57
+ // Cache
58
+ import { CacheManager, MemoryDriver } from '@tyno/tyno/cache'
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Core Modules
64
+
65
+ ### Middleware
66
+
67
+ Signature: `async (req, next?) => unknown`.
68
+
69
+ ```ts
70
+ // Onion model — await next() to enter downstream
71
+ app.use(async (req, next) => {
72
+ const start = Date.now()
73
+ const res = await next()
74
+ console.log(`${req.method} ${req.path} ${res.status} ${Date.now() - start}ms`)
75
+ return res
76
+ })
77
+
78
+ // Terminal (no next)
79
+ app.use((req) => ({ hello: 'world' }))
80
+
81
+ // Error middleware (3 args or Symbol marker)
82
+ app.use(async (err, req, next) => {
83
+ return Response.json({ error: err.message }, err.status || 500)
84
+ })
85
+
86
+ // Array registration (executed in order)
87
+ app.use([logger, auth, compress])
88
+ app.middleware([cors, requestId]) // middleware === use alias
89
+ ```
90
+
91
+ **Return value auto-normalization**:
92
+
93
+ | Return Type | Conversion | Content-Type | Status |
94
+ |------------|-----------|-------------|--------|
95
+ | `Response` instance | as-is | existing | existing |
96
+ | `string` | `Response.text(str)` | `text/plain; charset=utf-8` | 200 |
97
+ | `Buffer` | `Response.text(buf)` | `text/plain; charset=utf-8` | 200 |
98
+ | `object` (plain) | `Response.json(obj)` | `application/json; charset=utf-8` | 200 |
99
+ | `number` / `boolean` | `Response.text(String(v))` | `text/plain; charset=utf-8` | 200 |
100
+ | `ReadableStream` (has `pipe`) | streamed output | none (passthrough) | 200 |
101
+ | `AsyncIterable` / Generator | SSE stream | `text/event-stream` | 200 |
102
+ | `null` / `undefined` | `Response.empty()` | none | 204 |
103
+ | Image `Buffer` | `Response.image(buf, 'png')` | `image/png` | 200 |
104
+
105
+ > Images require explicit `Response.image()` — the framework won't guess Buffer type.
106
+
107
+ ### Request
108
+
109
+ Proxy-wrapped `IncomingMessage`. Built-in props are read-only; custom props stored in `DATA_KEY`.
110
+
111
+ ```ts
112
+ // Basic info
113
+ req.method // GET / POST
114
+ req.path // without query string
115
+ req.url // with query string
116
+ req.ip // X-Forwarded-For aware
117
+ req.protocol // http / https
118
+ req.secure // is HTTPS
119
+ req.fullUrl // protocol://host/url
120
+ req.httpVersion // 1.1 / 2.0
121
+ req.host // with port
122
+ req.hostname // without port
123
+ req.userAgent // User-Agent header
124
+ req.isAjax() // X-Requested-With check
125
+ req.wantsJSON() // Accept header check
126
+
127
+ // Query
128
+ req.query() // → { id: '1', name: 'Alice' }
129
+ req.query('id') // → '1'
130
+ req.query('x', 'def') // → 'def' with default
131
+ req.get('id') // alias
132
+ req.getQuery('id') // alias
133
+
134
+ // Body (lazy parse, cached)
135
+ await req.body() // JSON / urlencoded / text auto-detect
136
+ await req.post('title') // single POST field
137
+ await req.param('id') // GET priority, fallback POST
138
+ await req.input('id', 0, parseInt) // with filter
139
+
140
+ // Headers / Cookies
141
+ req.header('authorization') // case-insensitive
142
+ req.cookies // URL-decoded
143
+ req.getCookie('sessionId')
144
+
145
+ // File uploads (streaming multipart)
146
+ await req.files() // → { fields, files }
147
+ await req.file('avatar') // → single UploadedFile | null
148
+ // UploadedFile: { fieldname, filename, mimetype, filepath, size, buffer? }
149
+ ```
150
+
151
+ **Body config**:
152
+
153
+ ```ts
154
+ const app = new Tyno({
155
+ body: {
156
+ limit: '2mb', // JSON/urlencoded/text max size
157
+ uploadLimit: '20mb', // file upload max size
158
+ uploadBufferLimit: '512kb',// memory threshold, exceed → stream to disk
159
+ uploadDir: '/tmp',
160
+ keepExtensions: true
161
+ }
162
+ })
163
+ ```
164
+
165
+ ### Response
166
+
167
+ Construction + static factories + static facade (preset headers/cookies) — one class, three roles.
168
+
169
+ **Aliases**: `Response` (class) = `response` = `res`
170
+
171
+ ```ts
172
+ import { Response, res } from '@tyno/tyno'
173
+
174
+ // —— Static factories ——
175
+ Response.json({ ok: true }) // 200 application/json; charset=utf-8
176
+ Response.json(data, 201) // custom status
177
+ Response.text('hello') // 200 text/plain; charset=utf-8
178
+ Response.empty() // 204 no Content-Type
179
+ Response.empty(201) // 201
180
+ Response.redirect('/new') // 302 Location: /new
181
+ Response.redirect('/new', 301) // 301
182
+ Response.image(buf) // 200 image/png
183
+ Response.image(buf, 'jpeg') // 200 image/jpeg
184
+ Response.image(buf, 'image/webp') // 200 image/webp
185
+ Response.image(buf, 'svg', 201) // 201 image/svg+xml
186
+
187
+ // —— Instance construction ——
188
+ new Response(200, { 'X-Custom': 'yes' }, 'body')
189
+ new Response(404, {}, 'Not Found')
190
+
191
+ // —— Chainable modifiers ——
192
+ Response.json({ ok: true })
193
+ .set('X-Request-Id', 'abc')
194
+ .type('json')
195
+ .setStatus(201)
196
+ .setCookie('token', 'xxx', { httpOnly: true, maxAge: 3600, sameSite: 'Lax' })
197
+ .clearCookie('old_session')
198
+ .attachment('report.txt') // Content-Disposition: attachment
199
+
200
+ // —— Static facade (presets, final takes priority) ——
201
+ app.use(async (req, next) => {
202
+ Response.header('X-Powered-By', 'tyno')
203
+ Response.setCookie('track', 'abc', { httpOnly: true })
204
+ return next()
205
+ })
206
+ ```
207
+
208
+ **Response output reference**:
209
+
210
+ | Factory | Content-Type | Default Status | Body Type |
211
+ |---------|-------------|----------------|-----------|
212
+ | `json(data, status?)` | `application/json; charset=utf-8` | 200 | `string` (JSON) |
213
+ | `text(data, status?)` | `text/plain; charset=utf-8` | 200 | `string` |
214
+ | `empty(status?)` | none | 204 | `null` |
215
+ | `redirect(url, status?)` | none (`Location` header) | 302 | `null` |
216
+ | `image(data, type?, status?)` | `image/*` | 200 | `Buffer \| string` |
217
+ | `new Response(s, h, b)` | manual | manual | `ResponseBody` |
218
+
219
+ | `ResponseBody` Type | Output Behavior |
220
+ |-------------------|-----------------|
221
+ | `string` | `nodeRes.end(body)` |
222
+ | `Buffer` | `nodeRes.end(body)` |
223
+ | `NodeJS.ReadableStream` | `body.pipe(nodeRes)` |
224
+ | `AsyncIterable` | SSE chunked write |
225
+ | `null` | `nodeRes.end()` |
226
+
227
+ ### Router
228
+
229
+ Trie-based, with param constraints, wildcards, grouping, and fallback.
230
+
231
+ ```ts
232
+ import { Router } from '@tyno/tyno/router'
233
+
234
+ const r = new Router({ prefix: '/api' })
235
+
236
+ // Router-level middleware (supports array, use/middleware are equivalent)
237
+ r.use(async (req, next) => {
238
+ req.user = { id: 1 }
239
+ return next()
240
+ })
241
+ r.middleware([auth, adminCheck]) // array registration
242
+
243
+ // Method registration
244
+ r.get('/users/:id(\\d+)', (req) => ({ id: req.params.id }))
245
+ r.head('/health', () => Response.empty(200))
246
+ r.post('/users', async (req) => ({ created: await req.body() }))
247
+ r.put('/users/:id', async (req) => Response.empty(204))
248
+ r.delete('/users/:id', () => Response.empty(204))
249
+ r.patch('/users/:id', async (req) => 'ok')
250
+ r.all('/any', (req) => `${req.method} matched`)
251
+ r.get('/*', (req) => ({ wild: req.params['*'] }))
252
+
253
+ // Grouping
254
+ r.group('/admin', (admin) => {
255
+ admin.get('/dashboard', () => 'Admin Panel')
256
+ admin.get('/users', () => 'User List')
257
+ // equivalent to /api/admin/dashboard, /api/admin/users
258
+ })
259
+
260
+ // Fallback — called when no route matches
261
+ r.fallback((req) => Response.json({ error: 'Not Found' }, 404))
262
+
263
+ app.use(r.routes())
264
+ ```
265
+
266
+ **Path patterns**:
267
+
268
+ | Pattern | Meaning | Match |
269
+ |---------|---------|-------|
270
+ | `users` | static segment | `/api/users` |
271
+ | `:id` | param segment | `/api/123` |
272
+ | `:id(\d+)` | regex constraint | `/api/123` |
273
+ | `*` | wildcard | `/api/a/b/c` |
274
+
275
+ Priority: **static > param > wildcard**. Methods: `get/head/post/put/delete/patch/all`.
276
+
277
+ ---
278
+
279
+ ## Errors & Events
280
+
281
+ ### Error Handling
282
+
283
+ ```ts
284
+ import { HttpError, NotFound, RuntimeError, isRuntimeError } from '@tyno/tyno/errors'
285
+
286
+ // HttpError: 4xx defaults expose=true, 5xx expose=false
287
+ throw new NotFound('Resource not found')
288
+ throw new HttpError(401, 'Please login')
289
+
290
+ // RuntimeError: defaults 500, expose=false, with cause chain
291
+ throw new RuntimeError('Database connection failed', {
292
+ code: 'DB_FAIL',
293
+ cause: new Error('ECONNREFUSED')
294
+ })
295
+ ```
296
+
297
+ **Shortcut classes**: `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `Conflict`, `PayloadTooLarge`, `TooManyRequests`, `InternalServerError`.
298
+
299
+ **Error middleware** (3+ args or `IS_ERROR_MIDDLEWARE` marker, chained in registration order):
300
+
301
+ ```ts
302
+ import { asErrorMiddleware } from '@tyno/tyno'
303
+
304
+ app.use(async (err, req, next) => {
305
+ if (isRuntimeError(err)) {
306
+ return Response.json({ error: 'Service unavailable', code: err.code }, 500)
307
+ }
308
+ return Response.json({ error: err.message }, err.status || 500)
309
+ })
310
+ ```
311
+
312
+ ### Event System
313
+
314
+ Application extends `EventEmitter`. Full lifecycle:
315
+
316
+ | Event | Triggers | Args |
317
+ |-------|---------|------|
318
+ | `request` | request arrives, before compose, AsyncLocalStorage ready | `(req)` |
319
+ | `response` | compose completes, before sending | `(req, res)` |
320
+ | `response:sent` | after response sent | `(req, res)` |
321
+ | `error` | error occurs (only with listeners) | `(err, req)` |
322
+ | `ready` | server listening | `()` |
323
+
324
+ ```ts
325
+ import { Tyno } from '@tyno/tyno'
326
+ const app = new Tyno({ debug: true })
327
+
328
+ app.on('request', (req) => console.log(`→ ${req.method} ${req.path}`))
329
+ app.on('response', (req, res) => console.log(`← ${req.path} ${res.status}`))
330
+ app.on('response:sent', (req, res) => console.log(`✓ ${req.path} ${res.status} done`))
331
+ app.on('error', (err, req) => logger.error({ err, path: req.path }))
332
+ app.on('ready', () => console.log('Server ready'))
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Built-in Middleware
338
+
339
+ ```ts
340
+ import { cors, compress, serveStatic, requestId } from '@tyno/tyno/middleware'
341
+
342
+ // CORS
343
+ app.use(cors())
344
+ app.use(cors({ origin: ['https://a.com'], credentials: true, maxAge: 86400 }))
345
+
346
+ // Async compression (gzip/deflate), supports streaming, skips images/video
347
+ app.use(compress())
348
+ app.use(compress({ threshold: 2048, level: 6 }))
349
+
350
+ // Static files: ETag/304, Range/206, path traversal protection
351
+ app.use(serveStatic('./public', { prefix: '/static', maxAge: 3600 }))
352
+
353
+ // Request ID: auto-generate UUID, inject req.requestId, write response header
354
+ app.use(requestId())
355
+ app.use(requestId({ readFromHeader: false }))
356
+
357
+ // Dev error page — auto-attached when debug:true
358
+ new Tyno({ debug: true }) // shows stack + cause, JSON/HTML dual format
359
+ ```
360
+
361
+ ---
362
+
363
+ ## Cache
364
+
365
+ Memory / File / Redis drivers, unified `get/set/has/delete/clear/remember` API.
366
+
367
+ **Alias**: `Cache` (uppercase) = `cache` (lowercase)
368
+
369
+ ```ts
370
+ import { Cache } from '@tyno/tyno/facade'
371
+
372
+ const app = new Tyno({ cache: { driver: 'memory', prefix: 'my:', ttl: 3600 } })
373
+
374
+ // via app
375
+ await app.cache().set('user:1', { name: 'Alice' }, 60)
376
+ const user = await app.cache().get('user:1')
377
+
378
+ // remember — calls factory on cache miss
379
+ const config = await app.cache().remember('config', 3600, loadConfig)
380
+
381
+ // Global facade (inside middleware)
382
+ app.use(async () => {
383
+ await Cache.set('key', 'value')
384
+ return Response.json({ ok: true })
385
+ })
386
+ ```
387
+
388
+ **File driver**: `{ driver: 'file', file: { path: './storage/cache' } }`
389
+
390
+ **Redis driver**: requires `npm install redis`, `{ driver: 'redis', redis: { host, port } }`
391
+
392
+ ---
393
+
394
+ ## Other
395
+
396
+ ### Initializers
397
+
398
+ ```ts
399
+ app.initialize(async (app) => { await db.connect() })
400
+ app.listen(4567) // runs all initializers first
401
+ ```
402
+
403
+ ### HTTPS
404
+
405
+ ```ts
406
+ app.listen({ port: 443, tls: { key, cert } })
407
+ ```
408
+
409
+ ### Testing
410
+
411
+ ```ts
412
+ const res = await app.inject({ method: 'GET', url: '/?name=alice' })
413
+ res.status // 200
414
+ res.json() // { hello: 'alice' }
415
+ ```
416
+
417
+ ### SSE
418
+
419
+ ```ts
420
+ async function* gen() { yield 'chunk1'; yield 'chunk2' }
421
+ return gen()
422
+ ```
423
+
424
+ ---
425
+
426
+ ## API Reference
427
+
428
+ ### Application
429
+
430
+ | Method | Description |
431
+ |--------|-------------|
432
+ | `new Tyno({ debug?, body?, cache? })` | Create app |
433
+ | `use(fn\|[...fn])` / `middleware(fn\|[...fn])` | Register middleware, supports array |
434
+ | `initialize(fn)` | Register initializer |
435
+ | `listen(port\|opts)` | Start HTTP/HTTPS, emits `ready` |
436
+ | `close()` | Graceful shutdown |
437
+ | `inject(opts)` | Test request |
438
+ | `cache()` | Cache instance |
439
+ | `on/emit/once` | EventEmitter methods |
440
+
441
+ ### Router
442
+
443
+ | Method | Description |
444
+ |--------|-------------|
445
+ | `new Router({ prefix? })` | Create router |
446
+ | `use(fn\|[...fn])` / `middleware(fn\|[...fn])` | Router-level middleware, supports array |
447
+ | `get/head/post/put/delete/patch/all(path, ...h)` | Register route |
448
+ | `group(prefix, fn)` | Route grouping |
449
+ | `fallback(handler)` | No-match handler |
450
+ | `routes()` | Return middleware function |
451
+
452
+ ### Request
453
+
454
+ | Alias | Export Source |
455
+ |-------|--------------|
456
+ | `Request` (class) = `req` | `tyno` |
457
+ | `request` (global facade) | `tyno/facade` |
458
+
459
+ | Method | Description |
460
+ |--------|-------------|
461
+ | `query()/query(k)/get(k)` | Query params |
462
+ | `body()/post/param/input` | Body accessors |
463
+ | `files()/file(name)` | File upload |
464
+ | `header/cookies/getCookie` | Request headers |
465
+
466
+ ### Response
467
+
468
+ | Alias | Export Source |
469
+ |-------|--------------|
470
+ | `Response` (class) = `response` = `res` | `tyno` |
471
+
472
+ | Static Factory | Content-Type | Status |
473
+ |---------------|-------------|--------|
474
+ | `json(data, status?)` | `application/json` | 200 |
475
+ | `text(data, status?)` | `text/plain` | 200 |
476
+ | `empty(status?)` | — | 204 |
477
+ | `redirect(url, status?)` | `Location` header | 302 |
478
+ | `image(data, type?, status?)` | `image/*` | 200 |
479
+
480
+ | Instance/Static Methods | Description |
481
+ |------------------------|-------------|
482
+ | `set/setHeader/type/setStatus` | Chainable modifiers |
483
+ | `setCookie/clearCookie/attachment` | Cookies + download |
484
+ | `get(name)` | Read response header |
485
+ | `static header(name, value)` | Preset header (facade) |
486
+ | `static setCookie(name, value, options?)` | Preset cookie (facade) |
487
+
488
+ ### Error Classes
489
+
490
+ | Class | Description |
491
+ |-------|-------------|
492
+ | `AppError` | Base class |
493
+ | `HttpError(status, msg?, props?)` | HTTP error |
494
+ | `RuntimeError(msg, opts?)` | Runtime error |
495
+ | `NotFound/Forbidden/...` | Shortcut subclasses |
496
+
497
+ ### Global Facades
498
+
499
+ | Export | Source | Description |
500
+ |--------|--------|-------------|
501
+ | `req` | `tyno` | Request class alias |
502
+ | `res` | `tyno` | Response class alias |
503
+ | `request` | `tyno/facade` | Read current request |
504
+ | `Cache / cache` | `tyno/facade` | Cache operations |
505
+
506
+ ---
507
+
508
+ ## Project Structure
509
+
510
+ ```
511
+ tyno/
512
+ ├── src/
513
+ │ ├── index.ts # Main barrel
514
+ │ ├── application.ts # Application (EventEmitter)
515
+ │ ├── compose.ts # Onion model + error handling
516
+ │ ├── context.ts # AsyncLocalStorage
517
+ │ ├── types.ts # Type definitions
518
+ │ ├── response.ts # Response class
519
+ │ ├── response/sse.ts # SSEStream
520
+ │ ├── request/
521
+ │ │ ├── index.ts # Request class
522
+ │ │ ├── body-parser.ts # BodyParser
523
+ │ │ └── multipart-parser.ts # Streaming multipart
524
+ │ ├── router/
525
+ │ │ ├── index.ts # Router + group + fallback
526
+ │ │ ├── node.ts # TrieNode
527
+ │ │ └── parse-path.ts # Path parser
528
+ │ ├── errors/
529
+ │ │ ├── app-error.ts # AppError
530
+ │ │ ├── http-error.ts # HttpError
531
+ │ │ └── runtime-error.ts # RuntimeError
532
+ │ ├── middlewares/
533
+ │ │ ├── index.ts # Subpath barrel
534
+ │ │ ├── cors.ts # CORS
535
+ │ │ ├── compress.ts # Async compression
536
+ │ │ ├── static.ts # Static files + Range
537
+ │ │ ├── request-id.ts # Request ID
538
+ │ │ └── error-page.ts # Dev error page
539
+ │ ├── cache/
540
+ │ │ ├── index.ts / manager.ts / types.ts
541
+ │ │ └── drivers/ (memory / file / redis)
542
+ │ ├── facade/ # Facade subpath barrel
543
+ │ ├── request-global.ts # Global request facade
544
+ │ ├── cache-facade.ts # Global cache facade
545
+ │ └── mime.ts
546
+ ├── example/app.ts
547
+ ├── test/functional.test.ts
548
+ ├── scripts/build.mjs # Dual-format build script
549
+ ├── package.json
550
+ └── tsconfig.json
551
+ ```
552
+
553
+ ## License
554
+
555
+ MIT