@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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode HMR Client
|
|
3
|
+
*
|
|
4
|
+
* Injected into pages during development
|
|
5
|
+
* Handles module reloading and page updates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
(function hmrClient() {
|
|
9
|
+
const WS_URL = `ws://${window.location.hostname}:${window.location.port === '3000' ? '3000' : window.location.port}`;
|
|
10
|
+
let ws = null;
|
|
11
|
+
let reconnectAttempts = 0;
|
|
12
|
+
const MAX_RECONNECT = 10;
|
|
13
|
+
const RECONNECT_DELAY = 1000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Connect to HMR server via WebSocket
|
|
17
|
+
*/
|
|
18
|
+
function connect() {
|
|
19
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
ws = new WebSocket(WS_URL);
|
|
25
|
+
|
|
26
|
+
ws.onopen = () => {
|
|
27
|
+
console.log(`%c[HMR] Connected`, 'color: green; font-weight: bold;');
|
|
28
|
+
reconnectAttempts = 0;
|
|
29
|
+
sendPing();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
ws.onmessage = (event) => {
|
|
33
|
+
try {
|
|
34
|
+
const message = JSON.parse(event.data);
|
|
35
|
+
handleUpdate(message);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('[HMR] Failed to parse message:', err);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
ws.onerror = (error) => {
|
|
42
|
+
console.error('[HMR] WebSocket error:', error);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
ws.onclose = () => {
|
|
46
|
+
console.log(`%c[HMR] Disconnected`, 'color: orange; font-weight: bold;');
|
|
47
|
+
scheduleReconnect();
|
|
48
|
+
};
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error('[HMR] Connection failed:', err);
|
|
51
|
+
scheduleReconnect();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Schedule reconnection with exponential backoff
|
|
57
|
+
*/
|
|
58
|
+
function scheduleReconnect() {
|
|
59
|
+
if (reconnectAttempts >= MAX_RECONNECT) {
|
|
60
|
+
console.warn('[HMR] Max reconnection attempts reached. Giving up.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
reconnectAttempts++;
|
|
65
|
+
const delay = RECONNECT_DELAY * Math.pow(1.5, reconnectAttempts - 1);
|
|
66
|
+
console.log(`[HMR] Reconnecting in ${delay.toFixed(0)}ms... (attempt ${reconnectAttempts})`);
|
|
67
|
+
|
|
68
|
+
setTimeout(connect, delay);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Send ping to keep connection alive
|
|
73
|
+
*/
|
|
74
|
+
function sendPing() {
|
|
75
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
76
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
77
|
+
setTimeout(sendPing, 30000); // Ping every 30 seconds
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Handle update from HMR server
|
|
83
|
+
*/
|
|
84
|
+
function handleUpdate(message) {
|
|
85
|
+
if (message.type !== 'update') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { moduleType, modulePath, filename, timestamp } = message;
|
|
90
|
+
|
|
91
|
+
console.log(`%c[HMR] Update received: ${filename}`, 'color: #4CAF50; font-weight: bold;');
|
|
92
|
+
|
|
93
|
+
// Handle different module types
|
|
94
|
+
switch (moduleType) {
|
|
95
|
+
case 'page':
|
|
96
|
+
reloadPage();
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case 'style':
|
|
100
|
+
reloadStyles();
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case 'component':
|
|
104
|
+
reloadComponent(modulePath);
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'core':
|
|
108
|
+
// Core module changes require full reload
|
|
109
|
+
reloadPage();
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case 'markup':
|
|
113
|
+
reloadPage();
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
// Unknown type, do full reload to be safe
|
|
118
|
+
reloadPage();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reload all stylesheets
|
|
124
|
+
*/
|
|
125
|
+
function reloadStyles() {
|
|
126
|
+
console.log('%c[HMR] Reloading styles...', 'color: #2196F3;');
|
|
127
|
+
|
|
128
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
129
|
+
links.forEach(link => {
|
|
130
|
+
const href = link.href.split('?')[0];
|
|
131
|
+
link.href = href + '?t=' + Date.now();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Also reload inline styles if they reference external files
|
|
135
|
+
const styles = document.querySelectorAll('style');
|
|
136
|
+
styles.forEach(style => {
|
|
137
|
+
if (style.textContent.includes('@import')) {
|
|
138
|
+
style.textContent = style.textContent.replace(
|
|
139
|
+
/\?t=\d+/g,
|
|
140
|
+
`?t=${Date.now()}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log('%c[HMR] Styles reloaded ✓', 'color: green;');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reload a component by clearing its cache and refreshing
|
|
150
|
+
*/
|
|
151
|
+
function reloadComponent(componentPath) {
|
|
152
|
+
console.log(`%c[HMR] Reloading component: ${componentPath}`, 'color: #FF9800;');
|
|
153
|
+
|
|
154
|
+
// Find all instances of the component in the DOM
|
|
155
|
+
const componentName = componentPath.split('-')[0];
|
|
156
|
+
const componentElements = document.querySelectorAll(`[data-hmr-component="${componentPath}"]`);
|
|
157
|
+
|
|
158
|
+
if (componentElements.length === 0) {
|
|
159
|
+
console.log('[HMR] Component not found in DOM, doing full reload');
|
|
160
|
+
reloadPage();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// For Web Components, we need to reload the entire page
|
|
165
|
+
// because ES modules have singleton nature
|
|
166
|
+
reloadPage();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Perform full page reload
|
|
171
|
+
*/
|
|
172
|
+
function reloadPage() {
|
|
173
|
+
console.log('%c[HMR] Performing full page reload...', 'color: #FF5722; font-weight: bold;');
|
|
174
|
+
|
|
175
|
+
// Add query param to bust cache
|
|
176
|
+
const url = new URL(window.location);
|
|
177
|
+
url.searchParams.set('hmr', Date.now());
|
|
178
|
+
|
|
179
|
+
// Use replace instead of reload to avoid adding history entries
|
|
180
|
+
window.location.replace(url.toString());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Initialize HMR client
|
|
185
|
+
*/
|
|
186
|
+
function init() {
|
|
187
|
+
console.log('%c[AfriCode HMR Client] Initializing...', 'color: #673AB7; font-weight: bold;');
|
|
188
|
+
connect();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Initialize when DOM is ready
|
|
192
|
+
if (document.readyState === 'loading') {
|
|
193
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
194
|
+
} else {
|
|
195
|
+
init();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Cleanup on page unload
|
|
199
|
+
window.addEventListener('beforeunload', () => {
|
|
200
|
+
if (ws) {
|
|
201
|
+
ws.close();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
})();
|
package/core/hmr.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMR Server - Hot Module Replacement for AfriCode
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - WebSocket server for dev clients
|
|
6
|
+
* - File watcher for source changes
|
|
7
|
+
* - Module update broadcasting
|
|
8
|
+
* - Graceful reload handling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as nodePath from 'node:path';
|
|
12
|
+
import { watch } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
export class HMRServer {
|
|
15
|
+
constructor(port = 3001) {
|
|
16
|
+
this.port = port;
|
|
17
|
+
this.clients = new Set();
|
|
18
|
+
this.watchers = new Map();
|
|
19
|
+
this.fileChanges = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Start HMR WebSocket server
|
|
24
|
+
*/
|
|
25
|
+
start(devServer) {
|
|
26
|
+
const hmrPort = this.port;
|
|
27
|
+
|
|
28
|
+
// Upgrade HTTP connections to WebSocket
|
|
29
|
+
devServer.upgrade({
|
|
30
|
+
open(ws) {
|
|
31
|
+
console.log(`[HMR] Client connected from ${ws.remoteAddress}`);
|
|
32
|
+
this.clients.add(ws);
|
|
33
|
+
},
|
|
34
|
+
message(ws, message) {
|
|
35
|
+
// Handle ping/pong and client messages
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(message);
|
|
38
|
+
if (data.type === 'ping') {
|
|
39
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Invalid message, ignore
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
close(ws) {
|
|
46
|
+
this.clients.delete(ws);
|
|
47
|
+
console.log(`[HMR] Client disconnected`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Start file watching
|
|
52
|
+
this.watchDirectory(process.cwd());
|
|
53
|
+
console.log(`[HMR] Watching for changes...`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Watch directory for changes recursively
|
|
58
|
+
*/
|
|
59
|
+
watchDirectory(dir, prefix = '') {
|
|
60
|
+
const ignoreDirs = new Set([
|
|
61
|
+
'node_modules', '.git', 'dist', '.bun', 'build',
|
|
62
|
+
'.next', 'coverage', 'test-results'
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const watcher = watch(dir, { recursive: true }, (eventType, filename) => {
|
|
67
|
+
if (!filename) return;
|
|
68
|
+
|
|
69
|
+
const fullPath = nodePath.join(dir, filename);
|
|
70
|
+
const dirName = nodePath.dirname(filename);
|
|
71
|
+
|
|
72
|
+
// Ignore node_modules, build files, etc.
|
|
73
|
+
if (ignoreDirs.has(dirName.split(nodePath.sep)[0])) return;
|
|
74
|
+
if (filename.includes('node_modules')) return;
|
|
75
|
+
if (filename.endsWith('.test.js')) return;
|
|
76
|
+
|
|
77
|
+
// Only watch JS, JSX, TS, TSX, CSS, HTML
|
|
78
|
+
const validExts = ['.js', '.jsx', '.ts', '.tsx', '.css', '.html'];
|
|
79
|
+
if (!validExts.some(ext => filename.endsWith(ext))) return;
|
|
80
|
+
|
|
81
|
+
// Debounce rapid changes
|
|
82
|
+
const changeKey = fullPath;
|
|
83
|
+
if (this.fileChanges.has(changeKey)) {
|
|
84
|
+
clearTimeout(this.fileChanges.get(changeKey));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const timeoutId = setTimeout(() => {
|
|
88
|
+
this.handleFileChange(filename, fullPath);
|
|
89
|
+
this.fileChanges.delete(changeKey);
|
|
90
|
+
}, 100);
|
|
91
|
+
|
|
92
|
+
this.fileChanges.set(changeKey, timeoutId);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.watchers.set(dir, watcher);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`[HMR] Failed to watch ${dir}:`, err.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle file change - determine type and broadcast update
|
|
103
|
+
*/
|
|
104
|
+
handleFileChange(filename, fullPath) {
|
|
105
|
+
const ext = nodePath.extname(filename);
|
|
106
|
+
const relativePath = nodePath.relative(process.cwd(), fullPath);
|
|
107
|
+
const normalizedPath = relativePath.replace(/\\/g, '/'); // Normalize Windows paths
|
|
108
|
+
|
|
109
|
+
// Filter out ignored files
|
|
110
|
+
if (normalizedPath.includes('node_modules')) return;
|
|
111
|
+
if (normalizedPath.includes('.test.')) return;
|
|
112
|
+
if (normalizedPath.includes('test/')) return;
|
|
113
|
+
|
|
114
|
+
console.log(`[HMR] File changed: ${normalizedPath}`);
|
|
115
|
+
|
|
116
|
+
// Determine module type and extract clean path
|
|
117
|
+
let moduleType = 'unknown';
|
|
118
|
+
let modulePath = normalizedPath;
|
|
119
|
+
|
|
120
|
+
if (normalizedPath.includes('pages/') && (ext === '.js' || ext === '.html')) {
|
|
121
|
+
moduleType = 'page';
|
|
122
|
+
// Extract just the page name from the path
|
|
123
|
+
const match = normalizedPath.match(/pages\/([^/]+)/);
|
|
124
|
+
modulePath = match ? match[1].replace(ext, '') : normalizedPath;
|
|
125
|
+
} else if (normalizedPath.includes('components/') && ext === '.js') {
|
|
126
|
+
moduleType = 'component';
|
|
127
|
+
// Extract just the component name
|
|
128
|
+
const match = normalizedPath.match(/components\/([^/]+)/);
|
|
129
|
+
modulePath = match ? match[1].replace(ext, '') : normalizedPath;
|
|
130
|
+
} else if (normalizedPath.includes('core/') && ext === '.js') {
|
|
131
|
+
moduleType = 'core';
|
|
132
|
+
// Extract just the module name
|
|
133
|
+
const match = normalizedPath.match(/core\/([^/]+)/);
|
|
134
|
+
modulePath = match ? match[1].replace(ext, '') : normalizedPath;
|
|
135
|
+
} else if (ext === '.css') {
|
|
136
|
+
moduleType = 'style';
|
|
137
|
+
} else if (ext === '.html') {
|
|
138
|
+
moduleType = 'markup';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Broadcast change to all connected clients
|
|
142
|
+
this.broadcast({
|
|
143
|
+
type: 'update',
|
|
144
|
+
moduleType,
|
|
145
|
+
modulePath,
|
|
146
|
+
filename: normalizedPath,
|
|
147
|
+
timestamp: Date.now()
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Broadcast message to all connected clients
|
|
153
|
+
*/
|
|
154
|
+
broadcast(message) {
|
|
155
|
+
const payload = JSON.stringify(message);
|
|
156
|
+
let successCount = 0;
|
|
157
|
+
|
|
158
|
+
for (const client of this.clients) {
|
|
159
|
+
try {
|
|
160
|
+
client.send(payload);
|
|
161
|
+
successCount++;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
// Client disconnected, will be cleaned up on next event
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (successCount > 0) {
|
|
168
|
+
console.log(`[HMR] Broadcasted update to ${successCount} client(s)`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Stop all watchers and close WebSocket connections
|
|
174
|
+
*/
|
|
175
|
+
stop() {
|
|
176
|
+
for (const watcher of this.watchers.values()) {
|
|
177
|
+
try {
|
|
178
|
+
watcher.close();
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// Ignore close errors
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.watchers.clear();
|
|
184
|
+
this.clients.clear();
|
|
185
|
+
console.log(`[HMR] Server stopped`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create HMR server instance
|
|
191
|
+
*/
|
|
192
|
+
export function createHMRServer(port = 3001) {
|
|
193
|
+
return new HMRServer(port);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default HMRServer;
|
package/core/html.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode HTML Template Engine - TypeScript Definitions
|
|
3
|
+
* JSX-free template literal engine
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HTML template tag function
|
|
8
|
+
* Safely renders template literals as HTML strings
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```ts
|
|
12
|
+
* const name = 'Alice';
|
|
13
|
+
* const result = html`<h1>Hello ${name}</h1>`;
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param strings - Template string parts
|
|
17
|
+
* @param values - Interpolated values
|
|
18
|
+
* @returns Rendered HTML string
|
|
19
|
+
*/
|
|
20
|
+
export function html(
|
|
21
|
+
strings: TemplateStringsArray,
|
|
22
|
+
...values: any[]
|
|
23
|
+
): string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render options
|
|
27
|
+
*/
|
|
28
|
+
export interface RenderOptions {
|
|
29
|
+
/** Escape HTML special characters */
|
|
30
|
+
escape?: boolean;
|
|
31
|
+
/** Keep falsy values as string */
|
|
32
|
+
keepFalsy?: boolean;
|
|
33
|
+
/** Custom serializer */
|
|
34
|
+
serializer?: (value: any) => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Safe HTML rendering with custom options
|
|
39
|
+
*/
|
|
40
|
+
export function renderHtml(
|
|
41
|
+
strings: TemplateStringsArray,
|
|
42
|
+
values: any[],
|
|
43
|
+
options?: RenderOptions
|
|
44
|
+
): string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Layout component props
|
|
48
|
+
*/
|
|
49
|
+
export interface LayoutProps {
|
|
50
|
+
/** Use vertical layout (default: horizontal) */
|
|
51
|
+
vertical?: boolean;
|
|
52
|
+
/** Spacing between items (px) */
|
|
53
|
+
gap?: number;
|
|
54
|
+
/** Vertical alignment */
|
|
55
|
+
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
56
|
+
/** Horizontal justification */
|
|
57
|
+
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
|
|
58
|
+
/** Additional CSS classes */
|
|
59
|
+
class?: string;
|
|
60
|
+
/** Inline styles */
|
|
61
|
+
style?: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Layout component class
|
|
66
|
+
*/
|
|
67
|
+
export class Layout {
|
|
68
|
+
constructor(props?: LayoutProps);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Add child element or text
|
|
72
|
+
*/
|
|
73
|
+
addChild(child: HTMLElement | string): this;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Render to HTML element
|
|
77
|
+
*/
|
|
78
|
+
render(): HTMLElement;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get HTML string representation
|
|
82
|
+
*/
|
|
83
|
+
toString(): string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Flex layout shorthand
|
|
88
|
+
*/
|
|
89
|
+
export function flex(
|
|
90
|
+
options?: LayoutProps
|
|
91
|
+
): HTMLElement;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Grid layout shorthand
|
|
95
|
+
*/
|
|
96
|
+
export function grid(
|
|
97
|
+
columnCount: number,
|
|
98
|
+
gap?: number
|
|
99
|
+
): HTMLElement;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create safe text node
|
|
103
|
+
*/
|
|
104
|
+
export function text(content: string): string;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Escape HTML entities
|
|
108
|
+
*/
|
|
109
|
+
export function escape(str: string): string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Unescape HTML entities
|
|
113
|
+
*/
|
|
114
|
+
export function unescape(str: string): string;
|
|
115
|
+
|
|
116
|
+
export default html;
|
package/core/html.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode HTML Template Engine
|
|
3
|
+
*
|
|
4
|
+
* Provides the `html` tagged template literal for writing markup in JavaScript.
|
|
5
|
+
* This is a lightweight, zero-compile template system (JSX alternative).
|
|
6
|
+
*
|
|
7
|
+
* @module core/html
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* RawHtml wrapper class
|
|
12
|
+
* Marks HTML content as safe and prevents escaping when interpolated
|
|
13
|
+
*
|
|
14
|
+
* @class RawHtml
|
|
15
|
+
* @param {string} content - The raw HTML content
|
|
16
|
+
*/
|
|
17
|
+
export class RawHtml {
|
|
18
|
+
constructor(content) {
|
|
19
|
+
this.content = content;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toString() {
|
|
23
|
+
return this.content;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
valueOf() {
|
|
27
|
+
return this.content;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Escape HTML special characters to prevent XSS
|
|
33
|
+
*
|
|
34
|
+
* @param {any} str - Value to escape
|
|
35
|
+
* @returns {string} Escaped string
|
|
36
|
+
*/
|
|
37
|
+
function escapeHtml(str) {
|
|
38
|
+
if (typeof str !== 'string') {return str;}
|
|
39
|
+
|
|
40
|
+
const map = {
|
|
41
|
+
'&': '&',
|
|
42
|
+
'<': '<',
|
|
43
|
+
'>': '>',
|
|
44
|
+
'"': '"',
|
|
45
|
+
"'": '''
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return str.replace(/[&<>"']/g, char => map[char]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Safely interpolate a value into HTML
|
|
53
|
+
* - If RawHtml: insert as raw markup
|
|
54
|
+
* - If array: recursively process and join
|
|
55
|
+
* - If null/undefined/false: render empty string
|
|
56
|
+
* - Otherwise: escape as text
|
|
57
|
+
*
|
|
58
|
+
* @param {any} value - Value to interpolate
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function interpolateValue(value) {
|
|
62
|
+
// RawHtml: insert as raw markup (safe for nested templates)
|
|
63
|
+
if (value instanceof RawHtml) {
|
|
64
|
+
return value.toString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Arrays: recursively process and join
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map(v => interpolateValue(v)).join('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Falsy values: render empty string
|
|
73
|
+
if (value === null || value === undefined || value === false) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Functions: convert to string (for component functions, better to call them first)
|
|
78
|
+
if (typeof value === 'function') {
|
|
79
|
+
return escapeHtml(String(value));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Everything else: escape as text
|
|
83
|
+
return escapeHtml(String(value));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Tagged template literal for HTML
|
|
88
|
+
* Safely handles nested templates, arrays, and text escaping.
|
|
89
|
+
*
|
|
90
|
+
* Returns a RawHtml wrapper so nested templates aren't double-escaped.
|
|
91
|
+
*
|
|
92
|
+
* @param {TemplateStringsArray} strings
|
|
93
|
+
* @param {...any} values
|
|
94
|
+
* @returns {RawHtml}
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // Nested templates (no escaping of markup)
|
|
98
|
+
* const button = html`<af-button>Click</af-button>`;
|
|
99
|
+
* const page = html`<div>${button}</div>`;
|
|
100
|
+
* // Output: <div><af-button>Click</af-button></div>
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // Plain text (escaped for safety)
|
|
104
|
+
* const user = html`<p>${userInput}</p>`;
|
|
105
|
+
* // If userInput = "<script>alert('xss')</script>"
|
|
106
|
+
* // Output: <p><script>alert('xss')</script></p>
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // Arrays of templates
|
|
110
|
+
* const items = [html`<li>Item 1</li>`, html`<li>Item 2</li>`];
|
|
111
|
+
* const list = html`<ul>${items}</ul>`;
|
|
112
|
+
* // Output: <ul><li>Item 1</li><li>Item 2</li></ul>
|
|
113
|
+
*/
|
|
114
|
+
export function html(strings, ...values) {
|
|
115
|
+
let result = '';
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < values.length; i++) {
|
|
118
|
+
result += strings[i];
|
|
119
|
+
result += interpolateValue(values[i]);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
result += strings[strings.length - 1];
|
|
123
|
+
|
|
124
|
+
// Return as RawHtml so nested html() calls aren't escaped
|
|
125
|
+
return new RawHtml(result);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Standard Root Layout
|
|
130
|
+
* Similar to Next.js layout.js concept
|
|
131
|
+
*
|
|
132
|
+
* @param {Object} options
|
|
133
|
+
* @param {string} options.title - Page title
|
|
134
|
+
* @param {string} options.meta - Meta tags
|
|
135
|
+
* @param {RawHtml|string} options.children - Page content
|
|
136
|
+
* @returns {RawHtml}
|
|
137
|
+
*/
|
|
138
|
+
export function Layout({ title = 'AfriCode App', meta = '', children }) {
|
|
139
|
+
return html`<!DOCTYPE html>
|
|
140
|
+
<html lang="en">
|
|
141
|
+
<head>
|
|
142
|
+
<meta charset="UTF-8">
|
|
143
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
144
|
+
<title>${title}</title>
|
|
145
|
+
${meta}
|
|
146
|
+
<link rel="stylesheet" href="/styles/africanity.css">
|
|
147
|
+
<script type="module" src="/core/sdk.js"></script>
|
|
148
|
+
<script type="module">
|
|
149
|
+
import { init } from '/core/sdk.js';
|
|
150
|
+
init();
|
|
151
|
+
</script>
|
|
152
|
+
</head>
|
|
153
|
+
<body>
|
|
154
|
+
${children}
|
|
155
|
+
</body>
|
|
156
|
+
</html>`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
export default { html, Layout, RawHtml };
|