@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.
- 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 +84 -0
- package/dist/dev-server/route-scanner.js +113 -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 +660 -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,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';
|