@alt-stack/server-bun 0.4.5

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anthony Altieri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@alt-stack/server-bun",
3
+ "version": "0.4.5",
4
+ "description": "Type-safe server framework built on Bun with Zod validation",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "bun": "./src/index.ts",
11
+ "default": "./src/index.ts"
12
+ }
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "@alt-stack/server-core": "0.4.4"
19
+ },
20
+ "peerDependencies": {
21
+ "zod": "^4.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "bun-types": "^1.2.5",
25
+ "oxlint": "^1.24.0",
26
+ "typescript": "^5.9.2",
27
+ "zod": "^4.0.0"
28
+ },
29
+ "scripts": {
30
+ "test": "bun test",
31
+ "check-types": "tsc --noEmit"
32
+ }
33
+ }
package/src/docs.ts ADDED
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ import {
3
+ router,
4
+ publicProcedure,
5
+ generateOpenAPISpec,
6
+ ok,
7
+ } from "@alt-stack/server-core";
8
+ import type { Router, GenerateOpenAPISpecOptions } from "@alt-stack/server-core";
9
+
10
+ export interface CreateDocsRouterOptions extends GenerateOpenAPISpecOptions {
11
+ openapiPath?: string;
12
+ enableDocs?: boolean;
13
+ }
14
+
15
+ const SWAGGER_UI_HTML = `<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8">
19
+ <title>API Documentation</title>
20
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui.css" />
21
+ <style>
22
+ html {
23
+ box-sizing: border-box;
24
+ overflow: -moz-scrollbars-vertical;
25
+ overflow-y: scroll;
26
+ }
27
+ *, *:before, *:after {
28
+ box-sizing: inherit;
29
+ }
30
+ body {
31
+ margin:0;
32
+ background: #fafafa;
33
+ }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div id="swagger-ui"></div>
38
+ <script src="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui-bundle.js"></script>
39
+ <script src="https://unpkg.com/swagger-ui-dist@5.10.5/swagger-ui-standalone-preset.js"></script>
40
+ <script>
41
+ window.onload = function() {
42
+ const ui = SwaggerUIBundle({
43
+ url: '{{OPENAPI_URL}}',
44
+ dom_id: '#swagger-ui',
45
+ deepLinking: true,
46
+ presets: [
47
+ SwaggerUIBundle.presets.apis,
48
+ SwaggerUIStandalonePreset
49
+ ],
50
+ plugins: [
51
+ SwaggerUIBundle.plugins.DownloadUrl
52
+ ],
53
+ layout: "StandaloneLayout"
54
+ });
55
+ };
56
+ </script>
57
+ </body>
58
+ </html>`;
59
+
60
+ export function createDocsRouter<
61
+ TCustomContext extends object = Record<string, never>,
62
+ >(
63
+ config: Record<string, Router<TCustomContext> | Router<TCustomContext>[]>,
64
+ options: CreateDocsRouterOptions = {},
65
+ ): Router<TCustomContext> {
66
+ const spec = generateOpenAPISpec(config, options);
67
+ // openapiPath should be relative (without leading slash) so it works correctly when mounted
68
+ const openapiPathOption = options.openapiPath || "openapi.json";
69
+ const openapiPath = openapiPathOption.startsWith("/")
70
+ ? openapiPathOption.slice(1)
71
+ : openapiPathOption;
72
+ const enableDocs = options.enableDocs !== false; // Default to true
73
+
74
+ // Use router() function to create a router with tRPC-style API
75
+ const docsRouterConfig: Record<string, any> = {};
76
+
77
+ // Serve OpenAPI spec as JSON
78
+ const openapiSpecPath = `/${openapiPath}`;
79
+ docsRouterConfig[openapiSpecPath] = publicProcedure
80
+ .input({})
81
+ .output(z.any())
82
+ .get(() => {
83
+ return ok(spec);
84
+ });
85
+
86
+ // Serve interactive documentation (Swagger UI)
87
+ if (enableDocs) {
88
+ docsRouterConfig["/"] = publicProcedure
89
+ .input({})
90
+ .get(async (opts) => {
91
+ // Access Bun context via ctx.bun
92
+ const bunCtx = (opts.ctx as any).bun;
93
+ const requestUrl = new URL(bunCtx.req.url);
94
+ const baseUrl = requestUrl.origin;
95
+ // Get the pathname of the current request
96
+ const currentPath = requestUrl.pathname;
97
+ // Remove trailing slash if present
98
+ const basePath =
99
+ currentPath.endsWith("/") && currentPath !== "/"
100
+ ? currentPath.slice(0, -1)
101
+ : currentPath;
102
+ // Construct openapi URL at the same mount level
103
+ const openapiUrl = `${baseUrl}${basePath}/${openapiPath}`;
104
+ const html = SWAGGER_UI_HTML.replace("{{OPENAPI_URL}}", openapiUrl);
105
+ // Return HTML response with correct content type
106
+ return ok(
107
+ new Response(html, {
108
+ headers: { "Content-Type": "text/html" },
109
+ }),
110
+ );
111
+ });
112
+ }
113
+
114
+ return router<TCustomContext>(docsRouterConfig);
115
+ }
package/src/index.ts ADDED
@@ -0,0 +1,142 @@
1
+ import {
2
+ Router as BaseRouter,
3
+ router as baseRouter,
4
+ createRouter as baseCreateRouter,
5
+ mergeRouters as baseMergeRouters,
6
+ } from "@alt-stack/server-core";
7
+ import type { BunBaseContext } from "./types.ts";
8
+
9
+ // Re-export everything from server-core except Router and router utilities (which we override)
10
+ export {
11
+ // Init
12
+ init,
13
+ publicProcedure,
14
+ default400ErrorSchema,
15
+ default500ErrorSchema,
16
+ // Result utilities
17
+ ok,
18
+ err,
19
+ isOk,
20
+ isErr,
21
+ map,
22
+ flatMap,
23
+ mapError,
24
+ catchError,
25
+ unwrap,
26
+ unwrapOr,
27
+ unwrapOrElse,
28
+ match,
29
+ fold,
30
+ tryCatch,
31
+ tryCatchAsync,
32
+ isResultError,
33
+ assertResultError,
34
+ ResultAggregateError,
35
+ TaggedError,
36
+ // Middleware
37
+ createMiddleware,
38
+ createMiddlewareWithErrors,
39
+ middlewareMarker,
40
+ middlewareOk,
41
+ // Procedure builders
42
+ BaseProcedureBuilder,
43
+ ProcedureBuilder,
44
+ // OpenAPI
45
+ generateOpenAPISpec,
46
+ // Validation
47
+ validateInput,
48
+ parseSchema,
49
+ mergeInputs,
50
+ // Telemetry
51
+ resolveTelemetryConfig,
52
+ shouldIgnoreRoute,
53
+ initTelemetry,
54
+ createRequestSpan,
55
+ endSpanWithError,
56
+ setSpanOk,
57
+ // Error extraction
58
+ extractTagsFromSchema,
59
+ findHttpStatusForError,
60
+ } from "@alt-stack/server-core";
61
+
62
+ // Re-export all types from server-core
63
+ export type {
64
+ Result,
65
+ Ok,
66
+ Err,
67
+ ResultError,
68
+ InferErrorTag,
69
+ InferErrorTags,
70
+ NarrowError,
71
+ InitOptions,
72
+ InitResult,
73
+ MiddlewareFunction,
74
+ MiddlewareBuilder,
75
+ MiddlewareResult,
76
+ MiddlewareResultSuccess,
77
+ MiddlewareFunctionWithErrors,
78
+ MiddlewareBuilderWithErrors,
79
+ MiddlewareBuilderWithErrorsStaged,
80
+ AnyMiddlewareBuilderWithErrors,
81
+ AnyMiddlewareFunctionWithErrors,
82
+ Overwrite,
83
+ OpenAPISpec,
84
+ GenerateOpenAPISpecOptions,
85
+ OpenAPIPathItem,
86
+ OpenAPIOperation,
87
+ OpenAPIParameter,
88
+ OpenAPIRequestBody,
89
+ OpenAPIResponse,
90
+ ParseResult,
91
+ StructuredInput,
92
+ TelemetryConfig,
93
+ TelemetryOption,
94
+ ResolvedTelemetryConfig,
95
+ Span,
96
+ InputConfig,
97
+ TypedContext,
98
+ BaseContext,
99
+ InferInput,
100
+ Procedure,
101
+ ReadyProcedure,
102
+ PendingProcedure,
103
+ } from "@alt-stack/server-core";
104
+
105
+ // Pre-typed Router with Bun context baked in
106
+ export class Router<
107
+ TCustomContext extends BunBaseContext = BunBaseContext,
108
+ > extends BaseRouter<TCustomContext> {}
109
+
110
+ /**
111
+ * Pre-typed router function with Bun context as default.
112
+ * Routes defined with this function will have `ctx.bun` properly typed.
113
+ */
114
+ export function router<TCustomContext extends BunBaseContext = BunBaseContext>(
115
+ config: Parameters<typeof baseRouter<TCustomContext>>[0],
116
+ ): Router<TCustomContext> {
117
+ return baseRouter<TCustomContext>(config) as Router<TCustomContext>;
118
+ }
119
+
120
+ /**
121
+ * Pre-typed createRouter function with Bun context as default.
122
+ */
123
+ export function createRouter<TCustomContext extends BunBaseContext = BunBaseContext>(
124
+ config?: Record<string, Router<TCustomContext> | Router<TCustomContext>[]>,
125
+ ): Router<TCustomContext> {
126
+ return baseCreateRouter<TCustomContext>(config) as Router<TCustomContext>;
127
+ }
128
+
129
+ /**
130
+ * Pre-typed mergeRouters function with Bun context as default.
131
+ */
132
+ export function mergeRouters<TCustomContext extends BunBaseContext = BunBaseContext>(
133
+ ...routers: Router<TCustomContext>[]
134
+ ): Router<TCustomContext> {
135
+ return baseMergeRouters<TCustomContext>(...routers) as Router<TCustomContext>;
136
+ }
137
+
138
+ // Export Bun-specific functionality
139
+ export { createServer } from "./server.ts";
140
+ export { createDocsRouter } from "./docs.ts";
141
+ export type { CreateDocsRouterOptions } from "./docs.ts";
142
+ export type { BunBaseContext, BunServer } from "./types.ts";
package/src/router.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { BunServer } from "./types.ts";
2
+
3
+ /**
4
+ * Route definition for internal routing
5
+ */
6
+ interface RouteDefinition {
7
+ method: string;
8
+ path: string;
9
+ pattern: RegExp;
10
+ paramNames: string[];
11
+ handler: (
12
+ req: Request,
13
+ params: Record<string, string>,
14
+ server: BunServer,
15
+ ) => Promise<Response>;
16
+ }
17
+
18
+ /**
19
+ * Simple router for Bun that handles path matching and parameter extraction.
20
+ * Converts OpenAPI-style paths ({param}) to regex patterns for matching.
21
+ */
22
+ export class BunRouter {
23
+ private routes: RouteDefinition[] = [];
24
+
25
+ /**
26
+ * Register a route handler
27
+ * @param method HTTP method (GET, POST, PUT, PATCH, DELETE)
28
+ * @param path OpenAPI-style path (e.g., /users/{id})
29
+ * @param handler Request handler function
30
+ */
31
+ register(
32
+ method: string,
33
+ path: string,
34
+ handler: (
35
+ req: Request,
36
+ params: Record<string, string>,
37
+ server: BunServer,
38
+ ) => Promise<Response>,
39
+ ): void {
40
+ const { pattern, paramNames } = this.compilePath(path);
41
+ this.routes.push({ method, path, pattern, paramNames, handler });
42
+ }
43
+
44
+ /**
45
+ * Match a request to a registered route and execute its handler
46
+ * @param req The incoming Request
47
+ * @param server The Bun server instance
48
+ * @returns Response if a route matches, null otherwise
49
+ */
50
+ async handle(req: Request, server: BunServer): Promise<Response | null> {
51
+ const url = new URL(req.url);
52
+ const method = req.method;
53
+ const pathname = url.pathname;
54
+
55
+ for (const route of this.routes) {
56
+ if (route.method !== method) continue;
57
+
58
+ const match = pathname.match(route.pattern);
59
+ if (!match) continue;
60
+
61
+ // Extract parameters from the match
62
+ const params: Record<string, string> = {};
63
+ route.paramNames.forEach((name, index) => {
64
+ params[name] = decodeURIComponent(match[index + 1] ?? "");
65
+ });
66
+
67
+ return route.handler(req, params, server);
68
+ }
69
+
70
+ return null; // No matching route
71
+ }
72
+
73
+ /**
74
+ * Compile an OpenAPI-style path to a regex pattern
75
+ * Examples:
76
+ * - /users -> /^\/users$/
77
+ * - /users/{id} -> /^\/users\/([^\/]+)$/
78
+ * - /items/{category}/{id} -> /^\/items\/([^\/]+)\/([^\/]+)$/
79
+ */
80
+ private compilePath(path: string): {
81
+ pattern: RegExp;
82
+ paramNames: string[];
83
+ } {
84
+ const paramNames: string[] = [];
85
+
86
+ // Escape special regex characters except { and }
87
+ let regexStr = path.replace(/[.+?^$|()[\]\\]/g, "\\$&");
88
+
89
+ // Replace {param} with capture groups
90
+ regexStr = regexStr.replace(/\{([^}]+)\}/g, (_, paramName) => {
91
+ paramNames.push(paramName);
92
+ return "([^/]+)";
93
+ });
94
+
95
+ // Anchor the pattern
96
+ const pattern = new RegExp(`^${regexStr}$`);
97
+
98
+ return { pattern, paramNames };
99
+ }
100
+ }