@bunary/http 0.0.2 → 0.0.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/CHANGELOG.md +40 -0
- package/README.md +170 -3
- package/dist/app.d.ts +7 -25
- package/dist/app.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +344 -42
- package/dist/pathUtils.d.ts +34 -0
- package/dist/pathUtils.d.ts.map +1 -0
- package/dist/response.d.ts +26 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/router.d.ts +49 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/routes/builder.d.ts +17 -0
- package/dist/routes/builder.d.ts.map +1 -0
- package/dist/routes/find.d.ts +27 -0
- package/dist/routes/find.d.ts.map +1 -0
- package/dist/routes/group.d.ts +7 -0
- package/dist/routes/group.d.ts.map +1 -0
- package/dist/routes/index.d.ts +4 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/types/appOptions.d.ts +8 -0
- package/dist/types/appOptions.d.ts.map +1 -0
- package/dist/types/bunaryApp.d.ts +97 -0
- package/dist/types/bunaryApp.d.ts.map +1 -0
- package/dist/types/bunaryServer.d.ts +14 -0
- package/dist/types/bunaryServer.d.ts.map +1 -0
- package/dist/types/groupOptions.d.ts +13 -0
- package/dist/types/groupOptions.d.ts.map +1 -0
- package/dist/types/groupRouter.d.ts +26 -0
- package/dist/types/groupRouter.d.ts.map +1 -0
- package/dist/types/handlerResponse.d.ts +8 -0
- package/dist/types/handlerResponse.d.ts.map +1 -0
- package/dist/types/httpMethod.d.ts +5 -0
- package/dist/types/httpMethod.d.ts.map +1 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/middleware.d.ts +21 -0
- package/dist/types/middleware.d.ts.map +1 -0
- package/dist/types/pathParams.d.ts +6 -0
- package/dist/types/pathParams.d.ts.map +1 -0
- package/dist/types/queryParams.d.ts +5 -0
- package/dist/types/queryParams.d.ts.map +1 -0
- package/dist/types/requestContext.d.ts +36 -0
- package/dist/types/requestContext.d.ts.map +1 -0
- package/dist/types/route.d.ts +27 -0
- package/dist/types/route.d.ts.map +1 -0
- package/dist/types/routeBuilder.d.ts +24 -0
- package/dist/types/routeBuilder.d.ts.map +1 -0
- package/dist/types/routeHandler.d.ts +17 -0
- package/dist/types/routeHandler.d.ts.map +1 -0
- package/dist/types/routeInfo.d.ts +13 -0
- package/dist/types/routeInfo.d.ts.map +1 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,26 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
function compilePath(path) {
|
|
4
|
-
const paramNames = [];
|
|
5
|
-
const regexString = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, paramName) => {
|
|
6
|
-
paramNames.push(paramName);
|
|
7
|
-
return "([^/]+)";
|
|
8
|
-
});
|
|
9
|
-
return {
|
|
10
|
-
pattern: new RegExp(`^${regexString}$`),
|
|
11
|
-
paramNames
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function extractParams(path, route) {
|
|
15
|
-
const match = path.match(route.pattern);
|
|
16
|
-
if (!match)
|
|
17
|
-
return {};
|
|
18
|
-
const params = {};
|
|
19
|
-
for (let i = 0;i < route.paramNames.length; i++) {
|
|
20
|
-
params[route.paramNames[i]] = match[i + 1];
|
|
21
|
-
}
|
|
22
|
-
return params;
|
|
23
|
-
}
|
|
2
|
+
// src/response.ts
|
|
24
3
|
function toResponse(result) {
|
|
25
4
|
if (result instanceof Response) {
|
|
26
5
|
return result;
|
|
@@ -45,34 +24,296 @@ function toResponse(result) {
|
|
|
45
24
|
headers: { "Content-Type": "application/json" }
|
|
46
25
|
});
|
|
47
26
|
}
|
|
27
|
+
|
|
28
|
+
// src/router.ts
|
|
29
|
+
function compilePath(path) {
|
|
30
|
+
const paramNames = [];
|
|
31
|
+
const optionalParams = [];
|
|
32
|
+
let regexString = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
regexString = regexString.replace(/\/:([a-zA-Z_][a-zA-Z0-9_]*)(\\\?)?/g, (_match, paramName, isOptional) => {
|
|
34
|
+
if (paramNames.includes(paramName)) {
|
|
35
|
+
throw new Error(`Duplicate parameter name ":${paramName}" in route pattern "${path}". Each parameter name must be unique within a route.`);
|
|
36
|
+
}
|
|
37
|
+
paramNames.push(paramName);
|
|
38
|
+
if (isOptional) {
|
|
39
|
+
optionalParams.push(paramName);
|
|
40
|
+
return "(?:/([^/]+))?";
|
|
41
|
+
}
|
|
42
|
+
return "/([^/]+)";
|
|
43
|
+
});
|
|
44
|
+
regexString += "/?";
|
|
45
|
+
return {
|
|
46
|
+
pattern: new RegExp(`^${regexString}$`),
|
|
47
|
+
paramNames,
|
|
48
|
+
optionalParams
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function extractParams(path, route) {
|
|
52
|
+
const match = path.match(route.pattern);
|
|
53
|
+
if (!match)
|
|
54
|
+
return {};
|
|
55
|
+
const params = {};
|
|
56
|
+
for (let i = 0;i < route.paramNames.length; i++) {
|
|
57
|
+
const value = match[i + 1];
|
|
58
|
+
if (value !== undefined && value !== "") {
|
|
59
|
+
params[route.paramNames[i]] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return params;
|
|
63
|
+
}
|
|
64
|
+
function checkConstraints(params, constraints) {
|
|
65
|
+
if (!constraints)
|
|
66
|
+
return true;
|
|
67
|
+
for (const [param, pattern] of Object.entries(constraints)) {
|
|
68
|
+
const value = params[param];
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
if (!pattern.test(value))
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/routes/builder.ts
|
|
78
|
+
function compilePattern(pattern, param) {
|
|
79
|
+
try {
|
|
80
|
+
return new RegExp(pattern);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message = error instanceof Error ? error.message : "Invalid pattern";
|
|
83
|
+
throw new Error(`Invalid regex pattern for parameter "${param}": ${message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function createRouteBuilder(route, namedRoutes, app) {
|
|
87
|
+
function addConstraint(param, pattern) {
|
|
88
|
+
if (!route.constraints) {
|
|
89
|
+
route.constraints = {};
|
|
90
|
+
}
|
|
91
|
+
route.constraints[param] = pattern;
|
|
92
|
+
}
|
|
93
|
+
const builder = {
|
|
94
|
+
get get() {
|
|
95
|
+
return app.get;
|
|
96
|
+
},
|
|
97
|
+
get post() {
|
|
98
|
+
return app.post;
|
|
99
|
+
},
|
|
100
|
+
get put() {
|
|
101
|
+
return app.put;
|
|
102
|
+
},
|
|
103
|
+
get delete() {
|
|
104
|
+
return app.delete;
|
|
105
|
+
},
|
|
106
|
+
get patch() {
|
|
107
|
+
return app.patch;
|
|
108
|
+
},
|
|
109
|
+
get use() {
|
|
110
|
+
return app.use;
|
|
111
|
+
},
|
|
112
|
+
get group() {
|
|
113
|
+
return app.group;
|
|
114
|
+
},
|
|
115
|
+
get route() {
|
|
116
|
+
return app.route;
|
|
117
|
+
},
|
|
118
|
+
get hasRoute() {
|
|
119
|
+
return app.hasRoute;
|
|
120
|
+
},
|
|
121
|
+
get getRoutes() {
|
|
122
|
+
return app.getRoutes;
|
|
123
|
+
},
|
|
124
|
+
get listen() {
|
|
125
|
+
return app.listen;
|
|
126
|
+
},
|
|
127
|
+
get fetch() {
|
|
128
|
+
return app.fetch;
|
|
129
|
+
},
|
|
130
|
+
name: (name) => {
|
|
131
|
+
if (namedRoutes.has(name)) {
|
|
132
|
+
throw new Error(`Route name "${name}" is already defined`);
|
|
133
|
+
}
|
|
134
|
+
route.name = name;
|
|
135
|
+
namedRoutes.set(name, route);
|
|
136
|
+
return builder;
|
|
137
|
+
},
|
|
138
|
+
where: (paramOrConstraints, pattern) => {
|
|
139
|
+
if (typeof paramOrConstraints === "string") {
|
|
140
|
+
if (!pattern) {
|
|
141
|
+
throw new Error(`Pattern is required for constraint on "${paramOrConstraints}"`);
|
|
142
|
+
}
|
|
143
|
+
const regex = typeof pattern === "string" ? compilePattern(pattern, paramOrConstraints) : pattern;
|
|
144
|
+
addConstraint(paramOrConstraints, regex);
|
|
145
|
+
} else {
|
|
146
|
+
for (const [param, pat] of Object.entries(paramOrConstraints)) {
|
|
147
|
+
const regex = typeof pat === "string" ? compilePattern(pat, param) : pat;
|
|
148
|
+
addConstraint(param, regex);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return builder;
|
|
152
|
+
},
|
|
153
|
+
whereNumber: (param) => {
|
|
154
|
+
addConstraint(param, /^\d+$/);
|
|
155
|
+
return builder;
|
|
156
|
+
},
|
|
157
|
+
whereAlpha: (param) => {
|
|
158
|
+
addConstraint(param, /^[a-zA-Z]+$/);
|
|
159
|
+
return builder;
|
|
160
|
+
},
|
|
161
|
+
whereAlphaNumeric: (param) => {
|
|
162
|
+
addConstraint(param, /^[a-zA-Z0-9]+$/);
|
|
163
|
+
return builder;
|
|
164
|
+
},
|
|
165
|
+
whereUuid: (param) => {
|
|
166
|
+
addConstraint(param, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
167
|
+
return builder;
|
|
168
|
+
},
|
|
169
|
+
whereUlid: (param) => {
|
|
170
|
+
addConstraint(param, /^[0-9A-HJKMNP-TV-Z]{26}$/);
|
|
171
|
+
return builder;
|
|
172
|
+
},
|
|
173
|
+
whereIn: (param, values) => {
|
|
174
|
+
if (values.length === 0) {
|
|
175
|
+
throw new Error(`whereIn requires at least one value for parameter "${param}"`);
|
|
176
|
+
}
|
|
177
|
+
const escaped = values.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
178
|
+
addConstraint(param, new RegExp(`^(${escaped.join("|")})$`));
|
|
179
|
+
return builder;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
return builder;
|
|
183
|
+
}
|
|
184
|
+
function wrapBuilderWithNamePrefix(builder, namePrefix) {
|
|
185
|
+
if (!namePrefix)
|
|
186
|
+
return builder;
|
|
187
|
+
return new Proxy(builder, {
|
|
188
|
+
get(target, prop) {
|
|
189
|
+
if (prop === "name") {
|
|
190
|
+
return (name) => {
|
|
191
|
+
return target.name(namePrefix + name);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return target[prop];
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
// src/routes/find.ts
|
|
199
|
+
function findRoute(routes, method, path) {
|
|
200
|
+
for (const route of routes) {
|
|
201
|
+
if (route.pattern.test(path)) {
|
|
202
|
+
if (route.method === method) {
|
|
203
|
+
const params = extractParams(path, route);
|
|
204
|
+
if (!checkConstraints(params, route.constraints)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
return { route, params };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function hasMatchingPath(routes, path) {
|
|
214
|
+
return routes.some((route) => {
|
|
215
|
+
if (!route.pattern.test(path))
|
|
216
|
+
return false;
|
|
217
|
+
const params = extractParams(path, route);
|
|
218
|
+
return checkConstraints(params, route.constraints);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// src/pathUtils.ts
|
|
222
|
+
function normalizePrefix(prefix) {
|
|
223
|
+
let normalized = prefix;
|
|
224
|
+
if (!normalized.startsWith("/")) {
|
|
225
|
+
normalized = `/${normalized}`;
|
|
226
|
+
}
|
|
227
|
+
if (normalized.endsWith("/") && normalized.length > 1) {
|
|
228
|
+
normalized = normalized.slice(0, -1);
|
|
229
|
+
}
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
function joinPaths(prefix, path) {
|
|
233
|
+
const normalizedPrefix = normalizePrefix(prefix);
|
|
234
|
+
let normalizedPath = path;
|
|
235
|
+
if (!normalizedPath.startsWith("/") && normalizedPath !== "") {
|
|
236
|
+
normalizedPath = `/${normalizedPath}`;
|
|
237
|
+
}
|
|
238
|
+
if (normalizedPath === "/") {
|
|
239
|
+
return normalizedPrefix;
|
|
240
|
+
}
|
|
241
|
+
return normalizedPrefix + normalizedPath;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/routes/group.ts
|
|
245
|
+
function createGroupRouter(prefix, groupMiddleware, namePrefix, addRoute) {
|
|
246
|
+
const router = {
|
|
247
|
+
get: (path, handler) => {
|
|
248
|
+
const fullPath = joinPaths(prefix, path);
|
|
249
|
+
const builder = addRoute("GET", fullPath, handler, groupMiddleware);
|
|
250
|
+
return wrapBuilderWithNamePrefix(builder, namePrefix);
|
|
251
|
+
},
|
|
252
|
+
post: (path, handler) => {
|
|
253
|
+
const fullPath = joinPaths(prefix, path);
|
|
254
|
+
return wrapBuilderWithNamePrefix(addRoute("POST", fullPath, handler, groupMiddleware), namePrefix);
|
|
255
|
+
},
|
|
256
|
+
put: (path, handler) => {
|
|
257
|
+
const fullPath = joinPaths(prefix, path);
|
|
258
|
+
return wrapBuilderWithNamePrefix(addRoute("PUT", fullPath, handler, groupMiddleware), namePrefix);
|
|
259
|
+
},
|
|
260
|
+
delete: (path, handler) => {
|
|
261
|
+
const fullPath = joinPaths(prefix, path);
|
|
262
|
+
return wrapBuilderWithNamePrefix(addRoute("DELETE", fullPath, handler, groupMiddleware), namePrefix);
|
|
263
|
+
},
|
|
264
|
+
patch: (path, handler) => {
|
|
265
|
+
const fullPath = joinPaths(prefix, path);
|
|
266
|
+
return wrapBuilderWithNamePrefix(addRoute("PATCH", fullPath, handler, groupMiddleware), namePrefix);
|
|
267
|
+
},
|
|
268
|
+
group: (prefixOrOptions, callback) => {
|
|
269
|
+
const opts = typeof prefixOrOptions === "string" ? { prefix: prefixOrOptions } : prefixOrOptions;
|
|
270
|
+
const nestedPrefix = joinPaths(prefix, opts.prefix);
|
|
271
|
+
const nestedMiddleware = [...groupMiddleware, ...opts.middleware ?? []];
|
|
272
|
+
const nestedNamePrefix = namePrefix + (opts.name ?? "");
|
|
273
|
+
const nestedRouter = createGroupRouter(nestedPrefix, nestedMiddleware, nestedNamePrefix, addRoute);
|
|
274
|
+
callback(nestedRouter);
|
|
275
|
+
return router;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
return router;
|
|
279
|
+
}
|
|
280
|
+
// src/app.ts
|
|
48
281
|
function createApp() {
|
|
49
282
|
const routes = [];
|
|
50
283
|
const middlewares = [];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (route.pattern.test(path)) {
|
|
59
|
-
if (route.method === method) {
|
|
60
|
-
return { route, params: extractParams(path, route) };
|
|
61
|
-
}
|
|
62
|
-
}
|
|
284
|
+
const namedRoutes = new Map;
|
|
285
|
+
let globalMiddlewareVersion = 0;
|
|
286
|
+
const middlewareCache = new WeakMap;
|
|
287
|
+
function getMiddlewareChain(route) {
|
|
288
|
+
const cached = middlewareCache.get(route);
|
|
289
|
+
if (cached && cached.version === globalMiddlewareVersion) {
|
|
290
|
+
return cached.chain;
|
|
63
291
|
}
|
|
64
|
-
|
|
292
|
+
const chain = route.middleware ? [...middlewares, ...route.middleware] : middlewares.length > 0 ? [...middlewares] : [];
|
|
293
|
+
middlewareCache.set(route, { version: globalMiddlewareVersion, chain });
|
|
294
|
+
return chain;
|
|
65
295
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
296
|
+
function addRoute(method, path, handler, groupMiddleware = []) {
|
|
297
|
+
const { pattern, paramNames, optionalParams } = compilePath(path);
|
|
298
|
+
const route = {
|
|
299
|
+
method,
|
|
300
|
+
path,
|
|
301
|
+
pattern,
|
|
302
|
+
paramNames,
|
|
303
|
+
handler,
|
|
304
|
+
optionalParams: optionalParams.length > 0 ? optionalParams : undefined,
|
|
305
|
+
middleware: groupMiddleware.length > 0 ? [...groupMiddleware] : undefined
|
|
306
|
+
};
|
|
307
|
+
routes.push(route);
|
|
308
|
+
return createRouteBuilder(route, namedRoutes, app);
|
|
68
309
|
}
|
|
69
310
|
async function handleRequest(request) {
|
|
70
311
|
const url = new URL(request.url);
|
|
71
312
|
const path = url.pathname;
|
|
72
313
|
const method = request.method;
|
|
73
|
-
const match = findRoute(method, path);
|
|
314
|
+
const match = findRoute(routes, method, path);
|
|
74
315
|
if (!match) {
|
|
75
|
-
if (hasMatchingPath(path)) {
|
|
316
|
+
if (hasMatchingPath(routes, path)) {
|
|
76
317
|
return new Response(JSON.stringify({ error: "Method not allowed" }), {
|
|
77
318
|
status: 405,
|
|
78
319
|
headers: { "Content-Type": "application/json" }
|
|
@@ -86,13 +327,15 @@ function createApp() {
|
|
|
86
327
|
const ctx = {
|
|
87
328
|
request,
|
|
88
329
|
params: match.params,
|
|
89
|
-
query: url.searchParams
|
|
330
|
+
query: url.searchParams,
|
|
331
|
+
locals: {}
|
|
90
332
|
};
|
|
91
333
|
try {
|
|
334
|
+
const allMiddleware = getMiddlewareChain(match.route);
|
|
92
335
|
let index = 0;
|
|
93
336
|
const next = async () => {
|
|
94
|
-
if (index <
|
|
95
|
-
const middleware =
|
|
337
|
+
if (index < allMiddleware.length) {
|
|
338
|
+
const middleware = allMiddleware[index++];
|
|
96
339
|
return await middleware(ctx, next);
|
|
97
340
|
}
|
|
98
341
|
return await match.route.handler(ctx);
|
|
@@ -115,8 +358,67 @@ function createApp() {
|
|
|
115
358
|
patch: (path, handler) => addRoute("PATCH", path, handler),
|
|
116
359
|
use: (middleware) => {
|
|
117
360
|
middlewares.push(middleware);
|
|
361
|
+
globalMiddlewareVersion++;
|
|
362
|
+
return app;
|
|
363
|
+
},
|
|
364
|
+
group: (prefixOrOptions, callback) => {
|
|
365
|
+
const opts = typeof prefixOrOptions === "string" ? { prefix: prefixOrOptions } : prefixOrOptions;
|
|
366
|
+
const groupRouter = createGroupRouter(opts.prefix, opts.middleware ?? [], opts.name ?? "", addRoute);
|
|
367
|
+
callback(groupRouter);
|
|
118
368
|
return app;
|
|
119
369
|
},
|
|
370
|
+
route: (name, params) => {
|
|
371
|
+
const route = namedRoutes.get(name);
|
|
372
|
+
if (!route) {
|
|
373
|
+
throw new Error(`Route "${name}" not found`);
|
|
374
|
+
}
|
|
375
|
+
if (params) {
|
|
376
|
+
for (const [key, value] of Object.entries(params)) {
|
|
377
|
+
const strValue = String(value);
|
|
378
|
+
if (strValue.includes("\r") || strValue.includes(`
|
|
379
|
+
`) || strValue.includes("\x00")) {
|
|
380
|
+
throw new Error(`Invalid character in parameter "${key}": control characters are not allowed`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
let url = route.path;
|
|
385
|
+
const queryParams = {};
|
|
386
|
+
const usedParams = new Set;
|
|
387
|
+
for (const paramName of route.paramNames) {
|
|
388
|
+
const isOptional = route.optionalParams?.includes(paramName);
|
|
389
|
+
const value = params?.[paramName];
|
|
390
|
+
if (value !== undefined) {
|
|
391
|
+
url = url.replace(new RegExp(`:${paramName}\\??`), encodeURIComponent(String(value)));
|
|
392
|
+
usedParams.add(paramName);
|
|
393
|
+
} else if (isOptional) {
|
|
394
|
+
url = url.replace(new RegExp(`/:${paramName}\\?`), "");
|
|
395
|
+
} else {
|
|
396
|
+
throw new Error(`Missing required param "${paramName}" for route "${name}"`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (params) {
|
|
400
|
+
for (const [key, value] of Object.entries(params)) {
|
|
401
|
+
if (!usedParams.has(key)) {
|
|
402
|
+
queryParams[key] = String(value);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (Object.keys(queryParams).length > 0) {
|
|
407
|
+
const qs = new URLSearchParams(queryParams).toString();
|
|
408
|
+
url += `?${qs}`;
|
|
409
|
+
}
|
|
410
|
+
return url;
|
|
411
|
+
},
|
|
412
|
+
hasRoute: (name) => {
|
|
413
|
+
return namedRoutes.has(name);
|
|
414
|
+
},
|
|
415
|
+
getRoutes: () => {
|
|
416
|
+
return routes.map((route) => ({
|
|
417
|
+
name: route.name ?? null,
|
|
418
|
+
method: route.method,
|
|
419
|
+
path: route.path
|
|
420
|
+
}));
|
|
421
|
+
},
|
|
120
422
|
listen: (port = 3000, hostname = "localhost") => {
|
|
121
423
|
const server = Bun.serve({
|
|
122
424
|
port,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path manipulation utilities for route handling.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Normalize a path prefix (ensure leading slash, no trailing slash).
|
|
6
|
+
*
|
|
7
|
+
* @param prefix - Path prefix to normalize
|
|
8
|
+
* @returns Normalized prefix with leading slash and no trailing slash
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* normalizePrefix("api") // "/api"
|
|
13
|
+
* normalizePrefix("/api/") // "/api"
|
|
14
|
+
* normalizePrefix("/") // "/"
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizePrefix(prefix: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Join path segments, handling slashes correctly.
|
|
20
|
+
*
|
|
21
|
+
* @param prefix - Path prefix
|
|
22
|
+
* @param path - Path to append
|
|
23
|
+
* @returns Combined path
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* joinPaths("/api", "/users") // "/api/users"
|
|
28
|
+
* joinPaths("/api", "users") // "/api/users"
|
|
29
|
+
* joinPaths("/api/", "/users") // "/api/users"
|
|
30
|
+
* joinPaths("/", "/") // "/"
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function joinPaths(prefix: string, path: string): string;
|
|
34
|
+
//# sourceMappingURL=pathUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pathUtils.d.ts","sourceRoot":"","sources":["../src/pathUtils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAStD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAc9D"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response conversion utilities for route handlers.
|
|
3
|
+
*/
|
|
4
|
+
import type { HandlerResponse } from "./types/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Convert a handler response to a proper Response object.
|
|
7
|
+
*
|
|
8
|
+
* Handles the following response types:
|
|
9
|
+
* - Response: passed through unchanged
|
|
10
|
+
* - null/undefined: 204 No Content
|
|
11
|
+
* - string: text/plain response
|
|
12
|
+
* - number: text/plain response
|
|
13
|
+
* - object/array: JSON response
|
|
14
|
+
*
|
|
15
|
+
* @param result - The handler return value
|
|
16
|
+
* @returns A proper Response object
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* toResponse({ message: "Hello" }); // JSON response
|
|
21
|
+
* toResponse("Hello"); // text/plain response
|
|
22
|
+
* toResponse(null); // 204 No Content
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function toResponse(result: HandlerResponse): Response;
|
|
26
|
+
//# sourceMappingURL=response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../src/response.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,eAAe,GAAG,QAAQ,CAgC5D"}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route matching utilities for compiling paths and extracting parameters.
|
|
3
|
+
*/
|
|
4
|
+
import type { Route } from "./types/index.js";
|
|
5
|
+
/**
|
|
6
|
+
* Result of compiling a path pattern.
|
|
7
|
+
*/
|
|
8
|
+
export interface CompiledPath {
|
|
9
|
+
/** Regex pattern for matching */
|
|
10
|
+
pattern: RegExp;
|
|
11
|
+
/** Parameter names in order */
|
|
12
|
+
paramNames: string[];
|
|
13
|
+
/** Names of optional parameters */
|
|
14
|
+
optionalParams: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Compile a path pattern into a regex and extract parameter names.
|
|
18
|
+
* Supports optional parameters with :param? syntax.
|
|
19
|
+
*
|
|
20
|
+
* @param path - Route path pattern (e.g., "/users/:id" or "/users/:id?")
|
|
21
|
+
* @returns Object with regex pattern, parameter names, and optional param names
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const { pattern, paramNames, optionalParams } = compilePath("/users/:id?");
|
|
26
|
+
* // pattern matches "/users" and "/users/123"
|
|
27
|
+
* // paramNames = ["id"]
|
|
28
|
+
* // optionalParams = ["id"]
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function compilePath(path: string): CompiledPath;
|
|
32
|
+
/**
|
|
33
|
+
* Extract path parameters from a matched route.
|
|
34
|
+
* Handles optional parameters by only including them if they have values.
|
|
35
|
+
*
|
|
36
|
+
* @param path - The request path
|
|
37
|
+
* @param route - The matched route
|
|
38
|
+
* @returns Record of parameter names to values (undefined for missing optional params)
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractParams(path: string, route: Route): Record<string, string | undefined>;
|
|
41
|
+
/**
|
|
42
|
+
* Check if route constraints are satisfied.
|
|
43
|
+
*
|
|
44
|
+
* @param params - Extracted route parameters
|
|
45
|
+
* @param constraints - Parameter constraints (regex patterns)
|
|
46
|
+
* @returns True if all constraints pass
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkConstraints(params: Record<string, string | undefined>, constraints?: Record<string, RegExp>): boolean;
|
|
49
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,mCAAmC;IACnC,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAmCtD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAa5F;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EAC1C,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,OAAO,CAUT"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BunaryApp, Route, RouteBuilder } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Safely compile a string pattern to RegExp with error handling.
|
|
4
|
+
* Provides better error messages for invalid regex patterns.
|
|
5
|
+
*/
|
|
6
|
+
export declare function compilePattern(pattern: string, param: string): RegExp;
|
|
7
|
+
/**
|
|
8
|
+
* Create a RouteBuilder for a specific route.
|
|
9
|
+
* Each builder captures its own route reference to avoid shared mutable state issues.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createRouteBuilder(route: Route, namedRoutes: Map<string, Route>, app: BunaryApp): RouteBuilder;
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a route builder to auto-apply name prefix.
|
|
14
|
+
* Uses a Proxy to maintain dynamic getter behavior from the original builder.
|
|
15
|
+
*/
|
|
16
|
+
export declare function wrapBuilderWithNamePrefix(builder: RouteBuilder, namePrefix: string): RouteBuilder;
|
|
17
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/routes/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAExE;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAOrE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,EAC/B,GAAG,EAAE,SAAS,GACZ,YAAY,CAmHd;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,YAAY,CAajG"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Route } from "../types/index.js";
|
|
2
|
+
export interface RouteMatch {
|
|
3
|
+
route: Route;
|
|
4
|
+
params: Record<string, string | undefined>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Find a matching route for the given method and path.
|
|
8
|
+
*
|
|
9
|
+
* **Complexity**: O(n) where n is the number of registered routes.
|
|
10
|
+
* Routes are tested sequentially until a match is found.
|
|
11
|
+
*
|
|
12
|
+
* This linear search is suitable for most applications (up to ~100 routes).
|
|
13
|
+
* For applications with hundreds of routes, consider:
|
|
14
|
+
* - Grouping routes by common prefixes (reduces regex tests per request)
|
|
15
|
+
* - Using method-based route maps (the 405 "Method Not Allowed" check already
|
|
16
|
+
* iterates separately, so grouping by method could help)
|
|
17
|
+
* - Implementing a radix/prefix tree for static path segments
|
|
18
|
+
*
|
|
19
|
+
* The current design prioritizes simplicity and correctness. Route order matters:
|
|
20
|
+
* the first matching route wins, allowing intentional route shadowing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findRoute(routes: Route[], method: string, path: string): RouteMatch | null;
|
|
23
|
+
/**
|
|
24
|
+
* Check if any route matches the path (regardless of method).
|
|
25
|
+
*/
|
|
26
|
+
export declare function hasMatchingPath(routes: Route[], path: string): boolean;
|
|
27
|
+
//# sourceMappingURL=find.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/routes/find.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAc1F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAMtE"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GroupRouter, Middleware, RouteBuilder, RouteHandler } from "../types/index.js";
|
|
2
|
+
export type AddRouteFn = (method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, handler: RouteHandler, groupMiddleware?: Middleware[]) => RouteBuilder;
|
|
3
|
+
/**
|
|
4
|
+
* Create a group router for defining routes within a group.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createGroupRouter(prefix: string, groupMiddleware: Middleware[], namePrefix: string, addRoute: AddRouteFn): GroupRouter;
|
|
7
|
+
//# sourceMappingURL=group.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group.d.ts","sourceRoot":"","sources":["../../src/routes/group.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,MAAM,mBAAmB,CAAC;AAG3B,MAAM,MAAM,UAAU,GAAG,CACxB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,EACnD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,YAAY,EACrB,eAAe,CAAC,EAAE,UAAU,EAAE,KAC1B,YAAY,CAAC;AAElB;;GAEG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,UAAU,EAAE,EAC7B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,UAAU,GAClB,WAAW,CAoDb"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/routes/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC7F,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appOptions.d.ts","sourceRoot":"","sources":["../../src/types/appOptions.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB"}
|