vector-framework 0.8.2 → 0.9.1

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 (48) hide show
  1. package/dist/cli/index.js +91 -52
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/cli.js +1523 -0
  4. package/dist/core/config-loader.d.ts +15 -0
  5. package/dist/core/config-loader.d.ts.map +1 -0
  6. package/dist/core/config-loader.js +184 -0
  7. package/dist/core/config-loader.js.map +1 -0
  8. package/dist/core/router.d.ts +1 -0
  9. package/dist/core/router.d.ts.map +1 -1
  10. package/dist/core/router.js +3 -0
  11. package/dist/core/router.js.map +1 -1
  12. package/dist/core/vector.d.ts +10 -12
  13. package/dist/core/vector.d.ts.map +1 -1
  14. package/dist/core/vector.js +36 -25
  15. package/dist/core/vector.js.map +1 -1
  16. package/dist/dev/route-scanner.d.ts.map +1 -1
  17. package/dist/dev/route-scanner.js +25 -3
  18. package/dist/dev/route-scanner.js.map +1 -1
  19. package/dist/http.d.ts +6 -1
  20. package/dist/http.d.ts.map +1 -1
  21. package/dist/http.js +13 -4
  22. package/dist/http.js.map +1 -1
  23. package/dist/index.d.ts +4 -12
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +5 -5
  28. package/dist/middleware/manager.d.ts +2 -1
  29. package/dist/middleware/manager.d.ts.map +1 -1
  30. package/dist/middleware/manager.js +4 -0
  31. package/dist/middleware/manager.js.map +1 -1
  32. package/dist/types/index.d.ts +26 -0
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/dist/utils/path.d.ts +3 -0
  35. package/dist/utils/path.d.ts.map +1 -0
  36. package/dist/utils/path.js +9 -0
  37. package/dist/utils/path.js.map +1 -0
  38. package/package.json +6 -5
  39. package/src/cli/index.ts +105 -62
  40. package/src/core/config-loader.ts +213 -0
  41. package/src/core/router.ts +4 -0
  42. package/src/core/vector.ts +38 -32
  43. package/src/dev/route-scanner.ts +26 -3
  44. package/src/http.ts +21 -5
  45. package/src/index.ts +11 -16
  46. package/src/middleware/manager.ts +16 -4
  47. package/src/types/index.ts +44 -0
  48. package/src/utils/path.ts +9 -0
package/src/cli/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { watch } from "node:fs";
4
- import { join } from "node:path";
5
4
  import { parseArgs } from "node:util";
6
- import vector from "../core/vector";
5
+ import { getVectorInstance } from "../core/vector";
6
+ import { ConfigLoader } from "../core/config-loader";
7
7
 
