vector-framework 0.9.9 → 1.1.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 (54) hide show
  1. package/README.md +6 -5
  2. package/dist/cache/manager.d.ts +5 -2
  3. package/dist/cache/manager.d.ts.map +1 -1
  4. package/dist/cache/manager.js +21 -7
  5. package/dist/cache/manager.js.map +1 -1
  6. package/dist/cli/index.js +76 -98
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli.js +134 -69
  9. package/dist/core/config-loader.d.ts +2 -2
  10. package/dist/core/config-loader.d.ts.map +1 -1
  11. package/dist/core/config-loader.js +16 -21
  12. package/dist/core/config-loader.js.map +1 -1
  13. package/dist/core/router.d.ts +2 -0
  14. package/dist/core/router.d.ts.map +1 -1
  15. package/dist/core/router.js +52 -16
  16. package/dist/core/router.js.map +1 -1
  17. package/dist/core/server.d.ts +4 -3
  18. package/dist/core/server.d.ts.map +1 -1
  19. package/dist/core/server.js +40 -20
  20. package/dist/core/server.js.map +1 -1
  21. package/dist/core/vector.d.ts +7 -7
  22. package/dist/core/vector.d.ts.map +1 -1
  23. package/dist/core/vector.js +20 -21
  24. package/dist/core/vector.js.map +1 -1
  25. package/dist/dev/route-scanner.d.ts +1 -1
  26. package/dist/dev/route-scanner.d.ts.map +1 -1
  27. package/dist/dev/route-scanner.js +40 -42
  28. package/dist/dev/route-scanner.js.map +1 -1
  29. package/dist/http.d.ts +2 -2
  30. package/dist/http.d.ts.map +1 -1
  31. package/dist/http.js +70 -63
  32. package/dist/http.js.map +1 -1
  33. package/dist/index.d.ts +3 -3
  34. package/dist/index.js +4 -4
  35. package/dist/index.mjs +4 -4
  36. package/dist/middleware/manager.d.ts +1 -1
  37. package/dist/middleware/manager.d.ts.map +1 -1
  38. package/dist/middleware/manager.js.map +1 -1
  39. package/dist/utils/path.d.ts +1 -0
  40. package/dist/utils/path.d.ts.map +1 -1
  41. package/dist/utils/path.js +9 -3
  42. package/dist/utils/path.js.map +1 -1
  43. package/package.json +14 -9
  44. package/src/cache/manager.ts +23 -14
  45. package/src/cli/index.ts +83 -117
  46. package/src/core/config-loader.ts +19 -27
  47. package/src/core/router.ts +52 -18
  48. package/src/core/server.ts +43 -30
  49. package/src/core/vector.ts +25 -35
  50. package/src/dev/route-scanner.ts +41 -47
  51. package/src/http.ts +82 -112
  52. package/src/index.ts +3 -3
  53. package/src/middleware/manager.ts +4 -11
  54. package/src/utils/path.ts +13 -4
@@ -1,56 +1,66 @@
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";
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';
10
5
 
11
6
  export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
12
7
  private server: Server | null = null;
13
8
  private router: VectorRouter<TTypes>;
14
9
  private config: VectorConfig<TTypes>;
15
10
  private corsHandler: any;
11
+ private corsHeaders: Record<string, string> | null = null;
16
12
 
