@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 +21 -0
- package/package.json +33 -0
- package/src/docs.ts +115 -0
- package/src/index.ts +142 -0
- package/src/router.ts +100 -0
- package/src/server.spec.ts +458 -0
- package/src/server.ts +527 -0
- package/src/types.spec.ts +40 -0
- package/src/types.ts +18 -0
- package/tsconfig.json +12 -0
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
|
+
}
|