dynara 0.0.1

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 ADDED
@@ -0,0 +1,150 @@
1
+ # Marci
2
+
3
+ [![NPM version](https://img.shields.io/npm/v/%40den59k%2Fmarci)](https://www.npmjs.com/package/@den59k/marci)
4
+
5
+ An extremely simple HTTP framework for Bun — practically a typed wrapper around `Bun.serve`, with Fastify-style routing and fast schema validation. Made for people switching over from Fastify.
6
+
7
+ - **Bun only** — Node.js and Deno are not supported.
8
+ - **Minimal overhead** — routing is delegated to Bun's native router.
9
+ - **Typed validation** — powered by [TypeBox](https://www.npmjs.com/package/@sinclair/typebox), written with the compact [compact-json-schema](https://www.npmjs.com/package/compact-json-schema) syntax.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ bun add @den59k/marci
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { MarciApp } from '@den59k/marci'
21
+
22
+ const app = new MarciApp()
23
+
24
+ app.get('/', () => {
25
+ return { hello: 'world' }
26
+ })
27
+
28
+ app.listen(3000)
29
+ ```
30
+
31
+ A handler may return a plain value (sent as JSON), a `Response` (sent as-is), or `undefined` (an empty `200`).
32
+
33
+ ## Routes & validation
34
+
35
+ Routes use Bun's native patterns — `:param` for a single segment, `*` for a wildcard. The methods are `get`, `post`, `put`, `patch`, and `delete`.
36
+
37
+ Schemas are written with [compact-json-schema](https://www.npmjs.com/package/compact-json-schema) and validated with TypeBox. Pass them as a route-option object, or as a positional array — `[params]` / `[params, query]` for `GET`, `[params, body, query]` for the others. Validated `req.params`, `req.query`, and `req.body` are fully typed.
38
+
39
+ ```ts
40
+ import { schema } from 'compact-json-schema'
41
+
42
+ const params = schema({ id: 'number' })
43
+ const body = schema({ name: 'string', age: 'number?' }) // ? optional, ?? nullable
44
+
45
+ // Option object
46
+ app.post('/users/:id', { params, body }, (req) => {
47
+ req.params.id // number
48
+ req.body.name // string
49
+ return { ok: true }
50
+ })
51
+
52
+ // Positional array
53
+ app.get('/users/:id', [params], (req) => {
54
+ return { id: req.params.id } // number
55
+ })
56
+ ```
57
+
58
+ A few conveniences:
59
+
60
+ - **Array params** are comma-split: `GET /items/1,2,3` with `{ itemIds: { type: 'array', items: 'number' } }` yields `[1, 2, 3]`.
61
+ - **Query booleans** accept `?flag=true` or a bare `?flag`.
62
+ - `req.raw` exposes the underlying `BunRequest`, and `req.server` the Bun `Server`.
63
+
64
+ ## Hooks
65
+
66
+ ```ts
67
+ app.addHook('onRequest', (req) => {
68
+ // runs before every handler; throw to short-circuit the request
69
+ })
70
+
71
+ app.addHook('onListen', (server) => {
72
+ console.log(`Listening on ${server.url}`)
73
+ })
74
+ ```
75
+
76
+ ## Plugins & context
77
+
78
+ `register` mounts a group of routes under a prefix. Type the app with a context type to share data attached by hooks:
79
+
80
+ ```ts
81
+ type Ctx = { user: { id: number } }
82
+
83
+ app.register((users: MarciApp<Ctx>) => {
84
+ users.addHook('onRequest', (req) => { req.user = { id: 1 } })
85
+ users.get('/me', (req) => req.user)
86
+ }, { prefix: '/users' })
87
+ ```
88
+
89
+ For composable, reusable plugins there is the `marci()` builder. `use` adds plugins, `routes` adds handlers, and the result can be passed to `register`:
90
+
91
+ ```ts
92
+ import { marci, MarciApp } from '@den59k/marci'
93
+
94
+ const useAuth = (app: MarciApp<Ctx>) => {
95
+ app.addHook('onRequest', (req) => { req.user = { id: 1 } })
96
+ }
97
+
98
+ const users = marci<Ctx>()
99
+ .use(useAuth)
100
+ .routes((app) => {
101
+ app.get('/me', (req) => req.user)
102
+ })
103
+
104
+ app.register(users, { prefix: '/users' })
105
+ ```
106
+
107
+ ## Errors
108
+
109
+ Throw `HTTPError` to send an explicit status code; validation failures are turned into `400` responses automatically.
110
+
111
+ ```ts
112
+ import { HTTPError } from '@den59k/marci'
113
+
114
+ app.get('/secret', () => {
115
+ throw new HTTPError('Forbidden', 403) // text body
116
+ // throw new HTTPError({ reason: 'forbidden' }, 403) // JSON body
117
+ })
118
+ ```
119
+
120
+ ## Testing
121
+
122
+ `inject()` dispatches a request through your routes in-process — no server, no socket — and returns a real `Response`. It reuses the same routing, validation, and error handling as a live server.
123
+
124
+ ```ts
125
+ import { test, expect } from 'bun:test'
126
+
127
+ test('returns a user', async () => {
128
+ const res = await app.inject('/users/1')
129
+ expect(res.status).toBe(200)
130
+ expect(await res.json()).toEqual({ id: 1 })
131
+ })
132
+
133
+ // With a body:
134
+ await app.inject({ method: 'POST', url: '/users', body: { name: 'Alice' } })
135
+ ```
136
+
137
+ > Under `inject()` there is no Bun `Server`, so `req.server` is `undefined` and WebSocket upgrades are not exercised.
138
+
139
+ ## WebSockets
140
+
141
+ ```ts
142
+ app.registerWsHandler('/ws', {
143
+ open(ws) { ws.send('hello') },
144
+ message(ws, msg) { ws.send(msg) },
145
+ })
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT
@@ -0,0 +1,119 @@
1
+ declare class HTTPError extends Error {
2
+ statusCode: number;
3
+ data?: any;
4
+ message: string;
5
+ constructor(message: string | object, statusCode?: number);
6
+ }
7
+ import { BunRequest, Server, HeadersInit, BodyInit } from "bun";
8
+ import { SchemaItem, SchemaType } from "compact-json-schema";
9
+ type RouteOptions = {
10
+ params?: SchemaItem
11
+ body?: SchemaItem
12
+ query?: SchemaItem
13
+ };
14
+ type GetRouteOptions = {
15
+ params?: SchemaItem
16
+ query?: SchemaItem
17
+ };
18
+ interface MarciContext {}
19
+ type MarciRequest<
20
+ R extends object = {},
21
+ T extends RouteOptions = {}
22
+ > = MarciContext & R & {
23
+ params: T["params"] extends object ? SchemaType<T["params"]> : unknown
24
+ query: T["query"] extends object ? SchemaType<T["query"]> : unknown
25
+ body: T["body"] extends object ? SchemaType<T["body"]> : unknown
26
+ raw: BunRequest
27
+ server: Server
28
+ };
29
+ type GetMarciRequest<
30
+ R extends object = {},
31
+ T extends GetRouteOptions = {}
32
+ > = MarciContext & R & {
33
+ params: T["params"] extends object ? SchemaType<T["params"]> : unknown
34
+ query: T["query"] extends object ? SchemaType<T["query"]> : unknown
35
+ raw: BunRequest
36
+ server: Server
37
+ };
38
+ type RouteAction<
39
+ T extends RouteOptions,
40
+ R extends object = {}
41
+ > = (req: MarciRequest<R, T>) => (any | Promise<any>);
42
+ type GetRouteAction<
43
+ T extends GetRouteOptions,
44
+ R extends object = {}
45
+ > = (req: GetMarciRequest<R, T>) => (any | Promise<any>);
46
+ type RegisterPluginOptions = {
47
+ prefix?: string
48
+ };
49
+ type InjectOptions = {
50
+ method?: string
51
+ url: string
52
+ headers?: HeadersInit
53
+ body?: BodyInit | Record<string, any> | null
54
+ };
55
+ import { BunRequest as BunRequest3, Server as Server2, WebSocketHandler } from "bun";
56
+ import { SchemaItem as SchemaItem3 } from "compact-json-schema";
57
+ import { SchemaItem as SchemaItem2 } from "compact-json-schema";
58
+ type GetOptionsFromSchemaList<T extends readonly SchemaItem2[]> = T extends [SchemaItem2] ? {
59
+ params: T[0]
60
+ } : T extends [SchemaItem2, SchemaItem2] ? {
61
+ params: T[0]
62
+ query: T[1]
63
+ } : {};
64
+ type PostOptionsFromSchemaList<T extends readonly SchemaItem2[]> = T extends [SchemaItem2] ? {
65
+ params: T[0]
66
+ } : T extends [SchemaItem2, SchemaItem2] ? {
67
+ params: T[0]
68
+ body: T[1]
69
+ } : {};
70
+ declare class MarciApp<R extends object = {}> {
71
+ private routes;
72
+ private promises;
73
+ private prefix;
74
+ private root;
75
+ private server;
76
+ private onListenHooks;
77
+ private onRequestHooks;
78
+ private add;
79
+ addHook(where: "onListen", callback: (server: Bun.Server2) => void): void;
80
+ addHook(where: "onRequest", callback: (ctx: MarciRequest<R>) => void): void;
81
+ get(path: string, callback: GetRouteAction<{}, R>): void;
82
+ get<T extends GetRouteOptions>(path: string, options: T, callback: GetRouteAction<T, R>): void;
83
+ get<T extends readonly SchemaItem3[]>(path: string, schemas: [...T], callback: GetRouteAction<GetOptionsFromSchemaList<T>, R>): void;
84
+ post(path: string, callback: RouteAction<{}, R>): void;
85
+ post<T extends RouteOptions>(path: string, options: T, callback: RouteAction<T, R>): void;
86
+ post<T extends readonly SchemaItem3[]>(path: string, schemas: [...T], callback: RouteAction<PostOptionsFromSchemaList<T>, R>): void;
87
+ put(path: string, callback: RouteAction<{}, R>): void;
88
+ put<T extends RouteOptions>(path: string, options: T, callback: RouteAction<T, R>): void;
89
+ put<T extends readonly SchemaItem3[]>(path: string, schemas: [...T], callback: RouteAction<PostOptionsFromSchemaList<T>, R>): void;
90
+ patch(path: string, callback: RouteAction<{}, R>): void;
91
+ patch<T extends RouteOptions>(path: string, options: T, callback: RouteAction<T, R>): void;
92
+ patch<T extends readonly SchemaItem3[]>(path: string, schemas: [...T], callback: RouteAction<PostOptionsFromSchemaList<T>, R>): void;
93
+ delete(path: string, callback: RouteAction<{}, R>): void;
94
+ delete<T extends RouteOptions>(path: string, options: T, callback: RouteAction<T, R>): void;
95
+ delete<T extends readonly SchemaItem3[]>(path: string, schemas: [...T], callback: RouteAction<PostOptionsFromSchemaList<T>, R>): void;
96
+ register(plugin: (app: MarciApp<any>) => void | Promise<void>, options?: RegisterPluginOptions): void;
97
+ private websocket?;
98
+ private websocketPath?;
99
+ private websocketFetch?;
100
+ registerWsHandler<T>(ws: WebSocketHandler<T>): void;
101
+ registerWsHandler<T>(path: string, ws: WebSocketHandler<T>): void;
102
+ registerWsHandler<T>(onFetch: (req: BunRequest3) => any, ws: WebSocketHandler<T>): void;
103
+ private fetch;
104
+ registerNotFoundHandler(handler: (path: string, req: Request, server: Server2) => Response | Promise<Response | undefined>): void;
105
+ listen(port?: number, hostname?: string): Promise<Bun.Server2>;
106
+ /** Converts a thrown error into the same Response Bun's `error` handler produces. */
107
+ private handleError;
108
+ /**\\n\\t\\n\\t* Dispatches a request through the app's routes in-process — no server, no\\n\\t\\n\\t* socket. Matches the route the same way Bun would, then runs the onRequest\\n\\t\\n\\t* hooks, validation, handler, and error mapping, returning the Response.\\n\\t\\n\\t* Intended for integration testing.\\n\\t\\n\\t*\\n\\t\\n\\t* @example\\n\\t\\n\\t* const res = await app.inject("/users/1")\\n\\t\\n\\t* const res = await app.inject({ method: "POST", url: "/users", body: { name: "Alice" } })\\n\\t\\n\\t* expect(res.status).toBe(200)\\n\\t\\n\\t* expect(await res.json()).toEqual({ ... })\\n\\t\\n\\t*/
109
+ inject(options: string | InjectOptions): Promise<Response>;
110
+ }
111
+ type MarciSyntax<R extends object = {}> = ((app: MarciApp<any>) => Promise<void>) & {
112
+ use<
113
+ S extends object = {},
114
+ A extends any[] = []
115
+ >(plugin: (app: MarciApp<R & S>, ...args: A) => Promise<void> | void, ...args: A): MarciSyntax<R & S>
116
+ routes(app: (app: MarciApp<R>) => Promise<void> | void): MarciSyntax<R>
117
+ };
118
+ declare const marci: <R extends object = {}>() => MarciSyntax<R>;
119
+ export { marci, MarciSyntax, MarciRequest, MarciApp, InjectOptions, HTTPError };
package/dist/index.js ADDED
@@ -0,0 +1,426 @@
1
+ // src/error.ts
2
+ class HTTPError extends Error {
3
+ statusCode;
4
+ data;
5
+ message;
6
+ constructor(message, statusCode) {
7
+ super();
8
+ if (typeof message === "string") {
9
+ this.message = message;
10
+ } else if (typeof message === "object" && message !== null && !("error" in message)) {
11
+ this.data = { error: message };
12
+ this.message = "HTTP Error";
13
+ } else {
14
+ this.data = message;
15
+ this.message = "HTTP Error";
16
+ }
17
+ this.statusCode = statusCode ?? 400;
18
+ }
19
+ }
20
+
21
+ class ValidationError extends Error {
22
+ error;
23
+ where;
24
+ constructor(err, where) {
25
+ super(err.message);
26
+ this.error = err;
27
+ this.where = where;
28
+ }
29
+ }
30
+
31
+ // src/marci-syntax.ts
32
+ var marci = () => {
33
+ const plugins = [];
34
+ const handlers = [];
35
+ const app = Object.assign(async (app2) => {
36
+ for (let plugin of plugins) {
37
+ await plugin[0](app2, ...plugin.slice(1));
38
+ }
39
+ for (let handler of handlers) {
40
+ await handler(app2);
41
+ }
42
+ }, {
43
+ use(plugin, ...args) {
44
+ plugins.push([plugin, ...args]);
45
+ return this;
46
+ },
47
+ routes(handler) {
48
+ handlers.push(handler);
49
+ return this;
50
+ }
51
+ });
52
+ return app;
53
+ };
54
+ // src/MarciApp.ts
55
+ import { unfoldTypeBoxSchema } from "compact-json-schema";
56
+ import { TypeBoxError } from "@sinclair/typebox";
57
+ import { TypeCompiler } from "@sinclair/typebox/compiler";
58
+
59
+ // src/request.ts
60
+ import { Value } from "@sinclair/typebox/value";
61
+
62
+ class MarciRequestInternal {
63
+ server;
64
+ raw;
65
+ params;
66
+ query;
67
+ body = null;
68
+ constructor(req, server, paramsSchema, querySchema) {
69
+ if (paramsSchema && paramsSchema.arrayKeys) {
70
+ const params = req.params;
71
+ for (let key of paramsSchema.arrayKeys) {
72
+ if (params[key] && !params[key].startsWith("[")) {
73
+ params[key] = params[key].split(",");
74
+ }
75
+ }
76
+ this.params = Value.Parse(paramsSchema, params);
77
+ } else {
78
+ this.params = paramsSchema === null ? req.params : Value.Parse(paramsSchema, req.params);
79
+ }
80
+ if (req.url.includes("?")) {
81
+ const queryParams = new URL(req.url).searchParams;
82
+ if (querySchema && querySchema.type === "object") {
83
+ const query = {};
84
+ for (let [key, value] of queryParams.entries()) {
85
+ const schema = querySchema.properties[key];
86
+ if (!schema)
87
+ continue;
88
+ if (schema.type === "boolean" && value === "") {
89
+ query[key] = true;
90
+ continue;
91
+ }
92
+ query[key] = Value.Parse(schema, value);
93
+ }
94
+ this.query = query;
95
+ } else {
96
+ this.query = Object.fromEntries(queryParams.entries());
97
+ }
98
+ } else if (querySchema) {
99
+ this.query = Value.Parse(querySchema, {});
100
+ } else {
101
+ this.query = null;
102
+ }
103
+ this.raw = req;
104
+ this.server = server;
105
+ }
106
+ }
107
+
108
+ // src/utils.ts
109
+ import { Type } from "@sinclair/typebox";
110
+ import { provideTypeBoxMap } from "compact-json-schema";
111
+ var getRouteOptions = (method, schemas) => {
112
+ if (schemas.length === 0)
113
+ return {};
114
+ if (schemas.length === 1) {
115
+ return { params: schemas[0] };
116
+ }
117
+ if (method === "GET") {
118
+ return { params: schemas[0], query: schemas[1] };
119
+ } else {
120
+ if (schemas.length === 2) {
121
+ return { params: schemas[0], body: schemas[1] };
122
+ } else {
123
+ return { params: schemas[0], body: schemas[1], query: schemas[2] };
124
+ }
125
+ }
126
+ };
127
+ var getValidationError = (obj, step) => {
128
+ if (step) {
129
+ return `{"cause":"Validation error","where":"${step}","fields":{"${obj.path.slice(1)}":{"message":"${obj.message}"}}}`;
130
+ } else {
131
+ return `{"cause":"Validation error","fields":{"${obj.path.slice(1)}":{"message":"${obj.message}"}}}`;
132
+ }
133
+ };
134
+ provideTypeBoxMap({
135
+ string: Type.String,
136
+ boolean: Type.Boolean,
137
+ number: Type.Number,
138
+ integer: Type.Integer,
139
+ object: Type.Object,
140
+ array: Type.Array,
141
+ bigint: Type.BigInt,
142
+ union: Type.Union,
143
+ null: Type.Null,
144
+ literal: Type.Literal,
145
+ optional: Type.Optional,
146
+ any: Type.Any
147
+ });
148
+ var isDefault = (schema) => {
149
+ if (typeof schema !== "object" || schema === null)
150
+ return true;
151
+ for (let value of Object.values(schema)) {
152
+ if (value !== "string" && value !== "string?" && value !== "string??") {
153
+ return false;
154
+ }
155
+ }
156
+ return true;
157
+ };
158
+ var parseBody = (schema, req) => {
159
+ return new Promise((res, rej) => {
160
+ req.json().catch((e) => {
161
+ rej(new HTTPError(`Error on parsing body (${e.message})`, 400));
162
+ }).then((resp) => {
163
+ const check = schema.Check(resp);
164
+ if (!check) {
165
+ const error = schema.Errors(resp).First();
166
+ const err = new ValidationError(error, "body");
167
+ rej(err);
168
+ }
169
+ res(resp);
170
+ });
171
+ });
172
+ };
173
+ var matchSegments = (pattern, segments) => {
174
+ const patternSegments = pattern.split("/");
175
+ const params = {};
176
+ for (let i = 0;i < patternSegments.length; i++) {
177
+ const part = patternSegments[i];
178
+ if (i >= segments.length)
179
+ return null;
180
+ if (part === "*") {
181
+ return params;
182
+ }
183
+ if (part.startsWith(":")) {
184
+ params[part.slice(1)] = decodeURIComponent(segments[i]);
185
+ } else if (part !== segments[i]) {
186
+ return null;
187
+ }
188
+ }
189
+ return segments.length === patternSegments.length ? params : null;
190
+ };
191
+ var scorePattern = (pattern) => {
192
+ let score = 0;
193
+ for (const part of pattern.split("/")) {
194
+ if (part === "*")
195
+ score -= 1000;
196
+ else if (part.startsWith(":"))
197
+ score += 1;
198
+ else
199
+ score += 10;
200
+ }
201
+ return score;
202
+ };
203
+ var matchRoute = (routes, method, pathname) => {
204
+ const segments = pathname.split("/");
205
+ let best = null;
206
+ let bestScore = -Infinity;
207
+ for (const pattern in routes) {
208
+ const methods = routes[pattern];
209
+ if (!methods || typeof methods[method] !== "function")
210
+ continue;
211
+ const params = matchSegments(pattern, segments);
212
+ if (params === null)
213
+ continue;
214
+ const score = scorePattern(pattern);
215
+ if (score > bestScore) {
216
+ bestScore = score;
217
+ best = { handler: methods[method], params };
218
+ }
219
+ }
220
+ return best;
221
+ };
222
+
223
+ // src/MarciApp.ts
224
+ class MarciApp {
225
+ routes = {};
226
+ promises = [];
227
+ prefix = "";
228
+ root = null;
229
+ server;
230
+ onListenHooks = [];
231
+ onRequestHooks = [];
232
+ add(path, method, _options, callback) {
233
+ let fullPath = this.prefix + (path.endsWith("/") ? path.slice(0, -1) : path) || "/";
234
+ if (!(fullPath in this.routes)) {
235
+ this.routes[fullPath] = {};
236
+ }
237
+ const options = Array.isArray(_options) ? getRouteOptions(method, _options) : _options;
238
+ const paramsSchema = options.params && !isDefault(options.params) ? unfoldTypeBoxSchema(options.params) : null;
239
+ const querySchema = options.query ? unfoldTypeBoxSchema(options.query) : null;
240
+ const bodyValidation = options.body ? TypeCompiler.Compile(unfoldTypeBoxSchema(options.body)) : null;
241
+ if (paramsSchema && paramsSchema.properties) {
242
+ const arrayKeys = Object.entries(paramsSchema.properties).filter((i) => i[1].type === "array").map((i) => i[0]);
243
+ if (arrayKeys.length > 0) {
244
+ paramsSchema.arrayKeys = arrayKeys;
245
+ }
246
+ }
247
+ this.routes[fullPath][method] = async (req) => {
248
+ const request = new MarciRequestInternal(req, this.root?.server ?? this.server, paramsSchema, querySchema);
249
+ for (const callback2 of this.onRequestHooks) {
250
+ await callback2(request);
251
+ }
252
+ const body = bodyValidation === null ? undefined : await parseBody(bodyValidation, req);
253
+ request.body = body;
254
+ const resp = await callback(request);
255
+ if (resp === undefined) {
256
+ return new Response;
257
+ } else if (resp instanceof Response) {
258
+ return resp;
259
+ } else {
260
+ return Response.json(resp);
261
+ }
262
+ };
263
+ }
264
+ addHook(where, callback) {
265
+ if (where === "onRequest") {
266
+ this.onRequestHooks.push(callback);
267
+ } else if (where === "onListen") {
268
+ this.onListenHooks.push(callback);
269
+ }
270
+ }
271
+ get(path, ...args) {
272
+ if (args.length === 1) {
273
+ this.add(path, "GET", {}, args[0]);
274
+ } else {
275
+ this.add(path, "GET", args[0], args[1]);
276
+ }
277
+ }
278
+ post(path, ...args) {
279
+ if (args.length === 1) {
280
+ this.add(path, "POST", {}, args[0]);
281
+ } else {
282
+ this.add(path, "POST", args[0], args[1]);
283
+ }
284
+ }
285
+ put(path, ...args) {
286
+ if (args.length === 1) {
287
+ this.add(path, "PUT", {}, args[0]);
288
+ } else {
289
+ this.add(path, "PUT", args[0], args[1]);
290
+ }
291
+ }
292
+ patch(path, ...args) {
293
+ if (args.length === 1) {
294
+ this.add(path, "PATCH", {}, args[0]);
295
+ } else {
296
+ this.add(path, "PATCH", args[0], args[1]);
297
+ }
298
+ }
299
+ delete(path, ...args) {
300
+ if (args.length === 1) {
301
+ this.add(path, "DELETE", {}, args[0]);
302
+ } else {
303
+ this.add(path, "DELETE", args[0], args[1]);
304
+ }
305
+ }
306
+ register(plugin, options = {}) {
307
+ const app = new MarciApp;
308
+ app.root = this.root ?? this;
309
+ app.routes = this.routes;
310
+ app.onListenHooks = this.onListenHooks;
311
+ app.prefix = this.prefix + (options.prefix ?? "");
312
+ const resp = plugin(app);
313
+ if (typeof resp === "object") {
314
+ this.promises.push(resp);
315
+ }
316
+ }
317
+ websocket;
318
+ websocketPath;
319
+ websocketFetch;
320
+ registerWsHandler(path, ws) {
321
+ if (typeof path === "string") {
322
+ this.websocket = ws;
323
+ this.websocketPath = path;
324
+ } else if (typeof path === "function") {
325
+ this.websocket = ws;
326
+ this.websocketFetch = path;
327
+ } else {
328
+ this.websocket = path;
329
+ }
330
+ }
331
+ fetch = async (req, server) => {
332
+ const path = new URL(req.url).pathname;
333
+ if ((!this.websocketPath || this.websocketPath.startsWith(path)) && this.websocket) {
334
+ const data = this.websocketFetch ? await this.websocketFetch(req) : {};
335
+ if (data instanceof Response) {
336
+ return data;
337
+ }
338
+ if (server.upgrade(req, { data })) {
339
+ return;
340
+ }
341
+ }
342
+ return new Response(`Route ${req.method}:${path} not found`, { status: 404 });
343
+ };
344
+ registerNotFoundHandler(handler) {
345
+ this.fetch = async (req, server) => {
346
+ const path = new URL(req.url).pathname;
347
+ if ((!this.websocketPath || this.websocketPath.startsWith(path)) && this.websocket) {
348
+ const data = this.websocketFetch ? await this.websocketFetch(req) : {};
349
+ if (data instanceof Response) {
350
+ return data;
351
+ }
352
+ if (server.upgrade(req, { data })) {
353
+ return;
354
+ }
355
+ }
356
+ const resp = await handler(path, req, server);
357
+ return resp;
358
+ };
359
+ }
360
+ async listen(port, hostname) {
361
+ if (this.promises.length > 0) {
362
+ await Promise.all(this.promises);
363
+ }
364
+ const server = Bun.serve({
365
+ routes: this.routes,
366
+ port,
367
+ hostname,
368
+ fetch: this.fetch,
369
+ websocket: this.websocket,
370
+ error: (err) => this.handleError(err)
371
+ });
372
+ this.server = server;
373
+ for (const callback of this.onListenHooks) {
374
+ await callback(server);
375
+ }
376
+ return server;
377
+ }
378
+ handleError(err) {
379
+ if (err instanceof HTTPError) {
380
+ if (err.data) {
381
+ return Response.json(err.data, { status: err.statusCode });
382
+ }
383
+ return new Response(err.message, { status: err.statusCode });
384
+ } else if (err instanceof TypeBoxError || err instanceof ValidationError) {
385
+ return new Response(getValidationError(err.error, err.where), { status: 400, headers: { "Content-Type": "application/json" } });
386
+ } else {
387
+ console.error(err);
388
+ return new Response(err.message, { status: 500 });
389
+ }
390
+ }
391
+ async inject(options) {
392
+ if (this.root)
393
+ return this.root.inject(options);
394
+ const opts = typeof options === "string" ? { url: options } : options;
395
+ if (this.promises.length > 0) {
396
+ await Promise.all(this.promises);
397
+ }
398
+ const method = (opts.method ?? "GET").toUpperCase();
399
+ const rawUrl = opts.url.includes("://") ? opts.url : "http://localhost" + (opts.url.startsWith("/") ? opts.url : "/" + opts.url);
400
+ const url = new URL(rawUrl);
401
+ const pathname = url.pathname;
402
+ const headers = new Headers(opts.headers);
403
+ let body = opts.body;
404
+ if (body !== undefined && body !== null && typeof body === "object" && !(body instanceof ReadableStream) && !(body instanceof Blob) && !(body instanceof FormData) && !(body instanceof URLSearchParams) && !(body instanceof ArrayBuffer) && !ArrayBuffer.isView(body)) {
405
+ body = JSON.stringify(body);
406
+ if (!headers.has("content-type"))
407
+ headers.set("content-type", "application/json");
408
+ }
409
+ const req = new Request(url.href, { method, headers, body: body ?? undefined });
410
+ const match = matchRoute(this.routes, method, pathname);
411
+ if (!match) {
412
+ return this.fetch(req, { upgrade: () => false });
413
+ }
414
+ req.params = match.params;
415
+ try {
416
+ return await match.handler(req);
417
+ } catch (err) {
418
+ return this.handleError(err);
419
+ }
420
+ }
421
+ }
422
+ export {
423
+ marci,
424
+ MarciApp,
425
+ HTTPError
426
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "dynara",
3
+ "type": "module",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "development": "./src/index.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "license": "MIT",
14
+ "version": "0.0.1",
15
+ "description": "Simple HTTP framework powered by Bun",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/den59k/marci.git",
19
+ "directory": "packages/marci"
20
+ },
21
+ "scripts": {
22
+ "build": "bunx bunup src/index.ts --format esm",
23
+ "test": "bun test",
24
+ "release": "bun pm version patch"
25
+ },
26
+ "devDependencies": {
27
+ "bun-plugin-dts": "^0.3.0",
28
+ "bunup": "^0.8.58"
29
+ },
30
+ "peerDependencies": {
31
+ "@types/bun": "*",
32
+ "typescript": "^5"
33
+ },
34
+ "dependencies": {
35
+ "@sinclair/typebox": "^0.34.37",
36
+ "compact-json-schema": "^0.1.4"
37
+ },
38
+ "files": [
39
+ "dist"
40
+ ]
41
+ }