17
13
  constructor(router: VectorRouter<TTypes>, config: VectorConfig<TTypes>) {
18
14
  this.router = router;
19
15
  this.config = config;
20
16
 
21
17
  if (config.cors) {
22
- const { preflight, corsify } = cors(
23
- this.normalizeCorsOptions(config.cors)
24
- );
18
+ const opts = this.normalizeCorsOptions(config.cors);
19
+ const { preflight, corsify } = cors(opts);
25
20
  this.corsHandler = { preflight, corsify };
21
+
22
+ // Pre-build static CORS headers when origin is a fixed string.
23
+ // Avoids cloning the Response on every request via corsify().
24
+ if (typeof opts.origin === 'string') {
25
+ this.corsHeaders = {
26
+ 'access-control-allow-origin': opts.origin,
27
+ 'access-control-allow-methods': opts.allowMethods,
28
+ 'access-control-allow-headers': opts.allowHeaders,
29
+ 'access-control-expose-headers': opts.exposeHeaders,
30
+ 'access-control-max-age': String(opts.maxAge),
31
+ };
32
+ if (opts.credentials) {
33
+ this.corsHeaders['access-control-allow-credentials'] = 'true';
34
+ }
35
+ }
26
36
  }
27
37
  }
28
38
 
29
39
  private normalizeCorsOptions(options: CorsOptions): any {
30
40
  return {
31
- origin: options.origin || "*",
41
+ origin: options.origin || '*',
32
42
  credentials: options.credentials !== false,
33
43
  allowHeaders: Array.isArray(options.allowHeaders)
34
- ? options.allowHeaders.join(", ")
35
- : options.allowHeaders || "Content-Type, Authorization",
44
+ ? options.allowHeaders.join(', ')
45
+ : options.allowHeaders || 'Content-Type, Authorization',
36
46
  allowMethods: Array.isArray(options.allowMethods)
37
- ? options.allowMethods.join(", ")
38
- : options.allowMethods || "GET, POST, PUT, PATCH, DELETE, OPTIONS",
47
+ ? options.allowMethods.join(', ')
48
+ : options.allowMethods || 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
39
49
  exposeHeaders: Array.isArray(options.exposeHeaders)
40
- ? options.exposeHeaders.join(", ")
41
- : options.exposeHeaders || "Authorization",
50
+ ? options.exposeHeaders.join(', ')
51
+ : options.exposeHeaders || 'Authorization',
42
52
  maxAge: options.maxAge || 86400,
43
53
  };
44
54
  }
45
55
 
46
56
  async start(): Promise<Server> {
47
57
  const port = this.config.port || 3000;
48
- const hostname = this.config.hostname || "localhost";
58
+ const hostname = this.config.hostname || 'localhost';
49
59
 
50
60
  const fetch = async (request: Request): Promise<Response> => {
51
61
  try {
52
62
  // Handle CORS preflight
53
- if (this.corsHandler && request.method === "OPTIONS") {
63
+ if (this.corsHandler && request.method === 'OPTIONS') {
54
64
  return this.corsHandler.preflight(request);
55
65
  }
56
66
 
@@ -58,14 +68,18 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
58
68
  let response = await this.router.handle(request);
59
69
 
60
70
  // Apply CORS headers if configured
61
- if (this.corsHandler) {
71
+ if (this.corsHeaders) {
72
+ for (const [k, v] of Object.entries(this.corsHeaders)) {
73
+ response.headers.set(k, v);
74
+ }
75
+ } else if (this.corsHandler) {
62
76
  response = this.corsHandler.corsify(response, request);
63
77
  }
64
78
 
65
79
  return response;
66
80
  } catch (error) {
67
- console.error("Server error:", error);
68
- return new Response("Internal Server Error", { status: 500 });
81
+ console.error('Server error:', error);
82
+ return new Response('Internal Server Error', { status: 500 });
69
83
  }
70
84
  };
71
85
 
@@ -77,8 +91,8 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
77
91
  fetch,
78
92
  idleTimeout: this.config.idleTimeout || 60,
79
93
  error: (error) => {
80
- console.error("[ERROR] Server error:", error);
81
- return new Response("Internal Server Error", { status: 500 });
94
+ console.error('[ERROR] Server error:', error);
95
+ return new Response('Internal Server Error', { status: 500 });
82
96
  },
83
97
  });
84
98
 
@@ -87,8 +101,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
87
101
  throw new Error(`Failed to start server on ${hostname}:${port} - server object is invalid`);
88
102
  }
89
103
 
