weifuwu 0.2.1 → 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.
Files changed (45) hide show
  1. package/README.md +168 -0
  2. package/dist/compress.d.ts +6 -0
  3. package/dist/cookie.d.ts +12 -0
  4. package/dist/index.d.ts +21 -0
  5. package/dist/index.js +1420 -0
  6. package/dist/middleware.d.ts +21 -0
  7. package/dist/rate-limit.d.ts +8 -0
  8. package/dist/router.d.ts +55 -0
  9. package/dist/serve.d.ts +19 -0
  10. package/dist/static.d.ts +7 -0
  11. package/dist/tsx.d.ts +17 -0
  12. package/dist/types.d.ts +9 -0
  13. package/dist/upload.d.ts +14 -0
  14. package/dist/validate.d.ts +9 -0
  15. package/package.json +14 -2
  16. package/AGENTS.md +0 -105
  17. package/compress.ts +0 -69
  18. package/cookie.ts +0 -58
  19. package/index.ts +0 -21
  20. package/middleware.ts +0 -178
  21. package/rate-limit.ts +0 -68
  22. package/router.ts +0 -702
  23. package/serve.ts +0 -126
  24. package/static.ts +0 -113
  25. package/test/compress.test.ts +0 -106
  26. package/test/cookie.test.ts +0 -79
  27. package/test/fixtures/pages/about/page.tsx +0 -3
  28. package/test/fixtures/pages/blog/[slug]/load.ts +0 -3
  29. package/test/fixtures/pages/blog/[slug]/page.tsx +0 -3
  30. package/test/fixtures/pages/blog/[slug]/route.ts +0 -7
  31. package/test/fixtures/pages/blog/layout.tsx +0 -3
  32. package/test/fixtures/pages/layout.tsx +0 -12
  33. package/test/fixtures/pages/page.tsx +0 -3
  34. package/test/middleware.test.ts +0 -407
  35. package/test/rate-limit.test.ts +0 -94
  36. package/test/static.test.ts +0 -93
  37. package/test/tsx.test.ts +0 -285
  38. package/test/unode.test.ts +0 -401
  39. package/test/upload.test.ts +0 -130
  40. package/test/validate.test.ts +0 -133
  41. package/tsconfig.json +0 -13
  42. package/tsx.ts +0 -354
  43. package/types.ts +0 -23
  44. package/upload.ts +0 -101
  45. package/validate.ts +0 -88
