weifuwu 0.2.2 → 0.2.4
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 +90 -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 +1486 -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
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
|
-
}
|
package/test/compress.test.ts
DELETED
|
@@ -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
|
-
})
|
package/test/cookie.test.ts
DELETED
|
@@ -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,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
|
-
}
|