@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,280 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { cliLogger } from '../utils/logger.js';
|
|
5
|
+
// ESM equivalent of __dirname
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
/**
|
|
9
|
+
* Extract handler code from a route file
|
|
10
|
+
*/
|
|
11
|
+
function extractHandlerCode(filePath, method) {
|
|
12
|
+
try {
|
|
13
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
14
|
+
if (method) {
|
|
15
|
+
// Look for specific HTTP method handler
|
|
16
|
+
const patterns = [
|
|
17
|
+
// export const GET = ...
|
|
18
|
+
new RegExp(`export\\s+(?:const|let|var)\\s+${method}\\s*=\\s*([^;]+)`, 's'),
|
|
19
|
+
// export function GET() { ... }
|
|
20
|
+
new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\s*\\([^)]*\\)\\s*{([^}]+)}`, 's'),
|
|
21
|
+
// export async function GET() { ... }
|
|
22
|
+
new RegExp(`export\\s+async\\s+function\\s+${method}\\s*\\([^)]*\\)\\s*{([^}]+)}`, 's'),
|
|
23
|
+
];
|
|
24
|
+
for (const pattern of patterns) {
|
|
25
|
+
const match = content.match(pattern);
|
|
26
|
+
if (match) {
|
|
27
|
+
return match[0].trim();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Look for default export (page component)
|
|
33
|
+
const patterns = [
|
|
34
|
+
// export default function Component() { ... }
|
|
35
|
+
/export\s+default\s+(?:async\s+)?function\s+\w*\s*\([^)]*\)\s*{[^}]+}/s,
|
|
36
|
+
// const Component = () => { ... }; export default Component;
|
|
37
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*(?:\([^)]*\)|[^=]+)\s*=>\s*{[^}]+}.*export\s+default\s+\1/s,
|
|
38
|
+
// export default () => { ... }
|
|
39
|
+
/export\s+default\s+(?:\([^)]*\)|[^=]+)\s*=>\s*{[^}]+}/s,
|
|
40
|
+
];
|
|
41
|
+
for (const pattern of patterns) {
|
|
42
|
+
const match = content.match(pattern);
|
|
43
|
+
if (match) {
|
|
44
|
+
// Limit to first 10 lines for preview
|
|
45
|
+
const lines = match[0].split('\n').slice(0, 10);
|
|
46
|
+
if (lines.length >= 10) {
|
|
47
|
+
lines.push(' // ...');
|
|
48
|
+
}
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Enhanced route scanner that shows handler logic
|
|
61
|
+
*/
|
|
62
|
+
export async function routesCommand(options) {
|
|
63
|
+
try {
|
|
64
|
+
const projectDir = process.cwd();
|
|
65
|
+
const routesDir = resolve(options.routesDir || join(projectDir, 'routes'));
|
|
66
|
+
const outputDir = resolve(options.output || join(projectDir, 'src', 'generated'));
|
|
67
|
+
const showCode = options.showCode !== false; // Default to true
|
|
68
|
+
cliLogger.header('ZapJS Route Scanner');
|
|
69
|
+
// Check if routes directory exists
|
|
70
|
+
if (!existsSync(routesDir)) {
|
|
71
|
+
cliLogger.warn('No routes directory found');
|
|
72
|
+
cliLogger.keyValue('Expected', routesDir);
|
|
73
|
+
cliLogger.newline();
|
|
74
|
+
cliLogger.info('Create a routes/ directory with your route files to get started');
|
|
75
|
+
cliLogger.newline();
|
|
76
|
+
cliLogger.info('Next.js-style conventions:');
|
|
77
|
+
cliLogger.listItem('routes/index.tsx → /');
|
|
78
|
+
cliLogger.listItem('routes/about.tsx → /about');
|
|
79
|
+
cliLogger.listItem('routes/[postId].tsx → /:postId');
|
|
80
|
+
cliLogger.listItem('routes/posts/[id].tsx → /posts/:id');
|
|
81
|
+
cliLogger.listItem('routes/api/users.ts → /api/users');
|
|
82
|
+
cliLogger.listItem('routes/_layout.tsx → Layout wrapper');
|
|
83
|
+
cliLogger.listItem('routes/__root.tsx → Root layout');
|
|
84
|
+
cliLogger.newline();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Try to load the router package
|
|
88
|
+
cliLogger.spinner('loader', 'Loading route scanner...');
|
|
89
|
+
let router;
|
|
90
|
+
try {
|
|
91
|
+
// Path from dist/cli/commands/routes.js to dist/router/index.js
|
|
92
|
+
const routerPath = join(__dirname, '../../router/index.js');
|
|
93
|
+
if (existsSync(routerPath)) {
|
|
94
|
+
router = await import(routerPath);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
throw new Error(`Router module not found at ${routerPath}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
cliLogger.failSpinner('loader', 'Route scanner not found');
|
|
102
|
+
cliLogger.error('Error', error instanceof Error ? error.message : String(error));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
cliLogger.succeedSpinner('loader', 'Route scanner loaded');
|
|
106
|
+
// Scan routes
|
|
107
|
+
cliLogger.spinner('scan', `Scanning ${routesDir}...`);
|
|
108
|
+
const tree = router.scanRoutes(routesDir);
|
|
109
|
+
cliLogger.succeedSpinner('scan', 'Routes scanned');
|
|
110
|
+
// Output JSON if requested
|
|
111
|
+
if (options.json) {
|
|
112
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Print route summary with code
|
|
116
|
+
cliLogger.newline();
|
|
117
|
+
cliLogger.info('📁 Page Routes:');
|
|
118
|
+
cliLogger.newline();
|
|
119
|
+
if (tree.routes.length === 0) {
|
|
120
|
+
console.log(' (none)');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
for (const route of tree.routes) {
|
|
124
|
+
const params = route.params.length > 0
|
|
125
|
+
? ` [${route.params.map((p) => p.name).join(', ')}]`
|
|
126
|
+
: '';
|
|
127
|
+
const index = route.isIndex ? ' (index)' : '';
|
|
128
|
+
console.log(` ${route.urlPath}${params}${index}`);
|
|
129
|
+
console.log(` File: ${route.relativePath}`);
|
|
130
|
+
if (showCode) {
|
|
131
|
+
const code = extractHandlerCode(route.filePath);
|
|
132
|
+
if (code && options.verbose) {
|
|
133
|
+
console.log(' Handler:');
|
|
134
|
+
const codeLines = code.split('\n').map(line => ' ' + line);
|
|
135
|
+
console.log(codeLines.join('\n'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Show special exports
|
|
139
|
+
const features = [];
|
|
140
|
+
if (route.hasErrorComponent)
|
|
141
|
+
features.push('error boundary');
|
|
142
|
+
if (route.hasPendingComponent)
|
|
143
|
+
features.push('loading state');
|
|
144
|
+
if (route.hasMeta)
|
|
145
|
+
features.push('meta tags');
|
|
146
|
+
if (route.hasMiddleware)
|
|
147
|
+
features.push('middleware');
|
|
148
|
+
if (route.hasGenerateStaticParams)
|
|
149
|
+
features.push('SSG');
|
|
150
|
+
if (features.length > 0) {
|
|
151
|
+
console.log(` Features: ${features.join(', ')}`);
|
|
152
|
+
}
|
|
153
|
+
console.log();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
cliLogger.newline();
|
|
157
|
+
cliLogger.info('🌐 API Routes:');
|
|
158
|
+
cliLogger.newline();
|
|
159
|
+
if (tree.apiRoutes.length === 0) {
|
|
160
|
+
console.log(' (none)');
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
for (const route of tree.apiRoutes) {
|
|
164
|
+
const params = route.params.length > 0
|
|
165
|
+
? ` [${route.params.map((p) => p.name).join(', ')}]`
|
|
166
|
+
: '';
|
|
167
|
+
const methods = route.methods
|
|
168
|
+
? ` ${route.methods.join(' | ')}`
|
|
169
|
+
: '';
|
|
170
|
+
console.log(` ${route.urlPath}${params}`);
|
|
171
|
+
console.log(` File: ${route.relativePath}`);
|
|
172
|
+
console.log(` Methods:${methods}`);
|
|
173
|
+
if (showCode && route.methods) {
|
|
174
|
+
for (const method of route.methods) {
|
|
175
|
+
const code = extractHandlerCode(route.filePath, method);
|
|
176
|
+
if (code) {
|
|
177
|
+
if (options.verbose) {
|
|
178
|
+
console.log(` ${method} Handler:`);
|
|
179
|
+
const codeLines = code.split('\n').map(line => ' ' + line);
|
|
180
|
+
console.log(codeLines.join('\n'));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Just show first line
|
|
184
|
+
const firstLine = code.split('\n')[0];
|
|
185
|
+
console.log(` ${method}: ${firstLine.trim()}...`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Show features
|
|
191
|
+
const features = [];
|
|
192
|
+
if (route.hasMiddleware)
|
|
193
|
+
features.push('middleware');
|
|
194
|
+
if (features.length > 0) {
|
|
195
|
+
console.log(` Features: ${features.join(', ')}`);
|
|
196
|
+
}
|
|
197
|
+
console.log();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Show layouts
|
|
201
|
+
if (tree.layouts && tree.layouts.length > 0) {
|
|
202
|
+
cliLogger.newline();
|
|
203
|
+
cliLogger.info('📐 Layouts:');
|
|
204
|
+
cliLogger.newline();
|
|
205
|
+
for (const layout of tree.layouts) {
|
|
206
|
+
console.log(` ${layout.scopePath || '/'} (scope)`);
|
|
207
|
+
console.log(` File: ${layout.relativePath}`);
|
|
208
|
+
if (layout.parentLayout) {
|
|
209
|
+
console.log(` Parent: ${layout.parentLayout}`);
|
|
210
|
+
}
|
|
211
|
+
console.log();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Show WebSocket routes
|
|
215
|
+
if (tree.wsRoutes && tree.wsRoutes.length > 0) {
|
|
216
|
+
cliLogger.newline();
|
|
217
|
+
cliLogger.info('🔌 WebSocket Routes:');
|
|
218
|
+
cliLogger.newline();
|
|
219
|
+
for (const route of tree.wsRoutes) {
|
|
220
|
+
const params = route.params.length > 0
|
|
221
|
+
? ` [${route.params.map((p) => p.name).join(', ')}]`
|
|
222
|
+
: '';
|
|
223
|
+
console.log(` ${route.urlPath}${params}`);
|
|
224
|
+
console.log(` File: ${route.relativePath}`);
|
|
225
|
+
console.log();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Generate route tree files if output is specified
|
|
229
|
+
if (!options.json) {
|
|
230
|
+
cliLogger.spinner('generate', 'Generating route tree...');
|
|
231
|
+
if (!existsSync(outputDir)) {
|
|
232
|
+
mkdirSync(outputDir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
// Use enhanced route tree generation if available
|
|
235
|
+
if (router.generateEnhancedRouteTree) {
|
|
236
|
+
router.generateEnhancedRouteTree({
|
|
237
|
+
outputDir,
|
|
238
|
+
routeTree: tree,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
router.generateRouteTree({
|
|
243
|
+
outputDir,
|
|
244
|
+
routeTree: tree,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
cliLogger.succeedSpinner('generate', 'Route tree generated');
|
|
248
|
+
// Summary
|
|
249
|
+
const totalRoutes = tree.routes.length + tree.apiRoutes.length + (tree.wsRoutes?.length || 0);
|
|
250
|
+
cliLogger.newline();
|
|
251
|
+
cliLogger.success(`Found ${totalRoutes} total routes:`);
|
|
252
|
+
console.log(` - ${tree.routes.length} page routes`);
|
|
253
|
+
console.log(` - ${tree.apiRoutes.length} API routes`);
|
|
254
|
+
if (tree.wsRoutes?.length) {
|
|
255
|
+
console.log(` - ${tree.wsRoutes.length} WebSocket routes`);
|
|
256
|
+
}
|
|
257
|
+
if (tree.layouts?.length) {
|
|
258
|
+
console.log(` - ${tree.layouts.length} layouts`);
|
|
259
|
+
}
|
|
260
|
+
cliLogger.newline();
|
|
261
|
+
cliLogger.keyValue('Output', outputDir);
|
|
262
|
+
cliLogger.newline();
|
|
263
|
+
}
|
|
264
|
+
// Tips
|
|
265
|
+
if (!options.verbose && showCode) {
|
|
266
|
+
cliLogger.info('💡 Tip: Use --verbose flag to see full handler code');
|
|
267
|
+
cliLogger.newline();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
cliLogger.error('Route scanning failed');
|
|
272
|
+
if (error instanceof Error) {
|
|
273
|
+
cliLogger.error('Error details', error.message);
|
|
274
|
+
if (options.verbose) {
|
|
275
|
+
console.error(error.stack);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ServeOptions {
|
|
2
|
+
port?: string;
|
|
3
|
+
host?: string;
|
|
4
|
+
config?: string;
|
|
5
|
+
workers?: string;
|
|
6
|
+
logLevel?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Run production server
|
|
10
|
+
*
|
|
11
|
+
* This command now properly:
|
|
12
|
+
* 1. Starts an IPC server for TypeScript route handlers
|
|
13
|
+
* 2. Loads and registers route handlers from the route manifest
|
|
14
|
+
* 3. Passes proper --config and --socket args to the Rust binary
|
|
15
|
+
* 4. Coordinates both processes for graceful shutdown
|
|
16
|
+
*/
|
|
17
|
+
export declare function serveCommand(options: ServeOptions): Promise<void>;
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { findAvailablePort } from '../utils/port-finder.js';
|
|
7
|
+
import { IpcServer } from '../../runtime/index.js';
|
|
8
|
+
import { cliLogger } from '../utils/logger.js';
|
|
9
|
+
// Register tsx loader for TypeScript imports
|
|
10
|
+
let tsxRegistered = false;
|
|
11
|
+
async function ensureTsxRegistered() {
|
|
12
|
+
if (tsxRegistered)
|
|
13
|
+
return;
|
|
14
|
+
try {
|
|
15
|
+
const tsx = await import('tsx/esm/api');
|
|
16
|
+
tsx.register();
|
|
17
|
+
tsxRegistered = true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// tsx not available, try without it
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run production server
|
|
25
|
+
*
|
|
26
|
+
* This command now properly:
|
|
27
|
+
* 1. Starts an IPC server for TypeScript route handlers
|
|
28
|
+
* 2. Loads and registers route handlers from the route manifest
|
|
29
|
+
* 3. Passes proper --config and --socket args to the Rust binary
|
|
30
|
+
* 4. Coordinates both processes for graceful shutdown
|
|
31
|
+
*/
|
|
32
|
+
export async function serveCommand(options) {
|
|
33
|
+
try {
|
|
34
|
+
cliLogger.header('ZapJS Production Server');
|
|
35
|
+
// Determine working directory (dist or current)
|
|
36
|
+
const distDir = resolve('./dist');
|
|
37
|
+
const workDir = existsSync(join(distDir, 'bin', 'zap')) ? distDir : process.cwd();
|
|
38
|
+
let binPath = join(workDir, 'bin', 'zap');
|
|
39
|
+
// Check for binary in multiple locations
|
|
40
|
+
if (!existsSync(binPath)) {
|
|
41
|
+
const altPaths = [
|
|
42
|
+
join(process.cwd(), 'bin', 'zap'),
|
|
43
|
+
join(process.cwd(), 'target', 'release', 'zap'),
|
|
44
|
+
];
|
|
45
|
+
for (const altPath of altPaths) {
|
|
46
|
+
if (existsSync(altPath)) {
|
|
47
|
+
binPath = altPath;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!existsSync(binPath)) {
|
|
53
|
+
cliLogger.error('No production binary found');
|
|
54
|
+
cliLogger.info('Run `zap build` first to create a production build');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
// Load production config if available
|
|
58
|
+
let prodConfig = null;
|
|
59
|
+
const configPath = options.config || join(workDir, 'config.json');
|
|
60
|
+
if (existsSync(configPath)) {
|
|
61
|
+
try {
|
|
62
|
+
prodConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
63
|
+
cliLogger.success(`Loaded config from ${configPath}`);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
cliLogger.warn('Failed to parse config.json, using defaults');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await runProductionServer(binPath, options, workDir, prodConfig);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
cliLogger.error('Failed to start server');
|
|
73
|
+
if (error instanceof Error) {
|
|
74
|
+
cliLogger.error('Error details', error.message);
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function runProductionServer(binPath, options, workDir, prodConfig) {
|
|
80
|
+
const port = parseInt(options.port || prodConfig?.server?.port?.toString() || '3000');
|
|
81
|
+
const host = options.host || prodConfig?.server?.host || '0.0.0.0';
|
|
82
|
+
const logLevel = options.logLevel || prodConfig?.logging?.level || 'info';
|
|
83
|
+
// Find available port
|
|
84
|
+
cliLogger.spinner('port', `Checking port ${port}...`);
|
|
85
|
+
const availablePort = await findAvailablePort(port);
|
|
86
|
+
if (availablePort !== port) {
|
|
87
|
+
cliLogger.warn(`Port ${port} in use, using ${availablePort}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
cliLogger.succeedSpinner('port', `Port ${availablePort} available`);
|
|
91
|
+
}
|
|
92
|
+
// Generate unique socket path
|
|
93
|
+
const socketPath = join(tmpdir(), `zap-prod-${Date.now()}-${Math.random().toString(36).substring(7)}.sock`);
|
|
94
|
+
// Start IPC server for TypeScript handlers
|
|
95
|
+
cliLogger.spinner('ipc', 'Starting IPC server...');
|
|
96
|
+
const ipcServer = new IpcServer(socketPath);
|
|
97
|
+
await ipcServer.start();
|
|
98
|
+
cliLogger.succeedSpinner('ipc', 'IPC server started');
|
|
99
|
+
// Load and register route handlers
|
|
100
|
+
const routes = await loadRouteHandlers(ipcServer, workDir);
|
|
101
|
+
// Build Rust server configuration
|
|
102
|
+
const zapConfig = {
|
|
103
|
+
port: availablePort,
|
|
104
|
+
hostname: host,
|
|
105
|
+
ipc_socket_path: socketPath,
|
|
106
|
+
routes,
|
|
107
|
+
static_files: prodConfig?.static ? [{
|
|
108
|
+
prefix: prodConfig.static.prefix,
|
|
109
|
+
directory: prodConfig.static.directory,
|
|
110
|
+
}] : [],
|
|
111
|
+
middleware: {
|
|
112
|
+
enable_cors: true,
|
|
113
|
+
enable_logging: true,
|
|
114
|
+
enable_compression: true,
|
|
115
|
+
},
|
|
116
|
+
health_check_path: '/health',
|
|
117
|
+
};
|
|
118
|
+
// Also check for static directory in workDir
|
|
119
|
+
const staticDir = join(workDir, 'static');
|
|
120
|
+
if (existsSync(staticDir) && zapConfig.static_files.length === 0) {
|
|
121
|
+
zapConfig.static_files.push({
|
|
122
|
+
prefix: '/',
|
|
123
|
+
directory: staticDir,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Write config to temp file
|
|
127
|
+
const tempConfigPath = join(tmpdir(), `zap-config-${Date.now()}.json`);
|
|
128
|
+
writeFileSync(tempConfigPath, JSON.stringify(zapConfig, null, 2));
|
|
129
|
+
// Start Rust server
|
|
130
|
+
cliLogger.spinner('rust', 'Starting Rust HTTP server...');
|
|
131
|
+
const rustProcess = spawn(binPath, [
|
|
132
|
+
'--config', tempConfigPath,
|
|
133
|
+
'--socket', socketPath,
|
|
134
|
+
'--log-level', logLevel,
|
|
135
|
+
], {
|
|
136
|
+
cwd: workDir,
|
|
137
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
138
|
+
env: {
|
|
139
|
+
...process.env,
|
|
140
|
+
PORT: availablePort.toString(),
|
|
141
|
+
HOST: host,
|
|
142
|
+
RUST_LOG: logLevel,
|
|
143
|
+
ZAP_ENV: 'production',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
let started = false;
|
|
147
|
+
rustProcess.stdout?.on('data', (data) => {
|
|
148
|
+
const output = data.toString();
|
|
149
|
+
if (!started && (output.includes('listening') || output.includes('Server'))) {
|
|
150
|
+
started = true;
|
|
151
|
+
cliLogger.succeedSpinner('rust', 'Server started');
|
|
152
|
+
printServerInfo(host, availablePort, workDir, prodConfig, routes.length);
|
|
153
|
+
}
|
|
154
|
+
if (started) {
|
|
155
|
+
process.stdout.write(output);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
rustProcess.stderr?.on('data', (data) => {
|
|
159
|
+
const output = data.toString();
|
|
160
|
+
if (!started) {
|
|
161
|
+
// Don't fail immediately on stderr - Rust logs info to stderr sometimes
|
|
162
|
+
if (output.includes('error') || output.includes('Error')) {
|
|
163
|
+
cliLogger.failSpinner('rust', 'Server failed to start');
|
|
164
|
+
cliLogger.error(output);
|
|
165
|
+
cleanup(ipcServer, tempConfigPath);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
process.stderr.write(output);
|
|
170
|
+
});
|
|
171
|
+
rustProcess.on('error', (err) => {
|
|
172
|
+
cliLogger.failSpinner('rust', `Failed to start: ${err.message}`);
|
|
173
|
+
cleanup(ipcServer, tempConfigPath);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
|
176
|
+
rustProcess.on('exit', (code) => {
|
|
177
|
+
if (code !== 0 && code !== null) {
|
|
178
|
+
cliLogger.error(`Server exited with code ${code}`);
|
|
179
|
+
cleanup(ipcServer, tempConfigPath);
|
|
180
|
+
process.exit(code);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// If server doesn't output "listening", assume it started after a delay
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (!started) {
|
|
186
|
+
started = true;
|
|
187
|
+
cliLogger.succeedSpinner('rust', 'Server started');
|
|
188
|
+
printServerInfo(host, availablePort, workDir, prodConfig, routes.length);
|
|
189
|
+
}
|
|
190
|
+
}, 3000);
|
|
191
|
+
// Graceful shutdown
|
|
192
|
+
const shutdown = async () => {
|
|
193
|
+
cliLogger.warn('Shutting down...');
|
|
194
|
+
// Kill Rust process
|
|
195
|
+
if (!rustProcess.killed) {
|
|
196
|
+
rustProcess.kill('SIGTERM');
|
|
197
|
+
}
|
|
198
|
+
// Stop IPC server
|
|
199
|
+
await ipcServer.stop();
|
|
200
|
+
// Cleanup temp config
|
|
201
|
+
cleanup(null, tempConfigPath);
|
|
202
|
+
// Force kill after timeout
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
if (!rustProcess.killed) {
|
|
205
|
+
rustProcess.kill('SIGKILL');
|
|
206
|
+
}
|
|
207
|
+
process.exit(0);
|
|
208
|
+
}, 5000);
|
|
209
|
+
};
|
|
210
|
+
process.on('SIGINT', shutdown);
|
|
211
|
+
process.on('SIGTERM', shutdown);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Load TypeScript route handlers from route manifest
|
|
215
|
+
*/
|
|
216
|
+
async function loadRouteHandlers(ipcServer, workDir) {
|
|
217
|
+
const routes = [];
|
|
218
|
+
// Try multiple manifest locations
|
|
219
|
+
const manifestPaths = [
|
|
220
|
+
join(workDir, 'src', 'generated', 'routeManifest.json'),
|
|
221
|
+
join(process.cwd(), 'src', 'generated', 'routeManifest.json'),
|
|
222
|
+
];
|
|
223
|
+
let manifestPath = null;
|
|
224
|
+
for (const path of manifestPaths) {
|
|
225
|
+
if (existsSync(path)) {
|
|
226
|
+
manifestPath = path;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!manifestPath) {
|
|
231
|
+
cliLogger.info('No route manifest found - API routes will not be available');
|
|
232
|
+
return routes;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const manifestContent = readFileSync(manifestPath, 'utf-8');
|
|
236
|
+
const manifest = JSON.parse(manifestContent);
|
|
237
|
+
if (!manifest.apiRoutes || manifest.apiRoutes.length === 0) {
|
|
238
|
+
cliLogger.info('No API routes in manifest');
|
|
239
|
+
return routes;
|
|
240
|
+
}
|
|
241
|
+
// Register tsx for TypeScript imports
|
|
242
|
+
await ensureTsxRegistered();
|
|
243
|
+
cliLogger.spinner('routes', `Loading ${manifest.apiRoutes.length} API route handlers...`);
|
|
244
|
+
for (const apiRoute of manifest.apiRoutes) {
|
|
245
|
+
try {
|
|
246
|
+
// Find route file
|
|
247
|
+
const routePaths = [
|
|
248
|
+
join(workDir, 'routes', apiRoute.relativePath),
|
|
249
|
+
join(process.cwd(), 'routes', apiRoute.relativePath),
|
|
250
|
+
];
|
|
251
|
+
let routeFilePath = null;
|
|
252
|
+
for (const rp of routePaths) {
|
|
253
|
+
if (existsSync(rp)) {
|
|
254
|
+
routeFilePath = rp;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (!routeFilePath) {
|
|
259
|
+
cliLogger.warn(`[routes] Route file not found: ${apiRoute.relativePath}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
// Dynamic import the route module
|
|
263
|
+
const fileUrl = pathToFileURL(routeFilePath).href;
|
|
264
|
+
const routeModule = await import(fileUrl);
|
|
265
|
+
// Register each HTTP method handler
|
|
266
|
+
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
|
267
|
+
for (const method of methods) {
|
|
268
|
+
if (routeModule[method]) {
|
|
269
|
+
const handlerId = `handler_${method}_${apiRoute.urlPath.replace(/\//g, '_').replace(/:/g, '')}`;
|
|
270
|
+
// Register handler with IPC server
|
|
271
|
+
ipcServer.registerHandler(handlerId, async (req) => {
|
|
272
|
+
try {
|
|
273
|
+
const result = await routeModule[method](req);
|
|
274
|
+
return formatHandlerResponse(result);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
278
|
+
cliLogger.error(`[handler] ERROR in ${method} ${apiRoute.urlPath}:`, message);
|
|
279
|
+
return {
|
|
280
|
+
status: 500,
|
|
281
|
+
headers: { 'content-type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({ error: 'Internal Server Error', message }),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
routes.push({
|
|
287
|
+
method,
|
|
288
|
+
path: apiRoute.urlPath,
|
|
289
|
+
handler_id: handlerId,
|
|
290
|
+
is_typescript: true,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
297
|
+
cliLogger.warn(`[routes] Failed to load ${apiRoute.relativePath}: ${message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
cliLogger.succeedSpinner('routes', `Loaded ${routes.length} API route handlers`);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
304
|
+
cliLogger.warn(`Failed to load route manifest: ${message}`);
|
|
305
|
+
}
|
|
306
|
+
return routes;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Format handler result into IPC response
|
|
310
|
+
*/
|
|
311
|
+
function formatHandlerResponse(result) {
|
|
312
|
+
// Handle Response object
|
|
313
|
+
if (result instanceof Response) {
|
|
314
|
+
return {
|
|
315
|
+
status: result.status,
|
|
316
|
+
headers: Object.fromEntries(result.headers.entries()),
|
|
317
|
+
body: '',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
// Handle string
|
|
321
|
+
if (typeof result === 'string') {
|
|
322
|
+
return {
|
|
323
|
+
status: 200,
|
|
324
|
+
headers: { 'content-type': 'text/plain' },
|
|
325
|
+
body: result,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// Handle object (JSON)
|
|
329
|
+
if (typeof result === 'object' && result !== null) {
|
|
330
|
+
return {
|
|
331
|
+
status: 200,
|
|
332
|
+
headers: { 'content-type': 'application/json' },
|
|
333
|
+
body: JSON.stringify(result),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// Default
|
|
337
|
+
return {
|
|
338
|
+
status: 200,
|
|
339
|
+
headers: { 'content-type': 'text/plain' },
|
|
340
|
+
body: String(result),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Cleanup temp files
|
|
345
|
+
*/
|
|
346
|
+
function cleanup(ipcServer, configPath) {
|
|
347
|
+
if (ipcServer) {
|
|
348
|
+
try {
|
|
349
|
+
ipcServer.stop();
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Ignore cleanup errors
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (configPath && existsSync(configPath)) {
|
|
356
|
+
try {
|
|
357
|
+
unlinkSync(configPath);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Ignore cleanup errors
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function printServerInfo(host, port, workDir, config, routeCount) {
|
|
365
|
+
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
|
|
366
|
+
cliLogger.newline();
|
|
367
|
+
cliLogger.success('Production server running');
|
|
368
|
+
cliLogger.newline();
|
|
369
|
+
cliLogger.info('Endpoints:');
|
|
370
|
+
cliLogger.listItem(`http://${displayHost}:${port}`, '➜');
|
|
371
|
+
if (host === '0.0.0.0') {
|
|
372
|
+
cliLogger.listItem(`http://0.0.0.0:${port} (all interfaces)`, '➜');
|
|
373
|
+
}
|
|
374
|
+
if (routeCount !== undefined && routeCount > 0) {
|
|
375
|
+
cliLogger.newline();
|
|
376
|
+
cliLogger.keyValue('API routes', `${routeCount} handlers registered`);
|
|
377
|
+
}
|
|
378
|
+
if (config?.static) {
|
|
379
|
+
cliLogger.keyValue('Static files', config.static.directory);
|
|
380
|
+
}
|
|
381
|
+
cliLogger.newline();
|
|
382
|
+
cliLogger.keyValue('Working dir', workDir);
|
|
383
|
+
cliLogger.newline();
|
|
384
|
+
cliLogger.info('Press Ctrl+C to stop');
|
|
385
|
+
cliLogger.newline();
|
|
386
|
+
}
|