weifuwu 0.2.2 → 0.2.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.md +56 -5
- package/dist/compress.d.ts +6 -0
- package/dist/cookie.d.ts +12 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1420 -0
- package/dist/middleware.d.ts +21 -0
- package/dist/rate-limit.d.ts +8 -0
- package/dist/router.d.ts +55 -0
- package/dist/serve.d.ts +19 -0
- package/dist/static.d.ts +7 -0
- package/dist/tsx.d.ts +17 -0
- package/dist/types.d.ts +9 -0
- package/dist/upload.d.ts +14 -0
- package/dist/validate.d.ts +9 -0
- package/package.json +14 -2
- package/AGENTS.md +0 -105
- package/compress.ts +0 -69
- package/cookie.ts +0 -58
- package/index.ts +0 -21
- package/middleware.ts +0 -178
- package/rate-limit.ts +0 -68
- package/router.ts +0 -701
- package/serve.ts +0 -126
- package/static.ts +0 -113
- package/test/compress.test.ts +0 -106
- package/test/cookie.test.ts +0 -79
- package/test/fixtures/pages/about/page.tsx +0 -3
- package/test/fixtures/pages/blog/[slug]/load.ts +0 -3
- package/test/fixtures/pages/blog/[slug]/page.tsx +0 -3
- package/test/fixtures/pages/blog/[slug]/route.ts +0 -7
- package/test/fixtures/pages/blog/layout.tsx +0 -3
- package/test/fixtures/pages/layout.tsx +0 -12
- package/test/fixtures/pages/page.tsx +0 -3
- package/test/middleware.test.ts +0 -407
- package/test/rate-limit.test.ts +0 -94
- package/test/static.test.ts +0 -93
- package/test/tsx.test.ts +0 -285
- package/test/unode.test.ts +0 -401
- package/test/upload.test.ts +0 -130
- package/test/validate.test.ts +0 -133
- package/tsconfig.json +0 -13
- package/tsx.ts +0 -374
- package/types.ts +0 -23
- package/upload.ts +0 -101
- package/validate.ts +0 -88
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Middleware } from './types.ts';
|
|
2
|
+
export interface LoggerOptions {
|
|
3
|
+
format?: 'short' | 'combined';
|
|
4
|
+
}
|
|
5
|
+
export declare function logger(options?: LoggerOptions): Middleware;
|
|
6
|
+
export interface CORSOptions {
|
|
7
|
+
origin?: string | string[] | ((origin: string) => string | boolean | undefined);
|
|
8
|
+
methods?: string[];
|
|
9
|
+
allowedHeaders?: string[];
|
|
10
|
+
exposedHeaders?: string[];
|
|
11
|
+
credentials?: boolean;
|
|
12
|
+
maxAge?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function cors(options?: CORSOptions): Middleware;
|
|
15
|
+
export interface AuthOptions {
|
|
16
|
+
token?: string;
|
|
17
|
+
verify?: (token: string, req: Request) => unknown | Promise<unknown>;
|
|
18
|
+
proxy?: string | URL;
|
|
19
|
+
header?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function auth(options: AuthOptions): Middleware;
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type WebSocket } from 'ws';
|
|
2
|
+
import type { IncomingMessage } from 'node:http';
|
|
3
|
+
import type { Duplex } from 'node:stream';
|
|
4
|
+
import { type GraphQLSchema } from 'graphql';
|
|
5
|
+
import { streamText } from 'ai';
|
|
6
|
+
import type { Context, Handler, Middleware, ErrorHandler } from './types.ts';
|
|
7
|
+
type StreamTextParams = Parameters<typeof streamText>[0];
|
|
8
|
+
export type WebSocketHandler = {
|
|
9
|
+
open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
|
|
10
|
+
message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>;
|
|
11
|
+
close?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
|
|
12
|
+
error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
export type AIOptions = Omit<StreamTextParams, 'model'> & {
|
|
15
|
+
model: string | (() => any);
|
|
16
|
+
};
|
|
17
|
+
export type AIHandler = (req: Request, ctx: Context) => AIOptions | Promise<AIOptions>;
|
|
18
|
+
export type GraphQLOptions = {
|
|
19
|
+
schema: string | GraphQLSchema;
|
|
20
|
+
rootValue?: any;
|
|
21
|
+
resolvers?: any;
|
|
22
|
+
context?: (req: Request, ctx: Context) => Record<string, any> | Promise<Record<string, any>>;
|
|
23
|
+
graphiql?: boolean;
|
|
24
|
+
};
|
|
25
|
+
type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
26
|
+
export declare class Router {
|
|
27
|
+
private root;
|
|
28
|
+
private wsRoot;
|
|
29
|
+
private globalMws;
|
|
30
|
+
private errorHandler?;
|
|
31
|
+
use(mw: Middleware): this;
|
|
32
|
+
use(path: string, router: Router): this;
|
|
33
|
+
use(path: string, mw: Middleware): this;
|
|
34
|
+
get(path: string, ...args: [...Middleware[], Handler]): this;
|
|
35
|
+
post(path: string, ...args: [...Middleware[], Handler]): this;
|
|
36
|
+
put(path: string, ...args: [...Middleware[], Handler]): this;
|
|
37
|
+
delete(path: string, ...args: [...Middleware[], Handler]): this;
|
|
38
|
+
patch(path: string, ...args: [...Middleware[], Handler]): this;
|
|
39
|
+
head(path: string, ...args: [...Middleware[], Handler]): this;
|
|
40
|
+
options(path: string, ...args: [...Middleware[], Handler]): this;
|
|
41
|
+
all(path: string, ...args: [...Middleware[], Handler]): this;
|
|
42
|
+
onError(handler: ErrorHandler): this;
|
|
43
|
+
route(method: string, path: string, ...args: [...Middleware[], Handler]): this;
|
|
44
|
+
ws(path: string, ...args: [...Middleware[], WebSocketHandler]): this;
|
|
45
|
+
graphql(path: string, ...args: [...Middleware[], GraphQLOptions]): this;
|
|
46
|
+
ai(path: string, ...args: [...Middleware[], AIHandler]): this;
|
|
47
|
+
handler(): Handler;
|
|
48
|
+
websocketHandler(): WsUpgradeHandler;
|
|
49
|
+
private splitPath;
|
|
50
|
+
private matchTrie;
|
|
51
|
+
private matchWsTrie;
|
|
52
|
+
private handle;
|
|
53
|
+
private runChain;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
import type { Duplex } from 'node:stream';
|
|
3
|
+
import type { Handler } from './types.ts';
|
|
4
|
+
export interface ServeOptions {
|
|
5
|
+
port?: number;
|
|
6
|
+
hostname?: string;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface Server {
|
|
11
|
+
stop: () => void;
|
|
12
|
+
readonly port: number;
|
|
13
|
+
readonly hostname: string;
|
|
14
|
+
ready: Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function readBody(req: IncomingMessage): Promise<Buffer>;
|
|
17
|
+
export declare function createRequest(req: IncomingMessage, body: Buffer): [Request, Record<string, string>];
|
|
18
|
+
export declare function sendResponse(res: ServerResponse, response: Response): Promise<void>;
|
|
19
|
+
export declare function serve(handler: Handler, options?: ServeOptions): Server;
|
package/dist/static.d.ts
ADDED
package/dist/tsx.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Router } from './router.ts';
|
|
2
|
+
export interface TsxOptions {
|
|
3
|
+
dir: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const TsxContext: import("react").Context<{
|
|
6
|
+
params: Record<string, string>;
|
|
7
|
+
query: Record<string, string>;
|
|
8
|
+
user?: unknown;
|
|
9
|
+
parsed?: Record<string, unknown>;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function useTsx(): {
|
|
12
|
+
params: Record<string, string>;
|
|
13
|
+
query: Record<string, string>;
|
|
14
|
+
user?: unknown;
|
|
15
|
+
parsed?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
export declare function tsx(options: TsxOptions): Promise<Router>;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Context {
|
|
2
|
+
params: Record<string, string>;
|
|
3
|
+
query: Record<string, string>;
|
|
4
|
+
user?: unknown;
|
|
5
|
+
parsed?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export type Handler = (req: Request, ctx: Context) => Response | Promise<Response>;
|
|
8
|
+
export type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>;
|
|
9
|
+
export type ErrorHandler = (error: Error, req: Request, ctx: Context) => Response;
|
package/dist/upload.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Middleware } from './types.ts';
|
|
2
|
+
export interface UploadedFile {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
size: number;
|
|
6
|
+
path?: string;
|
|
7
|
+
buffer?: Buffer;
|
|
8
|
+
}
|
|
9
|
+
export interface UploadOptions {
|
|
10
|
+
dir?: string;
|
|
11
|
+
maxFileSize?: number;
|
|
12
|
+
allowedTypes?: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function upload(options?: UploadOptions): Middleware;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ZodSchema } from 'zod';
|
|
2
|
+
import type { Middleware } from './types.ts';
|
|
3
|
+
export interface ValidationSchemas {
|
|
4
|
+
body?: ZodSchema;
|
|
5
|
+
query?: ZodSchema;
|
|
6
|
+
params?: ZodSchema;
|
|
7
|
+
headers?: ZodSchema;
|
|
8
|
+
}
|
|
9
|
+
export declare function validate(schemas: ValidationSchemas): Middleware;
|
package/package.json
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weifuwu",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
|
|
5
|
-
"main": "index.
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./tsx": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
6
16
|
"scripts": {
|
|
17
|
+
"build": "esbuild index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:react --external:react-dom --external:esbuild --external:graphql --external:ws --external:zod --external:@graphql-tools/schema --external:ai",
|
|
18
|
+
"prepublishOnly": "npm run build && tsc --emitDeclarationOnly --outdir dist",
|
|
7
19
|
"test": "node --test 'test/**/*.test.ts'"
|
|
8
20
|
},
|
|
9
21
|
"dependencies": {
|
package/AGENTS.md
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
This is the weifuwu HTTP framework — pure Node.js, no build step.
|
|
2
|
-
|
|
3
|
-
## Commands
|
|
4
|
-
|
|
5
|
-
- `node --test` — run all tests
|
|
6
|
-
- `npm install` — install dependencies
|
|
7
|
-
- `npx tsc --noEmit` — type-check without emitting
|
|
8
|
-
|
|
9
|
-
## TypeScript rules
|
|
10
|
-
|
|
11
|
-
- All imports must use explicit `.ts` extensions (e.g. `import { x } from './foo.ts'`)
|
|
12
|
-
- Node.js v24+ supports TypeScript natively (no `--experimental-strip-types` needed)
|
|
13
|
-
- No `tsc` compiler needed for runtime (native TS via Node.js)
|
|
14
|
-
|
|
15
|
-
## Code conventions
|
|
16
|
-
|
|
17
|
-
- Read the full file before editing — context matters
|
|
18
|
-
- Follow existing patterns: `Handler = (req, ctx) => Response | Promise<Response>`
|
|
19
|
-
- All middleware returns a `Middleware` — `(req, ctx, next) => Response | Promise<Response>`
|
|
20
|
-
- Import types from `./types.ts`, source from individual files
|
|
21
|
-
- New modules get their own file, exported from `index.ts`
|
|
22
|
-
- Every module needs tests in `test/`
|
|
23
|
-
- All `ctx` mutations (like `ctx.parsed` or `ctx.user`) should be additive, never overwrite
|
|
24
|
-
|
|
25
|
-
## Dependencies
|
|
26
|
-
|
|
27
|
-
- `ws` for WebSocket server
|
|
28
|
-
- `graphql` + `@graphql-tools/schema` for GraphQL
|
|
29
|
-
- `ai` (Vercel AI SDK) for AI streaming
|
|
30
|
-
- `zod` for request validation
|
|
31
|
-
- `react` + `react-dom` for `.tsx()` SSR + hydration
|
|
32
|
-
- `esbuild` for hydration bundle compilation
|
|
33
|
-
- Node.js built-in `WebSocket` for WebSocket clients
|
|
34
|
-
- Node.js built-in `zlib` for response compression
|
|
35
|
-
|
|
36
|
-
## tsx() — React SSR + Auto Hydration
|
|
37
|
-
|
|
38
|
-
`tsx({ dir })` — creates a Router from a React pages directory:
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
type TsxRoute = {
|
|
42
|
-
component: React.ComponentType<any>
|
|
43
|
-
props?: Record<string, any> // custom props (merged with params + query)
|
|
44
|
-
source?: string // component source path → enables hydration
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type TsxHandler = (
|
|
48
|
-
req: Request,
|
|
49
|
-
ctx: Context,
|
|
50
|
-
) => TsxRoute | Promise<TsxRoute>
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
- SSR via `react-dom/server` `renderToReadableStream`
|
|
54
|
-
- Props are serialized as `window.__WEIFUWU_PROPS` in HTML
|
|
55
|
-
- Hydration: esbuild lazily compiles source → client bundle served at `/_wfw/client/`
|
|
56
|
-
- Props passed to component: `{ ...props, params, query }` (never `req`/`ctx`)
|
|
57
|
-
|
|
58
|
-
### File conventions
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
pages/
|
|
62
|
-
page.tsx → GET / (React component, default export)
|
|
63
|
-
layout.tsx → root layout (wraps all pages)
|
|
64
|
-
about/page.tsx → GET /about
|
|
65
|
-
blog/[slug]/
|
|
66
|
-
page.tsx → GET /blog/:slug
|
|
67
|
-
load.ts → data fetching (server-only, default export)
|
|
68
|
-
route.ts → POST /blog/:slug (API, named exports GET/POST/...)
|
|
69
|
-
blog/layout.tsx → /blog/* layout (auto-wraps blog pages)
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
- `page.tsx` — default export = React component, receives `{ params, query }` + load data
|
|
73
|
-
- `load.ts` — default export = async function `({ params, query }) => props`, server-only
|
|
74
|
-
- `layout.tsx` — default export = React component with `{ children }`, auto-nested by directory level
|
|
75
|
-
- `route.ts` — named exports `GET`/`POST`/`PUT`/`DELETE`/`PATCH`, standard Handler signature
|
|
76
|
-
|
|
77
|
-
### Usage
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
import { serve, Router } from 'weifuwu'
|
|
81
|
-
import { tsx } from 'weifuwu/tsx'
|
|
82
|
-
|
|
83
|
-
const r = new Router()
|
|
84
|
-
r.use('/', await tsx({ dir: './pages/' }))
|
|
85
|
-
|
|
86
|
-
// Other features coexist
|
|
87
|
-
r.ws('/chat', { message(ws, _, data) { ws.send(data) } })
|
|
88
|
-
|
|
89
|
-
serve(r.handler())
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Testing
|
|
93
|
-
|
|
94
|
-
```ts#test/example.test.ts
|
|
95
|
-
import { describe, it } from 'node:test'
|
|
96
|
-
import assert from 'node:assert/strict'
|
|
97
|
-
|
|
98
|
-
describe('example', () => {
|
|
99
|
-
it('works', () => {
|
|
100
|
-
assert.equal(1 + 1, 2)
|
|
101
|
-
})
|
|
102
|
-
})
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Tests live in `test/` and follow the pattern: create a `Router`, call `r.handler()(request, ctx)`, assert on the response. For end-to-end tests, use `serve()`.
|
package/compress.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { gzipSync, brotliCompressSync, constants } from 'node:zlib'
|
|
2
|
-
import type { Middleware } from './types.ts'
|
|
3
|
-
|
|
4
|
-
export interface CompressOptions {
|
|
5
|
-
level?: number
|
|
6
|
-
threshold?: number
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function compress(options?: CompressOptions): Middleware {
|
|
10
|
-
const level = options?.level ?? 6
|
|
11
|
-
const threshold = options?.threshold ?? 1024
|
|
12
|
-
|
|
13
|
-
return async (req, ctx, next) => {
|
|
14
|
-
const accept = req.headers.get('accept-encoding') ?? ''
|
|
15
|
-
|
|
16
|
-
const useBrotli = accept.includes('br')
|
|
17
|
-
const useGzip = !useBrotli && accept.includes('gzip')
|
|
18
|
-
const useDeflate = !useBrotli && !useGzip && accept.includes('deflate')
|
|
19
|
-
|
|
20
|
-
if (!useBrotli && !useGzip && !useDeflate) {
|
|
21
|
-
return next(req, ctx)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const res = await next(req, ctx)
|
|
25
|
-
|
|
26
|
-
if (res.status === 304 || res.status === 204 || res.status < 200 || res.status >= 300) {
|
|
27
|
-
return res
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const ce = res.headers.get('content-encoding')
|
|
31
|
-
if (ce) return res
|
|
32
|
-
|
|
33
|
-
const ct = res.headers.get('content-type') ?? ''
|
|
34
|
-
if (!ct || ct.startsWith('audio/') || ct.startsWith('video/') || ct.startsWith('image/') || ct === 'application/zip') {
|
|
35
|
-
return res
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const body = await res.bytes()
|
|
39
|
-
if (body.byteLength < threshold) return res
|
|
40
|
-
|
|
41
|
-
let compressed: Buffer
|
|
42
|
-
let encoding: string
|
|
43
|
-
|
|
44
|
-
if (useBrotli) {
|
|
45
|
-
compressed = brotliCompressSync(body, {
|
|
46
|
-
params: { [constants.BROTLI_PARAM_QUALITY]: Math.min(level, 11) },
|
|
47
|
-
})
|
|
48
|
-
encoding = 'br'
|
|
49
|
-
} else if (useGzip) {
|
|
50
|
-
compressed = gzipSync(body, { level: Math.min(level, 9) })
|
|
51
|
-
encoding = 'gzip'
|
|
52
|
-
} else {
|
|
53
|
-
compressed = gzipSync(body, { level: Math.min(level, 9) })
|
|
54
|
-
encoding = 'deflate'
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const headers = new Headers(res.headers)
|
|
58
|
-
headers.set('Content-Encoding', encoding)
|
|
59
|
-
headers.set('Content-Length', String(compressed.byteLength))
|
|
60
|
-
headers.delete('Content-Range')
|
|
61
|
-
headers.set('Vary', 'Accept-Encoding')
|
|
62
|
-
|
|
63
|
-
return new Response(compressed as BodyInit, {
|
|
64
|
-
status: res.status,
|
|
65
|
-
statusText: res.statusText,
|
|
66
|
-
headers,
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
}
|
package/cookie.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export interface CookieOptions {
|
|
2
|
-
domain?: string
|
|
3
|
-
path?: string
|
|
4
|
-
maxAge?: number
|
|
5
|
-
expires?: Date
|
|
6
|
-
httpOnly?: boolean
|
|
7
|
-
secure?: boolean
|
|
8
|
-
sameSite?: 'strict' | 'lax' | 'none'
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getCookies(req: Request): Record<string, string> {
|
|
12
|
-
const header = req.headers.get('cookie')
|
|
13
|
-
if (!header) return {}
|
|
14
|
-
|
|
15
|
-
const cookies: Record<string, string> = {}
|
|
16
|
-
for (const pair of header.split(';')) {
|
|
17
|
-
const idx = pair.indexOf('=')
|
|
18
|
-
if (idx === -1) continue
|
|
19
|
-
const name = pair.slice(0, idx).trim()
|
|
20
|
-
const value = pair.slice(idx + 1).trim()
|
|
21
|
-
if (name) {
|
|
22
|
-
cookies[name] = decodeURIComponent(value)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return cookies
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function serializeCookie(name: string, value: string, options?: CookieOptions): string {
|
|
29
|
-
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`]
|
|
30
|
-
if (options?.maxAge != null) parts.push(`Max-Age=${options.maxAge}`)
|
|
31
|
-
if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`)
|
|
32
|
-
if (options?.domain) parts.push(`Domain=${options.domain}`)
|
|
33
|
-
if (options?.path) parts.push(`Path=${options.path}`)
|
|
34
|
-
if (options?.httpOnly) parts.push('HttpOnly')
|
|
35
|
-
if (options?.secure) parts.push('Secure')
|
|
36
|
-
if (options?.sameSite) parts.push(`SameSite=${options.sameSite}`)
|
|
37
|
-
return parts.join('; ')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function setCookie(res: Response, name: string, value: string, options?: CookieOptions): Response {
|
|
41
|
-
const headers = new Headers(res.headers)
|
|
42
|
-
headers.append('Set-Cookie', serializeCookie(name, value, options))
|
|
43
|
-
return new Response(res.body, {
|
|
44
|
-
status: res.status,
|
|
45
|
-
statusText: res.statusText,
|
|
46
|
-
headers,
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function deleteCookie(res: Response, name: string, options?: Omit<CookieOptions, 'maxAge'>): Response {
|
|
51
|
-
const headers = new Headers(res.headers)
|
|
52
|
-
headers.append('Set-Cookie', serializeCookie(name, '', { ...options, maxAge: 0 }))
|
|
53
|
-
return new Response(res.body, {
|
|
54
|
-
status: res.status,
|
|
55
|
-
statusText: res.statusText,
|
|
56
|
-
headers,
|
|
57
|
-
})
|
|
58
|
-
}
|
package/index.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export type { Context, Handler, Middleware, ErrorHandler } from './types.ts'
|
|
2
|
-
export { serve } from './serve.ts'
|
|
3
|
-
export type { ServeOptions, Server } from './serve.ts'
|
|
4
|
-
export { Router } from './router.ts'
|
|
5
|
-
export type { WebSocketHandler, GraphQLOptions, AIHandler } from './router.ts'
|
|
6
|
-
export { tsx, TsxContext, useTsx } from './tsx.ts'
|
|
7
|
-
export type { TsxOptions } from './tsx.ts'
|
|
8
|
-
export { auth, cors, logger } from './middleware.ts'
|
|
9
|
-
export type { AuthOptions, CORSOptions, LoggerOptions } from './middleware.ts'
|
|
10
|
-
export { serveStatic } from './static.ts'
|
|
11
|
-
export type { ServeStaticOptions } from './static.ts'
|
|
12
|
-
export { validate } from './validate.ts'
|
|
13
|
-
export type { ValidationSchemas } from './validate.ts'
|
|
14
|
-
export { getCookies, setCookie, deleteCookie } from './cookie.ts'
|
|
15
|
-
export type { CookieOptions } from './cookie.ts'
|
|
16
|
-
export { upload } from './upload.ts'
|
|
17
|
-
export type { UploadOptions, UploadedFile } from './upload.ts'
|
|
18
|
-
export { rateLimit } from './rate-limit.ts'
|
|
19
|
-
export type { RateLimitOptions } from './rate-limit.ts'
|
|
20
|
-
export { compress } from './compress.ts'
|
|
21
|
-
export type { CompressOptions } from './compress.ts'
|
package/middleware.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import type { Middleware } from './types.ts'
|
|
2
|
-
|
|
3
|
-
// ── Logger ────────────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
export interface LoggerOptions {
|
|
6
|
-
format?: 'short' | 'combined'
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function logger(options?: LoggerOptions): Middleware {
|
|
10
|
-
return async (req, ctx, next) => {
|
|
11
|
-
const start = Date.now()
|
|
12
|
-
const url = new URL(req.url)
|
|
13
|
-
const res = await next(req, ctx)
|
|
14
|
-
const ms = Date.now() - start
|
|
15
|
-
|
|
16
|
-
if (options?.format === 'combined') {
|
|
17
|
-
console.log(`${req.method} ${url.pathname}${url.search} ${res.status} ${ms}ms`)
|
|
18
|
-
} else {
|
|
19
|
-
console.log(`${req.method} ${url.pathname} ${res.status} ${ms}ms`)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return res
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ── CORS ──────────────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
export interface CORSOptions {
|
|
29
|
-
origin?: string | string[] | ((origin: string) => string | boolean | undefined)
|
|
30
|
-
methods?: string[]
|
|
31
|
-
allowedHeaders?: string[]
|
|
32
|
-
exposedHeaders?: string[]
|
|
33
|
-
credentials?: boolean
|
|
34
|
-
maxAge?: number
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function cors(options?: CORSOptions): Middleware {
|
|
38
|
-
const opts: Required<Pick<CORSOptions, 'origin' | 'methods' | 'allowedHeaders'>> & CORSOptions = {
|
|
39
|
-
origin: '*',
|
|
40
|
-
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
|
41
|
-
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
42
|
-
...options,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function resolveOrigin(requestOrigin: string): string {
|
|
46
|
-
if (typeof opts.origin === 'string') return opts.origin === '*' ? '*' : opts.origin
|
|
47
|
-
if (Array.isArray(opts.origin)) {
|
|
48
|
-
return opts.origin.includes(requestOrigin) ? requestOrigin : ''
|
|
49
|
-
}
|
|
50
|
-
const result = opts.origin(requestOrigin)
|
|
51
|
-
if (typeof result === 'boolean') return result ? requestOrigin : ''
|
|
52
|
-
if (typeof result === 'string') return result
|
|
53
|
-
return ''
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function setCORSHeaders(res: Response, acao: string): Response {
|
|
57
|
-
if (!acao) return res
|
|
58
|
-
const headers = new Headers(res.headers)
|
|
59
|
-
headers.set('Access-Control-Allow-Origin', acao)
|
|
60
|
-
if (opts.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
|
|
61
|
-
if (opts.exposedHeaders?.length) headers.set('Access-Control-Expose-Headers', opts.exposedHeaders.join(', '))
|
|
62
|
-
headers.set('Vary', 'Origin')
|
|
63
|
-
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return (req, ctx, next) => {
|
|
67
|
-
const requestOrigin = req.headers.get('origin') ?? ''
|
|
68
|
-
const acao = resolveOrigin(requestOrigin)
|
|
69
|
-
|
|
70
|
-
if (req.method === 'OPTIONS' && acao) {
|
|
71
|
-
const headers = new Headers()
|
|
72
|
-
headers.set('Access-Control-Allow-Origin', acao)
|
|
73
|
-
headers.set('Access-Control-Allow-Methods', opts.methods.join(', '))
|
|
74
|
-
headers.set('Access-Control-Allow-Headers', opts.allowedHeaders.join(', '))
|
|
75
|
-
if (opts.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
|
|
76
|
-
if (opts.maxAge != null) headers.set('Access-Control-Max-Age', String(opts.maxAge))
|
|
77
|
-
headers.set('Vary', 'Origin')
|
|
78
|
-
return new Response(null, { status: 204, headers })
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!acao) return next(req, ctx)
|
|
82
|
-
|
|
83
|
-
return Promise.resolve(next(req, ctx)).then((res) => setCORSHeaders(res, acao))
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── Auth ───────────────────────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
export interface AuthOptions {
|
|
90
|
-
token?: string
|
|
91
|
-
verify?: (token: string, req: Request) => unknown | Promise<unknown>
|
|
92
|
-
proxy?: string | URL
|
|
93
|
-
header?: string
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function auth(options: AuthOptions): Middleware {
|
|
97
|
-
return async (req, ctx, next) => {
|
|
98
|
-
const headerName = options.header ?? 'Authorization'
|
|
99
|
-
const header = req.headers.get(headerName)
|
|
100
|
-
|
|
101
|
-
if (!header) {
|
|
102
|
-
return new Response('Unauthorized', {
|
|
103
|
-
status: 401,
|
|
104
|
-
headers: headerName.toLowerCase() === 'authorization'
|
|
105
|
-
? { 'WWW-Authenticate': 'Bearer' }
|
|
106
|
-
: undefined,
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let token = header
|
|
111
|
-
if (headerName.toLowerCase() === 'authorization') {
|
|
112
|
-
const parts = header.split(' ')
|
|
113
|
-
if (parts[0]?.toLowerCase() === 'bearer') {
|
|
114
|
-
token = parts.slice(1).join(' ')
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── Proxy mode ──────────────────────────────────────────────────────────
|
|
119
|
-
if (options.proxy) {
|
|
120
|
-
const proxyUrl = typeof options.proxy === 'string'
|
|
121
|
-
? new URL(options.proxy)
|
|
122
|
-
: options.proxy
|
|
123
|
-
|
|
124
|
-
const proxyHeaders: Record<string, string> = {}
|
|
125
|
-
|
|
126
|
-
if (headerName.toLowerCase() === 'authorization') {
|
|
127
|
-
proxyHeaders['Authorization'] = header
|
|
128
|
-
} else {
|
|
129
|
-
proxyUrl.searchParams.set('access_token', token)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
for (const name of ['x-forwarded-for', 'x-real-ip', 'user-agent', 'content-type']) {
|
|
133
|
-
const v = req.headers.get(name)
|
|
134
|
-
if (v) proxyHeaders[name] = v
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const proxyRes = await fetch(proxyUrl.href, { headers: proxyHeaders })
|
|
138
|
-
|
|
139
|
-
if (proxyRes.status >= 400) {
|
|
140
|
-
return new Response(await proxyRes.text() || 'Forbidden', { status: proxyRes.status })
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let userData: unknown = undefined
|
|
144
|
-
if (proxyRes.status === 200) {
|
|
145
|
-
const ct = proxyRes.headers.get('content-type')
|
|
146
|
-
if (ct?.includes('application/json')) {
|
|
147
|
-
try { userData = await proxyRes.json() } catch {}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
ctx.user = userData
|
|
152
|
-
return next(req, ctx)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── Static token mode ───────────────────────────────────────────────────
|
|
156
|
-
if (options.token) {
|
|
157
|
-
if (token !== options.token) {
|
|
158
|
-
return new Response('Forbidden', { status: 403 })
|
|
159
|
-
}
|
|
160
|
-
return next(req, ctx)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ── Verify mode ─────────────────────────────────────────────────────────
|
|
164
|
-
if (options.verify) {
|
|
165
|
-
const result = await options.verify(token, req)
|
|
166
|
-
if (!result) {
|
|
167
|
-
return new Response('Forbidden', { status: 403 })
|
|
168
|
-
}
|
|
169
|
-
if (typeof result === 'object' && result !== null) {
|
|
170
|
-
ctx.user = result
|
|
171
|
-
}
|
|
172
|
-
return next(req, ctx)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ── Trust any token (no validation configured) ─────────────────────────
|
|
176
|
-
return next(req, ctx)
|
|
177
|
-
}
|
|
178
|
-
}
|