90
- // Server logs are handled by CLI, keep this minimal
91
- console.log(`→ Vector server running at http://${hostname}:${port}`);
104
+ // Server logs are handled by CLI
92
105
 
93
106
  return this.server;
94
107
  } catch (error: any) {
@@ -112,7 +125,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
112
125
  if (this.server) {
113
126
  this.server.stop();
114
127
  this.server = null;
115
- console.log("Server stopped");
128
+ console.log('Server stopped');
116
129
  }
117
130
  }
118
131
 
@@ -125,7 +138,7 @@ export class VectorServer<TTypes extends VectorTypes = DefaultVectorTypes> {
125
138
  }
126
139
 
127
140
  getHostname(): string {
128
- return this.server?.hostname || this.config.hostname || "localhost";
141
+ return this.server?.hostname || this.config.hostname || 'localhost';
129
142
  }
130
143
 
131
144
  getUrl(): string {
@@ -1,11 +1,11 @@
1
- import type { Server } from "bun";
2
- import type { RouteEntry } from "itty-router";
3
- import { AuthManager } from "../auth/protected";
4
- import { CacheManager } from "../cache/manager";
5
- import { RouteGenerator } from "../dev/route-generator";
6
- import { RouteScanner } from "../dev/route-scanner";
7
- import { MiddlewareManager } from "../middleware/manager";
8
- import { toFileUrl } from "../utils/path";
1
+ import type { Server } from 'bun';
2
+ import type { RouteEntry } from 'itty-router';
3
+ import { AuthManager } from '../auth/protected';
4
+ import { CacheManager } from '../cache/manager';
5
+ import { RouteGenerator } from '../dev/route-generator';
6
+ import { RouteScanner } from '../dev/route-scanner';
7
+ import { MiddlewareManager } from '../middleware/manager';
8
+ import { toFileUrl } from '../utils/path';
9
9
  import type {
10
10
  CacheHandler,
11
11
  DefaultVectorTypes,
@@ -14,9 +14,9 @@ import type {
14
14
  RouteOptions,
15
15
  VectorConfig,
16
16
  VectorTypes,
17
- } from "../types";
18
- import { VectorRouter } from "./router";
19
- import { VectorServer } from "./server";
17
+ } from '../types';
18
+ import { VectorRouter } from './router';
19
+ import { VectorServer } from './server';
20
20
 
21
21
  // Internal-only class - not exposed to users
22
22
  export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
@@ -72,10 +72,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
72
72
  }
73
73
 
74
74
  // Internal method to add route
75
- addRoute(
76
- options: RouteOptions<TTypes>,
77
- handler: RouteHandler<TTypes>
78
- ): RouteEntry {
75
+ addRoute(options: RouteOptions<TTypes>, handler: RouteHandler<TTypes>): RouteEntry {
79
76
  return this.router.route(options, handler);
80
77
  }
81
78
 
@@ -110,7 +107,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
110
107
  }
111
108
 
112
109
  private async discoverRoutes() {
113
- const routesDir = this.config.routesDir || "./routes";
110
+ const routesDir = this.config.routesDir || './routes';
114
111
  const excludePatterns = this.config.routeExcludePatterns;
115
112
 
116
113
  // Always create a new RouteScanner with the current config's routesDir
@@ -134,8 +131,7 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
134
131
  const importPath = toFileUrl(route.path);
135
132
 
136
133
  const module = await import(importPath);
137
- const exported =
138
- route.name === "default" ? module.default : module[route.name];
134
+ const exported = route.name === 'default' ? module.default : module[route.name];
139
135
 
140
136
  if (exported) {
141
137
  if (this.isRouteDefinition(exported)) {
@@ -147,16 +143,13 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
147
143
  // Legacy support for direct RouteEntry (won't have middleware)
148
144
  this.router.addRoute(exported as RouteEntry);
149
145
  this.logRouteLoaded(exported as RouteEntry);
150
- } else if (typeof exported === "function") {
146
+ } else if (typeof exported === 'function') {
151
147
  this.router.route(route.options as any, exported);
152
148
  this.logRouteLoaded(route.options);
153
149
  }
154
150
  }
155
151
  } catch (error) {
156
- console.error(
157
- `Failed to load route ${route.name} from ${route.path}:`,
158
- error
159
- );
152
+ console.error(`Failed to load route ${route.name} from ${route.path}:`, error);
160
153
  }
