@africode/core 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/AFRICODE_FRAMEWORK_GUIDE.md +707 -0
  2. package/LICENSE +623 -0
  3. package/README.md +442 -0
  4. package/bin/africode.js +73 -0
  5. package/bin/africode.js.1758507140 +343 -0
  6. package/bin/cli.ts +83 -0
  7. package/bin/create-africode.js +158 -0
  8. package/bin/scaffold.ts +219 -0
  9. package/components/accordion.js +183 -0
  10. package/components/alert.js +131 -0
  11. package/components/auth.js +172 -0
  12. package/components/avatar.js +117 -0
  13. package/components/badge.js +104 -0
  14. package/components/base.d.ts +139 -0
  15. package/components/base.js +184 -0
  16. package/components/button.js +164 -0
  17. package/components/card.js +137 -0
  18. package/components/cultural-card.js +243 -0
  19. package/components/divider.js +83 -0
  20. package/components/dropdown.js +171 -0
  21. package/components/error-boundary.js +155 -0
  22. package/components/form.js +131 -0
  23. package/components/grid.js +273 -0
  24. package/components/hero.js +138 -0
  25. package/components/icon.js +36 -0
  26. package/components/index.js +57 -0
  27. package/components/input.js +256 -0
  28. package/components/kanga-card.js +185 -0
  29. package/components/language-switcher.js +108 -0
  30. package/components/loader.js +80 -0
  31. package/components/modal.js +262 -0
  32. package/components/motion.js +84 -0
  33. package/components/navbar.js +236 -0
  34. package/components/pattern-showcase.js +225 -0
  35. package/components/progress.js +134 -0
  36. package/components/react.js +111 -0
  37. package/components/section.js +54 -0
  38. package/components/select.js +322 -0
  39. package/components/sidebar.js +180 -0
  40. package/components/skeleton.js +85 -0
  41. package/components/table.js +181 -0
  42. package/components/tabs.js +202 -0
  43. package/components/theme-toggle.js +82 -0
  44. package/components/toast.js +139 -0
  45. package/components/tooltip.js +167 -0
  46. package/core/a2ui-schema-manager.js +344 -0
  47. package/core/a2ui.js +431 -0
  48. package/core/bun-runtime.js +799 -0
  49. package/core/cli/commands/add.js +23 -0
  50. package/core/cli/commands/audit.js +58 -0
  51. package/core/cli/commands/build.js +137 -0
  52. package/core/cli/commands/create-plugin.js +241 -0
  53. package/core/cli/commands/dev.js +228 -0
  54. package/core/cli/commands/lint.js +23 -0
  55. package/core/cli/commands/test.js +34 -0
  56. package/core/cli/migrator.js +71 -0
  57. package/core/cli/ui.js +46 -0
  58. package/core/compliance.js +628 -0
  59. package/core/config.js +263 -0
  60. package/core/db-advanced.js +481 -0
  61. package/core/db.js +284 -0
  62. package/core/enhanced-hmr.js +404 -0
  63. package/core/errors.js +222 -0
  64. package/core/file-router.js +290 -0
  65. package/core/heartbeat.js +64 -0
  66. package/core/hmr-client.js +204 -0
  67. package/core/hmr.js +196 -0
  68. package/core/html.d.ts +116 -0
  69. package/core/html.js +160 -0
  70. package/core/hydration.js +52 -0
  71. package/core/lipa-namba-journey.js +572 -0
  72. package/core/motion.js +106 -0
  73. package/core/nida-cig-middleware.js +455 -0
  74. package/core/patterns.d.ts +124 -0
  75. package/core/patterns.js +833 -0
  76. package/core/plugins/index.js +312 -0
  77. package/core/router.js +387 -0
  78. package/core/sdk-client.js +62 -0
  79. package/core/sdk.d.ts +133 -0
  80. package/core/sdk.js +123 -0
  81. package/core/seo.js +76 -0
  82. package/core/server/auth-endpoints.js +339 -0
  83. package/core/server/auth.js +180 -0
  84. package/core/server/csrf.js +206 -0
  85. package/core/server/db.js +39 -0
  86. package/core/server/middleware.js +324 -0
  87. package/core/server/rate-limit.js +238 -0
  88. package/core/server/render.js +69 -0
  89. package/core/server/router.js +120 -0
  90. package/core/shim.js +28 -0
  91. package/core/state.d.ts +86 -0
  92. package/core/state.js +242 -0
  93. package/core/store.d.ts +122 -0
  94. package/core/store.js +61 -0
  95. package/core/validation.d.ts +233 -0
  96. package/core/validation.js +590 -0
  97. package/core/websocket.js +639 -0
  98. package/dist/africode.js +2905 -0
  99. package/dist/africode.js.map +61 -0
  100. package/dist/build-info.json +23 -0
  101. package/dist/components.js +2888 -0
  102. package/dist/components.js.map +58 -0
  103. package/dist/styles/africanity.css +322 -0
  104. package/dist/styles/typography.css +141 -0
  105. package/docs/IDE-Guide.md +50 -0
  106. package/package.json +110 -0
  107. package/src/index.ts +196 -0
  108. package/styles/africanity.css +322 -0
  109. package/styles/typography.css +141 -0
  110. package/templates/starter/.env.example +15 -0
  111. package/templates/starter/africode.config.js +40 -0
  112. package/templates/starter/package.json +14 -0
  113. package/templates/starter/src/pages/index.html +46 -0
  114. package/templates/starter/src/pages/index.js +32 -0
  115. package/templates/starter/src/styles/main.css +4 -0
  116. package/templates/starter-3d/.env.example +7 -0
  117. package/templates/starter-3d/africode.config.js +29 -0
  118. package/templates/starter-3d/components/af-model-viewer.js +125 -0
  119. package/templates/starter-3d/package.json +15 -0
  120. package/templates/starter-3d/src/pages/index.html +46 -0
  121. package/templates/starter-3d/src/pages/index.js +50 -0
  122. package/templates/starter-3d/src/styles/main.css +4 -0
  123. package/templates/starter-react/.env.example +15 -0
  124. package/templates/starter-react/africode.config.js +40 -0
  125. package/templates/starter-react/package.json +16 -0
  126. package/templates/starter-react/src/pages/index.html +46 -0
  127. package/templates/starter-react/src/pages/index.js +68 -0
  128. package/templates/starter-react/src/styles/main.css +4 -0
  129. package/templates/starter-tailwind/.env.example +15 -0
  130. package/templates/starter-tailwind/africode.config.js +40 -0
  131. package/templates/starter-tailwind/package.json +20 -0
  132. package/templates/starter-tailwind/src/pages/index.html +46 -0
  133. package/templates/starter-tailwind/src/pages/index.js +37 -0
  134. package/templates/starter-tailwind/src/styles/main.css +4 -0
  135. package/templates/starter-tailwind/src/styles/tailwind.css +1 -0
  136. package/templates/starter-tailwind/src/tailwind-loader.js +30 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * AfriCode Plugin Registry
