@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.
- package/README-zh.md +572 -0
- package/README.md +555 -0
- package/example/app.ts +173 -0
- package/example/public/index.html +1 -0
- package/example/public/test.json +1 -0
- package/package.json +63 -0
- package/scripts/build.mjs +97 -0
- package/scripts/rename-cjs.mjs +23 -0
- package/src/application.ts +304 -0
- package/src/cache/drivers/file.ts +79 -0
- package/src/cache/drivers/memory.ts +72 -0
- package/src/cache/drivers/redis.ts +72 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/manager.ts +106 -0
- package/src/cache/types.ts +24 -0
- package/src/cache-facade.ts +64 -0
- package/src/compose.ts +139 -0
- package/src/context.ts +5 -0
- package/src/errors/app-error.ts +37 -0
- package/src/errors/http-error.ts +34 -0
- package/src/errors/index.ts +4 -0
- package/src/errors/runtime-error.ts +19 -0
- package/src/facade/index.ts +3 -0
- package/src/index.ts +29 -0
- package/src/middlewares/compress.ts +101 -0
- package/src/middlewares/cors.ts +57 -0
- package/src/middlewares/error-page.ts +89 -0
- package/src/middlewares/index.ts +9 -0
- package/src/middlewares/request-id.ts +47 -0
- package/src/middlewares/static.ts +138 -0
- package/src/mime.ts +38 -0
- package/src/request/body-parser.ts +61 -0
- package/src/request/index.ts +273 -0
- package/src/request/multipart-parser.ts +360 -0
- package/src/request-global.ts +31 -0
- package/src/response/sse.ts +54 -0
- package/src/response.ts +177 -0
- package/src/router/index.ts +290 -0
- package/src/router/node.ts +15 -0
- package/src/router/parse-path.ts +18 -0
- package/src/types.ts +109 -0
- package/test/functional.test.ts +614 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.cjs.json +9 -0
- 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
|