161
154
  }
162
155
 
@@ -164,24 +157,21 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
164
157
  this.router.sortRoutes();
165
158
  }
166
159
  } catch (error) {
167
- if (
168
- (error as any).code !== "ENOENT" &&
169
- (error as any).code !== "ENOTDIR"
170
- ) {
171
- console.error("Failed to discover routes:", error);
160
+ if ((error as any).code !== 'ENOENT' && (error as any).code !== 'ENOTDIR') {
161
+ console.error('Failed to discover routes:', error);
172
162
  }
173
163
  }
174
164
  }
175
165
 
176
166
  async loadRoute(routeModule: any) {
177
- if (typeof routeModule === "function") {
167
+ if (typeof routeModule === 'function') {
178
168
  const routeEntry = routeModule();
179
169
  if (Array.isArray(routeEntry)) {
180
170
  this.router.addRoute(routeEntry as RouteEntry);
181
171
  }
182
- } else if (routeModule && typeof routeModule === "object") {
172
+ } else if (routeModule && typeof routeModule === 'object') {
183
173
  for (const [, value] of Object.entries(routeModule)) {
184
- if (typeof value === "function") {
174
+ if (typeof value === 'function') {
185
175
  const routeEntry = (value as any)();
186
176
  if (Array.isArray(routeEntry)) {
187
177
  this.router.addRoute(routeEntry as RouteEntry);
@@ -198,10 +188,10 @@ export class Vector<TTypes extends VectorTypes = DefaultVectorTypes> {
198
188
  private isRouteDefinition(value: any): boolean {
199
189
  return (
200
190
  value &&
201
- typeof value === "object" &&
202
- "entry" in value &&
203
- "options" in value &&
204
- "handler" in value
191
+ typeof value === 'object' &&
192
+ 'entry' in value &&
193
+ 'options' in value &&
194
+ 'handler' in value
205
195
  );
206
196
  }
207
197
 
@@ -1,32 +1,31 @@
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
7
  private excludePatterns: string[];
8
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",
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
23
  ];
24
24
 
25
- constructor(routesDir = "./routes", excludePatterns?: string[]) {
25
+ constructor(routesDir = './routes', excludePatterns?: string[]) {
26
26
  // Always resolve from the current working directory (user's project)
27
27
  this.routesDir = resolve(process.cwd(), routesDir);
28
- this.excludePatterns =
29
- excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
28
+ this.excludePatterns = excludePatterns || RouteScanner.DEFAULT_EXCLUDE_PATTERNS;
30
29
  }
31
30
 
32
31
  async scan(): Promise<GeneratedRoute[]> {
@@ -40,7 +39,7 @@ export class RouteScanner {
40
39
  try {
41
40
  await this.scanDirectory(this.routesDir, routes);
42
41
  } catch (error) {
43
- if ((error as any).code === "ENOENT") {
42
+ if ((error as any).code === 'ENOENT') {
44
43
  console.warn(` ✗ Routes directory not accessible: ${this.routesDir}`);
45
44
  return [];
46
45
  }
@@ -56,15 +55,16 @@ export class RouteScanner {
56
55
  for (const pattern of this.excludePatterns) {
57
56
  // Convert glob pattern to regex
58
57
  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
58
+ .replace(/\./g, '\\.') // Escape dots
59
+ .replace(/\*\*/g, '__GLOBSTAR__') // protect ** before * replacement
60
+ .replace(/\*/g, '[^/]*') // * matches anything except /
61
+ .replace(/__GLOBSTAR__/g, '.*') // ** matches anything including /
62
+ .replace(/\?/g, '.'); // ? matches single character
63
63
 
64
64
  const regex = new RegExp(`^${regexPattern}$`);
65
65
 
66
66
  // Check both the full relative path and just the filename
67
- const filename = relativePath.split(sep).pop() || "";
67
+ const filename = relativePath.split(sep).pop() || '';
68
68
  if (regex.test(relativePath) || regex.test(filename)) {
69
69
  return true;
70
70
  }
@@ -73,11 +73,7 @@ export class RouteScanner {
73
73
  return false;
74
74
  }
75
75
 
76
- private async scanDirectory(
77
- dir: string,
78
- routes: GeneratedRoute[],
79
- basePath = ""
80
- ): Promise<void> {
76
+ private async scanDirectory(dir: string, routes: GeneratedRoute[], basePath = ''): Promise<void> {
81
77
  const entries = await fs.readdir(dir);
82
78
 
83
79
  for (const entry of entries) {
@@ -87,32 +83,30 @@ export class RouteScanner {
87
83
  if (stats.isDirectory()) {
88
84
  const newBasePath = basePath ? `${basePath}/${entry}` : entry;
89
85
  await this.scanDirectory(fullPath, routes, newBasePath);
90
- } else if (entry.endsWith(".ts") || entry.endsWith(".js")) {
86
+ } else if (entry.endsWith('.ts') || entry.endsWith('.js')) {
91
87
  // Skip excluded files (test files, etc.)
92
88
  if (this.isExcluded(fullPath)) {
93
89
  continue;
94
90
  }
95
91
  const routePath = relative(this.routesDir, fullPath)
96
- .replace(/\.(ts|js)$/, "")
92
+ .replace(/\.(ts|js)$/, '')
97
93
  .split(sep)
98
- .join("/");
94
+ .join('/');
99
95
 
100
96
  try {
101
97
  // Convert Windows paths to URLs for import
102
98
  const importPath =
103
- process.platform === "win32"
104
- ? `file:///${fullPath.replace(/\\/g, "/")}`
105
- : fullPath;
99
+ process.platform === 'win32' ? `file:///${fullPath.replace(/\\/g, '/')}` : fullPath;
106
100
 
107
101
  const module = await import(importPath);
108
102
 
109
- if (module.default && typeof module.default === "function") {
103
+ if (module.default && typeof module.default === 'function') {
110
104
  routes.push({
111
- name: "default",
105
+ name: 'default',
112
106
  path: fullPath,
113
- method: "GET",
107
+ method: 'GET',
114
108
  options: {
115
- method: "GET",
109
+ method: 'GET',
116
110
  path: `/${routePath}`,
117
111
  expose: true,
118
112
  },
@@ -120,15 +114,15 @@ export class RouteScanner {
120
114
  }
121
115
 
122
116
  for (const [name, value] of Object.entries(module)) {
123
- if (name === "default") continue;
117
+ if (name === 'default') continue;
124
118
 
125
119
  // Check for new RouteDefinition format
126
120
  if (
127
121
  value &&
128
- typeof value === "object" &&
129
- "entry" in value &&
130
- "options" in value &&
131
- "handler" in value
122
+ typeof value === 'object' &&
123
+ 'entry' in value &&
124
+ 'options' in value &&
125
+ 'handler' in value
132
126
  ) {
133
127
  const routeDef = value as any;
134
128
  routes.push({
@@ -161,7 +155,7 @@ export class RouteScanner {
161
155
  }
162
156
 
163
157
  enableWatch(callback: () => void) {
164
- if (typeof Bun !== "undefined" && Bun.env.NODE_ENV === "development") {
158
+ if (typeof Bun !== 'undefined' && Bun.env.NODE_ENV === 'development') {
165
159
  console.log(`Watching for route changes in ${this.routesDir}`);
166
160
 
167
161
  setInterval(async () => {