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.
Files changed (45) hide show
  1. package/README.md +56 -5
  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 -701
  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 -374
  43. package/types.ts +0 -23
  44. package/upload.ts +0 -101
  45. package/validate.ts +0 -88
package/test/tsx.test.ts DELETED
@@ -1,285 +0,0 @@
1
- import { describe, it, before, after } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { tsx } from '../tsx.ts'
4
- import { serve, Router } from '../index.ts'
5
- import type { Server } from '../serve.ts'
6
-
7
- const fixtures = './test/fixtures/pages'
8
-
9
- describe('tsx()', () => {
10
- describe('SSR rendering', () => {
11
- it('renders root page', async () => {
12
- const r = await tsx({ dir: fixtures })
13
- const res = await r.handler()(
14
- new Request('http://localhost/'),
15
- { params: {}, query: {} },
16
- )
17
- assert.equal(res.status, 200)
18
-
19
- const html = await res.text()
20
- assert.match(html, /<!DOCTYPE html>/)
21
- assert.match(html, /<h1>Home<\/h1>/)
22
- })
23
-
24
- it('renders nested static page', async () => {
25
- const r = await tsx({ dir: fixtures })
26
- const res = await r.handler()(
27
- new Request('http://localhost/about'),
28
- { params: {}, query: {} },
29
- )
30
- assert.equal(res.status, 200)
31
-
32
- const html = await res.text()
33
- assert.match(html, /<h1>About<\/h1>/)
34
- })
35
-
36
- it('renders dynamic route with params', async () => {
37
- const r = await tsx({ dir: fixtures })
38
- const res = await r.handler()(
39
- new Request('http://localhost/blog/hello-world'),
40
- { params: { slug: 'hello-world' }, query: {} },
41
- )
42
- assert.equal(res.status, 200)
43
-
44
- const html = await res.text()
45
- assert.match(html, /hello-world/)
46
- })
47
-
48
- it('passes query params', async () => {
49
- const r = await tsx({ dir: fixtures })
50
- const res = await r.handler()(
51
- new Request('http://localhost/?foo=bar'),
52
- { params: {}, query: { foo: 'bar' } },
53
- )
54
- assert.equal(res.status, 200)
55
- assert.equal((await res.text()).includes('Home'), true)
56
- })
57
-
58
- it('returns 404 for non-existent route', async () => {
59
- const r = await tsx({ dir: fixtures })
60
- const res = await r.handler()(
61
- new Request('http://localhost/nonexistent'),
62
- { params: {}, query: {} },
63
- )
64
- assert.equal(res.status, 404)
65
- })
66
- })
67
-
68
- describe('data loading (load.ts)', () => {
69
- it('calls load() and passes data as props', async () => {
70
- const r = await tsx({ dir: fixtures })
71
- const res = await r.handler()(
72
- new Request('http://localhost/blog/my-post'),
73
- { params: { slug: 'my-post' }, query: {} },
74
- )
75
- assert.equal(res.status, 200)
76
-
77
- const html = await res.text()
78
- assert.match(html, /Post: my-post/)
79
- })
80
-
81
- it('serializes props to __WEIFUWU_PROPS', async () => {
82
- const r = await tsx({ dir: fixtures })
83
- const res = await r.handler()(
84
- new Request('http://localhost/blog/my-post'),
85
- { params: { slug: 'my-post' }, query: {} },
86
- )
87
- const html = await res.text()
88
- assert.match(html, /__WEIFUWU_PROPS/)
89
- assert.match(html, /Post: my-post/)
90
- })
91
- })
92
-
93
- describe('layout chain', () => {
94
- it('wraps page with root layout', async () => {
95
- const r = await tsx({ dir: fixtures })
96
- const res = await r.handler()(
97
- new Request('http://localhost/'),
98
- { params: {}, query: {} },
99
- )
100
-
101
- const html = await res.text()
102
- assert.match(html, /<html>/)
103
- assert.match(html, /<title>App<\/title>/)
104
- assert.match(html, /__weifuwu_root/)
105
- })
106
-
107
- it('wraps blog pages with nested layouts', async () => {
108
- const r = await tsx({ dir: fixtures })
109
- const res = await r.handler()(
110
- new Request('http://localhost/blog/some-post'),
111
- { params: { slug: 'some-post' }, query: {} },
112
- )
113
-
114
- const html = await res.text()
115
- assert.match(html, /blog-layout/)
116
- })
117
- })
118
-
119
- describe('route.ts API', () => {
120
- it('GET returns SSR page, not route.ts', async () => {
121
- const r = await tsx({ dir: fixtures })
122
- const res = await r.handler()(
123
- new Request('http://localhost/blog/my-route'),
124
- { params: { slug: 'my-route' }, query: {} },
125
- )
126
- // GET is handled by page.tsx SSR, not route.ts
127
- const html = await res.text()
128
- assert.match(html, /<!DOCTYPE html>/)
129
- assert.match(html, /my-route/)
130
- })
131
-
132
- it('handles POST from route.ts', async () => {
133
- const r = await tsx({ dir: fixtures })
134
- const res = await r.handler()(
135
- new Request('http://localhost/blog/my-route', { method: 'POST' }),
136
- { params: { slug: 'my-route' }, query: {} },
137
- )
138
-
139
- const data = await res.json() as any
140
- assert.equal(data.method, 'POST')
141
- assert.equal(data.slug, 'my-route')
142
- })
143
- })
144
-
145
- describe('hydration', () => {
146
- it('injects hydration scripts', async () => {
147
- const r = await tsx({ dir: fixtures })
148
- const res = await r.handler()(
149
- new Request('http://localhost/'),
150
- { params: {}, query: {} },
151
- )
152
-
153
- const html = await res.text()
154
- assert.match(html, /<script type="module"/)
155
- assert.match(html, /__wfw\/client\//)
156
- })
157
-
158
- it('serves hydration bundle', async () => {
159
- const r = await tsx({ dir: fixtures })
160
- const res1 = await r.handler()(
161
- new Request('http://localhost/'),
162
- { params: {}, query: {} },
163
- )
164
- const html = await res1.text()
165
- const match = html.match(/src="(\/__wfw\/client\/[^"]+)"/)
166
- assert.ok(match)
167
-
168
- const res2 = await r.handler()(
169
- new Request(`http://localhost${match[1]}`),
170
- { params: {}, query: {} },
171
- )
172
- assert.equal(res2.status, 200)
173
- const js = await res2.text()
174
- assert.match(js, /hydrateRoot/)
175
- })
176
- })
177
-
178
- describe('mounting via router.use()', () => {
179
- it('works as sub-router', async () => {
180
- const pages = await tsx({ dir: fixtures })
181
- const main = new Router()
182
- main.use('/app', pages)
183
-
184
- const res = await main.handler()(
185
- new Request('http://localhost/app/about'),
186
- { params: {}, query: {} },
187
- )
188
- assert.equal(res.status, 200)
189
- const html = await res.text()
190
- assert.match(html, /<h1>About<\/h1>/)
191
- })
192
-
193
- it('coexists with other Router features', async () => {
194
- const pages = await tsx({ dir: fixtures })
195
- const main = new Router()
196
- main.use('/app', pages)
197
- main.get('/api/ping', () => Response.json({ ok: true }))
198
-
199
- const res1 = await main.handler()(
200
- new Request('http://localhost/app/about'),
201
- { params: {}, query: {} },
202
- )
203
- assert.equal(res1.status, 200)
204
- const html = await res1.text()
205
- assert.match(html, /<h1>About<\/h1>/)
206
-
207
- const res2 = await main.handler()(
208
- new Request('http://localhost/api/ping'),
209
- { params: {}, query: {} },
210
- )
211
- const data = await res2.json() as any
212
- assert.equal(data.ok, true)
213
- })
214
- })
215
-
216
- describe('end-to-end via serve()', () => {
217
- let server: Server
218
- let url: string
219
-
220
- before(async () => {
221
- const pages = await tsx({ dir: fixtures })
222
- server = serve(pages.handler(), { port: 0 })
223
- await server.ready
224
- url = `http://localhost:${server.port}`
225
- })
226
-
227
- after(() => server.stop())
228
-
229
- it('serves page via HTTP', async () => {
230
- const res = await fetch(`${url}/about`)
231
- assert.equal(res.status, 200)
232
- const html = await res.text()
233
- assert.match(html, /<h1>About<\/h1>/)
234
- })
235
-
236
- it('serves hydration bundle via HTTP', async () => {
237
- const res = await fetch(`${url}/`)
238
- const html = await res.text()
239
- const match = html.match(/src="(\/__wfw\/client\/[^"]+)"/)
240
- assert.ok(match)
241
-
242
- const bundleRes = await fetch(`${url}${match[1]}`)
243
- assert.equal(bundleRes.status, 200)
244
- const js = await bundleRes.text()
245
- assert.match(js, /hydrateRoot/)
246
- })
247
-
248
- it('serves dynamic route and API', async () => {
249
- const res = await fetch(`${url}/blog/test-article`)
250
- assert.equal(res.status, 200)
251
- const html = await res.text()
252
- assert.match(html, /Post: test-article/)
253
- })
254
- })
255
-
256
- describe('edge cases', () => {
257
- it('non-existent directory returns empty router', async () => {
258
- const r = await tsx({ dir: './test/fixtures/nonexistent' })
259
- const res = await r.handler()(
260
- new Request('http://localhost/'),
261
- { params: {}, query: {} },
262
- )
263
- assert.equal(res.status, 404)
264
- })
265
-
266
- it('empty directory returns empty router', async () => {
267
- const r = await tsx({ dir: './test/fixtures/empty' })
268
- const res = await r.handler()(
269
- new Request('http://localhost/'),
270
- { params: {}, query: {} },
271
- )
272
- assert.equal(res.status, 404)
273
- })
274
-
275
- it('handles page.ts without tsx extension', async () => {
276
- // about/ has page.tsx only, but scanPages also checks page.ts
277
- const r = await tsx({ dir: fixtures })
278
- const res = await r.handler()(
279
- new Request('http://localhost/about'),
280
- { params: {}, query: {} },
281
- )
282
- assert.equal(res.status, 200)
283
- })
284
- })
285
- })
@@ -1,401 +0,0 @@
1
- import { describe, it, before, after } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { WebSocket } from 'ws'
4
- import { serve, Router, type Handler, type Server } from '../index.ts'
5
-
6
- async function createTestServer(handler: Handler): Promise<{ server: Server; url: string }> {
7
- const server = serve(handler, { port: 0 })
8
- await server.ready
9
- return { server, url: `http://localhost:${server.port}` }
10
- }
11
-
12
- // ── serve ────────────────────────────────────────────────────────────────────
13
-
14
- describe('serve', () => {
15
- it('handles GET request', async () => {
16
- const { server, url } = await createTestServer(() => new Response('hello'))
17
- const res = await fetch(url)
18
- assert.equal(res.status, 200)
19
- assert.equal(await res.text(), 'hello')
20
- server.stop()
21
- })
22
-
23
- it('handles POST with body echo', async () => {
24
- const { server, url } = await createTestServer(async (req) => {
25
- const body = await req.text()
26
- return new Response(body, { status: 201 })
27
- })
28
- const res = await fetch(url, { method: 'POST', body: 'test data' })
29
- assert.equal(res.status, 201)
30
- assert.equal(await res.text(), 'test data')
31
- server.stop()
32
- })
33
-
34
- it('passes response headers through', async () => {
35
- const { server, url } = await createTestServer(() =>
36
- new Response('ok', { headers: { 'x-custom': 'value', 'content-type': 'text/plain' } }),
37
- )
38
- const res = await fetch(url)
39
- assert.equal(res.headers.get('x-custom'), 'value')
40
- assert.equal(res.headers.get('content-type'), 'text/plain')
41
- server.stop()
42
- })
43
-
44
- it('provides ctx.query from URL', async () => {
45
- const { server, url } = await createTestServer((req, ctx) =>
46
- Response.json(ctx.query),
47
- )
48
- const res = await fetch(`${url}?foo=bar&baz=qux`)
49
- const data = await res.json() as Record<string, string>
50
- assert.equal(data.foo, 'bar')
51
- assert.equal(data.baz, 'qux')
52
- server.stop()
53
- })
54
-
55
- it('returns 500 on handler error', async () => {
56
- const { server, url } = await createTestServer(() => {
57
- throw new Error('boom')
58
- })
59
- const res = await fetch(url)
60
- assert.equal(res.status, 500)
61
- assert.match(await res.text(), /Internal Server Error/)
62
- server.stop()
63
- })
64
-
65
- it('server.stop() closes the server', async () => {
66
- const server = serve(() => new Response('ok'), { port: 0 })
67
- await server.ready
68
- const port = server.port
69
- server.stop()
70
- await assert.rejects(() => fetch(`http://localhost:${port}`))
71
- })
72
-
73
- it('AbortSignal prevents server from starting', async () => {
74
- const ac = new AbortController()
75
- ac.abort()
76
- const server = serve(() => new Response('ok'), { port: 0, signal: ac.signal })
77
- await server.ready
78
- assert.equal(server.port, 0)
79
- server.stop()
80
- })
81
- })
82
-
83
- // ── Router ────────────────────────────────────────────────────────────────────
84
-
85
- describe('Router', () => {
86
- it('matches GET route', async () => {
87
- const r = new Router().get('/hello', () => new Response('world'))
88
- const res = await r.handler()(new Request('http://localhost/hello'), { params: {}, query: {} })
89
- assert.equal(res.status, 200)
90
- assert.equal(await res.text(), 'world')
91
- })
92
-
93
- it('matches POST route', async () => {
94
- const r = new Router().post('/data', async (req) => {
95
- const body = await req.text()
96
- return new Response(body, { status: 201 })
97
- })
98
- const res = await r.handler()(new Request('http://localhost/data', { method: 'POST', body: 'hello' }), { params: {}, query: {} })
99
- assert.equal(res.status, 201)
100
- assert.equal(await res.text(), 'hello')
101
- })
102
-
103
- it('provides ctx.params', async () => {
104
- const r = new Router().get('/users/:id', (req, ctx) =>
105
- Response.json({ id: ctx.params.id }),
106
- )
107
- const res = await r.handler()(new Request('http://localhost/users/42'), { params: {}, query: {} })
108
- const data = await res.json() as Record<string, string>
109
- assert.equal(data.id, '42')
110
- })
111
-
112
- it('multiple params', async () => {
113
- const r = new Router().get('/:a/:b', (req, ctx) =>
114
- Response.json({ a: ctx.params.a, b: ctx.params.b }),
115
- )
116
- const res = await r.handler()(new Request('http://localhost/foo/bar'), { params: {}, query: {} })
117
- const data = await res.json() as Record<string, string>
118
- assert.equal(data.a, 'foo')
119
- assert.equal(data.b, 'bar')
120
- })
121
-
122
- it('static route wins over param route', async () => {
123
- const r = new Router()
124
- .get('/users/me', () => new Response('me'))
125
- .get('/users/:id', (req, ctx) => new Response(ctx.params.id))
126
- const res1 = await r.handler()(new Request('http://localhost/users/me'), { params: {}, query: {} })
127
- assert.equal(await res1.text(), 'me')
128
- const res2 = await r.handler()(new Request('http://localhost/users/42'), { params: {}, query: {} })
129
- assert.equal(await res2.text(), '42')
130
- })
131
-
132
- it('wildcard matches remaining path', async () => {
133
- const r = new Router().all('/api/*', (req, ctx) =>
134
- Response.json({ wildcard: ctx.params['*'] }),
135
- )
136
- const res = await r.handler()(new Request('http://localhost/api/foo/bar'), { params: {}, query: {} })
137
- const data = await res.json() as Record<string, string>
138
- assert.equal(data.wildcard, 'foo/bar')
139
- })
140
-
141
- it('returns 404 for unmatched route', async () => {
142
- const r = new Router().get('/exists', () => new Response('ok'))
143
- const res = await r.handler()(new Request('http://localhost/nonexistent'), { params: {}, query: {} })
144
- assert.equal(res.status, 404)
145
- })
146
-
147
- it('all() matches any method', async () => {
148
- const r = new Router().all('/any', () => new Response('ok'))
149
- for (const method of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']) {
150
- const res = await r.handler()(new Request('http://localhost/any', { method }), { params: {}, query: {} })
151
- assert.equal(res.status, 200)
152
- }
153
- })
154
-
155
- it('sub-router mounting', async () => {
156
- const sub = new Router().get('/nested', () => new Response('sub'))
157
- const main = new Router().use('/api', sub)
158
- const res = await main.handler()(new Request('http://localhost/api/nested'), { params: {}, query: {} })
159
- assert.equal(await res.text(), 'sub')
160
- })
161
-
162
- it('sub-router preserves params', async () => {
163
- const sub = new Router().get('/:userId', (req, ctx) =>
164
- Response.json({ userId: ctx.params.userId }),
165
- )
166
- const main = new Router().use('/orgs/:orgId', sub)
167
- const res = await main.handler()(new Request('http://localhost/orgs/acme/john'), { params: {}, query: {} })
168
- const data = await res.json() as Record<string, string>
169
- assert.equal(data.userId, 'john')
170
- })
171
-
172
- it('middleware order: global → path → route', async () => {
173
- const order: number[] = []
174
- const r = new Router()
175
- .use((_req, _ctx, next) => { order.push(1); return next(_req, _ctx) })
176
- .use('/scoped', (_req, _ctx, next) => { order.push(2); return next(_req, _ctx) })
177
- .get('/scoped/route',
178
- (_req, _ctx, next) => { order.push(3); return next(_req, _ctx) },
179
- () => { order.push(4); return Response.json(order) },
180
- )
181
- await r.handler()(new Request('http://localhost/scoped/route'), { params: {}, query: {} })
182
- assert.deepEqual(order, [1, 2, 3, 4])
183
- })
184
-
185
- it('middleware short-circuits', async () => {
186
- const r = new Router()
187
- .use(() => new Response('blocked', { status: 403 }))
188
- .get('/blocked', () => new Response('should not reach'))
189
- const res = await r.handler()(new Request('http://localhost/blocked'), { params: {}, query: {} })
190
- assert.equal(res.status, 403)
191
- assert.equal(await res.text(), 'blocked')
192
- })
193
-
194
- it('middleware modifies response after next', async () => {
195
- const r = new Router()
196
- .use(async (_req, _ctx, next) => {
197
- const res = await next(_req, _ctx)
198
- const body = await res.json() as Record<string, unknown>
199
- body.modified = true
200
- return Response.json(body)
201
- })
202
- .get('/modify', () => Response.json({ original: true }))
203
- const res = await r.handler()(new Request('http://localhost/modify'), { params: {}, query: {} })
204
- const data = await res.json() as Record<string, unknown>
205
- assert.equal(data.original, true)
206
- assert.equal(data.modified, true)
207
- })
208
-
209
- it('onError catches handler exceptions', async () => {
210
- const r = new Router()
211
- .onError((err) => Response.json({ error: err.message }, { status: 500 }))
212
- .get('/crash', () => { throw new Error('oops') })
213
- const res = await r.handler()(new Request('http://localhost/crash'), { params: {}, query: {} })
214
- assert.equal(res.status, 500)
215
- const data = await res.json() as Record<string, string>
216
- assert.equal(data.error, 'oops')
217
- })
218
-
219
- it('root path / matches', async () => {
220
- const r = new Router().get('/', () => new Response('root'))
221
- const res = await r.handler()(new Request('http://localhost/'), { params: {}, query: {} })
222
- assert.equal(await res.text(), 'root')
223
- })
224
-
225
- it('same param name on same path position works', async () => {
226
- const r = new Router()
227
- .get('/:id', () => new Response('a'))
228
- .get('/:id/profile', () => new Response('b'))
229
- assert.equal(await (await r.handler()(new Request('http://localhost/foo'), { params: {}, query: {} })).text(), 'a')
230
- assert.equal(await (await r.handler()(new Request('http://localhost/foo/profile'), { params: {}, query: {} })).text(), 'b')
231
- })
232
-
233
- it('different param names on same path position throw', () => {
234
- const r = new Router()
235
- r.get('/:id', () => new Response('ok'))
236
- assert.throws(() => r.get('/:slug', () => new Response('ng')), /Param name conflict/)
237
- })
238
-
239
- it('multiple methods on same path', async () => {
240
- const r = new Router()
241
- .get('/resource', () => Response.json({ method: 'GET' }))
242
- .post('/resource', () => Response.json({ method: 'POST' }))
243
- .put('/resource', () => Response.json({ method: 'PUT' }))
244
- .delete('/resource', () => Response.json({ method: 'DELETE' }))
245
-
246
- const h = r.handler()
247
- const tests = ['GET', 'POST', 'PUT', 'DELETE'] as const
248
- for (const method of tests) {
249
- const res = await h(new Request('http://localhost/resource', { method }), { params: {}, query: {} })
250
- const data = await res.json() as Record<string, string>
251
- assert.equal(data.method, method)
252
- }
253
- })
254
-
255
- it('concurrent requests do not interfere', async () => {
256
- const r = new Router().get('/echo/:val', (req, ctx) =>
257
- Response.json({ val: ctx.params.val }),
258
- )
259
- const h = r.handler()
260
- const results = await Promise.all(
261
- Array.from({ length: 50 }, (_, i) =>
262
- Promise.resolve(h(new Request(`http://localhost/echo/${i}`), { params: {}, query: {} })).then(r => r.json()) as Promise<Record<string, string>>,
263
- ),
264
- )
265
- results.forEach((data, i) => {
266
- assert.equal(data.val, String(i))
267
- })
268
- })
269
- })
270
-
271
- // ── Router.ws ────────────────────────────────────────────────────────────────
272
-
273
- describe('Router.ws', () => {
274
- it('echos messages', async () => {
275
- const router = new Router()
276
- .ws('/echo', { message(ws, _ctx, data) { ws.send(`echo: ${data}`) } })
277
-
278
- const server = serve(router.handler(), {
279
- port: 0,
280
- websocket: router.websocketHandler(),
281
- })
282
- await server.ready
283
-
284
- const ws = new WebSocket(`ws://localhost:${server.port}/echo`)
285
- const msg = await new Promise<string>((resolve, reject) => {
286
- ws.on('open', () => ws.send('hello'))
287
- ws.on('message', (data) => resolve(data.toString()))
288
- ws.on('error', reject)
289
- setTimeout(() => reject(new Error('timeout')), 3000)
290
- })
291
- assert.equal(msg, 'echo: hello')
292
- ws.close()
293
- server.stop()
294
- })
295
-
296
- it('passes params from URL', async () => {
297
- const router = new Router()
298
- .ws('/chat/:room', { open(ws, ctx) { ws.send(ctx.params.room!) } })
299
-
300
- const server = serve(router.handler(), {
301
- port: 0,
302
- websocket: router.websocketHandler(),
303
- })
304
- await server.ready
305
-
306
- const ws = new WebSocket(`ws://localhost:${server.port}/chat/lobby`)
307
- const msg = await new Promise<string>((resolve, reject) => {
308
- ws.on('message', (data) => resolve(data.toString()))
309
- ws.on('error', reject)
310
- setTimeout(() => reject(new Error('timeout')), 3000)
311
- })
312
- assert.equal(msg, 'lobby')
313
- ws.close()
314
- server.stop()
315
- })
316
-
317
- it('middleware can reject upgrade', async () => {
318
- const router = new Router()
319
- .ws('/secure',
320
- (req, _ctx, next) => {
321
- const auth = req.headers.get('Authorization')
322
- if (!auth) return Response.json({ error: 'Unauthorized' }, { status: 401 })
323
- return next(req, _ctx)
324
- },
325
- { open(ws) { ws.send('authorized') } },
326
- )
327
-
328
- const server = serve(router.handler(), {
329
- port: 0,
330
- websocket: router.websocketHandler(),
331
- })
332
- await server.ready
333
-
334
- // Should be rejected (no auth header from Node.js built-in WebSocket)
335
- const ws = new WebSocket(`ws://localhost:${server.port}/secure`)
336
- const error = await new Promise<string | null>((resolve) => {
337
- ws.on('error', () => resolve('error'))
338
- ws.on('open', () => resolve(null))
339
- ws.on('unexpected-response', () => resolve('unexpected-response'))
340
- setTimeout(() => resolve('timeout'), 3000)
341
- })
342
- // Without auth, the middleware returns 401 and upgrade is rejected
343
- assert.ok(error === 'unexpected-response' || error === 'error', `Expected rejection, got: ${error}`)
344
- server.stop()
345
- })
346
- })
347
-
348
- // ── Router.graphql ────────────────────────────────────────────────────────────
349
-
350
- describe('Router.graphql', () => {
351
- it('handles GET query', async () => {
352
- const r = new Router()
353
- .graphql('/graphql', {
354
- schema: `type Query { hello: String }`,
355
- resolvers: { Query: { hello: () => 'world' } },
356
- })
357
-
358
- const { server, url } = await createTestServer(r.handler())
359
- const res = await fetch(`${url}/graphql?query={hello}`)
360
- assert.equal(res.status, 200)
361
- const data = await res.json() as Record<string, unknown>
362
- assert.deepEqual(data, { data: { hello: 'world' } })
363
- server.stop()
364
- })
365
-
366
- it('handles POST query', async () => {
367
- const r = new Router()
368
- .graphql('/graphql', {
369
- schema: `type Query { hello: String }`,
370
- resolvers: { Query: { hello: () => 'world' } },
371
- })
372
-
373
- const { server, url } = await createTestServer(r.handler())
374
- const res = await fetch(`${url}/graphql`, {
375
- method: 'POST',
376
- headers: { 'Content-Type': 'application/json' },
377
- body: JSON.stringify({ query: '{ hello }' }),
378
- })
379
- assert.equal(res.status, 200)
380
- const data = await res.json() as Record<string, unknown>
381
- assert.deepEqual(data, { data: { hello: 'world' } })
382
- server.stop()
383
- })
384
-
385
- it('returns GraphiQL HTML on GET without query', async () => {
386
- const r = new Router()
387
- .graphql('/graphql', {
388
- schema: `type Query { hello: String }`,
389
- resolvers: { Query: { hello: () => 'world' } },
390
- graphiql: true,
391
- })
392
-
393
- const { server, url } = await createTestServer(r.handler())
394
- const res = await fetch(`${url}/graphql`)
395
- assert.equal(res.status, 200)
396
- const text = await res.text()
397
- assert.ok(text.includes('GraphiQL'))
398
- assert.ok(text.includes('graphiql'))
399
- server.stop()
400
- })
401
- })