@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.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,846 @@
1
+ /**
2
+ * Development Server Implementation
3
+ *
4
+ * Provides a development server with:
5
+ * - Static file serving using Bun.file()
6
+ * - Framework auto-detection
7
+ * - JSX/TSX transpilation using Bun's built-in capabilities
8
+ * - SPA fallback support
9
+ * - Integration with the Router for API routes
10
+ * - Graceful shutdown handling
11
+ */
12
+
13
+ import { Router, type RouteMatch } from "../router/index.js";
14
+ import { Logger, createLogger } from "../logger/index.js";
15
+ import type { HTTPMethod } from "../types/index.js";
16
+ import type {
17
+ DevServerConfig,
18
+ PartialDevServerConfig,
19
+ DevServerState,
20
+ FrontendFramework,
21
+ FrameworkDetectionResult,
22
+ PackageDependencies,
23
+ FileResolution,
24
+ DevServerMiddleware,
25
+ DevServerEventListener,
26
+ DevServerEvent,
27
+ TransformResult,
28
+ TransformOptions,
29
+ HMRConfig,
30
+ ConsoleStreamConfig,
31
+ PartialConsoleStreamConfig,
32
+ SSRConfig,
33
+ PartialSSRConfig,
34
+ BuildManifest,
35
+ } from "./types.js";
36
+ import { HMRManager, createHMRManager } from "./hmr.js";
37
+ import { injectHMRScript } from "./hmr-client.js";
38
+ import { ConsoleStreamManager, createConsoleStreamManager, injectConsoleScript } from "./console-stream.js";
39
+ import { SSRRenderer, createSSRRenderer } from "./ssr.js";
40
+
41
+ // ============= Constants =============
42
+
43
+ const DEFAULT_PORT = 3000;
44
+ const DEFAULT_HOSTNAME = "localhost";
45
+ const DEFAULT_PUBLIC_DIR = "public";
46
+ const DEFAULT_PAGES_DIR = "pages";
47
+
48
+ const MIME_TYPES: Record<string, string> = {
49
+ ".html": "text/html; charset=utf-8",
50
+ ".css": "text/css; charset=utf-8",
51
+ ".js": "application/javascript; charset=utf-8",
52
+ ".mjs": "application/javascript; charset=utf-8",
53
+ ".ts": "application/typescript; charset=utf-8",
54
+ ".tsx": "application/typescript; charset=utf-8",
55
+ ".jsx": "application/javascript; charset=utf-8",
56
+ ".json": "application/json; charset=utf-8",
57
+ ".png": "image/png",
58
+ ".jpg": "image/jpeg",
59
+ ".jpeg": "image/jpeg",
60
+ ".gif": "image/gif",
61
+ ".svg": "image/svg+xml",
62
+ ".ico": "image/x-icon",
63
+ ".woff": "font/woff",
64
+ ".woff2": "font/woff2",
65
+ ".ttf": "font/ttf",
66
+ ".eot": "application/vnd.ms-fontobject",
67
+ ".webp": "image/webp",
68
+ ".avif": "image/avif",
69
+ ".mp4": "video/mp4",
70
+ ".webm": "video/webm",
71
+ ".mp3": "audio/mpeg",
72
+ ".wav": "audio/wav",
73
+ ".pdf": "application/pdf",
74
+ ".zip": "application/zip",
75
+ ".wasm": "application/wasm",
76
+ };
77
+
78
+ // ============= Framework Detection =============
79
+
80
+ const FRAMEWORK_INDICATORS: Record<FrontendFramework, string[]> = {
81
+ react: ["react", "react-dom"],
82
+ vue: ["vue"],
83
+ svelte: ["svelte"],
84
+ solid: ["solid-js"],
85
+ };
86
+
87
+ /**
88
+ * Detect framework from package.json dependencies
89
+ */
90
+ function detectFramework(rootDir: string): FrameworkDetectionResult {
91
+ try {
92
+ const packageJsonPath = `${rootDir}/package.json`;
93
+ const packageJsonFile = Bun.file(packageJsonPath);
94
+
95
+ if (!packageJsonFile.exists()) {
96
+ return {
97
+ framework: "react",
98
+ detected: false,
99
+ source: "config",
100
+ };
101
+ }
102
+
103
+ // Read package.json synchronously using Bun's sync read
104
+ const packageJson = JSON.parse(require("fs").readFileSync(packageJsonPath, "utf-8"));
105
+ const dependencies: PackageDependencies = {
106
+ ...packageJson.dependencies,
107
+ ...packageJson.devDependencies,
108
+ };
109
+
110
+ // Check for each framework in order of specificity
111
+ // Solid and Svelte are more specific than React/Vue
112
+ const frameworkOrder: FrontendFramework[] = ["solid", "svelte", "vue", "react"];
113
+
114
+ for (const framework of frameworkOrder) {
115
+ const indicators = FRAMEWORK_INDICATORS[framework];
116
+ if (indicators.some((pkg) => dependencies[pkg])) {
117
+ return {
118
+ framework,
119
+ detected: true,
120
+ source: "package.json",
121
+ };
122
+ }
123
+ }
124
+
125
+ // Default to React if no framework detected
126
+ return {
127
+ framework: "react",
128
+ detected: false,
129
+ source: "config",
130
+ };
131
+ } catch {
132
+ return {
133
+ framework: "react",
134
+ detected: false,
135
+ source: "config",
136
+ };
137
+ }
138
+ }
139
+
140
+ // ============= DevServer Class =============
141
+
142
+ export class DevServer {
143
+ private config: DevServerConfig;
144
+ private state: DevServerState;
145
+ private logger: Logger;
146
+ private router: Router | null = null;
147
+ private apiRouter: Router | null = null;
148
+ private server: ReturnType<typeof Bun.serve> | null = null;
149
+ private middlewares: DevServerMiddleware[] = [];
150
+ private eventListeners: DevServerEventListener[] = [];
151
+ private hmrManager: HMRManager | null = null;
152
+ private consoleStreamManager: ConsoleStreamManager | null = null;
153
+ private ssrRenderer: SSRRenderer | null = null;
154
+ private ssrEnabled = false;
155
+
156
+ constructor(config: PartialDevServerConfig) {
157
+ this.config = this.normalizeConfig(config);
158
+ this.logger = createLogger({
159
+ level: "debug",
160
+ pretty: true,
161
+ context: { component: "DevServer" },
162
+ });
163
+
164
+ // Detect framework
165
+ const frameworkResult =
166
+ this.config.framework === "auto"
167
+ ? detectFramework(this.config.rootDir)
168
+ : {
169
+ framework: this.config.framework as FrontendFramework,
170
+ detected: true,
171
+ source: "config" as const,
172
+ };
173
+
174
+ this.state = {
175
+ running: false,
176
+ port: this.config.port,
177
+ hostname: this.config.hostname,
178
+ framework: frameworkResult.framework,
179
+ startTime: null,
180
+ activeConnections: 0,
181
+ };
182
+
183
+ if (frameworkResult.detected) {
184
+ this.logger.info(`Detected framework: ${frameworkResult.framework}`, {
185
+ source: frameworkResult.source,
186
+ });
187
+ } else {
188
+ this.logger.info(`Using default framework: ${frameworkResult.framework}`);
189
+ }
190
+
191
+ // Initialize HMR if enabled
192
+ if (this.config.hmr) {
193
+ this.hmrManager = createHMRManager(
194
+ frameworkResult.framework,
195
+ this.config.port
196
+ );
197
+ this.logger.info("HMR enabled");
198
+ }
199
+
200
+ // Initialize Console Stream if enabled (default: true)
201
+ if (this.config.consoleStream?.enabled !== false) {
202
+ this.consoleStreamManager = createConsoleStreamManager(
203
+ this.config.port,
204
+ this.config.consoleStream
205
+ );
206
+ this.logger.info("Console streaming enabled");
207
+ }
208
+
209
+ this.emitEvent({
210
+ type: "framework-detected",
211
+ framework: frameworkResult.framework,
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Normalize partial config to full config with defaults
217
+ */
218
+ private normalizeConfig(config: PartialDevServerConfig): DevServerConfig {
219
+ return {
220
+ port: config.port ?? DEFAULT_PORT,
221
+ hostname: config.hostname ?? DEFAULT_HOSTNAME,
222
+ rootDir: config.rootDir,
223
+ publicDir: config.publicDir ?? DEFAULT_PUBLIC_DIR,
224
+ pagesDir: config.pagesDir ?? DEFAULT_PAGES_DIR,
225
+ hmr: config.hmr ?? true,
226
+ framework: config.framework ?? "auto",
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Get current server state
232
+ */
233
+ getState(): DevServerState {
234
+ return { ...this.state };
235
+ }
236
+
237
+ /**
238
+ * Get server configuration
239
+ */
240
+ getConfig(): DevServerConfig {
241
+ return { ...this.config };
242
+ }
243
+
244
+ /**
245
+ * Get detected framework
246
+ */
247
+ getFramework(): FrontendFramework {
248
+ return this.state.framework;
249
+ }
250
+
251
+ /**
252
+ * Set the API router for handling API routes
253
+ */
254
+ setApiRouter(router: Router): void {
255
+ this.apiRouter = router;
256
+ this.logger.debug("API router configured");
257
+ }
258
+
259
+ /**
260
+ * Add middleware to the dev server
261
+ */
262
+ use(middleware: DevServerMiddleware): void {
263
+ this.middlewares.push(middleware);
264
+ this.logger.debug("Middleware added");
265
+ }
266
+
267
+ /**
268
+ * Add event listener
269
+ */
270
+ onEvent(listener: DevServerEventListener): void {
271
+ this.eventListeners.push(listener);
272
+ }
273
+
274
+ /**
275
+ * Emit an event to all listeners
276
+ */
277
+ private emitEvent(event: DevServerEvent): void {
278
+ for (const listener of this.eventListeners) {
279
+ try {
280
+ listener(event);
281
+ } catch (error) {
282
+ this.logger.error("Event listener error", error);
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Get content type for a file path
289
+ */
290
+ private getContentType(filePath: string): string {
291
+ const ext = filePath.substring(filePath.lastIndexOf("."));
292
+ return MIME_TYPES[ext] || "application/octet-stream";
293
+ }
294
+
295
+ /**
296
+ * Resolve a request path to a file
297
+ */
298
+ private async resolveFile(pathname: string): Promise<FileResolution> {
299
+ // Remove leading slash and query string
300
+ const cleanPath = pathname.split("?")[0].replace(/^\//, "");
301
+
302
+ // Try to find the file in public directory
303
+ const publicPath = `${this.config.rootDir}/${this.config.publicDir}/${cleanPath}`;
304
+
305
+ // Check if file exists
306
+ const publicFile = Bun.file(publicPath);
307
+ if (await publicFile.exists()) {
308
+ return {
309
+ found: true,
310
+ filePath: publicPath,
311
+ contentType: this.getContentType(publicPath),
312
+ };
313
+ }
314
+
315
+ // Try index.html for directory requests
316
+ const indexPath = `${this.config.rootDir}/${this.config.publicDir}/${cleanPath}/index.html`;
317
+ const indexFile = Bun.file(indexPath);
318
+ if (await indexFile.exists()) {
319
+ return {
320
+ found: true,
321
+ filePath: indexPath,
322
+ contentType: "text/html; charset=utf-8",
323
+ };
324
+ }
325
+
326
+ // SPA fallback to root index.html
327
+ const rootIndexPath = `${this.config.rootDir}/${this.config.publicDir}/index.html`;
328
+ const rootIndexFile = Bun.file(rootIndexPath);
329
+ if (await rootIndexFile.exists()) {
330
+ return {
331
+ found: true,
332
+ filePath: rootIndexPath,
333
+ contentType: "text/html; charset=utf-8",
334
+ isFallback: true,
335
+ };
336
+ }
337
+
338
+ return { found: false };
339
+ }
340
+
341
+ /**
342
+ * Transform a file based on its type
343
+ */
344
+ private async transformFile(options: TransformOptions): Promise<TransformResult> {
345
+ const { filePath, content, framework } = options;
346
+
347
+ // For JSX/TSX files, Bun handles transpilation automatically
348
+ // We just need to set the correct content type
349
+ const ext = filePath.substring(filePath.lastIndexOf("."));
350
+
351
+ if (ext === ".jsx" || ext === ".tsx") {
352
+ // Bun automatically handles JSX transpilation
353
+ // The content is returned as-is since Bun's serve handles it
354
+ return {
355
+ content,
356
+ contentType: "application/javascript; charset=utf-8",
357
+ };
358
+ }
359
+
360
+ return {
361
+ content,
362
+ contentType: this.getContentType(filePath),
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Handle static file requests
368
+ */
369
+ private async handleStaticFile(pathname: string): Promise<Response | null> {
370
+ const resolution = await this.resolveFile(pathname);
371
+
372
+ if (!resolution.found || !resolution.filePath) {
373
+ return null;
374
+ }
375
+
376
+ const file = Bun.file(resolution.filePath);
377
+
378
+ if (!(await file.exists())) {
379
+ return null;
380
+ }
381
+
382
+ // Log if this is a fallback (SPA routing)
383
+ if (resolution.isFallback) {
384
+ this.logger.debug(`SPA fallback: ${pathname} -> index.html`);
385
+ }
386
+
387
+ // Inject HMR and Console scripts for HTML files
388
+ if (resolution.contentType === "text/html; charset=utf-8") {
389
+ let html = await file.text();
390
+
391
+ // Inject HMR script
392
+ if (this.hmrManager) {
393
+ html = injectHMRScript(html, this.hmrManager.getPort());
394
+ }
395
+
396
+ // Inject Console Stream script
397
+ if (this.consoleStreamManager) {
398
+ html = injectConsoleScript(html, this.consoleStreamManager.getPort());
399
+ }
400
+
401
+ return new Response(html, {
402
+ headers: {
403
+ "Content-Type": "text/html; charset=utf-8",
404
+ },
405
+ });
406
+ }
407
+
408
+ return new Response(file, {
409
+ headers: {
410
+ "Content-Type": resolution.contentType || this.getContentType(resolution.filePath),
411
+ },
412
+ });
413
+ }
414
+
415
+ /**
416
+ * Handle API route requests
417
+ */
418
+ private async handleApiRoute(request: Request): Promise<Response | null> {
419
+ if (!this.apiRouter) {
420
+ return null;
421
+ }
422
+
423
+ const url = new URL(request.url);
424
+ const method = request.method as HTTPMethod;
425
+ const match = this.apiRouter.match(method, url.pathname);
426
+
427
+ if (!match) {
428
+ return null;
429
+ }
430
+
431
+ this.logger.debug(`API route matched: ${method} ${url.pathname}`);
432
+
433
+ // Create a minimal context for the handler
434
+ const context = {
435
+ request,
436
+ method,
437
+ path: url.pathname,
438
+ url,
439
+ query: url.searchParams,
440
+ params: match.params,
441
+ headers: request.headers,
442
+ json: async () => request.json(),
443
+ text: async () => request.text(),
444
+ status: (code: number) => ({ status: code }),
445
+ header: (name: string, value: string) => ({ header: [name, value] }),
446
+ };
447
+
448
+ try {
449
+ const result = await match.handler(context);
450
+
451
+ if (result instanceof Response) {
452
+ return result;
453
+ }
454
+
455
+ // Convert result to JSON response
456
+ return Response.json(result);
457
+ } catch (error) {
458
+ this.logger.error(`API route error: ${url.pathname}`, error);
459
+ return Response.json(
460
+ {
461
+ error: "Internal Server Error",
462
+ statusCode: 500,
463
+ },
464
+ { status: 500 }
465
+ );
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Apply middleware chain
471
+ */
472
+ private async applyMiddleware(request: Request): Promise<Response | null> {
473
+ if (this.middlewares.length === 0) {
474
+ return null;
475
+ }
476
+
477
+ let index = 0;
478
+
479
+ const next = async (): Promise<Response> => {
480
+ if (index >= this.middlewares.length) {
481
+ // Return a placeholder that indicates no middleware handled it
482
+ return new Response(null, { status: 404 });
483
+ }
484
+
485
+ const middleware = this.middlewares[index++];
486
+ return middleware(request, next);
487
+ };
488
+
489
+ const response = await next();
490
+
491
+ // If middleware returned 404, it means no middleware handled the request
492
+ if (response.status === 404 && !response.body) {
493
+ return null;
494
+ }
495
+
496
+ return response;
497
+ }
498
+
499
+ /**
500
+ * Main request handler
501
+ */
502
+ private async handleRequest(request: Request): Promise<Response> {
503
+ const startTime = Date.now();
504
+ const url = new URL(request.url);
505
+ const pathname = url.pathname;
506
+
507
+ this.state.activeConnections++;
508
+
509
+ try {
510
+ // 1. Try middleware first
511
+ const middlewareResponse = await this.applyMiddleware(request);
512
+ if (middlewareResponse) {
513
+ this.logRequest(request.method, pathname, startTime);
514
+ return middlewareResponse;
515
+ }
516
+
517
+ // 2. Try API routes
518
+ const apiResponse = await this.handleApiRoute(request);
519
+ if (apiResponse) {
520
+ this.logRequest(request.method, pathname, startTime);
521
+ return apiResponse;
522
+ }
523
+
524
+ // 3. Try SSR if enabled
525
+ if (this.ssrEnabled) {
526
+ const ssrResponse = await this.handleSSRRequest(request);
527
+ if (ssrResponse) {
528
+ this.logRequest(request.method, pathname, startTime);
529
+ return ssrResponse;
530
+ }
531
+ }
532
+
533
+ // 4. Try static files
534
+ const staticResponse = await this.handleStaticFile(pathname);
535
+ if (staticResponse) {
536
+ this.logRequest(request.method, pathname, startTime);
537
+ return staticResponse;
538
+ }
539
+
540
+ // 5. 404 Not Found
541
+ this.logger.warn(`Not found: ${request.method} ${pathname}`);
542
+ return Response.json(
543
+ {
544
+ error: "Not Found",
545
+ statusCode: 404,
546
+ },
547
+ { status: 404 }
548
+ );
549
+ } catch (error) {
550
+ this.logger.error(`Request error: ${pathname}`, error);
551
+ return Response.json(
552
+ {
553
+ error: "Internal Server Error",
554
+ statusCode: 500,
555
+ stack: process.env.NODE_ENV !== "production" && error instanceof Error ? error.stack : undefined,
556
+ },
557
+ { status: 500 }
558
+ );
559
+ } finally {
560
+ this.state.activeConnections--;
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Log a request
566
+ */
567
+ private logRequest(method: string, path: string, startTime: number): void {
568
+ const duration = Date.now() - startTime;
569
+ this.logger.debug(`${method} ${path}`, { duration: `${duration}ms` });
570
+
571
+ this.emitEvent({
572
+ type: "request",
573
+ method,
574
+ path,
575
+ duration,
576
+ });
577
+ }
578
+
579
+ /**
580
+ * Start the development server
581
+ */
582
+ async start(): Promise<void> {
583
+ if (this.state.running) {
584
+ this.logger.warn("Server is already running");
585
+ return;
586
+ }
587
+
588
+ return new Promise((resolve, reject) => {
589
+ try {
590
+ this.server = Bun.serve({
591
+ port: this.config.port,
592
+ hostname: this.config.hostname,
593
+ fetch: this.handleRequest.bind(this),
594
+ });
595
+
596
+ this.state.running = true;
597
+ this.state.startTime = new Date();
598
+
599
+ // Start HMR file watching
600
+ if (this.hmrManager) {
601
+ this.hmrManager.startWatching(this.config.rootDir);
602
+ }
603
+
604
+ // Start Console Stream server
605
+ if (this.consoleStreamManager) {
606
+ this.consoleStreamManager.start();
607
+ }
608
+
609
+ this.logger.info(
610
+ `Development server started at http://${this.config.hostname}:${this.config.port}`
611
+ );
612
+
613
+ this.emitEvent({
614
+ type: "start",
615
+ port: this.config.port,
616
+ hostname: this.config.hostname,
617
+ });
618
+
619
+ resolve();
620
+ } catch (error) {
621
+ this.logger.error("Failed to start server", error);
622
+ reject(error);
623
+ }
624
+ });
625
+ }
626
+
627
+ /**
628
+ * Stop the development server
629
+ */
630
+ async stop(reason?: string): Promise<void> {
631
+ if (!this.state.running || !this.server) {
632
+ this.logger.warn("Server is not running");
633
+ return;
634
+ }
635
+
636
+ const server = this.server;
637
+ return new Promise((resolve, reject) => {
638
+ try {
639
+ // Stop HMR manager
640
+ if (this.hmrManager) {
641
+ this.hmrManager.stop();
642
+ }
643
+
644
+ // Stop Console Stream manager
645
+ if (this.consoleStreamManager) {
646
+ this.consoleStreamManager.stop();
647
+ }
648
+
649
+ server.stop(true);
650
+ this.state.running = false;
651
+ this.state.startTime = null;
652
+
653
+ this.logger.info(`Development server stopped${reason ? `: ${reason}` : ""}`);
654
+
655
+ this.emitEvent({
656
+ type: "stop",
657
+ reason,
658
+ });
659
+
660
+ resolve();
661
+ } catch (error) {
662
+ this.logger.error("Failed to stop server", error);
663
+ reject(error);
664
+ }
665
+ });
666
+ }
667
+
668
+ /**
669
+ * Restart the development server
670
+ */
671
+ async restart(): Promise<void> {
672
+ this.logger.info("Restarting development server...");
673
+ await this.stop("restart");
674
+ await this.start();
675
+ }
676
+
677
+ /**
678
+ * Check if the server is running
679
+ */
680
+ isRunning(): boolean {
681
+ return this.state.running;
682
+ }
683
+
684
+ /**
685
+ * Get server URL
686
+ */
687
+ getUrl(): string {
688
+ return `http://${this.config.hostname}:${this.config.port}`;
689
+ }
690
+
691
+ /**
692
+ * Get HMR manager
693
+ */
694
+ getHMRManager(): HMRManager | null {
695
+ return this.hmrManager;
696
+ }
697
+
698
+ /**
699
+ * Check if HMR is enabled
700
+ */
701
+ isHMREnabled(): boolean {
702
+ return this.hmrManager !== null && this.hmrManager.isEnabled();
703
+ }
704
+
705
+ /**
706
+ * Get HMR WebSocket URL
707
+ */
708
+ getHMRUrl(): string | null {
709
+ return this.hmrManager ? this.hmrManager.getWebSocketUrl() : null;
710
+ }
711
+
712
+ /**
713
+ * Get Console Stream manager
714
+ */
715
+ getConsoleStreamManager(): ConsoleStreamManager | null {
716
+ return this.consoleStreamManager;
717
+ }
718
+
719
+ /**
720
+ * Check if console streaming is enabled
721
+ */
722
+ isConsoleStreamEnabled(): boolean {
723
+ return this.consoleStreamManager !== null && this.consoleStreamManager.isEnabled();
724
+ }
725
+
726
+ /**
727
+ * Get Console Stream WebSocket URL
728
+ */
729
+ getConsoleStreamUrl(): string | null {
730
+ return this.consoleStreamManager ? this.consoleStreamManager.getWebSocketUrl() : null;
731
+ }
732
+
733
+ // ============= SSR Methods =============
734
+
735
+ /**
736
+ * Enable SSR with configuration
737
+ */
738
+ enableSSR(config: PartialSSRConfig): void {
739
+ this.ssrRenderer = createSSRRenderer({
740
+ ...config,
741
+ framework: config.framework || this.state.framework,
742
+ });
743
+ this.ssrEnabled = true;
744
+ this.logger.info("SSR enabled", { framework: config.framework || this.state.framework });
745
+ }
746
+
747
+ /**
748
+ * Disable SSR
749
+ */
750
+ disableSSR(): void {
751
+ this.ssrRenderer = null;
752
+ this.ssrEnabled = false;
753
+ this.logger.info("SSR disabled");
754
+ }
755
+
756
+ /**
757
+ * Check if SSR is enabled
758
+ */
759
+ isSSREnabled(): boolean {
760
+ return this.ssrEnabled && this.ssrRenderer !== null;
761
+ }
762
+
763
+ /**
764
+ * Get SSR renderer
765
+ */
766
+ getSSRRenderer(): SSRRenderer | null {
767
+ return this.ssrRenderer;
768
+ }
769
+
770
+ /**
771
+ * Set SSR renderer directly
772
+ */
773
+ setSSRRenderer(renderer: SSRRenderer): void {
774
+ this.ssrRenderer = renderer;
775
+ this.ssrEnabled = true;
776
+ this.logger.info("SSR renderer configured");
777
+ }
778
+
779
+ /**
780
+ * Handle SSR request
781
+ */
782
+ private async handleSSRRequest(request: Request): Promise<Response | null> {
783
+ if (!this.ssrEnabled || !this.ssrRenderer) {
784
+ return null;
785
+ }
786
+
787
+ const url = new URL(request.url);
788
+ const pathname = url.pathname;
789
+
790
+ // Skip static assets
791
+ if (this.isStaticAsset(pathname)) {
792
+ return null;
793
+ }
794
+
795
+ // Skip API routes
796
+ if (pathname.startsWith("/api/")) {
797
+ return null;
798
+ }
799
+
800
+ try {
801
+ // Check if streaming is enabled
802
+ if (this.ssrRenderer.isStreamingEnabled()) {
803
+ const stream = this.ssrRenderer.renderToStream(request.url, request);
804
+ return new Response(stream, {
805
+ headers: {
806
+ "Content-Type": "text/html; charset=utf-8",
807
+ },
808
+ });
809
+ } else {
810
+ const result = await this.ssrRenderer.render(request.url, request);
811
+ return new Response(result.html, {
812
+ status: result.status,
813
+ headers: {
814
+ "Content-Type": "text/html; charset=utf-8",
815
+ },
816
+ });
817
+ }
818
+ } catch (error) {
819
+ this.logger.error(`SSR render error: ${pathname}`, error);
820
+ return null;
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Check if path is a static asset
826
+ */
827
+ private isStaticAsset(pathname: string): boolean {
828
+ const staticExtensions = [
829
+ ".js", ".mjs", ".css", ".json",
830
+ ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".avif",
831
+ ".woff", ".woff2", ".ttf", ".eot",
832
+ ".mp4", ".webm", ".mp3", ".wav",
833
+ ".pdf", ".zip", ".wasm",
834
+ ];
835
+ return staticExtensions.some(ext => pathname.endsWith(ext));
836
+ }
837
+ }
838
+
839
+ // ============= Factory Function =============
840
+
841
+ /**
842
+ * Create a development server
843
+ */
844
+ export function createDevServer(config: PartialDevServerConfig & { consoleStream?: PartialConsoleStreamConfig }): DevServer {
845
+ return new DevServer(config);
846
+ }