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 +13 -0
- package/README.md +137 -0
- package/dist/core/app.d.ts +34 -0
- package/dist/core/autoload.d.ts +5 -0
- package/dist/core/hooks.d.ts +2 -0
- package/dist/core/loader.d.ts +1 -0
- package/dist/core/middleware.d.ts +2 -0
- package/dist/core/router.d.ts +12 -0
- package/dist/core/types.d.ts +61 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16758 -0
- package/dist/lib/http-error.d.ts +9 -0
- package/dist/lib/jwt.d.ts +2 -0
- package/dist/middlewares/cors.middleware.d.ts +4 -0
- package/dist/middlewares/error.middleware.d.ts +4 -0
- package/dist/middlewares/jwt-auth.middleware.d.ts +5 -0
- package/dist/middlewares/logger.middleware.d.ts +4 -0
- package/dist/middlewares/rate-limit.middleware.d.ts +2 -0
- package/dist/middlewares/require-auth.middleware.d.ts +4 -0
- package/dist/middlewares/validate.middleware.d.ts +6 -0
- package/dist/plugins/ejs.plugin.d.ts +6 -0
- package/dist/plugins/prisma.plugin.d.ts +5 -0
- package/dist/plugins/static.plugin.d.ts +5 -0
- package/package.json +35 -0
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 @@
|
|
|
1
|
+
export declare function scanDirectory(directory: string, extensions?: string[]): Promise<string[]>;
|
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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';
|