vector-framework 0.9.8 → 1.0.0

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
@@ -49,9 +49,6 @@ const command = positionals[0] || "dev";
49
49
 
50
50
  async function runDev() {
51
51
  const isDev = command === "dev";
52
- console.log(
53
- `\n→ Starting Vector ${isDev ? "development" : "production"} server\n`
54
- );
55
52
 
56
53
  let server: any = null;
57
54
  let vector: any = null;
@@ -66,83 +63,62 @@ async function runDev() {
66
63
 
67
64
  // Create the actual server start promise
68
65
  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
- }
66
+ // Load configuration using ConfigLoader
67
+ const configLoader = new ConfigLoader(values.config as string | undefined);
68
+ const config = await configLoader.load();
69
+
70
+ // Merge CLI options with loaded config
71
+ // Only use CLI values if config doesn't have them
72
+ config.port = config.port ?? Number.parseInt(values.port as string);
73
+ config.hostname = config.hostname ?? (values.host as string);
74
+ config.routesDir = config.routesDir ?? (values.routes as string);
75
+ config.development = config.development ?? isDev;
76
+ config.autoDiscover = true; // Always auto-discover routes
77
+
78
+ // Apply CLI CORS option if not explicitly set in config
79
+ // Only apply default CORS if config.cors is undefined (not set)
80
+ if (config.cors === undefined && values.cors) {
81
+ config.cors = {
82
+ origin: "*",
83
+ credentials: true,
84
+ allowHeaders: "Content-Type, Authorization",
85
+ allowMethods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
86
+ exposeHeaders: "Authorization",
87
+ maxAge: 86400,
88
+ };
89
+ }
94
90
 
95
- // Get Vector instance and configure handlers
96
- vector = getVectorInstance();
91
+ // Get Vector instance and configure handlers
92
+ vector = getVectorInstance();
97
93
 
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
- }
94
+ // Load and set auth handler if configured
95
+ const authHandler = await configLoader.loadAuthHandler();
96
+ if (authHandler) {
97
+ vector.setProtectedHandler(authHandler);
98
+ }
109
99
 
110
- // Start the server
111
- server = await vector.startServer(config);
100
+ // Load and set cache handler if configured
101
+ const cacheHandler = await configLoader.loadCacheHandler();
102
+ if (cacheHandler) {
103
+ vector.setCacheHandler(cacheHandler);
104
+ }
112
105
 
113
- // Verify the server is actually running
114
- if (!server || !server.port) {
115
- throw new Error("Server started but is not responding correctly");
116
- }
106
+ // Start the server
107
+ server = await vector.startServer(config);
117
108
 
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;
109
+ // Verify the server is actually running
110
+ if (!server || !server.port) {
111
+ throw new Error("Server started but is not responding correctly");
145
112
  }
113
+
114
+ const cyan = "\x1b[36m";
115
+ const reset = "\x1b[0m";
116
+
117
+ console.log(
118
+ `\nListening on ${cyan}http://${config.hostname}:${config.port}${reset}\n`
119
+ );
120
+
121
+ return { server, vector, config };
146
122
  })();
147
123
 
148
124
  // Race between server startup and timeout
@@ -206,9 +182,13 @@ async function runDev() {
206
182
  await new Promise((resolve) => setTimeout(resolve, 100));
207
183
 
208
184
  // Clear module cache to ensure fresh imports
209
- for (const key in require.cache) {
210
- if (!key.includes("node_modules")) {
211
- delete require.cache[key];
185
+ // Note: Bun uses ESM and doesn't have require.cache
186
+ // The Loader API will handle module reloading automatically
187
+ if (typeof require !== 'undefined' && require.cache) {
188
+ for (const key in require.cache) {
189
+ if (!key.includes("node_modules")) {
190
+ delete require.cache[key];
191
+ }
212
192
  }
213
193
  }
214
194
 
@@ -221,84 +201,93 @@ async function runDev() {
221
201
  console.error("\n[Reload Error]", error.message || error);
222
202
  // Don't exit the process on reload failures, just continue watching
223
203
  } finally {
224
- // Reset flag after a delay
225
- setTimeout(() => {
226
- isReloading = false;
227
- }, 2000); // 2 second cooldown
204
+ // Reset flag immediately after reload completes
205
+ // The lastReloadTime check provides additional protection
206
+ isReloading = false;
228
207
  }
229
208
  }, 500); // Increased debounce to 500ms
230
209
  }
231
210
  });
232
- } catch (err) {
233
- console.warn(" ⚠️ File watching not available");
211
+ } catch {
212
+ const yellow = "\x1b[33m";
213
+ const reset = "\x1b[0m";
214
+ console.warn(`${yellow}Warning: File watching not available${reset}`);
234
215
  }
