bro-framework 2.0.1 → 2.2.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/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  <a href="https://brojs.yessindevs.me">Documentation Website</a>
18
18
  </p>
19
19
  </p>
20
+
20
21
  ---
21
22
 
22
23
  > "NestJS wants four decorators, three modules, and an existential crisis just to handle a GET request. Express makes you write the same 40 lines of CORS, JSON parsing, and auth middleware for every project. bro.js gives you file routing, auto-validation, JWT auth, WebSockets, and live docs out of the box. Be honest: you just want to return an object."
package/bin/bro.js CHANGED
@@ -9,7 +9,7 @@ register();
9
9
 
10
10
  import { createServer } from '../src/server.js';
11
11
  import { colors, printBanner, printRoute, printHotReload } from '../src/logger.js';
12
- import { scanTasks } from '../src/tasks.js';
12
+ import { scanTasks, stopTasks } from '../src/tasks.js';
13
13
  import { generateSDK } from '../src/sdk.js';
14
14
  import dotenv from 'dotenv';
15
15
  import chokidar from 'chokidar';
@@ -122,7 +122,7 @@ if (command === 'init') {
122
122
 
123
123
  if (['sdk', 'generate-client', 'client'].includes(command)) {
124
124
  generateSDK().then(() => {
125
- console.log(`\n ${colors.green} bro-client.js generated successfully!${colors.reset}\n`);
125
+ console.log(`\n ${colors.green} bro-sdk.js generated successfully!${colors.reset}\n`);
126
126
  process.exit(0);
127
127
  }).catch(err => {
128
128
  console.error(`\n ${colors.red} Error generating SDK:${colors.reset}`, err.message);
@@ -182,6 +182,7 @@ async function bootstrap() {
182
182
  console.error("");
183
183
  process.exit(1);
184
184
  }
185
+ globalConfig.envData = envResult.data;
185
186
  }
186
187
 
187
188
  if (!fs.existsSync(routesDir)) {
@@ -236,6 +237,19 @@ async function bootstrap() {
236
237
  }
237
238
  });
238
239
  }
240
+
241
+ const handleShutdown = async (signal) => {
242
+ console.log(`\n[bro.js] Received ${signal}. Shutting down gracefully...`);
243
+ stopTasks();
244
+ if (io) io.close();
245
+ server.close(() => {
246
+ console.log('[bro.js] HTTP server closed.');
247
+ process.exit(0);
248
+ });
249
+ };
250
+
251
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
252
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
239
253
  });
240
254
  }
241
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bro-framework",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "The No-BS Backend Framework for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
package/src/auth.js CHANGED
@@ -1,31 +1,23 @@
1
1
  import jwt from 'jsonwebtoken';
2
2
 
3
- let secret = 'bro_default_secret_key';
4
-
5
- /**
6
- * Update the secret used for signing and verifying JWTs.
7
- * @param {string} newSecret
8
- */
9
- export function setJwtSecret(newSecret) {
10
- secret = newSecret;
11
- }
12
-
13
3
  /**
14
4
  * Signs a JWT payload.
15
5
  * @param {Object} payload - The data to embed in the token.
6
+ * @param {string} secret - The JWT secret key.
16
7
  * @param {jwt.SignOptions} [options] - jsonwebtoken sign options.
17
8
  * @returns {string} The signed JWT token.
18
9
  */
