@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Rate Limiting Middleware
|
|
3
|
+
*
|
|
4
|
+
* In-memory sliding window rate limiter for API endpoints.
|
|
5
|
+
* Framework-level middleware — protect by default.
|
|
6
|
+
*
|
|
7
|
+
* Algorithm: Fixed window with sliding expiry
|
|
8
|
+
* - Each client (identified by IP) gets a counter per window
|
|
9
|
+
* - When the window expires, the counter resets
|
|
10
|
+
* - If the counter exceeds the limit, requests are rejected with 429
|
|
11
|
+
*
|
|
12
|
+
* Limitations:
|
|
13
|
+
* - In-memory only (resets on server restart)
|
|
14
|
+
* - Single-process only (not shared across workers/instances)
|
|
15
|
+
* - For production multi-instance environments, use Redis-backed rate limiting
|
|
16
|
+
*
|
|
17
|
+
* Future: This interface is designed so a Redis/KV adapter can be
|
|
18
|
+
* swapped in without changing the middleware API.
|
|
19
|
+
*
|
|
20
|
+
* @module core/server/rate-limit
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { RateLimitError } from '../errors.js';
|
|
24
|
+
import { emitRateLimit } from '../config.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} RateLimitConfig
|
|
28
|
+
* @property {number} [windowMs=60000] - Time window in milliseconds (default: 1 minute)
|
|
29
|
+
* @property {number} [maxRequests=60] - Maximum requests per window (default: 60)
|
|
30
|
+
* @property {string} [message='Too many requests'] - Error message
|
|
31
|
+
* @property {Function} [keyGenerator] - Custom key generator: (request) => string
|
|
32
|
+
* @property {string[]} [excludePaths=[]] - Paths that skip rate limiting
|
|
33
|
+
* @property {Function} [onLimitReached] - Callback when limit is reached: (key, request) => void
|
|
34
|
+
* @property {Object} [headers=true] - Include rate limit headers in response
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* In-memory rate limit store.
|
|
39
|
+
* Maps client keys to their request tracking data.
|
|
40
|
+
*
|
|
41
|
+
* @type {Map<string, { count: number, resetAt: number }>}
|
|
42
|
+
*/
|
|
43
|
+
const store = new Map();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cleanup interval reference.
|
|
47
|
+
* Periodically removes expired entries to prevent memory leaks.
|
|
48
|
+
*/
|
|
49
|
+
let cleanupInterval = null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the client identifier from a request.
|
|
53
|
+
* Default: uses IP address from various headers.
|
|
54
|
+
*
|
|
55
|
+
* @param {Request} request
|
|
56
|
+
* @returns {string} Client identifier
|
|
57
|
+
*/
|
|
58
|
+
function defaultKeyGenerator(request) {
|
|
59
|
+
// Check standard headers in priority order
|
|
60
|
+
const forwarded = request.headers.get('X-Forwarded-For');
|
|
61
|
+
if (forwarded) {
|
|
62
|
+
// X-Forwarded-For can contain multiple IPs, take the first (client)
|
|
63
|
+
return forwarded.split(',')[0].trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const realIp = request.headers.get('X-Real-IP');
|
|
67
|
+
if (realIp) {return realIp;}
|
|
68
|
+
|
|
69
|
+
// Fallback: use the request URL host (not ideal, but safe fallback)
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(request.url);
|
|
72
|
+
return url.hostname;
|
|
73
|
+
} catch {
|
|
74
|
+
return 'unknown';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a rate limiting middleware.
|
|
80
|
+
*
|
|
81
|
+
* @param {RateLimitConfig} [config] - Configuration
|
|
82
|
+
* @returns {Function} Middleware function: (request) => { response: Response|null, headers: Record<string, string> }
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Basic usage — 60 requests per minute
|
|
86
|
+
* const limiter = createRateLimiter();
|
|
87
|
+
*
|
|
88
|
+
* // In your request handler:
|
|
89
|
+
* const { response, headers } = limiter(request);
|
|
90
|
+
* if (response) return response; // 429 Too Many Requests
|
|
91
|
+
* // Add headers to your success response for transparency
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* // Custom limits for auth endpoints
|
|
95
|
+
* const authLimiter = createRateLimiter({
|
|
96
|
+
* windowMs: 15 * 60 * 1000, // 15 minutes
|
|
97
|
+
* maxRequests: 5, // 5 attempts
|
|
98
|
+
* message: 'Too many login attempts. Please try again later.',
|
|
99
|
+
* onLimitReached: (key) => console.warn(`Rate limit reached for ${key}`)
|
|
100
|
+
* });
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // Skip rate limiting for certain paths
|
|
104
|
+
* const limiter = createRateLimiter({
|
|
105
|
+
* excludePaths: ['/api/health', '/api/status']
|
|
106
|
+
* });
|
|
107
|
+
*/
|
|
108
|
+
export function createRateLimiter(config = {}) {
|
|
109
|
+
const {
|
|
110
|
+
windowMs = 60 * 1000, // 1 minute
|
|
111
|
+
maxRequests = 60, // 60 requests per window
|
|
112
|
+
message = 'Too many requests. Please try again later.',
|
|
113
|
+
keyGenerator = defaultKeyGenerator,
|
|
114
|
+
excludePaths = [],
|
|
115
|
+
onLimitReached = null
|
|
116
|
+
} = config;
|
|
117
|
+
|
|
118
|
+
// Start cleanup interval if not already running
|
|
119
|
+
if (!cleanupInterval) {
|
|
120
|
+
cleanupInterval = setInterval(() => {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
for (const [key, data] of store.entries()) {
|
|
123
|
+
if (now >= data.resetAt) {
|
|
124
|
+
store.delete(key);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}, windowMs);
|
|
128
|
+
|
|
129
|
+
// Don't keep the process alive just for cleanup
|
|
130
|
+
if (cleanupInterval.unref) {
|
|
131
|
+
cleanupInterval.unref();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {Request} request
|
|
137
|
+
* @returns {{ response: Response|null, headers: Record<string, string> }}
|
|
138
|
+
*/
|
|
139
|
+
return function rateLimitMiddleware(request) {
|
|
140
|
+
// Check excluded paths
|
|
141
|
+
try {
|
|
142
|
+
const url = new URL(request.url);
|
|
143
|
+
if (excludePaths.some(path => url.pathname.startsWith(path))) {
|
|
144
|
+
return { response: null, headers: {} };
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Invalid URL, continue with rate limiting
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const key = keyGenerator(request);
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
|
|
153
|
+
// Get or create entry
|
|
154
|
+
let entry = store.get(key);
|
|
155
|
+
|
|
156
|
+
if (!entry || now >= entry.resetAt) {
|
|
157
|
+
// Window expired or new client — reset
|
|
158
|
+
entry = { count: 0, resetAt: now + windowMs };
|
|
159
|
+
store.set(key, entry);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Increment count
|
|
163
|
+
entry.count++;
|
|
164
|
+
|
|
165
|
+
// Build rate limit headers (always returned for transparency)
|
|
166
|
+
const remaining = Math.max(0, maxRequests - entry.count);
|
|
167
|
+
const resetAtSeconds = Math.ceil(entry.resetAt / 1000);
|
|
168
|
+
const headers = {
|
|
169
|
+
'X-RateLimit-Limit': String(maxRequests),
|
|
170
|
+
'X-RateLimit-Remaining': String(remaining),
|
|
171
|
+
'X-RateLimit-Reset': String(resetAtSeconds)
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Check if over limit
|
|
175
|
+
if (entry.count > maxRequests) {
|
|
176
|
+
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
|
|
177
|
+
|
|
178
|
+
// Emit hook
|
|
179
|
+
emitRateLimit(key, request);
|
|
180
|
+
|
|
181
|
+
// Call user callback if provided
|
|
182
|
+
if (onLimitReached) {
|
|
183
|
+
onLimitReached(key, request);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const error = new RateLimitError(retryAfter, message);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
response: new Response(
|
|
190
|
+
JSON.stringify(error.toJSON()),
|
|
191
|
+
{
|
|
192
|
+
status: 429,
|
|
193
|
+
headers: {
|
|
194
|
+
'Content-Type': 'application/json',
|
|
195
|
+
'Retry-After': String(retryAfter),
|
|
196
|
+
...headers
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
),
|
|
200
|
+
headers
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Under limit — pass through
|
|
205
|
+
return { response: null, headers };
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Reset the rate limit store.
|
|
211
|
+
* Useful for testing or administrative purposes.
|
|
212
|
+
*/
|
|
213
|
+
export function resetRateLimitStore() {
|
|
214
|
+
store.clear();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get the current store size (number of tracked clients).
|
|
219
|
+
* Useful for monitoring.
|
|
220
|
+
*
|
|
221
|
+
* @returns {number}
|
|
222
|
+
*/
|
|
223
|
+
export function getRateLimitStoreSize() {
|
|
224
|
+
return store.size;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Stop the cleanup interval.
|
|
229
|
+
* Call this during server shutdown.
|
|
230
|
+
*/
|
|
231
|
+
export function stopRateLimitCleanup() {
|
|
232
|
+
if (cleanupInterval) {
|
|
233
|
+
clearInterval(cleanupInterval);
|
|
234
|
+
cleanupInterval = null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export default { createRateLimiter, resetRateLimitStore, getRateLimitStoreSize, stopRateLimitCleanup };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode SSR Engine
|
|
3
|
+
* Implements Declarative Shadow DOM (DSD) for instant loading.
|
|
4
|
+
*
|
|
5
|
+
* "The primary requirement... is the optimization of the rendering pipeline." - Blueprint
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { file } from "bun";
|
|
9
|
+
|
|
10
|
+
// Registry of component definitions for SSR
|
|
11
|
+
// In a real framework, this would parse the component class
|
|
12
|
+
// For v1, we map tags to their Shadow DOM HTML
|
|
13
|
+
const componentRegistry = {
|
|
14
|
+
'af-navbar': (attrs) => `
|
|
15
|
+
<template shadowrootmode="open">
|
|
16
|
+
<style>
|
|
17
|
+
:host { display: block; font-family: 'Inter', system-ui; }
|
|
18
|
+
nav { background: #1EB53A; padding: 16px; color: white; display: flex; justify-content: space-between; }
|
|
19
|
+
.logo { font-weight: 800; font-size: 1.2rem; }
|
|
20
|
+
</style>
|
|
21
|
+
<nav>
|
|
22
|
+
<div class="logo">${attrs.logo || 'AfriCode'}</div>
|
|
23
|
+
<div class="links"><slot></slot></div>
|
|
24
|
+
</nav>
|
|
25
|
+
</template>
|
|
26
|
+
`,
|
|
27
|
+
'af-card': (attrs) => `
|
|
28
|
+
<template shadowrootmode="open">
|
|
29
|
+
<style>
|
|
30
|
+
:host { display: block; background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; color: white; }
|
|
31
|
+
</style>
|
|
32
|
+
<slot></slot>
|
|
33
|
+
</template>
|
|
34
|
+
`
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Bun-specific imports moved inside functions to allow browser-side evaluation without crashes
|
|
38
|
+
export async function renderPage(filePath) {
|
|
39
|
+
const { file } = await import("bun");
|
|
40
|
+
const fileRef = file(filePath);
|
|
41
|
+
let html = await fileRef.text();
|
|
42
|
+
|
|
43
|
+
// 1. SSR: Inject Declarative Shadow DOM
|
|
44
|
+
// Matches <af-tag ...></af-tag>
|
|
45
|
+
// Regex is simple for v1, real parser needed for production
|
|
46
|
+
for (const [tag, renderFn] of Object.entries(componentRegistry)) {
|
|
47
|
+
const regex = new RegExp(`<${tag}([^>]*)>`, 'g');
|
|
48
|
+
|
|
49
|
+
html = html.replace(regex, (match, attrString) => {
|
|
50
|
+
// Parse attributes
|
|
51
|
+
const attrs = {};
|
|
52
|
+
attrString.replace(/(\w+)="([^"]*)"/g, (m, key, value) => {
|
|
53
|
+
attrs[key] = value;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Return tag + DSD Template
|
|
57
|
+
return `<${tag}${attrString}>${renderFn(attrs)}`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. SEO: Inject Meta Tags if missing
|
|
62
|
+
if (!html.includes('<meta name="generator"')) {
|
|
63
|
+
html = html.replace('</head>', '<meta name="generator" content="AfriCode v1.0 SSR"></head>');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new Response(html, {
|
|
67
|
+
headers: { 'Content-Type': 'text/html' }
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Server-Side Router
|
|
3
|
+
*
|
|
4
|
+
* Handles dynamic API routing by mapping URLs to the file system.
|
|
5
|
+
* Edge-Ready: Uses strictly standard Request/Response objects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
// Cache routes in memory for performance
|
|
12
|
+
const routeCache = new Map();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Discovers API routes in the /pages/api directory
|
|
16
|
+
* Note: In a pure Edge environment (Cloudflare), this would be replaced
|
|
17
|
+
* by a build-time manifest injection.
|
|
18
|
+
*/
|
|
19
|
+
async function loadRoutes() {
|
|
20
|
+
const apiDir = path.join(process.cwd(), 'pages', 'api');
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(apiDir)) {return;}
|
|
23
|
+
|
|
24
|
+
const files = fs.readdirSync(apiDir);
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
if (file.endsWith('.js')) {
|
|
28
|
+
const routeName = '/api/' + file.replace('.js', '');
|
|
29
|
+
const modulePath = path.join(apiDir, file);
|
|
30
|
+
routeCache.set(routeName, modulePath);
|
|
31
|
+
console.log(`✓ Registered Route: ${routeName}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* HTTP Method Handler
|
|
38
|
+
* @param {object} module - The imported API module
|
|
39
|
+
* @param {Request} req - The incoming request
|
|
40
|
+
* @returns {Promise<Response>}
|
|
41
|
+
*/
|
|
42
|
+
async function executeHandler(module, req) {
|
|
43
|
+
const method = req.method.toUpperCase();
|
|
44
|
+
|
|
45
|
+
if (module[method]) {
|
|
46
|
+
return await module[method](req);
|
|
47
|
+
} else if (module.default) {
|
|
48
|
+
return await module.default(req);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return new Response('Method Not Allowed', { status: 405 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Router Handler
|
|
56
|
+
* @param {Request} req
|
|
57
|
+
* @returns {Promise<Response|null>}
|
|
58
|
+
*/
|
|
59
|
+
export async function handleApiRequest(req) {
|
|
60
|
+
const url = new URL(req.url);
|
|
61
|
+
const pathname = url.pathname;
|
|
62
|
+
|
|
63
|
+
// 0. Handle built-in auth endpoints
|
|
64
|
+
if (pathname.startsWith('/api/auth/')) {
|
|
65
|
+
try {
|
|
66
|
+
const { handleAuthRequest } = await import('./auth-endpoints.js');
|
|
67
|
+
const authResponse = await handleAuthRequest(req, pathname);
|
|
68
|
+
if (authResponse) {
|
|
69
|
+
return authResponse;
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(`Auth endpoint error at ${pathname}:`, err);
|
|
73
|
+
return new Response(JSON.stringify({ error: err.message }), {
|
|
74
|
+
status: 500,
|
|
75
|
+
headers: { 'Content-Type': 'application/json' }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Load routes lazily
|
|
81
|
+
if (routeCache.size === 0) {
|
|
82
|
+
await loadRoutes();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 1. Exact Match
|
|
86
|
+
let modulePath = routeCache.get(pathname);
|
|
87
|
+
|
|
88
|
+
// 2. Dynamic Match (Basic implementation)
|
|
89
|
+
// If no exact match, look for dynamic routes [id].js?
|
|
90
|
+
if (!modulePath) {
|
|
91
|
+
for (const [route, path] of routeCache.entries()) {
|
|
92
|
+
if (route.includes('[') && matchDynamicRoute(route, pathname)) {
|
|
93
|
+
modulePath = path;
|
|
94
|
+
// We would inject params into request here
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (modulePath) {
|
|
101
|
+
try {
|
|
102
|
+
// Import the module
|
|
103
|
+
const module = await import(modulePath);
|
|
104
|
+
return await executeHandler(module, req);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(`Status 500 at ${pathname}:`, err);
|
|
107
|
+
return new Response(JSON.stringify({ error: err.message }), {
|
|
108
|
+
status: 500,
|
|
109
|
+
headers: { 'Content-Type': 'application/json' }
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null; // Not captured -> 404 handled by server.js
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function matchDynamicRoute(routePattern, actualPath) {
|
|
118
|
+
// Placeholder logic for future expansion
|
|
119
|
+
return false;
|
|
120
|
+
}
|
package/core/shim.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode SSR DOM Shim
|
|
3
|
+
* Allows Web Components to be imported in Node/Bun environment.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
if (typeof globalThis.HTMLElement === 'undefined') {
|
|
7
|
+
globalThis.HTMLElement = class HTMLElement { };
|
|
8
|
+
globalThis.customElements = { define: () => { }, get: () => { } };
|
|
9
|
+
globalThis.document = {
|
|
10
|
+
createElement: () => ({
|
|
11
|
+
classList: { add: () => { } },
|
|
12
|
+
setAttribute: () => { },
|
|
13
|
+
style: {}
|
|
14
|
+
}),
|
|
15
|
+
head: { appendChild: () => { } },
|
|
16
|
+
body: { appendChild: () => { } }
|
|
17
|
+
};
|
|
18
|
+
globalThis.window = globalThis;
|
|
19
|
+
globalThis.CSSStyleSheet = class CSSStyleSheet { replaceSync() { } };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Global scope support for certain libraries
|
|
23
|
+
if (typeof global !== 'undefined') {
|
|
24
|
+
global.HTMLElement = globalThis.HTMLElement;
|
|
25
|
+
global.customElements = globalThis.customElements;
|
|
26
|
+
global.document = globalThis.document;
|
|
27
|
+
global.window = globalThis;
|
|
28
|
+
}
|
package/core/state.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode State Management - TypeScript Definitions
|
|
3
|
+
* Reactive state, signals, and effects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a reactive state object with Proxy-based reactivity
|
|
8
|
+
* @template T - The shape of the state object
|
|
9
|
+
* @param initialState - Initial state values
|
|
10
|
+
* @returns Reactive proxy of the state
|
|
11
|
+
*/
|
|
12
|
+
export function createReactiveState<T extends Record<string, any>>(
|
|
13
|
+
initialState: T
|
|
14
|
+
): T & {
|
|
15
|
+
[K in keyof T]: T[K];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Subscriber callback type
|
|
20
|
+
*/
|
|
21
|
+
export type Subscriber<T extends Record<string, any>> = (
|
|
22
|
+
newValue: T,
|
|
23
|
+
oldValue?: T
|
|
24
|
+
) => void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Subscribe to state changes
|
|
28
|
+
* @param state - Reactive state object to subscribe to
|
|
29
|
+
* @param callback - Called when state changes
|
|
30
|
+
* @returns Unsubscribe function
|
|
31
|
+
*/
|
|
32
|
+
export function subscribe<T extends Record<string, any>>(
|
|
33
|
+
state: T,
|
|
34
|
+
callback: Subscriber<T>
|
|
35
|
+
): () => void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Signal getter function
|
|
39
|
+
*/
|
|
40
|
+
export type SignalGetter<T> = () => T;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Signal setter function - accepts value or updater function
|
|
44
|
+
*/
|
|
45
|
+
export type SignalSetter<T> = (value: T | ((prev: T) => T)) => void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Signal tuple return type
|
|
49
|
+
*/
|
|
50
|
+
export type Signal<T> = [getter: SignalGetter<T>, setter: SignalSetter<T>];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a reactive signal with getter and setter
|
|
54
|
+
* @template T - Signal value type
|
|
55
|
+
* @param initialValue - Initial signal value
|
|
56
|
+
* @returns Tuple of [getter, setter]
|
|
57
|
+
*/
|
|
58
|
+
export function createSignal<T>(initialValue: T): Signal<T>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Effect callback - can optionally return cleanup function
|
|
62
|
+
*/
|
|
63
|
+
export type EffectCallback = () => void | (() => void);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Effect cleanup function
|
|
67
|
+
*/
|
|
68
|
+
export type EffectCleanup = () => void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create an effect that runs side effects
|
|
72
|
+
* Effects track dependencies from signals and state
|
|
73
|
+
* @param effect - Effect function to run
|
|
74
|
+
* @returns Cleanup function
|
|
75
|
+
*/
|
|
76
|
+
export function createEffect(effect: EffectCallback): EffectCleanup;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Internal subscriber map (for advanced use)
|
|
80
|
+
*/
|
|
81
|
+
export const subscribers: WeakMap<object, Set<Subscriber<any>>>;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Internal signal registry (for advanced use)
|
|
85
|
+
*/
|
|
86
|
+
export const signals: WeakMap<object, any>;
|