235
216
  }
236
217
  } catch (error: any) {
237
218
  const red = "\x1b[31m";
238
219
  const reset = "\x1b[0m";
239
220
 
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
- }
221
+ console.error(`\n${red}Error: ${error.message || error}${reset}\n`);
246
222
 
247
- if (error.stack) {
248
- console.error(`\nStack trace:`);
223
+ if (error.stack && process.env.NODE_ENV === "development") {
249
224
  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
225
  }
254
226
 
255
- // Ensure we exit with error code
256
227
  process.exit(1);
257
228
  }
258
229
  }
259
230
 
260
231
  async function runBuild() {
261
- console.log("\n→ Building Vector application\n");
262
-
263
232
  try {
264
233
  const { RouteScanner } = await import("../dev/route-scanner");
265
234
  const { RouteGenerator } = await import("../dev/route-generator");
266
235
 
236
+ // Step 1: Scan and generate routes
267
237
  const scanner = new RouteScanner(values.routes as string);
268
238
  const generator = new RouteGenerator();
269
239
 
270
240
  const routes = await scanner.scan();
271
241
  await generator.generate(routes);
272
242
 
273
- console.log(` Generated ${routes.length} routes`);
274
-
275
- // Use spawn based on runtime
243
+ // Step 2: Build the application with Bun
276
244
  if (typeof Bun !== "undefined") {
245
+ // Build the CLI as an executable
277
246
  const buildProcess = Bun.spawn([
278
247
  "bun",
279
248
  "build",
280
- "src/index.ts",
281
- "--outdir",
282
- "dist",
249
+ "src/cli/index.ts",
250
+ "--target",
251
+ "bun",
252
+ "--outfile",
253
+ "dist/server.js",
283
254
  "--minify",
284
255
  ]);
285
- await buildProcess.exited;
256
+
257
+ const exitCode = await buildProcess.exited;
258
+ if (exitCode !== 0) {
259
+ throw new Error(`Build failed with exit code ${exitCode}`);
260
+ }
286
261
  } else {
287
262
  // For Node.js, use child_process
288
263
  const { spawnSync } = await import("child_process");
289
- spawnSync(
264
+ const result = spawnSync(
290
265
  "bun",
291
- ["build", "src/index.ts", "--outdir", "dist", "--minify"],
266
+ [
267
+ "build",
268
+ "src/cli/index.ts",
269
+ "--target",
270
+ "bun",
271
+ "--outfile",
272
+ "dist/server.js",
273
+ "--minify",
274
+ ],
292
275
  {
293
276
  stdio: "inherit",
294
277
  shell: true,
295
278
  }
296
279
  );
280
+
281
+ if (result.status !== 0) {
282
+ throw new Error(`Build failed with exit code ${result.status}`);
283
+ }
297
284
  }
298
285
 
299
- console.log("\n ✓ Build complete\n");
300
- } catch (error) {
301
- console.error("[ERROR] Build failed:", error);
286
+ console.log("\nBuild complete: dist/server.js\n");
287
+ } catch (error: any) {
288
+ const red = "\x1b[31m";
289
+ const reset = "\x1b[0m";
290
+ console.error(`\n${red}Error: ${error.message || error}${reset}\n`);
302
291
  process.exit(1);
303
292
  }
304
293
  }
@@ -30,27 +30,21 @@ export class ConfigLoader<TTypes extends VectorTypes = DefaultVectorTypes> {
30
30
  // Check if config file exists before attempting to load
31
31
  if (existsSync(this.configPath)) {
32
32
  try {
33
- console.log(`→ Loading config from: ${this.configPath}`);
34
-
35
33
  // Use explicit file:// URL to ensure correct resolution
36
34
  const userConfigPath = toFileUrl(this.configPath);
37
35
  const userConfig = await import(userConfigPath);
38
36
  this.config = userConfig.default || userConfig;
39
37
  this.configSource = "user";
40
-
41
- console.log(" ✓ User config loaded successfully");
42
- } catch (error) {
38
+ } catch (error: any) {
39
+ const red = "\x1b[31m";
40
+ const reset = "\x1b[0m";
43
41
  console.error(
44
- ` ✗ Failed to load config from ${this.configPath}:`,
45
- error
42
+ `${red}Error loading config: ${error.message || error}${reset}`
46
43
  );
47
- console.log(" → Using default configuration");
48
44
  this.config = {};
49
45
  }
50
46
  } else {
51
47
  // Config file doesn't exist, use defaults
52
- console.log(` → No config file found at: ${this.configPath}`);
53
- console.log(" → Using default configuration");
54
48
  this.config = {};
55
49
  }
56
50
 
@@ -87,8 +87,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
87
87
  throw new Error(`Failed to start server on ${hostname}:${port} - server object is invalid`);
88
88
  }
89
89
 
90
- // Server logs are handled by CLI, keep this minimal
91
- console.log(`→ Vector server running at http://${hostname}:${port}`);
90
+ // Server logs are handled by CLI
92
91
 
93
92
  return this.server;
94
93
  } catch (error: any) {
@@ -86,6 +86,11 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
86
86
  // Clear previous middleware to avoid accumulation across multiple starts
87
87
  this.middlewareManager.clear();
88
88
 
89
+ // Only clear routes if we're doing auto-discovery
90
+ if (this.config.autoDiscover !== false) {
91
+ this.router.clearRoutes();
92
+ }
93
+
89
94
  if (config?.before) {
90
95
  this.middlewareManager.addBefore(...config.before);
91
96
  }
@@ -106,10 +111,11 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
106
111
 
107
112
  private async discoverRoutes() {
108
113
  const routesDir = this.config.routesDir || "./routes";
114
+ const excludePatterns = this.config.routeExcludePatterns;
109
115
 
110
116
  // Always create a new RouteScanner with the current config's routesDir
111
117
  // to ensure we're using the correct path from the user's config
112
- this.routeScanner = new RouteScanner(routesDir);
118
+ this.routeScanner = new RouteScanner(routesDir, excludePatterns);
113
119
 
114
120
  if (!this.routeGenerator) {
115
121
  this.routeGenerator = new RouteGenerator();
@@ -208,9 +214,8 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
208
214
  this.server.stop();
209
215
  this.server = null;
210
216
  }
211
- // Don't reset managers - they should persist for the singleton
212
- // Only clear route-specific state if needed
213
- this.router.clearRoutes();
217
+ // Don't reset managers or routes - they persist for the singleton
218
+ // Routes will be cleared on next startServer() call
214
219
  }
215
220
 
216
221
  getServer(): VectorServer<TTypes> | null {
@@ -1,13 +1,32 @@
1
- import { existsSync, promises as fs } from 'node:fs';
2
- import { join, relative, resolve, sep } from 'node:path';
3
- import type { GeneratedRoute } from '../types';
1
+ import { existsSync, promises as fs } from "node:fs";
2
+ import { join, relative, resolve, sep } from "node:path";
3
+ import type { GeneratedRoute } from "../types";
4
4
 
5
5
  export class RouteScanner {
6
6
  private routesDir: string;
7
-
8
- constructor(routesDir = './routes') {
7
+ private excludePatterns: string[];
8
+ private static readonly DEFAULT_EXCLUDE_PATTERNS = [
9
+ "*.test.ts",
10
+ "*.test.js",
11
+ "*.test.tsx",
12
+ "*.test.jsx",
13
+ "*.spec.ts",
14
+ "*.spec.js",
15
+ "*.spec.tsx",
16
+ "*.spec.jsx",
17
+ "*.tests.ts",
18
+ "*.tests.js",
19
+ "**/__tests__/**",
20
+ "*.interface.ts",
21
+ "*.type.ts",
22
+ "*.d.ts",
23
+ ];
24
+
25
+ constructor(routesDir = "./routes", excludePatterns?: string[]) {
9
26
  // Always resolve from the current working directory (user's project)
10
27
  this.routesDir = resolve(process.cwd(), routesDir);
28
+ this.excludePatterns =
29
+ excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
11
30
  }
12
31
 
13
32
  async scan(): Promise<GeneratedRoute[]> {
@@ -21,7 +40,7 @@ export class RouteScanner {
21
40
  try {
22
41
  await this.scanDirectory(this.routesDir, routes);
23
42
  } catch (error) {
24
- if ((error as any).code === 'ENOENT') {
43
+ if ((error as any).code === "ENOENT") {
25
44
  console.warn(` ✗ Routes directory not accessible: ${this.routesDir}`);
26
45
  return [];
27
46
  }
@@ -31,7 +50,34 @@ export class RouteScanner {
31
50
  return routes;
32
51
  }
33
52
 
34
- private async scanDirectory(dir: string, routes: GeneratedRoute[], basePath = ''): Promise<void> {
53
+ private isExcluded(filePath: string): boolean {
54
+ const relativePath = relative(this.routesDir, filePath);
55
+
56
+ for (const pattern of this.excludePatterns) {
57
+ // Convert glob pattern to regex
58
+ const regexPattern = pattern
59
+ .replace(/\./g, "\\.") // Escape dots
60
+ .replace(/\*/g, "[^/]*") // * matches anything except /
61
+ .replace(/\*\*/g, ".*") // ** matches anything including /
62
+ .replace(/\?/g, "."); // ? matches single character
63
+
64
+ const regex = new RegExp(`^${regexPattern}$`);
65
+
66
+ // Check both the full relative path and just the filename
67
+ const filename = relativePath.split(sep).pop() || "";
68
+ if (regex.test(relativePath) || regex.test(filename)) {
69
+ return true;
70
+ }
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ private async scanDirectory(
77
+ dir: string,
78
+ routes: GeneratedRoute[],
79
+ basePath = ""
80
+ ): Promise<void> {
35
81
  const entries = await fs.readdir(dir);
36
82
 
37
83
  for (const entry of entries) {
@@ -41,26 +87,32 @@ export class RouteScanner {
41
87
  if (stats.isDirectory()) {
42
88
  const newBasePath = basePath ? `${basePath}/${entry}` : entry;
43
89
  await this.scanDirectory(fullPath, routes, newBasePath);
44
- } else if (entry.endsWith('.ts') || entry.endsWith('.js')) {
90
+ } else if (entry.endsWith(".ts") || entry.endsWith(".js")) {
91
+ // Skip excluded files (test files, etc.)
92
+ if (this.isExcluded(fullPath)) {
93
+ continue;
94
+ }
45
95
  const routePath = relative(this.routesDir, fullPath)
46
- .replace(/\.(ts|js)$/, '')
96
+ .replace(/\.(ts|js)$/, "")
47
97
  .split(sep)
48
- .join('/');
98
+ .join("/");
49
99
 
50
100
  try {
51
101
  // Convert Windows paths to URLs for import
52
102
  const importPath =
53
- process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
103
+ process.platform === "win32"
104
+ ? `file:///${fullPath.replace(/\\/g, "/")}`
105
+ : fullPath;
54
106
 
55
107
  const module = await import(importPath);
56
108
 
57
- if (module.default && typeof module.default === 'function') {
109
+ if (module.default && typeof module.default === "function") {
58
110
  routes.push({
59
- name: 'default',
111
+ name: "default",
60
112
  path: fullPath,
61
- method: 'GET',
113
+ method: "GET",
62
114
  options: {
63
- method: 'GET',
115
+ method: "GET",
64
116
  path: `/${routePath}`,
65
117
  expose: true,
66
118
  },
@@ -68,10 +120,16 @@ export class RouteScanner {
68
120
  }
69
121
 
70
122
  for (const [name, value] of Object.entries(module)) {
71
- if (name === 'default') continue;
123
+ if (name === "default") continue;
72
124
 
73
125
  // Check for new RouteDefinition format
74
- if (value && typeof value === 'object' && 'entry' in value && 'options' in value && 'handler' in value) {
126
+ if (
127
+ value &&
128
+ typeof value === "object" &&
129
+ "entry" in value &&
130
+ "options" in value &&
131
+ "handler" in value
132
+ ) {
75
133
  const routeDef = value as any;
76
134
  routes.push({
77
135
  name,
@@ -103,7 +161,7 @@ export class RouteScanner {
103
161
  }
104
162
 
105
163
  enableWatch(callback: () => void) {
106
- if (typeof Bun !== 'undefined' && Bun.env.NODE_ENV === 'development') {
164
+ if (typeof Bun !== "undefined" && Bun.env.NODE_ENV === "development") {
107
165
  console.log(`Watching for route changes in ${this.routesDir}`);
108
166
 
109
167
  setInterval(async () => {
@@ -45,7 +45,13 @@ export class MiddlewareManager<
45
45
  let currentResponse = response;
46
46
 
47
47
  for (const handler of this.finallyHandlers) {
48
- currentResponse = await handler(currentResponse, request);
48
+ try {
49
+ currentResponse = await handler(currentResponse, request);
50
+ } catch (error) {
51
+ // Log but don't throw - we don't want to break the response chain
52
+ console.error('After middleware error:', error);
53
+ // Continue with the current response
54
+ }
49
55
  }
50
56
 
51
57
  return currentResponse;
@@ -85,6 +85,7 @@ export interface VectorConfig<TTypes extends VectorTypes = DefaultVectorTypes> {
85
85
  before?: BeforeMiddlewareHandler<TTypes>[];
86
86
  finally?: AfterMiddlewareHandler<TTypes>[];
87
87
  routesDir?: string;
88
+ routeExcludePatterns?: string[];
88
89
  autoDiscover?: boolean;
89
90
  idleTimeout?: number;
90
91
  }
@@ -97,6 +98,7 @@ export interface VectorConfigSchema<TTypes extends VectorTypes = DefaultVectorTy
97
98
  reusePort?: boolean;
98
99
  development?: boolean;
99
100
  routesDir?: string;
101
+ routeExcludePatterns?: string[];
100
102
  idleTimeout?: number;
101
103
 
102
104
  // Middleware functions