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.
Files changed (49) hide show
  1. package/index.ts +196 -0
  2. package/package.json +31 -0
  3. package/template/README.md +62 -0
  4. package/template/app/controllers/ExampleController.ts +61 -0
  5. package/template/artisan +2 -0
  6. package/template/bootstrap/app.ts +44 -0
  7. package/template/bunfig.toml +7 -0
  8. package/template/config/database.ts +25 -0
  9. package/template/config/plugins.ts +7 -0
  10. package/template/config/security.ts +158 -0
  11. package/template/framework/cli/Command.ts +17 -0
  12. package/template/framework/cli/ConsoleApplication.ts +55 -0
  13. package/template/framework/cli/artisan.ts +16 -0
  14. package/template/framework/cli/commands/MakeControllerCommand.ts +41 -0
  15. package/template/framework/cli/commands/MakeMiddlewareCommand.ts +41 -0
  16. package/template/framework/cli/commands/MakeModelCommand.ts +36 -0
  17. package/template/framework/cli/commands/MakeValidatorCommand.ts +42 -0
  18. package/template/framework/controller/Controller.ts +222 -0
  19. package/template/framework/core/Application.ts +208 -0
  20. package/template/framework/core/Container.ts +100 -0
  21. package/template/framework/core/Kernel.ts +297 -0
  22. package/template/framework/database/DatabaseAdapter.ts +18 -0
  23. package/template/framework/database/PrismaAdapter.ts +65 -0
  24. package/template/framework/database/SqlAdapter.ts +117 -0
  25. package/template/framework/gateway/Gateway.ts +109 -0
  26. package/template/framework/gateway/GatewayManager.ts +150 -0
  27. package/template/framework/gateway/WebSocketAdapter.ts +159 -0
  28. package/template/framework/gateway/WebSocketGateway.ts +182 -0
  29. package/template/framework/http/Request.ts +608 -0
  30. package/template/framework/http/Response.ts +525 -0
  31. package/template/framework/http/Server.ts +161 -0
  32. package/template/framework/http/UploadedFile.ts +145 -0
  33. package/template/framework/middleware/Middleware.ts +50 -0
  34. package/template/framework/middleware/Pipeline.ts +89 -0
  35. package/template/framework/plugin/Plugin.ts +26 -0
  36. package/template/framework/plugin/PluginManager.ts +61 -0
  37. package/template/framework/routing/RouteRegistry.ts +185 -0
  38. package/template/framework/routing/Router.ts +280 -0
  39. package/template/framework/security/CorsMiddleware.ts +151 -0
  40. package/template/framework/security/CsrfMiddleware.ts +121 -0
  41. package/template/framework/security/HelmetMiddleware.ts +138 -0
  42. package/template/framework/security/InputSanitizerMiddleware.ts +134 -0
  43. package/template/framework/security/RateLimiterMiddleware.ts +189 -0
  44. package/template/framework/security/SecurityManager.ts +128 -0
  45. package/template/framework/validation/Validator.ts +482 -0
  46. package/template/package.json +24 -0
  47. package/template/routes/api.ts +56 -0
  48. package/template/server.ts +29 -0
  49. 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
+ }