8
8
  const { values, positionals } = parseArgs({
9
9
  args: Bun.argv.slice(2),
@@ -45,92 +45,135 @@ async function runDev() {
45
45
  `\n→ Starting Vector ${isDev ? "development" : "production"} server\n`
46
46
  );
47
47
 
48
- const config: any = {
49
- port: Number.parseInt(values.port as string),
50
- hostname: values.host as string,
51
- routesDir: values.routes as string,
52
- development: isDev,
53
- autoDiscover: true,
54
- cors: values.cors
55
- ? {
48
+ let server: any = null;
49
+ let vector: any = null;
50
+
51
+ async function startServer() {
52
+ try {
53
+ // Load configuration using ConfigLoader
54
+ const configLoader = new ConfigLoader();
55
+ const config = await configLoader.load();
56
+ const configSource = configLoader.getConfigSource();
57
+
58
+ // Merge CLI options with loaded config
59
+ config.port = config.port || Number.parseInt(values.port as string);
60
+ config.hostname = config.hostname || (values.host as string);
61
+ config.routesDir = config.routesDir || (values.routes as string);
62
+ config.development = isDev;
63
+ config.autoDiscover = true;
64
+
65
+ // Apply CLI CORS option if not set in config
66
+ if (!config.cors && values.cors) {
67
+ config.cors = {
56
68
  origin: "*",
57
69
  credentials: true,
58
70
  allowHeaders: "Content-Type, Authorization",
59
71
  allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
60
72
  exposeHeaders: "Authorization",
61
73
  maxAge: 86400,
62
- }
63
- : undefined,
64
- };
74
+ };
75
+ }
65
76
 
66
- try {
67
- const userConfigPath = join(process.cwd(), "vector.config.ts");
68
- try {
69
- const userConfig = await import(userConfigPath);
70
- if (userConfig.default) {
71
- // Properly merge config, preserving middleware arrays
72
- const {
73
- before,
74
- finally: finallyMiddleware,
75
- ...otherConfig
76
- } = userConfig.default;
77
-
78
- // Merge non-middleware config
79
- Object.assign(config, otherConfig);
80
-
81
- // Handle middleware arrays properly - these need to be set after Object.assign
82
- // to avoid being overwritten
83
- if (before) {
84
- config.before = before;
85
- }
86
- if (finallyMiddleware) {
87
- config.finally = finallyMiddleware;
88
- }
77
+ // Get Vector instance and configure handlers
78
+ vector = getVectorInstance();
79
+
80
+ // Load and set auth handler if configured
81
+ const authHandler = await configLoader.loadAuthHandler();
82
+ if (authHandler) {
83
+ vector.setProtectedHandler(authHandler);
89
84
  }
90
- } catch {
91
- // No user config file, use defaults
92
- }
93
85
 
94
- await vector.serve(config);
86
+ // Load and set cache handler if configured
87
+ const cacheHandler = await configLoader.loadCacheHandler();
88
+ if (cacheHandler) {
89
+ vector.setCacheHandler(cacheHandler);
90
+ }
95
91
 
96
- const gray = "\x1b[90m";
97
- const reset = "\x1b[0m";
98
- const cyan = "\x1b[36m";
99
- const green = "\x1b[32m";
92
+ // Start the server
93
+ server = await vector.startServer(config);
100
94
 
101
- console.log(` ${gray}Routes${reset} ${config.routesDir}`);
102
- if (isDev && values.watch) {
103
- console.log(` ${gray}Watching${reset} All project files`);
95
+ const gray = "\x1b[90m";
96
+ const reset = "\x1b[0m";
97
+ const cyan = "\x1b[36m";
98
+ const green = "\x1b[32m";
104
99
 
100
+ console.log(` ${gray}Config${reset} ${configSource === 'user' ? 'User config loaded' : 'Using defaults'}`);
101
+ console.log(` ${gray}Routes${reset} ${config.routesDir}`);
102
+ if (isDev && values.watch) {
103
+ console.log(` ${gray}Watching${reset} All project files`);
104
+ }
105
+ console.log(
106
+ ` ${gray}CORS${reset} ${values.cors ? "Enabled" : "Disabled"}`
107
+ );
108
+ console.log(
109
+ ` ${gray}Mode${reset} ${isDev ? "Development" : "Production"}\n`
110
+ );
111
+ console.log(
112
+ ` ${green}Ready${reset} → ${cyan}http://${config.hostname}:${config.port}${reset}\n`
113
+ );
114
+
115
+ return { server, vector, config };
116
+ } catch (error) {
117
+ console.error("[ERROR] Failed to start server:", error);
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ try {
123
+ // Start the server initially
124
+ const result = await startServer();
125
+ server = result.server;
126
+
127
+ // Setup file watching for hot reload
128
+ if (isDev && values.watch) {
105
129
  try {
130
+ let reloadTimeout: any = null;
131
+
106
132
  // Watch entire project directory for changes
107
133
  watch(process.cwd(), { recursive: true }, async (_, filename) => {
108
134
  if (
109
135
  filename &&
110
136
  (filename.endsWith(".ts") ||
111
137
  filename.endsWith(".js") ||
112
- filename.endsWith(".json"))
138
+ filename.endsWith(".json")) &&
139
+ !filename.includes("node_modules") &&
140
+ !filename.includes(".git")
113
141
  ) {
114
- console.log(`\n 🔄 File changed: ${filename}`);
115
- console.log(" 🔄 Restarting server...\n");
116
-
117
- // Exit the current process, which will trigger a restart if using --watch flag
118
- process.exit(0);
142
+ // Debounce reload to avoid multiple restarts
143
+ if (reloadTimeout) {
144
+ clearTimeout(reloadTimeout);
145
+ }
146
+
147
+ reloadTimeout = setTimeout(async () => {
148
+ console.log(`\n 🔄 File changed: ${filename}`);
149
+ console.log(" 🔄 Reloading server...\n");
150
+
151
+ // Stop the current server
152
+ if (vector) {
153
+ vector.stop();
154
+ }
155
+
156
+ // Clear module cache to ensure fresh imports
157
+ for (const key in require.cache) {
158
+ if (!key.includes("node_modules")) {
159
+ delete require.cache[key];
160
+ }
161
+ }
162
+
163
+ // Restart the server
164
+ try {
165
+ const result = await startServer();
166
+ server = result.server;
167
+ } catch (error) {
168
+ console.error(" ❌ Failed to reload server:", error);
169
+ }
170
+ }, 100); // 100ms debounce
119
171
  }
120
172
  });
121
173
  } catch (err) {
122
174
  console.warn(" ⚠️ File watching not available");
123
175
  }
124
176
  }
125
- console.log(
126
- ` ${gray}CORS${reset} ${values.cors ? "Enabled" : "Disabled"}`
127
- );
128
- console.log(
129
- ` ${gray}Mode${reset} ${isDev ? "Development" : "Production"}\n`
130
- );
131
- console.log(
132
- ` ${green}Ready${reset} → ${cyan}http://${config.hostname}:${config.port}${reset}\n`
133
- );
134
177
  } catch (error) {
135
178
  console.error("[ERROR] Failed to start server:", error);
136
179
  process.exit(1);
@@ -0,0 +1,213 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { toFileUrl } from '../utils/path';
4
+ import type {
5
+ AfterMiddlewareHandler,
6
+ BeforeMiddlewareHandler,
7
+ CacheHandler,
8
+ CorsOptions,
9
+ DefaultVectorTypes,
10
+ ProtectedHandler,
11
+ VectorConfig,
12
+ VectorConfigSchema,
13
+ VectorTypes,
14
+ } from '../types';
15
+
16
+ export class ConfigLoader<TTypes extends VectorTypes = DefaultVectorTypes> {
17
+ private configPath: string;
18
+ private config: VectorConfigSchema<TTypes> | null = null;
19
+ private configSource: 'user' | 'default' = 'default';
20
+
21
+ constructor(configPath = 'vector.config.ts') {
22
+ // Always resolve from the current working directory (user's project)
23
+ this.configPath = resolve(process.cwd(), configPath);
24
+ }
25
+
26
+ async load(): Promise<VectorConfig<TTypes>> {
27
+ // Check if config file exists before attempting to load
28
+ if (existsSync(this.configPath)) {
29
+ try {
30
+ console.log(`→ Loading config from: ${this.configPath}`);
31
+
32
+ // Use explicit file:// URL to ensure correct resolution
33
+ const userConfigPath = toFileUrl(this.configPath);
34
+ const userConfig = await import(userConfigPath);
35
+ this.config = userConfig.default || userConfig;
36
+ this.configSource = 'user';
37
+
38
+ console.log(' ✓ User config loaded successfully');
39
+ } catch (error) {
40
+ console.error(` ✗ Failed to load config from ${this.configPath}:`, error);
41
+ console.log(' → Using default configuration');
42
+ this.config = {};
43
+ }
44
+ } else {
45
+ // Config file doesn't exist, use defaults
46
+ console.log(` → No config file found at: ${this.configPath}`);
47
+ console.log(' → Using default configuration');
48
+ this.config = {};
49
+ }
50
+
51
+ // Convert new config schema to legacy VectorConfig format
52
+ return await this.buildLegacyConfig();
53
+ }
54
+
55
+ getConfigSource(): 'user' | 'default' {
56
+ return this.configSource;
57
+ }
58
+
59
+ private async buildLegacyConfig(): Promise<VectorConfig<TTypes>> {
60
+ const config: VectorConfig<TTypes> = {};
61
+
62
+ // Server configuration
63
+ if (this.config?.server) {
64
+ config.port = this.config.server.port;
65
+ config.hostname = this.config.server.hostname;
66
+ config.reusePort = this.config.server.reusePort;
67
+ config.development = this.config.server.development;
68
+ }
69
+
70
+ // Routes configuration
71
+ if (this.config?.routes) {
72
+ config.routesDir = this.config.routes.dir || './routes';
73
+ config.autoDiscover = this.config.routes.autoDiscover !== false;
74
+ } else {
75
+ config.routesDir = './routes';
76
+ config.autoDiscover = true;
77
+ }
78
+
79
+ // CORS configuration
80
+ if (this.config?.cors) {
81
+ if (typeof this.config.cors === 'boolean') {
82
+ config.cors = this.config.cors
83
+ ? {
84
+ origin: '*',
85
+ credentials: true,
86
+ allowHeaders: 'Content-Type, Authorization',
87
+ allowMethods: 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
88
+ exposeHeaders: 'Authorization',
89
+ maxAge: 86400,
90
+ }
91
+ : undefined;
92
+ } else {
93
+ config.cors = this.config.cors as CorsOptions;
94
+ }
95
+ }
96
+
97
+ // Load middleware - support both direct functions and file paths
98
+ if (this.config?.before) {
99
+ // Direct functions provided
100
+ console.log('Using direct before middleware functions:', this.config.before.length);
101
+ config.before = this.config.before;
102
+ } else if (this.config?.middleware?.before) {
103
+ // File paths provided (legacy)
104
+ console.log('Loading before middleware from file paths:', this.config.middleware.before);
105
+ config.before = await this.loadMiddleware<BeforeMiddlewareHandler<TTypes>>(
106
+ this.config.middleware.before
107
+ );
108
+ console.log('Loaded before middleware:', config.before?.length);
109
+ }
110
+
111
+ if (this.config?.after) {
112
+ // Direct functions provided
113
+ console.log('Using direct after middleware functions:', this.config.after.length);
114
+ config.finally = this.config.after;
115
+ } else if (this.config?.middleware?.after) {
116
+ // File paths provided (legacy)
117
+ console.log('Loading after middleware from file paths:', this.config.middleware.after);
118
+ config.finally = await this.loadMiddleware<AfterMiddlewareHandler<TTypes>>(
119
+ this.config.middleware.after
120
+ );
121
+ console.log('Loaded after middleware:', config.finally?.length);
122
+ }
123
+
124
+ return config;
125
+ }
126
+
127
+ private async loadMiddleware<T>(paths: string[]): Promise<T[]> {
128
+ const middleware: T[] = [];
129
+
130
+ for (const path of paths) {
131
+ try {
132
+ const modulePath = resolve(process.cwd(), path);
133
+ const importPath = toFileUrl(modulePath);
134
+ const module = await import(importPath);
135
+ const handler = module.default || module;
136
+
137
+ if (typeof handler === 'function') {
138
+ middleware.push(handler as T);
139
+ } else {
140
+ console.warn(`Middleware at ${path} does not export a function`);
141
+ }
142
+ } catch (error) {
143
+ console.error(`Failed to load middleware from ${path}:`, error);
144
+ }
145
+ }
146
+
147
+ return middleware;
148
+ }
149
+
150
+ async loadAuthHandler(): Promise<ProtectedHandler<TTypes> | null> {
151
+ // Direct function provided
152
+ if (this.config?.auth) {
153
+ console.log('Using direct auth handler function');
154
+ return this.config.auth;
155
+ }
156
+
157
+ // File path provided (legacy)
158
+ if (!this.config?.handlers?.auth) {
159
+ return null;
160
+ }
161
+
162
+ try {
163
+ const modulePath = resolve(process.cwd(), this.config.handlers.auth);
164
+ const importPath = toFileUrl(modulePath);
165
+ const module = await import(importPath);
166
+ const handler = module.default || module;
167
+
168
+ if (typeof handler === 'function') {
169
+ return handler as ProtectedHandler<TTypes>;
170
+ } else {
171
+ console.warn(`Auth handler at ${this.config.handlers.auth} does not export a function`);
172
+ return null;
173
+ }
174
+ } catch (error) {
175
+ console.error(`Failed to load auth handler from ${this.config.handlers.auth}:`, error);
176
+ return null;
177
+ }
178
+ }
179
+
180
+ async loadCacheHandler(): Promise<CacheHandler | null> {
181
+ // Direct function provided
182
+ if (this.config?.cache) {
183
+ console.log('Using direct cache handler function');
184
+ return this.config.cache;
185
+ }
186
+
187
+ // File path provided (legacy)
188
+ if (!this.config?.handlers?.cache) {
189
+ return null;
190
+ }
191
+
192
+ try {
193
+ const modulePath = resolve(process.cwd(), this.config.handlers.cache);
194
+ const importPath = toFileUrl(modulePath);
195
+ const module = await import(importPath);
196
+ const handler = module.default || module;
197
+
198
+ if (typeof handler === 'function') {
199
+ return handler as CacheHandler;
200
+ } else {
201
+ console.warn(`Cache handler at ${this.config.handlers.cache} does not export a function`);
202
+ return null;
203
+ }
204
+ } catch (error) {
205
+ console.error(`Failed to load cache handler from ${this.config.handlers.cache}:`, error);
206
+ return null;
207
+ }
208
+ }
209
+
210
+ getConfig(): VectorConfigSchema<TTypes> | null {
211
+ return this.config;
212
+ }
213
+ }
@@ -256,4 +256,8 @@ export class VectorRouter<TTypes extends VectorTypes = DefaultVectorTypes> {
256
256
 
257
257
  return APIError.notFound('Route not found');
258
258
  }
259
+
260
+ clearRoutes(): void {
261
+ this.routes = [];
262
+ }
259
263
  }
@@ -5,9 +5,8 @@ import { CacheManager } from '../cache/manager';
5
5
  import { RouteGenerator } from '../dev/route-generator';
6
6
  import { RouteScanner } from '../dev/route-scanner';
7
7
  import { MiddlewareManager } from '../middleware/manager';
8
+ import { toFileUrl } from '../utils/path';
8
9
  import type {
9
- AfterMiddlewareHandler,
10
- BeforeMiddlewareHandler,
11
10
  CacheHandler,
12
11
  DefaultVectorTypes,
13
12
  ProtectedHandler,
@@ -19,6 +18,7 @@ import type {
19
18
  import { VectorRouter } from './router';
20
19
  import { VectorServer } from './server';
21
20
 
21
+ // Internal-only class - not exposed to users
22
22
  export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
23
23
  private static instance: Vector<any>;
24
24
  private router: VectorRouter<TTypes>;
@@ -43,6 +43,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
43
43
  );
44
44
  }
45
45
 
46
+ // Internal use only - not exposed to users
46
47
  static getInstance<T extends VectorTypes = DefaultVectorTypes>(): Vector<T> {
47
48
  if (!Vector.instance) {
48
49
  Vector.instance = new Vector<T>();
@@ -50,46 +51,38 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
50
51
  return Vector.instance as Vector<T>;
51
52
  }
52
53
 
53
- set protected(handler: ProtectedHandler<TTypes>) {
54
+ // Internal method to set protected handler
55
+ setProtectedHandler(handler: ProtectedHandler<TTypes>) {
54
56
  this._protectedHandler = handler;
55
57
  this.authManager.setProtectedHandler(handler);
56
58
  }
57
59
 
58
- get protected(): ProtectedHandler<TTypes> | null {
60
+ getProtectedHandler(): ProtectedHandler<TTypes> | null {
59
61
  return this._protectedHandler;
60
62
  }
61
63
 
62
- set cache(handler: CacheHandler) {
64
+ // Internal method to set cache handler
65
+ setCacheHandler(handler: CacheHandler) {
63
66
  this._cacheHandler = handler;
64
67
  this.cacheManager.setCacheHandler(handler);
65
68
  }
66
69
 
67
- get cache(): CacheHandler | null {
70
+ getCacheHandler(): CacheHandler | null {
68
71
  return this._cacheHandler;
69
72
  }
70
73
 
71
- route(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): RouteEntry {
74
+ // Internal method to add route
75
+ addRoute(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): RouteEntry {
72
76
  return this.router.route(options, handler);
73
77
  }
74
78
 
75
- use(...middleware: BeforeMiddlewareHandler<TTypes>[]): this {
76
- this.middlewareManager.addBefore(...middleware);
77
- return this;
78
- }
79
-
80
- before(...middleware: BeforeMiddlewareHandler<TTypes>[]): this {
81
- this.middlewareManager.addBefore(...middleware);
82
- return this;
83
- }
84
-
85
- finally(...middleware: AfterMiddlewareHandler<TTypes>[]): this {
86
- this.middlewareManager.addFinally(...middleware);
87
- return this;
88
- }
89
-
90
- async serve(config?: VectorConfig<TTypes>): Promise<Server> {
79
+ // Internal method to start server - only called by CLI
80
+ async startServer(config?: VectorConfig<TTypes>): Promise<Server> {
91
81
  this.config = { ...this.config, ...config };
92
82
 
83
+ // Clear previous middleware to avoid accumulation across multiple starts
84
+ this.middlewareManager.clear();
85
+
93
86
  if (config?.before) {
94
87
  this.middlewareManager.addBefore(...config.before);
95
88
  }
@@ -129,17 +122,19 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
129
122
 
130
123
  for (const route of routes) {
131
124
  try {
132
- // Convert Windows paths to URLs for import
133
- const importPath =
134
- process.platform === 'win32'
135
- ? `file:///${route.path.replace(/\\/g, '/')}`
136
- : route.path;
125
+ const importPath = toFileUrl(route.path);
137
126
 
138
127
  const module = await import(importPath);
139
128
  const exported = route.name === 'default' ? module.default : module[route.name];
140
129
 
141
130
  if (exported) {
142
- if (this.isRouteEntry(exported)) {
131
+ if (this.isRouteDefinition(exported)) {
132
+ // Use router.route() to ensure middleware is applied
133
+ const routeDef = exported as any;
134
+ this.router.route(routeDef.options, routeDef.handler);
135
+ this.logRouteLoaded(routeDef.options);
136
+ } else if (this.isRouteEntry(exported)) {
137
+ // Legacy support for direct RouteEntry (won't have middleware)
143
138
  this.router.addRoute(exported as RouteEntry);
144
139
  this.logRouteLoaded(exported as RouteEntry);
145
140
  } else if (typeof exported === 'function') {
@@ -185,6 +180,10 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
185
180
  return Array.isArray(value) && value.length >= 3;
186
181
  }
187
182
 
183
+ private isRouteDefinition(value: any): boolean {
184
+ return value && typeof value === 'object' && 'entry' in value && 'options' in value && 'handler' in value;
185
+ }
186
+
188
187
  private logRouteLoaded(route: RouteEntry | RouteOptions): void {
189
188
  if (Array.isArray(route)) {
190
189
  console.log(` ✓ Loaded route: ${route[0]} ${route[3] || route[1]}`);
@@ -198,6 +197,9 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
198
197
  this.server.stop();
199
198
  this.server = null;
200
199
  }
200
+ // Don't reset managers - they should persist for the singleton
201
+ // Only clear route-specific state if needed
202
+ this.router.clearRoutes();
201
203
  }
202
204
 
203
205
  getServer(): VectorServer<TTypes> | null {
@@ -215,8 +217,12 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
215
217
  getAuthManager(): AuthManager<TTypes> {
216
218
  return this.authManager;
217
219
  }
218
- }
219
220
 
220
- const vector = Vector.getInstance();
221
+ // Reset instance for testing purposes only
222
+ static resetInstance(): void {
223
+ Vector.instance = null as any;
224
+ }
225
+ }
221
226
 
222
- export default vector;
227
+ // Export for internal use only
228
+ export const getVectorInstance = Vector.getInstance;
@@ -1,4 +1,4 @@
1
- import { promises as fs } from 'node:fs';
1
+ import { existsSync, promises as fs } from 'node:fs';
2
2
  import { join, relative, resolve, sep } from 'node:path';
3
3
  import type { GeneratedRoute } from '../types';
4
4
 
@@ -6,17 +6,29 @@ export class RouteScanner {
6
6
  private routesDir: string;
7
7
 
8
8
  constructor(routesDir = './routes') {
9
+ // Always resolve from the current working directory (user's project)
9
10
  this.routesDir = resolve(process.cwd(), routesDir);
10
11
  }
11
12
 
12
13
  async scan(): Promise<GeneratedRoute[]> {
13
14
  const routes: GeneratedRoute[] = [];
14
15
 
16
+ // Check if routes directory exists before attempting to scan
17
+ if (!existsSync(this.routesDir)) {
18
+ console.log(` → Routes directory not found: ${this.routesDir}`);
19
+ console.log(' → No routes will be auto-discovered');
20
+ return [];
21
+ }
22
+
15
23
  try {
24
+ console.log(` → Scanning routes from: ${this.routesDir}`);
16
25
  await this.scanDirectory(this.routesDir, routes);
26
+ if (routes.length > 0) {
27
+ console.log(` ✓ Found ${routes.length} route${routes.length === 1 ? '' : 's'}`);
28
+ }
17
29
  } catch (error) {
18
30
  if ((error as any).code === 'ENOENT') {
19
- console.warn(`Routes directory not found: ${this.routesDir}`);
31
+ console.warn(`Routes directory not accessible: ${this.routesDir}`);
20
32
  return [];
21
33
  }
22
34
  throw error;
@@ -64,7 +76,18 @@ export class RouteScanner {
64
76
  for (const [name, value] of Object.entries(module)) {
65
77
  if (name === 'default') continue;
66
78
 
67
- if (Array.isArray(value) && value.length >= 4) {
79
+ // Check for new RouteDefinition format
80
+ if (value && typeof value === 'object' && 'entry' in value && 'options' in value && 'handler' in value) {
81
+ const routeDef = value as any;
82
+ routes.push({
83
+ name,
84
+ path: fullPath,
85
+ method: routeDef.options.method as string,
86
+ options: routeDef.options,
87
+ });
88
+ }
89
+ // Legacy RouteEntry format support
90
+ else if (Array.isArray(value) && value.length >= 4) {
68
91
  const [method, , , path] = value;
69
92
  routes.push({
70
93
  name,