package/serve.ts DELETED
@@ -1,126 +0,0 @@
1
- import http, { type IncomingMessage, type ServerResponse } from 'node:http'
2
- import type { Duplex } from 'node:stream'
3
- import type { Handler } from './types.ts'
4
-
5
- export interface ServeOptions {
6
- port?: number
7
- hostname?: string
8
- signal?: AbortSignal
9
- websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void
10
- }
11
-
12
- export interface Server {
13
- stop: () => void
14
- readonly port: number
15
- readonly hostname: string
16
- ready: Promise<void>
17
- }
18
-
19
- export async function readBody(req: IncomingMessage): Promise<Buffer> {
20
- const chunks: Buffer[] = []
21
- for await (const chunk of req) {
22
- chunks.push(chunk as Buffer)
23
- }
24
- return Buffer.concat(chunks)
25
- }
26
-
27
- export function createRequest(req: IncomingMessage, body: Buffer): [Request, Record<string, string>] {
28
- const url = new URL(req.url ?? '/', 'http://localhost')
29
- const query = Object.fromEntries(url.searchParams)
30
-
31
- const headers: Record<string, string> = {}
32
- for (const [key, value] of Object.entries(req.headers)) {
33
- if (value !== undefined) {
34
- headers[key] = Array.isArray(value) ? value.join(', ') : value
35
- }
36
- }
37
-
38
- const request = new Request(url.href, {
39
- method: req.method?.toUpperCase() ?? 'GET',
40
- headers,
41
- body: (req.method !== 'GET' && req.method !== 'HEAD' && body.length > 0)
42
- ? body as BodyInit
43
- : null,
44
- })
45
-
46
- return [request, query]
47
- }
48
-
49
- export async function sendResponse(res: ServerResponse, response: Response): Promise<void> {
50
- const headers: Record<string, string | string[]> = {}
51
- response.headers.forEach((value, key) => {
52
- headers[key] = value
53
- })
54
-
55
- res.writeHead(response.status, response.statusText, headers)
56
-
57
- if (response.body) {
58
- const reader = response.body.getReader()
59
- try {
60
- while (true) {
61
- const { done, value } = await reader.read()
62
- if (done) break
63
- res.write(value)
64
- }
65
- } finally {
66
- reader.releaseLock()
67
- }
68
- }
69
-
70
- res.end()
71
- }
72
-
73
- export function serve(handler: Handler, options?: ServeOptions): Server {
74
- const port = options?.port ?? 0
75
- const hostname = options?.hostname ?? '0.0.0.0'
76
-
77
- const server = http.createServer(async (req, res) => {
78
- try {
79
- const body = await readBody(req)
80
- const [request, query] = createRequest(req, body)
81
- const response = await handler(request, { params: {}, query })
82
- await sendResponse(res, response)
83
- } catch {
84
- res.writeHead(500, { 'Content-Type': 'text/plain' })
85
- res.end('Internal Server Error')
86
- }
87
- })
88
-
89
- if (options?.websocket) {
90
- server.on('upgrade', options.websocket)
91
- }
92
-
93
- let resolveReady!: () => void
94
- const ready = new Promise<void>((r) => { resolveReady = r })
95
-
96
- if (options?.signal) {
97
- if (options.signal.aborted) {
98
- server.close()
99
- resolveReady()
100
- return {
101
- stop: () => {},
102
- ready,
103
- get port() { return 0 },
104
- get hostname() { return hostname },
105
- }
106
- }
107
- options.signal.addEventListener('abort', () => { server.close() }, { once: true })
108
- }
109
-
110
- server.listen(port, hostname, () => { resolveReady() })
111
-
112
- return {
113
- stop: () => { server.close() },
114
- ready,
115
- get port() {
116
- const addr = server.address()
117
- if (!addr || typeof addr === 'string') return 0
118
- return addr.port
119
- },
120
- get hostname() {
121
- const addr = server.address()
122
- if (!addr) return hostname
123
- return typeof addr === 'string' ? addr : addr.address
124
- },
125
- }
126
- }
package/static.ts DELETED
@@ -1,113 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
- import { open } from 'node:fs/promises'
3
- import { extname, resolve, normalize, sep } from 'node:path'
4
- import type { Handler } from './types.ts'
5
-
6
- export interface ServeStaticOptions {
7
- index?: string
8
- maxAge?: number
9
- immutable?: boolean
10
- }
11
-
12
- export function serveStatic(root: string, options?: ServeStaticOptions): Handler {
13
- const rootDir = resolve(root)
14
-
15
- const opts = options ?? {}
16
-
17
- return async (req, ctx) => {
18
- const relativePath = ctx.params['*'] ?? new URL(req.url).pathname.slice(1)
19
- const decoded = decodeURIComponent(relativePath)
20
-
21
- if (decoded.includes('..') || decoded.includes('\0')) {
22
- return new Response('Forbidden', { status: 403 })
23
- }
24
-
25
- let filePath = normalize(resolve(rootDir, decoded))
26
- if (!filePath.startsWith(rootDir + sep) && filePath !== rootDir) {
27
- return new Response('Forbidden', { status: 403 })
28
- }
29
-
30
- let fileHandle
31
- try {
32
- fileHandle = await open(filePath, 'r')
33
- const stat = await fileHandle.stat()
34
-
35
- if (stat.isDirectory()) {
36
- await fileHandle.close()
37
- const indexFile = opts.index ?? 'index.html'
38
- filePath = resolve(filePath, indexFile)
39
- if (!filePath.startsWith(rootDir + sep)) {
40
- return new Response('Forbidden', { status: 403 })
41
- }
42
- fileHandle = await open(filePath, 'r')
43
- const dirStat = await fileHandle.stat()
44
- if (!dirStat.isFile()) {
45
- await fileHandle.close()
46
- return new Response('Not Found', { status: 404 })
47
- }
48
- }
49
-
50
- const mimeType = MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream'
51
-
52
- const etag = `"${createHash('md5').update(`${stat.size}-${stat.mtimeMs}`).digest('hex')}"`
53
- const ifNoneMatch = req.headers.get('if-none-match')
54
- if (ifNoneMatch === etag) {
55
- await fileHandle.close()
56
- return new Response(null, { status: 304 })
57
- }
58
-
59
- const ifModifiedSince = req.headers.get('if-modified-since')
60
- if (ifModifiedSince && stat.mtimeMs <= new Date(ifModifiedSince).getTime()) {
61
- await fileHandle.close()
62
- return new Response(null, { status: 304 })
63
- }
64
-
65
- const headers: Record<string, string> = {
66
- 'Content-Type': mimeType,
67
- 'Content-Length': String(stat.size),
68
- 'ETag': etag,
69
- 'Last-Modified': stat.mtime.toUTCString(),
70
- 'Cache-Control': opts.immutable
71
- ? `public, max-age=${opts.maxAge ?? 31536000}, immutable`
72
- : `public, max-age=${opts.maxAge ?? 0}`,
73
- }
74
-
75
- const stream = fileHandle.readableWebStream()
76
- return new Response(stream as unknown as BodyInit, { headers })
77
- } catch (err) {
78
- if (fileHandle) await fileHandle.close().catch(() => {})
79
- if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
80
- return new Response('Not Found', { status: 404 })
81
- }
82
- return new Response('Internal Server Error', { status: 500 })
83
- }
84
- }
85
- }
86
-
87
- const MIME_TYPES: Record<string, string> = {
88
- '.html': 'text/html; charset=utf-8',
89
- '.htm': 'text/html; charset=utf-8',
90
- '.css': 'text/css; charset=utf-8',
91
- '.js': 'application/javascript; charset=utf-8',
92
- '.mjs': 'application/javascript; charset=utf-8',
93
- '.json': 'application/json',
94
- '.png': 'image/png',
95
- '.jpg': 'image/jpeg',
96
- '.jpeg': 'image/jpeg',
97
- '.gif': 'image/gif',
98
- '.svg': 'image/svg+xml',
99
- '.ico': 'image/x-icon',
100
- '.webp': 'image/webp',
101
- '.avif': 'image/avif',
102
- '.woff': 'font/woff',
103
- '.woff2': 'font/woff2',
104
- '.ttf': 'font/ttf',
105
- '.otf': 'font/otf',
106
- '.eot': 'application/vnd.ms-fontobject',
107
- '.txt': 'text/plain; charset=utf-8',
108
- '.xml': 'application/xml',
109
- '.pdf': 'application/pdf',
110
- '.zip': 'application/zip',
111
- '.wasm': 'application/wasm',
112
- '.map': 'application/json',
113
- }
@@ -1,106 +0,0 @@
1
- import { describe, it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { gunzipSync, brotliDecompressSync } from 'node:zlib'
4
- import { Router } from '../router.ts'
5
- import { compress } from '../compress.ts'
6
-
7
- describe('compress', () => {
8
- it('compresses with gzip when accepted', async () => {
9
- const r = new Router()
10
- .use(compress({ threshold: 0 }))
11
- .get('/data', () => new Response('hello '.repeat(100)))
12
-
13
- const res = await r.handler()(
14
- new Request('http://localhost/data', { headers: { 'accept-encoding': 'gzip' } }),
15
- { params: {}, query: {} },
16
- )
17
- assert.equal(res.headers.get('content-encoding'), 'gzip')
18
- const body = await res.bytes()
19
- const decoded = gunzipSync(Buffer.from(body)).toString()
20
- assert.equal(decoded, 'hello '.repeat(100))
21
- })
22
-
23
- it('compresses with brotli (preferred over gzip)', async () => {
24
- const r = new Router()
25
- .use(compress({ threshold: 0 }))
26
- .get('/data', () => new Response('hello '.repeat(100)))
27
-
28
- const res = await r.handler()(
29
- new Request('http://localhost/data', { headers: { 'accept-encoding': 'br, gzip' } }),
30
- { params: {}, query: {} },
31
- )
32
- assert.equal(res.headers.get('content-encoding'), 'br')
33
- })
34
-
35
- it('skips compression for small responses', async () => {
36
- const r = new Router()
37
- .use(compress({ threshold: 1000 }))
38
- .get('/data', () => new Response('small'))
39
-
40
- const res = await r.handler()(
41
- new Request('http://localhost/data', { headers: { 'accept-encoding': 'gzip' } }),
42
- { params: {}, query: {} },
43
- )
44
- assert.equal(res.headers.get('content-encoding'), null)
45
- })
46
-
47
- it('skips compression when Accept-Encoding is absent', async () => {
48
- const r = new Router()
49
- .use(compress({ threshold: 0 }))
50
- .get('/data', () => new Response('hello '.repeat(100)))
51
-
52
- const res = await r.handler()(
53
- new Request('http://localhost/data'),
54
- { params: {}, query: {} },
55
- )
56
- assert.equal(res.headers.get('content-encoding'), null)
57
- })
58
-
59
- it('does not compress images', async () => {
60
- const r = new Router()
61
- .use(compress({ threshold: 0 }))
62
- .get('/data', () => new Response('fakeimage', { headers: { 'content-type': 'image/png' } }))
63
-
64
- const res = await r.handler()(
65
- new Request('http://localhost/data', { headers: { 'accept-encoding': 'gzip' } }),
66
- { params: {}, query: {} },
67
- )
68
- assert.equal(res.headers.get('content-encoding'), null)
69
- })
70
-
71
- it('does not compress already encoded responses', async () => {
72
- const r = new Router()
73
- .use(compress({ threshold: 0 }))
74
- .get('/data', () => new Response('data', { headers: { 'content-encoding': 'gzip' } }))
75
-
76
- const res = await r.handler()(
77
- new Request('http://localhost/data', { headers: { 'accept-encoding': 'gzip' } }),
78
- { params: {}, query: {} },
79
- )
80
- assert.equal(res.headers.get('content-encoding'), 'gzip')
81
- })
82
-
83
- it('sets Content-Length after compression', async () => {
84
- const r = new Router()
85
- .use(compress({ threshold: 0 }))
86
- .get('/data', () => new Response('hello '.repeat(100)))
87
-
88
- const res = await r.handler()(
89
- new Request('http://localhost/data', { headers: { 'accept-encoding': 'gzip' } }),
90
- { params: {}, query: {} },
91
- )
92
- assert.ok(res.headers.get('content-length'))
93
- })
94
-
95
- it('sets Vary: Accept-Encoding', async () => {
96
- const r = new Router()
97
- .use(compress({ threshold: 0 }))
98
- .get('/data', () => new Response('hello '.repeat(100)))
99
-
100
- const res = await r.handler()(
101
- new Request('http://localhost/data', { headers: { 'accept-encoding': 'gzip' } }),
102
- { params: {}, query: {} },
103
- )
104
- assert.equal(res.headers.get('Vary'), 'Accept-Encoding')
105
- })
106
- })
@@ -1,79 +0,0 @@
1
- import { describe, it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { getCookies, setCookie, deleteCookie } from '../cookie.ts'
4
-
5
- describe('getCookies', () => {
6
- it('parses a single cookie', () => {
7
- const req = new Request('http://localhost', { headers: { cookie: 'name=value' } })
8
- assert.deepEqual(getCookies(req), { name: 'value' })
9
- })
10
-
11
- it('parses multiple cookies', () => {
12
- const req = new Request('http://localhost', { headers: { cookie: 'a=1; b=2; c=3' } })
13
- assert.deepEqual(getCookies(req), { a: '1', b: '2', c: '3' })
14
- })
15
-
16
- it('decodes URL-encoded values', () => {
17
- const req = new Request('http://localhost', { headers: { cookie: 'name=hello%20world' } })
18
- assert.deepEqual(getCookies(req), { name: 'hello world' })
19
- })
20
-
21
- it('returns empty object when no cookie header', () => {
22
- const req = new Request('http://localhost')
23
- assert.deepEqual(getCookies(req), {})
24
- })
25
-
26
- it('handles whitespace around pairs', () => {
27
- const req = new Request('http://localhost', { headers: { cookie: ' a = 1 ; b=2 ' } })
28
- assert.deepEqual(getCookies(req), { a: '1', b: '2' })
29
- })
30
- })
31
-
32
- describe('setCookie', () => {
33
- it('sets a cookie on the response', () => {
34
- const res = new Response('ok')
35
- const updated = setCookie(res, 'session', 'abc123')
36
- assert.equal(updated.headers.get('Set-Cookie'), 'session=abc123')
37
- })
38
-
39
- it('appends multiple Set-Cookie headers', () => {
40
- let res = new Response('ok')
41
- res = setCookie(res, 'a', '1')
42
- res = setCookie(res, 'b', '2')
43
- const headers = res.headers.getSetCookie?.() ?? res.headers.get('Set-Cookie')
44
- assert.ok(Array.isArray(headers) ? headers.length === 2 : true)
45
- })
46
-
47
- it('adds cookie options', () => {
48
- const res = new Response('ok')
49
- const updated = setCookie(res, 'token', 'xyz', {
50
- httpOnly: true,
51
- secure: true,
52
- sameSite: 'lax',
53
- maxAge: 3600,
54
- path: '/',
55
- })
56
- const cookie = updated.headers.get('Set-Cookie')!
57
- assert.ok(cookie.includes('HttpOnly'))
58
- assert.ok(cookie.includes('Secure'))
59
- assert.ok(cookie.includes('SameSite=lax'))
60
- assert.ok(cookie.includes('Max-Age=3600'))
61
- assert.ok(cookie.includes('Path=/'))
62
- })
63
-
64
- it('does not mutate original response', () => {
65
- const res = new Response('ok')
66
- setCookie(res, 'x', 'y')
67
- assert.equal(res.headers.get('Set-Cookie'), null)
68
- })
69
- })
70
-
71
- describe('deleteCookie', () => {
72
- it('sets Max-Age=0 to expire the cookie', () => {
73
- const res = new Response('ok')
74
- const updated = deleteCookie(res, 'session')
75
- const cookie = updated.headers.get('Set-Cookie')!
76
- assert.ok(cookie.includes('session='))
77
- assert.ok(cookie.includes('Max-Age=0'))
78
- })
79
- })
@@ -1,3 +0,0 @@
1
- export default function About() {
2
- return <h1>About</h1>
3
- }
@@ -1,3 +0,0 @@
1
- export default async function load({ params }: { params: { slug: string } }) {
2
- return { post: { title: `Post: ${params.slug}` } }
3
- }
@@ -1,3 +0,0 @@
1
- export default function Post({ post, params }: { post?: { title: string }; params: { slug: string } }) {
2
- return <article><h1>{post?.title ?? params.slug}</h1></article>
3
- }
@@ -1,7 +0,0 @@
1
- export async function GET(req: Request, ctx: { params: { slug: string } }) {
2
- return Response.json({ method: 'GET', slug: ctx.params.slug })
3
- }
4
-
5
- export async function POST(req: Request, ctx: { params: { slug: string } }) {
6
- return Response.json({ method: 'POST', slug: ctx.params.slug })
7
- }
@@ -1,3 +0,0 @@
1
- export default function BlogLayout({ children }: { children: any }) {
2
- return <div className="blog-layout">{children}</div>
3
- }
@@ -1,12 +0,0 @@
1
- export default function RootLayout({ children }: { children: any }) {
2
- return (
3
- <html>
4
- <head>
5
- <title>App</title>
6
- </head>
7
- <body>
8
- <div id="__weifuwu_root">{children}</div>
9
- </body>
10
- </html>
11
- )
12
- }
@@ -1,3 +0,0 @@
1
- export default function Home() {
2
- return <h1>Home</h1>
3
- }