@zap-js/client 0.0.2 → 0.0.4

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 (115) hide show
  1. package/README.md +310 -24
  2. package/bin/zap +0 -0
  3. package/bin/zap-codegen +0 -0
  4. package/dist/cli/commands/build.d.ts +11 -0
  5. package/dist/cli/commands/build.js +282 -0
  6. package/dist/cli/commands/codegen.d.ts +8 -0
  7. package/dist/cli/commands/codegen.js +95 -0
  8. package/dist/cli/commands/dev.d.ts +20 -0
  9. package/dist/cli/commands/dev.js +78 -0
  10. package/dist/cli/commands/new.d.ts +9 -0
  11. package/dist/cli/commands/new.js +307 -0
  12. package/dist/cli/commands/routes-old.d.ts +9 -0
  13. package/dist/cli/commands/routes-old.js +106 -0
  14. package/dist/cli/commands/routes.d.ts +11 -0
  15. package/dist/cli/commands/routes.js +280 -0
  16. package/dist/cli/commands/serve.d.ts +17 -0
  17. package/dist/cli/commands/serve.js +386 -0
  18. package/dist/cli/index.d.ts +2 -0
  19. package/dist/cli/index.js +76 -0
  20. package/dist/cli/utils/index.d.ts +2 -0
  21. package/dist/cli/utils/index.js +2 -0
  22. package/dist/cli/utils/logger.d.ts +84 -0
  23. package/dist/cli/utils/logger.js +181 -0
  24. package/dist/cli/utils/port-finder.d.ts +8 -0
  25. package/dist/cli/utils/port-finder.js +48 -0
  26. package/dist/dev-server/codegen-runner.d.ts +41 -0
  27. package/dist/dev-server/codegen-runner.js +172 -0
  28. package/dist/dev-server/hot-reload.d.ts +72 -0
  29. package/dist/dev-server/hot-reload.js +280 -0
  30. package/dist/dev-server/index.d.ts +8 -0
  31. package/dist/dev-server/index.js +8 -0
  32. package/dist/dev-server/route-scanner.d.ts +71 -0
  33. package/dist/dev-server/route-scanner.js +114 -0
  34. package/dist/dev-server/rust-builder.d.ts +66 -0
  35. package/dist/dev-server/rust-builder.js +286 -0
  36. package/dist/dev-server/server.d.ts +147 -0
  37. package/dist/dev-server/server.js +658 -0
  38. package/dist/dev-server/vite-proxy.d.ts +56 -0
  39. package/dist/dev-server/vite-proxy.js +212 -0
  40. package/dist/dev-server/watcher.d.ts +48 -0
  41. package/dist/dev-server/watcher.js +127 -0
  42. package/dist/router/codegen-enhanced.d.ts +5 -0
  43. package/dist/router/codegen-enhanced.js +275 -0
  44. package/dist/router/codegen.d.ts +17 -0
  45. package/dist/router/codegen.js +654 -0
  46. package/dist/router/index.d.ts +16 -0
  47. package/dist/router/index.js +19 -0
  48. package/dist/router/scanner.d.ts +86 -0
  49. package/dist/router/scanner.js +689 -0
  50. package/dist/router/ssg.d.ts +115 -0
  51. package/dist/router/ssg.js +202 -0
  52. package/dist/router/types.d.ts +124 -0
  53. package/dist/router/types.js +9 -0
  54. package/dist/router/watch.d.ts +38 -0
  55. package/dist/router/watch.js +135 -0
  56. package/dist/runtime/csrf.d.ts +146 -0
  57. package/dist/runtime/csrf.js +166 -0
  58. package/dist/runtime/error-boundary.d.ts +129 -0
  59. package/dist/runtime/error-boundary.js +287 -0
  60. package/dist/runtime/hooks.d.ts +83 -0
  61. package/dist/runtime/hooks.js +96 -0
  62. package/dist/runtime/index.d.ts +229 -0
  63. package/dist/runtime/index.js +449 -0
  64. package/dist/runtime/ipc-client.d.ts +144 -0
  65. package/dist/runtime/ipc-client.js +621 -0
  66. package/dist/runtime/logger.d.ts +71 -0
  67. package/dist/runtime/logger.js +164 -0
  68. package/dist/runtime/middleware.d.ts +66 -0
  69. package/dist/runtime/middleware.js +114 -0
  70. package/dist/runtime/process-manager.d.ts +51 -0
  71. package/dist/runtime/process-manager.js +207 -0
  72. package/dist/runtime/router-simple.d.ts +98 -0
  73. package/dist/runtime/router-simple.js +330 -0
  74. package/dist/runtime/router.d.ts +103 -0
  75. package/dist/runtime/router.js +435 -0
  76. package/dist/runtime/rpc-client.d.ts +35 -0
  77. package/dist/runtime/rpc-client.js +140 -0
  78. package/dist/runtime/streaming-utils.d.ts +86 -0
  79. package/dist/runtime/streaming-utils.js +150 -0
  80. package/dist/runtime/types.d.ts +465 -0
  81. package/dist/runtime/types.js +60 -0
  82. package/dist/runtime/websockets-utils.d.ts +50 -0
  83. package/dist/runtime/websockets-utils.js +92 -0
  84. package/package.json +30 -20
  85. package/index.js +0 -29
  86. package/internal/cli/package.json +0 -46
  87. package/internal/cli/tsconfig.tsbuildinfo +0 -1
  88. package/internal/dev-server/node_modules/ora/index.d.ts +0 -332
  89. package/internal/dev-server/node_modules/ora/index.js +0 -416
  90. package/internal/dev-server/node_modules/ora/license +0 -9
  91. package/internal/dev-server/node_modules/ora/node_modules/string-width/index.d.ts +0 -36
  92. package/internal/dev-server/node_modules/ora/node_modules/string-width/index.js +0 -65
  93. package/internal/dev-server/node_modules/ora/node_modules/string-width/license +0 -9
  94. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
  95. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/README.md +0 -107
  96. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.d.ts +0 -3
  97. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.js +0 -4
  98. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.mjs +0 -4
  99. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/package.json +0 -46
  100. package/internal/dev-server/node_modules/ora/node_modules/string-width/package.json +0 -60
  101. package/internal/dev-server/node_modules/ora/node_modules/string-width/readme.md +0 -62
  102. package/internal/dev-server/node_modules/ora/package.json +0 -66
  103. package/internal/dev-server/node_modules/ora/readme.md +0 -325
  104. package/internal/dev-server/package.json +0 -41
  105. package/internal/router/package.json +0 -28
  106. package/internal/runtime/package.json +0 -41
  107. package/internal/runtime/src/error-boundary.tsx +0 -476
  108. package/internal/runtime/src/router-simple.tsx +0 -640
  109. package/internal/runtime/src/router.tsx +0 -771
  110. package/internal/runtime/tsconfig.tsbuildinfo +0 -1
  111. package/src/errors.js +0 -33
  112. package/src/logger.js +0 -10
  113. package/src/middleware.js +0 -32
  114. package/src/router.js +0 -41
  115. package/src/types.js +0 -39