19
- export function signJwt(payload, options = { expiresIn: '1d' }) {
10
+ export function signJwt(payload, secret, options = { expiresIn: '1d' }) {
20
11
  return jwt.sign(payload, secret, options);
21
12
  }
22
13
 
23
14
  /**
24
15
  * Verifies and decodes a JWT token.
25
16
  * @param {string} token - The JWT token to verify.
17
+ * @param {string} secret - The JWT secret key.
26
18
  * @returns {{ valid: boolean, payload?: any, error?: string }}
27
19
  */
28
- export function verifyJwt(token) {
20
+ export function verifyJwt(token, secret) {
29
21
  try {
30
22
  const payload = jwt.verify(token, secret);
31
23
  return { valid: true, payload };
package/src/index.d.ts CHANGED
@@ -3,6 +3,8 @@ import { z, ZodTypeAny } from 'zod';
3
3
  type InferZod<T> = T extends ZodTypeAny ? z.infer<T> : any;
4
4
 
5
5
  export interface BroContext<Body = any, Params = any, Query = any> {
6
+ env?: any;
7
+ jwt?: { sign: (payload: any, options?: any) => string };
6
8
  body: InferZod<Body>;
7
9
  params: InferZod<Params>;
8
10
  query: InferZod<Query>;
@@ -15,7 +17,8 @@ export interface BroContext<Body = any, Params = any, Query = any> {
15
17
 
16
18
  export interface RouteConfig<Body = any, Params = any, Query = any> {
17
19
  auth?: boolean;
18
- upload?: boolean;
20
+ upload?: boolean | { limits?: any, fields?: { name: string, maxCount?: number }[], single?: string, array?: string, fileFilter?: any, storage?: any };
21
+ schema?: { body?: Body; params?: Params; query?: Query; };
19
22
  body?: Body;
20
23
  params?: Params;
21
24
  query?: Query;
@@ -46,6 +49,14 @@ export interface BroConfig {
46
49
  windowMs: number;
47
50
  max: number;
48
51
  };
52
+ upload?: {
53
+ limits?: {
54
+ fileSize?: number;
55
+ files?: number;
56
+ fields?: number;
57
+ [key: string]: any;
58
+ };
59
+ };
49
60
  db?: () => Promise<any> | any;
50
61
  sockets?: (io: any, db: any) => Promise<void> | void;
51
62
  }
package/src/router.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { pathToFileURL } from 'url';
4
+ import crypto from 'crypto';
4
5
  import { zodToJsonSchema } from 'zod-to-json-schema';
5
6
 
6
7
  /**
@@ -48,10 +49,11 @@ export function parseRouteFile(filePath, routesDir) {
48
49
  if (routePath === '/.') routePath = '';
49
50
 
50
51
  if (namePart !== 'index') {
51
- const formattedName = namePart.replace(/\[(.*?)\]/g, ':$1');
52
- routePath += `/${formattedName}`;
52
+ routePath += `/${namePart}`;
53
53
  }
54
54
 
55
+ routePath = routePath.replace(/\[(.*?)\]/g, ':$1');
56
+
55
57
  if (routePath === '') routePath = '/';
56
58
 
57
59
  return { routePath, method };
@@ -68,6 +70,7 @@ export function parseRouteFile(filePath, routesDir) {
68
70
  export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
69
71
  const files = scanDir(routesDir);
70
72
  const loadedRoutes = [];
73
+ const routeModules = [];
71
74
 
72
75
  for (const file of files) {
73
76
  const routeInfo = parseRouteFile(file, routesDir);
@@ -77,68 +80,92 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
77
80
 
78
81
  if (typeof app[method] !== 'function') continue;
79
82
 
80
- try {
81
- const moduleUrl = pathToFileURL(file).href + '?update=' + Date.now();
82
- const module = await import(moduleUrl);
83
- const config = module.default;
84
-
85
- if (!config) continue;
83
+ const moduleUrl = pathToFileURL(file).href + '?update=' + crypto.randomUUID();
84
+ const module = await import(moduleUrl);
85
+ const config = module.default;
86
+
87
+ if (!config) continue;
88
+
89
+ routeModules.push({ file, routePath, method, config });
90
+ }
91
+
92
+ for (const { file, routePath, method, config } of routeModules) {
93
+ const handler = createHandler(config);
94
+ app[method](routePath, handler);
95
+
96
+ if (openApiSpec) {
97
+ const openApiPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
98
+ if (!openApiSpec.paths[openApiPath]) openApiSpec.paths[openApiPath] = {};
86
99
 
87
- const handler = createHandler(config);
88
- app[method](routePath, handler);
100
+ const operation = {
101
+ summary: config.summary || `${method.toUpperCase()} ${routePath}`,
102
+ responses: { '200': { description: 'Successful response' } }
103
+ };
89
104
 
90
- if (openApiSpec) {
91
- const openApiPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
92
- if (!openApiSpec.paths[openApiPath]) openApiSpec.paths[openApiPath] = {};
93
-
94
- const operation = {
95
- summary: config.summary || `${method.toUpperCase()} ${routePath}`,
96
- responses: { '200': { description: 'Successful response' } }
105
+ const bodySchema = config.schema?.body || config.body;
106
+ const paramsSchema = config.schema?.params || config.params;
107
+ const querySchema = config.schema?.query || config.query;
108
+
109
+ if (bodySchema) {
110
+ operation.requestBody = {
111
+ content: { 'application/json': { schema: zodToJsonSchema(bodySchema) } }
97
112
  };
98
-
99
- if (config.body) {
100
- operation.requestBody = {
101
- content: { 'application/json': { schema: zodToJsonSchema(config.body) } }
102
- };
103
- }
104
-
105
- if (config.params) {
106
- operation.parameters = operation.parameters || [];
107
- const pSchema = zodToJsonSchema(config.params);
108
- if (pSchema.properties) {
109
- for (const [key, schema] of Object.entries(pSchema.properties)) {
110
- operation.parameters.push({ name: key, in: 'path', required: true, schema });
111
- }
113
+ }
114
+
115
+ if (paramsSchema) {
116
+ operation.parameters = operation.parameters || [];
117
+ const pSchema = zodToJsonSchema(paramsSchema);
118
+ if (pSchema.properties) {
119
+ for (const [key, schema] of Object.entries(pSchema.properties)) {
120
+ operation.parameters.push({ name: key, in: 'path', required: true, schema });
112
121
  }
113
122
  }
123
+ }
114
124
 
115
- if (config.query) {
116
- operation.parameters = operation.parameters || [];
117
- const qSchema = zodToJsonSchema(config.query);
118
- if (qSchema.properties) {
119
- for (const [key, schema] of Object.entries(qSchema.properties)) {
120
- operation.parameters.push({
121
- name: key,
122
- in: 'query',
123
- required: qSchema.required?.includes(key),
124
- schema
125
- });
126
- }
125
+ if (querySchema) {
126
+ operation.parameters = operation.parameters || [];
127
+ const qSchema = zodToJsonSchema(querySchema);
128
+ if (qSchema.properties) {
129
+ for (const [key, schema] of Object.entries(qSchema.properties)) {
130
+ operation.parameters.push({
131
+ name: key,
132
+ in: 'query',
133
+ required: qSchema.required?.includes(key),
134
+ schema
135
+ });
127
136
  }
128
137
  }
129
-
130
- openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
131
138
  }
132
139
 
133
- loadedRoutes.push({
134
- method: method.toUpperCase(),
135
- path: routePath,
136
- auth: !!config.auth
137
- });
140
+ // Auto-inject security definition if auth is true
141
+ if (config.auth) {
142
+ operation.security = [{ bearerAuth: [] }];
143
+ }
144
+
145
+ if (config.upload) {
146
+ operation.requestBody = operation.requestBody || { content: {} };
147
+ operation.requestBody.content['multipart/form-data'] = {
148
+ schema: { type: 'object' }
149
+ };
150
+ }
151
+
152
+ if (bodySchema || paramsSchema || querySchema) {
153
+ operation.responses['400'] = { $ref: '#/components/responses/BadRequest' };
154
+ }
155
+ if (config.auth) {
156
+ operation.responses['401'] = { $ref: '#/components/responses/Unauthorized' };
157
+ }
158
+ operation.responses['404'] = { $ref: '#/components/responses/NotFound' };
159
+ operation.responses['500'] = { $ref: '#/components/responses/ServerError' };
138
160
 
139
- } catch (err) {
140
- console.error(`[bro.js] Failed to load route ${file}:`, err);
161
+ openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
141
162
  }
163
+
164
+ loadedRoutes.push({
165
+ method: method.toUpperCase(),
166
+ path: routePath,
167
+ auth: !!config.auth
168
+ });
142
169
  }
143
170
 
144
171
  return loadedRoutes;
package/src/sdk.js CHANGED
@@ -44,7 +44,10 @@ async function request(method, path, data) {
44
44
  options.body = JSON.stringify(data);
45
45
  } else if (data && ['GET', 'DELETE'].includes(method.toUpperCase())) {
46
46
  const params = new URLSearchParams(data);
47
- path += '?' + params.toString();
47
+ const qs = params.toString();
48
+ if (qs) {
49
+ path += '?' + qs;
50
+ }
48
51
  }
49
52
 
50
53
  const url = CONFIG.baseURL + path;
@@ -113,12 +116,16 @@ function generateApiObject(endpoints) {
113
116
  function renderTree(node, indent = ' ') {
114
117
  let result = '';
115
118
 
119
+ const isValidIdentifier = (key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key);
120
+
116
121
  for (const [key, val] of Object.entries(node)) {
122
+ const formattedKey = isValidIdentifier(key) ? key : `"${key}"`;
123
+
117
124
  if (val._isParam) {
118
- result += `${indent}${key}: (${key}) => ({\n`;
125
+ result += `${indent}${formattedKey}: (${key}) => ({\n`;
119
126
 
120
127
  for (const [m, p] of Object.entries(val._methods)) {
121
- const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${$1}');
128
+ const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${encodeURIComponent($1)}');
122
129
  result += `${indent} ${m}: (data) => request('${m}', \`${templatedPath}\`, data),\n`;
123
130
  }
124
131
 
@@ -129,7 +136,7 @@ function generateApiObject(endpoints) {
129
136
 
130
137
  result += `${indent}}),\n`;
131
138
  } else {
132
- result += `${indent}${key}: {\n`;
139
+ result += `${indent}${formattedKey}: {\n`;
133
140
  for (const [m, p] of Object.entries(val._methods)) {
134
141
  result += `${indent} ${m}: (data) => request('${m}', '${p}', data),\n`;
135
142
  }
package/src/server.js CHANGED
@@ -5,11 +5,9 @@ import { Server } from 'socket.io';
5
5
  import rateLimit from 'express-rate-limit';
6
6
  import multer from 'multer';
7
7
  import { apiReference } from '@scalar/express-api-reference';
8
- import { verifyJwt, signJwt, setJwtSecret } from './auth.js';
8
+ import { verifyJwt, signJwt } from './auth.js';
9
9
  import { loadRoutes } from './router.js';
10
10
 
11
- const upload = multer();
12
-
13
11
  /**
14
12
  * Creates and configures the core Express server.
15
13
  * @param {Object} globalConfig - User's bro.config.js configurations.
@@ -18,12 +16,19 @@ const upload = multer();
18
16
  * @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, io: import('socket.io').Server }>}
19
17
  */
20
18
  export async function createServer(globalConfig, routesDir, db) {
19
+ if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
20
+ throw new Error('CRITICAL SECURITY ERROR: You are running in production with a default JWT secret! Please set auth.jwtSecret in bro.config.js or via JWT_SECRET environment variable.');
21
+ }
22
+
21
23
  const app = express();
22
24
  const server = http.createServer(app);
23
25
 
24
26
  const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : true;
25
27
 
26
- app.use(cors(typeof corsConfig === 'object' ? corsConfig : {}));
28
+ if (corsConfig !== false) {
29
+ app.use(cors(typeof corsConfig === 'object' ? corsConfig : {}));
30
+ }
31
+
27
32
  app.use(express.json());
28
33
 
29
34
  if (globalConfig.rateLimit) {
@@ -36,24 +41,49 @@ export async function createServer(globalConfig, routesDir, db) {
36
41
  await globalConfig.sockets(io, db);
37
42
  }
38
43
 
39
- if (globalConfig.jwtSecret) {
40
- setJwtSecret(globalConfig.jwtSecret);
41
- }
42
-
43
44
  const createHandler = (routeConfig) => {
44
45
  const middlewares = [];
46
+ const bodySchema = routeConfig.schema?.body || routeConfig.body;
47
+ const paramsSchema = routeConfig.schema?.params || routeConfig.params;
48
+ const querySchema = routeConfig.schema?.query || routeConfig.query;
45
49
 
46
50
  if (routeConfig.rateLimit) {
47
51
  middlewares.push(rateLimit(routeConfig.rateLimit));
48
52
  }
49
53
 
50
54
  if (routeConfig.upload) {
51
- middlewares.push(upload.any());
55
+ const routeMulterConfig = {
56
+ limits: globalConfig.upload?.limits || {
57
+ fileSize: 10 * 1024 * 1024,
58
+ files: 5,
59
+ fields: 20
60
+ }
61
+ };
62
+ if (typeof routeConfig.upload === 'object') {
63
+ if (routeConfig.upload.limits) {
64
+ routeMulterConfig.limits = { ...routeMulterConfig.limits, ...routeConfig.upload.limits };
65
+ }
66
+ if (routeConfig.upload.fileFilter) routeMulterConfig.fileFilter = routeConfig.upload.fileFilter;
67
+ if (routeConfig.upload.storage) routeMulterConfig.storage = routeConfig.upload.storage;
68
+ }
69
+
70
+ const uploadParser = multer(routeMulterConfig);
71
+
72
+ if (typeof routeConfig.upload === 'object' && routeConfig.upload.fields) {
73
+ middlewares.push(uploadParser.fields(routeConfig.upload.fields));
74
+ } else if (typeof routeConfig.upload === 'object' && routeConfig.upload.single) {
75
+ middlewares.push(uploadParser.single(routeConfig.upload.single));
76
+ } else if (typeof routeConfig.upload === 'object' && routeConfig.upload.array) {
77
+ middlewares.push(uploadParser.array(routeConfig.upload.array));
78
+ } else {
79
+ middlewares.push(uploadParser.any());
80
+ }
52
81
  }
53
82
 
54
83
  middlewares.push(async (req, res) => {
55
84
  try {
56
85
  const ctx = {
86
+ env: globalConfig.envData || process.env,
57
87
  db,
58
88
  io,
59
89
  body: req.body,
@@ -61,7 +91,7 @@ export async function createServer(globalConfig, routesDir, db) {
61
91
  query: req.query,
62
92
  files: req.files || req.file,
63
93
  user: null,
64
- jwt: { sign: signJwt },
94
+ jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
65
95
  error: (status, message) => {
66
96
  const err = new Error(message);
67
97
  err.status = status;
@@ -76,7 +106,7 @@ export async function createServer(globalConfig, routesDir, db) {
76
106
  }
77
107
 
78
108
  const token = authHeader.split(' ')[1];
79
- const authResult = verifyJwt(token);
109
+ const authResult = verifyJwt(token, globalConfig.jwtSecret);
80
110
 
81
111
  if (!authResult.valid) {
82
112
  return res.status(401).json({ error: 'Unauthorized', details: authResult.error });
@@ -85,24 +115,24 @@ export async function createServer(globalConfig, routesDir, db) {
85
115
  ctx.user = authResult.payload;
86
116
  }
87
117
 
88
- if (routeConfig.params) {
89
- const result = routeConfig.params.safeParse(req.params);
118
+ if (paramsSchema) {
119
+ const result = paramsSchema.safeParse(req.params);
90
120
  if (!result.success) {
91
121
  return res.status(400).json({ error: 'Invalid URL Parameters', details: result.error.flatten() });
92
122
  }
93
123
  ctx.params = result.data;
94
124
  }
95
125
 
96
- if (routeConfig.body) {
97
- const result = routeConfig.body.safeParse(req.body);
126
+ if (bodySchema) {
127
+ const result = bodySchema.safeParse(req.body);
98
128
  if (!result.success) {
99
129
  return res.status(400).json({ error: 'Invalid Request Body', details: result.error.flatten() });
100
130
  }
101
131
  ctx.body = result.data;
102
132
  }
103
133
 
104
- if (routeConfig.query) {
105
- const result = routeConfig.query.safeParse(req.query);
134
+ if (querySchema) {
135
+ const result = querySchema.safeParse(req.query);
106
136
  if (!result.success) {
107
137
  return res.status(400).json({ error: 'Invalid Query Parameters', details: result.error.flatten() });
108
138
  }
@@ -143,6 +173,21 @@ export async function createServer(globalConfig, routesDir, db) {
143
173
  let openApiSpec = {
144
174
  openapi: '3.0.0',
145
175
  info: { title: 'bro.js API', version: '1.0.0' },
176
+ components: {
177
+ securitySchemes: {
178
+ bearerAuth: {
179
+ type: 'http',
180
+ scheme: 'bearer',
181
+ bearerFormat: 'JWT'
182
+ }
183
+ },
184
+ responses: {
185
+ BadRequest: { description: 'Bad Request', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
186
+ Unauthorized: { description: 'Unauthorized', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
187
+ NotFound: { description: 'Not Found', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
188
+ ServerError: { description: 'Internal Server Error', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } }
189
+ }
190
+ },
146
191
  paths: {}
147
192
  };
148
193
 
@@ -174,10 +219,20 @@ export async function createServer(globalConfig, routesDir, db) {
174
219
  routeStack(req, res, next);
175
220
  });
176
221
 
222
+ app.use((req, res) => {
223
+ res.status(404).json({ error: 'Not Found' });
224
+ });
225
+
226
+ app.use((err, req, res, next) => {
227
+ console.error(`[bro.js] Uncaught Error:`, err);
228
+ res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
229
+ });
230
+
177
231
  const reload = async () => {
178
232
  const newRouter = express.Router();
179
- openApiSpec.paths = {};
180
- const routes = await loadRoutes(newRouter, routesDir, createHandler, openApiSpec);
233
+ const tempSpec = { paths: {} };
234
+ const routes = await loadRoutes(newRouter, routesDir, createHandler, tempSpec);
235
+ openApiSpec.paths = tempSpec.paths;
181
236
  routeStack = newRouter;
182
237
  return routes;
183
238
  };
package/src/tasks.js CHANGED
@@ -3,29 +3,39 @@ import path from 'path';
3
3
  import { pathToFileURL } from 'url';
4
4
  import cron from 'node-cron';
5
5
  import { colors } from './logger.js';
6
+ import { scanDir } from './router.js';
7
+
8
+ let taskHandles = [];
9
+
10
+ export function stopTasks() {
11
+ taskHandles.forEach(t => t.stop());
12
+ taskHandles = [];
13
+ }
6
14
 
7
15
  export async function scanTasks(ctx) {
8
16
  const tasksDir = path.join(process.cwd(), 'tasks');
9
17
  if (!fs.existsSync(tasksDir)) return;
10
18
 
11
- const files = fs.readdirSync(tasksDir).filter(f => f.endsWith('.js'));
19
+ stopTasks();
20
+
21
+ const files = scanDir(tasksDir);
12
22
  if (files.length === 0) return;
13
23
 
14
24
  let count = 0;
15
25
  for (const file of files) {
16
- const filePath = path.join(tasksDir, file);
17
26
  try {
18
- const moduleUrl = pathToFileURL(filePath).href;
27
+ const moduleUrl = pathToFileURL(file).href;
19
28
  const taskModule = await import(moduleUrl);
20
29
 
21
30
  if (taskModule.cron && typeof taskModule.handler === 'function') {
22
- cron.schedule(taskModule.cron, async () => {
31
+ const task = cron.schedule(taskModule.cron, async () => {
23
32
  try {
24
33
  await taskModule.handler(ctx);
25
34
  } catch (err) {
26
35
  console.error(`\n ${colors.red}❌ Task Error (${file}):${colors.reset}`, err);
27
36
  }
28
37
  });
38
+ taskHandles.push(task);
29
39
  count++;
30
40
  }
31
41
  } catch (err) {