@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,728 @@
1
+ /**
2
+ * Hot Module Replacement (HMR) Implementation
3
+ *
4
+ * Provides live updates without full page refreshes for a better developer experience.
5
+ * Supports React Fast Refresh, Vue Hot Component Replacement, Svelte HMR, and Solid Hot Reloading.
6
+ *
7
+ * @module frontend/hmr
8
+ */
9
+
10
+ import { createLogger, type Logger } from "../logger/index.js";
11
+ import type {
12
+ HMRClient,
13
+ HMRConfig,
14
+ HMRUpdate,
15
+ HMRUpdateError,
16
+ HMRDependencyNode,
17
+ HMRClientMessage,
18
+ HMRServerMessage,
19
+ FileChangeEvent,
20
+ FrontendFramework,
21
+ } from "./types.js";
22
+ import { HMR_CLIENT_SCRIPT } from "./hmr-client.js";
23
+
24
+ // ============= Constants =============
25
+
26
+ const DEFAULT_DEBOUNCE_MS = 100;
27
+ const DEFAULT_IGNORE_PATTERNS = ["node_modules", ".git", "dist", "build", ".bun"];
28
+
29
+ // ============= File Watcher Types =============
30
+
31
+ interface FileWatchEvent {
32
+ event: 'create' | 'update' | 'delete';
33
+ filePath: string;
34
+ }
35
+
36
+ type FileWatchCallback = (event: FileWatchEvent) => void;
37
+
38
+ // ============= HMRManager Class =============
39
+
40
+ /**
41
+ * Manages Hot Module Replacement for the development server.
42
+ *
43
+ * Features:
44
+ * - WebSocket server for client communication
45
+ * - File watching with dependency tracking
46
+ * - Framework-specific HMR support (React, Vue, Svelte, Solid)
47
+ * - Debounced file change handling
48
+ * - Error overlay support
49
+ */
50
+ export class HMRManager {
51
+ private config: HMRConfig;
52
+ private logger: Logger;
53
+ private clients: Map<string, HMRClient> = new Map();
54
+ private dependencyGraph: Map<string, HMRDependencyNode> = new Map();
55
+ private pendingUpdates: Map<string, FileChangeEvent> = new Map();
56
+ private debounceTimer: ReturnType<typeof setTimeout> | null = null;
57
+ private framework: FrontendFramework;
58
+ private devServerPort: number;
59
+ private watcher: ReturnType<typeof import('fs').watch> | null = null;
60
+
61
+ constructor(framework: FrontendFramework, devServerPort: number, config?: Partial<HMRConfig>) {
62
+ this.framework = framework;
63
+ this.devServerPort = devServerPort;
64
+ this.config = this.normalizeConfig(config);
65
+ this.logger = createLogger({
66
+ level: "debug",
67
+ pretty: true,
68
+ context: { component: "HMRManager" },
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Normalize partial config to full config with defaults
74
+ */
75
+ private normalizeConfig(config?: Partial<HMRConfig>): HMRConfig {
76
+ return {
77
+ enabled: config?.enabled ?? true,
78
+ port: config?.port ?? this.devServerPort + 1,
79
+ debounceMs: config?.debounceMs ?? DEFAULT_DEBOUNCE_MS,
80
+ ignorePatterns: config?.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Get the HMR client script for injection
86
+ */
87
+ getClientScript(): string {
88
+ return HMR_CLIENT_SCRIPT;
89
+ }
90
+
91
+ /**
92
+ * Get the WebSocket URL for HMR
93
+ */
94
+ getWebSocketUrl(): string {
95
+ return `ws://localhost:${this.config.port}/_hmr`;
96
+ }
97
+
98
+ /**
99
+ * Get the HMR port
100
+ */
101
+ getPort(): number {
102
+ return this.config.port!;
103
+ }
104
+
105
+ /**
106
+ * Check if HMR is enabled
107
+ */
108
+ isEnabled(): boolean {
109
+ return this.config.enabled;
110
+ }
111
+
112
+ /**
113
+ * Handle WebSocket upgrade request
114
+ */
115
+ handleUpgrade(request: Request): WebSocket | null {
116
+ const url = new URL(request.url);
117
+
118
+ if (url.pathname !== "/_hmr") {
119
+ return null;
120
+ }
121
+
122
+ // Check if this is a WebSocket upgrade request
123
+ const upgradeHeader = request.headers.get("upgrade");
124
+ if (upgradeHeader !== "websocket") {
125
+ return null;
126
+ }
127
+
128
+ // Create WebSocket pair using Bun's native WebSocket
129
+ const server = Bun.serve({
130
+ port: this.config.port,
131
+ fetch: this.handleWebSocketFetch.bind(this),
132
+ websocket: {
133
+ open: this.handleWebSocketOpen.bind(this),
134
+ close: this.handleWebSocketClose.bind(this),
135
+ message: this.handleWebSocketMessage.bind(this),
136
+ },
137
+ });
138
+
139
+ this.logger.info(`HMR WebSocket server started on port ${this.config.port}`);
140
+
141
+ return null; // The server handles the WebSocket directly
142
+ }
143
+
144
+ /**
145
+ * Handle fetch requests for WebSocket server
146
+ */
147
+ private handleWebSocketFetch(request: Request, server: any): Response | undefined {
148
+ const url = new URL(request.url);
149
+
150
+ if (url.pathname === "/_hmr") {
151
+ const success = server.upgrade(request);
152
+ if (success) {
153
+ return undefined; // WebSocket upgrade successful
154
+ }
155
+ return new Response("WebSocket upgrade failed", { status: 400 });
156
+ }
157
+
158
+ return new Response("Not found", { status: 404 });
159
+ }
160
+
161
+ /**
162
+ * Handle WebSocket connection open
163
+ */
164
+ private handleWebSocketOpen(ws: any): void {
165
+ const clientId = this.generateClientId();
166
+ const client: HMRClient = {
167
+ id: clientId,
168
+ ws: ws,
169
+ subscribedFiles: new Set(),
170
+ };
171
+
172
+ this.clients.set(clientId, client);
173
+ ws.data = { clientId };
174
+
175
+ this.logger.debug(`HMR client connected: ${clientId}`);
176
+
177
+ // Send connected message
178
+ this.sendToClient(ws, {
179
+ type: "connected",
180
+ clientId,
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Handle WebSocket connection close
186
+ */
187
+ private handleWebSocketClose(ws: any): void {
188
+ const clientId = ws.data?.clientId;
189
+ if (clientId) {
190
+ this.clients.delete(clientId);
191
+ this.logger.debug(`HMR client disconnected: ${clientId}`);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Handle WebSocket message from client
197
+ */
198
+ private handleWebSocketMessage(ws: any, message: string | Buffer): void {
199
+ try {
200
+ const data: HMRClientMessage = JSON.parse(message.toString());
201
+ const clientId = ws.data?.clientId;
202
+
203
+ if (!clientId) {
204
+ return;
205
+ }
206
+
207
+ const client = this.clients.get(clientId);
208
+ if (!client) {
209
+ return;
210
+ }
211
+
212
+ switch (data.type) {
213
+ case "subscribe":
214
+ client.subscribedFiles.add(data.fileId);
215
+ this.logger.debug(`Client ${clientId} subscribed to: ${data.fileId}`);
216
+ break;
217
+
218
+ case "unsubscribe":
219
+ client.subscribedFiles.delete(data.fileId);
220
+ this.logger.debug(`Client ${clientId} unsubscribed from: ${data.fileId}`);
221
+ break;
222
+
223
+ case "ping":
224
+ this.sendToClient(ws, { type: "pong" });
225
+ break;
226
+
227
+ case "module-accepted":
228
+ this.handleModuleAccepted(data.moduleId, data.dependencies);
229
+ break;
230
+ }
231
+ } catch (error) {
232
+ this.logger.error("Failed to parse HMR message", error);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Send message to a WebSocket client
238
+ */
239
+ private sendToClient(ws: WebSocket, message: HMRServerMessage): void {
240
+ if (ws.readyState === WebSocket.OPEN) {
241
+ ws.send(JSON.stringify(message));
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Broadcast update to all connected clients
247
+ */
248
+ broadcastUpdate(update: HMRUpdate): void {
249
+ const message = JSON.stringify(update);
250
+
251
+ for (const client of this.clients.values()) {
252
+ // Check if client is subscribed to any of the changed files
253
+ const isSubscribed = update.changes.some(
254
+ (file) => client.subscribedFiles.has(file)
255
+ );
256
+
257
+ if (isSubscribed || update.type === "reload" || update.type === "error") {
258
+ if (client.ws.readyState === WebSocket.OPEN) {
259
+ client.ws.send(message);
260
+ }
261
+ }
262
+ }
263
+
264
+ this.logger.debug(`Broadcasted ${update.type} update for: ${update.fileId}`);
265
+ }
266
+
267
+ /**
268
+ * Start file watching
269
+ */
270
+ startWatching(rootDir: string): void {
271
+ if (!this.config.enabled) {
272
+ return;
273
+ }
274
+
275
+ this.logger.info(`Starting file watcher for: ${rootDir}`);
276
+
277
+ // Use Node's fs.watch for file watching
278
+ const fs = require('fs');
279
+ const path = require('path');
280
+
281
+ try {
282
+ this.watcher = fs.watch(rootDir, { recursive: true }, (eventType: string, filename: string | null) => {
283
+ if (!filename) return;
284
+
285
+ const filePath = path.join(rootDir, filename);
286
+
287
+ if (eventType === 'rename') {
288
+ // Check if file exists to determine if it's create or delete
289
+ try {
290
+ fs.accessSync(filePath, fs.constants.F_OK);
291
+ this.handleFileChange(filePath, "create");
292
+ } catch {
293
+ this.handleFileChange(filePath, "delete");
294
+ }
295
+ } else if (eventType === 'change') {
296
+ this.handleFileChange(filePath, "update");
297
+ }
298
+ });
299
+
300
+ this.logger.info("File watcher started");
301
+ } catch (error) {
302
+ this.logger.error("Failed to start file watcher", error);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Stop file watching
308
+ */
309
+ stopWatching(): void {
310
+ if (this.watcher) {
311
+ this.watcher.close();
312
+ this.watcher = null;
313
+ this.logger.info("File watcher stopped");
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Handle a file change event
319
+ */
320
+ private handleFileChange(filePath: string, event: "create" | "update" | "delete"): void {
321
+ // Check if file should be ignored
322
+ if (this.shouldIgnoreFile(filePath)) {
323
+ return;
324
+ }
325
+
326
+ this.logger.debug(`File ${event}: ${filePath}`);
327
+
328
+ // Add to pending updates
329
+ this.pendingUpdates.set(filePath, {
330
+ path: filePath,
331
+ event,
332
+ timestamp: Date.now(),
333
+ });
334
+
335
+ // Debounce updates
336
+ this.scheduleDebouncedUpdate();
337
+ }
338
+
339
+ /**
340
+ * Check if a file should be ignored
341
+ */
342
+ private shouldIgnoreFile(filePath: string): boolean {
343
+ // Check ignore patterns
344
+ for (const pattern of this.config.ignorePatterns) {
345
+ if (filePath.includes(pattern)) {
346
+ return true;
347
+ }
348
+ }
349
+
350
+ // Only watch relevant file types
351
+ const relevantExtensions = [".js", ".jsx", ".ts", ".tsx", ".css", ".scss", ".sass", ".less", ".vue", ".svelte"];
352
+ const ext = filePath.substring(filePath.lastIndexOf("."));
353
+
354
+ return !relevantExtensions.includes(ext);
355
+ }
356
+
357
+ /**
358
+ * Schedule debounced update processing
359
+ */
360
+ private scheduleDebouncedUpdate(): void {
361
+ if (this.debounceTimer) {
362
+ clearTimeout(this.debounceTimer);
363
+ }
364
+
365
+ this.debounceTimer = setTimeout(() => {
366
+ this.processPendingUpdates();
367
+ }, this.config.debounceMs);
368
+ }
369
+
370
+ /**
371
+ * Process all pending file updates
372
+ */
373
+ private processPendingUpdates(): void {
374
+ if (this.pendingUpdates.size === 0) {
375
+ return;
376
+ }
377
+
378
+ const updates = Array.from(this.pendingUpdates.values());
379
+ this.pendingUpdates.clear();
380
+
381
+ // Group updates by type
382
+ const changedFiles = updates.map((u) => u.path);
383
+
384
+ // Determine update type based on file changes
385
+ const updateType = this.determineUpdateType(changedFiles);
386
+
387
+ // Create and broadcast update
388
+ const update: HMRUpdate = {
389
+ type: updateType,
390
+ fileId: this.generateFileId(changedFiles[0]),
391
+ timestamp: Date.now(),
392
+ changes: changedFiles,
393
+ };
394
+
395
+ this.broadcastUpdate(update);
396
+ }
397
+
398
+ /**
399
+ * Determine the type of update needed
400
+ */
401
+ private determineUpdateType(changedFiles: string[]): "update" | "reload" {
402
+ // Check if any changed file requires a full reload
403
+ for (const file of changedFiles) {
404
+ const ext = file.substring(file.lastIndexOf("."));
405
+
406
+ // Configuration files always require full reload
407
+ if (file.includes("config") || file.endsWith(".config.js") || file.endsWith(".config.ts")) {
408
+ return "reload";
409
+ }
410
+
411
+ // HTML files require full reload
412
+ if (ext === ".html") {
413
+ return "reload";
414
+ }
415
+
416
+ // Check dependency graph for breaking changes
417
+ const node = this.dependencyGraph.get(file);
418
+ if (node && node.importedBy.size > 0) {
419
+ // If this file is imported by others, check if it's a breaking change
420
+ // For now, we'll be conservative and trigger an update
421
+ // In a full implementation, we'd analyze the actual changes
422
+ }
423
+ }
424
+
425
+ return "update";
426
+ }
427
+
428
+ /**
429
+ * Handle module accepted from client
430
+ */
431
+ private handleModuleAccepted(moduleId: string, dependencies: string[]): void {
432
+ // Update dependency graph
433
+ const node = this.dependencyGraph.get(moduleId);
434
+ if (node) {
435
+ // Update imports
436
+ node.imports = new Set(dependencies);
437
+
438
+ // Update reverse dependencies
439
+ for (const dep of dependencies) {
440
+ const depNode = this.dependencyGraph.get(dep);
441
+ if (depNode) {
442
+ depNode.importedBy.add(moduleId);
443
+ }
444
+ }
445
+ }
446
+
447
+ this.logger.debug(`Module accepted: ${moduleId}`);
448
+ }
449
+
450
+ /**
451
+ * Register a file in the dependency graph
452
+ */
453
+ registerFile(filePath: string, imports: string[] = []): void {
454
+ const node: HMRDependencyNode = {
455
+ filePath,
456
+ imports: new Set(imports),
457
+ importedBy: new Set(),
458
+ lastModified: Date.now(),
459
+ };
460
+
461
+ // Update reverse dependencies
462
+ for (const imp of imports) {
463
+ const importedNode = this.dependencyGraph.get(imp);
464
+ if (importedNode) {
465
+ importedNode.importedBy.add(filePath);
466
+ }
467
+ }
468
+
469
+ this.dependencyGraph.set(filePath, node);
470
+ }
471
+
472
+ /**
473
+ * Get files that depend on a given file
474
+ */
475
+ getDependents(filePath: string): string[] {
476
+ const node = this.dependencyGraph.get(filePath);
477
+ if (!node) {
478
+ return [];
479
+ }
480
+
481
+ return Array.from(node.importedBy);
482
+ }
483
+
484
+ /**
485
+ * Get files that a file imports
486
+ */
487
+ getImports(filePath: string): string[] {
488
+ const node = this.dependencyGraph.get(filePath);
489
+ if (!node) {
490
+ return [];
491
+ }
492
+
493
+ return Array.from(node.imports);
494
+ }
495
+
496
+ /**
497
+ * Broadcast an error to all clients
498
+ */
499
+ broadcastError(error: Error | HMRUpdateError): void {
500
+ const updateError: HMRUpdateError = error instanceof Error
501
+ ? {
502
+ message: error.message,
503
+ stack: error.stack,
504
+ }
505
+ : error;
506
+
507
+ const update: HMRUpdate = {
508
+ type: "error",
509
+ fileId: updateError.file || "unknown",
510
+ timestamp: Date.now(),
511
+ changes: [],
512
+ error: updateError,
513
+ };
514
+
515
+ this.broadcastUpdate(update);
516
+ }
517
+
518
+ /**
519
+ * Generate a unique client ID
520
+ */
521
+ private generateClientId(): string {
522
+ return `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
523
+ }
524
+
525
+ /**
526
+ * Generate a file ID from a file path
527
+ */
528
+ private generateFileId(filePath: string): string {
529
+ // Normalize the file path for consistent IDs
530
+ return filePath.replace(/\\/g, "/");
531
+ }
532
+
533
+ /**
534
+ * Get the number of connected clients
535
+ */
536
+ getClientCount(): number {
537
+ return this.clients.size;
538
+ }
539
+
540
+ /**
541
+ * Get all connected client IDs
542
+ */
543
+ getClientIds(): string[] {
544
+ return Array.from(this.clients.keys());
545
+ }
546
+
547
+ /**
548
+ * Disconnect all clients
549
+ */
550
+ disconnectAll(): void {
551
+ for (const client of this.clients.values()) {
552
+ if (client.ws.readyState === WebSocket.OPEN) {
553
+ client.ws.close();
554
+ }
555
+ }
556
+
557
+ this.clients.clear();
558
+ this.logger.info("All HMR clients disconnected");
559
+ }
560
+
561
+ /**
562
+ * Stop the HMR manager
563
+ */
564
+ stop(): void {
565
+ this.stopWatching();
566
+ this.disconnectAll();
567
+ this.dependencyGraph.clear();
568
+ this.pendingUpdates.clear();
569
+
570
+ if (this.debounceTimer) {
571
+ clearTimeout(this.debounceTimer);
572
+ this.debounceTimer = null;
573
+ }
574
+
575
+ this.logger.info("HMR manager stopped");
576
+ }
577
+
578
+ /**
579
+ * Get framework-specific HMR runtime code
580
+ */
581
+ getFrameworkRuntime(): string {
582
+ switch (this.framework) {
583
+ case "react":
584
+ return this.getReactRuntime();
585
+ case "vue":
586
+ return this.getVueRuntime();
587
+ case "svelte":
588
+ return this.getSvelteRuntime();
589
+ case "solid":
590
+ return this.getSolidRuntime();
591
+ default:
592
+ return "";
593
+ }
594
+ }
595
+
596
+ /**
597
+ * React Fast Refresh runtime
598
+ */
599
+ private getReactRuntime(): string {
600
+ return `
601
+ // React Fast Refresh Runtime
602
+ (function() {
603
+ if (typeof window !== 'undefined') {
604
+ window.__HMR_REACT_REFRESH__ = true;
605
+ }
606
+ })();
607
+ `;
608
+ }
609
+
610
+ /**
611
+ * Vue HMR runtime
612
+ */
613
+ private getVueRuntime(): string {
614
+ return `
615
+ // Vue HMR Runtime
616
+ (function() {
617
+ if (typeof window !== 'undefined') {
618
+ window.__HMR_VUE__ = true;
619
+ }
620
+ })();
621
+ `;
622
+ }
623
+
624
+ /**
625
+ * Svelte HMR runtime
626
+ */
627
+ private getSvelteRuntime(): string {
628
+ return `
629
+ // Svelte HMR Runtime
630
+ (function() {
631
+ if (typeof window !== 'undefined') {
632
+ window.__HMR_SVELTE__ = true;
633
+ }
634
+ })();
635
+ `;
636
+ }
637
+
638
+ /**
639
+ * Solid HMR runtime
640
+ */
641
+ private getSolidRuntime(): string {
642
+ return `
643
+ // Solid HMR Runtime
644
+ (function() {
645
+ if (typeof window !== 'undefined') {
646
+ window.__HMR_SOLID__ = true;
647
+ }
648
+ })();
649
+ `;
650
+ }
651
+ }
652
+
653
+ // ============= Factory Function =============
654
+
655
+ /**
656
+ * Create an HMR manager
657
+ */
658
+ export function createHMRManager(
659
+ framework: FrontendFramework,
660
+ devServerPort: number,
661
+ config?: Partial<HMRConfig>
662
+ ): HMRManager {
663
+ return new HMRManager(framework, devServerPort, config);
664
+ }
665
+
666
+ // ============= Utility Functions =============
667
+
668
+ /**
669
+ * Check if a file is an HMR boundary
670
+ * (a file that can accept hot updates without propagating to parents)
671
+ */
672
+ export function isHMRBoundary(filePath: string, content: string): boolean {
673
+ // Check for HMR acceptance patterns
674
+ const patterns = [
675
+ /module\.hot\.accept/,
676
+ /import\.meta\.hot\.accept/,
677
+ /if\s*\(\s*import\.meta\.hot\s*\)/,
678
+ ];
679
+
680
+ return patterns.some((pattern) => pattern.test(content));
681
+ }
682
+
683
+ /**
684
+ * Parse imports from a file's content
685
+ */
686
+ export function parseImports(content: string, filePath: string): string[] {
687
+ const imports: string[] = [];
688
+
689
+ // Match ES6 imports
690
+ const es6Pattern = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
691
+ let match;
692
+
693
+ while ((match = es6Pattern.exec(content)) !== null) {
694
+ imports.push(resolveImport(match[1], filePath));
695
+ }
696
+
697
+ // Match dynamic imports
698
+ const dynamicPattern = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
699
+
700
+ while ((match = dynamicPattern.exec(content)) !== null) {
701
+ imports.push(resolveImport(match[1], filePath));
702
+ }
703
+
704
+ // Match CommonJS requires
705
+ const requirePattern = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
706
+
707
+ while ((match = requirePattern.exec(content)) !== null) {
708
+ imports.push(resolveImport(match[1], filePath));
709
+ }
710
+
711
+ return imports;
712
+ }
713
+
714
+ /**
715
+ * Resolve an import path relative to the importing file
716
+ */
717
+ function resolveImport(importPath: string, fromFile: string): string {
718
+ // Skip bare imports (node_modules)
719
+ if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
720
+ return importPath;
721
+ }
722
+
723
+ // Resolve relative paths
724
+ const dir = fromFile.substring(0, fromFile.lastIndexOf("/"));
725
+ const resolved = new URL(importPath, `file://${dir}/`).pathname;
726
+
727
+ return resolved;
728
+ }