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,525 @@
1
+ /**
2
+ * PhoenixJS - Response Helper
3
+ *
4
+ * Static helper class and fluent builder for creating HTTP responses.
5
+ */
6
+
7
+ export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
8
+
9
+ export interface ResponseOptions {
10
+ status?: number;
11
+ headers?: Record<string, string>;
12
+ }
13
+
14
+ export interface CookieOptions {
15
+ maxAge?: number;
16
+ expires?: Date;
17
+ path?: string;
18
+ domain?: string;
19
+ secure?: boolean;
20
+ httpOnly?: boolean;
21
+ sameSite?: 'Strict' | 'Lax' | 'None';
22
+ }
23
+
24
+ interface Cookie {
25
+ name: string;
26
+ value: string;
27
+ options: CookieOptions;
28
+ }
29
+
30
+ /**
31
+ * Fluent Response Builder
32
+ * Allows chaining methods to build a response
33
+ */
34
+ export class ResponseBuilder {
35
+ private statusCode = 200;
36
+ private responseHeaders: Map<string, string> = new Map();
37
+ private responseCookies: Cookie[] = [];
38
+
39
+ /**
40
+ * Set the response status code
41
+ */
42
+ status(code: number): this {
43
+ this.statusCode = code;
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Add a header to the response
49
+ */
50
+ header(name: string, value: string): this {
51
+ this.responseHeaders.set(name, value);
52
+ return this;
53
+ }
54
+
55
+ /**
56
+ * Add multiple headers
57
+ */
58
+ withHeaders(headers: Record<string, string>): this {
59
+ for (const [name, value] of Object.entries(headers)) {
60
+ this.responseHeaders.set(name, value);
61
+ }
62
+ return this;
63
+ }
64
+
65
+ /**
66
+ * Set a cookie
67
+ */
68
+ cookie(name: string, value: string, options: CookieOptions = {}): this {
69
+ this.responseCookies.push({ name, value, options });
70
+ return this;
71
+ }
72
+
73
+ /**
74
+ * Remove a cookie (set it to expire immediately)
75
+ */
76
+ withoutCookie(name: string, options: CookieOptions = {}): this {
77
+ this.responseCookies.push({
78
+ name,
79
+ value: '',
80
+ options: { ...options, maxAge: 0 },
81
+ });
82
+ return this;
83
+ }
84
+
85
+ /**
86
+ * Set CORS headers
87
+ */
88
+ cors(origin: string = '*', options: {
89
+ methods?: string[];
90
+ headers?: string[];
91
+ credentials?: boolean;
92
+ maxAge?: number;
93
+ } = {}): this {
94
+ this.header('Access-Control-Allow-Origin', origin);
95
+
96
+ if (options.methods) {
97
+ this.header('Access-Control-Allow-Methods', options.methods.join(', '));
98
+ }
99
+ if (options.headers) {
100
+ this.header('Access-Control-Allow-Headers', options.headers.join(', '));
101
+ }
102
+ if (options.credentials) {
103
+ this.header('Access-Control-Allow-Credentials', 'true');
104
+ }
105
+ if (options.maxAge) {
106
+ this.header('Access-Control-Max-Age', options.maxAge.toString());
107
+ }
108
+
109
+ return this;
110
+ }
111
+
112
+ /**
113
+ * Set cache control headers
114
+ */
115
+ cache(maxAge: number, options: {
116
+ public?: boolean;
117
+ private?: boolean;
118
+ noStore?: boolean;
119
+ mustRevalidate?: boolean;
120
+ } = {}): this {
121
+ const directives: string[] = [];
122
+
123
+ if (options.public) directives.push('public');
124
+ if (options.private) directives.push('private');
125
+ if (options.noStore) directives.push('no-store');
126
+ if (options.mustRevalidate) directives.push('must-revalidate');
127
+
128
+ directives.push(`max-age=${maxAge}`);
129
+
130
+ this.header('Cache-Control', directives.join(', '));
131
+ return this;
132
+ }
133
+
134
+ /**
135
+ * Disable caching
136
+ */
137
+ noCache(): this {
138
+ this.header('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
139
+ this.header('Pragma', 'no-cache');
140
+ this.header('Expires', '0');
141
+ return this;
142
+ }
143
+
144
+ /**
145
+ * Build and return a JSON response
146
+ */
147
+ json(data: unknown): Response {
148
+ this.header('Content-Type', 'application/json');
149
+ return this.build(JSON.stringify(data));
150
+ }
151
+
152
+ /**
153
+ * Build and return a text response
154
+ */
155
+ text(content: string): Response {
156
+ this.header('Content-Type', 'text/plain');
157
+ return this.build(content);
158
+ }
159
+
160
+ /**
161
+ * Build and return an HTML response
162
+ */
163
+ html(content: string): Response {
164
+ this.header('Content-Type', 'text/html');
165
+ return this.build(content);
166
+ }
167
+
168
+ /**
169
+ * Build and return an XML response
170
+ */
171
+ xml(content: string): Response {
172
+ this.header('Content-Type', 'application/xml');
173
+ return this.build(content);
174
+ }
175
+
176
+ /**
177
+ * Build and return a file download response
178
+ */
179
+ download(content: Buffer | string | Uint8Array, filename: string, mimeType?: string): Response {
180
+ const contentType = mimeType ?? 'application/octet-stream';
181
+ this.header('Content-Type', contentType);
182
+ this.header('Content-Disposition', `attachment; filename="${filename}"`);
183
+ return this.build(content);
184
+ }
185
+
186
+ /**
187
+ * Build and return an inline file response (displayed in browser)
188
+ */
189
+ file(content: Buffer | Uint8Array, mimeType: string, filename?: string): Response {
190
+ this.header('Content-Type', mimeType);
191
+ if (filename) {
192
+ this.header('Content-Disposition', `inline; filename="${filename}"`);
193
+ }
194
+ return this.build(content);
195
+ }
196
+
197
+ /**
198
+ * Build and return a streaming response
199
+ */
200
+ stream(readable: ReadableStream, mimeType: string = 'application/octet-stream'): Response {
201
+ this.header('Content-Type', mimeType);
202
+ return new Response(readable, {
203
+ status: this.statusCode,
204
+ headers: this.buildHeaders(),
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Build and return a redirect response
210
+ */
211
+ redirect(url: string, permanent: boolean = false): Response {
212
+ this.statusCode = permanent ? 301 : 302;
213
+ this.header('Location', url);
214
+ return this.build(null);
215
+ }
216
+
217
+ /**
218
+ * Build and return a 204 No Content response
219
+ */
220
+ noContent(): Response {
221
+ this.statusCode = 204;
222
+ return this.build(null);
223
+ }
224
+
225
+ /**
226
+ * Build the final response
227
+ */
228
+ build(body?: string | ArrayBuffer | Uint8Array | ReadableStream | null): Response {
229
+ return new Response(body, {
230
+ status: this.statusCode,
231
+ headers: this.buildHeaders(),
232
+ });
233
+ }
234
+
235
+ /**
236
+ * Build headers including cookies
237
+ */
238
+ private buildHeaders(): Headers {
239
+ const headers = new Headers();
240
+
241
+ for (const [name, value] of this.responseHeaders) {
242
+ headers.set(name, value);
243
+ }
244
+
245
+ // Add cookies as Set-Cookie headers
246
+ for (const cookie of this.responseCookies) {
247
+ headers.append('Set-Cookie', this.serializeCookie(cookie));
248
+ }
249
+
250
+ return headers;
251
+ }
252
+
253
+ /**
254
+ * Serialize a cookie to a Set-Cookie header value
255
+ */
256
+ private serializeCookie(cookie: Cookie): string {
257
+ const parts = [`${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`];
258
+ const opts = cookie.options;
259
+
260
+ if (opts.maxAge !== undefined) {
261
+ parts.push(`Max-Age=${opts.maxAge}`);
262
+ }
263
+ if (opts.expires) {
264
+ parts.push(`Expires=${opts.expires.toUTCString()}`);
265
+ }
266
+ if (opts.path) {
267
+ parts.push(`Path=${opts.path}`);
268
+ }
269
+ if (opts.domain) {
270
+ parts.push(`Domain=${opts.domain}`);
271
+ }
272
+ if (opts.secure) {
273
+ parts.push('Secure');
274
+ }
275
+ if (opts.httpOnly) {
276
+ parts.push('HttpOnly');
277
+ }
278
+ if (opts.sameSite) {
279
+ parts.push(`SameSite=${opts.sameSite}`);
280
+ }
281
+
282
+ return parts.join('; ');
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Static Response Helper
288
+ * Provides shorthand methods for common responses
289
+ */
290
+ export class FrameworkResponse {
291
+ /**
292
+ * Create a new ResponseBuilder for fluent API
293
+ */
294
+ static create(): ResponseBuilder {
295
+ return new ResponseBuilder();
296
+ }
297
+
298
+ /**
299
+ * Create a JSON response
300
+ */
301
+ static json(data: unknown, status = 200, headers: Record<string, string> = {}): Response {
302
+ return new Response(JSON.stringify(data), {
303
+ status,
304
+ headers: {
305
+ 'Content-Type': 'application/json',
306
+ ...headers,
307
+ },
308
+ });
309
+ }
310
+
311
+ /**
312
+ * Create a text response
313
+ */
314
+ static text(content: string, status = 200, headers: Record<string, string> = {}): Response {
315
+ return new Response(content, {
316
+ status,
317
+ headers: {
318
+ 'Content-Type': 'text/plain',
319
+ ...headers,
320
+ },
321
+ });
322
+ }
323
+
324
+ /**
325
+ * Create an HTML response
326
+ */
327
+ static html(content: string, status = 200, headers: Record<string, string> = {}): Response {
328
+ return new Response(content, {
329
+ status,
330
+ headers: {
331
+ 'Content-Type': 'text/html',
332
+ ...headers,
333
+ },
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Create an XML response
339
+ */
340
+ static xml(content: string, status = 200, headers: Record<string, string> = {}): Response {
341
+ return new Response(content, {
342
+ status,
343
+ headers: {
344
+ 'Content-Type': 'application/xml',
345
+ ...headers,
346
+ },
347
+ });
348
+ }
349
+
350
+ /**
351
+ * Create a redirect response
352
+ */
353
+ static redirect(url: string, status = 302): Response {
354
+ return new Response(null, {
355
+ status,
356
+ headers: {
357
+ Location: url,
358
+ },
359
+ });
360
+ }
361
+
362
+ /**
363
+ * Create a permanent redirect (301)
364
+ */
365
+ static permanentRedirect(url: string): Response {
366
+ return FrameworkResponse.redirect(url, 301);
367
+ }
368
+
369
+ /**
370
+ * Create a 204 No Content response
371
+ */
372
+ static noContent(): Response {
373
+ return new Response(null, { status: 204 });
374
+ }
375
+
376
+ /**
377
+ * Create an error response
378
+ */
379
+ static error(message: string, status = 500, details?: Record<string, unknown>): Response {
380
+ const body: Record<string, unknown> = {
381
+ error: true,
382
+ message,
383
+ };
384
+ if (details) {
385
+ body.details = details;
386
+ }
387
+ return FrameworkResponse.json(body, status);
388
+ }
389
+
390
+ /**
391
+ * Create a 400 Bad Request response
392
+ */
393
+ static badRequest(message: string, details?: Record<string, unknown>): Response {
394
+ return FrameworkResponse.error(message, 400, details);
395
+ }
396
+
397
+ /**
398
+ * Create a 401 Unauthorized response
399
+ */
400
+ static unauthorized(message = 'Unauthorized'): Response {
401
+ return FrameworkResponse.error(message, 401);
402
+ }
403
+
404
+ /**
405
+ * Create a 403 Forbidden response
406
+ */
407
+ static forbidden(message = 'Forbidden'): Response {
408
+ return FrameworkResponse.error(message, 403);
409
+ }
410
+
411
+ /**
412
+ * Create a 404 Not Found response
413
+ */
414
+ static notFound(message = 'Not Found'): Response {
415
+ return FrameworkResponse.error(message, 404);
416
+ }
417
+
418
+ /**
419
+ * Create a 405 Method Not Allowed response
420
+ */
421
+ static methodNotAllowed(message = 'Method Not Allowed', allowed?: string[]): Response {
422
+ const headers: Record<string, string> = {};
423
+ if (allowed) {
424
+ headers['Allow'] = allowed.join(', ');
425
+ }
426
+ return new Response(JSON.stringify({ error: true, message }), {
427
+ status: 405,
428
+ headers: {
429
+ 'Content-Type': 'application/json',
430
+ ...headers,
431
+ },
432
+ });
433
+ }
434
+
435
+ /**
436
+ * Create a 409 Conflict response
437
+ */
438
+ static conflict(message = 'Conflict', details?: Record<string, unknown>): Response {
439
+ return FrameworkResponse.error(message, 409, details);
440
+ }
441
+
442
+ /**
443
+ * Create a 422 Unprocessable Entity response (validation errors)
444
+ */
445
+ static validationError(errors: Record<string, string[]>): Response {
446
+ return FrameworkResponse.json({
447
+ error: true,
448
+ message: 'Validation failed',
449
+ errors,
450
+ }, 422);
451
+ }
452
+
453
+ /**
454
+ * Create a 429 Too Many Requests response
455
+ */
456
+ static tooManyRequests(message = 'Too Many Requests', retryAfter?: number): Response {
457
+ const headers: Record<string, string> = {};
458
+ if (retryAfter) {
459
+ headers['Retry-After'] = retryAfter.toString();
460
+ }
461
+ return new Response(JSON.stringify({ error: true, message }), {
462
+ status: 429,
463
+ headers: {
464
+ 'Content-Type': 'application/json',
465
+ ...headers,
466
+ },
467
+ });
468
+ }
469
+
470
+ /**
471
+ * Create a 500 Internal Server Error response
472
+ */
473
+ static serverError(message = 'Internal Server Error', details?: Record<string, unknown>): Response {
474
+ return FrameworkResponse.error(message, 500, details);
475
+ }
476
+
477
+ /**
478
+ * Create a 502 Bad Gateway response
479
+ */
480
+ static badGateway(message = 'Bad Gateway'): Response {
481
+ return FrameworkResponse.error(message, 502);
482
+ }
483
+
484
+ /**
485
+ * Create a 503 Service Unavailable response
486
+ */
487
+ static serviceUnavailable(message = 'Service Unavailable', retryAfter?: number): Response {
488
+ const headers: Record<string, string> = {};
489
+ if (retryAfter) {
490
+ headers['Retry-After'] = retryAfter.toString();
491
+ }
492
+ return new Response(JSON.stringify({ error: true, message }), {
493
+ status: 503,
494
+ headers: {
495
+ 'Content-Type': 'application/json',
496
+ ...headers,
497
+ },
498
+ });
499
+ }
500
+
501
+ /**
502
+ * Create a file download response
503
+ */
504
+ static download(content: Buffer | string | Uint8Array, filename: string, mimeType = 'application/octet-stream'): Response {
505
+ return new Response(content, {
506
+ status: 200,
507
+ headers: {
508
+ 'Content-Type': mimeType,
509
+ 'Content-Disposition': `attachment; filename="${filename}"`,
510
+ },
511
+ });
512
+ }
513
+
514
+ /**
515
+ * Create a streaming response
516
+ */
517
+ static stream(readable: ReadableStream, mimeType = 'application/octet-stream'): Response {
518
+ return new Response(readable, {
519
+ status: 200,
520
+ headers: {
521
+ 'Content-Type': mimeType,
522
+ },
523
+ });
524
+ }
525
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * PhoenixJS - HTTP Server
3
+ *
4
+ * Wraps Bun.serve with lifecycle hooks and configuration.
5
+ */
6
+
7
+ import { Kernel } from '@framework/core/Kernel';
8
+ import { Application } from '@framework/core/Application';
9
+ import { WebSocketAdapter } from '@framework/gateway/WebSocketAdapter';
10
+ import type { WebSocketData } from '@framework/gateway/Gateway';
11
+
12
+ // Use the inferred type from Bun.serve to avoid generic type issues
13
+ type BunServer = ReturnType<typeof Bun.serve>;
14
+
15
+ export interface ServerConfig {
16
+ port: number;
17
+ host: string;
18
+ development?: boolean;
19
+ }
20
+
21
+ export interface ServerHooks {
22
+ onStart?: (server: BunServer) => void;
23
+ onStop?: () => void;
24
+ onError?: (error: Error) => void;
25
+ }
26
+
27
+ export class Server {
28
+ private kernel: Kernel;
29
+ private app: Application;
30
+ private config: ServerConfig;
31
+ private hooks: ServerHooks;
32
+ private server: BunServer | null = null;
33
+ private wsAdapter: WebSocketAdapter;
34
+
35
+ constructor(kernel: Kernel, config?: Partial<ServerConfig>, hooks?: ServerHooks) {
36
+ this.kernel = kernel;
37
+ this.app = kernel.getApplication();
38
+ this.config = {
39
+ port: config?.port ?? this.app.getConfig('port'),
40
+ host: config?.host ?? this.app.getConfig('host'),
41
+ development: config?.development ?? this.app.getConfig('env') === 'development',
42
+ };
43
+ this.hooks = hooks ?? {};
44
+ this.wsAdapter = new WebSocketAdapter();
45
+ }
46
+
47
+ /**
48
+ * Start the server
49
+ */
50
+ start(): BunServer {
51
+ const kernel = this.kernel;
52
+ const hooks = this.hooks;
53
+ const wsAdapter = this.wsAdapter;
54
+
55
+ // Initialize WebSocket gateways
56
+ const gatewayManager = this.app.getGatewayManager();
57
+ gatewayManager.registerAdapter(wsAdapter);
58
+ wsAdapter.start(gatewayManager.getByTransport('websocket'));
59
+
60
+ this.server = Bun.serve<WebSocketData>({
61
+ port: this.config.port,
62
+ hostname: this.config.host,
63
+ development: this.config.development,
64
+
65
+ fetch: async (request: Request, server: BunServer): Promise<Response | undefined> => {
66
+ // Check for WebSocket upgrade
67
+ const url = new URL(request.url);
68
+ if (wsAdapter.hasGatewayForPath(url.pathname)) {
69
+ const upgraded = wsAdapter.handleUpgrade(request, server);
70
+ if (upgraded) {
71
+ return undefined;
72
+ }
73
+ return new Response('WebSocket upgrade failed', { status: 400 });
74
+ }
75
+
76
+ return kernel.handle(request);
77
+ },
78
+
79
+ websocket: wsAdapter.getWebSocketConfig(),
80
+
81
+ error: (error: Error): Response => {
82
+ if (hooks.onError) {
83
+ hooks.onError(error);
84
+ }
85
+ console.error('[Server Error]', error);
86
+ return new Response('Internal Server Error', { status: 500 });
87
+ },
88
+ });
89
+
90
+ wsAdapter.setServer(this.server);
91
+
92
+ if (this.hooks.onStart) {
93
+ this.hooks.onStart(this.server);
94
+ }
95
+
96
+ this.logStartup();
97
+ return this.server;
98
+ }
99
+
100
+ /**
101
+ * Log server startup information
102
+ */
103
+ private logStartup(): void {
104
+ const appName = this.app.getConfig('name');
105
+ const env = this.app.environment();
106
+
107
+ console.log('');
108
+ console.log(`🚀 ${appName} started successfully!`);
109
+ console.log('');
110
+ console.log(` Local: http://${this.config.host}:${this.config.port}`);
111
+ console.log(` Environment: ${env}`);
112
+ console.log(` Debug: ${this.app.isDebug() ? 'enabled' : 'disabled'}`);
113
+ console.log('');
114
+ console.log(' Press Ctrl+C to stop');
115
+ console.log('');
116
+ }
117
+
118
+ /**
119
+ * Stop the server
120
+ */
121
+ stop(): void {
122
+ if (this.server) {
123
+ this.server.stop();
124
+ this.server = null;
125
+
126
+ if (this.hooks.onStop) {
127
+ this.hooks.onStop();
128
+ }
129
+
130
+ console.log('Server stopped');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get the underlying Bun server
136
+ */
137
+ getServer(): BunServer | null {
138
+ return this.server;
139
+ }
140
+
141
+ /**
142
+ * Check if server is running
143
+ */
144
+ isRunning(): boolean {
145
+ return this.server !== null;
146
+ }
147
+
148
+ /**
149
+ * Get the port
150
+ */
151
+ getPort(): number {
152
+ return this.config.port;
153
+ }
154
+
155
+ /**
156
+ * Get the host
157
+ */
158
+ getHost(): string {
159
+ return this.config.host;
160
+ }
161
+ }