@africode/core 5.0.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 (136) hide show
  1. package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
  2. package/LICENSE +623 -0
  3. package/README.md +442 -0
  4. package/bin/africode.js +73 -0
  5. package/bin/africode.js.1758507140 +343 -0
  6. package/bin/cli.ts +83 -0
  7. package/bin/create-africode.js +158 -0
  8. package/bin/scaffold.ts +219 -0
  9. package/components/accordion.js +183 -0
  10. package/components/alert.js +131 -0
  11. package/components/auth.js +172 -0
  12. package/components/avatar.js +117 -0
  13. package/components/badge.js +104 -0
  14. package/components/base.d.ts +139 -0
  15. package/components/base.js +184 -0
  16. package/components/button.js +164 -0
  17. package/components/card.js +137 -0
  18. package/components/cultural-card.js +243 -0
  19. package/components/divider.js +83 -0
  20. package/components/dropdown.js +171 -0
  21. package/components/error-boundary.js +155 -0
  22. package/components/form.js +131 -0
  23. package/components/grid.js +273 -0
  24. package/components/hero.js +138 -0
  25. package/components/icon.js +36 -0
  26. package/components/index.js +57 -0
  27. package/components/input.js +256 -0
  28. package/components/kanga-card.js +185 -0
  29. package/components/language-switcher.js +108 -0
  30. package/components/loader.js +80 -0
  31. package/components/modal.js +262 -0
  32. package/components/motion.js +84 -0
  33. package/components/navbar.js +236 -0
  34. package/components/pattern-showcase.js +225 -0
  35. package/components/progress.js +134 -0
  36. package/components/react.js +111 -0
  37. package/components/section.js +54 -0
  38. package/components/select.js +322 -0
  39. package/components/sidebar.js +180 -0
  40. package/components/skeleton.js +85 -0
  41. package/components/table.js +181 -0
  42. package/components/tabs.js +202 -0
  43. package/components/theme-toggle.js +82 -0
  44. package/components/toast.js +139 -0
  45. package/components/tooltip.js +167 -0
  46. package/core/a2ui-schema-manager.js +344 -0
  47. package/core/a2ui.js +431 -0
  48. package/core/bun-runtime.js +799 -0
  49. package/core/cli/commands/add.js +23 -0
  50. package/core/cli/commands/audit.js +58 -0
  51. package/core/cli/commands/build.js +137 -0
  52. package/core/cli/commands/create-plugin.js +241 -0
  53. package/core/cli/commands/dev.js +228 -0
  54. package/core/cli/commands/lint.js +23 -0
  55. package/core/cli/commands/test.js +34 -0
  56. package/core/cli/migrator.js +71 -0
  57. package/core/cli/ui.js +46 -0
  58. package/core/compliance.js +628 -0
  59. package/core/config.js +263 -0
  60. package/core/db-advanced.js +481 -0
  61. package/core/db.js +284 -0
  62. package/core/enhanced-hmr.js +404 -0
  63. package/core/errors.js +222 -0
  64. package/core/file-router.js +290 -0
  65. package/core/heartbeat.js +64 -0
  66. package/core/hmr-client.js +204 -0
  67. package/core/hmr.js +196 -0
  68. package/core/html.d.ts +116 -0
  69. package/core/html.js +160 -0
  70. package/core/hydration.js +52 -0
  71. package/core/lipa-namba-journey.js +572 -0
  72. package/core/motion.js +106 -0
  73. package/core/nida-cig-middleware.js +455 -0
  74. package/core/patterns.d.ts +124 -0
  75. package/core/patterns.js +833 -0
  76. package/core/plugins/index.js +312 -0
  77. package/core/router.js +387 -0
  78. package/core/sdk-client.js +62 -0
  79. package/core/sdk.d.ts +133 -0
  80. package/core/sdk.js +123 -0
  81. package/core/seo.js +76 -0
  82. package/core/server/auth-endpoints.js +339 -0
  83. package/core/server/auth.js +180 -0
  84. package/core/server/csrf.js +206 -0
  85. package/core/server/db.js +39 -0
  86. package/core/server/middleware.js +324 -0
  87. package/core/server/rate-limit.js +238 -0
  88. package/core/server/render.js +69 -0
  89. package/core/server/router.js +120 -0
  90. package/core/shim.js +28 -0
  91. package/core/state.d.ts +86 -0
  92. package/core/state.js +242 -0
  93. package/core/store.d.ts +122 -0
  94. package/core/store.js +61 -0
  95. package/core/validation.d.ts +233 -0
  96. package/core/validation.js +590 -0
  97. package/core/websocket.js +639 -0
  98. package/dist/africode.js +2905 -0
  99. package/dist/africode.js.map +61 -0
  100. package/dist/build-info.json +23 -0
  101. package/dist/components.js +2888 -0
  102. package/dist/components.js.map +58 -0
  103. package/dist/styles/africanity.css +322 -0
  104. package/dist/styles/typography.css +141 -0
  105. package/docs/IDE-Guide.md +50 -0
  106. package/package.json +110 -0
  107. package/src/index.ts +196 -0
  108. package/styles/africanity.css +322 -0
  109. package/styles/typography.css +141 -0
  110. package/templates/starter/.env.example +15 -0
  111. package/templates/starter/africode.config.js +40 -0
  112. package/templates/starter/package.json +14 -0
  113. package/templates/starter/src/pages/index.html +46 -0
  114. package/templates/starter/src/pages/index.js +32 -0
  115. package/templates/starter/src/styles/main.css +4 -0
  116. package/templates/starter-3d/.env.example +7 -0
  117. package/templates/starter-3d/africode.config.js +29 -0
  118. package/templates/starter-3d/components/af-model-viewer.js +125 -0
  119. package/templates/starter-3d/package.json +15 -0
  120. package/templates/starter-3d/src/pages/index.html +46 -0
  121. package/templates/starter-3d/src/pages/index.js +50 -0
  122. package/templates/starter-3d/src/styles/main.css +4 -0
  123. package/templates/starter-react/.env.example +15 -0
  124. package/templates/starter-react/africode.config.js +40 -0
  125. package/templates/starter-react/package.json +16 -0
  126. package/templates/starter-react/src/pages/index.html +46 -0
  127. package/templates/starter-react/src/pages/index.js +68 -0
  128. package/templates/starter-react/src/styles/main.css +4 -0
  129. package/templates/starter-tailwind/.env.example +15 -0
  130. package/templates/starter-tailwind/africode.config.js +40 -0
  131. package/templates/starter-tailwind/package.json +20 -0
  132. package/templates/starter-tailwind/src/pages/index.html +46 -0
  133. package/templates/starter-tailwind/src/pages/index.js +37 -0
  134. package/templates/starter-tailwind/src/styles/main.css +4 -0
  135. package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
  136. package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
