@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.
- package/README.md +310 -24
- package/bin/zap +0 -0
- package/bin/zap-codegen +0 -0
- package/dist/cli/commands/build.d.ts +11 -0
- package/dist/cli/commands/build.js +282 -0
- package/dist/cli/commands/codegen.d.ts +8 -0
- package/dist/cli/commands/codegen.js +95 -0
- package/dist/cli/commands/dev.d.ts +20 -0
- package/dist/cli/commands/dev.js +78 -0
- package/dist/cli/commands/new.d.ts +9 -0
- package/dist/cli/commands/new.js +307 -0
- package/dist/cli/commands/routes-old.d.ts +9 -0
- package/dist/cli/commands/routes-old.js +106 -0
- package/dist/cli/commands/routes.d.ts +11 -0
- package/dist/cli/commands/routes.js +280 -0
- package/dist/cli/commands/serve.d.ts +17 -0
- package/dist/cli/commands/serve.js +386 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +76 -0
- package/dist/cli/utils/index.d.ts +2 -0
- package/dist/cli/utils/index.js +2 -0
- package/dist/cli/utils/logger.d.ts +84 -0
- package/dist/cli/utils/logger.js +181 -0
- package/dist/cli/utils/port-finder.d.ts +8 -0
- package/dist/cli/utils/port-finder.js +48 -0
- package/dist/dev-server/codegen-runner.d.ts +41 -0
- package/dist/dev-server/codegen-runner.js +172 -0
- package/dist/dev-server/hot-reload.d.ts +72 -0
- package/dist/dev-server/hot-reload.js +280 -0
- package/dist/dev-server/index.d.ts +8 -0
- package/dist/dev-server/index.js +8 -0
- package/dist/dev-server/route-scanner.d.ts +71 -0
- package/dist/dev-server/route-scanner.js +114 -0
- package/dist/dev-server/rust-builder.d.ts +66 -0
- package/dist/dev-server/rust-builder.js +286 -0
- package/dist/dev-server/server.d.ts +147 -0
- package/dist/dev-server/server.js +658 -0
- package/dist/dev-server/vite-proxy.d.ts +56 -0
- package/dist/dev-server/vite-proxy.js +212 -0
- package/dist/dev-server/watcher.d.ts +48 -0
- package/dist/dev-server/watcher.js +127 -0
- package/dist/router/codegen-enhanced.d.ts +5 -0
- package/dist/router/codegen-enhanced.js +275 -0
- package/dist/router/codegen.d.ts +17 -0
- package/dist/router/codegen.js +654 -0
- package/dist/router/index.d.ts +16 -0
- package/dist/router/index.js +19 -0
- package/dist/router/scanner.d.ts +86 -0
- package/dist/router/scanner.js +689 -0
- package/dist/router/ssg.d.ts +115 -0
- package/dist/router/ssg.js +202 -0
- package/dist/router/types.d.ts +124 -0
- package/dist/router/types.js +9 -0
- package/dist/router/watch.d.ts +38 -0
- package/dist/router/watch.js +135 -0
- package/dist/runtime/csrf.d.ts +146 -0
- package/dist/runtime/csrf.js +166 -0
- package/dist/runtime/error-boundary.d.ts +129 -0
- package/dist/runtime/error-boundary.js +287 -0
- package/dist/runtime/hooks.d.ts +83 -0
- package/dist/runtime/hooks.js +96 -0
- package/dist/runtime/index.d.ts +229 -0
- package/dist/runtime/index.js +449 -0
- package/dist/runtime/ipc-client.d.ts +144 -0
- package/dist/runtime/ipc-client.js +621 -0
- package/dist/runtime/logger.d.ts +71 -0
- package/dist/runtime/logger.js +164 -0
- package/dist/runtime/middleware.d.ts +66 -0
- package/dist/runtime/middleware.js +114 -0
- package/dist/runtime/process-manager.d.ts +51 -0
- package/dist/runtime/process-manager.js +207 -0
- package/dist/runtime/router-simple.d.ts +98 -0
- package/dist/runtime/router-simple.js +330 -0
- package/dist/runtime/router.d.ts +103 -0
- package/dist/runtime/router.js +435 -0
- package/dist/runtime/rpc-client.d.ts +35 -0
- package/dist/runtime/rpc-client.js +140 -0
- package/dist/runtime/streaming-utils.d.ts +86 -0
- package/dist/runtime/streaming-utils.js +150 -0
- package/dist/runtime/types.d.ts +465 -0
- package/dist/runtime/types.js +60 -0
- package/dist/runtime/websockets-utils.d.ts +50 -0
- package/dist/runtime/websockets-utils.js +92 -0
- package/package.json +30 -20
- package/index.js +0 -29
- package/internal/cli/package.json +0 -46
- package/internal/cli/tsconfig.tsbuildinfo +0 -1
- package/internal/dev-server/node_modules/ora/index.d.ts +0 -332
- package/internal/dev-server/node_modules/ora/index.js +0 -416
- package/internal/dev-server/node_modules/ora/license +0 -9
- package/internal/dev-server/node_modules/ora/node_modules/string-width/index.d.ts +0 -36
- package/internal/dev-server/node_modules/ora/node_modules/string-width/index.js +0 -65
- package/internal/dev-server/node_modules/ora/node_modules/string-width/license +0 -9
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/README.md +0 -107
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.d.ts +0 -3
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.js +0 -4
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.mjs +0 -4
- package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/package.json +0 -46
- package/internal/dev-server/node_modules/ora/node_modules/string-width/package.json +0 -60
- package/internal/dev-server/node_modules/ora/node_modules/string-width/readme.md +0 -62
- package/internal/dev-server/node_modules/ora/package.json +0 -66
- package/internal/dev-server/node_modules/ora/readme.md +0 -325
- package/internal/dev-server/package.json +0 -41
- package/internal/router/package.json +0 -28
- package/internal/runtime/package.json +0 -41
- package/internal/runtime/src/error-boundary.tsx +0 -476
- package/internal/runtime/src/router-simple.tsx +0 -640
- package/internal/runtime/src/router.tsx +0 -771
- package/internal/runtime/tsconfig.tsbuildinfo +0 -1
- package/src/errors.js +0 -33
- package/src/logger.js +0 -10
- package/src/middleware.js +0 -32
- package/src/router.js +0 -41
- 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
|
+
}
|