@useavalon/avalon 0.1.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 (159) hide show
  1. package/README.md +54 -0
  2. package/mod.ts +301 -0
  3. package/package.json +85 -0
  4. package/src/build/README.md +310 -0
  5. package/src/build/integration-bundler-plugin.ts +116 -0
  6. package/src/build/integration-config.ts +168 -0
  7. package/src/build/integration-detection-plugin.ts +117 -0
  8. package/src/build/integration-resolver-plugin.ts +90 -0
  9. package/src/build/island-manifest.ts +269 -0
  10. package/src/build/island-types-generator.ts +476 -0
  11. package/src/build/mdx-island-transform.ts +464 -0
  12. package/src/build/mdx-plugin.ts +98 -0
  13. package/src/build/page-island-transform.ts +598 -0
  14. package/src/build/prop-extractors/index.ts +21 -0
  15. package/src/build/prop-extractors/lit.ts +140 -0
  16. package/src/build/prop-extractors/qwik.ts +16 -0
  17. package/src/build/prop-extractors/solid.ts +125 -0
  18. package/src/build/prop-extractors/svelte.ts +194 -0
  19. package/src/build/prop-extractors/vue.ts +111 -0
  20. package/src/build/sidecar-file-manager.ts +104 -0
  21. package/src/build/sidecar-renderer.ts +30 -0
  22. package/src/client/adapters/index.ts +13 -0
  23. package/src/client/adapters/lit-adapter.ts +654 -0
  24. package/src/client/adapters/preact-adapter.ts +331 -0
  25. package/src/client/adapters/qwik-adapter.ts +345 -0
  26. package/src/client/adapters/react-adapter.ts +353 -0
  27. package/src/client/adapters/solid-adapter.ts +451 -0
  28. package/src/client/adapters/svelte-adapter.ts +524 -0
  29. package/src/client/adapters/vue-adapter.ts +467 -0
  30. package/src/client/components.ts +35 -0
  31. package/src/client/css-hmr-handler.ts +344 -0
  32. package/src/client/framework-adapter.ts +462 -0
  33. package/src/client/hmr-coordinator.ts +396 -0
  34. package/src/client/hmr-error-overlay.js +533 -0
  35. package/src/client/main.js +816 -0
  36. package/src/client/tests/css-hmr-handler.test.ts +360 -0
  37. package/src/client/tests/framework-adapter.test.ts +519 -0
  38. package/src/client/tests/hmr-coordinator.test.ts +176 -0
  39. package/src/client/tests/hydration-option-parsing.test.ts +107 -0
  40. package/src/client/tests/lit-adapter.test.ts +427 -0
  41. package/src/client/tests/preact-adapter.test.ts +353 -0
  42. package/src/client/tests/qwik-adapter.test.ts +343 -0
  43. package/src/client/tests/react-adapter.test.ts +317 -0
  44. package/src/client/tests/solid-adapter.test.ts +396 -0
  45. package/src/client/tests/svelte-adapter.test.ts +387 -0
  46. package/src/client/tests/vue-adapter.test.ts +407 -0
  47. package/src/client/types/framework-runtime.d.ts +68 -0
  48. package/src/client/types/vite-hmr.d.ts +46 -0
  49. package/src/client/types/vite-virtual-modules.d.ts +60 -0
  50. package/src/components/Image.tsx +123 -0
  51. package/src/components/IslandErrorBoundary.tsx +145 -0
  52. package/src/components/LayoutDataErrorBoundary.tsx +141 -0
  53. package/src/components/LayoutErrorBoundary.tsx +127 -0
  54. package/src/components/PersistentIsland.tsx +52 -0
  55. package/src/components/StreamingErrorBoundary.tsx +233 -0
  56. package/src/components/StreamingLayout.tsx +538 -0
  57. package/src/components/tests/component-analyzer.test.ts +96 -0
  58. package/src/components/tests/component-detection.test.ts +347 -0
  59. package/src/components/tests/persistent-islands.test.ts +398 -0
  60. package/src/core/components/component-analyzer.ts +192 -0
  61. package/src/core/components/component-detection.ts +508 -0
  62. package/src/core/components/enhanced-framework-detector.ts +500 -0
  63. package/src/core/components/framework-registry.ts +563 -0
  64. package/src/core/components/tests/enhanced-framework-detector.test.ts +577 -0
  65. package/src/core/components/tests/framework-registry.test.ts +465 -0
  66. package/src/core/content/mdx-processor.ts +46 -0
  67. package/src/core/integrations/README.md +282 -0
  68. package/src/core/integrations/index.ts +19 -0
  69. package/src/core/integrations/loader.ts +125 -0
  70. package/src/core/integrations/registry.ts +195 -0
  71. package/src/core/islands/island-persistence.ts +325 -0
  72. package/src/core/islands/island-state-serializer.ts +258 -0
  73. package/src/core/islands/persistent-island-context.tsx +80 -0
  74. package/src/core/islands/use-persistent-state.ts +68 -0
  75. package/src/core/layout/enhanced-layout-resolver.ts +322 -0
  76. package/src/core/layout/layout-cache-manager.ts +485 -0
  77. package/src/core/layout/layout-composer.ts +357 -0
  78. package/src/core/layout/layout-data-loader.ts +516 -0
  79. package/src/core/layout/layout-discovery.ts +243 -0
  80. package/src/core/layout/layout-matcher.ts +299 -0
  81. package/src/core/layout/layout-types.ts +110 -0
  82. package/src/core/layout/tests/enhanced-layout-resolver.test.ts +477 -0
  83. package/src/core/layout/tests/layout-cache-optimization.test.ts +149 -0
  84. package/src/core/layout/tests/layout-composer.test.ts +486 -0
  85. package/src/core/layout/tests/layout-data-loader.test.ts +443 -0
  86. package/src/core/layout/tests/layout-discovery.test.ts +253 -0
  87. package/src/core/layout/tests/layout-matcher.test.ts +480 -0
  88. package/src/core/modules/framework-module-resolver.ts +273 -0
  89. package/src/core/modules/tests/framework-module-resolver.test.ts +263 -0
  90. package/src/core/modules/tests/module-resolution-integration.test.ts +117 -0
  91. package/src/islands/component-analysis.ts +213 -0
  92. package/src/islands/css-utils.ts +565 -0
  93. package/src/islands/discovery/index.ts +80 -0
  94. package/src/islands/discovery/registry.ts +340 -0
  95. package/src/islands/discovery/resolver.ts +477 -0
  96. package/src/islands/discovery/scanner.ts +386 -0
  97. package/src/islands/discovery/tests/island-discovery.test.ts +881 -0
  98. package/src/islands/discovery/types.ts +117 -0
  99. package/src/islands/discovery/validator.ts +544 -0
  100. package/src/islands/discovery/watcher.ts +368 -0
  101. package/src/islands/framework-detection.ts +428 -0
  102. package/src/islands/integration-loader.ts +490 -0
  103. package/src/islands/island.tsx +565 -0
  104. package/src/islands/render-cache.ts +550 -0
  105. package/src/islands/types.ts +80 -0
  106. package/src/islands/universal-css-collector.ts +157 -0
  107. package/src/islands/universal-head-collector.ts +137 -0
  108. package/src/layout-system.d.ts +592 -0
  109. package/src/layout-system.ts +218 -0
  110. package/src/middleware/__tests__/discovery.test.ts +107 -0
  111. package/src/middleware/discovery.ts +268 -0
  112. package/src/middleware/executor.ts +315 -0
  113. package/src/middleware/index.ts +76 -0
  114. package/src/middleware/types.ts +99 -0
  115. package/src/nitro/build-config.ts +576 -0
  116. package/src/nitro/config.ts +483 -0
  117. package/src/nitro/error-handler.ts +636 -0
  118. package/src/nitro/index.ts +173 -0
  119. package/src/nitro/island-manifest.ts +584 -0
  120. package/src/nitro/middleware-adapter.ts +260 -0
  121. package/src/nitro/renderer.ts +1458 -0
  122. package/src/nitro/route-discovery.ts +439 -0
  123. package/src/nitro/types.ts +321 -0
  124. package/src/render/collect-css.ts +198 -0
  125. package/src/render/error-pages.ts +79 -0
  126. package/src/render/isolated-ssr-renderer.ts +654 -0
  127. package/src/render/ssr.ts +1030 -0
  128. package/src/schemas/api.ts +30 -0
  129. package/src/schemas/core.ts +64 -0
  130. package/src/schemas/index.ts +212 -0
  131. package/src/schemas/layout.ts +279 -0
  132. package/src/schemas/routing/index.ts +38 -0
  133. package/src/schemas/routing.ts +376 -0
  134. package/src/types/as-island.ts +20 -0
  135. package/src/types/image.d.ts +106 -0
  136. package/src/types/index.d.ts +22 -0
  137. package/src/types/island-jsx.d.ts +33 -0
  138. package/src/types/island-prop.d.ts +20 -0
  139. package/src/types/layout.ts +285 -0
  140. package/src/types/mdx.d.ts +6 -0
  141. package/src/types/routing.ts +555 -0
  142. package/src/types/tests/layout-types.test.ts +197 -0
  143. package/src/types/types.ts +5 -0
  144. package/src/types/urlpattern.d.ts +49 -0
  145. package/src/types/vite-env.d.ts +11 -0
  146. package/src/utils/dev-logger.ts +299 -0
  147. package/src/utils/fs.ts +151 -0
  148. package/src/vite-plugin/auto-discover.ts +551 -0
  149. package/src/vite-plugin/config.ts +266 -0
  150. package/src/vite-plugin/errors.ts +127 -0
  151. package/src/vite-plugin/image-optimization.ts +151 -0
  152. package/src/vite-plugin/integration-activator.ts +126 -0
  153. package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
  154. package/src/vite-plugin/module-discovery.ts +189 -0
  155. package/src/vite-plugin/nitro-integration.ts +1334 -0
  156. package/src/vite-plugin/plugin.ts +329 -0
  157. package/src/vite-plugin/tests/image-optimization.test.ts +54 -0
  158. package/src/vite-plugin/types.ts +327 -0
  159. package/src/vite-plugin/validation.ts +228 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Route Discovery Module for Nitro
