vibe-gx 1.0.3 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-gx",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A lightweight, regex-based Node.js web framework built for speed and simplicity.",
5
5
  "type": "module",
6
6
  "main": "vibe.js",
@@ -54,6 +54,19 @@ function parseMultipart(req, res, media, options, resolve, reject) {
54
54
  let bb;
55
55
  let fileError = null;
56
56
  const streaming = media.streaming === true;
57
+ let pendingWrites = 0;
58
+ let busboyFinished = false;
59
+
60
+ // Helper to check if we're done
61
+ const checkComplete = () => {
62
+ if (busboyFinished && pendingWrites === 0) {
63
+ if (fileError) {
64
+ reject(fileError);
65
+ } else {
66
+ resolve();
67
+ }
68
+ }
69
+ };
57
70
 
58
71
  try {
59
72
  bb = busboy({
@@ -75,9 +88,15 @@ function parseMultipart(req, res, media, options, resolve, reject) {
75
88
  const { filename, mimeType } = info;
76
89
  if (!filename) return file.resume();
77
90
 
78
- // File type validation
91
+ // File type validation - support wildcards like "image/*"
79
92
  if (media.allowedTypes && Array.isArray(media.allowedTypes)) {
80
- if (!media.allowedTypes.includes(mimeType)) {
93
+ const isAllowed = media.allowedTypes.some((allowed) => {
94
+ if (allowed.endsWith("/*")) {
95
+ return mimeType.startsWith(allowed.slice(0, -1));
96
+ }
97
+ return allowed === mimeType;
98
+ });
99
+ if (!isAllowed) {
81
100
  fileError = new Error(
82
101
  `File type '${mimeType}' not allowed. Allowed: ${media.allowedTypes.join(", ")}`,
83
102
  );
@@ -92,6 +111,8 @@ function parseMultipart(req, res, media, options, resolve, reject) {
92
111
  }
93
112
 
94
113
  // BUFFERING MODE: Write to disk
114
+ pendingWrites++;
115
+
95
116
  const parent = media.public ? options.publicFolder || "" : "";
96
117
  const dest = path.resolve(
97
118
  path.join(parent, media.dest || (media.public ? "uploads" : "private")),
@@ -103,6 +124,8 @@ function parseMultipart(req, res, media, options, resolve, reject) {
103
124
  !dest.startsWith(path.resolve(options.publicFolder || ""))
104
125
  ) {
105
126
  console.warn("Attempted upload outside public folder, skipping");
127
+ pendingWrites--;
128
+ checkComplete();
106
129
  return file.resume();
107
130
  }
108
131
 
@@ -110,6 +133,8 @@ function parseMultipart(req, res, media, options, resolve, reject) {
110
133
  if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
111
134
  } catch (err) {
112
135
  console.error("Failed to create upload folder:", err);
136
+ pendingWrites--;
137
+ checkComplete();
113
138
  return file.resume();
114
139
  }
115
140
 
@@ -137,17 +162,24 @@ function parseMultipart(req, res, media, options, resolve, reject) {
137
162
  file.unpipe(writeStream);
138
163
  writeStream.end();
139
164
  // Clean up partial file
140
- fs.unlink(filePath, () => {});
165
+ fs.unlink(filePath, () => {
166
+ pendingWrites--;
167
+ checkComplete();
168
+ });
141
169
  });
142
170
 
143
171
  file.on("error", (err) => {
144
172
  console.error("File stream error:", err);
145
173
  writeStream.end();
174
+ pendingWrites--;
175
+ checkComplete();
146
176
  });
147
177
 
148
178
  writeStream.on("error", (err) => {
149
179
  console.error("Write stream error:", err);
150
180
  file.resume();
181
+ pendingWrites--;
182
+ checkComplete();
151
183
  });
152
184
 
153
185
  writeStream.on("finish", () => {
@@ -160,6 +192,8 @@ function parseMultipart(req, res, media, options, resolve, reject) {
160
192
  size,
161
193
  });
162
194
  }
195
+ pendingWrites--;
196
+ checkComplete();
163
197
  });
164
198
 
165
199
  file.pipe(writeStream);
@@ -172,11 +206,8 @@ function parseMultipart(req, res, media, options, resolve, reject) {
172
206
  });
173
207
 
174
208
  bb.on("finish", () => {
175
- if (fileError) {
176
- reject(fileError);
177
- } else {
178
- resolve();
179
- }
209
+ busboyFinished = true;
210
+ checkComplete();
180
211
  });
181
212
 
182
213
  req.pipe(bb);
package/vibe.d.ts CHANGED
@@ -26,15 +26,26 @@ export interface UploadedFile {
26
26
 
27
27
  /**
28
28
  * Configuration for file uploads on a specific route.
29
+ *
30
+ * @example
31
+ * {
32
+ * dest: "uploads",
33
+ * maxSize: 5 * 1024 * 1024, // 5MB
34
+ * allowedTypes: ["image/jpeg", "image/png"],
35
+ * public: true
36
+ * }
29
37
  */
30
38
  export interface MediaOptions {
31
39
  /** Save file inside the configured public folder. Default: true */
32
40
  public?: boolean;
33
41
  /** Subfolder destination for uploads (e.g., "uploads/avatars") */
34
42
  dest?: string;
35
- /** Maximum allowed file size in bytes. Default: 10 MB */
43
+ /** Maximum allowed file size in bytes. Default: 10 MB (10485760) */
36
44
  maxSize?: number;
37
- /** Allowed MIME types (e.g., ["image/png", "image/jpeg"]) */
45
+ /**
46
+ * Allowed MIME types. Supports wildcards like "image/*"
47
+ * @example ["image/jpeg", "image/png", "application/pdf"]
48
+ */
38
49
  allowedTypes?: string[];
39
50
  /** Enable streaming mode for large files. Use req.on('file', ...) */
40
51
  streaming?: boolean;
@@ -42,11 +53,40 @@ export interface MediaOptions {
42
53
 
43
54
  /**
44
55
  * Options for registering a route.
56
+ *
57
+ * @example
58
+ * // With interceptor only
59
+ * { intercept: authMiddleware }
60
+ *
61
+ * @example
62
+ * // With file upload
63
+ * {
64
+ * intercept: authMiddleware,
65
+ * media: {
66
+ * dest: "uploads",
67
+ * maxSize: 10 * 1024 * 1024,
68
+ * allowedTypes: ["image/*"]
69
+ * }
70
+ * }
45
71
  */
46
72
  export interface RouteOptions {
47
- /** Middleware(s) to run before the main handler */
73
+ /**
74
+ * Middleware function(s) to run before the handler.
75
+ * Return false to stop execution.
76
+ * @example
77
+ * intercept: (req, res) => {
78
+ * if (!req.headers.authorization) {
79
+ * res.unauthorized();
80
+ * return false;
81
+ * }
82
+ * return true;
83
+ * }
84
+ */
48
85
  intercept?: Interceptor | Interceptor[];
49
- /** Configuration for file uploads */
86
+ /**
87
+ * Configuration for file uploads (multipart/form-data).
88
+ * Files will be available in req.files array.
89
+ */
50
90
  media?: MediaOptions;
51
91
  }
52
92
 
@@ -175,13 +215,45 @@ export const color: Record<ColorName, (text: string) => string>;
175
215
 
176
216
  /**
177
217
  * Route registration function.
178
- * Supports two signatures:
179
- * 1. (path, handler)
180
- * 2. (path, options, handler)
218
+ *
219
+ * @example
220
+ * // Simple handler
221
+ * app.get("/path", (req, res) => { ... });
222
+ *
223
+ * @example
224
+ * // Static response
225
+ * app.get("/", "Hello World");
226
+ *
227
+ * @example
228
+ * // With options (interceptor + file upload)
229
+ * app.post("/upload", {
230
+ * intercept: authMiddleware,
231
+ * media: {
232
+ * dest: "uploads",
233
+ * maxSize: 10 * 1024 * 1024,
234
+ * allowedTypes: ["image/*"]
235
+ * }
236
+ * }, handler);
181
237
  */
182
238
  export interface RouteRegistrar {
239
+ /**
240
+ * Register a route with a handler or static response.
241
+ * @param path - Route path (e.g., "/users/:id")
242
+ * @param handler - Handler function, string, number, or object
243
+ */
183
244
  (path: string, handler: Handler | string | number | object): void;
184
- (path: string, options: RouteOptions, handler: Handler): void;
245
+
246
+ /**
247
+ * Register a route with options and handler.
248
+ * @param path - Route path (e.g., "/upload")
249
+ * @param options - Route options (intercept, media)
250
+ * @param handler - Handler function
251
+ */
252
+ (
253
+ path: string,
254
+ options: RouteOptions,
255
+ handler: Handler | string | number | object,
256
+ ): void;
185
257
  }
186
258
 
187
259
  /** Sub-router or prefixed router instance */