@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,660 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import { FileWatcher } from './watcher.js';
|
|
6
|
+
import { RustBuilder } from './rust-builder.js';
|
|
7
|
+
import { ViteProxy } from './vite-proxy.js';
|
|
8
|
+
import { CodegenRunner } from './codegen-runner.js';
|
|
9
|
+
import { HotReloadServer } from './hot-reload.js';
|
|
10
|
+
import { RouteScannerRunner } from './route-scanner.js';
|
|
11
|
+
import { ProcessManager, IpcServer } from '../runtime/index.js';
|
|
12
|
+
import { cliLogger } from '../cli/utils/logger.js';
|
|
13
|
+
// Register tsx loader for TypeScript imports
|
|
14
|
+
// This must be called before any dynamic imports of .ts files
|
|
15
|
+
let tsxRegistered = false;
|
|
16
|
+
async function ensureTsxRegistered() {
|
|
17
|
+
if (tsxRegistered)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
const tsx = await import('tsx/esm/api');
|
|
21
|
+
tsx.register();
|
|
22
|
+
tsxRegistered = true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// tsx not available, TypeScript imports won't work
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* DevServer - Unified development server orchestrator
|
|
30
|
+
*
|
|
31
|
+
* Coordinates all development components:
|
|
32
|
+
* - Rust backend compilation with file watching
|
|
33
|
+
* - Vite frontend dev server
|
|
34
|
+
* - Automatic TypeScript binding generation
|
|
35
|
+
* - Hot reload signaling
|
|
36
|
+
*
|
|
37
|
+
* Workflow:
|
|
38
|
+
* 1. Initial build of Rust backend
|
|
39
|
+
* 2. Generate TypeScript bindings
|
|
40
|
+
* 3. Start Vite dev server
|
|
41
|
+
* 4. Start hot reload WebSocket server
|
|
42
|
+
* 5. Watch for file changes and orchestrate rebuilds
|
|
43
|
+
*/
|
|
44
|
+
export class DevServer extends EventEmitter {
|
|
45
|
+
constructor(config) {
|
|
46
|
+
super();
|
|
47
|
+
// Runtime components for Rust server + IPC
|
|
48
|
+
this.processManager = null;
|
|
49
|
+
this.ipcServer = null;
|
|
50
|
+
this.socketPath = '';
|
|
51
|
+
this.currentRouteTree = null;
|
|
52
|
+
this.registeredHandlers = new Map(); // handlerId -> filePath
|
|
53
|
+
// Timing
|
|
54
|
+
this.startTime = 0;
|
|
55
|
+
this.config = {
|
|
56
|
+
rustPort: 3000,
|
|
57
|
+
vitePort: 5173,
|
|
58
|
+
hotReloadPort: 3001,
|
|
59
|
+
logLevel: 'info',
|
|
60
|
+
release: false,
|
|
61
|
+
skipInitialBuild: false,
|
|
62
|
+
openBrowser: false,
|
|
63
|
+
...config,
|
|
64
|
+
};
|
|
65
|
+
// If binaryPath is provided, skip initial build
|
|
66
|
+
if (this.config.binaryPath) {
|
|
67
|
+
this.config.skipInitialBuild = true;
|
|
68
|
+
}
|
|
69
|
+
this.state = {
|
|
70
|
+
phase: 'starting',
|
|
71
|
+
rustReady: false,
|
|
72
|
+
viteReady: false,
|
|
73
|
+
lastError: null,
|
|
74
|
+
};
|
|
75
|
+
// Initialize components
|
|
76
|
+
this.watcher = new FileWatcher({
|
|
77
|
+
rootDir: this.config.projectDir,
|
|
78
|
+
});
|
|
79
|
+
this.rustBuilder = new RustBuilder({
|
|
80
|
+
projectDir: this.config.projectDir,
|
|
81
|
+
release: this.config.release,
|
|
82
|
+
bin: 'zap',
|
|
83
|
+
binaryPath: this.config.binaryPath,
|
|
84
|
+
});
|
|
85
|
+
this.viteProxy = new ViteProxy({
|
|
86
|
+
projectDir: this.config.projectDir,
|
|
87
|
+
port: this.config.vitePort,
|
|
88
|
+
});
|
|
89
|
+
this.codegenRunner = new CodegenRunner({
|
|
90
|
+
projectDir: this.config.projectDir,
|
|
91
|
+
codegenBinary: this.config.codegenBinaryPath,
|
|
92
|
+
});
|
|
93
|
+
this.hotReloadServer = new HotReloadServer({
|
|
94
|
+
port: this.config.hotReloadPort,
|
|
95
|
+
});
|
|
96
|
+
this.routeScanner = new RouteScannerRunner({
|
|
97
|
+
projectDir: this.config.projectDir,
|
|
98
|
+
});
|
|
99
|
+
this.setupEventHandlers();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Set up event handlers for all components
|
|
103
|
+
*/
|
|
104
|
+
setupEventHandlers() {
|
|
105
|
+
// File watcher events
|
|
106
|
+
this.watcher.on('rust', (event) => this.handleRustChange(event));
|
|
107
|
+
this.watcher.on('typescript', (event) => this.handleTypeScriptChange(event));
|
|
108
|
+
this.watcher.on('config', (event) => this.handleConfigChange(event));
|
|
109
|
+
this.watcher.on('error', (err) => this.log('error', `Watcher error: ${err.message}`));
|
|
110
|
+
// Rust builder events
|
|
111
|
+
this.rustBuilder.on('build-start', () => {
|
|
112
|
+
this.state.phase = 'rebuilding';
|
|
113
|
+
this.emit('rust-build-start');
|
|
114
|
+
});
|
|
115
|
+
this.rustBuilder.on('build-complete', (result) => {
|
|
116
|
+
this.emit('rust-build-complete', result);
|
|
117
|
+
});
|
|
118
|
+
this.rustBuilder.on('error', (msg) => {
|
|
119
|
+
this.log('error', msg);
|
|
120
|
+
});
|
|
121
|
+
this.rustBuilder.on('warning', (msg) => {
|
|
122
|
+
this.log('warn', msg);
|
|
123
|
+
});
|
|
124
|
+
// Vite events
|
|
125
|
+
this.viteProxy.on('ready', (port) => {
|
|
126
|
+
this.state.viteReady = true;
|
|
127
|
+
this.emit('vite-ready', port);
|
|
128
|
+
});
|
|
129
|
+
this.viteProxy.on('error', (err) => {
|
|
130
|
+
this.log('error', `Vite error: ${err.message}`);
|
|
131
|
+
});
|
|
132
|
+
this.viteProxy.on('hmr-update', () => {
|
|
133
|
+
this.emit('hmr-update');
|
|
134
|
+
});
|
|
135
|
+
// Hot reload events
|
|
136
|
+
this.hotReloadServer.on('client-connected', () => {
|
|
137
|
+
this.log('debug', 'Hot reload client connected');
|
|
138
|
+
});
|
|
139
|
+
// Codegen events
|
|
140
|
+
this.codegenRunner.on('complete', (result) => {
|
|
141
|
+
if (result.success) {
|
|
142
|
+
this.log('info', 'TypeScript bindings regenerated');
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Route scanner events
|
|
146
|
+
this.routeScanner.on('routes-changed', async (tree) => {
|
|
147
|
+
this.log('info', `Routes updated (${tree.routes.length} pages, ${tree.apiRoutes.length} API)`);
|
|
148
|
+
this.hotReloadServer.reload('routes', []);
|
|
149
|
+
// Restart Rust server to pick up new routes
|
|
150
|
+
try {
|
|
151
|
+
await this.restartRustServer();
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
155
|
+
this.log('error', `Failed to restart Rust server: ${message}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
this.routeScanner.on('error', (err) => {
|
|
159
|
+
this.log('warn', `Route scanner error: ${err.message}`);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Start the development server
|
|
164
|
+
*/
|
|
165
|
+
async start() {
|
|
166
|
+
this.startTime = Date.now();
|
|
167
|
+
cliLogger.header('ZapJS Dev Server');
|
|
168
|
+
try {
|
|
169
|
+
// Phase 1: Initial Rust build
|
|
170
|
+
if (!this.config.skipInitialBuild) {
|
|
171
|
+
await this.buildRust();
|
|
172
|
+
}
|
|
173
|
+
// Phase 2: Generate TypeScript bindings
|
|
174
|
+
await this.runCodegen();
|
|
175
|
+
// Phase 2.5: Scan routes
|
|
176
|
+
const routeTree = await this.scanRoutes();
|
|
177
|
+
this.currentRouteTree = routeTree;
|
|
178
|
+
// Phase 3: Start Rust HTTP server with IPC
|
|
179
|
+
await this.startRustServer(routeTree);
|
|
180
|
+
// Phase 4: Start other servers in parallel
|
|
181
|
+
await Promise.all([
|
|
182
|
+
this.startHotReloadServer(),
|
|
183
|
+
this.startViteServer(),
|
|
184
|
+
]);
|
|
185
|
+
// Phase 5: Start file watcher and route watcher
|
|
186
|
+
this.startWatcher();
|
|
187
|
+
await this.startRouteWatcher();
|
|
188
|
+
// Ready!
|
|
189
|
+
this.state.phase = 'ready';
|
|
190
|
+
this.printReadyMessage();
|
|
191
|
+
// Open browser if requested
|
|
192
|
+
if (this.config.openBrowser) {
|
|
193
|
+
this.openBrowser();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
this.state.phase = 'error';
|
|
198
|
+
this.state.lastError = err instanceof Error ? err.message : String(err);
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Stop all servers immediately
|
|
204
|
+
*/
|
|
205
|
+
async stop() {
|
|
206
|
+
if (this.state.phase === 'stopped')
|
|
207
|
+
return;
|
|
208
|
+
this.state.phase = 'stopped';
|
|
209
|
+
cliLogger.newline();
|
|
210
|
+
cliLogger.warn('Shutting down...');
|
|
211
|
+
// Kill Rust server first (most important)
|
|
212
|
+
if (this.processManager) {
|
|
213
|
+
this.processManager.stop();
|
|
214
|
+
this.processManager = null;
|
|
215
|
+
}
|
|
216
|
+
// Stop IPC
|
|
217
|
+
if (this.ipcServer) {
|
|
218
|
+
this.ipcServer.stop();
|
|
219
|
+
this.ipcServer = null;
|
|
220
|
+
}
|
|
221
|
+
// Stop other components
|
|
222
|
+
try {
|
|
223
|
+
this.watcher.stop();
|
|
224
|
+
this.viteProxy.stop();
|
|
225
|
+
this.hotReloadServer.stop();
|
|
226
|
+
this.routeScanner.stopWatching();
|
|
227
|
+
this.rustBuilder.cancel();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Ignore errors during cleanup
|
|
231
|
+
}
|
|
232
|
+
cliLogger.success('Goodbye!');
|
|
233
|
+
cliLogger.newline();
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Build Rust backend
|
|
238
|
+
*/
|
|
239
|
+
async buildRust() {
|
|
240
|
+
cliLogger.spinner('rust-build', 'Building Rust backend...');
|
|
241
|
+
const result = await this.rustBuilder.build();
|
|
242
|
+
if (result.success) {
|
|
243
|
+
const duration = (result.duration / 1000).toFixed(2);
|
|
244
|
+
cliLogger.succeedSpinner('rust-build', `Rust build complete (${duration}s)`);
|
|
245
|
+
this.state.rustReady = true;
|
|
246
|
+
if (result.warnings.length > 0) {
|
|
247
|
+
cliLogger.warn(`${result.warnings.length} warnings`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
cliLogger.failSpinner('rust-build', 'Rust build failed');
|
|
252
|
+
for (const error of result.errors) {
|
|
253
|
+
cliLogger.error(error);
|
|
254
|
+
}
|
|
255
|
+
throw new Error('Rust build failed');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Run codegen to generate TypeScript bindings
|
|
260
|
+
*/
|
|
261
|
+
async runCodegen() {
|
|
262
|
+
cliLogger.spinner('codegen', 'Generating TypeScript bindings...');
|
|
263
|
+
const success = await this.codegenRunner.run();
|
|
264
|
+
if (success) {
|
|
265
|
+
cliLogger.succeedSpinner('codegen', 'TypeScript bindings generated');
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
cliLogger.warn('Codegen skipped (binary not found)');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Scan routes directory and generate route tree
|
|
273
|
+
*/
|
|
274
|
+
async scanRoutes() {
|
|
275
|
+
if (!this.routeScanner.hasRoutesDir()) {
|
|
276
|
+
this.log('debug', 'No routes directory found');
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
cliLogger.spinner('routes', 'Scanning routes...');
|
|
280
|
+
const tree = await this.routeScanner.scan();
|
|
281
|
+
if (tree) {
|
|
282
|
+
cliLogger.succeedSpinner('routes', `Found ${tree.routes.length} pages, ${tree.apiRoutes.length} API routes`);
|
|
283
|
+
return tree;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
cliLogger.warn('Route scanning skipped');
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Start the Rust HTTP server with IPC for TypeScript handlers
|
|
292
|
+
*/
|
|
293
|
+
async startRustServer(routeTree) {
|
|
294
|
+
cliLogger.spinner('rust-server', 'Starting Rust HTTP server...');
|
|
295
|
+
try {
|
|
296
|
+
// Generate unique socket path for this dev session
|
|
297
|
+
this.socketPath = path.join(tmpdir(), `zap-dev-${Date.now()}-${Math.random().toString(36).substring(7)}.sock`);
|
|
298
|
+
// Create and start IPC server first
|
|
299
|
+
this.ipcServer = new IpcServer(this.socketPath);
|
|
300
|
+
await this.ipcServer.start();
|
|
301
|
+
this.log('debug', `IPC server listening on ${this.socketPath}`);
|
|
302
|
+
// Load and register route handlers
|
|
303
|
+
const routes = await this.loadRouteHandlers(routeTree);
|
|
304
|
+
console.log(`[dev-server] Loaded ${routes.length} route configurations`);
|
|
305
|
+
// Build Rust server configuration
|
|
306
|
+
const config = this.buildRustConfig(routes);
|
|
307
|
+
console.log(`[dev-server] Built Rust config with ${config.routes.length} routes`);
|
|
308
|
+
// Get binary path
|
|
309
|
+
const binaryPath = this.rustBuilder.getBinaryPath();
|
|
310
|
+
this.log('debug', `Using Rust binary: ${binaryPath}`);
|
|
311
|
+
// Create process manager and start Rust server
|
|
312
|
+
this.processManager = new ProcessManager(binaryPath, this.socketPath);
|
|
313
|
+
await this.processManager.start(config, this.config.logLevel || 'info');
|
|
314
|
+
this.state.rustReady = true;
|
|
315
|
+
cliLogger.succeedSpinner('rust-server', `Rust server ready on port ${this.config.rustPort}`);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
cliLogger.failSpinner('rust-server', 'Failed to start Rust server');
|
|
319
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
320
|
+
this.log('error', `Rust server error: ${message}`);
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Load TypeScript route handlers and register them with IPC server
|
|
326
|
+
*/
|
|
327
|
+
async loadRouteHandlers(routeTree) {
|
|
328
|
+
const routes = [];
|
|
329
|
+
if (!routeTree || !routeTree.apiRoutes || routeTree.apiRoutes.length === 0) {
|
|
330
|
+
console.log('[routes] No API routes to register');
|
|
331
|
+
return routes;
|
|
332
|
+
}
|
|
333
|
+
// Register tsx loader for TypeScript imports
|
|
334
|
+
await ensureTsxRegistered();
|
|
335
|
+
console.log(`[routes] Loading ${routeTree.apiRoutes.length} API route handlers...`);
|
|
336
|
+
for (const apiRoute of routeTree.apiRoutes) {
|
|
337
|
+
try {
|
|
338
|
+
// Construct the full file path
|
|
339
|
+
const routeFilePath = path.join(this.config.projectDir, 'routes', apiRoute.relativePath);
|
|
340
|
+
console.log(`[routes] Loading: ${routeFilePath}`);
|
|
341
|
+
// Dynamic import the route module using file URL for ESM compatibility
|
|
342
|
+
const fileUrl = pathToFileURL(routeFilePath).href;
|
|
343
|
+
const routeModule = await import(fileUrl);
|
|
344
|
+
console.log(`[routes] Loaded module, exports: ${Object.keys(routeModule).join(', ')}`);
|
|
345
|
+
// Check for each HTTP method handler
|
|
346
|
+
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
|
347
|
+
for (const method of methods) {
|
|
348
|
+
if (routeModule[method]) {
|
|
349
|
+
const handlerId = `handler_${method}_${apiRoute.urlPath.replace(/\//g, '_').replace(/:/g, '')}`;
|
|
350
|
+
console.log(`[routes] Registering handler: ${handlerId} for ${method} ${apiRoute.urlPath}`);
|
|
351
|
+
// Register handler with IPC server
|
|
352
|
+
this.ipcServer.registerHandler(handlerId, async (req) => {
|
|
353
|
+
console.log(`[handler] Received request: ${method} ${apiRoute.urlPath}`);
|
|
354
|
+
console.log(`[handler] Request data:`, JSON.stringify(req, null, 2));
|
|
355
|
+
try {
|
|
356
|
+
// Call the route handler
|
|
357
|
+
console.log(`[handler] Calling handler function...`);
|
|
358
|
+
const result = await routeModule[method](req);
|
|
359
|
+
console.log(`[handler] Handler returned:`, JSON.stringify(result, null, 2));
|
|
360
|
+
// Convert result to IPC response format
|
|
361
|
+
const response = this.formatHandlerResponse(result);
|
|
362
|
+
console.log(`[handler] Formatted response:`, JSON.stringify(response, null, 2));
|
|
363
|
+
return response;
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
367
|
+
const stack = err instanceof Error ? err.stack : '';
|
|
368
|
+
console.error(`[handler] ERROR in ${method} ${apiRoute.urlPath}: ${message}`);
|
|
369
|
+
console.error(`[handler] Stack: ${stack}`);
|
|
370
|
+
return {
|
|
371
|
+
status: 500,
|
|
372
|
+
headers: { 'content-type': 'application/json' },
|
|
373
|
+
body: JSON.stringify({ error: 'Internal Server Error', message }),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// Track handler mapping
|
|
378
|
+
this.registeredHandlers.set(handlerId, routeFilePath);
|
|
379
|
+
// Add route to config
|
|
380
|
+
routes.push({
|
|
381
|
+
method,
|
|
382
|
+
path: apiRoute.urlPath,
|
|
383
|
+
handler_id: handlerId,
|
|
384
|
+
is_typescript: true,
|
|
385
|
+
});
|
|
386
|
+
console.log(`[routes] ✓ Registered: ${method} ${apiRoute.urlPath} -> ${handlerId}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
392
|
+
console.error(`[routes] Failed to load ${apiRoute.relativePath}: ${message}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
console.log(`[routes] Total registered: ${routes.length} API handlers`);
|
|
396
|
+
return routes;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Format handler result into IPC response
|
|
400
|
+
*/
|
|
401
|
+
formatHandlerResponse(result) {
|
|
402
|
+
// Handle Response object
|
|
403
|
+
if (result instanceof Response) {
|
|
404
|
+
return {
|
|
405
|
+
status: result.status,
|
|
406
|
+
headers: Object.fromEntries(result.headers.entries()),
|
|
407
|
+
body: '', // Will need to await text() but keeping sync for now
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
// Handle string
|
|
411
|
+
if (typeof result === 'string') {
|
|
412
|
+
return {
|
|
413
|
+
status: 200,
|
|
414
|
+
headers: { 'content-type': 'text/plain' },
|
|
415
|
+
body: result,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// Handle object (JSON)
|
|
419
|
+
if (typeof result === 'object' && result !== null) {
|
|
420
|
+
return {
|
|
421
|
+
status: 200,
|
|
422
|
+
headers: { 'content-type': 'application/json' },
|
|
423
|
+
body: JSON.stringify(result),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
// Default
|
|
427
|
+
return {
|
|
428
|
+
status: 200,
|
|
429
|
+
headers: { 'content-type': 'text/plain' },
|
|
430
|
+
body: String(result),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Build Rust server configuration from routes
|
|
435
|
+
*/
|
|
436
|
+
buildRustConfig(routes) {
|
|
437
|
+
return {
|
|
438
|
+
port: this.config.rustPort,
|
|
439
|
+
hostname: '127.0.0.1',
|
|
440
|
+
ipc_socket_path: this.socketPath,
|
|
441
|
+
routes,
|
|
442
|
+
static_files: [],
|
|
443
|
+
middleware: {
|
|
444
|
+
enable_cors: true,
|
|
445
|
+
enable_logging: true,
|
|
446
|
+
enable_compression: false,
|
|
447
|
+
},
|
|
448
|
+
health_check_path: '/health',
|
|
449
|
+
metrics_path: '/metrics',
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Restart Rust server (called when routes change)
|
|
454
|
+
*/
|
|
455
|
+
async restartRustServer() {
|
|
456
|
+
this.log('info', 'Restarting Rust server...');
|
|
457
|
+
// Stop existing server
|
|
458
|
+
if (this.processManager) {
|
|
459
|
+
await this.processManager.stop();
|
|
460
|
+
}
|
|
461
|
+
if (this.ipcServer) {
|
|
462
|
+
await this.ipcServer.stop();
|
|
463
|
+
}
|
|
464
|
+
// Clear registered handlers
|
|
465
|
+
this.registeredHandlers.clear();
|
|
466
|
+
// Re-scan routes and restart
|
|
467
|
+
const routeTree = await this.scanRoutes();
|
|
468
|
+
this.currentRouteTree = routeTree;
|
|
469
|
+
await this.startRustServer(routeTree);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Start the route file watcher
|
|
473
|
+
*/
|
|
474
|
+
async startRouteWatcher() {
|
|
475
|
+
if (!this.routeScanner.hasRoutesDir()) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
await this.routeScanner.startWatching();
|
|
479
|
+
this.log('debug', 'Route watcher started');
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Start the hot reload WebSocket server
|
|
483
|
+
*/
|
|
484
|
+
async startHotReloadServer() {
|
|
485
|
+
await this.hotReloadServer.start();
|
|
486
|
+
this.log('debug', `Hot reload server on port ${this.config.hotReloadPort}`);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Start the Vite dev server
|
|
490
|
+
*/
|
|
491
|
+
async startViteServer() {
|
|
492
|
+
cliLogger.spinner('vite', 'Starting Vite dev server...');
|
|
493
|
+
try {
|
|
494
|
+
await this.viteProxy.start();
|
|
495
|
+
cliLogger.succeedSpinner('vite', `Vite ready on port ${this.viteProxy.getPort()}`);
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
cliLogger.warn('Vite not available (frontend only)');
|
|
499
|
+
this.log('debug', `Vite error: ${err}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Start the file watcher
|
|
504
|
+
*/
|
|
505
|
+
startWatcher() {
|
|
506
|
+
this.watcher.start();
|
|
507
|
+
this.log('debug', 'File watcher started');
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Handle Rust file changes
|
|
511
|
+
*/
|
|
512
|
+
async handleRustChange(event) {
|
|
513
|
+
const relativePath = path.relative(this.config.projectDir, event.path);
|
|
514
|
+
cliLogger.newline();
|
|
515
|
+
cliLogger.info(`[${event.type}] ${relativePath}`);
|
|
516
|
+
cliLogger.spinner('rust-rebuild', 'Rebuilding Rust...');
|
|
517
|
+
const result = await this.rustBuilder.build();
|
|
518
|
+
if (result.success) {
|
|
519
|
+
const duration = (result.duration / 1000).toFixed(2);
|
|
520
|
+
cliLogger.succeedSpinner('rust-rebuild', `Rust rebuild complete (${duration}s)`);
|
|
521
|
+
// Regenerate bindings
|
|
522
|
+
await this.codegenRunner.run();
|
|
523
|
+
// Signal hot reload
|
|
524
|
+
this.hotReloadServer.reload('rust', [relativePath]);
|
|
525
|
+
this.state.phase = 'ready';
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
cliLogger.failSpinner('rust-rebuild', 'Rust build failed');
|
|
529
|
+
for (const error of result.errors.slice(0, 3)) {
|
|
530
|
+
cliLogger.error(error);
|
|
531
|
+
}
|
|
532
|
+
// Notify clients of error
|
|
533
|
+
this.hotReloadServer.notifyError(result.errors.join('\n'));
|
|
534
|
+
this.state.phase = 'error';
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Handle TypeScript file changes
|
|
539
|
+
*/
|
|
540
|
+
async handleTypeScriptChange(event) {
|
|
541
|
+
const relativePath = path.relative(this.config.projectDir, event.path);
|
|
542
|
+
this.log('debug', `[${event.type}] ${relativePath}`);
|
|
543
|
+
// Vite HMR handles TypeScript changes automatically
|
|
544
|
+
// Just emit the event for logging
|
|
545
|
+
this.emit('typescript-change', event);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Handle config file changes
|
|
549
|
+
*/
|
|
550
|
+
async handleConfigChange(event) {
|
|
551
|
+
const relativePath = path.relative(this.config.projectDir, event.path);
|
|
552
|
+
cliLogger.newline();
|
|
553
|
+
cliLogger.warn(`[config] ${relativePath}`);
|
|
554
|
+
// For significant config changes, restart might be needed
|
|
555
|
+
if (relativePath.includes('Cargo.toml')) {
|
|
556
|
+
cliLogger.warn('Cargo.toml changed - rebuilding...');
|
|
557
|
+
await this.handleRustChange(event);
|
|
558
|
+
}
|
|
559
|
+
else if (relativePath.includes('vite.config')) {
|
|
560
|
+
cliLogger.warn('Vite config changed - restarting Vite...');
|
|
561
|
+
await this.viteProxy.restart();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Print the ready message
|
|
566
|
+
*/
|
|
567
|
+
printReadyMessage() {
|
|
568
|
+
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2);
|
|
569
|
+
cliLogger.newline();
|
|
570
|
+
cliLogger.success(`Dev server ready in ${elapsed}s`);
|
|
571
|
+
cliLogger.newline();
|
|
572
|
+
cliLogger.info('Servers:');
|
|
573
|
+
cliLogger.listItem(`API: http://127.0.0.1:${this.config.rustPort}`, '➜');
|
|
574
|
+
if (this.viteProxy.getPort()) {
|
|
575
|
+
cliLogger.listItem(`Frontend: http://127.0.0.1:${this.viteProxy.getPort()}`, '➜');
|
|
576
|
+
}
|
|
577
|
+
cliLogger.listItem(`Hot Reload: ws://127.0.0.1:${this.config.hotReloadPort}`, '➜');
|
|
578
|
+
cliLogger.newline();
|
|
579
|
+
cliLogger.info('Press Ctrl+C to stop');
|
|
580
|
+
cliLogger.newline();
|
|
581
|
+
// Show keyboard shortcuts
|
|
582
|
+
cliLogger.info('Shortcuts:');
|
|
583
|
+
console.log(' r - Rebuild Rust');
|
|
584
|
+
console.log(' c - Regenerate codegen');
|
|
585
|
+
console.log(' q - Quit');
|
|
586
|
+
cliLogger.newline();
|
|
587
|
+
// Setup keyboard input
|
|
588
|
+
this.setupKeyboardInput();
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Set up keyboard input handling
|
|
592
|
+
*/
|
|
593
|
+
setupKeyboardInput() {
|
|
594
|
+
if (process.stdin.isTTY) {
|
|
595
|
+
process.stdin.setRawMode(true);
|
|
596
|
+
process.stdin.resume();
|
|
597
|
+
process.stdin.setEncoding('utf8');
|
|
598
|
+
process.stdin.on('data', async (key) => {
|
|
599
|
+
// Ctrl+C
|
|
600
|
+
if (key === '\u0003') {
|
|
601
|
+
await this.stop();
|
|
602
|
+
process.exit(0);
|
|
603
|
+
}
|
|
604
|
+
// 'r' - rebuild
|
|
605
|
+
if (key === 'r') {
|
|
606
|
+
cliLogger.newline();
|
|
607
|
+
cliLogger.info('Manual rebuild triggered...');
|
|
608
|
+
await this.rustBuilder.build();
|
|
609
|
+
}
|
|
610
|
+
// 'c' - codegen
|
|
611
|
+
if (key === 'c') {
|
|
612
|
+
cliLogger.newline();
|
|
613
|
+
cliLogger.info('Regenerating bindings...');
|
|
614
|
+
await this.codegenRunner.run();
|
|
615
|
+
}
|
|
616
|
+
// 'q' - quit
|
|
617
|
+
if (key === 'q') {
|
|
618
|
+
await this.stop();
|
|
619
|
+
process.exit(0);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Open the browser
|
|
626
|
+
*/
|
|
627
|
+
openBrowser() {
|
|
628
|
+
const url = this.viteProxy.getPort()
|
|
629
|
+
? `http://127.0.0.1:${this.viteProxy.getPort()}`
|
|
630
|
+
: `http://127.0.0.1:${this.config.rustPort}`;
|
|
631
|
+
import('child_process').then(({ exec }) => {
|
|
632
|
+
const command = process.platform === 'darwin' ? 'open' :
|
|
633
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
634
|
+
exec(`${command} ${url}`);
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Log a message with the appropriate level
|
|
639
|
+
*/
|
|
640
|
+
log(level, message) {
|
|
641
|
+
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
642
|
+
const configLevel = levels[this.config.logLevel || 'info'];
|
|
643
|
+
const msgLevel = levels[level];
|
|
644
|
+
if (msgLevel >= configLevel) {
|
|
645
|
+
const prefix = {
|
|
646
|
+
debug: '[debug]',
|
|
647
|
+
info: '[info]',
|
|
648
|
+
warn: '[warn]',
|
|
649
|
+
error: '[error]',
|
|
650
|
+
}[level];
|
|
651
|
+
console.log(`${prefix} ${message}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Get current server state
|
|
656
|
+
*/
|
|
657
|
+
getState() {
|
|
658
|
+
return { ...this.state };
|
|
659
|
+
}
|
|
660
|
+
}
|