vector-framework 0.9.5 → 0.9.7

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/src/cli/index.ts CHANGED
@@ -36,6 +36,10 @@ const { values, positionals } = parseArgs({
36
36
  type: "boolean",
37
37
  default: true,
38
38
  },
39
+ config: {
40
+ type: "string",
41
+ short: "c",
42
+ },
39
43
  },
40
44
  strict: true,
41
45
  allowPositionals: true,
@@ -52,80 +56,97 @@ async function runDev() {
52
56
  let server: any = null;
53
57
  let vector: any = null;
54
58
 
55
- async function startServer() {
56
- try {
57
- // Load configuration using ConfigLoader
58
- const configLoader = new ConfigLoader();
59
- const config = await configLoader.load();
60
- const configSource = configLoader.getConfigSource();
61
-
62
- // Merge CLI options with loaded config
63
- // Only use CLI values if config doesn't have them
64
- config.port = config.port ?? Number.parseInt(values.port as string);
65
- config.hostname = config.hostname ?? (values.host as string);
66
- config.routesDir = config.routesDir ?? (values.routes as string);
67
- config.development = config.development ?? isDev;
68
- config.autoDiscover = true; // Always auto-discover routes
69
-
70
- // Apply CLI CORS option if not set in config
71
- if (!config.cors && values.cors) {
72
- config.cors = {
73
- origin: "*",
74
- credentials: true,
75
- allowHeaders: "Content-Type, Authorization",
76
- allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
77
- exposeHeaders: "Authorization",
78
- maxAge: 86400,
79
- };
80
- }
59
+ async function startServer(): Promise<{ server: any; vector: any; config: any }> {
60
+ // Create a timeout promise that rejects after 10 seconds
61
+ const timeoutPromise = new Promise<never>((_, reject) => {
62
+ setTimeout(() => {
63
+ reject(new Error("Server startup timed out (10s)"));
64
+ }, 10000);
65
+ });
81
66
 
82
- // Get Vector instance and configure handlers
83
- vector = getVectorInstance();
67
+ // Create the actual server start promise
68
+ const serverStartPromise = (async (): Promise<{ server: any; vector: any; config: any }> => {
69
+ try {
70
+ // Load configuration using ConfigLoader
71
+ const configLoader = new ConfigLoader(values.config as string | undefined);
72
+ const config = await configLoader.load();
73
+ const configSource = configLoader.getConfigSource();
74
+
75
+ // Merge CLI options with loaded config
76
+ // Only use CLI values if config doesn't have them
77
+ config.port = config.port ?? Number.parseInt(values.port as string);
78
+ config.hostname = config.hostname ?? (values.host as string);
79
+ config.routesDir = config.routesDir ?? (values.routes as string);
80
+ config.development = config.development ?? isDev;
81
+ config.autoDiscover = true; // Always auto-discover routes
82
+
83
+ // Apply CLI CORS option if not set in config
84
+ if (!config.cors && values.cors) {
85
+ config.cors = {
86
+ origin: "*",
87
+ credentials: true,
88
+ allowHeaders: "Content-Type, Authorization",
89
+ allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
90
+ exposeHeaders: "Authorization",
91
+ maxAge: 86400,
92
+ };
93
+ }
84
94
 
85
- // Load and set auth handler if configured
86
- const authHandler = await configLoader.loadAuthHandler();
87
- if (authHandler) {
88
- vector.setProtectedHandler(authHandler);
89
- }
95
+ // Get Vector instance and configure handlers
96
+ vector = getVectorInstance();
90
97
 
91
- // Load and set cache handler if configured
92
- const cacheHandler = await configLoader.loadCacheHandler();
93
- if (cacheHandler) {
94
- vector.setCacheHandler(cacheHandler);
95
- }
98
+ // Load and set auth handler if configured
99
+ const authHandler = await configLoader.loadAuthHandler();
100
+ if (authHandler) {
101
+ vector.setProtectedHandler(authHandler);
102
+ }
103
+
104
+ // Load and set cache handler if configured
105
+ const cacheHandler = await configLoader.loadCacheHandler();
106
+ if (cacheHandler) {
107
+ vector.setCacheHandler(cacheHandler);
108
+ }
96
109
 
97
- // Start the server
98
- server = await vector.startServer(config);
110
+ // Start the server
111
+ server = await vector.startServer(config);
99
112
 
100
- const gray = "\x1b[90m";
101
- const reset = "\x1b[0m";
102
- const cyan = "\x1b[36m";
103
- const green = "\x1b[32m";
113
+ // Verify the server is actually running
114
+ if (!server || !server.port) {
115
+ throw new Error("Server started but is not responding correctly");
116
+ }
104
117
 
105
- console.log(
106
- ` ${gray}Config${reset} ${
107
- configSource === "user" ? "User config loaded" : "Using defaults"
108
- }`
109
- );
110
- console.log(` ${gray}Routes${reset} ${config.routesDir}`);
111
- if (isDev && values.watch) {
112
- console.log(` ${gray}Watching${reset} All project files`);
118
+ const gray = "\x1b[90m";
119
+ const reset = "\x1b[0m";
120
+ const cyan = "\x1b[36m";
121
+ const green = "\x1b[32m";
122
+
123
+ console.log(
124
+ ` ${gray}Config${reset} ${
125
+ configSource === "user" ? "User config loaded" : "Using defaults"
126
+ }`
127
+ );
128
+ console.log(` ${gray}Routes${reset} ${config.routesDir}`);
129
+ if (isDev && values.watch) {
130
+ console.log(` ${gray}Watching${reset} All project files`);
131
+ }
132
+ console.log(
133
+ ` ${gray}CORS${reset} ${config.cors ? "Enabled" : "Disabled"}`
134
+ );
135
+ console.log(
136
+ ` ${gray}Mode${reset} ${config.development ? "Development" : "Production"}\n`
137
+ );
138
+ console.log(
139
+ ` ${green}Ready${reset} → ${cyan}http://${config.hostname}:${config.port}${reset}\n`
140
+ );
141
+
142
+ return { server, vector, config };
143
+ } catch (error) {
144
+ throw error;
113
145
  }
114
- console.log(
115
- ` ${gray}CORS${reset} ${config.cors ? "Enabled" : "Disabled"}`
116
- );
117
- console.log(
118
- ` ${gray}Mode${reset} ${config.development ? "Development" : "Production"}\n`
119
- );
120
- console.log(
121
- ` ${green}Ready${reset} → ${cyan}http://${config.hostname}:${config.port}${reset}\n`
122
- );
146
+ })();
123
147
 
124
- return { server, vector, config };
125
- } catch (error) {
126
- console.error("[ERROR] Failed to start server:", error);
127
- throw error;
128
- }
148
+ // Race between server startup and timeout
149
+ return await Promise.race([serverStartPromise, timeoutPromise]);
129
150
  }
130
151
 
131
152
  try {
@@ -196,8 +217,9 @@ async function runDev() {
196
217
  const result = await startServer();
197
218
  server = result.server;
198
219
  vector = result.vector;
199
- } catch (error) {
200
- console.error(" Failed to reload server:", error);
220
+ } catch (error: any) {
221
+ console.error("\n[Reload Error]", error.message || error);
222
+ // Don't exit the process on reload failures, just continue watching
201
223
  } finally {
202
224
  // Reset flag after a delay
203
225
  setTimeout(() => {
@@ -211,8 +233,26 @@ async function runDev() {
211
233
  console.warn(" ⚠️ File watching not available");
212
234
  }
213
235
  }
214
- } catch (error) {
215
- console.error("[ERROR] Failed to start server:", error);
236
+ } catch (error: any) {
237
+ const red = "\x1b[31m";
238
+ const reset = "\x1b[0m";
239
+
240
+ console.error(`\n${red}[ERROR] Failed to start server${reset}\n`);
241
+
242
+ // Always show the error message and stack trace
243
+ if (error.message) {
244
+ console.error(`Message: ${error.message}`);
245
+ }
246
+
247
+ if (error.stack) {
248
+ console.error(`\nStack trace:`);
249
+ console.error(error.stack);
250
+ } else if (!error.message) {
251
+ // If no message or stack, show the raw error
252
+ console.error(`Raw error:`, error);
253
+ }
254
+
255
+ // Ensure we exit with error code
216
256
  process.exit(1);
217
257
  }
218
258
  }
@@ -289,6 +329,7 @@ Options:
289
329
  -h, --host <host> Hostname to bind to (default: localhost)
290
330
  -r, --routes <dir> Routes directory (default: ./routes)
291
331
  -w, --watch Watch for file changes (default: true)
332
+ -c, --config <path> Path to config file (default: vector.config.ts)
292
333
  --cors Enable CORS (default: true)
293
334
  `);
294
335
  process.exit(1);
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
2
+ import { resolve, isAbsolute } from "node:path";
3
3
  import { toFileUrl } from "../utils/path";
4
4
  import type {
5
5
  CacheHandler,
@@ -16,9 +16,14 @@ export class ConfigLoader<TTypes extends VectorTypes = DefaultVectorTypes> {
16
16
  private config: VectorConfigSchema<TTypes> | null = null;
17
17
  private configSource: "user" | "default" = "default";
18
18
 
19
- constructor(configPath = "vector.config.ts") {
20
- // Always resolve from the current working directory (user's project)
21
- this.configPath = resolve(process.cwd(), configPath);
19
+ constructor(configPath?: string) {
20
+ // Use provided config path or default to vector.config.ts
21
+ const path = configPath || "vector.config.ts";
22
+
23
+ // Handle absolute vs relative paths
24
+ this.configPath = isAbsolute(path)
25
+ ? path
26
+ : resolve(process.cwd(), path);
22
27
  }
23
28
 
24
29
  async load(): Promise<VectorConfig<TTypes>> {
@@ -67,6 +72,7 @@ export class ConfigLoader<TTypes extends VectorTypes = DefaultVectorTypes> {
67
72
  config.reusePort = this.config.reusePort;
68
73
  config.development = this.config.development;
69
74
  config.routesDir = this.config.routesDir || "./routes";
75
+ config.idleTimeout = this.config.idleTimeout;
70
76
  }
71
77
 
72
78
  // Always auto-discover routes
@@ -1,7 +1,12 @@
1
- import type { Server } from 'bun';
2
- import { cors } from 'itty-router';
3
- import type { CorsOptions, DefaultVectorTypes, VectorConfig, VectorTypes } from '../types';
4
- import type { VectorRouter } from './router';
1
+ import type { Server } from "bun";
2
+ import { cors } from "itty-router";
3
+ import type {
4
+ CorsOptions,
5
+ DefaultVectorTypes,
6
+ VectorConfig,
7
+ VectorTypes,
8
+ } from "../types";
9
+ import type { VectorRouter } from "./router";
5
10
 
6
11
  export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
7
12
  private server: Server | null = null;
@@ -14,36 +19,38 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
14
19
  this.config = config;
15
20
 
16
21
  if (config.cors) {
17
- const { preflight, corsify } = cors(this.normalizeCorsOptions(config.cors));
22
+ const { preflight, corsify } = cors(
23
+ this.normalizeCorsOptions(config.cors)
24
+ );
18
25
  this.corsHandler = { preflight, corsify };
19
26
  }
20
27
  }
21
28
 
22
29
  private normalizeCorsOptions(options: CorsOptions): any {
23
30
  return {
24
- origin: options.origin || '*',
31
+ origin: options.origin || "*",
25
32
  credentials: options.credentials !== false,
26
33
  allowHeaders: Array.isArray(options.allowHeaders)
27
- ? options.allowHeaders.join(', ')
28
- : options.allowHeaders || 'Content-Type, Authorization',
34
+ ? options.allowHeaders.join(", ")
35
+ : options.allowHeaders || "Content-Type, Authorization",
29
36
  allowMethods: Array.isArray(options.allowMethods)
30
- ? options.allowMethods.join(', ')
31
- : options.allowMethods || 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
37
+ ? options.allowMethods.join(", ")
38
+ : options.allowMethods || "GET, POST, PUT, PATCH, DELETE, OPTIONS",
32
39
  exposeHeaders: Array.isArray(options.exposeHeaders)
33
- ? options.exposeHeaders.join(', ')
34
- : options.exposeHeaders || 'Authorization',
40
+ ? options.exposeHeaders.join(", ")
41
+ : options.exposeHeaders || "Authorization",
35
42
  maxAge: options.maxAge || 86400,
36
43
  };
37
44
  }
38
45
 
39
46
  async start(): Promise<Server> {
40
47
  const port = this.config.port || 3000;
41
- const hostname = this.config.hostname || 'localhost';
48
+ const hostname = this.config.hostname || "localhost";
42
49
 
43
50
  const fetch = async (request: Request): Promise<Response> => {
44
51
  try {
45
52
  // Handle CORS preflight
46
- if (this.corsHandler && request.method === 'OPTIONS') {
53
+ if (this.corsHandler && request.method === "OPTIONS") {
47
54
  return this.corsHandler.preflight(request);
48
55
  }
49
56
 
@@ -57,33 +64,55 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
57
64
 
58
65
  return response;
59
66
  } catch (error) {
60
- console.error('Server error:', error);
61
- return new Response('Internal Server Error', { status: 500 });
67
+ console.error("Server error:", error);
68
+ return new Response("Internal Server Error", { status: 500 });
62
69
  }
63
70
  };
64
71
 
65
- this.server = Bun.serve({
66
- port,
67
- hostname,
68
- reusePort: this.config.reusePort !== false,
69
- fetch,
70
- error: (error) => {
71
- console.error('[ERROR] Server error:', error);
72
- return new Response('Internal Server Error', { status: 500 });
73
- },
74
- });
72
+ try {
73
+ this.server = Bun.serve({
74
+ port,
75
+ hostname,
76
+ reusePort: this.config.reusePort !== false,
77
+ fetch,
78
+ idleTimeout: this.config.idleTimeout || 60,
79
+ error: (error) => {
80
+ console.error("[ERROR] Server error:", error);
81
+ return new Response("Internal Server Error", { status: 500 });
82
+ },
83
+ });
84
+
85
+ // Validate that the server actually started
86
+ if (!this.server || !this.server.port) {
87
+ throw new Error(`Failed to start server on ${hostname}:${port} - server object is invalid`);
88
+ }
75
89
 
76
- // Server logs are handled by CLI, keep this minimal
77
- console.log(`→ Vector server running at http://${hostname}:${port}`);
90
+ // Server logs are handled by CLI, keep this minimal
91
+ console.log(`→ Vector server running at http://${hostname}:${port}`);
92
+
93
+ return this.server;
94
+ } catch (error: any) {
95
+ // Enhance error message with context for common issues
96
+ if (error.code === 'EADDRINUSE' || error.message?.includes('address already in use')) {
97
+ error.message = `Port ${port} is already in use`;
98
+ error.port = port;
99
+ } else if (error.code === 'EACCES' || error.message?.includes('permission denied')) {
100
+ error.message = `Permission denied to bind to port ${port}`;
101
+ error.port = port;
102
+ } else if (error.message?.includes('EADDRNOTAVAIL')) {
103
+ error.message = `Cannot bind to hostname ${hostname}`;
104
+ error.hostname = hostname;
105
+ }
78
106
 
79
- return this.server;
107
+ throw error;
108
+ }
80
109
  }
81
110
 
82
111
  stop() {
83
112
  if (this.server) {
84
113
  this.server.stop();
85
114
  this.server = null;
86
- console.log('Server stopped');
115
+ console.log("Server stopped");
87
116
  }
88
117
  }
89
118
 
@@ -96,7 +125,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
96
125
  }
97
126
 
98
127
  getHostname(): string {
99
- return this.server?.hostname || this.config.hostname || 'localhost';
128
+ return this.server?.hostname || this.config.hostname || "localhost";
100
129
  }
101
130
 
102
131
  getUrl(): string {
@@ -83,6 +83,7 @@ export interface VectorConfig<TTypes extends VectorTypes = DefaultVectorTypes> {
83
83
  finally?: AfterMiddlewareHandler<TTypes>[];
84
84
  routesDir?: string;
85
85
  autoDiscover?: boolean;
86
+ idleTimeout?: number;
86
87
  }
87
88
 
88
89
  // New config-driven schema - flat structure
@@ -93,18 +94,19 @@ export interface VectorConfigSchema<TTypes extends VectorTypes = DefaultVectorTy
93
94
  reusePort?: boolean;
94
95
  development?: boolean;
95
96
  routesDir?: string;
96
-
97
+ idleTimeout?: number;
98
+
97
99
  // Middleware functions
98
100
  before?: BeforeMiddlewareHandler<TTypes>[];
99
101
  after?: AfterMiddlewareHandler<TTypes>[];
100
-
102
+
101
103
  // Handler functions
102
104
  auth?: ProtectedHandler<TTypes>;
103
105
  cache?: CacheHandler;
104
-
106
+
105
107
  // CORS configuration
106
108
  cors?: CorsOptions | boolean;
107
-
109
+
108
110
  // Custom types for TypeScript
109
111
  types?: VectorTypes;
110
112
  }