3
+ *
4
+ * A minimal, strict, and predictable plug-in architecture for the AfriCode Framework.
5
+ * Employs deterministic execution, strong boundary isolation, and a stable context
6
+ * contract to prevent ecosystem bloat and ensure framework resilience.
7
+ */
8
+
9
+ // Core framework version to check plugin compatibility
10
+ export const AFRICODE_VERSION = '4.0.0';
11
+
12
+ // Only these specific hooks are permitted in v1
13
+ export const ALLOWED_HOOKS = new Set([
14
+ 'onConfigLoad',
15
+ 'onComponentRegister',
16
+ 'onRouteRegister',
17
+ 'onStateInit',
18
+ 'onServerStart',
19
+ 'onRequest',
20
+ 'onResponse',
21
+ 'onError',
22
+ 'onCliCommandRegister',
23
+ ]);
24
+
25
+ // Protected core CLI commands that plugins cannot overwrite
26
+ const PROTECTED_CLI_COMMANDS = new Set([
27
+ 'dev',
28
+ 'build',
29
+ 'start',
30
+ 'test',
31
+ 'lint',
32
+ 'audit',
33
+ 'add',
34
+ 'migrate',
35
+ 'create',
36
+ 'help',
37
+ ]);
38
+
39
+ export class AfriPluginRegistry {
40
+ constructor(options = {}) {
41
+ this.plugins = new Map();
42
+ // Mode mapping: safe (swallow errors), strict (propagate crashes immediately)
43
+ this.mode = options.mode || 'safe';
44
+
45
+ // Plugin state tracking
46
+ this.disabledPlugins = new Set();
47
+
48
+ // Internal structured store for deterministic hook execution (arrays maintain registration order)
49
+ this.hooks = {
50
+ onConfigLoad: [],
51
+ onComponentRegister: [],
52
+ onRouteRegister: [],
53
+ onStateInit: [],
54
+ onServerStart: [],
55
+ onRequest: [],
56
+ onResponse: [],
57
+ onError: [],
58
+ onCliCommandRegister: [],
59
+ };
60
+
61
+ // Store plugin-registered CLI commands
62
+ this.pluginCommands = new Map();
63
+ }
64
+
65
+ /**
66
+ * Registers a new plugin into the framework.
67
+ * @param {Object} manifest - The Plugin manifest
68
+ * @param {Object} hooks - The hooks implementation
69
+ */
70
+ register(manifest, hooks) {
71
+ this._validateManifest(manifest);
72
+ this._validateCompatibility(manifest.compatibleWith);
73
+ this._validateHooks(manifest.name, hooks);
74
+
75
+ if (this.plugins.has(manifest.name)) {
76
+ console.warn(`[AfriCode Plugin] Plugin '${manifest.name}' is already registered. Skipping.`);
77
+ return;
78
+ }
79
+
80
+ this.plugins.set(manifest.name, manifest);
81
+
82
+ // Bind hooks into the deterministic execution pipeline
83
+ for (const [hookName, handler] of Object.entries(hooks)) {
84
+ this.hooks[hookName].push({
85
+ pluginName: manifest.name,
86
+ handler,
87
+ });
88
+ }
89
+
90
+ // Observability Hook
91
+ this._logObservability('onPluginLoad', manifest.name, `Registered v${manifest.version}`);
92
+ }
93
+
94
+ /**
95
+ * Disable a plugin at runtime. Safely ejects it from the pipeline.
96
+ */
97
+ disable(pluginName) {
98
+ if (!this.plugins.has(pluginName)) {
99
+ return;
100
+ }
101
+ this.disabledPlugins.add(pluginName);
102
+ this._logObservability('onPluginDisable', pluginName, 'Plugin forcefully disabled.');
103
+ }
104
+
105
+ /**
106
+ * Enable a previously disabled plugin.
107
+ */
108
+ enable(pluginName) {
109
+ this.disabledPlugins.delete(pluginName);
110
+ }
111
+
112
+ /**
113
+ * Execute a specific hook safely across all registered plugins in registration order.
114
+ * @param {string} hookName - The name of the hook to trigger
115
+ * @param {Object} context - A stable execution context exposed to plugins
116
+ */
117
+ async emit(hookName, context = {}) {
118
+ if (!ALLOWED_HOOKS.has(hookName)) {
119
+ throw new Error(`[AfriCode Plugin] Attempted to emit unknown hook: ${hookName}`);
120
+ }
121
+
122
+ const handlers = this.hooks[hookName];
123
+ if (!handlers || handlers.length === 0) {
124
+ return context;
125
+ }
126
+
127
+ // Clone context defensively to prevent deep framework mutation
128
+ // but allow mutations on specifically designed properties.
129
+ const stableContext = this._buildStableContext(hookName, context);
130
+
131
+ for (const { pluginName, handler } of handlers) {
132
+ if (this.disabledPlugins.has(pluginName)) {
133
+ continue;
134
+ } // Skip disabled plugins
135
+
136
+ const hookPromise = async () => {
137
+ const startTime = performance.now();
138
+ await handler(stableContext);
139
+ const latency = performance.now() - startTime;
140
+
141
+ // Deep trace for heavy hooks taking >50ms
142
+ if (latency > 50) {
143
+ this._logObservability(
144
+ 'onHookComplete',
145
+ pluginName,
146
+ `Hook ${hookName} resolved in ${latency.toFixed(2)}ms`
147
+ );
148
+ }
149
+ };
150
+
151
+ const timeoutPromise = new Promise((_, reject) =>
152
+ setTimeout(() => reject(new Error(`[Timeout] Plugin Execution exceeded 200ms`)), 200)
153
+ );
154
+
155
+ try {
156
+ // Execution Guarantee: Plugin must resolve within 200ms boundary
157
+ await Promise.race([hookPromise(), timeoutPromise]);
158
+ } catch (error) {
159
+ this._logObservability(
160
+ 'onPluginError',
161
+ pluginName,
162
+ `Failed during '${hookName}': ${error.message}`
163
+ );
164
+
165
+ // Isolation Policy: Bubble crash explicitly if registry runs in 'strict' mode
166
+ if (this.mode === 'strict') {
167
+ throw error;
168
+ }
169
+
170
+ // Safe mode: Prevent crash mapping downstream
171
+ if (hookName !== 'onError') {
172
+ await this.emit('onError', {
173
+ error,
174
+ source: `plugin:${pluginName}`,
175
+ hook: hookName,
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ return stableContext;
182
+ }
183
+
184
+ /**
185
+ * Internal observability tracking logger to format structured output guarantees
186
+ */
187
+ _logObservability(event, pluginName, metadata) {
188
+ const timestamp = new Date().toISOString();
189
+ console.log(
190
+ `[AfriCode Observability] ${timestamp} | EVENT: ${event} | PLUGIN: ${pluginName} | ${metadata}`
191
+ );
192
+ }
193
+
194
+ /**
195
+ * Validates the schema of a plugin manifest.
196
+ */
197
+ _validateManifest(manifest) {
198
+ if (!manifest || typeof manifest !== 'object') {
199
+ throw new Error('[AfriCode Plugin] Plugin manifest must be a valid object.');
200
+ }
201
+ if (!manifest.name || typeof manifest.name !== 'string') {
202
+ throw new Error("[AfriCode Plugin] Plugin manifest is missing a valid 'name' property.");
203
+ }
204
+ if (!manifest.version || typeof manifest.version !== 'string') {
205
+ throw new Error(
206
+ `[AfriCode Plugin: ${manifest.name}] Manifest is missing a valid 'version' property.`
207
+ );
208
+ }
209
+ if (!manifest.compatibleWith) {
210
+ throw new Error(
211
+ `[AfriCode Plugin: ${manifest.name}] Manifest is missing 'compatibleWith' boundary property.`
212
+ );
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Optional lightweight boundary check against standard semantic mapping.
218
+ */
219
+ _validateCompatibility(compatibleWith) {
220
+ // In a full production scenario, use a semantic versioning library.
221
+ // For v1, we provide a structured warning if versions mismatched wildly.
222
+ if (typeof compatibleWith !== 'string') {
223
+ return;
224
+ }
225
+
226
+ // Very basic extraction of major version target (e.g., ^2.0.0 or ~3.0.0)
227
+ const targetMajor = compatibleWith.match(/\d+/)?.[0];
228
+ const currentMajor = AFRICODE_VERSION.split('.')[0];
229
+
230
+ if (targetMajor && targetMajor !== currentMajor) {
231
+ console.warn(
232
+ `[AfriCode Plugin Warning] Plugin requires compatibility '${compatibleWith}' but framework is v${AFRICODE_VERSION}. Unexpected behavior may occur.`
233
+ );
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Ensures plugins only utilize globally permitted v1 hooks.
239
+ */
240
+ _validateHooks(pluginName, hooks) {
241
+ if (!hooks || typeof hooks !== 'object') {
242
+ throw new Error(`[AfriCode Plugin: ${pluginName}] Hooks object is missing or invalid.`);
243
+ }
244
+
245
+ for (const hookName of Object.keys(hooks)) {
246
+ if (!ALLOWED_HOOKS.has(hookName)) {
247
+ throw new Error(
248
+ `[AfriCode Plugin: ${pluginName}] Invalid hook attempt: '${hookName}'. Authorized hooks are explicitly scoped.`
249
+ );
250
+ }
251
+ if (typeof hooks[hookName] !== 'function') {
252
+ throw new Error(`[AfriCode Plugin: ${pluginName}] Hook '${hookName}' must be a function.`);
253
+ }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Scopes the contextual payload based on the specific hook type.
259
+ * Prevents plugins from pulling down random core internals.
260
+ */
261
+ _buildStableContext(hookName, payload = {}) {
262
+ switch (hookName) {
263
+ case 'onCliCommandRegister':
264
+ return {
265
+ // Safe command registration mechanism
266
+ registerCommand: (commandName, commandHandler) => {
267
+ if (PROTECTED_CLI_COMMANDS.has(commandName)) {
268
+ console.warn(
269
+ `[AfriCode Plugin] Refused to overwrite core framework CLI command: '${commandName}'.`
270
+ );
271
+ return false;
272
+ }
273
+ this.pluginCommands.set(commandName, commandHandler);
274
+ return true;
275
+ },
276
+ };
277
+ case 'onRouteRegister':
278
+ return {
279
+ addRoute: payload.addRoute, // The framework passes a strict routing interface
280
+ router: payload.router,
281
+ };
282
+ case 'onServerStart':
283
+ return { port: payload.port };
284
+ case 'onStateInit':
285
+ return { store: payload.store };
286
+ case 'onComponentRegister':
287
+ return { componentName: payload.name, componentClass: payload.component };
288
+ case 'onRequest':
289
+ case 'onResponse':
290
+ // Plugins receive specific restricted request/response maps
291
+ return { req: payload.req, res: payload.res, meta: payload.meta || {} };
292
+ case 'onError':
293
+ return { error: payload.error, source: payload.source };
294
+ case 'onConfigLoad': {
295
+ // Security Hardening: Never dump RAW process.env unconditionally
296
+ // Mask sensitive keys by delivering a tailored subset mapping.
297
+ const env = typeof process !== 'undefined' ? process.env : {};
298
+ const safeEnvSubset = {
299
+ NODE_ENV: env.NODE_ENV || 'development',
300
+ PORT: env.PORT || 3000,
301
+ // Blacklist database IPs, JWT secrets, Stripe Keys from rogue downstream plugins
302
+ };
303
+ return { config: payload.config, env: safeEnvSubset };
304
+ }
305
+ default:
306
+ return payload;
307
+ }
308
+ }
309
+ }
310
+
311
+ // Global static registry instance for core
312
+ export const registry = new AfriPluginRegistry();
package/core/router.js ADDED
@@ -0,0 +1,387 @@
1
+ /**
2
+ * FileSystemRouter - Next.js style file-based routing for AfriCode
3
+ *
4
+ * Maps URL paths to files in the pages directory:
5
+ * - / → pages/index.html
6
+ * - /about → pages/about.html
7
+ * - /users/profile → pages/users/profile.html
8
+ * - /users/[id] → pages/users/[id].js (dynamic route)
9
+ * - /api/users → pages/api/users.js (API routes)
10
+ * - /api/[...slug] → pages/api/[...slug].js (catch-all routes)
11
+ */
12
+
13
+ import { readdirSync, statSync } from 'fs';
14
+ import { join, relative, extname } from 'path';
15
+
16
+ export class FileSystemRouter {
17
+ constructor(pagesDir = './pages') {
18
+ this.pagesDir = pagesDir;
19
+ this.routes = []; // Ordered routes (dynamic routes last)
20
+ this.apiRoutes = []; // API routes
21
+ this.routeMap = new Map(); // Static routes for fast lookup
22
+ this.scanRoutes();
23
+ }
24
+
25
+ /**
26
+ * Recursively scan the pages directory and build route mappings
27
+ * @private
28
+ */
29
+ scanRoutes() {
30
+ try {
31
+ this._scanDirectory('', this.pagesDir);
32
+
33
+ // Sort routes: exact matches first, then dynamic, then catch-all
34
+ this.routes.sort((a, b) => {
35
+ const aPriority = this._getRoutePriority(a.pattern);
36
+ const bPriority = this._getRoutePriority(b.pattern);
37
+ return aPriority - bPriority;
38
+ });
39
+
40
+ this.apiRoutes.sort((a, b) => {
41
+ const aPriority = this._getRoutePriority(a.pattern);
42
+ const bPriority = this._getRoutePriority(b.pattern);
43
+ return aPriority - bPriority;
44
+ });
45
+ } catch (error) {
46
+ console.warn(`[AfriCode Router] Failed to scan pages directory: ${error.message}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Recursively scan a directory for route files
52
+ * @private
53
+ */
54
+ _scanDirectory(prefix, dirPath) {
55
+ try {
56
+ const entries = readdirSync(dirPath, { withFileTypes: true });
57
+
58
+ for (const entry of entries) {
59
+ // Skip special files/dirs
60
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
61
+ continue;
62
+ }
63
+
64
+ const fullPath = join(dirPath, entry.name);
65
+ const routePath = prefix ? `${prefix}/${entry.name}` : `/${entry.name}`;
66
+
67
+ if (entry.isDirectory()) {
68
+ // Recursively scan subdirectories
69
+ this._scanDirectory(routePath, fullPath);
70
+ } else if (this._isRouteFile(entry.name)) {
71
+ this._addRoute(routePath, fullPath, entry.name);
72
+ }
73
+ }
74
+ } catch (error) {
75
+ console.warn(`[AfriCode Router] Failed to scan directory ${dirPath}: ${error.message}`);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Check if a file should be treated as a route
81
+ * @private
82
+ */
83
+ _isRouteFile(filename) {
84
+ const validExtensions = ['.js', '.jsx', '.ts', '.tsx', '.html'];
85
+ return validExtensions.some(ext => filename.endsWith(ext)) &&
86
+ filename !== 'package.json';
87
+ }
88
+
89
+ /**
90
+ * Add a route from a file
91
+ * @private
92
+ */
93
+ _addRoute(routePath, filePath, filename) {
94
+ // Remove file extension and trailing index
95
+ let pattern = routePath.replace(/\.(js|jsx|ts|tsx|html)$/, '');
96
+
97
+ // Handle index.js → /
98
+ if (pattern.endsWith('/index')) {
99
+ pattern = pattern.replace(/\/index$/, '') || '/';
100
+ }
101
+
102
+ // Separate API routes from page routes
103
+ const isApi = pattern.startsWith('/api');
104
+
105
+ // Convert file path to route pattern with params
106
+ const route = {
107
+ pattern,
108
+ filePath,
109
+ isApi,
110
+ isDynamic: pattern.includes('['),
111
+ params: this._extractParams(pattern)
112
+ };
113
+
114
+ // Add to appropriate collection
115
+ if (isApi) {
116
+ this.apiRoutes.push(route);
117
+ } else {
118
+ this.routes.push(route);
119
+ // Add exact matches to fast lookup map
120
+ if (!route.isDynamic) {
121
+ this.routeMap.set(pattern, route);
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Extract parameter names from route pattern
128
+ * /users/[id] → ['id']
129
+ * /posts/[...slug] → ['slug'] (with rest: true)
130
+ * @private
131
+ */
132
+ _extractParams(pattern) {
133
+ const params = {};
134
+ const matches = pattern.match(/\[([^\]]+)\]/g) || [];
135
+
136
+ matches.forEach((match, index) => {
137
+ const name = match.slice(1, -1); // Remove [ and ]
138
+ if (name.startsWith('...')) {
139
+ params[name.slice(3)] = { rest: true, index };
140
+ } else {
141
+ params[name] = { rest: false, index };
142
+ }
143
+ });
144
+
145
+ return params;
146
+ }
147
+
148
+ /**
149
+ * Get route priority for sorting
150
+ * Lower number = higher priority
151
+ * @private
152
+ */
153
+ _getRoutePriority(pattern) {
154
+ // Exact matches: priority 0
155
+ if (!pattern.includes('[')) {
156
+ return 0;
157
+ }
158
+
159
+ // Dynamic with rest params (catch-all): priority 2
160
+ if (pattern.includes('[...')) {
161
+ return 2;
162
+ }
163
+
164
+ // Regular dynamic params: priority 1
165
+ return 1;
166
+ }
167
+
168
+ /**
169
+ * Check if a route matches a URL pathname
170
+ * @private
171
+ */
172
+ _matchRoute(route, pathname) {
173
+ if (!route.isDynamic) {
174
+ return route.pattern === pathname ? {} : null;
175
+ }
176
+
177
+ const patternParts = route.pattern.split('/').filter(Boolean);
178
+ const pathParts = pathname.split('/').filter(Boolean);
179
+
180
+ // Check if catch-all route
181
+ const hasCatchAll = Object.values(route.params).some(p => p.rest);
182
+
183
+ if (hasCatchAll) {
184
+ // Catch-all must match at least as many segments (minus 1 for the catch-all param)
185
+ if (pathParts.length < patternParts.length - 1) {
186
+ return null;
187
+ }
188
+
189
+ // Verify static segments match and capture catch-all
190
+ const params = {};
191
+
192
+ for (let i = 0; i < patternParts.length; i++) {
193
+ const part = patternParts[i];
194
+
195
+ if (part.startsWith('[...')) {
196
+ // Catch-all: capture remaining segments
197
+ // [... is 4 chars to remove from start, ] is 1 char from end
198
+ const paramName = part.slice(4, -1); // Remove [... and ]
199
+ params[paramName] = pathParts.slice(i).join('/');
200
+ return params; // IMPORTANT: Return immediately after matching catch-all
201
+ } else if (part !== pathParts[i]) {
202
+ // Static segment mismatch
203
+ return null;
204
+ }
205
+ }
206
+
207
+ return params;
208
+ }
209
+
210
+ // Not a catch-all route
211
+ if (patternParts.length !== pathParts.length) {
212
+ return null;
213
+ }
214
+
215
+ // Regular dynamic route matching (NOT catch-all)
216
+ const params = {};
217
+ for (let i = 0; i < patternParts.length; i++) {
218
+ const part = patternParts[i];
219
+
220
+ if (part.startsWith('[')) {
221
+ // Dynamic segment
222
+ const paramName = part.slice(1, -1);
223
+ params[paramName] = pathParts[i];
224
+ } else if (part !== pathParts[i]) {
225
+ // Static segment mismatch
226
+ return null;
227
+ }
228
+ }
229
+
230
+ return params;
231
+ }
232
+
233
+ /**
234
+ * Check which route should be used (prioritize based on specificity)
235
+ * @private
236
+ */
237
+ _findBestMatch(pathname) {
238
+ const exactMatches = [];
239
+ const dynamicMatches = [];
240
+ const catchAllMatches = [];
241
+
242
+ // Try all routes
243
+ for (const route of this.routes) {
244
+ const params = this._matchRoute(route, pathname);
245
+ if (params !== null) {
246
+ if (!route.isDynamic) {
247
+ exactMatches.push({ route, params });
248
+ } else if (Object.values(route.params).some(p => p.rest)) {
249
+ catchAllMatches.push({ route, params });
250
+ } else {
251
+ dynamicMatches.push({ route, params });
252
+ }
253
+ }
254
+ }
255
+
256
+ // Return best match: exact > dynamic > catch-all
257
+ if (exactMatches.length > 0) return exactMatches[0];
258
+ if (dynamicMatches.length > 0) return dynamicMatches[0];
259
+ if (catchAllMatches.length > 0) return catchAllMatches[0];
260
+ return null;
261
+ }
262
+
263
+ /**
264
+ * Check which API route should be used (prioritize based on specificity)
265
+ * @private
266
+ */
267
+ _findBestApiMatch(pathname) {
268
+ const exactMatches = [];
269
+ const dynamicMatches = [];
270
+ const catchAllMatches = [];
271
+
272
+ // Try all API routes
273
+ for (const route of this.apiRoutes) {
274
+ const params = this._matchRoute(route, pathname);
275
+ if (params !== null) {
276
+ if (!route.isDynamic) {
277
+ exactMatches.push({ route, params });
278
+ } else if (Object.values(route.params).some(p => p.rest)) {
279
+ catchAllMatches.push({ route, params });
280
+ } else {
281
+ dynamicMatches.push({ route, params });
282
+ }
283
+ }
284
+ }
285
+
286
+ // Return best match: exact > dynamic > catch-all
287
+ if (exactMatches.length > 0) return exactMatches[0];
288
+ if (dynamicMatches.length > 0) return dynamicMatches[0];
289
+ if (catchAllMatches.length > 0) return catchAllMatches[0];
290
+ return null;
291
+ }
292
+
293
+ /**
294
+ * Resolve a URL path to a file path and extract parameters
295
+ * @param {string} pathname - The URL pathname
296
+ * @returns {object|null} - { filePath, isApi, params, isDynamic } or null
297
+ */
298
+ resolve(pathname) {
299
+ // Normalize pathname
300
+ pathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
301
+ if (pathname.endsWith('/') && pathname !== '/') {
302
+ pathname = pathname.slice(0, -1);
303
+ }
304
+
305
+ // Fast path for exact matches
306
+ if (this.routeMap.has(pathname)) {
307
+ const route = this.routeMap.get(pathname);
308
+ return {
309
+ filePath: route.filePath,
310
+ isApi: route.isApi,
311
+ params: {},
312
+ isDynamic: false
313
+ };
314
+ }
315
+
316
+ // Check API routes first
317
+ const apiMatch = this._findBestApiMatch(pathname);
318
+ if (apiMatch) {
319
+ return {
320
+ filePath: apiMatch.route.filePath,
321
+ isApi: true,
322
+ params: apiMatch.params,
323
+ isDynamic: apiMatch.route.isDynamic
324
+ };
325
+ }
326
+
327
+ // Check page routes
328
+ const pageMatch = this._findBestMatch(pathname);
329
+ if (pageMatch) {
330
+ return {
331
+ filePath: pageMatch.route.filePath,
332
+ isApi: false,
333
+ params: pageMatch.params,
334
+ isDynamic: pageMatch.route.isDynamic
335
+ };
336
+ }
337
+
338
+ return null;
339
+ }
340
+
341
+ /**
342
+ * Get all registered routes for debugging/manifest
343
+ */
344
+ getRoutes() {
345
+ return {
346
+ pages: this.routes.map(r => ({
347
+ pattern: r.pattern,
348
+ file: r.filePath,
349
+ isDynamic: r.isDynamic
350
+ })),
351
+ apis: this.apiRoutes.map(r => ({
352
+ pattern: r.pattern,
353
+ file: r.filePath,
354
+ isDynamic: r.isDynamic
355
+ }))
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Get route manifest for build/analysis
361
+ */
362
+ getManifest() {
363
+ const manifest = {
364
+ timestamp: new Date().toISOString(),
365
+ pages: {},
366
+ apis: {}
367
+ };
368
+
369
+ for (const route of this.routes) {
370
+ manifest.pages[route.pattern] = {
371
+ file: relative(this.pagesDir, route.filePath),
372
+ dynamic: route.isDynamic,
373
+ params: route.params
374
+ };
375
+ }
376
+
377
+ for (const route of this.apiRoutes) {
378
+ manifest.apis[route.pattern] = {
379
+ file: relative(this.pagesDir, route.filePath),
380
+ dynamic: route.isDynamic,
381
+ params: route.params
382
+ };
383
+ }
384
+
385
+ return manifest;
386
+ }
387
+ }