@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/errors.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Error System
|
|
3
|
+
*
|
|
4
|
+
* Standardized error hierarchy for the entire framework.
|
|
5
|
+
* Every error has: code, message, field (optional), context (optional).
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - All framework errors extend AfriCodeError
|
|
9
|
+
* - Errors carry structured metadata for programmatic handling
|
|
10
|
+
* - toJSON() produces a consistent API response format
|
|
11
|
+
* - Error codes are namespaced: VALIDATION_*, SECURITY_*, DB_*, etc.
|
|
12
|
+
*
|
|
13
|
+
* @module core/errors
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base error class for all AfriCode framework errors.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* throw new AfriCodeError('Something failed', 'GENERIC_ERROR', { detail: '...' });
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // In an API handler:
|
|
24
|
+
* try { ... } catch (err) {
|
|
25
|
+
* if (err instanceof AfriCodeError) {
|
|
26
|
+
* return new Response(JSON.stringify(err.toJSON()), { status: err.statusCode });
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
export class AfriCodeError extends Error {
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} message - Human-readable error message
|
|
33
|
+
* @param {string} code - Machine-readable error code (e.g. 'VALIDATION_FAILED')
|
|
34
|
+
* @param {Object} [context={}] - Additional context for debugging
|
|
35
|
+
* @param {number} [statusCode=500] - HTTP status code
|
|
36
|
+
*/
|
|
37
|
+
constructor(message, code = 'INTERNAL_ERROR', context = {}, statusCode = 500) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'AfriCodeError';
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.context = context;
|
|
42
|
+
this.statusCode = statusCode;
|
|
43
|
+
this.timestamp = new Date().toISOString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Produce a consistent JSON structure for API responses.
|
|
48
|
+
* @returns {Object}
|
|
49
|
+
*/
|
|
50
|
+
toJSON() {
|
|
51
|
+
return {
|
|
52
|
+
error: {
|
|
53
|
+
code: this.code,
|
|
54
|
+
message: this.message,
|
|
55
|
+
...(this.context.field ? { field: this.context.field } : {}),
|
|
56
|
+
...(Object.keys(this.context).length > 0 ? { context: this.context } : {})
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create an HTTP Response from this error.
|
|
63
|
+
* @returns {Response}
|
|
64
|
+
*/
|
|
65
|
+
toResponse() {
|
|
66
|
+
return new Response(JSON.stringify(this.toJSON()), {
|
|
67
|
+
status: this.statusCode,
|
|
68
|
+
headers: { 'Content-Type': 'application/json' }
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validation error — thrown when input fails schema validation.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* throw new ValidationError("Field 'avatar_url' must be a valid URL or null", 'avatar_url');
|
|
78
|
+
*/
|
|
79
|
+
export class ValidationError extends AfriCodeError {
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} message
|
|
82
|
+
* @param {string} [field] - The field that failed validation
|
|
83
|
+
* @param {Object} [context={}]
|
|
84
|
+
*/
|
|
85
|
+
constructor(message, field = null, context = {}) {
|
|
86
|
+
super(message, 'VALIDATION_FAILED', { field, ...context }, 422);
|
|
87
|
+
this.name = 'ValidationError';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a ValidationError from a Zod error.
|
|
92
|
+
* @param {import('zod').ZodError} zodError
|
|
93
|
+
* @returns {ValidationError}
|
|
94
|
+
*/
|
|
95
|
+
static fromZodError(zodError) {
|
|
96
|
+
const firstIssue = zodError.issues[0];
|
|
97
|
+
const field = firstIssue?.path?.join('.') || null;
|
|
98
|
+
const message = firstIssue?.message || 'Validation failed';
|
|
99
|
+
|
|
100
|
+
return new ValidationError(message, field, {
|
|
101
|
+
issues: zodError.issues.map(issue => ({
|
|
102
|
+
field: issue.path.join('.'),
|
|
103
|
+
message: issue.message,
|
|
104
|
+
code: issue.code
|
|
105
|
+
}))
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Invalid URL error — specific subtype of validation error for URL fields.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* throw new InvalidUrlError('avatar_url', '');
|
|
115
|
+
* // → { error: { code: "INVALID_URL", message: "Field 'avatar_url' ...", field: "avatar_url" } }
|
|
116
|
+
*/
|
|
117
|
+
export class InvalidUrlError extends AfriCodeError {
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} field - The URL field name
|
|
120
|
+
* @param {*} value - The invalid value that was provided
|
|
121
|
+
*/
|
|
122
|
+
constructor(field, value) {
|
|
123
|
+
const valueDesc = value === '' ? 'an empty string' : `"${value}"`;
|
|
124
|
+
super(
|
|
125
|
+
`Field '${field}' must be a valid URL or null. Got ${valueDesc}.`,
|
|
126
|
+
'INVALID_URL',
|
|
127
|
+
{ field, received: value },
|
|
128
|
+
422
|
|
129
|
+
);
|
|
130
|
+
this.name = 'InvalidUrlError';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Security error — thrown for CSRF violations, rate limiting, etc.
|
|
136
|
+
*/
|
|
137
|
+
export class SecurityError extends AfriCodeError {
|
|
138
|
+
/**
|
|
139
|
+
* @param {string} message
|
|
140
|
+
* @param {string} [code='SECURITY_VIOLATION']
|
|
141
|
+
* @param {Object} [context={}]
|
|
142
|
+
* @param {number} [statusCode=403]
|
|
143
|
+
*/
|
|
144
|
+
constructor(message, code = 'SECURITY_VIOLATION', context = {}, statusCode = 403) {
|
|
145
|
+
super(message, code, context, statusCode);
|
|
146
|
+
this.name = 'SecurityError';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* CSRF error — thrown when CSRF token is missing or mismatched.
|
|
152
|
+
*/
|
|
153
|
+
export class CsrfError extends SecurityError {
|
|
154
|
+
constructor(reason = 'missing') {
|
|
155
|
+
const messages = {
|
|
156
|
+
missing: 'CSRF token missing. Include the token in both the cookie and the X-CSRF-Token header.',
|
|
157
|
+
mismatch: 'CSRF token mismatch. The header token does not match the cookie token.'
|
|
158
|
+
};
|
|
159
|
+
super(messages[reason] || messages.missing, 'CSRF_FAILURE', { reason });
|
|
160
|
+
this.name = 'CsrfError';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Rate limit error — thrown when request limit is exceeded.
|
|
166
|
+
*/
|
|
167
|
+
export class RateLimitError extends SecurityError {
|
|
168
|
+
/**
|
|
169
|
+
* @param {number} retryAfter - Seconds until the client can retry
|
|
170
|
+
* @param {string} [message]
|
|
171
|
+
*/
|
|
172
|
+
constructor(retryAfter, message = 'Too many requests. Please try again later.') {
|
|
173
|
+
super(message, 'RATE_LIMIT_EXCEEDED', { retryAfter }, 429);
|
|
174
|
+
this.name = 'RateLimitError';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Database error — thrown for DB operation failures.
|
|
180
|
+
*/
|
|
181
|
+
export class DatabaseError extends AfriCodeError {
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} message
|
|
184
|
+
* @param {string} [operation] - The operation that failed (e.g. 'INSERT', 'UPDATE')
|
|
185
|
+
* @param {Object} [context={}]
|
|
186
|
+
*/
|
|
187
|
+
constructor(message, operation = null, context = {}) {
|
|
188
|
+
super(message, 'DATABASE_ERROR', { operation, ...context }, 500);
|
|
189
|
+
this.name = 'DatabaseError';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Component error — thrown for Web Component failures.
|
|
195
|
+
*/
|
|
196
|
+
export class ComponentError extends AfriCodeError {
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} message
|
|
199
|
+
* @param {string} componentName - The component tag name (e.g. 'af-button')
|
|
200
|
+
* @param {Object} [context={}]
|
|
201
|
+
*/
|
|
202
|
+
constructor(message, componentName, context = {}) {
|
|
203
|
+
super(
|
|
204
|
+
`Component Error in <${componentName}>: ${message}`,
|
|
205
|
+
'COMPONENT_ERROR',
|
|
206
|
+
{ componentName, ...context },
|
|
207
|
+
500
|
|
208
|
+
);
|
|
209
|
+
this.name = 'ComponentError';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default {
|
|
214
|
+
AfriCodeError,
|
|
215
|
+
ValidationError,
|
|
216
|
+
InvalidUrlError,
|
|
217
|
+
SecurityError,
|
|
218
|
+
CsrfError,
|
|
219
|
+
RateLimitError,
|
|
220
|
+
DatabaseError,
|
|
221
|
+
ComponentError
|
|
222
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-Based Router for Bun Runtime
|
|
3
|
+
* Automatic mapping of /pages directory to URLs
|
|
4
|
+
* Supports dynamic routes, nested layouts, and API routes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, statSync } from 'fs';
|
|
8
|
+
import { join, extname, dirname, relative } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
export class FileBasedRouter {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
pagesDir: options.pagesDir || 'pages',
|
|
15
|
+
apiDir: options.apiDir || 'pages/api',
|
|
16
|
+
layoutsDir: options.layoutsDir || 'layouts',
|
|
17
|
+
middlewareDir: options.middlewareDir || 'middleware',
|
|
18
|
+
...options
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.routes = new Map();
|
|
22
|
+
this.apiRoutes = new Map();
|
|
23
|
+
this.layouts = new Map();
|
|
24
|
+
this.middleware = new Map();
|
|
25
|
+
|
|
26
|
+
this._scanRoutes();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Scan pages directory and build route mappings
|
|
31
|
+
*/
|
|
32
|
+
_scanRoutes() {
|
|
33
|
+
const pagesPath = join(process.cwd(), this.options.pagesDir);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
this._scanDirectory(pagesPath, '');
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn('[Router] Pages directory not found:', pagesPath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Recursively scan directory for route files
|
|
44
|
+
*/
|
|
45
|
+
_scanDirectory(dirPath, routePrefix) {
|
|
46
|
+
const items = readdirSync(dirPath);
|
|
47
|
+
|
|
48
|
+
for (const item of items) {
|
|
49
|
+
const itemPath = join(dirPath, item);
|
|
50
|
+
const stat = statSync(itemPath);
|
|
51
|
+
|
|
52
|
+
if (stat.isDirectory()) {
|
|
53
|
+
// Handle nested directories
|
|
54
|
+
const subRoute = routePrefix + '/' + item;
|
|
55
|
+
this._scanDirectory(itemPath, subRoute);
|
|
56
|
+
} else if (stat.isFile()) {
|
|
57
|
+
// Handle route files
|
|
58
|
+
this._processRouteFile(itemPath, item, routePrefix);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process individual route file
|
|
65
|
+
*/
|
|
66
|
+
_processRouteFile(filePath, filename, routePrefix) {
|
|
67
|
+
const ext = extname(filename);
|
|
68
|
+
const name = filename.replace(ext, '');
|
|
69
|
+
|
|
70
|
+
// Skip non-route files
|
|
71
|
+
if (!['.js', '.ts', '.mjs'].includes(ext)) return;
|
|
72
|
+
|
|
73
|
+
// Handle API routes
|
|
74
|
+
if (filePath.includes('/api/') || routePrefix.includes('/api')) {
|
|
75
|
+
this._addApiRoute(filePath, filename, routePrefix);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle page routes
|
|
80
|
+
this._addPageRoute(filePath, filename, routePrefix);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add API route mapping
|
|
85
|
+
*/
|
|
86
|
+
_addApiRoute(filePath, filename, routePrefix) {
|
|
87
|
+
let routePath = routePrefix + '/' + filename.replace(/\.(js|ts|mjs)$/, '');
|
|
88
|
+
|
|
89
|
+
// Handle dynamic routes
|
|
90
|
+
routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
|
|
91
|
+
|
|
92
|
+
// Handle catch-all routes
|
|
93
|
+
routePath = routePath.replace(/\[\.\.\.([^\]]+)\]/g, '*$1');
|
|
94
|
+
|
|
95
|
+
this.apiRoutes.set(routePath, {
|
|
96
|
+
filePath,
|
|
97
|
+
handler: null, // Lazy load
|
|
98
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log(`[Router] API Route: ${routePath} -> ${relative(process.cwd(), filePath)}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add page route mapping
|
|
106
|
+
*/
|
|
107
|
+
_addPageRoute(filePath, filename, routePrefix) {
|
|
108
|
+
let routePath = routePrefix;
|
|
109
|
+
|
|
110
|
+
// Handle index files
|
|
111
|
+
if (filename === 'index.js' || filename === 'index.ts' || filename === 'index.mjs') {
|
|
112
|
+
// Keep routePrefix as is
|
|
113
|
+
} else {
|
|
114
|
+
routePath += '/' + filename.replace(/\.(js|ts|mjs)$/, '');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle dynamic routes
|
|
118
|
+
routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
|
|
119
|
+
|
|
120
|
+
// Handle catch-all routes
|
|
121
|
+
routePath = routePath.replace(/\[\.\.\.([^\]]+)\]/g, '*$1');
|
|
122
|
+
|
|
123
|
+
// Ensure root route
|
|
124
|
+
if (routePath === '') routePath = '/';
|
|
125
|
+
|
|
126
|
+
this.routes.set(routePath, {
|
|
127
|
+
filePath,
|
|
128
|
+
component: null, // Lazy load
|
|
129
|
+
layout: this._findLayoutForRoute(routePath)
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log(`[Router] Page Route: ${routePath} -> ${relative(process.cwd(), filePath)}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Find appropriate layout for route
|
|
137
|
+
*/
|
|
138
|
+
_findLayoutForRoute(routePath) {
|
|
139
|
+
// Check for nested layouts
|
|
140
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
141
|
+
let layoutPath = null;
|
|
142
|
+
|
|
143
|
+
// Walk up the route hierarchy
|
|
144
|
+
for (let i = segments.length; i >= 0; i--) {
|
|
145
|
+
const testPath = '/' + segments.slice(0, i).join('/');
|
|
146
|
+
const layoutFile = join(process.cwd(), this.options.layoutsDir, testPath, 'layout.js');
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
statSync(layoutFile);
|
|
150
|
+
layoutPath = layoutFile;
|
|
151
|
+
break;
|
|
152
|
+
} catch {
|
|
153
|
+
// Continue searching
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fallback to root layout
|
|
158
|
+
if (!layoutPath) {
|
|
159
|
+
const rootLayout = join(process.cwd(), this.options.layoutsDir, 'layout.js');
|
|
160
|
+
try {
|
|
161
|
+
statSync(rootLayout);
|
|
162
|
+
layoutPath = rootLayout;
|
|
163
|
+
} catch {
|
|
164
|
+
// No layout found
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return layoutPath;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Match incoming request to route
|
|
173
|
+
*/
|
|
174
|
+
matchRoute(pathname, method = 'GET') {
|
|
175
|
+
// Check API routes first
|
|
176
|
+
for (const [routePath, route] of this.apiRoutes) {
|
|
177
|
+
const params = this._matchRoutePattern(routePath, pathname);
|
|
178
|
+
if (params && route.methods.includes(method)) {
|
|
179
|
+
return {
|
|
180
|
+
type: 'api',
|
|
181
|
+
route: routePath,
|
|
182
|
+
filePath: route.filePath,
|
|
183
|
+
params,
|
|
184
|
+
handler: route.handler
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check page routes
|
|
190
|
+
for (const [routePath, route] of this.routes) {
|
|
191
|
+
const params = this._matchRoutePattern(routePath, pathname);
|
|
192
|
+
if (params) {
|
|
193
|
+
return {
|
|
194
|
+
type: 'page',
|
|
195
|
+
route: routePath,
|
|
196
|
+
filePath: route.filePath,
|
|
197
|
+
params,
|
|
198
|
+
component: route.component,
|
|
199
|
+
layout: route.layout
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Match route pattern with pathname
|
|
209
|
+
*/
|
|
210
|
+
_matchRoutePattern(pattern, pathname) {
|
|
211
|
+
const patternParts = pattern.split('/').filter(Boolean);
|
|
212
|
+
const pathParts = pathname.split('/').filter(Boolean);
|
|
213
|
+
|
|
214
|
+
if (pattern === '/' && pathname === '/') {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (patternParts.length !== pathParts.length && !pattern.includes('*')) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const params = {};
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
225
|
+
const patternPart = patternParts[i];
|
|
226
|
+
const pathPart = pathParts[i];
|
|
227
|
+
|
|
228
|
+
if (patternPart.startsWith(':')) {
|
|
229
|
+
// Dynamic parameter
|
|
230
|
+
const paramName = patternPart.slice(1);
|
|
231
|
+
params[paramName] = pathPart;
|
|
232
|
+
} else if (patternPart.startsWith('*')) {
|
|
233
|
+
// Catch-all parameter
|
|
234
|
+
const paramName = patternPart.slice(1);
|
|
235
|
+
params[paramName] = pathParts.slice(i).join('/');
|
|
236
|
+
return params; // Catch-all consumes rest
|
|
237
|
+
} else if (patternPart !== pathPart) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return params;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Load route handler/component lazily
|
|
247
|
+
*/
|
|
248
|
+
async loadRoute(routeMatch) {
|
|
249
|
+
if (routeMatch.type === 'api') {
|
|
250
|
+
if (!routeMatch.handler) {
|
|
251
|
+
const module = await import(routeMatch.filePath);
|
|
252
|
+
routeMatch.handler = module.default || module;
|
|
253
|
+
this.apiRoutes.get(routeMatch.route).handler = routeMatch.handler;
|
|
254
|
+
}
|
|
255
|
+
return routeMatch.handler;
|
|
256
|
+
} else {
|
|
257
|
+
if (!routeMatch.component) {
|
|
258
|
+
const module = await import(routeMatch.filePath);
|
|
259
|
+
routeMatch.component = module.default || module;
|
|
260
|
+
this.routes.get(routeMatch.route).component = routeMatch.component;
|
|
261
|
+
}
|
|
262
|
+
return routeMatch.component;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get all registered routes
|
|
268
|
+
*/
|
|
269
|
+
getRoutes() {
|
|
270
|
+
return {
|
|
271
|
+
pages: Array.from(this.routes.keys()),
|
|
272
|
+
apis: Array.from(this.apiRoutes.keys())
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Clear route cache (for hot reloading)
|
|
278
|
+
*/
|
|
279
|
+
clearCache() {
|
|
280
|
+
for (const route of this.apiRoutes.values()) {
|
|
281
|
+
route.handler = null;
|
|
282
|
+
}
|
|
283
|
+
for (const route of this.routes.values()) {
|
|
284
|
+
route.component = null;
|
|
285
|
+
}
|
|
286
|
+
console.log('[Router] Route cache cleared');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export default FileBasedRouter;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AfriCode Heartbeat (Integrity System)
|
|
3
|
+
*
|
|
4
|
+
* "Unremovable" integrity check that logs usage and validates
|
|
5
|
+
* the framework environment.
|
|
6
|
+
*
|
|
7
|
+
* @module core/heartbeat
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const HEARTBEAT_KEY = 'af_integrity';
|
|
11
|
+
const FRAMEWORK_VERSION = '1.0.0';
|
|
12
|
+
|
|
13
|
+
export function initHeartbeat() {
|
|
14
|
+
// 1. Console Stamp
|
|
15
|
+
console.log(
|
|
16
|
+
`%c AfriCode v${FRAMEWORK_VERSION} %c Powered by the Rhythm of the Continent `,
|
|
17
|
+
'background: #1EB53A; color: white; padding: 4px; border-radius: 4px 0 0 4px; font-weight: bold;',
|
|
18
|
+
'background: #121212; color: #FCD116; padding: 4px; border-radius: 0 4px 4px 0;'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// 2. DOM Injection (Watermark)
|
|
22
|
+
// Ensures attribution exists in the DOM, invisible but present
|
|
23
|
+
if (typeof document !== 'undefined') {
|
|
24
|
+
const meta = document.createElement('meta');
|
|
25
|
+
meta.name = 'generator';
|
|
26
|
+
meta.content = `AfriCode ${FRAMEWORK_VERSION}`;
|
|
27
|
+
document.head.appendChild(meta);
|
|
28
|
+
|
|
29
|
+
// "Unremovable" attribute watcher
|
|
30
|
+
// If someone tries to remove the attribute, it puts it back
|
|
31
|
+
document.documentElement.setAttribute('data-africode', 'true');
|
|
32
|
+
|
|
33
|
+
const observer = new MutationObserver((mutations) => {
|
|
34
|
+
mutations.forEach((mutation) => {
|
|
35
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'data-africode') {
|
|
36
|
+
if (!document.documentElement.hasAttribute('data-africode')) {
|
|
37
|
+
document.documentElement.setAttribute('data-africode', 'true');
|
|
38
|
+
console.warn('AfriCode integrity check: Attribute restored.');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
observer.observe(document.documentElement, { attributes: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Usage Telemetry (Mock)
|
|
48
|
+
// In a real scenario, this would POST to a server
|
|
49
|
+
const usage = {
|
|
50
|
+
url: typeof window !== 'undefined' ? window.location.href : 'server',
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
version: FRAMEWORK_VERSION
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Storing locally for now
|
|
56
|
+
try {
|
|
57
|
+
localStorage.setItem(HEARTBEAT_KEY, JSON.stringify(usage));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// Ignore in non-browser envs
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Auto-init on import
|
|
64
|
+
initHeartbeat();
|