58 | : T extends `:${infer Param}`
59 | ? { [K in Param]: string }
60 | : T extends `*`
61 | ? { '*': string }
62 | : {};
63 |
64 | /**
65 | * Infer params from string
66 | */
67 | export type Params = ExtractParams
& E;
68 |
69 | export type ParserType = B extends 'text' ? string : (
70 | B extends 'json' ? Record : (
71 | B extends 'form' ? FormData : (
72 | B extends 'buffer' ? ArrayBuffer : (
73 | B extends 'blob' ? Blob : any
74 | )
75 | )
76 | )
77 | );
78 |
79 | /**
80 | * WebSocket data
81 | */
82 | export interface WSContext {
83 | /**
84 | * The current context
85 | */
86 | ctx: Context<'none', P>;
87 | /**
88 | * The router meta
89 | */
90 | meta: RouterMeta;
91 | }
92 |
93 | /**
94 | * All common headers name
95 | */
96 | export type CommonHeader = "Content-Type" | "Authorization" | "User-Agent"
97 | | "Access-Control-Allow-Origin" | "Access-Control-Max-Age" | "Access-Control-Allow-Headers"
98 | | "Access-Control-Allow-Credentials" | "Access-Control-Expose-Headers" | "Vary" | "Accept"
99 | | "Accept-Encoding" | "Accept-Language" | "Connection" | "Cache-Control" | "Set-Cookie" | "Cookie"
100 | | "Referer" | "Content-Length" | "Date" | "Expect" | "Server" | "Location" | "If-Modified-Since" | "ETag"
101 | | "X-XSS-Protection" | "X-Content-Type-Options" | "Referrer-Policy" | "Expect-CT" | "Content-Security-Policy"
102 | | "Cross-Origin-Opener-Policy" | "Cross-Origin-Embedder-Policy" | "Cross-Origin-Resource-Policy"
103 | | "Permissions-Policy" | "X-Powered-By" | "X-DNS-Prefetch-Control" | "Public-Key-Pins"
104 | | "X-Frame-Options" | "Strict-Transport-Security";
105 |
106 | export type CommonHeaders = {
107 | [head in CommonHeader]?: string;
108 | }
109 |
110 | /**
111 | * Represent a `head` object
112 | */
113 | export interface ContextHeaders extends CommonHeaders, Dict { };
114 |
115 | /**
116 | * Represent a request context
117 | */
118 | export interface Context extends Request {
119 | /**
120 | * Parsed request body
121 | */
122 | data: D;
123 | /**
124 | * Parsed request parameter with additional properties if specified
125 | */
126 | params: Params & Dict;
127 | /**
128 | * Request query start index (include `?`).
129 | */
130 | query: number;
131 | /**
132 | * Request path start index (skip first `/`).
133 | * This field only exists only if `base` is not specified
134 | */
135 | path: number;
136 | /**
137 | * Use to set response
138 | */
139 | set: ContextSet;
140 | }
141 |
142 | /**
143 | * Common status code
144 | */
145 | export type StatusCode =
146 | // 1xx
147 | 100 | 101 | 102 | 103
148 | // 2xx
149 | | 200 | 201 | 202 | 203 | 204 | 205 | 206
150 | | 207 | 208 | 226
151 | // 3xx
152 | | 300 | 301 | 302 | 303 | 304 | 307 | 308
153 | // 4xx
154 | | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407
155 | | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415
156 | | 416 | 417 | 418 | 422 | 423 | 424 | 425
157 | | 426 | 428 | 429 | 431 | 451
158 | // 5xx
159 | | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507
160 | | 508 | 510 | 511
161 | // Other
162 | | (number & {}) | (bigint & {});
163 |
164 | export interface ContextSet extends ResponseInit {
165 | headers?: ContextHeaders;
166 | status?: StatusCode;
167 | }
168 |
169 | /**
170 | * Create a context set object
171 | */
172 | export function ContextSet() { }
173 | ContextSet.prototype = Object.create(null);
174 | ContextSet.prototype.headers = null;
175 | ContextSet.prototype.status = null;
176 | ContextSet.prototype.statusText = null;
177 |
178 | /**
179 | * Blob part
180 | */
181 | export type BlobPart = string | Blob | BufferSource;
182 |
183 | /**
184 | * A Response body
185 | */
186 | export type ResponseBody = ReadableStream | BlobPart | BlobPart[] | FormData | URLSearchParams;
187 |
188 | /**
189 | * A route handler function
190 | */
191 | export interface Handler extends RouteOptions {
192 | (ctx: Context, T>, meta: RouterMeta): any;
193 | }
194 |
195 | export interface RouterMeta {
196 | /**
197 | * Whether the server is using HTTPS
198 | */
199 | https: boolean;
200 | /**
201 | * The base URL with protocol and base host
202 | */
203 | base: string;
204 | /**
205 | * The base host
206 | */
207 | host: string;
208 | /**
209 | * Whether the server is using default port
210 | */
211 | defaultPort: boolean;
212 | /**
213 | * The debug server
214 | */
215 | server: Server;
216 | /**
217 | * The router
218 | */
219 | router: Router;
220 | /**
221 | * Whether server is in dev mode
222 | */
223 | dev: boolean;
224 | }
225 |
226 |
227 | /**
228 | * Builtin body parser
229 | * - 'json': req.json()
230 | * - 'text': req.text()
231 | * - 'form': req.formData()
232 | * - 'blob': req.blob()
233 | * - 'buffer': req.arrayBuffer()
234 | */
235 | export type BodyParser = 'json' | 'text' | 'form' | 'blob' | 'buffer' | 'none';
236 |
237 | type TrimEndPath = P extends `${infer C}/` ? C : P;
238 | type AddStartPath
= P extends `/${infer C}` ? `/${C}` : `/${P}`;
239 |
240 | /**
241 | * Normalize a path
242 | */
243 | export type Normalize
= TrimEndPath> extends '' ? '/' : TrimEndPath>;
244 |
245 | /**
246 | * Concat path
247 | */
248 | export type ConcatPath = Normalize<`${Normalize}${Normalize}`>;
249 |
250 | /**
251 | * Fetch metadatas
252 | */
253 | export interface FetchMeta {
254 | /**
255 | * Parameters to pass into fetch scope
256 | */
257 | params: string[];
258 |
259 | /**
260 | * The body of the fetch function
261 | */
262 | body: string;
263 |
264 | /**
265 | * All values corresponding to the parameters
266 | */
267 | values: any[];
268 | }
269 |
270 | interface AllOptions extends BasicServeOptions, TLSServeOptions, WebSocketServeOptions, TLSWebSocketServeOptions { }
271 |
272 | export interface ServeOptions extends Partial {
273 | /**
274 | * Enable inspect mode
275 | */
276 | inspector?: boolean;
277 |
278 | /**
279 | * Should be set to something like `http://localhost:3000`
280 | * This enables optimizations for path parsing but does not work with subdomain
281 | */
282 | base?: string;
283 |
284 | /**
285 | * The minimum length of the request domain.
286 | *
287 | * Use this instead of `base` to work with subdomain
288 | */
289 | uriLen?: number;
290 |
291 | /**
292 | * If the value is not set or set to any other value than `false`,
293 | * this will change the prototype of `Request` to include properties
294 | * frequently used by Stric, which improves performance
295 | */
296 | requestOptimization?: boolean;
297 | }
298 |
299 | /**
300 | * An error handler
301 | */
302 | export interface ErrorHandler {
303 | (this: Server, err: Errorlike): any
304 | }
305 |
306 | /**
307 | * Handle body parsing error
308 | */
309 | export interface BodyHandler {
310 | (err: any, ...args: Parameters): any;
311 | }
312 |
313 | export interface RouteOptions {
314 | /**
315 | * Select a body parser
316 | */
317 | body?: BodyParser;
318 |
319 | /**
320 | * Whether to use the handler as macro
321 | */
322 | macro?: boolean;
323 |
324 | /**
325 | * Specify a wrapper.
326 | * If set to false, the parent wrapper will be disabled
327 | */
328 | wrap?: ResponseWrap;
329 |
330 | /**
331 | * Whether to force chain wrap with `then`
332 | */
333 | chain?: boolean;
334 | }
335 |
336 | export type ResponseWrap = keyof typeof wrap | Wrapper | true | false;
337 |
338 | // Behave like a post middleware
339 | export interface Wrapper {
340 | (response: any, ...args: Parameters): any;
341 |
342 | // Private props for modifying at compile time
343 | callName?: string;
344 | params?: string;
345 | hasParams?: boolean;
346 | }
347 |
348 | export type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch' | 'all' | 'guard' | 'reject';
349 | export type RouterMethods = {
350 | [K in HttpMethod]: (
351 | path: T, handler: O extends { body: infer B }
352 | ? (
353 | B extends BodyParser
354 | ? Handler, B>
355 | : Handler>
356 | ) : Handler>,
357 | options?: O
358 | ) => Router;
359 | };
360 |
361 | /**
362 | * Specific plugin for router
363 | */
364 | export interface Plugin {
365 | (app: Router): Router | void | Promise;
366 |
367 | /**
368 | * Only register the plugin after start listening
369 | */
370 | afterListen?: boolean;
371 | }
372 |
373 | /**
374 | * An object as a plugin
375 | */
376 | export interface PluginObject {
377 | plugin: Plugin;
378 |
379 | /**
380 | * Only register the plugin after start listening
381 | */
382 | afterListen?: boolean;
383 | }
384 |
385 | export type RouterPlugin = Plugin | PluginObject;
386 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './core/main';
2 | export * from './plugins/group';
3 | export * from './plugins/tester';
4 | export * from './plugins/ws';
5 |
--------------------------------------------------------------------------------
/src/plugins/group.ts:
--------------------------------------------------------------------------------
1 | import { Router, wrap } from "../core/main";
2 | import type {
3 | ConcatPath, Handler, ResponseWrap, RouterPlugin,
4 | HttpMethod, BodyParser, RouteOptions
5 | } from "../core/types";
6 | import { convert, methodsLowerCase as methods } from "../core/constants";
7 |
8 | export type GroupMethods = {
9 | [K in HttpMethod]: (
10 | path: T, handler: O extends { body: infer B }
11 | ? (
12 | B extends BodyParser
13 | ? Handler, B>
14 | : Handler>
15 | ) : Handler>,
16 | options?: O
17 | ) => Group;
18 | };
19 |
20 | export interface Group extends GroupMethods { }
21 |
22 | // @ts-ignore Shorthand
23 | export const route: GroupMethods<'/'> = {};
24 | for (const method of methods)
25 | route[method] = (...args: any[]) => new Group()[method](...args);
26 |
27 | /**
28 | * A routes group. Can be used as a plugin
29 | */
30 | export class Group {
31 | record: any[][] = [];
32 | plugins: any[] = [];
33 |
34 | /**
35 | * Create a new routes group
36 | * @param root
37 | */
38 | // @ts-ignore
39 | constructor(public root: Root = '/') {
40 | if (root !== '/' && root.endsWith('/'))
41 | // @ts-ignore
42 | root = root.slice(0, -1);
43 | this.root = root;
44 |
45 | for (const method of methods) this[method] = (path: string, handler: Handler, opts: any) => {
46 | // Special cases
47 | path = convert(path);
48 |
49 | const args = [method, path, handler];
50 | if (opts) args.push(opts);
51 |
52 | this.record.push(args);
53 | return this;
54 | }
55 | }
56 |
57 | /**
58 | * Use the default response wrapper for a group of subroutes
59 | */
60 | wrap(path: string): this;
61 |
62 | /**
63 | * Wrap the response
64 | */
65 | wrap(path: string, handler: ResponseWrap = 'plain') {
66 | if (typeof handler === 'string')
67 | handler = wrap[handler];
68 |
69 | if (this.root !== '/')
70 | path = this.root + path;
71 | path = convert(path);
72 |
73 | this.record.push(['wrap', path, handler]);
74 | return this;
75 | }
76 |
77 | /**
78 | * Add a plugin
79 | * @param plugin
80 | */
81 | plug(...plugins: RouterPlugin[]) {
82 | this.plugins.push(...plugins);
83 | return this;
84 | }
85 |
86 | private fixPath(p: string) {
87 | return this.root === '/' ? p : this.root + p;
88 | }
89 |
90 | /**
91 | * Get the plugin
92 | */
93 | plugin(app: Router) {
94 | let item: any;
95 |
96 | for (item of this.plugins) {
97 | // Set the correct root
98 | if (item instanceof Group) {
99 | if (item.root === '/')
100 | item.root = this.root;
101 | else if (this.root !== '/')
102 | // @ts-ignore
103 | item.root = this.root + item.root;
104 | }
105 |
106 | app.plug(item);
107 | }
108 |
109 | for (item of this.record) app[item[0]](
110 | this.fixPath(item[1]), ...item.slice(2)
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/plugins/tester.ts:
--------------------------------------------------------------------------------
1 | import Router from "../core/main";
2 |
3 | class ResponseStatus {
4 | constructor(public code: number, public text: string) { }
5 | }
6 |
7 | const toTxT = (r: Response) => r.text(),
8 | toJSON = (r: Response) => r.json(),
9 | toBlob = (r: Response) => r.blob(),
10 | toForm = (r: Response) => r.formData(),
11 | toBuffer = (r: Response) => r.arrayBuffer(),
12 | getStatus = (r: Response) => new ResponseStatus(r.status, r.statusText),
13 | getStatusCode = (r: Response) => r.status,
14 | getStatusText = (r: Response) => r.statusText,
15 | getHeaders = (r: Response) => r.headers,
16 | responseIsOk = (r: Response) => r.ok;
17 |
18 | type Params = [url: string, init?: Omit & { body?: BodyInit | Dict }];
19 |
20 | export interface MockOptions {
21 | /**
22 | * Represent the log level
23 | * `0`: No logging. This is the default value
24 | * `1`: Log only path
25 | */
26 | logLevel?: 0 | 1;
27 | }
28 |
29 | /**
30 | * Create a tester for the current router
31 | */
32 | export function mock(app: Router, opts: MockOptions = {}) {
33 | if (!app.listening) app.listen();
34 | const { logLevel: logLvl = 0 } = opts, base = app.details.base;
35 |
36 | return {
37 | /**
38 | * Create a WS client based on the path
39 | */
40 | ws(path: string | URL, opts?: ConstructorParameters[1]) {
41 | path = base + path;
42 | return new WebSocket(path, opts);
43 | },
44 | /**
45 | * Mock the current fetch handler.
46 | *
47 | * If a non-response object is returned, an empty 404 response is returned instead
48 | */
49 | async fetch(...args: Params): Promise {
50 | if (logLvl >= 1) console.info('Testing', '`' + args[0] + '`');
51 | args[0] = base + args[0];
52 |
53 | // Automatically stringify the body if body is JSON
54 | if (args[1]?.body) {
55 | const b = args[1].body as any;
56 | if (typeof b === 'object')
57 | if (b.toString === Object.prototype.toString)
58 | // @ts-ignore
59 | args[1].body = JSON.stringify(b);
60 | }
61 |
62 | // @ts-ignore Save microticks
63 | return await fetch(new Request(...args));
64 | },
65 |
66 | /**
67 | * Mock a request and return the status message
68 | */
69 | async head(...args: Params): Promise {
70 | return this.fetch(...args).then(getHeaders);
71 | },
72 |
73 | /**
74 | * Mock a request and convert the response to an ArrayBuffer
75 | */
76 | async ok(...args: Params): Promise {
77 | return this.fetch(...args).then(responseIsOk);
78 | },
79 |
80 | /**
81 | * Mock a request and return the status message
82 | */
83 | async msg(...args: Params): Promise {
84 | return this.fetch(...args).then(getStatusText);
85 | },
86 |
87 | /**
88 | * Mock a request and get the status code and message
89 | */
90 | async stat(...args: Params): Promise {
91 | return this.fetch(...args).then(getStatus);
92 | },
93 |
94 | /**
95 | * Mock a request and get the status code
96 | */
97 | async code(...args: Params): Promise {
98 | return this.fetch(...args).then(getStatusCode);
99 | },
100 |
101 | /**
102 | * Mock a request and convert the response to string
103 | */
104 | async text(...args: Params): Promise {
105 | return this.fetch(...args).then(toTxT);
106 | },
107 |
108 | /**
109 | * Mock a request and convert the response to JSON
110 | */
111 | async json(...args: Params): Promise {
112 | return this.fetch(...args).then(toJSON);
113 | },
114 |
115 | /**
116 | * Mock a request and convert the response to Blob
117 | */
118 | async blob(...args: Params): Promise {
119 | return this.fetch(...args).then(toBlob);
120 | },
121 |
122 | /**
123 | * Mock a request and convert the response to form data
124 | */
125 | async form(...args: Params): Promise {
126 | return this.fetch(...args).then(toForm);
127 | },
128 |
129 | /**
130 | * Mock a request and convert the response to an ArrayBuffer
131 | */
132 | async buf(...args: Params): Promise {
133 | return this.fetch(...args).then(toBuffer);
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/plugins/ws.ts:
--------------------------------------------------------------------------------
1 | import type { Server, WebSocketHandler } from 'bun';
2 | import type { PluginObject, Router, RouterMeta } from '..';
3 | import { wsHandlerDataKey } from '../core/router/compiler/constants';
4 |
5 | export namespace ws {
6 | /**
7 | * Create a dynamic websocket route.
8 | */
9 | export function route(handler: WebSocketHandler, noOptions: boolean = false) {
10 | return new Route(handler, noOptions);
11 | }
12 |
13 | export interface Route {
14 | /**
15 | * Upgrade the connection to a WebSocket connection.
16 | * User after attaching the route to a server
17 | */
18 | upgrade(c: Request, opts?: {
19 | /**
20 | * Send any additional headers while upgrading, like cookies
21 | */
22 | headers?: HeadersInit;
23 |
24 | /**
25 | * This value is passed to the {@link ServerWebSocket.data} property
26 | */
27 | data?: T;
28 | }): boolean;
29 |
30 | /**
31 | * The attached server
32 | */
33 | readonly server: Server;
34 |
35 | /**
36 | * The attached meta. Only works with Stric plugins
37 | */
38 | readonly meta: RouterMeta;
39 | }
40 |
41 | export class Route implements PluginObject {
42 | constructor(public readonly handler: WebSocketHandler, public noOptions: boolean = false) { }
43 |
44 | /**
45 | * Attach this route to a server
46 | */
47 | attach(server: Server) {
48 | const defOpts = {
49 | data: { [wsHandlerDataKey]: this.handler }
50 | };
51 |
52 | this.upgrade = this.noOptions
53 | ? (c: Request) => server.upgrade(c, defOpts)
54 | : Function('k', 's', `var i=k.data,h=i.${wsHandlerDataKey};`
55 | + `return (c,o)=>{`
56 | + `if(o===undefined)o=k;`
57 | + `else if('data'in o)o.data.${wsHandlerDataKey}=h;`
58 | + `else o.data=i;`
59 | + `return s.upgrade(c,o)}`
60 | )(defOpts, server);
61 |
62 | // @ts-ignore
63 | this.server = server;
64 |
65 | return this;
66 | }
67 |
68 | /**
69 | * This plugin runs after listening
70 | */
71 | plugin(app: Router) {
72 | if (app.details.server === null)
73 | throw new Error('This plugin needs to be registered after the server started!');
74 |
75 | this.attach(app.details.server);
76 | // @ts-ignore
77 | this.meta = app.details;
78 | return app;
79 | }
80 |
81 | // This plugin is registered after listening
82 | afterListen = true;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/basic.test.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { test, expect } from 'bun:test';
3 | import { macro, mock, router, wrap } from '..';
4 |
5 | const predefinedBody = { hi: 'there' }, invalidBody = { status: 400 };
6 |
7 | // Create the function;
8 | const app = router()
9 | .set('port', 3000)
10 | .get('/', macro('Hi'))
11 |
12 | .get('/id/:id', c => new Response(c.params.id))
13 | .get('/:name/dashboard/:cat', c => new Response(c.params.name + ' ' + c.params.cat))
14 |
15 | .post('/json', c => wrap.json(c.data), { body: 'json' })
16 | .all('/json', () => wrap.json(predefinedBody))
17 |
18 | .get('/api/v1/hi', () => 'Hi')
19 |
20 | .guard('/api/v1', async c => c.method === 'GET' ? null : true)
21 | .reject('/api/v1', () => 'No enter!')
22 | .wrap('/api/v1')
23 |
24 | .all('/json/*', c => new Response(c.params['*']))
25 |
26 | .get('/str/1', () => 'Hello')
27 | .get('/str/2', async () => 'Hi')
28 | .get('/str/3', (_, meta) => meta.server.port)
29 |
30 | .get('/str/4', c => {
31 | c.set = { status: 418 };
32 | return 'I\'m a teapot';
33 | })
34 |
35 | .get('/str/5', macro(10))
36 | .wrap('/str', 'send')
37 |
38 | .use(404)
39 | .use(400, (e, c) => new Response(c.url + ': ' + e, invalidBody));
40 |
41 | // Tracking time
42 | console.time('Build fetch');
43 | console.log(app.meta);
44 |
45 | // Report process memory usage and build time
46 | console.timeEnd('Build fetch');
47 |
48 | const tester = mock(app, { logLevel: 1 });
49 | console.log(process.memoryUsage());
50 |
51 | // GET / should returns 'Hi'
52 | test('GET /', async () => {
53 | const res = await tester.text('/');
54 | expect(res).toBe('Hi');
55 | });
56 |
57 | // Dynamic path test
58 | test('GET /id/:id', async () => {
59 | const randomNum = String(Math.round(Math.random() * 101)),
60 | res = await tester.text(`/id/${randomNum}?param`);
61 |
62 | expect(res).toBe(randomNum);
63 | });
64 |
65 | // Edge case test
66 | test('GET /:name/dashboard/:cat', async () => {
67 | const randomNum = String(Math.round(Math.random() * 101)),
68 | res = await tester.text(`/${randomNum}/dashboard/main`);
69 |
70 | expect(res).toBe(randomNum + ' main');
71 | });
72 |
73 | // JSON test
74 | test('POST /json', async () => {
75 | const rnd = { value: Math.round(Math.random()) },
76 | res = await tester.json('/json', {
77 | method: 'POST',
78 | body: rnd
79 | });
80 |
81 | expect(res).toStrictEqual(rnd);
82 | });
83 |
84 | test('404', async () => {
85 | let res: any = await tester.code('/path/that/does/not/exists');
86 | expect(res).toBe(404);
87 |
88 | res = await tester.text('/json/any', { method: 'PUT' });
89 | expect(res).toBe('any');
90 |
91 | res = await tester.text('/api/v1/hi');
92 | expect(res).toBe('No enter!');
93 | });
94 |
95 | test('400', async () => {
96 | const res = await tester.fetch('/json', { method: 'POST' });
97 |
98 | expect(res.status).toBe(400);
99 | console.log(await res.text());
100 | });
101 |
102 | test('Wrapper', async () => {
103 | let res: any = await tester.text('/str/1');
104 | expect(res).toBe('Hello');
105 |
106 | res = await tester.text('/str/2');
107 | expect(res).toBe('Hi');
108 |
109 | res = await tester.text('/str/3');
110 | expect(res).toBe('3000');
111 |
112 | res = await tester.fetch('/str/4');
113 | expect(await res.text()).toBe(`I'm a teapot`);
114 | expect(res.status).toBe(418);
115 |
116 | res = await tester.text('/str/5');
117 | expect(res).toBe('10');
118 | });
119 |
--------------------------------------------------------------------------------
/tests/group.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "bun:test";
2 | import { Group, mock, router, route } from "..";
3 |
4 | const a = route.get('/a', () => 'Hi', { wrap: 'send' }),
5 | b = new Group('/b').plug(a),
6 | c = new Group('/c').plug(b),
7 | app = router(c).set('port', 3001).use(404);
8 |
9 | const client = mock(app, { logLevel: 1 });
10 |
11 | test('Nested group', async () => {
12 | const res = await client.text('/c/b/a');
13 | expect(res).toBe('Hi');
14 | });
15 |
--------------------------------------------------------------------------------
/tests/ws.test.ts:
--------------------------------------------------------------------------------
1 | import { mock, router, ws } from '..';
2 | import { test, expect } from 'bun:test';
3 |
4 | const route = ws.route({
5 | message(ws) {
6 | ws.send('Hi');
7 | }
8 | }, true), app = router(route)
9 | .set('port', 3002)
10 | .all('/', c => route.upgrade(c))
11 | .listen();
12 |
13 | const client = mock(app);
14 |
15 | test('Dynamic WS', done => {
16 | const socket = client.ws('/');
17 |
18 | socket.onopen = () => socket.send('');
19 | socket.onmessage = m => {
20 | expect(m.data).toBe('Hi');
21 | done();
22 | };
23 | });
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ESNext",
4 | "lib": [
5 | "ESNext"
6 | ],
7 | "types": [
8 | "bun-types"
9 | ],
10 | "esModuleInterop": true,
11 | "target": "ESNext",
12 | "skipDefaultLibCheck": true,
13 | "moduleResolution": "node",
14 | "declaration": true,
15 | "emitDeclarationOnly": true
16 | },
17 | "include": [
18 | "src"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------