@zap-js/client 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +310 -24
  2. package/bin/zap +0 -0
  3. package/bin/zap-codegen +0 -0
  4. package/dist/cli/commands/build.d.ts +11 -0
  5. package/dist/cli/commands/build.js +282 -0
  6. package/dist/cli/commands/codegen.d.ts +8 -0
  7. package/dist/cli/commands/codegen.js +95 -0
  8. package/dist/cli/commands/dev.d.ts +20 -0
  9. package/dist/cli/commands/dev.js +78 -0
  10. package/dist/cli/commands/new.d.ts +9 -0
  11. package/dist/cli/commands/new.js +307 -0
  12. package/dist/cli/commands/routes-old.d.ts +9 -0
  13. package/dist/cli/commands/routes-old.js +106 -0
  14. package/dist/cli/commands/routes.d.ts +11 -0
  15. package/dist/cli/commands/routes.js +280 -0
  16. package/dist/cli/commands/serve.d.ts +17 -0
  17. package/dist/cli/commands/serve.js +386 -0
  18. package/dist/cli/index.d.ts +2 -0
  19. package/dist/cli/index.js +76 -0
  20. package/dist/cli/utils/index.d.ts +2 -0
  21. package/dist/cli/utils/index.js +2 -0
  22. package/dist/cli/utils/logger.d.ts +84 -0
  23. package/dist/cli/utils/logger.js +181 -0
  24. package/dist/cli/utils/port-finder.d.ts +8 -0
  25. package/dist/cli/utils/port-finder.js +48 -0
  26. package/dist/dev-server/codegen-runner.d.ts +41 -0
  27. package/dist/dev-server/codegen-runner.js +172 -0
  28. package/dist/dev-server/hot-reload.d.ts +72 -0
  29. package/dist/dev-server/hot-reload.js +280 -0
  30. package/dist/dev-server/index.d.ts +8 -0
  31. package/dist/dev-server/index.js +8 -0
  32. package/dist/dev-server/route-scanner.d.ts +71 -0
  33. package/dist/dev-server/route-scanner.js +114 -0
  34. package/dist/dev-server/rust-builder.d.ts +66 -0
  35. package/dist/dev-server/rust-builder.js +286 -0
  36. package/dist/dev-server/server.d.ts +147 -0
  37. package/dist/dev-server/server.js +658 -0
  38. package/dist/dev-server/vite-proxy.d.ts +56 -0
  39. package/dist/dev-server/vite-proxy.js +212 -0
  40. package/dist/dev-server/watcher.d.ts +48 -0
  41. package/dist/dev-server/watcher.js +127 -0
  42. package/dist/router/codegen-enhanced.d.ts +5 -0
  43. package/dist/router/codegen-enhanced.js +275 -0
  44. package/dist/router/codegen.d.ts +17 -0
  45. package/dist/router/codegen.js +654 -0
  46. package/dist/router/index.d.ts +16 -0
  47. package/dist/router/index.js +19 -0
  48. package/dist/router/scanner.d.ts +86 -0
  49. package/dist/router/scanner.js +689 -0
  50. package/dist/router/ssg.d.ts +115 -0
  51. package/dist/router/ssg.js +202 -0
  52. package/dist/router/types.d.ts +124 -0
  53. package/dist/router/types.js +9 -0
  54. package/dist/router/watch.d.ts +38 -0
  55. package/dist/router/watch.js +135 -0
  56. package/dist/runtime/csrf.d.ts +146 -0
  57. package/dist/runtime/csrf.js +166 -0
  58. package/dist/runtime/error-boundary.d.ts +129 -0
  59. package/dist/runtime/error-boundary.js +287 -0
  60. package/dist/runtime/hooks.d.ts +83 -0
  61. package/dist/runtime/hooks.js +96 -0
  62. package/dist/runtime/index.d.ts +229 -0
  63. package/dist/runtime/index.js +449 -0
  64. package/dist/runtime/ipc-client.d.ts +144 -0
  65. package/dist/runtime/ipc-client.js +621 -0
  66. package/dist/runtime/logger.d.ts +71 -0
  67. package/dist/runtime/logger.js +164 -0
  68. package/dist/runtime/middleware.d.ts +66 -0
  69. package/dist/runtime/middleware.js +114 -0
  70. package/dist/runtime/process-manager.d.ts +51 -0
  71. package/dist/runtime/process-manager.js +207 -0
  72. package/dist/runtime/router-simple.d.ts +98 -0
  73. package/dist/runtime/router-simple.js +330 -0
  74. package/dist/runtime/router.d.ts +103 -0
  75. package/dist/runtime/router.js +435 -0
  76. package/dist/runtime/rpc-client.d.ts +35 -0
  77. package/dist/runtime/rpc-client.js +140 -0
  78. package/dist/runtime/streaming-utils.d.ts +86 -0
  79. package/dist/runtime/streaming-utils.js +150 -0
  80. package/dist/runtime/types.d.ts +465 -0
  81. package/dist/runtime/types.js +60 -0
  82. package/dist/runtime/websockets-utils.d.ts +50 -0
  83. package/dist/runtime/websockets-utils.js +92 -0
  84. package/package.json +30 -20
  85. package/index.js +0 -29
  86. package/internal/cli/package.json +0 -46
  87. package/internal/cli/tsconfig.tsbuildinfo +0 -1
  88. package/internal/dev-server/node_modules/ora/index.d.ts +0 -332
  89. package/internal/dev-server/node_modules/ora/index.js +0 -416
  90. package/internal/dev-server/node_modules/ora/license +0 -9
  91. package/internal/dev-server/node_modules/ora/node_modules/string-width/index.d.ts +0 -36
  92. package/internal/dev-server/node_modules/ora/node_modules/string-width/index.js +0 -65
  93. package/internal/dev-server/node_modules/ora/node_modules/string-width/license +0 -9
  94. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
  95. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/README.md +0 -107
  96. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.d.ts +0 -3
  97. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.js +0 -4
  98. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/index.mjs +0 -4
  99. package/internal/dev-server/node_modules/ora/node_modules/string-width/node_modules/emoji-regex/package.json +0 -46
  100. package/internal/dev-server/node_modules/ora/node_modules/string-width/package.json +0 -60
  101. package/internal/dev-server/node_modules/ora/node_modules/string-width/readme.md +0 -62
  102. package/internal/dev-server/node_modules/ora/package.json +0 -66
  103. package/internal/dev-server/node_modules/ora/readme.md +0 -325
  104. package/internal/dev-server/package.json +0 -41
  105. package/internal/router/package.json +0 -28
  106. package/internal/runtime/package.json +0 -41
  107. package/internal/runtime/src/error-boundary.tsx +0 -476
  108. package/internal/runtime/src/router-simple.tsx +0 -640
  109. package/internal/runtime/src/router.tsx +0 -771
  110. package/internal/runtime/tsconfig.tsbuildinfo +0 -1
  111. package/src/errors.js +0 -33
  112. package/src/logger.js +0 -10
  113. package/src/middleware.js +0 -32
  114. package/src/router.js +0 -41
  115. package/src/types.js +0 -39
@@ -0,0 +1,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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};