create-phoenixjs 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/index.ts +196 -0
- package/package.json +31 -0
- package/template/README.md +62 -0
- package/template/app/controllers/ExampleController.ts +61 -0
- package/template/artisan +2 -0
- package/template/bootstrap/app.ts +44 -0
- package/template/bunfig.toml +7 -0
- package/template/config/database.ts +25 -0
- package/template/config/plugins.ts +7 -0
- package/template/config/security.ts +158 -0
- package/template/framework/cli/Command.ts +17 -0
- package/template/framework/cli/ConsoleApplication.ts +55 -0
- package/template/framework/cli/artisan.ts +16 -0
- package/template/framework/cli/commands/MakeControllerCommand.ts +41 -0
- package/template/framework/cli/commands/MakeMiddlewareCommand.ts +41 -0
- package/template/framework/cli/commands/MakeModelCommand.ts +36 -0
- package/template/framework/cli/commands/MakeValidatorCommand.ts +42 -0
- package/template/framework/controller/Controller.ts +222 -0
- package/template/framework/core/Application.ts +208 -0
- package/template/framework/core/Container.ts +100 -0
- package/template/framework/core/Kernel.ts +297 -0
- package/template/framework/database/DatabaseAdapter.ts +18 -0
- package/template/framework/database/PrismaAdapter.ts +65 -0
- package/template/framework/database/SqlAdapter.ts +117 -0
- package/template/framework/gateway/Gateway.ts +109 -0
- package/template/framework/gateway/GatewayManager.ts +150 -0
- package/template/framework/gateway/WebSocketAdapter.ts +159 -0
- package/template/framework/gateway/WebSocketGateway.ts +182 -0
- package/template/framework/http/Request.ts +608 -0
- package/template/framework/http/Response.ts +525 -0
- package/template/framework/http/Server.ts +161 -0
- package/template/framework/http/UploadedFile.ts +145 -0
- package/template/framework/middleware/Middleware.ts +50 -0
- package/template/framework/middleware/Pipeline.ts +89 -0
- package/template/framework/plugin/Plugin.ts +26 -0
- package/template/framework/plugin/PluginManager.ts +61 -0
- package/template/framework/routing/RouteRegistry.ts +185 -0
- package/template/framework/routing/Router.ts +280 -0
- package/template/framework/security/CorsMiddleware.ts +151 -0
- package/template/framework/security/CsrfMiddleware.ts +121 -0
- package/template/framework/security/HelmetMiddleware.ts +138 -0
- package/template/framework/security/InputSanitizerMiddleware.ts +134 -0
- package/template/framework/security/RateLimiterMiddleware.ts +189 -0
- package/template/framework/security/SecurityManager.ts +128 -0
- package/template/framework/validation/Validator.ts +482 -0
- package/template/package.json +24 -0
- package/template/routes/api.ts +56 -0
- package/template/server.ts +29 -0
- package/template/tsconfig.json +49 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - Uploaded File Handler
|
|
3
|
+
*
|
|
4
|
+
* Represents a file uploaded via HTTP request.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
9
|
+
|
|
10
|
+
export interface UploadedFileOptions {
|
|
11
|
+
name: string;
|
|
12
|
+
originalName: string;
|
|
13
|
+
mimeType: string;
|
|
14
|
+
size: number;
|
|
15
|
+
content: ArrayBuffer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class UploadedFile {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly originalName: string;
|
|
21
|
+
readonly mimeType: string;
|
|
22
|
+
readonly size: number;
|
|
23
|
+
private content: ArrayBuffer;
|
|
24
|
+
|
|
25
|
+
constructor(options: UploadedFileOptions) {
|
|
26
|
+
this.name = options.name;
|
|
27
|
+
this.originalName = options.originalName;
|
|
28
|
+
this.mimeType = options.mimeType;
|
|
29
|
+
this.size = options.size;
|
|
30
|
+
this.content = options.content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the file extension
|
|
35
|
+
*/
|
|
36
|
+
extension(): string {
|
|
37
|
+
const parts = this.originalName.split('.');
|
|
38
|
+
return parts.length > 1 ? parts[parts.length - 1] : '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if the file is valid (has content)
|
|
43
|
+
*/
|
|
44
|
+
isValid(): boolean {
|
|
45
|
+
return this.size > 0 && this.content.byteLength > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the file content as ArrayBuffer
|
|
50
|
+
*/
|
|
51
|
+
getContent(): ArrayBuffer {
|
|
52
|
+
return this.content;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the file content as Buffer
|
|
57
|
+
*/
|
|
58
|
+
getBuffer(): Buffer {
|
|
59
|
+
return Buffer.from(this.content);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the file content as text
|
|
64
|
+
*/
|
|
65
|
+
async text(): Promise<string> {
|
|
66
|
+
const decoder = new TextDecoder();
|
|
67
|
+
return decoder.decode(this.content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the file content as base64
|
|
72
|
+
*/
|
|
73
|
+
toBase64(): string {
|
|
74
|
+
return Buffer.from(this.content).toString('base64');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Store the file to a given path
|
|
79
|
+
* Returns the full path where the file was stored
|
|
80
|
+
*/
|
|
81
|
+
async store(directory: string, filename?: string): Promise<string> {
|
|
82
|
+
const finalFilename = filename ?? this.generateFilename();
|
|
83
|
+
const fullPath = join(directory, finalFilename);
|
|
84
|
+
|
|
85
|
+
// Ensure directory exists
|
|
86
|
+
await mkdir(directory, { recursive: true });
|
|
87
|
+
|
|
88
|
+
// Write file
|
|
89
|
+
await writeFile(fullPath, this.getBuffer());
|
|
90
|
+
|
|
91
|
+
return fullPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Store the file with its original name
|
|
96
|
+
*/
|
|
97
|
+
async storeAs(directory: string, filename: string): Promise<string> {
|
|
98
|
+
return this.store(directory, filename);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate a unique filename
|
|
103
|
+
*/
|
|
104
|
+
private generateFilename(): string {
|
|
105
|
+
const timestamp = Date.now();
|
|
106
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
107
|
+
const ext = this.extension();
|
|
108
|
+
return ext ? `${timestamp}_${random}.${ext}` : `${timestamp}_${random}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if the file has a specific MIME type
|
|
113
|
+
*/
|
|
114
|
+
hasMimeType(type: string): boolean {
|
|
115
|
+
return this.mimeType === type;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if the file is an image
|
|
120
|
+
*/
|
|
121
|
+
isImage(): boolean {
|
|
122
|
+
return this.mimeType.startsWith('image/');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if the file is a video
|
|
127
|
+
*/
|
|
128
|
+
isVideo(): boolean {
|
|
129
|
+
return this.mimeType.startsWith('video/');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if the file is an audio file
|
|
134
|
+
*/
|
|
135
|
+
isAudio(): boolean {
|
|
136
|
+
return this.mimeType.startsWith('audio/');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if the file is a PDF
|
|
141
|
+
*/
|
|
142
|
+
isPdf(): boolean {
|
|
143
|
+
return this.mimeType === 'application/pdf';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - Middleware
|
|
3
|
+
*
|
|
4
|
+
* Middleware interface and types for the middleware pipeline.
|
|
5
|
+
* Inspired by Laravel's middleware system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { FrameworkRequest } from '@framework/http/Request';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The function signature for the next middleware in the chain
|
|
12
|
+
*/
|
|
13
|
+
export type NextFunction = (request: FrameworkRequest) => Promise<Response>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Middleware interface
|
|
17
|
+
*
|
|
18
|
+
* All middleware must implement this interface. The handle method receives
|
|
19
|
+
* the request and a next function to call the next middleware in the chain.
|
|
20
|
+
*/
|
|
21
|
+
export interface Middleware {
|
|
22
|
+
/**
|
|
23
|
+
* Handle the incoming request
|
|
24
|
+
*
|
|
25
|
+
* @param request - The incoming request
|
|
26
|
+
* @param next - The next middleware in the chain
|
|
27
|
+
* @returns The response from the middleware chain
|
|
28
|
+
*/
|
|
29
|
+
handle(request: FrameworkRequest, next: NextFunction): Promise<Response>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Middleware class (alternative to interface for those who prefer classes)
|
|
34
|
+
*/
|
|
35
|
+
export abstract class MiddlewareBase implements Middleware {
|
|
36
|
+
abstract handle(request: FrameworkRequest, next: NextFunction): Promise<Response>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Type for middleware that can be registered
|
|
41
|
+
* Supports class instances, constructor functions, and callback functions
|
|
42
|
+
*/
|
|
43
|
+
export type MiddlewareHandler =
|
|
44
|
+
| Middleware
|
|
45
|
+
| ((request: FrameworkRequest, next: NextFunction) => Promise<Response>);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Type for middleware that can be resolved from string name
|
|
49
|
+
*/
|
|
50
|
+
export type MiddlewareResolvable = string | MiddlewareHandler;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - Middleware Pipeline
|
|
3
|
+
*
|
|
4
|
+
* A pipeline for executing middleware in sequence.
|
|
5
|
+
* Inspired by Laravel's Pipeline class.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { FrameworkRequest } from '@framework/http/Request';
|
|
9
|
+
import type { MiddlewareHandler, NextFunction } from '@framework/middleware/Middleware';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pipeline class for chaining middleware
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* const response = await new Pipeline()
|
|
16
|
+
* .send(request)
|
|
17
|
+
* .through([middleware1, middleware2])
|
|
18
|
+
* .then(finalHandler);
|
|
19
|
+
*/
|
|
20
|
+
export class Pipeline {
|
|
21
|
+
private passable: FrameworkRequest | null = null;
|
|
22
|
+
private pipes: MiddlewareHandler[] = [];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set the object being sent through the pipeline
|
|
26
|
+
*/
|
|
27
|
+
send(passable: FrameworkRequest): this {
|
|
28
|
+
this.passable = passable;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Set the array of pipes (middleware)
|
|
34
|
+
*/
|
|
35
|
+
through(pipes: MiddlewareHandler[]): this {
|
|
36
|
+
this.pipes = pipes;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Add a single pipe to the pipeline
|
|
42
|
+
*/
|
|
43
|
+
pipe(pipe: MiddlewareHandler): this {
|
|
44
|
+
this.pipes.push(pipe);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run the pipeline with a final destination callback
|
|
50
|
+
*/
|
|
51
|
+
async then(destination: (request: FrameworkRequest) => Promise<Response>): Promise<Response> {
|
|
52
|
+
if (!this.passable) {
|
|
53
|
+
throw new Error('Pipeline: No passable set. Call send() first.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build the middleware chain from the end backwards
|
|
57
|
+
const pipeline = this.pipes.reduceRight<NextFunction>(
|
|
58
|
+
(next, pipe) => {
|
|
59
|
+
return async (request: FrameworkRequest): Promise<Response> => {
|
|
60
|
+
return this.executePipe(pipe, request, next);
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
destination
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return pipeline(this.passable);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute a single pipe (middleware)
|
|
71
|
+
*/
|
|
72
|
+
private async executePipe(
|
|
73
|
+
pipe: MiddlewareHandler,
|
|
74
|
+
request: FrameworkRequest,
|
|
75
|
+
next: NextFunction
|
|
76
|
+
): Promise<Response> {
|
|
77
|
+
// If it's an object with handle method (Middleware interface)
|
|
78
|
+
if (typeof pipe === 'object' && 'handle' in pipe) {
|
|
79
|
+
return pipe.handle(request, next);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If it's a function (callback middleware)
|
|
83
|
+
if (typeof pipe === 'function') {
|
|
84
|
+
return pipe(request, next);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(`Pipeline: Invalid middleware type: ${typeof pipe}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Application } from '@framework/core/Application';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin Interface
|
|
5
|
+
*
|
|
6
|
+
* Plugins allow extending the framework with external functionality.
|
|
7
|
+
* They can register services, add middleware, register routes, etc.
|
|
8
|
+
*/
|
|
9
|
+
export interface Plugin {
|
|
10
|
+
/**
|
|
11
|
+
* The unique name of the plugin
|
|
12
|
+
*/
|
|
13
|
+
name: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register services on the application
|
|
17
|
+
* This is called when the plugin is registered
|
|
18
|
+
*/
|
|
19
|
+
register(app: Application): void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Boot the plugin
|
|
23
|
+
* This is called after all services are registered
|
|
24
|
+
*/
|
|
25
|
+
boot?(app: Application): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Application } from '@framework/core/Application';
|
|
2
|
+
import { Plugin } from '@framework/plugin/Plugin';
|
|
3
|
+
|
|
4
|
+
export class PluginManager {
|
|
5
|
+
protected app: Application;
|
|
6
|
+
protected plugins: Map<string, Plugin> = new Map();
|
|
7
|
+
protected booted = false;
|
|
8
|
+
|
|
9
|
+
constructor(app: Application) {
|
|
10
|
+
this.app = app;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register a plugin with the application
|
|
15
|
+
*/
|
|
16
|
+
register(plugin: Plugin): void {
|
|
17
|
+
if (this.plugins.has(plugin.name)) {
|
|
18
|
+
console.warn(`Plugin ${plugin.name} is already registered.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.plugins.set(plugin.name, plugin);
|
|
23
|
+
plugin.register(this.app);
|
|
24
|
+
|
|
25
|
+
// If app is already booted and plugin has boot method, boot it immediately
|
|
26
|
+
if (this.booted && plugin.boot) {
|
|
27
|
+
plugin.boot(this.app);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Boot all registered plugins
|
|
33
|
+
*/
|
|
34
|
+
boot(): void {
|
|
35
|
+
if (this.booted) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const plugin of this.plugins.values()) {
|
|
40
|
+
if (plugin.boot) {
|
|
41
|
+
plugin.boot(this.app);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.booted = true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a registered plugin by name
|
|
50
|
+
*/
|
|
51
|
+
get(name: string): Plugin | undefined {
|
|
52
|
+
return this.plugins.get(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get all registered plugins
|
|
57
|
+
*/
|
|
58
|
+
getAll(): Plugin[] {
|
|
59
|
+
return Array.from(this.plugins.values());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PhoenixJS - Route Registry
|
|
3
|
+
*
|
|
4
|
+
* Internal storage for route definitions with pattern compilation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { MiddlewareResolvable } from '@framework/middleware/Middleware';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* HTTP methods supported by the router
|
|
11
|
+
*/
|
|
12
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Route handler types
|
|
16
|
+
* - String: 'Controller@method' format
|
|
17
|
+
* - Function: Direct handler function
|
|
18
|
+
*/
|
|
19
|
+
export type RouteHandler =
|
|
20
|
+
| string
|
|
21
|
+
| ((params: Record<string, string>) => Promise<Response> | Response);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Route definition
|
|
25
|
+
*/
|
|
26
|
+
export interface Route {
|
|
27
|
+
/** HTTP method */
|
|
28
|
+
method: HttpMethod;
|
|
29
|
+
/** Original path pattern (e.g., '/users/:id') */
|
|
30
|
+
path: string;
|
|
31
|
+
/** Compiled regex pattern */
|
|
32
|
+
pattern: RegExp;
|
|
33
|
+
/** Parameter names extracted from path */
|
|
34
|
+
paramNames: string[];
|
|
35
|
+
/** Route handler (controller@method or function) */
|
|
36
|
+
handler: RouteHandler;
|
|
37
|
+
/** Middleware stack for this route */
|
|
38
|
+
middleware: MiddlewareResolvable[];
|
|
39
|
+
/** Route name for URL generation */
|
|
40
|
+
name?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Match result from route resolution
|
|
45
|
+
*/
|
|
46
|
+
export interface RouteMatch {
|
|
47
|
+
route: Route;
|
|
48
|
+
params: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* RouteRegistry - Internal storage and matching
|
|
53
|
+
*/
|
|
54
|
+
export class RouteRegistry {
|
|
55
|
+
private routes: Route[] = [];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add a route to the registry
|
|
59
|
+
*/
|
|
60
|
+
add(
|
|
61
|
+
method: HttpMethod,
|
|
62
|
+
path: string,
|
|
63
|
+
handler: RouteHandler,
|
|
64
|
+
middleware: MiddlewareResolvable[] = [],
|
|
65
|
+
name?: string
|
|
66
|
+
): Route {
|
|
67
|
+
const { pattern, paramNames } = this.compilePath(path);
|
|
68
|
+
|
|
69
|
+
const route: Route = {
|
|
70
|
+
method,
|
|
71
|
+
path,
|
|
72
|
+
pattern,
|
|
73
|
+
paramNames,
|
|
74
|
+
handler,
|
|
75
|
+
middleware,
|
|
76
|
+
name,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
this.routes.push(route);
|
|
80
|
+
return route;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Find a matching route for method and path
|
|
85
|
+
*/
|
|
86
|
+
match(method: string, path: string): RouteMatch | null {
|
|
87
|
+
const normalizedMethod = method.toUpperCase() as HttpMethod;
|
|
88
|
+
|
|
89
|
+
for (const route of this.routes) {
|
|
90
|
+
// Check method match (HEAD can match GET routes)
|
|
91
|
+
if (route.method !== normalizedMethod) {
|
|
92
|
+
if (!(normalizedMethod === 'HEAD' && route.method === 'GET')) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check path match
|
|
98
|
+
const match = route.pattern.exec(path);
|
|
99
|
+
if (match) {
|
|
100
|
+
// Extract params from match groups
|
|
101
|
+
const params: Record<string, string> = {};
|
|
102
|
+
route.paramNames.forEach((name, index) => {
|
|
103
|
+
params[name] = match[index + 1] || '';
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return { route, params };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get all registered routes
|
|
115
|
+
*/
|
|
116
|
+
all(): Route[] {
|
|
117
|
+
return [...this.routes];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Find a route by name
|
|
122
|
+
*/
|
|
123
|
+
findByName(name: string): Route | undefined {
|
|
124
|
+
return this.routes.find(r => r.name === name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Clear all routes (useful for testing)
|
|
129
|
+
*/
|
|
130
|
+
clear(): void {
|
|
131
|
+
this.routes = [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get count of registered routes
|
|
136
|
+
*/
|
|
137
|
+
count(): number {
|
|
138
|
+
return this.routes.length;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compile a path pattern into a regex
|
|
143
|
+
*
|
|
144
|
+
* Supports:
|
|
145
|
+
* - Static paths: /users
|
|
146
|
+
* - Named params: /users/:id
|
|
147
|
+
* - Optional params: /users/:id?
|
|
148
|
+
* - Wildcards: /files/*
|
|
149
|
+
*/
|
|
150
|
+
private compilePath(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
151
|
+
const paramNames: string[] = [];
|
|
152
|
+
|
|
153
|
+
// Normalize path
|
|
154
|
+
let normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
155
|
+
|
|
156
|
+
// Handle wildcards (*) first - replace with placeholder
|
|
157
|
+
const WILDCARD_PLACEHOLDER = '__WILDCARD__';
|
|
158
|
+
normalizedPath = normalizedPath.replace(/\*$/g, WILDCARD_PLACEHOLDER);
|
|
159
|
+
normalizedPath = normalizedPath.replace(/\*\//g, `${WILDCARD_PLACEHOLDER}/`);
|
|
160
|
+
|
|
161
|
+
// Escape special regex characters except for our patterns
|
|
162
|
+
let regexString = normalizedPath
|
|
163
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
164
|
+
|
|
165
|
+
// Convert wildcard placeholders back to regex
|
|
166
|
+
regexString = regexString.replace(/__WILDCARD__/g, '.*');
|
|
167
|
+
|
|
168
|
+
// Handle optional params (:param?)
|
|
169
|
+
regexString = regexString.replace(/:(\w+)\?/g, (_, name) => {
|
|
170
|
+
paramNames.push(name);
|
|
171
|
+
return '([^/]*)';
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Handle required params (:param)
|
|
175
|
+
regexString = regexString.replace(/:(\w+)/g, (_, name) => {
|
|
176
|
+
paramNames.push(name);
|
|
177
|
+
return '([^/]+)';
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Create the final regex
|
|
181
|
+
const pattern = new RegExp(`^${regexString}$`);
|
|
182
|
+
|
|
183
|
+
return { pattern, paramNames };
|
|
184
|
+
}
|
|
185
|
+
}
|