@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router System with Auto-Selection
|
|
3
|
+
*
|
|
4
|
+
* Automatically selects the optimal router implementation based on route count:
|
|
5
|
+
* - LinearRouter: ≤10 routes (O(1) static, O(n) dynamic)
|
|
6
|
+
* - RegexRouter: 11-50 routes (compiled regex patterns)
|
|
7
|
+
* - TreeRouter: >50 routes (O(log n) radix tree)
|
|
8
|
+
*
|
|
9
|
+
* You can also explicitly choose a router type for specific use cases.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
HTTPMethod,
|
|
14
|
+
MiddlewareHandler,
|
|
15
|
+
PathParams,
|
|
16
|
+
RouteHandler,
|
|
17
|
+
} from "../types";
|
|
18
|
+
import { LinearRouter } from "./linear";
|
|
19
|
+
import { RegexRouter } from "./regex";
|
|
20
|
+
import { TreeRouter } from "./tree";
|
|
21
|
+
|
|
22
|
+
// ============= Types =============
|
|
23
|
+
|
|
24
|
+
export interface RouteMatch {
|
|
25
|
+
handler: RouteHandler;
|
|
26
|
+
params: PathParams;
|
|
27
|
+
middleware?: MiddlewareHandler[];
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RouteOptions {
|
|
32
|
+
name?: string;
|
|
33
|
+
middleware?: MiddlewareHandler | MiddlewareHandler[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type RouterType = "auto" | "linear" | "regex" | "tree";
|
|
37
|
+
|
|
38
|
+
export interface RouterConfig {
|
|
39
|
+
type?: RouterType;
|
|
40
|
+
linearThreshold?: number;
|
|
41
|
+
regexThreshold?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface RouterLike {
|
|
45
|
+
match(method: HTTPMethod | "ALL", pathname: string): RouteMatch | undefined;
|
|
46
|
+
group(
|
|
47
|
+
prefix: string,
|
|
48
|
+
options?: { middleware?: MiddlewareHandler | MiddlewareHandler[] },
|
|
49
|
+
): RouterLike;
|
|
50
|
+
getRoutes(): Array<{
|
|
51
|
+
method: HTTPMethod | "ALL";
|
|
52
|
+
pattern: string;
|
|
53
|
+
name?: string;
|
|
54
|
+
}>;
|
|
55
|
+
getRouterType(): string;
|
|
56
|
+
get(pattern: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
57
|
+
post(pattern: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
58
|
+
put(pattern: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
59
|
+
patch(pattern: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
60
|
+
delete(pattern: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
61
|
+
head(pattern: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
62
|
+
options(pattern: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
63
|
+
all(pattern: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============= Thresholds =============
|
|
67
|
+
|
|
68
|
+
const DEFAULT_LINEAR_THRESHOLD = 10;
|
|
69
|
+
const DEFAULT_REGEX_THRESHOLD = 50;
|
|
70
|
+
|
|
71
|
+
// ============= AutoRouter Class =============
|
|
72
|
+
|
|
73
|
+
export class Router {
|
|
74
|
+
private router: RouterLike;
|
|
75
|
+
private config: Required<RouterConfig>;
|
|
76
|
+
private pendingRoutes: Array<{
|
|
77
|
+
method: HTTPMethod | "ALL";
|
|
78
|
+
pattern: string;
|
|
79
|
+
handler: RouteHandler;
|
|
80
|
+
options?: RouteOptions;
|
|
81
|
+
}> = [];
|
|
82
|
+
private isBuilt = false;
|
|
83
|
+
private groupPrefix = "";
|
|
84
|
+
private groupMiddleware: MiddlewareHandler[] = [];
|
|
85
|
+
|
|
86
|
+
constructor(config: RouterConfig = {}) {
|
|
87
|
+
this.config = {
|
|
88
|
+
type: config.type ?? "auto",
|
|
89
|
+
linearThreshold: config.linearThreshold ?? DEFAULT_LINEAR_THRESHOLD,
|
|
90
|
+
regexThreshold: config.regexThreshold ?? DEFAULT_REGEX_THRESHOLD,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (this.config.type !== "auto") {
|
|
94
|
+
this.router = this.createRouter(this.config.type);
|
|
95
|
+
} else {
|
|
96
|
+
this.router = new LinearRouter();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private createRouter(type: RouterType): RouterLike {
|
|
101
|
+
switch (type) {
|
|
102
|
+
case "linear":
|
|
103
|
+
return new LinearRouter();
|
|
104
|
+
case "regex":
|
|
105
|
+
return new RegexRouter();
|
|
106
|
+
case "tree":
|
|
107
|
+
return new TreeRouter();
|
|
108
|
+
default:
|
|
109
|
+
return new LinearRouter();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private getOptimalRouterType(count: number): "linear" | "regex" | "tree" {
|
|
114
|
+
if (count <= this.config.linearThreshold) {
|
|
115
|
+
return "linear";
|
|
116
|
+
}
|
|
117
|
+
if (count <= this.config.regexThreshold) {
|
|
118
|
+
return "regex";
|
|
119
|
+
}
|
|
120
|
+
return "tree";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private migrateRouter(newType: "linear" | "regex" | "tree"): void {
|
|
124
|
+
this.router = this.createRouter(newType);
|
|
125
|
+
|
|
126
|
+
for (const route of this.pendingRoutes) {
|
|
127
|
+
this.addToRouter(
|
|
128
|
+
route.method,
|
|
129
|
+
route.pattern,
|
|
130
|
+
route.handler,
|
|
131
|
+
route.options,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private addToRouter(
|
|
137
|
+
method: HTTPMethod | "ALL",
|
|
138
|
+
pattern: string,
|
|
139
|
+
handler: RouteHandler,
|
|
140
|
+
options?: RouteOptions,
|
|
141
|
+
): void {
|
|
142
|
+
switch (method) {
|
|
143
|
+
case "GET":
|
|
144
|
+
this.router.get(pattern, handler, options);
|
|
145
|
+
break;
|
|
146
|
+
case "POST":
|
|
147
|
+
this.router.post(pattern, handler, options);
|
|
148
|
+
break;
|
|
149
|
+
case "PUT":
|
|
150
|
+
this.router.put(pattern, handler, options);
|
|
151
|
+
break;
|
|
152
|
+
case "PATCH":
|
|
153
|
+
this.router.patch(pattern, handler, options);
|
|
154
|
+
break;
|
|
155
|
+
case "DELETE":
|
|
156
|
+
this.router.delete(pattern, handler, options);
|
|
157
|
+
break;
|
|
158
|
+
case "HEAD":
|
|
159
|
+
this.router.head(pattern, handler, options);
|
|
160
|
+
break;
|
|
161
|
+
case "OPTIONS":
|
|
162
|
+
this.router.options(pattern, handler, options);
|
|
163
|
+
break;
|
|
164
|
+
case "ALL":
|
|
165
|
+
this.router.all(pattern, handler, options);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private addRoute(
|
|
171
|
+
method: HTTPMethod | "ALL",
|
|
172
|
+
pattern: string,
|
|
173
|
+
handler: RouteHandler,
|
|
174
|
+
options?: RouteOptions,
|
|
175
|
+
): void {
|
|
176
|
+
const fullPattern = this.groupPrefix + pattern;
|
|
177
|
+
|
|
178
|
+
const optsMiddleware = options?.middleware;
|
|
179
|
+
const routeMiddleware: MiddlewareHandler[] = optsMiddleware
|
|
180
|
+
? Array.isArray(optsMiddleware)
|
|
181
|
+
? optsMiddleware
|
|
182
|
+
: [optsMiddleware]
|
|
183
|
+
: [];
|
|
184
|
+
|
|
185
|
+
const allMiddleware = [...this.groupMiddleware, ...routeMiddleware];
|
|
186
|
+
const fullOptions: RouteOptions | undefined =
|
|
187
|
+
options?.name || allMiddleware.length > 0
|
|
188
|
+
? {
|
|
189
|
+
name: options?.name,
|
|
190
|
+
middleware: allMiddleware,
|
|
191
|
+
}
|
|
192
|
+
: undefined;
|
|
193
|
+
|
|
194
|
+
this.pendingRoutes.push({
|
|
195
|
+
method,
|
|
196
|
+
pattern: fullPattern,
|
|
197
|
+
handler,
|
|
198
|
+
options: fullOptions,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
this.addToRouter(method, fullPattern, handler, fullOptions);
|
|
202
|
+
|
|
203
|
+
if (this.config.type === "auto" && !this.isBuilt) {
|
|
204
|
+
const routeCount = this.pendingRoutes.length;
|
|
205
|
+
const optimalType = this.getOptimalRouterType(routeCount);
|
|
206
|
+
const currentType = this.router.getRouterType();
|
|
207
|
+
|
|
208
|
+
if (currentType !== optimalType) {
|
|
209
|
+
this.migrateRouter(optimalType);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
match(method: HTTPMethod | "ALL", pathname: string): RouteMatch | undefined {
|
|
215
|
+
this.isBuilt = true;
|
|
216
|
+
return this.router.match(method, pathname);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
group(
|
|
220
|
+
prefix: string,
|
|
221
|
+
options?: { middleware?: MiddlewareHandler | MiddlewareHandler[] },
|
|
222
|
+
): Router {
|
|
223
|
+
const childRouter = new Router(this.config);
|
|
224
|
+
|
|
225
|
+
(childRouter as unknown as { router: RouterLike }).router = this.router;
|
|
226
|
+
childRouter.pendingRoutes = this.pendingRoutes;
|
|
227
|
+
childRouter.groupPrefix = this.groupPrefix + prefix;
|
|
228
|
+
childRouter.isBuilt = this.isBuilt;
|
|
229
|
+
|
|
230
|
+
const optsMiddleware = options?.middleware;
|
|
231
|
+
const middlewareArray: MiddlewareHandler[] = optsMiddleware
|
|
232
|
+
? Array.isArray(optsMiddleware)
|
|
233
|
+
? optsMiddleware
|
|
234
|
+
: [optsMiddleware]
|
|
235
|
+
: [];
|
|
236
|
+
|
|
237
|
+
childRouter.groupMiddleware = [...this.groupMiddleware, ...middlewareArray];
|
|
238
|
+
|
|
239
|
+
return childRouter;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getRoutes(): Array<{
|
|
243
|
+
method: HTTPMethod | "ALL";
|
|
244
|
+
pattern: string;
|
|
245
|
+
name?: string;
|
|
246
|
+
}> {
|
|
247
|
+
return this.router.getRoutes();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
getRouterType(): string {
|
|
251
|
+
return this.router.getRouterType();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getRouteCount(): number {
|
|
255
|
+
return this.pendingRoutes.length;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getConfig(): Required<RouterConfig> {
|
|
259
|
+
return { ...this.config };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
get(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
263
|
+
this.addRoute("GET", pattern, handler, options);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
post(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
267
|
+
this.addRoute("POST", pattern, handler, options);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
put(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
271
|
+
this.addRoute("PUT", pattern, handler, options);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
patch(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
275
|
+
this.addRoute("PATCH", pattern, handler, options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
delete(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
279
|
+
this.addRoute("DELETE", pattern, handler, options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
head(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
283
|
+
this.addRoute("HEAD", pattern, handler, options);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
options(
|
|
287
|
+
pattern: string,
|
|
288
|
+
handler: RouteHandler,
|
|
289
|
+
options?: RouteOptions,
|
|
290
|
+
): void {
|
|
291
|
+
this.addRoute("OPTIONS", pattern, handler, options);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
all(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
295
|
+
this.addRoute("ALL", pattern, handler, options);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ============= Re-export Router Types =============
|
|
300
|
+
|
|
301
|
+
export { LinearRouter } from "./linear";
|
|
302
|
+
export { RegexRouter } from "./regex";
|
|
303
|
+
export { TreeRouter } from "./tree";
|
|
304
|
+
|
|
305
|
+
// ============= Utility: Route URL Generation =============
|
|
306
|
+
|
|
307
|
+
export function generateUrl(pattern: string, params: PathParams = {}): string {
|
|
308
|
+
return pattern
|
|
309
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)\?/g, (_, name) => params[name] ?? "")
|
|
310
|
+
.replace(
|
|
311
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)<[^>]+>/g,
|
|
312
|
+
(_, name) => params[name] ?? "",
|
|
313
|
+
)
|
|
314
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
315
|
+
if (params[name] === undefined) {
|
|
316
|
+
throw new Error(`Missing required parameter: ${name}`);
|
|
317
|
+
}
|
|
318
|
+
return params[name];
|
|
319
|
+
})
|
|
320
|
+
.replace(/\*/g, () => params["*"] ?? "");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ============= Factory Functions =============
|
|
324
|
+
|
|
325
|
+
export function createRouter(config?: RouterConfig): Router {
|
|
326
|
+
return new Router(config);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function createLinearRouter(): LinearRouter {
|
|
330
|
+
return new LinearRouter();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function createRegexRouter(): RegexRouter {
|
|
334
|
+
return new RegexRouter();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function createTreeRouter(): TreeRouter {
|
|
338
|
+
return new TreeRouter();
|
|
339
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinearRouter
|
|
3
|
+
*
|
|
4
|
+
* Optimized for applications with ≤10 routes.
|
|
5
|
+
* - O(1) static route lookups via Map
|
|
6
|
+
* - Simple O(n) iteration for dynamic routes
|
|
7
|
+
* - Zero startup cost (no tree building)
|
|
8
|
+
*
|
|
9
|
+
* Best for: Microservices, development mode, small APIs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
HTTPMethod,
|
|
14
|
+
MiddlewareHandler,
|
|
15
|
+
PathParams,
|
|
16
|
+
RouteHandler,
|
|
17
|
+
} from "../types";
|
|
18
|
+
|
|
19
|
+
// ============= Types =============
|
|
20
|
+
|
|
21
|
+
export interface RouteMatch {
|
|
22
|
+
handler: RouteHandler;
|
|
23
|
+
params: PathParams;
|
|
24
|
+
middleware?: MiddlewareHandler[];
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface DynamicRoute {
|
|
29
|
+
method: HTTPMethod | "ALL";
|
|
30
|
+
pattern: string;
|
|
31
|
+
handler: RouteHandler;
|
|
32
|
+
middleware?: MiddlewareHandler[];
|
|
33
|
+
name?: string;
|
|
34
|
+
regex: RegExp;
|
|
35
|
+
paramNames: string[];
|
|
36
|
+
priority: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RouteOptions {
|
|
40
|
+
name?: string;
|
|
41
|
+
middleware?: MiddlewareHandler | MiddlewareHandler[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type StaticRouteKey = `${HTTPMethod | "ALL"}:${string}`;
|
|
45
|
+
|
|
46
|
+
// ============= Pattern Utilities =============
|
|
47
|
+
|
|
48
|
+
function patternToRegex(pattern: string): {
|
|
49
|
+
regex: RegExp;
|
|
50
|
+
paramNames: string[];
|
|
51
|
+
isStatic: boolean;
|
|
52
|
+
hasWildcard: boolean;
|
|
53
|
+
} {
|
|
54
|
+
const paramNames: string[] = [];
|
|
55
|
+
let isStatic = true;
|
|
56
|
+
let hasWildcard = false;
|
|
57
|
+
|
|
58
|
+
const segments: string[] = [];
|
|
59
|
+
let i = 0;
|
|
60
|
+
|
|
61
|
+
while (i < pattern.length) {
|
|
62
|
+
if (pattern[i] === ":") {
|
|
63
|
+
i++;
|
|
64
|
+
|
|
65
|
+
let name = "";
|
|
66
|
+
while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) {
|
|
67
|
+
name += pattern[i];
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let optional = false;
|
|
72
|
+
if (i < pattern.length && pattern[i] === "?") {
|
|
73
|
+
optional = true;
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let customRegex = "";
|
|
78
|
+
if (i < pattern.length && pattern[i] === "<") {
|
|
79
|
+
i++;
|
|
80
|
+
while (i < pattern.length && pattern[i] !== ">") {
|
|
81
|
+
customRegex += pattern[i];
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
paramNames.push(name);
|
|
88
|
+
isStatic = false;
|
|
89
|
+
|
|
90
|
+
if (optional) {
|
|
91
|
+
if (segments.length > 0 && segments[segments.length - 1] === "/") {
|
|
92
|
+
segments.pop();
|
|
93
|
+
}
|
|
94
|
+
segments.push("(?:/([^/]*))?");
|
|
95
|
+
} else if (customRegex) {
|
|
96
|
+
segments.push(`(${customRegex})`);
|
|
97
|
+
} else {
|
|
98
|
+
segments.push("([^/]+)");
|
|
99
|
+
}
|
|
100
|
+
} else if (pattern[i] === "*") {
|
|
101
|
+
hasWildcard = true;
|
|
102
|
+
isStatic = false;
|
|
103
|
+
paramNames.push("*");
|
|
104
|
+
segments.push("(.*)");
|
|
105
|
+
i++;
|
|
106
|
+
} else {
|
|
107
|
+
const char = pattern[i];
|
|
108
|
+
if (/[.+^${}()|[\]\\]/.test(char)) {
|
|
109
|
+
segments.push(`\\${char}`);
|
|
110
|
+
} else {
|
|
111
|
+
segments.push(char);
|
|
112
|
+
}
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const regexStr = `^${segments.join("")}/?$`;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
regex: new RegExp(regexStr, "i"),
|
|
121
|
+
paramNames,
|
|
122
|
+
isStatic,
|
|
123
|
+
hasWildcard,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function extractParams(
|
|
128
|
+
regex: RegExp,
|
|
129
|
+
paramNames: string[],
|
|
130
|
+
pathname: string,
|
|
131
|
+
): PathParams {
|
|
132
|
+
const params: PathParams = {};
|
|
133
|
+
const match = pathname.match(regex);
|
|
134
|
+
|
|
135
|
+
if (match) {
|
|
136
|
+
paramNames.forEach((name, index) => {
|
|
137
|
+
if (match[index + 1] !== undefined) {
|
|
138
|
+
params[name] = match[index + 1];
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return params;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============= LinearRouter Class =============
|
|
147
|
+
|
|
148
|
+
export class LinearRouter {
|
|
149
|
+
private staticRoutes: Map<
|
|
150
|
+
StaticRouteKey,
|
|
151
|
+
{
|
|
152
|
+
handler: RouteHandler;
|
|
153
|
+
middleware?: MiddlewareHandler[];
|
|
154
|
+
name?: string;
|
|
155
|
+
}
|
|
156
|
+
> = new Map();
|
|
157
|
+
|
|
158
|
+
private dynamicRoutes: DynamicRoute[] = [];
|
|
159
|
+
|
|
160
|
+
private groupPrefix = "";
|
|
161
|
+
private groupMiddleware: MiddlewareHandler[] = [];
|
|
162
|
+
private routeCounter = 0;
|
|
163
|
+
|
|
164
|
+
private addRoute(
|
|
165
|
+
method: HTTPMethod | "ALL",
|
|
166
|
+
pattern: string,
|
|
167
|
+
handler: RouteHandler,
|
|
168
|
+
options?: RouteOptions,
|
|
169
|
+
): void {
|
|
170
|
+
const fullPattern = this.groupPrefix + pattern;
|
|
171
|
+
const { regex, paramNames, isStatic } = patternToRegex(fullPattern);
|
|
172
|
+
|
|
173
|
+
const optsMiddleware = options?.middleware;
|
|
174
|
+
const routeMiddleware: MiddlewareHandler[] = optsMiddleware
|
|
175
|
+
? Array.isArray(optsMiddleware)
|
|
176
|
+
? optsMiddleware
|
|
177
|
+
: [optsMiddleware]
|
|
178
|
+
: [];
|
|
179
|
+
|
|
180
|
+
const middleware = [...this.groupMiddleware, ...routeMiddleware];
|
|
181
|
+
|
|
182
|
+
if (isStatic) {
|
|
183
|
+
const normalizedPath = fullPattern.toLowerCase();
|
|
184
|
+
const key: StaticRouteKey = `${method}:${normalizedPath}`;
|
|
185
|
+
|
|
186
|
+
this.staticRoutes.set(key, { handler, middleware, name: options?.name });
|
|
187
|
+
|
|
188
|
+
if (!normalizedPath.endsWith("/")) {
|
|
189
|
+
this.staticRoutes.set(`${method}:${normalizedPath}/`, {
|
|
190
|
+
handler,
|
|
191
|
+
middleware,
|
|
192
|
+
name: options?.name,
|
|
193
|
+
});
|
|
194
|
+
} else if (normalizedPath.length > 1) {
|
|
195
|
+
this.staticRoutes.set(`${method}:${normalizedPath.slice(0, -1)}`, {
|
|
196
|
+
handler,
|
|
197
|
+
middleware,
|
|
198
|
+
name: options?.name,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
this.dynamicRoutes.push({
|
|
203
|
+
method,
|
|
204
|
+
pattern: fullPattern,
|
|
205
|
+
handler,
|
|
206
|
+
middleware,
|
|
207
|
+
name: options?.name,
|
|
208
|
+
regex,
|
|
209
|
+
paramNames,
|
|
210
|
+
priority: this.routeCounter++,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
this.sortDynamicRoutes();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private sortDynamicRoutes(): void {
|
|
218
|
+
this.dynamicRoutes.sort((a, b) => {
|
|
219
|
+
if (a.paramNames.length !== b.paramNames.length) {
|
|
220
|
+
return a.paramNames.length - b.paramNames.length;
|
|
221
|
+
}
|
|
222
|
+
return a.priority - b.priority;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
match(method: HTTPMethod | "ALL", pathname: string): RouteMatch | undefined {
|
|
227
|
+
const normalizedPath = pathname.toLowerCase();
|
|
228
|
+
|
|
229
|
+
const staticKey: StaticRouteKey = `${method}:${normalizedPath}`;
|
|
230
|
+
const staticRoute = this.staticRoutes.get(staticKey);
|
|
231
|
+
|
|
232
|
+
if (staticRoute) {
|
|
233
|
+
return {
|
|
234
|
+
handler: staticRoute.handler,
|
|
235
|
+
params: {},
|
|
236
|
+
middleware: staticRoute.middleware,
|
|
237
|
+
name: staticRoute.name,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const allKey: StaticRouteKey = `ALL:${normalizedPath}`;
|
|
242
|
+
const allRoute = this.staticRoutes.get(allKey);
|
|
243
|
+
|
|
244
|
+
if (allRoute) {
|
|
245
|
+
return {
|
|
246
|
+
handler: allRoute.handler,
|
|
247
|
+
params: {},
|
|
248
|
+
middleware: allRoute.middleware,
|
|
249
|
+
name: allRoute.name,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const route of this.dynamicRoutes) {
|
|
254
|
+
if (route.method !== "ALL" && route.method !== method) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (route.regex.test(pathname)) {
|
|
259
|
+
const params = extractParams(route.regex, route.paramNames, pathname);
|
|
260
|
+
return {
|
|
261
|
+
handler: route.handler,
|
|
262
|
+
params,
|
|
263
|
+
middleware: route.middleware,
|
|
264
|
+
name: route.name,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
group(
|
|
273
|
+
prefix: string,
|
|
274
|
+
options?: { middleware?: MiddlewareHandler | MiddlewareHandler[] },
|
|
275
|
+
): LinearRouter {
|
|
276
|
+
const childRouter = new LinearRouter();
|
|
277
|
+
|
|
278
|
+
childRouter.staticRoutes = this.staticRoutes;
|
|
279
|
+
childRouter.dynamicRoutes = this.dynamicRoutes;
|
|
280
|
+
childRouter.groupPrefix = this.groupPrefix + prefix;
|
|
281
|
+
|
|
282
|
+
const optsMiddleware = options?.middleware;
|
|
283
|
+
const middlewareArray: MiddlewareHandler[] = optsMiddleware
|
|
284
|
+
? Array.isArray(optsMiddleware)
|
|
285
|
+
? optsMiddleware
|
|
286
|
+
: [optsMiddleware]
|
|
287
|
+
: [];
|
|
288
|
+
|
|
289
|
+
childRouter.groupMiddleware = [...this.groupMiddleware, ...middlewareArray];
|
|
290
|
+
childRouter.routeCounter = this.routeCounter;
|
|
291
|
+
|
|
292
|
+
return childRouter;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getRoutes(): Array<{
|
|
296
|
+
method: HTTPMethod | "ALL";
|
|
297
|
+
pattern: string;
|
|
298
|
+
name?: string;
|
|
299
|
+
}> {
|
|
300
|
+
const routes: Array<{
|
|
301
|
+
method: HTTPMethod | "ALL";
|
|
302
|
+
pattern: string;
|
|
303
|
+
name?: string;
|
|
304
|
+
}> = [];
|
|
305
|
+
const seenPatterns = new Set<string>();
|
|
306
|
+
|
|
307
|
+
for (const [key, value] of this.staticRoutes) {
|
|
308
|
+
const [method, path] = key.split(":") as [HTTPMethod | "ALL", string];
|
|
309
|
+
const pattern =
|
|
310
|
+
path.endsWith("/") && path.length > 1 ? path.slice(0, -1) : path;
|
|
311
|
+
const dedupeKey = `${method}:${pattern}`;
|
|
312
|
+
|
|
313
|
+
if (!seenPatterns.has(dedupeKey)) {
|
|
314
|
+
seenPatterns.add(dedupeKey);
|
|
315
|
+
routes.push({ method, pattern: pattern || "/", name: value.name });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const route of this.dynamicRoutes) {
|
|
320
|
+
routes.push({
|
|
321
|
+
method: route.method,
|
|
322
|
+
pattern: route.pattern,
|
|
323
|
+
name: route.name,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return routes;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getRouterType(): "linear" {
|
|
331
|
+
return "linear";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
getRouteCount(): { static: number; dynamic: number; total: number } {
|
|
335
|
+
const staticCount = Math.floor(this.staticRoutes.size / 2);
|
|
336
|
+
return {
|
|
337
|
+
static: staticCount,
|
|
338
|
+
dynamic: this.dynamicRoutes.length,
|
|
339
|
+
total: staticCount + this.dynamicRoutes.length,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
get(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
344
|
+
this.addRoute("GET", pattern, handler, options);
|
|
345
|
+
}
|
|
346
|
+
post(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
347
|
+
this.addRoute("POST", pattern, handler, options);
|
|
348
|
+
}
|
|
349
|
+
put(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
350
|
+
this.addRoute("PUT", pattern, handler, options);
|
|
351
|
+
}
|
|
352
|
+
patch(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
353
|
+
this.addRoute("PATCH", pattern, handler, options);
|
|
354
|
+
}
|
|
355
|
+
delete(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
356
|
+
this.addRoute("DELETE", pattern, handler, options);
|
|
357
|
+
}
|
|
358
|
+
head(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
359
|
+
this.addRoute("HEAD", pattern, handler, options);
|
|
360
|
+
}
|
|
361
|
+
options(
|
|
362
|
+
pattern: string,
|
|
363
|
+
handler: RouteHandler,
|
|
364
|
+
options?: RouteOptions,
|
|
365
|
+
): void {
|
|
366
|
+
this.addRoute("OPTIONS", pattern, handler, options);
|
|
367
|
+
}
|
|
368
|
+
all(pattern: string, handler: RouteHandler, options?: RouteOptions): void {
|
|
369
|
+
this.addRoute("ALL", pattern, handler, options);
|
|
370
|
+
}
|
|
371
|
+
}
|