package/core/db.js ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * AfriCode Database Engine
3
+ * Type-safe SQLite ORM with migrations and schema management
4
+ *
5
+ * URL fields (avatar_url, repository_url, demo_url) are validated strictly:
6
+ * - Valid URL → accepted
7
+ * - null → accepted (nullable columns)
8
+ * - "" (empty string) → rejected with error
9
+ */
10
+ import { Database } from "bun:sqlite";
11
+ import { Validation, schemas } from './validation.js';
12
+ import { InvalidUrlError } from './errors.js';
13
+ import { emitDatabaseError } from './config.js';
14
+
15
+ // Automatically creates/opens 'afriCode.db' in the project root
16
+ const dbUrl = new URL('../afriCode.db', import.meta.url);
17
+ const isWindows = typeof process !== 'undefined' && process.platform === 'win32';
18
+ const dbPath = decodeURIComponent(isWindows ? dbUrl.pathname.replace(/^\//, '') : dbUrl.pathname);
19
+ const db = new Database(dbPath, { create: true });
20
+
21
+ // Enable WAL mode for better concurrency
22
+ db.query("PRAGMA journal_mode = WAL;").run();
23
+
24
+ // Initialize schema
25
+ initSchema();
26
+
27
+ function initSchema() {
28
+ // Users table
29
+ db.query(`
30
+ CREATE TABLE IF NOT EXISTS users (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ email TEXT UNIQUE NOT NULL,
33
+ username TEXT UNIQUE NOT NULL,
34
+ password_hash TEXT NOT NULL,
35
+ full_name TEXT,
36
+ avatar_url TEXT,
37
+ bio TEXT,
38
+ theme TEXT DEFAULT 'africanity',
39
+ language TEXT DEFAULT 'en',
40
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
41
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
42
+ )
43
+ `).run();
44
+
45
+ // Projects table
46
+ db.query(`
47
+ CREATE TABLE IF NOT EXISTS projects (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ name TEXT NOT NULL,
50
+ description TEXT,
51
+ owner_id INTEGER NOT NULL,
52
+ is_public BOOLEAN DEFAULT 0,
53
+ repository_url TEXT,
54
+ demo_url TEXT,
55
+ tags TEXT, -- JSON array
56
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
57
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
58
+ FOREIGN KEY (owner_id) REFERENCES users(id)
59
+ )
60
+ `).run();
61
+
62
+ // Components table for storing custom components
63
+ db.query(`
64
+ CREATE TABLE IF NOT EXISTS components (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ name TEXT UNIQUE NOT NULL,
67
+ code TEXT NOT NULL,
68
+ author_id INTEGER NOT NULL,
69
+ category TEXT,
70
+ tags TEXT, -- JSON array
71
+ is_public BOOLEAN DEFAULT 0,
72
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
73
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
74
+ FOREIGN KEY (author_id) REFERENCES users(id)
75
+ )
76
+ `).run();
77
+
78
+ // Sessions table for auth
79
+ db.query(`
80
+ CREATE TABLE IF NOT EXISTS sessions (
81
+ id TEXT PRIMARY KEY,
82
+ user_id INTEGER NOT NULL,
83
+ expires_at DATETIME NOT NULL,
84
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
85
+ FOREIGN KEY (user_id) REFERENCES users(id)
86
+ )
87
+ `).run();
88
+ }
89
+
90
+ /**
91
+ * Validate URL fields strictly.
92
+ * Rejects empty strings, accepts valid URLs and null.
93
+ * Uses standardized InvalidUrlError for consistent error format.
94
+ *
95
+ * @param {Object} data - The data object to validate
96
+ * @param {string[]} urlFields - Field names that should be validated as URLs
97
+ * @throws {InvalidUrlError} If any URL field contains an empty string or invalid URL
98
+ */
99
+ function validateUrlFields(data, urlFields) {
100
+ for (const field of urlFields) {
101
+ if (!(field in data)) {continue;}
102
+
103
+ const value = data[field];
104
+
105
+ // null is allowed (nullable columns)
106
+ if (value === null || value === undefined) {continue;}
107
+
108
+ // Empty string is strictly rejected
109
+ if (value === '') {
110
+ throw new InvalidUrlError(field, value);
111
+ }
112
+
113
+ // Must be a valid URL
114
+ try {
115
+ new URL(value);
116
+ } catch {
117
+ throw new InvalidUrlError(field, value);
118
+ }
119
+ }
120
+ }
121
+
122
+ /**
123
+ * User Model Operations
124
+ */
125
+ export const User = {
126
+ create: (userData) => {
127
+ const { email, username, password_hash, full_name } = userData;
128
+ const result = run(`
129
+ INSERT INTO users (email, username, password_hash, full_name)
130
+ VALUES (?, ?, ?, ?)
131
+ `, [email, username, password_hash, full_name]);
132
+ return result.lastInsertRowid;
133
+ },
134
+
135
+ findById: (id) => get('SELECT * FROM users WHERE id = ?', [id]),
136
+
137
+ findByEmail: (email) => get('SELECT * FROM users WHERE email = ?', [email]),
138
+
139
+ findByUsername: (username) => get('SELECT * FROM users WHERE username = ?', [username]),
140
+
141
+ update: (id, updates) => {
142
+ // Validate URL fields strictly: no empty strings, only valid URLs or null
143
+ validateUrlFields(updates, ['avatar_url']);
144
+
145
+ const fields = Object.keys(updates);
146
+ const values = Object.values(updates);
147
+ const setClause = fields.map(field => `${field} = ?`).join(', ');
148
+ const sql = `UPDATE users SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`;
149
+ try {
150
+ console.log('DB User.update:', { sql, params: [...values, id] });
151
+ return run(sql, [...values, id]);
152
+ } catch (err) {
153
+ console.error('DB User.update error:', err, { sql, params: [...values, id] });
154
+ throw err;
155
+ }
156
+ }
157
+ };
158
+
159
+ /**
160
+ * Project Model Operations
161
+ */
162
+ export const Project = {
163
+ create: (projectData) => {
164
+ // Validate URL fields strictly: no empty strings, only valid URLs or null
165
+ validateUrlFields(projectData, ['repository_url', 'demo_url']);
166
+
167
+ const { name, description, owner_id, repository_url, demo_url, tags } = projectData;
168
+ const result = run(`
169
+ INSERT INTO projects (name, description, owner_id, repository_url, demo_url, tags)
170
+ VALUES (?, ?, ?, ?, ?, ?)
171
+ `, [name, description, owner_id, repository_url || null, demo_url || null, JSON.stringify(tags || [])]);
172
+ return result.lastInsertRowid;
173
+ },
174
+
175
+ findByUser: (userId, limit = 50) => query(`
176
+ SELECT p.*, u.username as owner_username
177
+ FROM projects p
178
+ JOIN users u ON p.owner_id = u.id
179
+ WHERE p.owner_id = ? OR p.is_public = 1
180
+ ORDER BY p.created_at DESC
181
+ LIMIT ?
182
+ `, [userId, limit]),
183
+
184
+ findById: (id) => get(`
185
+ SELECT p.*, u.username as owner_username
186
+ FROM projects p
187
+ JOIN users u ON p.owner_id = u.id
188
+ WHERE p.id = ?
189
+ `, [id])
190
+ };
191
+
192
+ /**
193
+ * Component Model Operations
194
+ */
195
+ export const Component = {
196
+ create: (componentData) => {
197
+ const { name, code, author_id, category, tags } = componentData;
198
+ const result = run(`
199
+ INSERT INTO components (name, code, author_id, category, tags)
200
+ VALUES (?, ?, ?, ?, ?)
201
+ `, [name, code, author_id, category, JSON.stringify(tags || [])]);
202
+ return result.lastInsertRowid;
203
+ },
204
+
205
+ findPublic: (limit = 100) => query(`
206
+ SELECT c.*, u.username as author_username
207
+ FROM components c
208
+ JOIN users u ON c.author_id = u.id
209
+ WHERE c.is_public = 1
210
+ ORDER BY c.created_at DESC
211
+ LIMIT ?
212
+ `, [limit]),
213
+
214
+ findById: (id) => get(`
215
+ SELECT c.*, u.username as author_username
216
+ FROM components c
217
+ JOIN users u ON c.author_id = u.id
218
+ WHERE c.id = ?
219
+ `, [id])
220
+ };
221
+
222
+ /**
223
+ * Session Model Operations
224
+ */
225
+ export const Session = {
226
+ create: (sessionData) => {
227
+ const { id, user_id, expires_at } = sessionData;
228
+ return run(`
229
+ INSERT INTO sessions (id, user_id, expires_at)
230
+ VALUES (?, ?, ?)
231
+ `, [id, user_id, expires_at]);
232
+ },
233
+
234
+ findById: (id) => get(`
235
+ SELECT s.*, u.username, u.email
236
+ FROM sessions s
237
+ JOIN users u ON s.user_id = u.id
238
+ WHERE s.id = ? AND s.expires_at > CURRENT_TIMESTAMP
239
+ `, [id]),
240
+
241
+ update: (id, updates) => {
242
+ const fields = Object.keys(updates);
243
+ const values = Object.values(updates);
244
+ const setClause = fields.map(field => `${field} = ?`).join(', ');
245
+ const sql = `UPDATE sessions SET ${setClause} WHERE id = ?`;
246
+ return run(sql, [...values, id]);
247
+ },
248
+
249
+ delete: (id) => run('DELETE FROM sessions WHERE id = ?', [id]),
250
+
251
+ cleanup: () => run('DELETE FROM sessions WHERE expires_at <= CURRENT_TIMESTAMP')
252
+ };
253
+
254
+ /**
255
+ * Execute a query and return all results
256
+ * @param {string} sql - SQL query
257
+ * @param {Array} params - Binding parameters
258
+ * @returns {Array} Array of row objects
259
+ */
260
+ export function query(sql, params = []) {
261
+ return db.query(sql).all(...params);
262
+ }
263
+
264
+ /**
265
+ * Execute a query and return a single result
266
+ * @param {string} sql - SQL query
267
+ * @param {Array} params - Binding parameters
268
+ * @returns {Object} Single row object
269
+ */
270
+ export function get(sql, params = []) {
271
+ return db.query(sql).get(...params);
272
+ }
273
+
274
+ /**
275
+ * Run a command (INSERT, UPDATE, DELETE)
276
+ * @param {string} sql - SQL command
277
+ * @param {Array} params - Binding parameters
278
+ * @returns {Object} Result info (changes, lastInsertRowid)
279
+ */
280
+ export function run(sql, params = []) {
281
+ return db.query(sql).run(...params);
282
+ }
283
+
284
+ export default { query, get, run, native: db, User, Project, Component, Session };
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Enhanced HMR Middleware for AfriCode v5.0.0
3
+ * Zero-downtime hot reload with session preservation
4
+ *
5
+ * Features:
6
+ * - WebSocket-based file watching
7
+ * - Graceful module replacement
8
+ * - Database connection persistence
9
+ * - Session state retention
10
+ * - Soft reloads without full restart
11
+ */
12
+
13
+ import { watch } from 'node:fs';
14
+ import { join, extname, dirname } from 'node:path';
15
+
16
+ export class EnhancedHMRMiddleware {
17
+ constructor(options = {}) {
18
+ this.options = {
19
+ port: options.port || 3001,
20
+ watchPaths: options.watchPaths || ['pages', 'components', 'core', 'styles'],
21
+ ignorePatterns: options.ignorePatterns || [
22
+ 'node_modules',
23
+ '.git',
24
+ 'dist',
25
+ '.bun',
26
+ 'build',
27
+ 'coverage',
28
+ 'test-results',
29
+ '*.test.js',
30
+ '*.spec.js'
31
+ ],
32
+ debounceMs: options.debounceMs || 100,
33
+ ...options
34
+ };
35
+
36
+ this.clients = new Set();
37
+ this.watchers = new Map();
38
+ this.fileChanges = new Map();
39
+ this.sessions = new Map(); // Store session data during reloads
40
+ this.dbConnections = new Map(); // Preserve database connections
41
+ }
42
+
43
+ /**
44
+ * Initialize HMR server with Bun.serve integration
45
+ */
46
+ async initialize(devServer) {
47
+ console.log(`[HMR] Initializing enhanced hot reload on port ${this.options.port}`);
48
+
49
+ // Setup WebSocket upgrade handler
50
+ devServer.upgrade({
51
+ open: (ws) => this.handleClientConnect(ws),
52
+ message: (ws, message) => this.handleClientMessage(ws, message),
53
+ close: (ws) => this.handleClientDisconnect(ws)
54
+ });
55
+
56
+ // Start file watching
57
+ await this.startFileWatching();
58
+
59
+ console.log(`[HMR] Watching ${this.options.watchPaths.length} paths for changes...`);
60
+ }
61
+
62
+ /**
63
+ * Handle new client connection
64
+ */
65
+ handleClientConnect(ws) {
66
+ console.log(`[HMR] Client connected from ${ws.remoteAddress}`);
67
+ this.clients.add(ws);
68
+
69
+ // Send welcome message with current state
70
+ ws.send(JSON.stringify({
71
+ type: 'welcome',
72
+ timestamp: Date.now(),
73
+ sessionId: this.generateSessionId()
74
+ }));
75
+ }
76
+
77
+ /**
78
+ * Handle client messages (ping, session data, etc.)
79
+ */
80
+ handleClientMessage(ws, message) {
81
+ try {
82
+ const data = JSON.parse(message);
83
+
84
+ switch (data.type) {
85
+ case 'ping':
86
+ ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
87
+ break;
88
+
89
+ case 'session-data':
90
+ // Store session data for preservation during reloads
91
+ this.sessions.set(data.sessionId, data.data);
92
+ break;
93
+
94
+ case 'db-connection':
95
+ // Preserve database connection state
96
+ this.dbConnections.set(data.connectionId, data.state);
97
+ break;
98
+
99
+ default:
100
+ console.log(`[HMR] Unknown message type: ${data.type}`);
101
+ }
102
+ } catch (error) {
103
+ console.error('[HMR] Error parsing client message:', error);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Handle client disconnection
109
+ */
110
+ handleClientDisconnect(ws) {
111
+ console.log('[HMR] Client disconnected');
112
+ this.clients.delete(ws);
113
+ }
114
+
115
+ /**
116
+ * Start watching files for changes
117
+ */
118
+ async startFileWatching() {
119
+ const watchPromises = this.options.watchPaths.map(path =>
120
+ this.watchDirectory(path)
121
+ );
122
+
123
+ await Promise.all(watchPromises);
124
+ }
125
+
126
+ /**
127
+ * Watch a directory recursively
128
+ */
129
+ async watchDirectory(dirPath) {
130
+ try {
131
+ const watcher = watch(dirPath, { recursive: true }, (eventType, filename) => {
132
+ if (!filename) return;
133
+
134
+ const fullPath = join(dirPath, filename);
135
+
136
+ // Check ignore patterns
137
+ if (this.shouldIgnoreFile(fullPath)) return;
138
+
139
+ // Debounce rapid changes
140
+ const changeKey = fullPath;
141
+ if (this.fileChanges.has(changeKey)) {
142
+ clearTimeout(this.fileChanges.get(changeKey));
143
+ }
144
+
145
+ const timeoutId = setTimeout(() => {
146
+ this.handleFileChange(filename, fullPath);
147
+ this.fileChanges.delete(changeKey);
148
+ }, this.options.debounceMs);
149
+
150
+ this.fileChanges.set(changeKey, timeoutId);
151
+ });
152
+
153
+ this.watchers.set(dirPath, watcher);
154
+ console.log(`[HMR] Watching directory: ${dirPath}`);
155
+
156
+ } catch (error) {
157
+ console.error(`[HMR] Failed to watch ${dirPath}:`, error.message);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Check if file should be ignored
163
+ */
164
+ shouldIgnoreFile(filePath) {
165
+ const normalizedPath = filePath.replace(/\\/g, '/');
166
+
167
+ return this.options.ignorePatterns.some(pattern => {
168
+ if (pattern.includes('*')) {
169
+ // Simple glob matching
170
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
171
+ return regex.test(normalizedPath);
172
+ }
173
+ return normalizedPath.includes(pattern);
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Handle file change and determine reload strategy
179
+ */
180
+ handleFileChange(filename, fullPath) {
181
+ const ext = extname(filename);
182
+ const relativePath = this.getRelativePath(fullPath);
183
+
184
+ console.log(`[HMR] File changed: ${relativePath}`);
185
+
186
+ // Determine change type and reload strategy
187
+ const changeType = this.classifyChange(filename, ext);
188
+
189
+ switch (changeType) {
190
+ case 'page':
191
+ this.handlePageChange(relativePath);
192
+ break;
193
+
194
+ case 'component':
195
+ this.handleComponentChange(relativePath);
196
+ break;
197
+
198
+ case 'core':
199
+ this.handleCoreChange(relativePath);
200
+ break;
201
+
202
+ case 'style':
203
+ this.handleStyleChange(relativePath);
204
+ break;
205
+
206
+ case 'config':
207
+ this.handleConfigChange(relativePath);
208
+ break;
209
+
210
+ default:
211
+ this.handleGenericChange(relativePath);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Classify the type of file change
217
+ */
218
+ classifyChange(filename, ext) {
219
+ if (filename.includes('pages/')) {
220
+ return ext === '.html' ? 'page' : 'api';
221
+ }
222
+
223
+ if (filename.includes('components/')) {
224
+ return 'component';
225
+ }
226
+
227
+ if (filename.includes('core/')) {
228
+ return 'core';
229
+ }
230
+
231
+ if (ext === '.css') {
232
+ return 'style';
233
+ }
234
+
235
+ if (filename.includes('config') || filename.includes('bunfig')) {
236
+ return 'config';
237
+ }
238
+
239
+ return 'generic';
240
+ }
241
+
242
+ /**
243
+ * Handle page changes (soft reload)
244
+ */
245
+ handlePageChange(relativePath) {
246
+ this.broadcast({
247
+ type: 'page-update',
248
+ path: relativePath,
249
+ strategy: 'soft-reload',
250
+ preserveSession: true,
251
+ timestamp: Date.now()
252
+ });
253
+ }
254
+
255
+ /**
256
+ * Handle component changes (hot swap)
257
+ */
258
+ handleComponentChange(relativePath) {
259
+ this.broadcast({
260
+ type: 'component-update',
261
+ path: relativePath,
262
+ strategy: 'hot-swap',
263
+ componentName: this.extractComponentName(relativePath),
264
+ timestamp: Date.now()
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Handle core module changes (full reload required)
270
+ */
271
+ handleCoreChange(relativePath) {
272
+ console.log(`[HMR] Core module changed, triggering full reload: ${relativePath}`);
273
+
274
+ this.broadcast({
275
+ type: 'core-update',
276
+ path: relativePath,
277
+ strategy: 'full-reload',
278
+ preserveSession: true,
279
+ preserveConnections: true,
280
+ timestamp: Date.now()
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Handle style changes (CSS injection)
286
+ */
287
+ handleStyleChange(relativePath) {
288
+ this.broadcast({
289
+ type: 'style-update',
290
+ path: relativePath,
291
+ strategy: 'css-injection',
292
+ timestamp: Date.now()
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Handle config changes (server restart)
298
+ */
299
+ handleConfigChange(relativePath) {
300
+ console.log(`[HMR] Config changed, server restart required: ${relativePath}`);
301
+
302
+ this.broadcast({
303
+ type: 'config-update',
304
+ path: relativePath,
305
+ strategy: 'server-restart',
306
+ timestamp: Date.now()
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Handle generic file changes
312
+ */
313
+ handleGenericChange(relativePath) {
314
+ this.broadcast({
315
+ type: 'file-update',
316
+ path: relativePath,
317
+ strategy: 'soft-reload',
318
+ timestamp: Date.now()
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Broadcast message to all connected clients
324
+ */
325
+ broadcast(message) {
326
+ const messageStr = JSON.stringify(message);
327
+
328
+ for (const client of this.clients) {
329
+ try {
330
+ client.send(messageStr);
331
+ } catch (error) {
332
+ console.error('[HMR] Failed to send to client:', error);
333
+ this.clients.delete(client);
334
+ }
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Extract component name from path
340
+ */
341
+ extractComponentName(path) {
342
+ const match = path.match(/components\/([^/]+)\.js$/);
343
+ return match ? match[1] : 'unknown';
344
+ }
345
+
346
+ /**
347
+ * Get relative path from project root
348
+ */
349
+ getRelativePath(fullPath) {
350
+ return fullPath.replace(process.cwd() + '/', '');
351
+ }
352
+
353
+ /**
354
+ * Generate unique session ID
355
+ */
356
+ generateSessionId() {
357
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
358
+ }
359
+
360
+ /**
361
+ * Get preserved session data
362
+ */
363
+ getSessionData(sessionId) {
364
+ return this.sessions.get(sessionId) || {};
365
+ }
366
+
367
+ /**
368
+ * Get preserved database connection state
369
+ */
370
+ getConnectionState(connectionId) {
371
+ return this.dbConnections.get(connectionId) || {};
372
+ }
373
+
374
+ /**
375
+ * Cleanup watchers on shutdown
376
+ */
377
+ async shutdown() {
378
+ console.log('[HMR] Shutting down...');
379
+
380
+ // Close all watchers
381
+ for (const [path, watcher] of this.watchers) {
382
+ watcher.close();
383
+ }
384
+ this.watchers.clear();
385
+
386
+ // Close all client connections
387
+ for (const client of this.clients) {
388
+ try {
389
+ client.close();
390
+ } catch (error) {
391
+ // Ignore errors during shutdown
392
+ }
393
+ }
394
+ this.clients.clear();
395
+
396
+ // Clear session data
397
+ this.sessions.clear();
398
+ this.dbConnections.clear();
399
+
400
+ console.log('[HMR] Shutdown complete');
401
+ }
402
+ }
403
+
404
+ export default EnhancedHMRMiddleware;