3
+ *
4
+ * This module provides minimal route discovery for Avalon's SSR pages.
5
+ *
6
+ * IMPORTANT: This module is simplified to complement Nitro's native routing:
7
+ * - API routes: Handled by Nitro's auto-discovery from `api/` directory
8
+ * - Page routes: Discovered here for SSR rendering (pages are components, not h3 handlers)
9
+ * - Middleware: Handled by Nitro's auto-discovery from `middleware/` directory
10
+ *
11
+ * The page discovery is needed because Avalon pages are React/Vue/Svelte components
12
+ * that require SSR rendering, which is different from Nitro's h3 route handlers.
13
+ *
14
+ * @module nitro/route-discovery
15
+ */
16
+
17
+ import { stat } from "node:fs/promises";
18
+ import { basename, dirname, relative } from "node:path";
19
+ import { walk } from "../utils/fs.ts";
20
+ import type { DiscoveredRoute } from "./types.ts";
21
+
22
+ /**
23
+ * Supported page file extensions
24
+ */
25
+ export const PAGE_EXTENSIONS = [
26
+ ".tsx",
27
+ ".ts",
28
+ ".jsx",
29
+ ".js",
30
+ ".vue",
31
+ ".svelte",
32
+ ".md",
33
+ ".mdx",
34
+ ];
35
+
36
+ /**
37
+ * Options for page route discovery
38
+ */
39
+ export interface PageDiscoveryOptions {
40
+ /** Pages directory path (absolute or relative to project root) */
41
+ pagesDir: string;
42
+ /** Enable development mode logging */
43
+ developmentMode?: boolean;
44
+ /** Directories to exclude from scanning */
45
+ excludeDirectories?: string[];
46
+ }
47
+
48
+ /**
49
+ * Result of file path to pattern conversion
50
+ */
51
+ export interface FilePathPatternResult {
52
+ /** Route pattern (e.g., /users/:id) */
53
+ pattern: string;
54
+ /** Extracted parameter names */
55
+ params: string[];
56
+ }
57
+
58
+ /**
59
+ * Discovers page routes from multiple directories with route prefixes
60
+ *
61
+ * This supports modular architecture where pages can be in different modules,
62
+ * each with their own route prefix.
63
+ *
64
+ * @param pageDirs - Array of page directories with their route prefixes
65
+ * @param options - Discovery options
66
+ * @returns Array of discovered page routes
67
+ */
68
+ export async function discoverPageRoutesFromMultipleDirs(
69
+ pageDirs: Array<{ dir: string; prefix: string }>,
70
+ options?: Pick<PageDiscoveryOptions, "developmentMode" | "excludeDirectories">
71
+ ): Promise<DiscoveredRoute[]> {
72
+ const allRoutes: DiscoveredRoute[] = [];
73
+
74
+ for (const { dir, prefix } of pageDirs) {
75
+ const routes = await discoverPageRoutes(dir, options);
76
+
77
+ // Apply prefix to routes (except for root prefix '/')
78
+ for (const route of routes) {
79
+ if (prefix !== '/') {
80
+ // Combine prefix with pattern
81
+ // /prefix + /path -> /prefix/path
82
+ // /prefix + / -> /prefix
83
+ const combinedPattern = route.pattern === '/'
84
+ ? prefix
85
+ : prefix + route.pattern;
86
+ route.pattern = combinedPattern;
87
+ }
88
+ allRoutes.push(route);
89
+ }
90
+ }
91
+
92
+ // Sort all routes by specificity
93
+ return sortRoutesBySpecificity(allRoutes);
94
+ }
95
+
96
+ /**
97
+ * Discovers page routes from the pages directory for SSR rendering
98
+ *
99
+ * NOTE: This is specifically for page components that need SSR rendering.
100
+ * API routes should be placed in the `api/` directory and are auto-discovered
101
+ * by Nitro's native file-system routing.
102
+ *
103
+ * @param pagesDir - Path to the pages directory
104
+ * @param options - Discovery options
105
+ * @returns Array of discovered page routes
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const routes = await discoverPageRoutes('src/pages', {
110
+ * developmentMode: true,
111
+ * });
112
+ * ```
113
+ */
114
+ export async function discoverPageRoutes(
115
+ pagesDir: string,
116
+ options?: Pick<PageDiscoveryOptions, "developmentMode" | "excludeDirectories">
117
+ ): Promise<DiscoveredRoute[]> {
118
+ const routes: DiscoveredRoute[] = [];
119
+ const excludeDirs = options?.excludeDirectories ?? ["node_modules", ".git"];
120
+
121
+ try {
122
+ // Check if directory exists
123
+ const statResult = await stat(pagesDir);
124
+ if (!statResult.isDirectory()) {
125
+ if (options?.developmentMode) {
126
+ console.warn(
127
+ `[route-discovery] Pages path is not a directory: ${pagesDir}`
128
+ );
129
+ }
130
+ return [];
131
+ }
132
+ } catch (error) {
133
+ if (error instanceof Error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
134
+ if (options?.developmentMode) {
135
+ console.warn(
136
+ `[route-discovery] Pages directory not found: ${pagesDir}`
137
+ );
138
+ }
139
+ return [];
140
+ }
141
+ throw error;
142
+ }
143
+
144
+ // Walk through the pages directory
145
+ const extensions = PAGE_EXTENSIONS.map((e) => e.slice(1)); // Remove leading dot
146
+
147
+ for await (
148
+ const entry of walk(pagesDir, {
149
+ includeDirs: false,
150
+ includeSymlinks: false,
151
+ exts: extensions,
152
+ })
153
+ ) {
154
+ if (!entry.isFile) continue;
155
+
156
+ const relativePath = relative(pagesDir, entry.path);
157
+
158
+ // Skip files in excluded directories
159
+ if (excludeDirs.some((dir) => relativePath.includes(dir))) {
160
+ continue;
161
+ }
162
+
163
+ // Skip private files (in folders starting with _)
164
+ if (isPrivateFile(relativePath)) {
165
+ continue;
166
+ }
167
+
168
+ // Convert file path to route pattern
169
+ const { pattern, params } = filePathToPattern(relativePath);
170
+
171
+ routes.push({
172
+ type: "page",
173
+ filePath: entry.path,
174
+ pattern,
175
+ params,
176
+ });
177
+ }
178
+
179
+ // Sort routes by specificity (more specific routes first)
180
+ return sortRoutesBySpecificity(routes);
181
+ }
182
+
183
+ /**
184
+ * Converts a file path to a route pattern
185
+ *
186
+ * Handles:
187
+ * - Index files (index.tsx -> /)
188
+ * - Dynamic segments ([param] -> :param)
189
+ * - Catch-all segments ([...slug] -> **)
190
+ * - Route groups ((group) -> removed from path)
191
+ *
192
+ * @param filePath - Relative file path from pages directory
193
+ * @returns Pattern and extracted parameter names
194
+ *
195
+ * @example
196
+ * ```ts
197
+ * filePathToPattern('users/[id].tsx')
198
+ * // { pattern: '/users/:id', params: ['id'] }
199
+ *
200
+ * filePathToPattern('blog/[...slug].tsx')
201
+ * // { pattern: '/blog/**', params: ['slug'] }
202
+ * ```
203
+ */
204
+ export function filePathToPattern(filePath: string): FilePathPatternResult {
205
+ const params: string[] = [];
206
+
207
+ let pattern = filePath
208
+ // Normalize path separators
209
+ .replace(/\\/g, "/")
210
+ // Remove file extension
211
+ .replace(/\.(tsx|ts|jsx|js|vue|svelte|md|mdx)$/, "")
212
+ // Remove route groups (parentheses)
213
+ .replace(/\([^)]+\)\//g, "")
214
+ .replace(/\([^)]+\)$/, "");
215
+
216
+ // Handle index files
217
+ if (basename(pattern) === "index") {
218
+ pattern = dirname(pattern);
219
+ if (pattern === ".") {
220
+ pattern = "";
221
+ }
222
+ }
223
+
224
+ // Convert dynamic segments [param] to :param
225
+ // Convert catch-all segments [...slug] to **
226
+ pattern = pattern.replace(/\[([^\]]+)\]/g, (_, param) => {
227
+ if (param.startsWith("...")) {
228
+ // Catch-all segment
229
+ const paramName = param.slice(3);
230
+ params.push(paramName);
231
+ return "**";
232
+ } else {
233
+ // Dynamic segment
234
+ params.push(param);
235
+ return `:${param}`;
236
+ }
237
+ });
238
+
239
+ // Ensure leading slash
240
+ if (!pattern.startsWith("/")) {
241
+ pattern = "/" + pattern;
242
+ }
243
+
244
+ // Handle root path
245
+ if (pattern === "/" || pattern === "") {
246
+ pattern = "/";
247
+ }
248
+
249
+ // Remove trailing slash (except for root)
250
+ if (pattern.length > 1 && pattern.endsWith("/")) {
251
+ pattern = pattern.slice(0, -1);
252
+ }
253
+
254
+ return { pattern, params };
255
+ }
256
+
257
+ /**
258
+ * Checks if a file is in a private folder (starts with _)
259
+ *
260
+ * @param relativePath - Relative file path
261
+ * @returns True if the file is private
262
+ */
263
+ export function isPrivateFile(relativePath: string): boolean {
264
+ const pathParts = relativePath.split(/[/\\]/);
265
+ return pathParts.some((part) => part.startsWith("_"));
266
+ }
267
+
268
+ /**
269
+ * Calculates route specificity score for sorting
270
+ * Lower score = more specific = higher priority
271
+ *
272
+ * @param route - Discovered route
273
+ * @returns Specificity score
274
+ */
275
+ export function calculateRouteSpecificity(route: DiscoveredRoute): number {
276
+ const segments = route.pattern.split("/").filter((s) => s.length > 0);
277
+ let score = 0;
278
+
279
+ for (const segment of segments) {
280
+ if (segment === "**") {
281
+ // Catch-all has lowest priority
282
+ score += 1000;
283
+ } else if (segment.startsWith(":")) {
284
+ // Dynamic segment has medium priority
285
+ score += 100;
286
+ } else {
287
+ // Static segment has highest priority
288
+ score += 1;
289
+ }
290
+ }
291
+
292
+ // Shorter paths are more specific (for same type of segments)
293
+ score += segments.length;
294
+
295
+ return score;
296
+ }
297
+
298
+ /**
299
+ * Sorts routes by specificity (most specific first)
300
+ *
301
+ * @param routes - Array of discovered routes
302
+ * @returns Sorted array of routes
303
+ */
304
+ export function sortRoutesBySpecificity(
305
+ routes: DiscoveredRoute[]
306
+ ): DiscoveredRoute[] {
307
+ return [...routes].sort((a, b) => {
308
+ const scoreA = calculateRouteSpecificity(a);
309
+ const scoreB = calculateRouteSpecificity(b);
310
+ return scoreA - scoreB;
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Validates a route pattern for correctness
316
+ *
317
+ * @param pattern - Route pattern to validate
318
+ * @returns Array of validation errors (empty if valid)
319
+ */
320
+ export function validateRoutePattern(pattern: string): string[] {
321
+ const errors: string[] = [];
322
+
323
+ // Check for leading slash
324
+ if (!pattern.startsWith("/")) {
325
+ errors.push("Route pattern must start with /");
326
+ }
327
+
328
+ // Check for malformed dynamic segments
329
+ const malformedSegments = pattern.match(/\[[^\]]*$/g);
330
+ if (malformedSegments) {
331
+ errors.push(
332
+ `Malformed dynamic segments: ${malformedSegments.join(", ")}. Dynamic segments must be properly closed with ]`
333
+ );
334
+ }
335
+
336
+ // Check for empty dynamic segments
337
+ const emptySegments = pattern.match(/\[\]/g);
338
+ if (emptySegments) {
339
+ errors.push(
340
+ "Empty dynamic segments [] are not allowed. Use [param] for dynamic segments or [...rest] for catch-all"
341
+ );
342
+ }
343
+
344
+ // Check for nested dynamic segments
345
+ const nestedSegments = pattern.match(/\[[^\]]*\[[^\]]*\]/g);
346
+ if (nestedSegments) {
347
+ errors.push(
348
+ `Nested dynamic segments are not supported: ${nestedSegments.join(", ")}`
349
+ );
350
+ }
351
+
352
+ return errors;
353
+ }
354
+
355
+ /**
356
+ * Extracts parameter names from a route pattern
357
+ *
358
+ * @param pattern - Route pattern (e.g., /users/:id/posts/:postId)
359
+ * @returns Array of parameter names
360
+ */
361
+ export function extractParamsFromPattern(pattern: string): string[] {
362
+ const params: string[] = [];
363
+
364
+ // Match :param patterns
365
+ const dynamicMatches = pattern.matchAll(/:([^/]+)/g);
366
+ for (const match of dynamicMatches) {
367
+ params.push(match[1]);
368
+ }
369
+
370
+ // Match ** catch-all (represented as unnamed param)
371
+ if (pattern.includes("**")) {
372
+ params.push("slug");
373
+ }
374
+
375
+ return params;
376
+ }
377
+
378
+ /**
379
+ * Checks if a route pattern matches a given URL path
380
+ *
381
+ * @param pattern - Route pattern
382
+ * @param path - URL path to match
383
+ * @returns True if the pattern matches the path
384
+ */
385
+ export function matchRoutePattern(
386
+ pattern: string,
387
+ path: string
388
+ ): { matches: boolean; params: Record<string, string> } {
389
+ const params: Record<string, string> = {};
390
+
391
+ // Normalize paths
392
+ const normalizedPattern = pattern.replace(/\/$/, "") || "/";
393
+ const normalizedPath = path.replace(/\/$/, "") || "/";
394
+
395
+ const patternSegments = normalizedPattern.split("/").filter((s) => s);
396
+ const pathSegments = normalizedPath.split("/").filter((s) => s);
397
+
398
+ let patternIndex = 0;
399
+ let pathIndex = 0;
400
+
401
+ while (patternIndex < patternSegments.length) {
402
+ const patternSegment = patternSegments[patternIndex];
403
+
404
+ if (patternSegment === "**") {
405
+ // Catch-all: match remaining path segments
406
+ const remainingPath = pathSegments.slice(pathIndex).join("/");
407
+ params["slug"] = remainingPath;
408
+ return { matches: true, params };
409
+ }
410
+
411
+ if (pathIndex >= pathSegments.length) {
412
+ // No more path segments but pattern has more
413
+ return { matches: false, params: {} };
414
+ }
415
+
416
+ const pathSegment = pathSegments[pathIndex];
417
+
418
+ if (patternSegment.startsWith(":")) {
419
+ // Dynamic segment: extract param value
420
+ const paramName = patternSegment.slice(1);
421
+ params[paramName] = pathSegment;
422
+ } else if (patternSegment !== pathSegment) {
423
+ // Static segment: must match exactly
424
+ return { matches: false, params: {} };
425
+ }
426
+
427
+ patternIndex++;
428
+ pathIndex++;
429
+ }
430
+
431
+ // Check if all path segments were consumed
432
+ if (pathIndex < pathSegments.length) {
433
+ return { matches: false, params: {} };
434
+ }
435
+
436
+ return { matches: true, params };
437
+ }
438
+
439
+