@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.
- package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
- package/LICENSE +623 -0
- package/README.md +442 -0
- package/bin/africode.js +73 -0
- package/bin/africode.js.1758507140 +343 -0
- package/bin/cli.ts +83 -0
- package/bin/create-africode.js +158 -0
- package/bin/scaffold.ts +219 -0
- package/components/accordion.js +183 -0
- package/components/alert.js +131 -0
- package/components/auth.js +172 -0
- package/components/avatar.js +117 -0
- package/components/badge.js +104 -0
- package/components/base.d.ts +139 -0
- package/components/base.js +184 -0
- package/components/button.js +164 -0
- package/components/card.js +137 -0
- package/components/cultural-card.js +243 -0
- package/components/divider.js +83 -0
- package/components/dropdown.js +171 -0
- package/components/error-boundary.js +155 -0
- package/components/form.js +131 -0
- package/components/grid.js +273 -0
- package/components/hero.js +138 -0
- package/components/icon.js +36 -0
- package/components/index.js +57 -0
- package/components/input.js +256 -0
- package/components/kanga-card.js +185 -0
- package/components/language-switcher.js +108 -0
- package/components/loader.js +80 -0
- package/components/modal.js +262 -0
- package/components/motion.js +84 -0
- package/components/navbar.js +236 -0
- package/components/pattern-showcase.js +225 -0
- package/components/progress.js +134 -0
- package/components/react.js +111 -0
- package/components/section.js +54 -0
- package/components/select.js +322 -0
- package/components/sidebar.js +180 -0
- package/components/skeleton.js +85 -0
- package/components/table.js +181 -0
- package/components/tabs.js +202 -0
- package/components/theme-toggle.js +82 -0
- package/components/toast.js +139 -0
- package/components/tooltip.js +167 -0
- package/core/a2ui-schema-manager.js +344 -0
- package/core/a2ui.js +431 -0
- package/core/bun-runtime.js +799 -0
- package/core/cli/commands/add.js +23 -0
- package/core/cli/commands/audit.js +58 -0
- package/core/cli/commands/build.js +137 -0
- package/core/cli/commands/create-plugin.js +241 -0
- package/core/cli/commands/dev.js +228 -0
- package/core/cli/commands/lint.js +23 -0
- package/core/cli/commands/test.js +34 -0
- package/core/cli/migrator.js +71 -0
- package/core/cli/ui.js +46 -0
- package/core/compliance.js +628 -0
- package/core/config.js +263 -0
- package/core/db-advanced.js +481 -0
- package/core/db.js +284 -0
- package/core/enhanced-hmr.js +404 -0
- package/core/errors.js +222 -0
- package/core/file-router.js +290 -0
- package/core/heartbeat.js +64 -0
- package/core/hmr-client.js +204 -0
- package/core/hmr.js +196 -0
- package/core/html.d.ts +116 -0
- package/core/html.js +160 -0
- package/core/hydration.js +52 -0
- package/core/lipa-namba-journey.js +572 -0
- package/core/motion.js +106 -0
- package/core/nida-cig-middleware.js +455 -0
- package/core/patterns.d.ts +124 -0
- package/core/patterns.js +833 -0
- package/core/plugins/index.js +312 -0
- package/core/router.js +387 -0
- package/core/sdk-client.js +62 -0
- package/core/sdk.d.ts +133 -0
- package/core/sdk.js +123 -0
- package/core/seo.js +76 -0
- package/core/server/auth-endpoints.js +339 -0
- package/core/server/auth.js +180 -0
- package/core/server/csrf.js +206 -0
- package/core/server/db.js +39 -0
- package/core/server/middleware.js +324 -0
- package/core/server/rate-limit.js +238 -0
- package/core/server/render.js +69 -0
- package/core/server/router.js +120 -0
- package/core/shim.js +28 -0
- package/core/state.d.ts +86 -0
- package/core/state.js +242 -0
- package/core/store.d.ts +122 -0
- package/core/store.js +61 -0
- package/core/validation.d.ts +233 -0
- package/core/validation.js +590 -0
- package/core/websocket.js +639 -0
- package/dist/africode.js +2905 -0
- package/dist/africode.js.map +61 -0
- package/dist/build-info.json +23 -0
- package/dist/components.js +2888 -0
- package/dist/components.js.map +58 -0
- package/dist/styles/africanity.css +322 -0
- package/dist/styles/typography.css +141 -0
- package/docs/IDE-Guide.md +50 -0
- package/package.json +110 -0
- package/src/index.ts +196 -0
- package/styles/africanity.css +322 -0
- package/styles/typography.css +141 -0
- package/templates/starter/.env.example +15 -0
- package/templates/starter/africode.config.js +40 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/pages/index.html +46 -0
- package/templates/starter/src/pages/index.js +32 -0
- package/templates/starter/src/styles/main.css +4 -0
- package/templates/starter-3d/.env.example +7 -0
- package/templates/starter-3d/africode.config.js +29 -0
- package/templates/starter-3d/components/af-model-viewer.js +125 -0
- package/templates/starter-3d/package.json +15 -0
- package/templates/starter-3d/src/pages/index.html +46 -0
- package/templates/starter-3d/src/pages/index.js +50 -0
- package/templates/starter-3d/src/styles/main.css +4 -0
- package/templates/starter-react/.env.example +15 -0
- package/templates/starter-react/africode.config.js +40 -0
- package/templates/starter-react/package.json +16 -0
- package/templates/starter-react/src/pages/index.html +46 -0
- package/templates/starter-react/src/pages/index.js +68 -0
- package/templates/starter-react/src/styles/main.css +4 -0
- package/templates/starter-tailwind/.env.example +15 -0
- package/templates/starter-tailwind/africode.config.js +40 -0
- package/templates/starter-tailwind/package.json +20 -0
- package/templates/starter-tailwind/src/pages/index.html +46 -0
- package/templates/starter-tailwind/src/pages/index.js +37 -0
- package/templates/starter-tailwind/src/styles/main.css +4 -0
- package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
- 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;
|