@zap-js/client 0.0.2 → 0.0.5

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 +84 -0
  33. package/dist/dev-server/route-scanner.js +113 -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 +660 -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,654 @@
1
+ /**
2
+ * Route tree code generator
3
+ *
4
+ * Generates TypeScript files for:
5
+ * - routeTree.ts - Route definitions and type-safe paths
6
+ * - routeManifest.json - JSON manifest for Rust server
7
+ * - routerConfig.ts - Client-side router configuration
8
+ */
9
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
10
+ import { join, relative } from 'path';
11
+ /**
12
+ * Generate route tree TypeScript file and manifest
13
+ */
14
+ export function generateRouteTree(options) {
15
+ const { outputDir, routeTree } = options;
16
+ // Ensure output directory exists
17
+ if (!existsSync(outputDir)) {
18
+ mkdirSync(outputDir, { recursive: true });
19
+ }
20
+ // Generate TypeScript route tree
21
+ const tsContent = generateTypeScriptRouteTree(routeTree, outputDir);
22
+ writeFileSync(join(outputDir, 'routeTree.ts'), tsContent);
23
+ // Generate JSON manifest for Rust
24
+ const manifest = generateManifest(routeTree);
25
+ writeFileSync(join(outputDir, 'routeManifest.json'), JSON.stringify(manifest, null, 2));
26
+ // Generate client router configuration
27
+ const routerConfig = generateRouterConfig(routeTree, outputDir);
28
+ writeFileSync(join(outputDir, 'routerConfig.ts'), routerConfig);
29
+ }
30
+ function generateTypeScriptRouteTree(tree, outputDir) {
31
+ // Check if any routes have special components
32
+ const hasAnyErrorComponents = tree.routes.some((r) => r.hasErrorComponent);
33
+ const hasAnyPendingComponents = tree.routes.some((r) => r.hasPendingComponent);
34
+ const hasAnyMeta = tree.routes.some((r) => r.hasMeta);
35
+ const lines = [
36
+ '/**',
37
+ ' * Auto-generated by @zapjs/router',
38
+ ' * DO NOT EDIT MANUALLY',
39
+ ' */',
40
+ '',
41
+ "import { lazy } from 'react';",
42
+ ];
43
+ // Import ErrorBoundary if any routes have error components
44
+ if (hasAnyErrorComponents) {
45
+ lines.push("import { ErrorBoundary } from '@zap-js/client';");
46
+ }
47
+ lines.push('');
48
+ // Generate layout imports
49
+ if (tree.layouts.length > 0) {
50
+ lines.push('// Layout component imports');
51
+ const layoutImports = generateLayoutImports(tree.layouts, outputDir);
52
+ lines.push(...layoutImports);
53
+ lines.push('');
54
+ }
55
+ // Generate route imports
56
+ lines.push('// Route component imports');
57
+ const imports = generateRouteImports(tree.routes, outputDir);
58
+ lines.push(...imports);
59
+ lines.push('');
60
+ // Generate route path types
61
+ lines.push('// Route path type definitions');
62
+ lines.push(generatePathTypes(tree));
63
+ lines.push('');
64
+ // Generate layout config
65
+ if (tree.layouts.length > 0) {
66
+ lines.push('// Layout configuration');
67
+ lines.push(generateLayoutConfig(tree, outputDir));
68
+ lines.push('');
69
+ }
70
+ // Generate route config
71
+ lines.push('// Route configuration');
72
+ lines.push(generateRouteConfig(tree, outputDir));
73
+ lines.push('');
74
+ // Generate API route config
75
+ if (tree.apiRoutes.length > 0) {
76
+ lines.push('// API route configuration');
77
+ lines.push(generateApiRouteConfig(tree));
78
+ lines.push('');
79
+ }
80
+ // Generate WebSocket route config
81
+ if (tree.wsRoutes.length > 0) {
82
+ lines.push('// WebSocket route configuration');
83
+ lines.push(generateWsRouteConfig(tree));
84
+ lines.push('');
85
+ }
86
+ // Generate helper functions
87
+ lines.push('// Helper functions');
88
+ lines.push(generateHelpers(tree));
89
+ return lines.join('\n');
90
+ }
91
+ function generateLayoutImports(layouts, outputDir) {
92
+ const lines = [];
93
+ for (const layout of layouts) {
94
+ const varName = layoutToVarName(layout);
95
+ const relativePath = getRelativeImportPath(outputDir, layout.filePath);
96
+ lines.push(`const ${varName} = lazy(() => import('${relativePath}'));`);
97
+ }
98
+ return lines;
99
+ }
100
+ function generateRouteImports(routes, outputDir) {
101
+ const lines = [];
102
+ for (const route of routes) {
103
+ if (route.type !== 'page')
104
+ continue;
105
+ const varName = routeToVarName(route);
106
+ const relativePath = getRelativeImportPath(outputDir, route.filePath);
107
+ // Main component import
108
+ lines.push(`const ${varName} = lazy(() => import('${relativePath}'));`);
109
+ // Error component import (if route has one)
110
+ if (route.hasErrorComponent) {
111
+ const errorVarName = routeToErrorVarName(route);
112
+ const exportName = route.errorComponentExport || 'errorComponent';
113
+ lines.push(`const ${errorVarName} = lazy(() => import('${relativePath}').then(m => ({ default: m.${exportName} })));`);
114
+ }
115
+ // Pending component import (if route has one)
116
+ if (route.hasPendingComponent) {
117
+ const pendingVarName = routeToPendingVarName(route);
118
+ const exportName = route.pendingComponentExport || 'pendingComponent';
119
+ lines.push(`const ${pendingVarName} = lazy(() => import('${relativePath}').then(m => ({ default: m.${exportName} })));`);
120
+ }
121
+ // Meta import (if route has one)
122
+ if (route.hasMeta) {
123
+ const metaVarName = routeToMetaVarName(route);
124
+ lines.push(`const ${metaVarName} = () => import('${relativePath}').then(m => m.meta);`);
125
+ }
126
+ // Middleware import (if route has one)
127
+ if (route.hasMiddleware) {
128
+ const middlewareVarName = routeToMiddlewareVarName(route);
129
+ lines.push(`const ${middlewareVarName} = () => import('${relativePath}').then(m => m.middleware);`);
130
+ }
131
+ }
132
+ return lines;
133
+ }
134
+ function generatePathTypes(tree) {
135
+ const allRoutes = [...tree.routes, ...tree.apiRoutes];
136
+ const wsPaths = tree.wsRoutes.map((r) => ` | '${r.urlPath}'`);
137
+ const paths = allRoutes.map((r) => ` | '${r.urlPath}'`);
138
+ let result = `export type RoutePath =\n${paths.join('\n')};`;
139
+ if (wsPaths.length > 0) {
140
+ result += `\n\nexport type WebSocketPath =\n${wsPaths.join('\n')};`;
141
+ }
142
+ return result;
143
+ }
144
+ function generateLayoutConfig(tree, outputDir) {
145
+ const lines = ['export const layouts = ['];
146
+ for (const layout of tree.layouts) {
147
+ const varName = layoutToVarName(layout);
148
+ lines.push(' {');
149
+ lines.push(` path: '${layout.urlPath}',`);
150
+ lines.push(` scopePath: '${layout.scopePath}',`);
151
+ lines.push(` component: ${varName},`);
152
+ if (layout.parentLayout) {
153
+ const parentLayout = tree.layouts.find(l => l.filePath === layout.parentLayout);
154
+ if (parentLayout) {
155
+ lines.push(` parentLayout: '${parentLayout.urlPath}',`);
156
+ }
157
+ }
158
+ lines.push(' },');
159
+ }
160
+ lines.push('] as const;');
161
+ return lines.join('\n');
162
+ }
163
+ function generateRouteConfig(tree, outputDir) {
164
+ const lines = ['export const routes = ['];
165
+ for (const route of tree.routes) {
166
+ if (route.type !== 'page')
167
+ continue;
168
+ const varName = routeToVarName(route);
169
+ lines.push(' {');
170
+ lines.push(` path: '${route.urlPath}',`);
171
+ lines.push(` component: ${varName},`);
172
+ lines.push(` isIndex: ${route.isIndex},`);
173
+ if (route.params.length > 0) {
174
+ // Include full param info with optional flag
175
+ const paramInfo = route.params.map((p) => ({
176
+ name: p.name,
177
+ optional: p.optional,
178
+ catchAll: p.catchAll,
179
+ }));
180
+ lines.push(` params: ${JSON.stringify(paramInfo)},`);
181
+ }
182
+ // Add priority
183
+ if (route.priority !== undefined) {
184
+ lines.push(` priority: ${route.priority},`);
185
+ }
186
+ // Add layout path if route has a parent layout
187
+ if (route.layoutPath) {
188
+ const layout = tree.layouts.find(l => l.filePath === route.layoutPath);
189
+ if (layout) {
190
+ lines.push(` layoutPath: '${layout.urlPath}',`);
191
+ }
192
+ }
193
+ // Add error component reference (TanStack style)
194
+ if (route.hasErrorComponent) {
195
+ const errorVarName = routeToErrorVarName(route);
196
+ lines.push(` errorComponent: ${errorVarName},`);
197
+ }
198
+ // Add pending component reference
199
+ if (route.hasPendingComponent) {
200
+ const pendingVarName = routeToPendingVarName(route);
201
+ lines.push(` pendingComponent: ${pendingVarName},`);
202
+ }
203
+ // Add meta loader reference
204
+ if (route.hasMeta) {
205
+ const metaVarName = routeToMetaVarName(route);
206
+ lines.push(` meta: ${metaVarName},`);
207
+ }
208
+ // Add middleware loader reference
209
+ if (route.hasMiddleware) {
210
+ const middlewareVarName = routeToMiddlewareVarName(route);
211
+ lines.push(` middleware: ${middlewareVarName},`);
212
+ }
213
+ lines.push(' },');
214
+ }
215
+ lines.push('] as const;');
216
+ return lines.join('\n');
217
+ }
218
+ function generateApiRouteConfig(tree) {
219
+ const lines = ['export const apiRoutes = ['];
220
+ for (const route of tree.apiRoutes) {
221
+ lines.push(' {');
222
+ lines.push(` path: '${route.urlPath}',`);
223
+ lines.push(` filePath: '${route.relativePath}',`);
224
+ lines.push(` isIndex: ${route.isIndex},`);
225
+ lines.push(` methods: ${JSON.stringify(route.methods || ['GET'])},`);
226
+ if (route.params.length > 0) {
227
+ const paramInfo = route.params.map((p) => ({
228
+ name: p.name,
229
+ optional: p.optional,
230
+ catchAll: p.catchAll,
231
+ }));
232
+ lines.push(` params: ${JSON.stringify(paramInfo)},`);
233
+ }
234
+ if (route.priority !== undefined) {
235
+ lines.push(` priority: ${route.priority},`);
236
+ }
237
+ if (route.hasMiddleware) {
238
+ lines.push(` hasMiddleware: true,`);
239
+ }
240
+ lines.push(' },');
241
+ }
242
+ lines.push('] as const;');
243
+ return lines.join('\n');
244
+ }
245
+ function generateWsRouteConfig(tree) {
246
+ const lines = ['export const wsRoutes = ['];
247
+ for (const route of tree.wsRoutes) {
248
+ lines.push(' {');
249
+ lines.push(` path: '${route.urlPath}',`);
250
+ lines.push(` filePath: '${route.relativePath}',`);
251
+ if (route.params.length > 0) {
252
+ const paramInfo = route.params.map((p) => ({
253
+ name: p.name,
254
+ optional: p.optional,
255
+ catchAll: p.catchAll,
256
+ }));
257
+ lines.push(` params: ${JSON.stringify(paramInfo)},`);
258
+ }
259
+ lines.push(' },');
260
+ }
261
+ lines.push('] as const;');
262
+ return lines.join('\n');
263
+ }
264
+ function generateHelpers(tree) {
265
+ const allRoutes = [...tree.routes, ...tree.apiRoutes];
266
+ // Build path helper function generators
267
+ const pathHelpers = [];
268
+ for (const route of allRoutes) {
269
+ if (route.params.length === 0)
270
+ continue;
271
+ const fnName = urlPathToFunctionName(route.urlPath);
272
+ // Handle optional params with ? suffix in type
273
+ const params = route.params.map((p) => {
274
+ const optional = p.optional ? '?' : '';
275
+ return `${p.name}${optional}: string`;
276
+ }).join(', ');
277
+ let template = route.urlPath;
278
+ for (const param of route.params) {
279
+ const optionalMarker = param.optional ? '?' : '';
280
+ if (param.catchAll) {
281
+ template = template.replace(`*${param.name}${optionalMarker}`, `\${${param.name} || ''}`);
282
+ }
283
+ else {
284
+ template = template.replace(`:${param.name}${optionalMarker}`, `\${${param.name} || ''}`);
285
+ }
286
+ }
287
+ pathHelpers.push(`export function ${fnName}(${params}): string {`);
288
+ pathHelpers.push(` return \`${template}\`.replace(/\\/+$/, '') || '/';`);
289
+ pathHelpers.push('}');
290
+ pathHelpers.push('');
291
+ }
292
+ return pathHelpers.join('\n');
293
+ }
294
+ function generateManifest(tree) {
295
+ return {
296
+ version: '1.0.0',
297
+ generatedAt: new Date().toISOString(),
298
+ routes: tree.routes.map((r) => ({
299
+ ...r,
300
+ // Remove absolute paths for security
301
+ filePath: r.relativePath,
302
+ })),
303
+ apiRoutes: tree.apiRoutes.map((r) => ({
304
+ ...r,
305
+ filePath: r.relativePath,
306
+ })),
307
+ };
308
+ }
309
+ function routeToVarName(route) {
310
+ // Convert path like /posts/:postId to PostsPostIdRoute
311
+ // Also handles [param] style (e.g., /blog/[slug] -> BlogSlugRoute)
312
+ let name = route.urlPath
313
+ .replace(/^\//, '')
314
+ .replace(/\//g, '_')
315
+ .replace(/:/g, '')
316
+ .replace(/\*/g, '')
317
+ .replace(/-/g, '_')
318
+ .replace(/\?/g, '')
319
+ .replace(/\[/g, '')
320
+ .replace(/\]/g, '');
321
+ if (!name)
322
+ name = 'Index';
323
+ // PascalCase
324
+ name = name
325
+ .split('_')
326
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
327
+ .join('');
328
+ return `${name}Route`;
329
+ }
330
+ function routeToErrorVarName(route) {
331
+ // Convert path like /posts/:postId to PostsPostIdErrorComponent
332
+ // Also handles [param] style (e.g., /blog/[slug] -> BlogSlugErrorComponent)
333
+ let name = route.urlPath
334
+ .replace(/^\//, '')
335
+ .replace(/\//g, '_')
336
+ .replace(/:/g, '')
337
+ .replace(/\*/g, '')
338
+ .replace(/-/g, '_')
339
+ .replace(/\?/g, '')
340
+ .replace(/\[/g, '')
341
+ .replace(/\]/g, '');
342
+ if (!name)
343
+ name = 'Index';
344
+ // PascalCase
345
+ name = name
346
+ .split('_')
347
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
348
+ .join('');
349
+ return `${name}ErrorComponent`;
350
+ }
351
+ function routeToPendingVarName(route) {
352
+ // Convert path like /posts/:postId to PostsPostIdPendingComponent
353
+ // Also handles [param] style (e.g., /blog/[slug] -> BlogSlugPendingComponent)
354
+ let name = route.urlPath
355
+ .replace(/^\//, '')
356
+ .replace(/\//g, '_')
357
+ .replace(/:/g, '')
358
+ .replace(/\*/g, '')
359
+ .replace(/-/g, '_')
360
+ .replace(/\?/g, '')
361
+ .replace(/\[/g, '')
362
+ .replace(/\]/g, '');
363
+ if (!name)
364
+ name = 'Index';
365
+ // PascalCase
366
+ name = name
367
+ .split('_')
368
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
369
+ .join('');
370
+ return `${name}PendingComponent`;
371
+ }
372
+ function routeToMetaVarName(route) {
373
+ // Also handles [param] style (e.g., /blog/[slug] -> BlogSlugMeta)
374
+ let name = route.urlPath
375
+ .replace(/^\//, '')
376
+ .replace(/\//g, '_')
377
+ .replace(/:/g, '')
378
+ .replace(/\*/g, '')
379
+ .replace(/-/g, '_')
380
+ .replace(/\?/g, '')
381
+ .replace(/\[/g, '')
382
+ .replace(/\]/g, '');
383
+ if (!name)
384
+ name = 'Index';
385
+ name = name
386
+ .split('_')
387
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
388
+ .join('');
389
+ return `${name}Meta`;
390
+ }
391
+ function routeToMiddlewareVarName(route) {
392
+ // Also handles [param] style (e.g., /blog/[slug] -> BlogSlugMiddleware)
393
+ let name = route.urlPath
394
+ .replace(/^\//, '')
395
+ .replace(/\//g, '_')
396
+ .replace(/:/g, '')
397
+ .replace(/\*/g, '')
398
+ .replace(/-/g, '_')
399
+ .replace(/\?/g, '')
400
+ .replace(/\[/g, '')
401
+ .replace(/\]/g, '');
402
+ if (!name)
403
+ name = 'Index';
404
+ name = name
405
+ .split('_')
406
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
407
+ .join('');
408
+ return `${name}Middleware`;
409
+ }
410
+ function layoutToVarName(layout) {
411
+ // Convert scope path to PascalCase layout name
412
+ // Also handles [param] style in layout paths
413
+ let name = layout.scopePath
414
+ .replace(/\//g, '_')
415
+ .replace(/-/g, '_')
416
+ .replace(/\[/g, '')
417
+ .replace(/\]/g, '');
418
+ if (!name)
419
+ name = 'Root';
420
+ name = name
421
+ .split('_')
422
+ .filter(Boolean)
423
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
424
+ .join('');
425
+ return `${name}Layout`;
426
+ }
427
+ function urlPathToFunctionName(urlPath) {
428
+ // Convert path like /posts/:postId to postsPostIdPath
429
+ // Also handles [param] style (e.g., /blog/[slug] -> blogSlugPath)
430
+ let name = urlPath
431
+ .replace(/^\//, '')
432
+ .replace(/\//g, '_')
433
+ .replace(/:/g, '')
434
+ .replace(/\*/g, '')
435
+ .replace(/-/g, '_')
436
+ .replace(/\?/g, '')
437
+ .replace(/\[/g, '')
438
+ .replace(/\]/g, '');
439
+ if (!name)
440
+ name = 'index';
441
+ // camelCase
442
+ const parts = name.split('_');
443
+ name = parts[0] + parts.slice(1).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('');
444
+ return `${name}Path`;
445
+ }
446
+ function generateParamsType(params) {
447
+ if (params.length === 0)
448
+ return 'never';
449
+ const entries = params.map((p) => `${p.name}: string`);
450
+ return `{ ${entries.join('; ')} }`;
451
+ }
452
+ function getRelativeImportPath(fromDir, toFile) {
453
+ let rel = relative(fromDir, toFile);
454
+ // Ensure it starts with ./
455
+ if (!rel.startsWith('.')) {
456
+ rel = './' + rel;
457
+ }
458
+ // Remove extension
459
+ rel = rel.replace(/\.(tsx?|jsx?)$/, '');
460
+ return rel;
461
+ }
462
+ /**
463
+ * Generate a Rust-compatible route manifest
464
+ */
465
+ export function generateRustManifest(tree) {
466
+ const routes = tree.apiRoutes.map((r) => ({
467
+ path: r.urlPath,
468
+ handler: pathToHandlerName(r.relativePath),
469
+ methods: r.methods || ['GET'],
470
+ params: r.params.map((p) => p.name),
471
+ }));
472
+ return JSON.stringify({ api_routes: routes }, null, 2);
473
+ }
474
+ function pathToHandlerName(relativePath) {
475
+ // Convert routes/api/users.[id].ts to api_users_id
476
+ return relativePath
477
+ .replace(/^routes\//, '')
478
+ .replace(/\.(tsx?|jsx?)$/, '')
479
+ .replace(/\//g, '_')
480
+ .replace(/\$/g, '')
481
+ .replace(/\[/g, '')
482
+ .replace(/\]/g, '')
483
+ .replace(/\./g, '_');
484
+ }
485
+ /**
486
+ * Generate client-side router configuration
487
+ * This provides the route definitions needed for client-side navigation
488
+ */
489
+ function generateRouterConfig(tree, outputDir) {
490
+ const lines = [
491
+ '/**',
492
+ ' * Auto-generated client router configuration',
493
+ ' * DO NOT EDIT MANUALLY',
494
+ ' */',
495
+ '',
496
+ "import { lazy, type ComponentType } from 'react';",
497
+ '',
498
+ ];
499
+ // Generate route interface
500
+ lines.push('export interface RouteDefinition {');
501
+ lines.push(' path: string;');
502
+ lines.push(' pattern: RegExp;');
503
+ lines.push(' paramNames: string[];');
504
+ lines.push(' component: React.LazyExoticComponent<ComponentType<any>>;');
505
+ lines.push(' isIndex: boolean;');
506
+ lines.push(' layoutPath?: string;');
507
+ lines.push(' errorComponent?: React.LazyExoticComponent<ComponentType<any>>;');
508
+ lines.push(' pendingComponent?: React.LazyExoticComponent<ComponentType<any>>;');
509
+ lines.push('}');
510
+ lines.push('');
511
+ // Generate component imports
512
+ lines.push('// Route component imports');
513
+ for (const route of tree.routes) {
514
+ if (route.type !== 'page')
515
+ continue;
516
+ const varName = routeToVarName(route);
517
+ const relativePath = getRelativeImportPath(outputDir, route.filePath);
518
+ lines.push(`const ${varName} = lazy(() => import('${relativePath}'));`);
519
+ if (route.hasErrorComponent) {
520
+ const errorVarName = routeToErrorVarName(route);
521
+ const exportName = route.errorComponentExport || 'errorComponent';
522
+ lines.push(`const ${errorVarName} = lazy(() => import('${relativePath}').then(m => ({ default: m.${exportName} })));`);
523
+ }
524
+ if (route.hasPendingComponent) {
525
+ const pendingVarName = routeToPendingVarName(route);
526
+ const exportName = route.pendingComponentExport || 'pendingComponent';
527
+ lines.push(`const ${pendingVarName} = lazy(() => import('${relativePath}').then(m => ({ default: m.${exportName} })));`);
528
+ }
529
+ }
530
+ lines.push('');
531
+ // Generate route pattern converter
532
+ lines.push('/**');
533
+ lines.push(' * Convert route path pattern to regex');
534
+ lines.push(' * :param -> named capture group');
535
+ lines.push(' * *param -> catch-all capture group');
536
+ lines.push(' */');
537
+ lines.push('function pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {');
538
+ lines.push(' const paramNames: string[] = [];');
539
+ lines.push(' let regexStr = path');
540
+ lines.push(" .replace(/\\//g, '\\\\/')");
541
+ lines.push(" .replace(/:\\w+\\??/g, (match) => {");
542
+ lines.push(" const isOptional = match.endsWith('?');");
543
+ lines.push(" const name = match.slice(1).replace('?', '');");
544
+ lines.push(' paramNames.push(name);');
545
+ lines.push(" return isOptional ? '([^/]*)?' : '([^/]+)';");
546
+ lines.push(' })');
547
+ lines.push(" .replace(/\\*\\w+\\??/g, (match) => {");
548
+ lines.push(" const isOptional = match.endsWith('?');");
549
+ lines.push(" const name = match.slice(1).replace('?', '');");
550
+ lines.push(' paramNames.push(name);');
551
+ lines.push(" return isOptional ? '(.*)?' : '(.+)';");
552
+ lines.push(' });');
553
+ lines.push(" return { pattern: new RegExp(`^${regexStr}$`), paramNames };");
554
+ lines.push('}');
555
+ lines.push('');
556
+ // Generate routes array
557
+ lines.push('// Route definitions with pre-compiled patterns');
558
+ lines.push('export const routeDefinitions: RouteDefinition[] = [');
559
+ for (const route of tree.routes) {
560
+ if (route.type !== 'page')
561
+ continue;
562
+ const varName = routeToVarName(route);
563
+ const { pattern, paramNames } = pathToRegexForCodegen(route.urlPath);
564
+ lines.push(' {');
565
+ lines.push(` path: '${route.urlPath}',`);
566
+ lines.push(` pattern: ${pattern},`);
567
+ lines.push(` paramNames: ${JSON.stringify(paramNames)},`);
568
+ lines.push(` component: ${varName},`);
569
+ lines.push(` isIndex: ${route.isIndex},`);
570
+ if (route.layoutPath) {
571
+ const layout = tree.layouts.find(l => l.filePath === route.layoutPath);
572
+ if (layout) {
573
+ lines.push(` layoutPath: '${layout.urlPath}',`);
574
+ }
575
+ }
576
+ if (route.hasErrorComponent) {
577
+ lines.push(` errorComponent: ${routeToErrorVarName(route)},`);
578
+ }
579
+ if (route.hasPendingComponent) {
580
+ lines.push(` pendingComponent: ${routeToPendingVarName(route)},`);
581
+ }
582
+ lines.push(' },');
583
+ }
584
+ lines.push('];');
585
+ lines.push('');
586
+ // Generate path type
587
+ const paths = tree.routes
588
+ .filter(r => r.type === 'page')
589
+ .map(r => ` | '${r.urlPath}'`);
590
+ lines.push(`export type RoutePath =\n${paths.join('\n')};`);
591
+ lines.push('');
592
+ // Generate params type map
593
+ lines.push('// Type-safe params for each route');
594
+ lines.push('export interface RouteParams {');
595
+ for (const route of tree.routes) {
596
+ if (route.type !== 'page')
597
+ continue;
598
+ if (route.params.length === 0) {
599
+ lines.push(` '${route.urlPath}': Record<string, never>;`);
600
+ }
601
+ else {
602
+ const params = route.params.map(p => `${p.name}: string`).join('; ');
603
+ lines.push(` '${route.urlPath}': { ${params} };`);
604
+ }
605
+ }
606
+ lines.push('}');
607
+ lines.push('');
608
+ // Generate path builder functions
609
+ lines.push('// Type-safe path builders');
610
+ for (const route of tree.routes) {
611
+ if (route.type !== 'page' || route.params.length === 0)
612
+ continue;
613
+ const fnName = urlPathToFunctionName(route.urlPath);
614
+ const params = route.params
615
+ .map(p => `${p.name}${p.optional ? '?' : ''}: string`)
616
+ .join(', ');
617
+ let template = route.urlPath;
618
+ for (const param of route.params) {
619
+ const optionalMarker = param.optional ? '?' : '';
620
+ if (param.catchAll) {
621
+ template = template.replace(`*${param.name}${optionalMarker}`, `\${${param.name} || ''}`);
622
+ }
623
+ else {
624
+ template = template.replace(`:${param.name}${optionalMarker}`, `\${${param.name} || ''}`);
625
+ }
626
+ }
627
+ lines.push(`export function ${fnName}(${params}): string {`);
628
+ lines.push(` return \`${template}\`.replace(/\\/+$/, '') || '/';`);
629
+ lines.push('}');
630
+ lines.push('');
631
+ }
632
+ return lines.join('\n');
633
+ }
634
+ /**
635
+ * Convert path pattern to regex at codegen time
636
+ */
637
+ function pathToRegexForCodegen(path) {
638
+ const paramNames = [];
639
+ let regexStr = path
640
+ .replace(/\//g, '\\/')
641
+ .replace(/:\w+\??/g, (match) => {
642
+ const isOptional = match.endsWith('?');
643
+ const name = match.slice(1).replace('?', '');
644
+ paramNames.push(name);
645
+ return isOptional ? '([^/]*)?' : '([^/]+)';
646
+ })
647
+ .replace(/\*\w+\??/g, (match) => {
648
+ const isOptional = match.endsWith('?');
649
+ const name = match.slice(1).replace('?', '');
650
+ paramNames.push(name);
651
+ return isOptional ? '(.*)?' : '(.+)';
652
+ });
653
+ return { pattern: `/^${regexStr}$/`, paramNames };
654
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @zapjs/router
3
+ *
4
+ * File-based routing for ZapJS (Next.js style conventions)
5
+ *
6
+ * File naming:
7
+ * - [param].tsx → /:param (dynamic segment)
8
+ * - [...slug].tsx → /*slug (catch-all)
9
+ * - [[...slug]].tsx → /*slug? (optional catch-all)
10
+ */
11
+ export type { RouteType, HttpMethod, RouteParam, ScannedRoute, LayoutRoute, RootRoute, WebSocketRoute, RouteTree, ScanOptions, CodegenOptions, WatchOptions, RouteMatch, RouteManifest, } from './types.js';
12
+ export { RouteScanner, scanRoutes, flattenRoutes, findParentLayout } from './scanner.js';
13
+ export { generateRouteTree, generateRustManifest } from './codegen.js';
14
+ export { generateEnhancedRouteTree } from './codegen-enhanced.js';
15
+ export { RouteWatcher, watchRoutes, watchAndRegenerate } from './watch.js';
16
+ export { buildSsg, buildPrerenderedRoutes, findSsgRoutes, collectStaticParams, buildPath, getOutputPath, writeSsgManifest, readSsgManifest, isStaticPath, getStaticRoute, type StaticParams, type GenerateStaticParamsFn, type PrerenderedRoute, type SsgManifest, type SsgOptions, } from './ssg.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @zapjs/router
3
+ *
4
+ * File-based routing for ZapJS (Next.js style conventions)
5
+ *
6
+ * File naming:
7
+ * - [param].tsx → /:param (dynamic segment)
8
+ * - [...slug].tsx → /*slug (catch-all)
9
+ * - [[...slug]].tsx → /*slug? (optional catch-all)
10
+ */
11
+ // Scanner
12
+ export { RouteScanner, scanRoutes, flattenRoutes, findParentLayout } from './scanner.js';
13
+ // Codegen
14
+ export { generateRouteTree, generateRustManifest } from './codegen.js';
15
+ export { generateEnhancedRouteTree } from './codegen-enhanced.js';
16
+ // Watcher
17
+ export { RouteWatcher, watchRoutes, watchAndRegenerate } from './watch.js';
18
+ // SSG (Static Site Generation)
19
+ export { buildSsg, buildPrerenderedRoutes, findSsgRoutes, collectStaticParams, buildPath, getOutputPath, writeSsgManifest, readSsgManifest, isStaticPath, getStaticRoute, } from './ssg.js';