pronghorn 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Pronghorn 🦌
2
+
3
+ Pronghorn is a fast, lightweight, TypeScript-first backend framework built for [Bun](https://bun.sh/). Like its namesake, it's designed for speed and agility, with a clean API, zero-config setup, and a modular plugin system. Pronghorn brings together the simplicity of [Express](https://expressjs.com/) and the performance-first mindset of [Fastify](https://fastify.dev/), giving developers a streamlined way to build modern, scalable applications.
4
+
5
+ > Built exclusively for Bun, no Node.js support, by design.> Built exclusively for Bun, no Node.js support, by design.
6
+
7
+ ## Why Pronghorn
8
+
9
+ Pronghorn avoids two failure modes seen in other Bun frameworks: forced file-based conventions, and unecessary Node-era abstractions layered on top of what Bun's own `Bun.serve` already provides.
10
+ - Routes, middleware, and plugins are all defined programmatically, and registered via plain method calls (`app.get(...)`, `app.use(...)`, `app.register(...)`).
11
+ - Filesystem-convention discovery is available as an opt-in layer, fully composable with manual registration.
12
+ - It sits directly on top of `Bun.serve`, with a typed `Context` wrapping the standard `Request`/`Response` Web APIs.
13
+ - Middleware declares its own scope (`"global"` or `"route"`), so you can control exactly where each one runs.
14
+ - Pluggable, not batteries-included, it ships with generic building blocks (CORS, logging, JWT parsing, rate limiting, validation, EJS, static files) with zero opinions about your database or business logic.
15
+ - First-class WebSockets via `app.ws(path, handlers)`, dispatched through Bun's native WebSocket support.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ bun add pronghorn
21
+ ```
22
+
23
+ Requires Bun `>=1.3.0`. Installing under Node.js fails intentionally during `preinstall`.
24
+
25
+ ## Quick Start
26
+
27
+ ```ts
28
+ import { createApp, cors, logger, errorHandler } from 'pronghorn'
29
+
30
+ const app = createApp()
31
+
32
+ app.use(cors)
33
+ app.use(logger)
34
+ app.use(errorHandler)
35
+
36
+ app.get('/', context => context.json({ message: 'Hello from Pronghorn 🦌' }))
37
+
38
+ const server = await app.listen(4000)
39
+ console.log(`Server running at http://localhost:${server.port}`)
40
+ ```
41
+
42
+ ## Core concepts
43
+
44
+ ### Routes
45
+
46
+ Registered via `app.get/post/put/patch/delete(path, handler, options?)`, with `:param` dynamic segments and optional per-route middlewares/schema.
47
+
48
+ ### Middleware
49
+
50
+ Shape: `(context, next) => Response`. Each middleware exports a scope, `"global"` (applied via `app.use()`, runs on every request) or `"route"` (never auto-applied, opt in via a route's middlewares option). This is why `createJwtAuth` (global, non-blocking token decode) and `requireAuth` (route-scoped, blocking) are separate exports.
51
+
52
+ ### Plugins
53
+
54
+ Plain functions receiving the shared AppContext, which can call `app.decorate(name, value)` to attach anything app-wide: a Database client, a render function, a logger. `app.register()` awaits async plugins before the server starts.
55
+
56
+ ### Decorators
57
+
58
+ Anything decorated in a plugin is readable in any request via `context.get<T>(name)`, this is how `ejsPlugin` exposes `render`, `prismaPlugin` exposes `prisma`, and `staticPlugin` exposes `serveStatic`.
59
+
60
+ ### Hooks
61
+
62
+ `app.addHook(name, handler)` for `"onRequest"`, `"preHandler"`, or `"onResponse"`, fixed lifecycle points independent of the middleware chain.
63
+
64
+ ### WebSockets
65
+
66
+ `app.ws(path, handlers)` registers `open`/`message`/`close` handlers, dispatched by path through Bun's single native websocket object.
67
+
68
+ ### Schema validation
69
+
70
+ Pass a Zod schema via a route's schema option; Pronghorn validates automatically and populates `context.locals.body`/`context.locals.query`, throwing a structured `HttpError(400)` on failure.
71
+
72
+ ### Error handling
73
+
74
+ Throw `HttpError` or use `badRequest`/`unauthorized`/`forbidden`/`notFound`; the `errorHandler` middleware converts these into clean JSON responses.
75
+
76
+ ### Graceful shutdown
77
+
78
+ `app.onClose(handler)` registers cleanup logic that runs automatically on `SIGINT`/`SIGTERM` before the server stops.
79
+
80
+ ## Autoloading (optional)
81
+
82
+ ```ts
83
+ import { createApp, autoloadRoutes, autoloadMiddlewares, autoloadPlugins, autoloadHooks } from 'pronghorn'
84
+ import { join } from 'node:path'
85
+
86
+ const app = createApp()
87
+
88
+ await autoloadPlugins(app, join(__dirname, 'plugins'))
89
+ await autoloadMiddlewares(app, join(__dirname, 'middlewares'))
90
+ await autoloadHooks(app, join(__dirname, 'hooks'))
91
+ await autoloadRoutes(app, join(__dirname, 'routes'))
92
+
93
+ await app.listen(4000)
94
+ ```
95
+
96
+ Route files use a `.method.ts` suffix (`.get.ts`, `.post.ts`, etc.), folder structure maps to path segments, `index` files map to the parent path, and `[param]` folders/files become `:param`.
97
+
98
+ ## Built-in middleware
99
+
100
+ | Export | Scope | Description |
101
+ | ---------------------------- | ---------------- | ------------------------------------------------------------- |
102
+ | `cors` | global | CORS headers + `OPTIONS` preflight handling |
103
+ | `logger` | global | Logs method, path, status, duration |
104
+ | `errorHandler` | global | Converts thrown `HttpErrors` into JSON responses |
105
+ | createJwtAuth({ secret }) | global (factory) | Decodes `Bearer` JWT into `context.locals.user`, never blocks |
106
+ | `requireAuth` | route | Rejects with `401` unless `context.locals.user` is set |
107
+ | `rateLimit(limit, windowMs)` | route (factory) | In-memory IP-keyed token-bucket limiter |
108
+ | `validate(schema)` | - | Applied automatically via a route's `schema` option |
109
+
110
+ ## Built-in plugins
111
+
112
+ | Export | Decorates | Description |
113
+ | ------------ | ----------- | ------------------------------------------------------------------------------------------ |
114
+ | ejsPlugin | render | Renders `.ejs` views, optional shared layout |
115
+ | staticPlugin | serveStatic | Serves files from a public directory with path-traversal protection |
116
+ | prismaPlugin | prisma | Attaches any DB client you pass in, no `@prisma/client` dependency inside Pronghorm itself |
117
+
118
+ ## API reference
119
+
120
+ `createApp(): App` creates a new application instance.
121
+
122
+ | Method | Description |
123
+ | ----------------------------------------------------- | -------------------------------------------- |
124
+ | `.use(middleware, scope?)` | Registers global middleware |
125
+ | `.register(plugin, options?)` | Runs a plugin against the shared app context |
126
+ | `.get/post/put/patch/delete(path, handler, options?)` | Registers a route |
127
+ | `.ws(path, handlers)` | Registers a WebSocket route |
128
+ | `.addHook(name, handler)` | Registers a lifecycle hook |
129
+ | `.onClose(handler)` | Registers shutdown cleanup logic |
130
+ | `.listen(port)` | Starts the Bun server |
131
+ | `.close()` | Stops the server manually |
132
+
133
+ `Context` exposes `request`, `params`, `query`, `locals`, `get<T>(name)`, `json(data, status?)`, and `redirect(url, status?)`.
134
+
135
+ ## License
136
+
137
+ [WTFPL](https://www.wtfpl.net/) (Do What the Fuck You Want to Public License), see [LICENSE](./LICENSE) for details.
@@ -0,0 +1,34 @@
1
+ import type { AppContext, Dispatcher, Handler, Hook, HookName, Method, Middleware, MiddlewareScope, PluginFn, RouteOptions, WebSocketHandlers, WebSocketData, CloseHandler } from '../core/types';
2
+ import { type Route } from '../core/router';
3
+ import type { Server } from 'bun';
4
+ export declare class App {
5
+ private globalMiddlewares;
6
+ private routes;
7
+ private wsRoutes;
8
+ private hooks;
9
+ private closeHandlers;
10
+ private decorators;
11
+ private store;
12
+ private server;
13
+ readonly context: AppContext;
14
+ use(middleware: Middleware, scope?: MiddlewareScope): this;
15
+ register(plugin: PluginFn, options?: Record<string, unknown>): Promise<this>;
16
+ addHook(name: HookName, handler: Hook): this;
17
+ onClose(handler: CloseHandler): this;
18
+ route(method: Method, path: string, handler: Handler, options?: RouteOptions): this;
19
+ get(path: string, handler: Handler, options?: RouteOptions): this;
20
+ post(path: string, handler: Handler, options?: RouteOptions): this;
21
+ put(path: string, handler: Handler, options?: RouteOptions): this;
22
+ patch(path: string, handler: Handler, options?: RouteOptions): this;
23
+ delete(path: string, handler: Handler, options?: RouteOptions): this;
24
+ ws(path: string, handlers: WebSocketHandlers): this;
25
+ getWsRoute(path: string): WebSocketHandlers | undefined;
26
+ hasWsRoute(path: string): boolean;
27
+ dispatcher(): Dispatcher;
28
+ private buildContext;
29
+ listen(port: number): Promise<Server<WebSocketData>>;
30
+ close(): void;
31
+ getRoutes(): Route[];
32
+ getDecorators(): Record<string, unknown>;
33
+ }
34
+ export declare function createApp(): App;
@@ -0,0 +1,5 @@
1
+ import type { App } from './app';
2
+ export declare function autoloadPlugins(app: App, directory: string): Promise<void>;
3
+ export declare function autoloadMiddlewares(app: App, directory: string): Promise<void>;
4
+ export declare function autoloadHooks(app: App, directory: string): Promise<void>;
5
+ export declare function autoloadRoutes(app: App, directory: string): Promise<void>;
@@ -0,0 +1,2 @@
1
+ import type { Hook, Context } from './types';
2
+ export declare function runHooks(hooks: Hook[], context: Context): Promise<void>;
@@ -0,0 +1 @@
1
+ export declare function scanDirectory(directory: string, extensions?: string[]): Promise<string[]>;
@@ -0,0 +1,2 @@
1
+ import type { Middleware, Dispatcher } from './types';
2
+ export declare function composeMiddlewares(middlewares: Middleware[], finalHandler: Dispatcher): Dispatcher;
@@ -0,0 +1,12 @@
1
+ import type { Handler, Middleware, Method, RouteSchema } from './types';
2
+ export interface Route {
3
+ method: Method;
4
+ segments: string[];
5
+ handler: Handler;
6
+ middlewares: Middleware[];
7
+ schema?: RouteSchema;
8
+ }
9
+ export declare function matchRoute(routes: Route[], method: string, pathname: string): {
10
+ route: Route;
11
+ params: Record<string, string>;
12
+ } | null;
@@ -0,0 +1,61 @@
1
+ import type { ServerWebSocket } from 'bun';
2
+ import type { ZodType } from 'zod';
3
+ export interface Context {
4
+ request: Request;
5
+ params: Record<string, string>;
6
+ query: URLSearchParams;
7
+ locals: Record<string, unknown>;
8
+ decorators: Record<string, unknown>;
9
+ get: <T = unknown>(name: string) => T;
10
+ json: (data: unknown, status?: number) => Response;
11
+ redirect: (url: string, status?: number) => Response;
12
+ }
13
+ export interface AppContext {
14
+ set: (key: string, value: unknown) => void;
15
+ get: <T = unknown>(key: string) => T;
16
+ decorate: (name: string, value: unknown) => void;
17
+ decorators: Record<string, unknown>;
18
+ }
19
+ export type PluginFn = (app: AppContext, options?: Record<string, unknown>) => Promise<void> | void;
20
+ export type MiddlewareScope = 'global' | 'route';
21
+ export type Next = () => Promise<Response>;
22
+ export type Middleware = (context: Context, next: Next) => Promise<Response>;
23
+ export type Handler = (context: Context) => Promise<Response> | Response;
24
+ export type Dispatcher = (context: Context) => Promise<Response>;
25
+ export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head';
26
+ export interface RouteSchema {
27
+ body?: ZodType;
28
+ query?: ZodType;
29
+ }
30
+ export interface RouteOptions {
31
+ middlewares?: Middleware[];
32
+ schema?: RouteSchema;
33
+ }
34
+ export type HookName = 'onRequest' | 'preHandler' | 'onResponse';
35
+ export type Hook = (context: Context) => Promise<void> | void;
36
+ export type CloseHandler = () => Promise<void> | void;
37
+ export interface WebSocketData {
38
+ path: string;
39
+ }
40
+ export interface WebSocketHandlers {
41
+ open?: (ws: ServerWebSocket<WebSocketData>) => void;
42
+ message?: (ws: ServerWebSocket<WebSocketData>, message: string | Buffer) => void;
43
+ close?: (ws: ServerWebSocket<WebSocketData>) => void;
44
+ }
45
+ export interface MiddlewareFileModule {
46
+ default: Middleware;
47
+ scope?: MiddlewareScope;
48
+ }
49
+ export interface PluginFileModule {
50
+ default: PluginFn;
51
+ options?: Record<string, unknown>;
52
+ }
53
+ export interface RouteFileModule {
54
+ default: Handler;
55
+ middlewares?: Middleware[];
56
+ schema?: RouteSchema;
57
+ }
58
+ export interface HookFileModule {
59
+ name: HookName;
60
+ handler: Hook;
61
+ }
@@ -0,0 +1,16 @@
1
+ export { createApp, App } from './core/app';
2
+ export { composeMiddlewares } from './core/middleware';
3
+ export { autoloadPlugins, autoloadMiddlewares, autoloadHooks, autoloadRoutes } from './core/autoload';
4
+ export { HttpError, badRequest, unauthorized, forbidden, notFound } from './lib/http-error';
5
+ export { signToken, verifyToken } from './lib/jwt';
6
+ export { validate } from './middlewares/validate.middleware';
7
+ export { default as cors } from './middlewares/cors.middleware';
8
+ export { default as logger } from './middlewares/logger.middleware';
9
+ export { default as errorHandler } from './middlewares/error.middleware';
10
+ export { createJwtAuth } from './middlewares/jwt-auth.middleware';
11
+ export { default as requireAuth } from './middlewares/require-auth.middleware';
12
+ export { rateLimit } from './middlewares/rate-limit.middleware';
13
+ export { ejsPlugin } from './plugins/ejs.plugin';
14
+ export { staticPlugin } from './plugins/static.plugin';
15
+ export { prismaPlugin } from './plugins/prisma.plugin';
16
+ export type * from './core/types';