@@ -0,0 +1,689 @@
1
+ /**
2
+ * Route file scanner for ZapJS (Next.js style conventions)
3
+ *
4
+ * File Routing Conventions:
5
+ * - index.tsx → /
6
+ * - about.tsx → /about
7
+ * - [param].tsx → /:param (required dynamic segment)
8
+ * - [param]?.tsx → /:param? (optional dynamic segment)
9
+ * - [...slug].tsx → /*slug (catch-all)
10
+ * - [[...slug]].tsx → /*slug? (optional catch-all)
11
+ * - posts.[id].tsx → /posts/:id
12
+ * - _layout.tsx → Layout scoped to directory segment
13
+ * - __root.tsx → Root layout
14
+ * - (group)/ → Route group (no URL segment)
15
+ * - _excluded/ → Excluded from routing (underscore prefix for folders)
16
+ * - api/*.ts → API routes (separate folder)
17
+ * - ws/*.ts → WebSocket routes (dedicated folder)
18
+ *
19
+ * Legacy support: $param syntax is still supported for backwards compatibility
20
+ */
21
+ import { readdirSync, existsSync, readFileSync } from 'fs';
22
+ import { join, relative, extname, basename, dirname } from 'path';
23
+ const DEFAULT_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js'];
24
+ const API_FOLDER = 'api';
25
+ const WS_FOLDER = 'ws';
26
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
27
+ /**
28
+ * Detect special exports in a route file
29
+ *
30
+ * Looks for TanStack Router style exports:
31
+ * - errorComponent - Custom error boundary component
32
+ * - pendingComponent - Loading/pending state component
33
+ * - meta - Route metadata function
34
+ * - middleware - Route middleware array
35
+ * - WEBSOCKET - WebSocket handler object
36
+ * - HTTP method handlers (GET, POST, etc.) for API routes
37
+ */
38
+ function detectRouteExports(filePath, isApiRoute) {
39
+ try {
40
+ const content = readFileSync(filePath, 'utf-8');
41
+ const result = {
42
+ hasErrorComponent: false,
43
+ hasPendingComponent: false,
44
+ hasMeta: false,
45
+ hasMiddleware: false,
46
+ hasWebSocket: false,
47
+ hasGenerateStaticParams: false,
48
+ };
49
+ // Detect errorComponent export
50
+ // Patterns:
51
+ // - export const errorComponent = ...
52
+ // - export function errorComponent(...
53
+ // - export { errorComponent }
54
+ // - export { SomeComponent as errorComponent }
55
+ const errorComponentPatterns = [
56
+ /export\s+(?:const|let|var)\s+errorComponent\b/,
57
+ /export\s+function\s+errorComponent\s*\(/,
58
+ /export\s*\{\s*(?:[\w]+\s+as\s+)?errorComponent\s*[,}]/,
59
+ ];
60
+ for (const pattern of errorComponentPatterns) {
61
+ if (pattern.test(content)) {
62
+ result.hasErrorComponent = true;
63
+ result.errorComponentExport = 'errorComponent';
64
+ break;
65
+ }
66
+ }
67
+ // Detect pendingComponent export
68
+ const pendingComponentPatterns = [
69
+ /export\s+(?:const|let|var)\s+pendingComponent\b/,
70
+ /export\s+function\s+pendingComponent\s*\(/,
71
+ /export\s*\{\s*(?:[\w]+\s+as\s+)?pendingComponent\s*[,}]/,
72
+ ];
73
+ for (const pattern of pendingComponentPatterns) {
74
+ if (pattern.test(content)) {
75
+ result.hasPendingComponent = true;
76
+ result.pendingComponentExport = 'pendingComponent';
77
+ break;
78
+ }
79
+ }
80
+ // Detect meta export (route metadata)
81
+ const metaPatterns = [
82
+ /export\s+(?:const|let|var)\s+meta\b/,
83
+ /export\s+(?:async\s+)?function\s+meta\s*\(/,
84
+ /export\s*\{\s*(?:[\w]+\s+as\s+)?meta\s*[,}]/,
85
+ ];
86
+ for (const pattern of metaPatterns) {
87
+ if (pattern.test(content)) {
88
+ result.hasMeta = true;
89
+ break;
90
+ }
91
+ }
92
+ // Detect middleware export
93
+ const middlewarePatterns = [
94
+ /export\s+(?:const|let|var)\s+middleware\b/,
95
+ /export\s*\{\s*(?:[\w]+\s+as\s+)?middleware\s*[,}]/,
96
+ ];
97
+ for (const pattern of middlewarePatterns) {
98
+ if (pattern.test(content)) {
99
+ result.hasMiddleware = true;
100
+ break;
101
+ }
102
+ }
103
+ // Detect WEBSOCKET export
104
+ const wsPatterns = [
105
+ /export\s+(?:const|let|var)\s+WEBSOCKET\b/,
106
+ /export\s*\{\s*(?:[\w]+\s+as\s+)?WEBSOCKET\s*[,}]/,
107
+ ];
108
+ for (const pattern of wsPatterns) {
109
+ if (pattern.test(content)) {
110
+ result.hasWebSocket = true;
111
+ break;
112
+ }
113
+ }
114
+ // Detect generateStaticParams export (SSG)
115
+ const ssgPatterns = [
116
+ /export\s+(?:const|let|var)\s+generateStaticParams\b/,
117
+ /export\s+(?:async\s+)?function\s+generateStaticParams\s*\(/,
118
+ /export\s*\{\s*(?:[\w]+\s+as\s+)?generateStaticParams\s*[,}]/,
119
+ ];
120
+ for (const pattern of ssgPatterns) {
121
+ if (pattern.test(content)) {
122
+ result.hasGenerateStaticParams = true;
123
+ break;
124
+ }
125
+ }
126
+ // For API routes, detect HTTP method exports
127
+ if (isApiRoute) {
128
+ const detectedMethods = [];
129
+ for (const method of HTTP_METHODS) {
130
+ // Patterns for HTTP method exports:
131
+ // - export const GET = ...
132
+ // - export function GET(...
133
+ // - export async function GET(...
134
+ // - export { GET }
135
+ const methodPatterns = [
136
+ new RegExp(`export\\s+(?:const|let|var)\\s+${method}\\b`),
137
+ new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\s*\\(`),
138
+ new RegExp(`export\\s*\\{\\s*(?:[\\w]+\\s+as\\s+)?${method}\\s*[,}]`),
139
+ ];
140
+ for (const pattern of methodPatterns) {
141
+ if (pattern.test(content)) {
142
+ detectedMethods.push(method);
143
+ break;
144
+ }
145
+ }
146
+ }
147
+ if (detectedMethods.length > 0) {
148
+ result.methods = detectedMethods;
149
+ }
150
+ }
151
+ return result;
152
+ }
153
+ catch {
154
+ // If we can't read the file, return defaults
155
+ return {
156
+ hasErrorComponent: false,
157
+ hasPendingComponent: false,
158
+ hasMeta: false,
159
+ hasMiddleware: false,
160
+ hasWebSocket: false,
161
+ hasGenerateStaticParams: false,
162
+ methods: isApiRoute ? HTTP_METHODS : undefined,
163
+ };
164
+ }
165
+ }
166
+ /**
167
+ * Calculate route priority score
168
+ * Higher score = more specific route
169
+ * Static segments > dynamic segments > catch-all
170
+ */
171
+ function calculateRoutePriority(urlPath, params) {
172
+ let score = 0;
173
+ const segments = urlPath.split('/').filter(Boolean);
174
+ // Base score: 1000 points per segment (ensures longer paths generally win)
175
+ score += segments.length * 1000;
176
+ for (let i = 0; i < segments.length; i++) {
177
+ const segment = segments[i];
178
+ if (segment.startsWith('*')) {
179
+ // Catch-all: lowest priority (0 points)
180
+ // Optional catch-all even lower
181
+ const isOptional = params.some(p => p.catchAll && p.optional);
182
+ score += isOptional ? -100 : 0;
183
+ }
184
+ else if (segment.startsWith(':')) {
185
+ // Dynamic segment: medium priority (100 points)
186
+ const paramName = segment.slice(1).replace('?', '');
187
+ const isOptional = params.some(p => p.name === paramName && p.optional);
188
+ score += isOptional ? 50 : 100;
189
+ }
190
+ else {
191
+ // Static segment: high priority (500 points)
192
+ score += 500;
193
+ }
194
+ }
195
+ // Index routes get a small bonus
196
+ if (urlPath === '/') {
197
+ score += 10;
198
+ }
199
+ return score;
200
+ }
201
+ export class RouteScanner {
202
+ constructor(options) {
203
+ this.routesDir = options.routesDir;
204
+ this.extensions = options.extensions ?? DEFAULT_EXTENSIONS;
205
+ this.includeApi = options.includeApi ?? true;
206
+ }
207
+ /**
208
+ * Scan the routes directory and build a route tree
209
+ */
210
+ scan() {
211
+ if (!existsSync(this.routesDir)) {
212
+ return {
213
+ root: null,
214
+ routes: [],
215
+ layouts: [],
216
+ apiRoutes: [],
217
+ wsRoutes: [],
218
+ };
219
+ }
220
+ const routes = [];
221
+ const layouts = [];
222
+ const apiRoutes = [];
223
+ const wsRoutes = [];
224
+ let root = null;
225
+ this.scanDirectory(this.routesDir, '', routes, layouts, apiRoutes, wsRoutes, (r) => {
226
+ root = r;
227
+ });
228
+ // Assign layout paths to routes based on directory scope
229
+ this.assignLayouts(routes, layouts);
230
+ // Calculate priorities and sort by priority (descending)
231
+ routes.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
232
+ apiRoutes.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
233
+ return { root, routes, layouts, apiRoutes, wsRoutes };
234
+ }
235
+ /**
236
+ * Assign parent layouts to routes based on directory structure
237
+ */
238
+ assignLayouts(routes, layouts) {
239
+ // Sort layouts by scope path length (longest first for most specific match)
240
+ const sortedLayouts = [...layouts].sort((a, b) => b.scopePath.length - a.scopePath.length);
241
+ for (const route of routes) {
242
+ const routeDir = dirname(route.relativePath);
243
+ // Find the most specific layout that contains this route
244
+ for (const layout of sortedLayouts) {
245
+ if (routeDir === layout.scopePath || routeDir.startsWith(layout.scopePath + '/') || layout.scopePath === '') {
246
+ route.layoutPath = layout.filePath;
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ // Also set parent layouts for nested layouts
252
+ for (let i = 0; i < layouts.length; i++) {
253
+ const layout = layouts[i];
254
+ for (const parentLayout of sortedLayouts) {
255
+ if (parentLayout === layout)
256
+ continue;
257
+ const layoutDir = dirname(layout.relativePath);
258
+ if (layoutDir.startsWith(parentLayout.scopePath + '/') || (parentLayout.scopePath === '' && layoutDir !== '')) {
259
+ layout.parentLayout = parentLayout.filePath;
260
+ break;
261
+ }
262
+ }
263
+ }
264
+ }
265
+ scanDirectory(dir, pathPrefix, routes, layouts, apiRoutes, wsRoutes, setRoot) {
266
+ const entries = readdirSync(dir, { withFileTypes: true });
267
+ for (const entry of entries) {
268
+ const fullPath = join(dir, entry.name);
269
+ const relativePath = join(pathPrefix, entry.name);
270
+ if (entry.isDirectory()) {
271
+ // Skip excluded directories (prefixed with _ but not __ for root layout or _layout)
272
+ // Also support legacy - prefix
273
+ if ((entry.name.startsWith('_') && entry.name !== '_layout' && !entry.name.startsWith('__')) ||
274
+ entry.name.startsWith('-')) {
275
+ continue;
276
+ }
277
+ // Handle route groups (parentheses)
278
+ if (entry.name.startsWith('(') && entry.name.endsWith(')')) {
279
+ // Route group - no URL segment
280
+ this.scanDirectory(fullPath, pathPrefix, routes, layouts, apiRoutes, wsRoutes, setRoot);
281
+ continue;
282
+ }
283
+ // Handle API folder
284
+ if (entry.name === API_FOLDER && this.includeApi) {
285
+ this.scanApiDirectory(fullPath, '/api', apiRoutes, wsRoutes);
286
+ continue;
287
+ }
288
+ // Handle WebSocket folder
289
+ if (entry.name === WS_FOLDER) {
290
+ this.scanWsDirectory(fullPath, '/ws', wsRoutes);
291
+ continue;
292
+ }
293
+ // Regular directory - add to path
294
+ const urlSegment = this.fileNameToUrlSegment(entry.name);
295
+ const newPrefix = pathPrefix ? `${pathPrefix}/${entry.name}` : entry.name;
296
+ this.scanDirectory(fullPath, newPrefix, routes, layouts, apiRoutes, wsRoutes, setRoot);
297
+ continue;
298
+ }
299
+ // Handle files
300
+ if (!this.isRouteFile(entry.name)) {
301
+ continue;
302
+ }
303
+ const baseName = this.getBaseName(entry.name);
304
+ // Handle root layout
305
+ if (baseName === '__root') {
306
+ const rootLayout = {
307
+ type: 'root',
308
+ filePath: fullPath,
309
+ relativePath,
310
+ urlPath: '/',
311
+ children: [],
312
+ scopePath: '',
313
+ };
314
+ setRoot(rootLayout);
315
+ continue;
316
+ }
317
+ // Handle layouts - scoped to their directory segment
318
+ if (baseName === '_layout') {
319
+ const layout = {
320
+ filePath: fullPath,
321
+ relativePath,
322
+ urlPath: this.prefixToUrl(pathPrefix),
323
+ children: [],
324
+ scopePath: pathPrefix, // Directory scope for nested layouts
325
+ };
326
+ layouts.push(layout);
327
+ continue;
328
+ }
329
+ // Regular route
330
+ const route = this.parseRouteFile(fullPath, relativePath, pathPrefix, baseName);
331
+ routes.push(route);
332
+ }
333
+ }
334
+ scanApiDirectory(dir, urlPrefix, apiRoutes, wsRoutes) {
335
+ const entries = readdirSync(dir, { withFileTypes: true });
336
+ for (const entry of entries) {
337
+ const fullPath = join(dir, entry.name);
338
+ const relativePath = relative(this.routesDir, fullPath);
339
+ if (entry.isDirectory()) {
340
+ if (entry.name.startsWith('-') || entry.name.startsWith('_'))
341
+ continue;
342
+ const urlSegment = this.fileNameToUrlSegment(entry.name);
343
+ this.scanApiDirectory(fullPath, `${urlPrefix}/${urlSegment}`, apiRoutes, wsRoutes);
344
+ continue;
345
+ }
346
+ if (!this.isRouteFile(entry.name)) {
347
+ continue;
348
+ }
349
+ const baseName = this.getBaseName(entry.name);
350
+ const route = this.parseApiRouteFile(fullPath, relativePath, urlPrefix, baseName);
351
+ // Check if this API route has WEBSOCKET export
352
+ const exports = detectRouteExports(fullPath, true);
353
+ if (exports.hasWebSocket) {
354
+ wsRoutes.push({
355
+ filePath: fullPath,
356
+ relativePath,
357
+ urlPath: route.urlPath,
358
+ params: route.params,
359
+ });
360
+ }
361
+ apiRoutes.push(route);
362
+ }
363
+ }
364
+ /**
365
+ * Scan dedicated WebSocket folder
366
+ */
367
+ scanWsDirectory(dir, urlPrefix, wsRoutes) {
368
+ const entries = readdirSync(dir, { withFileTypes: true });
369
+ for (const entry of entries) {
370
+ const fullPath = join(dir, entry.name);
371
+ const relativePath = relative(this.routesDir, fullPath);
372
+ if (entry.isDirectory()) {
373
+ if (entry.name.startsWith('-') || entry.name.startsWith('_'))
374
+ continue;
375
+ const urlSegment = this.fileNameToUrlSegment(entry.name);
376
+ this.scanWsDirectory(fullPath, `${urlPrefix}/${urlSegment}`, wsRoutes);
377
+ continue;
378
+ }
379
+ if (!this.isRouteFile(entry.name)) {
380
+ continue;
381
+ }
382
+ const baseName = this.getBaseName(entry.name);
383
+ const wsRoute = this.parseWsRouteFile(fullPath, relativePath, urlPrefix, baseName);
384
+ wsRoutes.push(wsRoute);
385
+ }
386
+ }
387
+ /**
388
+ * Parse a WebSocket route file
389
+ */
390
+ parseWsRouteFile(filePath, relativePath, urlPrefix, baseName) {
391
+ const params = [];
392
+ let urlPath;
393
+ if (baseName === 'index') {
394
+ urlPath = urlPrefix;
395
+ }
396
+ else {
397
+ // Preserve brackets when splitting
398
+ const segments = this.splitPreservingBrackets(baseName);
399
+ const urlSegments = [];
400
+ let paramIndex = urlPrefix.split('/').filter(Boolean).length;
401
+ for (const segment of segments) {
402
+ const parsed = this.parseSegment(segment, paramIndex);
403
+ params.push(...parsed.params);
404
+ urlSegments.push(parsed.urlSegment);
405
+ paramIndex++;
406
+ }
407
+ urlPath = `${urlPrefix}/${urlSegments.join('/')}`;
408
+ }
409
+ return {
410
+ filePath,
411
+ relativePath,
412
+ urlPath,
413
+ params,
414
+ };
415
+ }
416
+ /**
417
+ * Parse a single segment for params
418
+ *
419
+ * Supports Next.js style:
420
+ * - [param] → :param (required)
421
+ * - [param]? → :param? (optional - file-level only)
422
+ * - [...slug] → *slug (catch-all)
423
+ * - [[...slug]] → *slug? (optional catch-all)
424
+ *
425
+ * Legacy TanStack style (still supported):
426
+ * - $param → :param
427
+ * - $param? → :param?
428
+ * - $...rest → *rest
429
+ * - $...rest? → *rest?
430
+ */
431
+ parseSegment(segment, paramIndex) {
432
+ const params = [];
433
+ // Next.js style: [[...slug]] - optional catch-all
434
+ if (segment.startsWith('[[...') && segment.endsWith(']]')) {
435
+ const paramName = segment.slice(5, -2); // Extract 'slug' from '[[...slug]]'
436
+ params.push({
437
+ name: paramName,
438
+ index: paramIndex,
439
+ catchAll: true,
440
+ optional: true,
441
+ });
442
+ return { urlSegment: `*${paramName}?`, params };
443
+ }
444
+ // Next.js style: [...slug] - catch-all
445
+ if (segment.startsWith('[...') && segment.endsWith(']')) {
446
+ const paramName = segment.slice(4, -1); // Extract 'slug' from '[...slug]'
447
+ params.push({
448
+ name: paramName,
449
+ index: paramIndex,
450
+ catchAll: true,
451
+ optional: false,
452
+ });
453
+ return { urlSegment: `*${paramName}`, params };
454
+ }
455
+ // Next.js style: [param] or [param]? - dynamic segment
456
+ if (segment.startsWith('[') && (segment.endsWith(']') || segment.endsWith(']?'))) {
457
+ const isOptional = segment.endsWith(']?');
458
+ const paramName = isOptional
459
+ ? segment.slice(1, -2) // Extract 'param' from '[param]?'
460
+ : segment.slice(1, -1); // Extract 'param' from '[param]'
461
+ params.push({
462
+ name: paramName,
463
+ index: paramIndex,
464
+ catchAll: false,
465
+ optional: isOptional,
466
+ });
467
+ return {
468
+ urlSegment: `:${paramName}${isOptional ? '?' : ''}`,
469
+ params,
470
+ };
471
+ }
472
+ // Legacy TanStack style: $param, $param?, $...rest, $...rest?
473
+ if (segment.startsWith('$')) {
474
+ let paramName = segment.slice(1);
475
+ const isCatchAll = paramName.startsWith('...');
476
+ if (isCatchAll) {
477
+ paramName = paramName.slice(3);
478
+ }
479
+ const isOptional = paramName.endsWith('?');
480
+ if (isOptional) {
481
+ paramName = paramName.slice(0, -1);
482
+ }
483
+ params.push({
484
+ name: paramName,
485
+ index: paramIndex,
486
+ catchAll: isCatchAll,
487
+ optional: isOptional,
488
+ });
489
+ const urlSegment = isCatchAll
490
+ ? `*${paramName}${isOptional ? '?' : ''}`
491
+ : `:${paramName}${isOptional ? '?' : ''}`;
492
+ return { urlSegment, params };
493
+ }
494
+ return { urlSegment: segment, params: [] };
495
+ }
496
+ parseRouteFile(filePath, relativePath, pathPrefix, baseName) {
497
+ const params = [];
498
+ let urlPath;
499
+ let isIndex = false;
500
+ if (baseName === 'index') {
501
+ // Index route
502
+ urlPath = this.prefixToUrl(pathPrefix);
503
+ isIndex = true;
504
+ }
505
+ else {
506
+ // Parse the base name (may have dot-separated segments)
507
+ // But preserve brackets: [param], [...slug], [[...slug]]
508
+ const segments = this.splitPreservingBrackets(baseName);
509
+ const urlSegments = [];
510
+ let paramIndex = pathPrefix.split('/').filter(Boolean).length;
511
+ for (const segment of segments) {
512
+ const parsed = this.parseSegment(segment, paramIndex);
513
+ params.push(...parsed.params);
514
+ urlSegments.push(parsed.urlSegment);
515
+ paramIndex++;
516
+ }
517
+ const base = this.prefixToUrl(pathPrefix);
518
+ urlPath = base === '/'
519
+ ? `/${urlSegments.join('/')}`
520
+ : `${base}/${urlSegments.join('/')}`;
521
+ }
522
+ // Detect special exports
523
+ const exports = detectRouteExports(filePath, false);
524
+ // Calculate priority
525
+ const priority = calculateRoutePriority(urlPath, params);
526
+ return {
527
+ filePath,
528
+ relativePath,
529
+ urlPath,
530
+ type: 'page',
531
+ params,
532
+ isIndex,
533
+ hasErrorComponent: exports.hasErrorComponent || undefined,
534
+ errorComponentExport: exports.errorComponentExport,
535
+ hasPendingComponent: exports.hasPendingComponent || undefined,
536
+ pendingComponentExport: exports.pendingComponentExport,
537
+ hasMeta: exports.hasMeta || undefined,
538
+ hasMiddleware: exports.hasMiddleware || undefined,
539
+ hasGenerateStaticParams: exports.hasGenerateStaticParams || undefined,
540
+ priority,
541
+ };
542
+ }
543
+ parseApiRouteFile(filePath, relativePath, urlPrefix, baseName) {
544
+ const params = [];
545
+ let urlPath;
546
+ let isIndex = false;
547
+ if (baseName === 'index') {
548
+ urlPath = urlPrefix;
549
+ isIndex = true;
550
+ }
551
+ else {
552
+ // Preserve brackets when splitting
553
+ const segments = this.splitPreservingBrackets(baseName);
554
+ const urlSegments = [];
555
+ let paramIndex = urlPrefix.split('/').filter(Boolean).length;
556
+ for (const segment of segments) {
557
+ const parsed = this.parseSegment(segment, paramIndex);
558
+ params.push(...parsed.params);
559
+ urlSegments.push(parsed.urlSegment);
560
+ paramIndex++;
561
+ }
562
+ urlPath = `${urlPrefix}/${urlSegments.join('/')}`;
563
+ }
564
+ // Detect HTTP method exports in the API route file
565
+ const exports = detectRouteExports(filePath, true);
566
+ // Calculate priority
567
+ const priority = calculateRoutePriority(urlPath, params);
568
+ return {
569
+ filePath,
570
+ relativePath,
571
+ urlPath,
572
+ type: 'api',
573
+ params,
574
+ methods: exports.methods ?? HTTP_METHODS, // Fall back to all methods if detection fails
575
+ isIndex,
576
+ hasMiddleware: exports.hasMiddleware || undefined,
577
+ priority,
578
+ };
579
+ }
580
+ isRouteFile(fileName) {
581
+ const ext = extname(fileName);
582
+ return this.extensions.includes(ext);
583
+ }
584
+ getBaseName(fileName) {
585
+ const ext = extname(fileName);
586
+ return basename(fileName, ext);
587
+ }
588
+ fileNameToUrlSegment(name) {
589
+ // Next.js style: [[...slug]] - optional catch-all
590
+ if (name.startsWith('[[...') && name.endsWith(']]')) {
591
+ const paramName = name.slice(5, -2);
592
+ return `*${paramName}?`;
593
+ }
594
+ // Next.js style: [...slug] - catch-all
595
+ if (name.startsWith('[...') && name.endsWith(']')) {
596
+ const paramName = name.slice(4, -1);
597
+ return `*${paramName}`;
598
+ }
599
+ // Next.js style: [param] - dynamic segment
600
+ if (name.startsWith('[') && name.endsWith(']')) {
601
+ const paramName = name.slice(1, -1);
602
+ return `:${paramName}`;
603
+ }
604
+ // Legacy TanStack style: $param
605
+ if (name.startsWith('$')) {
606
+ const paramName = name.slice(1);
607
+ if (paramName.startsWith('...')) {
608
+ return `*${paramName.slice(3)}`;
609
+ }
610
+ return `:${paramName}`;
611
+ }
612
+ return name;
613
+ }
614
+ /**
615
+ * Split a filename by dots, but preserve bracketed segments
616
+ * e.g., "posts.[id]" -> ["posts", "[id]"]
617
+ * e.g., "[...slug]" -> ["[...slug]"]
618
+ * e.g., "[[...path]]" -> ["[[...path]]"]
619
+ */
620
+ splitPreservingBrackets(name) {
621
+ const segments = [];
622
+ let current = '';
623
+ let bracketDepth = 0;
624
+ for (let i = 0; i < name.length; i++) {
625
+ const char = name[i];
626
+ if (char === '[') {
627
+ bracketDepth++;
628
+ current += char;
629
+ }
630
+ else if (char === ']') {
631
+ bracketDepth--;
632
+ current += char;
633
+ }
634
+ else if (char === '.' && bracketDepth === 0) {
635
+ // Split on dot only when not inside brackets
636
+ if (current) {
637
+ segments.push(current);
638
+ current = '';
639
+ }
640
+ }
641
+ else {
642
+ current += char;
643
+ }
644
+ }
645
+ // Add the last segment
646
+ if (current) {
647
+ segments.push(current);
648
+ }
649
+ return segments;
650
+ }
651
+ prefixToUrl(prefix) {
652
+ if (!prefix)
653
+ return '/';
654
+ const segments = prefix.split('/').filter(Boolean);
655
+ const urlSegments = segments.map((s) => this.fileNameToUrlSegment(s));
656
+ return '/' + urlSegments.join('/');
657
+ }
658
+ }
659
+ /**
660
+ * Convenience function to scan routes
661
+ */
662
+ export function scanRoutes(routesDir, options) {
663
+ const scanner = new RouteScanner({
664
+ routesDir,
665
+ ...options,
666
+ });
667
+ return scanner.scan();
668
+ }
669
+ /**
670
+ * Convert route tree to a flat list for debugging/display
671
+ */
672
+ export function flattenRoutes(tree) {
673
+ return [...tree.routes, ...tree.apiRoutes];
674
+ }
675
+ /**
676
+ * Get the parent layout for a route
677
+ */
678
+ export function findParentLayout(route, layouts) {
679
+ // Find the layout with the longest matching path prefix
680
+ let bestMatch = null;
681
+ let bestLength = -1;
682
+ for (const layout of layouts) {
683
+ if (route.urlPath.startsWith(layout.urlPath) && layout.urlPath.length > bestLength) {
684
+ bestMatch = layout;
685
+ bestLength = layout.urlPath.length;
686
+ }
687
+ }
688
+ return bestMatch;
689
+ }