@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
@@ -0,0 +1,54 @@
1
+ type WaitResolver = (value: { done: boolean; value?: string }) => void;
2
+
3
+ export class SSEStream {
4
+ private _queue: string[] = [];
5
+ private _waitResolver: WaitResolver | null = null;
6
+ private _ended = false;
7
+ private _error: Error | null = null;
8
+
9
+ send(data: unknown): void {
10
+ if (this._ended) return;
11
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
12
+ if (this._waitResolver) {
13
+ this._waitResolver({ done: false, value: payload });
14
+ this._waitResolver = null;
15
+ } else {
16
+ this._queue.push(payload);
17
+ }
18
+ }
19
+
20
+ end(): void {
21
+ if (this._ended) return;
22
+ this._ended = true;
23
+ if (this._waitResolver) {
24
+ this._waitResolver({ done: true });
25
+ this._waitResolver = null;
26
+ }
27
+ }
28
+
29
+ error(err: Error): void {
30
+ this._error = err;
31
+ this.end();
32
+ }
33
+
34
+ [Symbol.asyncIterator](): AsyncIterator<string> {
35
+ const self = this;
36
+ return {
37
+ async next(): Promise<IteratorResult<string>> {
38
+ if (self._ended && self._queue.length === 0) {
39
+ return { done: true, value: undefined as unknown as string };
40
+ }
41
+ if (self._queue.length > 0) {
42
+ return { done: false, value: self._queue.shift()! };
43
+ }
44
+ return new Promise<IteratorResult<string>>((resolve) => {
45
+ self._waitResolver = (val) => {
46
+ resolve(val.done
47
+ ? { done: true, value: undefined as unknown as string }
48
+ : { done: false, value: val.value! });
49
+ };
50
+ });
51
+ }
52
+ };
53
+ }
54
+ }
@@ -0,0 +1,177 @@
1
+ import { lookupMime } from './mime.ts';
2
+ import { requestStore } from './context.ts';
3
+ import type { CookieOptions, Headers, ResponseBody } from './types.ts';
4
+
5
+ const PENDING_KEY = '_tyno_response_pending';
6
+
7
+ function getPending(): { headers: Record<string, string | number | string[]>; cookies: string[] } {
8
+ const req = requestStore.getStore();
9
+ if (!req) throw new Error('No active request context');
10
+ const data = (req as unknown as Record<string, unknown>);
11
+ if (!data[PENDING_KEY]) {
12
+ data[PENDING_KEY] = { headers: {}, cookies: [] };
13
+ }
14
+ return data[PENDING_KEY] as { headers: Record<string, string | number | string[]>; cookies: string[] };
15
+ }
16
+
17
+ /**
18
+ * 可构造且可链式修改的响应对象,支持流式 body (Stream / AsyncIterator) 与 Cookie。
19
+ * 内部所有 Content-Type 键统一使用 'Content-Type'(Pascal-Case)。
20
+ */
21
+ export class Response {
22
+ status: number;
23
+ headers: Headers;
24
+ body: ResponseBody;
25
+ /** Set-Cookie 值列表 */
26
+ _cookies: string[];
27
+
28
+ constructor(status: number = 200, headers: Headers = {}, body: ResponseBody = null) {
29
+ this.status = status;
30
+ this.headers = { ...headers };
31
+ this.body = body;
32
+ this._cookies = [];
33
+ }
34
+
35
+ // ==================== 链式修改 ====================
36
+
37
+ /** 设置响应头(链式) */
38
+ set(name: string, value: string | number | string[]): this {
39
+ this.headers[name] = value;
40
+ return this;
41
+ }
42
+
43
+ /** set 的别名 */
44
+ setHeader(name: string, value: string | number | string[]): this {
45
+ return this.set(name, value);
46
+ }
47
+
48
+ /** 获取响应头(大小写不敏感) */
49
+ get(name: string): string | undefined {
50
+ if (name in this.headers) return this.headers[name] as string;
51
+ const lower = name.toLowerCase();
52
+ for (const k in this.headers) {
53
+ if (k.toLowerCase() === lower) return this.headers[k] as string;
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ /** 设置 Content-Type(支持简写如 'json'、'html'),统一使用 'Content-Type' 键 */
59
+ type(type: string): this {
60
+ this.headers['Content-Type'] = type.includes('/') ? type : lookupMime(type);
61
+ return this;
62
+ }
63
+
64
+ /** 设置状态码(链式) */
65
+ setStatus(status: number): this {
66
+ this.status = status;
67
+ return this;
68
+ }
69
+
70
+ // ==================== Cookie ====================
71
+
72
+ /** 设置 Cookie,值自动 encodeURIComponent */
73
+ setCookie(name: string, value: string, options: CookieOptions = {}): this {
74
+ const parts = [`${name}=${encodeURIComponent(value)}`];
75
+ if (options.maxAge != null) parts.push(`Max-Age=${options.maxAge}`);
76
+ if (options.expires) {
77
+ const exp = options.expires instanceof Date ? options.expires.toUTCString() : options.expires;
78
+ parts.push(`Expires=${exp}`);
79
+ }
80
+ if (options.path) parts.push(`Path=${options.path}`);
81
+ if (options.domain) parts.push(`Domain=${options.domain}`);
82
+ if (options.secure) parts.push('Secure');
83
+ if (options.httpOnly) parts.push('HttpOnly');
84
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
85
+ this._cookies.push(parts.join('; '));
86
+ return this;
87
+ }
88
+
89
+ /** 清除 Cookie(设置立即过期) */
90
+ clearCookie(name: string, options: CookieOptions = {}): this {
91
+ return this.setCookie(name, '', { ...options, maxAge: 0, expires: new Date(0) });
92
+ }
93
+
94
+ // ==================== 便利方法 ====================
95
+
96
+ /** 附加为文件下载 */
97
+ attachment(filename?: string): this {
98
+ this.headers['Content-Disposition'] = filename
99
+ ? `attachment; filename="${filename}"`
100
+ : 'attachment';
101
+ return this;
102
+ }
103
+
104
+ // ==================== 静态工厂 ====================
105
+
106
+ /** 快速创建 JSON 响应 */
107
+ static json(data: unknown, status: number = 200): Response {
108
+ return new Response(
109
+ status,
110
+ { 'Content-Type': 'application/json; charset=utf-8' },
111
+ JSON.stringify(data)
112
+ );
113
+ }
114
+
115
+ /** 快速创建纯文本响应 */
116
+ static text(data: string | Buffer, status: number = 200): Response {
117
+ const body = typeof data === 'string' ? data : String(data);
118
+ return new Response(
119
+ status,
120
+ { 'Content-Type': 'text/plain; charset=utf-8' },
121
+ body
122
+ );
123
+ }
124
+
125
+ /** 快速创建无内容响应 */
126
+ static empty(status: number = 204): Response {
127
+ return new Response(status, {}, null);
128
+ }
129
+
130
+ /** 快速创建重定向 */
131
+ static redirect(url: string, status: number = 302): Response {
132
+ return new Response(status, { Location: url }, null);
133
+ }
134
+
135
+ /** 快速创建图片响应 */
136
+ static image(data: Buffer | string, type: string = 'png', status: number = 200): Response {
137
+ const mime = type.includes('/') ? type : (lookupMime(type) || `image/${type}`);
138
+ return new Response(status, { 'Content-Type': mime }, data);
139
+ }
140
+
141
+ // ==================== 静态门面方法(预设 header / cookie) ====================
142
+
143
+ /** 预设响应头,最终响应已设置同名 header 时跳过 */
144
+ static header(name: string, value: string | number | string[]): void {
145
+ getPending().headers[name] = value;
146
+ }
147
+
148
+ /** 预设 Cookie,始终追加到最终响应 */
149
+ static setCookie(name: string, value: string, options: CookieOptions = {}): void {
150
+ const parts = [`${name}=${encodeURIComponent(value)}`];
151
+ if (options.maxAge != null) parts.push(`Max-Age=${options.maxAge}`);
152
+ if (options.expires) {
153
+ const exp = options.expires instanceof Date ? options.expires.toUTCString() : options.expires;
154
+ parts.push(`Expires=${exp}`);
155
+ }
156
+ if (options.path) parts.push(`Path=${options.path}`);
157
+ if (options.domain) parts.push(`Domain=${options.domain}`);
158
+ if (options.secure) parts.push('Secure');
159
+ if (options.httpOnly) parts.push('HttpOnly');
160
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
161
+ getPending().cookies.push(parts.join('; '));
162
+ }
163
+
164
+ /** @internal 获取 pending 数据,供 mergeResponseFacade 读取 */
165
+ static _getPending(): { headers: Record<string, string | number | string[]>; cookies: string[] } | null {
166
+ const req = requestStore.getStore();
167
+ if (!req) return null;
168
+ const data = (req as unknown as Record<string, unknown>);
169
+ const key = '_tyno_response_pending';
170
+ const pending = data[key] as { headers: Record<string, string | number | string[]>; cookies: string[] } | undefined;
171
+ if (pending && (Object.keys(pending.headers).length > 0 || pending.cookies.length > 0)) {
172
+ delete data[key];
173
+ return pending;
174
+ }
175
+ return null;
176
+ }
177
+ }
@@ -0,0 +1,290 @@
1
+ import { compose } from '../compose.ts';
2
+ import { Response } from '../response.ts';
3
+ import TrieNode, { type ParamNode } from './node.ts';
4
+ import { parsePath, type PathSegment } from './parse-path.ts';
5
+ import type { AnyMiddleware, TynoRequest, NextFunction } from '../types.ts';
6
+
7
+ interface MatchResult {
8
+ params: Record<string, string>;
9
+ handlers: AnyMiddleware[];
10
+ }
11
+
12
+ export interface RouterOptions {
13
+ prefix?: string;
14
+ }
15
+
16
+ export class Router {
17
+ prefix: string;
18
+ middlewares: AnyMiddleware[] = [];
19
+ root: TrieNode = new TrieNode();
20
+ private _fallbackHandler: AnyMiddleware | null = null;
21
+
22
+ constructor(opts: RouterOptions = {}) {
23
+ this.prefix = opts.prefix || '';
24
+ }
25
+
26
+ all(path: string, ...handlers: AnyMiddleware[]): this {
27
+ this._addRoute('*', path, handlers);
28
+ return this;
29
+ }
30
+ get(path: string, ...handlers: AnyMiddleware[]): this {
31
+ this._addRoute('get', path, handlers);
32
+ return this;
33
+ }
34
+ head(path: string, ...handlers: AnyMiddleware[]): this {
35
+ this._addRoute('head', path, handlers);
36
+ return this;
37
+ }
38
+ post(path: string, ...handlers: AnyMiddleware[]): this {
39
+ this._addRoute('post', path, handlers);
40
+ return this;
41
+ }
42
+ put(path: string, ...handlers: AnyMiddleware[]): this {
43
+ this._addRoute('put', path, handlers);
44
+ return this;
45
+ }
46
+ delete(path: string, ...handlers: AnyMiddleware[]): this {
47
+ this._addRoute('delete', path, handlers);
48
+ return this;
49
+ }
50
+ patch(path: string, ...handlers: AnyMiddleware[]): this {
51
+ this._addRoute('patch', path, handlers);
52
+ return this;
53
+ }
54
+
55
+ /**
56
+ * 注册路由级中间件。支持单个函数或函数数组,按注册顺序依次执行。
57
+ * `middleware()` 是同名别名。
58
+ *
59
+ * @example
60
+ * router.use(logger);
61
+ * router.use([auth, adminCheck]);
62
+ */
63
+ use(fn: AnyMiddleware | AnyMiddleware[]): this {
64
+ if (Array.isArray(fn)) {
65
+ for (const f of fn) this.middlewares.push(f);
66
+ } else {
67
+ this.middlewares.push(fn);
68
+ }
69
+ return this;
70
+ }
71
+
72
+ /** `use()` 的别名 */
73
+ middleware(fn: AnyMiddleware | AnyMiddleware[]): this {
74
+ return this.use(fn);
75
+ }
76
+
77
+ /**
78
+ * 路由分组:在指定前缀下批量注册路由(直接合并 trie,前缀加入段)。
79
+ *
80
+ * @example
81
+ * router.group('/admin', (r) => {
82
+ * r.get('/dashboard', () => 'Admin Dashboard');
83
+ * r.get('/users', () => 'User List');
84
+ * });
85
+ */
86
+ group(prefix: string, fn: (router: Router) => void): this {
87
+ const sub = new Router({ prefix: '' });
88
+ fn(sub);
89
+ // 合并子路由中间件
90
+ for (const mw of sub.middlewares) {
91
+ this.middlewares.push(mw);
92
+ }
93
+ // 解析前缀为段
94
+ const prefixSegments = prefix.split('/').filter(Boolean);
95
+ // 合并子路由 trie,前缀段作为父节点
96
+ for (const [seg, child] of sub.root.children) {
97
+ const fullSegments = [...prefixSegments, seg];
98
+ this._mergeTrieWithPrefix(child, fullSegments);
99
+ }
100
+ // 合并通配符
101
+ if (sub.root.wildcardNode) {
102
+ this._mergeTrieWithPrefix(sub.root.wildcardNode, [...prefixSegments, '*']);
103
+ }
104
+ // 合并根节点 handlers(无路径的请求,如仅 /admin)
105
+ for (const [method, handlers] of sub.root.handlers) {
106
+ // 在 prefix 段的叶子节点注册
107
+ this._addRouteAtPrefix(prefixSegments, method, handlers);
108
+ }
109
+ return this;
110
+ }
111
+
112
+ /** 在指定路径段末尾注册 route */
113
+ private _addRouteAtPrefix(segments: string[], method: string, handlers: AnyMiddleware[]): void {
114
+ let node = this.root;
115
+ for (const seg of segments) {
116
+ if (!node.children.has(seg)) {
117
+ node.children.set(seg, new TrieNode());
118
+ }
119
+ node = node.children.get(seg)!;
120
+ }
121
+ node.handlers.set(method, handlers);
122
+ }
123
+
124
+ /** 将子 trie 节点递归合并到当前 trie,前缀段已解析 */
125
+ private _mergeTrieWithPrefix(src: TrieNode, prefixSegments: string[]): void {
126
+ // 合并 handlers
127
+ for (const [method, handlers] of src.handlers) {
128
+ this._addRouteAtPrefix(prefixSegments, method, handlers);
129
+ }
130
+ // 合并静态子节点
131
+ for (const [seg, child] of src.children) {
132
+ this._mergeTrieWithPrefix(child, [...prefixSegments, seg]);
133
+ }
134
+ // 合并参数节点
135
+ if (src.paramNode) {
136
+ let node = this.root;
137
+ for (const seg of prefixSegments) {
138
+ if (!node.children.has(seg)) node.children.set(seg, new TrieNode());
139
+ node = node.children.get(seg)!;
140
+ }
141
+ if (node.paramNode) {
142
+ if (node.paramNode.name !== src.paramNode.name) {
143
+ throw new Error(`Param conflict: ${src.paramNode.name} vs ${node.paramNode.name}`);
144
+ }
145
+ this._mergeTrieWithPrefix(src.paramNode.node, []); // recurse into param
146
+ } else {
147
+ node.paramNode = {
148
+ name: src.paramNode.name,
149
+ pattern: src.paramNode.pattern,
150
+ regex: src.paramNode.regex,
151
+ node: new TrieNode()
152
+ };
153
+ // copy handlers from param node
154
+ for (const [m, h] of src.paramNode.node.handlers) {
155
+ node.paramNode.node.handlers.set(m, h);
156
+ }
157
+ node.paramNode.node.children = new Map(src.paramNode.node.children);
158
+ if (src.paramNode.node.wildcardNode) {
159
+ node.paramNode.node.wildcardNode = new TrieNode();
160
+ for (const [m, h] of src.paramNode.node.wildcardNode.handlers) {
161
+ node.paramNode.node.wildcardNode.handlers.set(m, h);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ // 合并通配符节点
167
+ if (src.wildcardNode) {
168
+ let node = this.root;
169
+ for (const seg of prefixSegments) {
170
+ if (!node.children.has(seg)) node.children.set(seg, new TrieNode());
171
+ node = node.children.get(seg)!;
172
+ }
173
+ if (!node.wildcardNode) node.wildcardNode = new TrieNode();
174
+ for (const [m, h] of src.wildcardNode.handlers) {
175
+ node.wildcardNode.handlers.set(m, h);
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 设置 fallback 处理器:当所有路由都不匹配时调用。
182
+ *
183
+ * @example
184
+ * router.fallback((req) => Response.json({ error: 'Not Found' }, 404));
185
+ */
186
+ fallback(handler: AnyMiddleware): this {
187
+ this._fallbackHandler = handler;
188
+ return this;
189
+ }
190
+
191
+ routes(): AnyMiddleware {
192
+ const self = this;
193
+ return async (req: TynoRequest, next?: NextFunction): Promise<unknown> => {
194
+ let pathToMatch: string = req.path;
195
+ if (self.prefix) {
196
+ if (!pathToMatch.startsWith(self.prefix)) {
197
+ if (next) return next();
198
+ return new Response(404, {}, 'Not Found');
199
+ }
200
+ pathToMatch = pathToMatch.slice(self.prefix.length) || '/';
201
+ }
202
+
203
+ const matchResult = self._match(pathToMatch, req.method.toLowerCase());
204
+ if (matchResult) {
205
+ req.params = matchResult.params;
206
+ const stack = [...self.middlewares, ...matchResult.handlers];
207
+ return compose(req, stack);
208
+ }
209
+
210
+ // 未匹配:优先 fallback,其次 next,最后 404
211
+ if (self._fallbackHandler) {
212
+ return (self._fallbackHandler as (req: TynoRequest) => unknown)(req);
213
+ }
214
+ if (next) return next();
215
+ return new Response(404, {}, 'Not Found');
216
+ };
217
+ }
218
+
219
+ private _addRoute(method: string, path: string, handlers: AnyMiddleware[]): void {
220
+ const segments = parsePath(path);
221
+ let node = this.root;
222
+ for (const seg of segments) {
223
+ if (typeof seg === 'string') {
224
+ if (!node.children.has(seg)) node.children.set(seg, new TrieNode());
225
+ node = node.children.get(seg)!;
226
+ } else if (seg.type === 'param') {
227
+ if (!node.paramNode) {
228
+ const paramNode: ParamNode = {
229
+ name: seg.name,
230
+ pattern: seg.pattern,
231
+ regex: seg.pattern ? new RegExp('^' + seg.pattern + '$') : null,
232
+ node: new TrieNode()
233
+ };
234
+ node.paramNode = paramNode;
235
+ } else {
236
+ if (node.paramNode.name !== seg.name)
237
+ throw new Error(`Param conflict: cannot use ${seg.name}, already defined as ${node.paramNode.name}`);
238
+ if (node.paramNode.pattern !== seg.pattern)
239
+ throw new Error(`Pattern conflict for param ${seg.name}: different regex`);
240
+ }
241
+ node = node.paramNode.node;
242
+ } else if (seg.type === 'wildcard') {
243
+ if (!node.wildcardNode) node.wildcardNode = new TrieNode();
244
+ node = node.wildcardNode;
245
+ break;
246
+ }
247
+ }
248
+ const methodKey = method.toLowerCase();
249
+ node.handlers.set(methodKey, handlers);
250
+ }
251
+
252
+ private _match(path: string, method: string): MatchResult | null {
253
+ const segments = path.split('/').filter(Boolean);
254
+ const result = this._matchNode(this.root, segments, 0, {}, method);
255
+ return result ? { params: result.params, handlers: result.handlers } : null;
256
+ }
257
+
258
+ private _matchNode(node: TrieNode, segments: string[], idx: number, params: Record<string, string>, method: string): MatchResult | null {
259
+ if (idx === segments.length) return this._getHandlers(node, method, params);
260
+
261
+ const current = segments[idx];
262
+ if (node.children.has(current)) {
263
+ const match = this._matchNode(node.children.get(current)!, segments, idx + 1, params, method);
264
+ if (match) return match;
265
+ }
266
+ if (node.paramNode) {
267
+ const { name, regex, node: paramNode } = node.paramNode;
268
+ if (regex) {
269
+ if (regex.test(current)) {
270
+ const match = this._matchNode(paramNode, segments, idx + 1, { ...params, [name]: current }, method);
271
+ if (match) return match;
272
+ }
273
+ } else {
274
+ const match = this._matchNode(paramNode, segments, idx + 1, { ...params, [name]: current }, method);
275
+ if (match) return match;
276
+ }
277
+ }
278
+ if (node.wildcardNode) {
279
+ const wildValue = segments.slice(idx).join('/');
280
+ return this._getHandlers(node.wildcardNode, method, { ...params, '*': wildValue });
281
+ }
282
+ return null;
283
+ }
284
+
285
+ private _getHandlers(node: TrieNode, method: string, params: Record<string, string>): MatchResult | null {
286
+ const handlers = node.handlers.get(method) || node.handlers.get('*');
287
+ if (handlers) return { params, handlers };
288
+ return null;
289
+ }
290
+ }
@@ -0,0 +1,15 @@
1
+ import type { AnyMiddleware } from '../types.ts';
2
+
3
+ export interface ParamNode {
4
+ name: string;
5
+ pattern: string | null;
6
+ regex: RegExp | null;
7
+ node: TrieNode;
8
+ }
9
+
10
+ export default class TrieNode {
11
+ children: Map<string, TrieNode> = new Map();
12
+ paramNode: ParamNode | null = null;
13
+ wildcardNode: TrieNode | null = null;
14
+ handlers: Map<string, AnyMiddleware[]> = new Map();
15
+ }
@@ -0,0 +1,18 @@
1
+ export type PathSegment = string | { type: 'param'; name: string; pattern: string | null } | { type: 'wildcard' };
2
+
3
+ export function parsePath(path: string): PathSegment[] {
4
+ const parts = path.split('/').filter(Boolean);
5
+ const segments: PathSegment[] = [];
6
+ for (const part of parts) {
7
+ if (part === '*') {
8
+ segments.push({ type: 'wildcard' });
9
+ } else if (part.startsWith(':')) {
10
+ const match = part.match(/^:([^()]+)(?:\((.+)\))?$/);
11
+ if (!match) throw new Error(`Invalid param segment: ${part}`);
12
+ segments.push({ type: 'param', name: match[1], pattern: match[2] || null });
13
+ } else {
14
+ segments.push(part);
15
+ }
16
+ }
17
+ return segments;
18
+ }
package/src/types.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { Response } from './response.ts';
3
+ import type { Request } from './request/index.ts';
4
+ import type { Application } from './application.ts';
5
+ import type { CacheConfig } from './cache/types.ts';
6
+
7
+ // ==================== Symbol 标记 ====================
8
+
9
+ /** 标记中间件为错误中间件(优先级高于 fn.length 检测) */
10
+ export const IS_ERROR_MIDDLEWARE: symbol = Symbol.for('tyno.error-middleware');
11
+
12
+ // ==================== 函数类型 ====================
13
+
14
+ /** 初始化器:在 listen() 启动服务前自动调用,接收 app 实例 */
15
+ export type InitializerFn = (app: Application) => unknown | Promise<unknown>;
16
+
17
+ /** next 函数:无参进入下游中间件,传 err 则触发错误中间件 */
18
+ export type NextFunction = (err?: unknown) => Promise<Response>;
19
+
20
+ /** 中间件请求对象:Request 实例经 Proxy 包裹,支持任意自定义属性 */
21
+ export type TynoRequest = Request & { [key: string]: unknown };
22
+
23
+ /** 普通中间件:(req, next) => any */
24
+ export type Middleware = (req: TynoRequest, next: NextFunction) => unknown | Promise<unknown>;
25
+
26
+ /** 终端中间件:(req) => any */
27
+ export type TerminalMiddleware = (req: TynoRequest) => unknown | Promise<unknown>;
28
+
29
+ /** 错误中间件:(err, req, next) => any */
30
+ export type ErrorMiddleware = (err: unknown, req: TynoRequest, next: NextFunction) => unknown | Promise<unknown>;
31
+
32
+ /** 所有中间件类型的联合 */
33
+ export type AnyMiddleware = Middleware | TerminalMiddleware | ErrorMiddleware;
34
+
35
+ // ==================== 数据类型 ====================
36
+
37
+ /** 响应体类型 */
38
+ export type ResponseBody = string | Buffer | NodeJS.ReadableStream | AsyncIterable<unknown> | null;
39
+
40
+ /** 响应头类型 */
41
+ export type Headers = Record<string, string | number | string[]>;
42
+
43
+ /** Cookie 选项 */
44
+ export interface CookieOptions {
45
+ maxAge?: number;
46
+ expires?: Date | string;
47
+ path?: string;
48
+ domain?: string;
49
+ secure?: boolean;
50
+ httpOnly?: boolean;
51
+ sameSite?: 'Strict' | 'Lax' | 'None';
52
+ }
53
+
54
+ /** Body 解析选项 */
55
+ export interface BodyOptions {
56
+ limit?: number | string;
57
+ }
58
+
59
+ /** Body 解析器配置(传入 Application 构造函数) */
60
+ export interface BodyParserConfig {
61
+ /** 普通 body(JSON / urlencoded / text)大小限制,默认 '1mb' */
62
+ limit?: number | string;
63
+ /** 文件上传总大小限制,默认 '10mb' */
64
+ uploadLimit?: number | string;
65
+ /** 文件上传临时目录,默认 os.tmpdir() */
66
+ uploadDir?: string;
67
+ /** 是否保留文件扩展名 */
68
+ keepExtensions?: boolean;
69
+ /** 文件内存缓冲阈值,超过后写入磁盘(默认 '512kb') */
70
+ uploadBufferLimit?: number | string;
71
+ }
72
+
73
+ /** listen 方法选项 */
74
+ export interface ListenOptions {
75
+ port: number;
76
+ tls?: { key: Buffer | string; cert: Buffer | string };
77
+ gracefulShutdown?: boolean;
78
+ host?: string;
79
+ }
80
+
81
+ /** Application 构造选项 */
82
+ export interface ApplicationOptions {
83
+ /** 调试模式:自动附加开发错误页中间件,显示完整堆栈 */
84
+ debug?: boolean;
85
+ /** Body 解析器配置 */
86
+ body?: BodyParserConfig;
87
+ /** 缓存配置(驱动类型、TTL、前缀等) */
88
+ cache?: CacheConfig;
89
+ }
90
+
91
+ /** inject 测试方法选项 */
92
+ export interface InjectOptions {
93
+ method?: string;
94
+ url?: string;
95
+ headers?: Record<string, string>;
96
+ body?: string | Buffer | Record<string, unknown>;
97
+ cookies?: Record<string, string>;
98
+ }
99
+
100
+ /** inject 测试方法返回值 */
101
+ export interface InjectResult {
102
+ status: number;
103
+ headers: Record<string, unknown>;
104
+ body: string;
105
+ json<T = unknown>(): T;
106
+ }
107
+
108
+ /** Application 请求处理回调 */
109
+ export type RequestHandler = (nodeReq: IncomingMessage, nodeRes: ServerResponse) => Promise<void>;