bro-framework 2.2.1 → 2.3.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/package.json CHANGED
@@ -1,88 +1,88 @@
1
- {
2
- "name": "bro-framework",
3
- "version": "2.2.1",
4
- "description": "The No-BS Backend Framework for Node.js",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/medyass1ne/bro.js"
8
- },
9
- "homepage": "https://brojs.yessindevs.me",
10
- "author": "Yessindevs",
11
- "license": "MIT",
12
- "main": "src/index.js",
13
- "types": "src/index.d.ts",
14
- "type": "module",
15
- "exports": {
16
- ".": {
17
- "types": "./src/index.d.ts",
18
- "default": "./src/index.js"
19
- }
20
- },
21
- "bin": {
22
- "bro": "./bin/bro.js"
23
- },
24
- "files": [
25
- "src",
26
- "bin"
27
- ],
28
- "dependencies": {
29
- "@scalar/express-api-reference": "^0.10.18",
30
- "chokidar": "^5.0.0",
31
- "cors": "^2.8.6",
32
- "dotenv": "^16.4.5",
33
- "express": "^4.21.1",
34
- "express-rate-limit": "^8.7.0",
35
- "jsonwebtoken": "^9.0.2",
36
- "multer": "^2.3.0",
37
- "node-cron": "^4.6.0",
38
- "socket.io": "^4.8.3",
39
- "tsx": "^4.23.13",
40
- "zod": "^3.23.8",
41
- "zod-to-json-schema": "^3.25.2"
42
- },
43
- "keywords": [
44
- "bro",
45
- "brojs",
46
- "bro-framework",
47
- "framework",
48
- "backend",
49
- "api",
50
- "server",
51
- "node",
52
- "nodejs",
53
- "express",
54
- "rest",
55
- "zero-config",
56
- "zero-boilerplate",
57
- "file-based-routing",
58
- "auto-routing",
59
- "zod",
60
- "validation",
61
- "jwt",
62
- "authentication",
63
- "auth",
64
- "websocket",
65
- "websockets",
66
- "socket.io",
67
- "realtime",
68
- "openapi",
69
- "swagger",
70
- "scalar",
71
- "docs",
72
- "auto-docs",
73
- "sdk",
74
- "sdk-generator",
75
- "rpc",
76
- "trpc-alternative",
77
- "express-alternative",
78
- "cron",
79
- "scheduler",
80
- "background-tasks",
81
- "file-upload",
82
- "rate-limit",
83
- "esm",
84
- "ai-friendly",
85
- "developer-experience",
86
- "dx"
87
- ]
88
- }
1
+ {
2
+ "name": "bro-framework",
3
+ "version": "2.3.0",
4
+ "description": "The No-BS Backend Framework for Node.js",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/medyass1ne/bro.js"
8
+ },
9
+ "homepage": "https://brojs.yessindevs.me",
10
+ "author": "Yessindevs",
11
+ "license": "MIT",
12
+ "main": "src/index.js",
13
+ "types": "src/index.d.ts",
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./src/index.d.ts",
18
+ "default": "./src/index.js"
19
+ }
20
+ },
21
+ "bin": {
22
+ "bro": "./bin/bro.js"
23
+ },
24
+ "files": [
25
+ "src",
26
+ "bin"
27
+ ],
28
+ "dependencies": {
29
+ "@scalar/express-api-reference": "^0.10.18",
30
+ "chokidar": "^5.0.0",
31
+ "cors": "^2.8.6",
32
+ "dotenv": "^16.4.5",
33
+ "express": "^4.21.1",
34
+ "express-rate-limit": "^8.7.0",
35
+ "jsonwebtoken": "^9.0.2",
36
+ "multer": "^2.3.0",
37
+ "node-cron": "^4.6.0",
38
+ "socket.io": "^4.8.3",
39
+ "tsx": "^4.23.13",
40
+ "zod": "^3.23.8",
41
+ "zod-to-json-schema": "^3.25.2"
42
+ },
43
+ "keywords": [
44
+ "bro",
45
+ "brojs",
46
+ "bro-framework",
47
+ "framework",
48
+ "backend",
49
+ "api",
50
+ "server",
51
+ "node",
52
+ "nodejs",
53
+ "express",
54
+ "rest",
55
+ "zero-config",
56
+ "zero-boilerplate",
57
+ "file-based-routing",
58
+ "auto-routing",
59
+ "zod",
60
+ "validation",
61
+ "jwt",
62
+ "authentication",
63
+ "auth",
64
+ "websocket",
65
+ "websockets",
66
+ "socket.io",
67
+ "realtime",
68
+ "openapi",
69
+ "swagger",
70
+ "scalar",
71
+ "docs",
72
+ "auto-docs",
73
+ "sdk",
74
+ "sdk-generator",
75
+ "rpc",
76
+ "trpc-alternative",
77
+ "express-alternative",
78
+ "cron",
79
+ "scheduler",
80
+ "background-tasks",
81
+ "file-upload",
82
+ "rate-limit",
83
+ "esm",
84
+ "ai-friendly",
85
+ "developer-experience",
86
+ "dx"
87
+ ]
88
+ }
package/src/auth.js CHANGED
@@ -1,33 +1,33 @@
1
- import jwt from 'jsonwebtoken';
2
-
3
- /**
4
- * Signs a JWT payload.
5
- * @param {Object} payload - The data to embed in the token.
6
- * @param {string} secret - The JWT secret key.
7
- * @param {jwt.SignOptions} [options] - jsonwebtoken sign options.
8
- * @returns {string} The signed JWT token.
9
- */
10
- export function signJwt(payload, secret, options = { expiresIn: '1d' }) {
11
- return jwt.sign(payload, secret, options);
12
- }
13
-
14
- /**
15
- * Verifies and decodes a JWT token.
16
- * @param {string} token - The JWT token to verify.
17
- * @param {string} secret - The JWT secret key.
18
- * @returns {{ valid: boolean, payload?: any, error?: string }}
19
- */
20
- export function verifyJwt(token, secret) {
21
- try {
22
- const payload = jwt.verify(token, secret);
23
- return { valid: true, payload };
24
- } catch (error) {
25
- let message = 'Invalid token';
26
- if (error.name === 'TokenExpiredError') {
27
- message = 'Token has expired';
28
- } else if (error.name === 'JsonWebTokenError') {
29
- message = 'Signature verification failed';
30
- }
31
- return { valid: false, error: message };
32
- }
33
- }
1
+ import jwt from 'jsonwebtoken';
2
+
3
+ /**
4
+ * Signs a JWT payload.
5
+ * @param {Object} payload - The data to embed in the token.
6
+ * @param {string} secret - The JWT secret key.
7
+ * @param {jwt.SignOptions} [options] - jsonwebtoken sign options.
8
+ * @returns {string} The signed JWT token.
9
+ */
10
+ export function signJwt(payload, secret, options = { expiresIn: '1d' }) {
11
+ return jwt.sign(payload, secret, options);
12
+ }
13
+
14
+ /**
15
+ * Verifies and decodes a JWT token.
16
+ * @param {string} token - The JWT token to verify.
17
+ * @param {string} secret - The JWT secret key.
18
+ * @returns {{ valid: boolean, payload?: any, error?: string }}
19
+ */
20
+ export function verifyJwt(token, secret) {
21
+ try {
22
+ const payload = jwt.verify(token, secret);
23
+ return { valid: true, payload };
24
+ } catch (error) {
25
+ let message = 'Invalid token';
26
+ if (error.name === 'TokenExpiredError') {
27
+ message = 'Token has expired';
28
+ } else if (error.name === 'JsonWebTokenError') {
29
+ message = 'Signature verification failed';
30
+ }
31
+ return { valid: false, error: message };
32
+ }
33
+ }
package/src/index.d.ts CHANGED
@@ -1,66 +1,80 @@
1
- import { z, ZodTypeAny } from 'zod';
2
-
3
- type InferZod<T> = T extends ZodTypeAny ? z.infer<T> : any;
4
-
5
- export interface BroContext<Body = any, Params = any, Query = any> {
6
- env?: any;
7
- jwt?: { sign: (payload: any, options?: any) => string };
8
- body: InferZod<Body>;
9
- params: InferZod<Params>;
10
- query: InferZod<Query>;
11
- user?: any;
12
- db?: any;
13
- io?: any;
14
- files?: any[];
15
- error?: any;
16
- }
17
-
18
- export interface RouteConfig<Body = any, Params = any, Query = any> {
19
- auth?: 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; };
22
- body?: Body;
23
- params?: Params;
24
- query?: Query;
25
- rateLimit?: {
26
- windowMs: number;
27
- max: number;
28
- };
29
- summary?: string;
30
- handler: (ctx: BroContext<Body, Params, Query>) => Promise<any> | any;
31
- }
32
-
33
- export function defineRoute<Body = any, Params = any, Query = any>(
34
- config: RouteConfig<Body, Params, Query>
35
- ): RouteConfig<Body, Params, Query>;
36
-
37
- export interface BroConfig {
38
- env?: ZodTypeAny;
39
- server?: {
40
- port?: number;
41
- cors?: boolean | object;
42
- };
43
- auth?: {
44
- jwtSecret?: string;
45
- expiresIn?: string | number;
46
- };
47
- docs?: boolean | { auth?: { user: string; pass: string } };
48
- rateLimit?: {
49
- windowMs: number;
50
- max: number;
51
- };
52
- upload?: {
53
- limits?: {
54
- fileSize?: number;
55
- files?: number;
56
- fields?: number;
57
- [key: string]: any;
58
- };
59
- };
60
- db?: () => Promise<any> | any;
61
- sockets?: (io: any, db: any) => Promise<void> | void;
62
- }
63
-
64
- export function defineConfig(config: BroConfig): BroConfig;
65
-
66
- export { z } from 'zod';
1
+ import { z, ZodTypeAny } from 'zod';
2
+
3
+ type InferZod<T> = T extends ZodTypeAny ? z.infer<T> : any;
4
+
5
+ export interface BroContext<Body = any, Params = any, Query = any> {
6
+ env?: any;
7
+ jwt?: { sign: (payload: any, options?: any) => string };
8
+ body: InferZod<Body>;
9
+ params: InferZod<Params>;
10
+ query: InferZod<Query>;
11
+ user?: any;
12
+ db?: any;
13
+ io?: any;
14
+ files?: any[];
15
+ locale: string;
16
+ t: (key: string, values?: Record<string, unknown>) => string;
17
+ error?: any;
18
+ }
19
+
20
+ export interface RouteConfig<Body = any, Params = any, Query = any> {
21
+ auth?: boolean;
22
+ upload?: boolean | { limits?: any, fields?: { name: string, maxCount?: number }[], single?: string, array?: string, fileFilter?: any, storage?: any };
23
+ body?: Body;
24
+ params?: Params;
25
+ query?: Query;
26
+ response?: ZodTypeAny;
27
+ rateLimit?: {
28
+ windowMs: number;
29
+ max: number;
30
+ };
31
+ summary?: string;
32
+ handler: (ctx: BroContext<Body, Params, Query>) => Promise<any> | any;
33
+ }
34
+
35
+ export function defineRoute<Body = any, Params = any, Query = any>(
36
+ config: RouteConfig<Body, Params, Query>
37
+ ): RouteConfig<Body, Params, Query>;
38
+
39
+ export function loadLocale(directory: string, options?: { defaultLocale?: string }): Promise<{
40
+ locales: string[];
41
+ defaultLocale: string;
42
+ resolveLocale: (request: any) => string;
43
+ translate: (locale: string, key: string, values?: Record<string, unknown>) => string;
44
+ }>;
45
+
46
+ export interface BroConfig {
47
+ env?: ZodTypeAny;
48
+ server?: {
49
+ port?: number;
50
+ cors?: boolean | object;
51
+ };
52
+ locale?: {
53
+ directory?: string;
54
+ defaultLocale?: string;
55
+ };
56
+ auth?: {
57
+ jwtSecret?: string;
58
+ expiresIn?: string | number;
59
+ };
60
+ docs?: boolean | { auth?: { user: string; pass: string } };
61
+ rateLimit?: {
62
+ windowMs: number;
63
+ max: number;
64
+ };
65
+ upload?: {
66
+ limits?: {
67
+ fileSize?: number;
68
+ files?: number;
69
+ fields?: number;
70
+ [key: string]: any;
71
+ };
72
+ };
73
+ db?: () => Promise<any> | any;
74
+ sockets?: (io: any, db: any) => Promise<void> | void;
75
+ onShutdown?: (db: any) => Promise<void> | void;
76
+ }
77
+
78
+ export function defineConfig(config: BroConfig): BroConfig;
79
+
80
+ export { z } from 'zod';
package/src/index.js CHANGED
@@ -26,3 +26,4 @@ export function defineConfig(config) {
26
26
  }
27
27
 
28
28
  export { z };
29
+ export { loadLocale } from './locale.js';
package/src/locale.js ADDED
@@ -0,0 +1,118 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ import crypto from 'node:crypto';
5
+
6
+ const LOCALE_EXTENSIONS = new Set(['.js', '.mjs', '.ts']);
7
+
8
+ function localeFromFilename(fileName) {
9
+ return path.basename(fileName, path.extname(fileName));
10
+ }
11
+
12
+ function normalizeLocale(locale) {
13
+ const normalized = String(locale || '').trim().replaceAll('_', '-').toLowerCase();
14
+ const aliases = {
15
+ english: 'en',
16
+ arabic: 'ar',
17
+ french: 'fr',
18
+ spanish: 'es',
19
+ german: 'de',
20
+ portuguese: 'pt',
21
+ italian: 'it',
22
+ japanese: 'ja',
23
+ korean: 'ko',
24
+ chinese: 'zh'
25
+ };
26
+ return aliases[normalized] || normalized;
27
+ }
28
+
29
+ function findLocale(locales, requestedLocale, fallbackLocale) {
30
+ const requested = normalizeLocale(requestedLocale);
31
+ const exact = locales.find(locale => normalizeLocale(locale) === requested);
32
+ if (exact) return exact;
33
+
34
+ const language = requested.split('-')[0];
35
+ const languageMatch = locales.find(locale => normalizeLocale(locale).split('-')[0] === language);
36
+ if (languageMatch) return languageMatch;
37
+
38
+ return fallbackLocale;
39
+ }
40
+
41
+ function readMessage(messages, key) {
42
+ return key.split('.').reduce((value, part) => value?.[part], messages);
43
+ }
44
+
45
+ function interpolate(message, values) {
46
+ return message.replace(/\{(\w+)\}/g, (_, name) => {
47
+ return values[name] === undefined ? `{${name}}` : String(values[name]);
48
+ });
49
+ }
50
+
51
+ function parseAcceptLanguage(header) {
52
+ return String(header || '')
53
+ .split(',')
54
+ .map(value => {
55
+ const [tag, qVal] = value.split(';').map(s => s.trim());
56
+ let q = 1;
57
+ if (qVal && qVal.startsWith('q=')) {
58
+ const parsedQ = parseFloat(qVal.slice(2));
59
+ if (!isNaN(parsedQ)) q = parsedQ;
60
+ }
61
+ return { tag, q };
62
+ })
63
+ .filter(item => item.tag && item.q > 0)
64
+ .sort((a, b) => b.q - a.q)
65
+ .map(item => item.tag);
66
+ }
67
+
68
+ /**
69
+ * Loads translation files from the application's locale directory.
70
+ * Each file name becomes a locale, for example locale/en-US.js.
71
+ * @param {string} directory
72
+ * @param {{ defaultLocale?: string }} [options]
73
+ */
74
+ export async function loadLocale(directory, options = {}) {
75
+ const files = fs.existsSync(directory)
76
+ ? fs.readdirSync(directory).filter(file => {
77
+ const fullPath = path.join(directory, file);
78
+ return fs.statSync(fullPath).isFile() && LOCALE_EXTENSIONS.has(path.extname(file));
79
+ })
80
+ : [];
81
+
82
+ const messages = {};
83
+ for (const file of files) {
84
+ const module = await import(`${pathToFileURL(path.join(directory, file)).href}?update=${crypto.randomUUID()}`);
85
+ const catalog = module.default || module.messages || module;
86
+ if (catalog && typeof catalog === 'object') {
87
+ messages[localeFromFilename(file)] = catalog;
88
+ }
89
+ }
90
+
91
+ const locales = Object.keys(messages);
92
+ const configuredDefault = options.defaultLocale;
93
+ const defaultLocale = findLocale(locales, configuredDefault, null)
94
+ || locales.find(locale => ['en', 'en-us'].includes(normalizeLocale(locale)))
95
+ || locales[0]
96
+ || 'en';
97
+
98
+ function resolveLocale(request) {
99
+ const requestedLocales = parseAcceptLanguage(request?.headers?.['accept-language']);
100
+ for (const requested of requestedLocales) {
101
+ const match = findLocale(locales, requested, null);
102
+ if (match) return match;
103
+ }
104
+ return defaultLocale;
105
+ }
106
+
107
+ function translate(locale, key, values = {}) {
108
+ const selectedLocale = findLocale(locales, locale, defaultLocale);
109
+ const message = readMessage(messages[selectedLocale], key)
110
+ ?? readMessage(messages[defaultLocale], key);
111
+
112
+ if (message === undefined) return key;
113
+ if (typeof message !== 'string') return message;
114
+ return interpolate(message, values);
115
+ }
116
+
117
+ return { locales, defaultLocale, resolveLocale, translate };
118
+ }
package/src/logger.js CHANGED
@@ -1,79 +1,79 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
-
5
- export const colors = {
6
- reset: "\x1b[0m",
7
- bold: "\x1b[1m",
8
- dim: "\x1b[2m",
9
- red: "\x1b[31m",
10
- green: "\x1b[32m",
11
- yellow: "\x1b[33m",
12
- blue: "\x1b[34m",
13
- magenta: "\x1b[35m",
14
- cyan: "\x1b[36m",
15
- white: "\x1b[37m"
16
- };
17
-
18
- export function formatMethod(method) {
19
- const m = method.toUpperCase().padEnd(6);
20
- switch(method.toUpperCase()) {
21
- case 'GET': return `${colors.green}${colors.bold}${m}${colors.reset}`;
22
- case 'POST': return `${colors.blue}${colors.bold}${m}${colors.reset}`;
23
- case 'PUT': return `${colors.yellow}${colors.bold}${m}${colors.reset}`;
24
- case 'DELETE': return `${colors.red}${colors.bold}${m}${colors.reset}`;
25
- case 'PATCH': return `${colors.magenta}${colors.bold}${m}${colors.reset}`;
26
- default: return `${colors.white}${colors.bold}${m}${colors.reset}`;
27
- }
28
- }
29
-
30
- export function printBanner(port, durationMs) {
31
- const time = durationMs.toFixed(0);
32
-
33
- let version = "2.2.0";
34
- try {
35
- const __filename = fileURLToPath(import.meta.url);
36
- const __dirname = path.dirname(__filename);
37
- const pkgPath = path.join(__dirname, '..', 'package.json');
38
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
39
- version = pkg.version;
40
- } catch(e) {}
41
-
42
- const innerWidth = 47;
43
-
44
- const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
45
-
46
- const formatLine = (content = "") => {
47
- const visibleLength = stripAnsi(content).length;
48
- const padding = Math.max(0, innerWidth - visibleLength);
49
- return `${colors.green}│${colors.reset}${content}${" ".repeat(padding)}${colors.green}│${colors.reset}`;
50
- };
51
-
52
- const borderTop = `${colors.green}╭${"─".repeat(innerWidth)}╮${colors.reset}`;
53
- const borderBottom = `${colors.green}╰${"─".repeat(innerWidth)}╯${colors.reset}`;
54
-
55
- console.log("");
56
- console.log(borderTop);
57
- console.log(formatLine());
58
- console.log(formatLine(` ${colors.bold}bro.js${colors.reset} v${version}`));
59
- console.log(formatLine());
60
- console.log(formatLine(` ➜ ${colors.bold}Local:${colors.reset} ${colors.cyan}http://localhost:${port}${colors.reset}`));
61
- console.log(formatLine(` ➜ ${colors.bold}Ready in:${colors.reset} ${colors.yellow}${time}ms${colors.reset}`));
62
- console.log(formatLine());
63
- console.log(borderBottom);
64
- console.log("");
65
- }
66
-
67
- export function printRoute(method, routePath, hasAuth, isLast = false) {
68
- const branch = isLast ? "╰──" : "├──";
69
- const coloredMethod = formatMethod(method);
70
- const authIcon = hasAuth ? " 🔒" : "";
71
-
72
- console.log(` ${colors.dim}${branch}${colors.reset} ${coloredMethod} ${routePath}${authIcon}`);
73
- }
74
-
75
- export function printHotReload(fileName, event, reloadTimeMs) {
76
- const time = typeof reloadTimeMs === 'number' ? reloadTimeMs.toFixed(0) : reloadTimeMs;
77
- console.log(`\n ${colors.cyan}Route updated:${colors.reset} ${colors.bold}${fileName}${colors.reset} ${colors.dim}(${event})${colors.reset}`);
78
- console.log(` ${colors.dim}Remapped in ${time}ms${colors.reset}\n`);
79
- }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ export const colors = {
6
+ reset: "\x1b[0m",
7
+ bold: "\x1b[1m",
8
+ dim: "\x1b[2m",
9
+ red: "\x1b[31m",
10
+ green: "\x1b[32m",
11
+ yellow: "\x1b[33m",
12
+ blue: "\x1b[34m",
13
+ magenta: "\x1b[35m",
14
+ cyan: "\x1b[36m",
15
+ white: "\x1b[37m"
16
+ };
17
+
18
+ export function formatMethod(method) {
19
+ const m = method.toUpperCase().padEnd(6);
20
+ switch(method.toUpperCase()) {
21
+ case 'GET': return `${colors.green}${colors.bold}${m}${colors.reset}`;
22
+ case 'POST': return `${colors.blue}${colors.bold}${m}${colors.reset}`;
23
+ case 'PUT': return `${colors.yellow}${colors.bold}${m}${colors.reset}`;
24
+ case 'DELETE': return `${colors.red}${colors.bold}${m}${colors.reset}`;
25
+ case 'PATCH': return `${colors.magenta}${colors.bold}${m}${colors.reset}`;
26
+ default: return `${colors.white}${colors.bold}${m}${colors.reset}`;
27
+ }
28
+ }
29
+
30
+ export function printBanner(port, durationMs) {
31
+ const time = durationMs.toFixed(0);
32
+
33
+ let version = "2.2.0";
34
+ try {
35
+ const __filename = fileURLToPath(import.meta.url);
36
+ const __dirname = path.dirname(__filename);
37
+ const pkgPath = path.join(__dirname, '..', 'package.json');
38
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
39
+ version = pkg.version;
40
+ } catch(e) {}
41
+
42
+ const innerWidth = 47;
43
+
44
+ const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
45
+
46
+ const formatLine = (content = "") => {
47
+ const visibleLength = stripAnsi(content).length;
48
+ const padding = Math.max(0, innerWidth - visibleLength);
49
+ return `${colors.green}│${colors.reset}${content}${" ".repeat(padding)}${colors.green}│${colors.reset}`;
50
+ };
51
+
52
+ const borderTop = `${colors.green}╭${"─".repeat(innerWidth)}╮${colors.reset}`;
53
+ const borderBottom = `${colors.green}╰${"─".repeat(innerWidth)}╯${colors.reset}`;
54
+
55
+ console.log("");
56
+ console.log(borderTop);
57
+ console.log(formatLine());
58
+ console.log(formatLine(` ${colors.bold}bro.js${colors.reset} v${version}`));
59
+ console.log(formatLine());
60
+ console.log(formatLine(` ➜ ${colors.bold}Local:${colors.reset} ${colors.cyan}http://localhost:${port}${colors.reset}`));
61
+ console.log(formatLine(` ➜ ${colors.bold}Ready in:${colors.reset} ${colors.yellow}${time}ms${colors.reset}`));
62
+ console.log(formatLine());
63
+ console.log(borderBottom);
64
+ console.log("");
65
+ }
66
+
67
+ export function printRoute(method, routePath, hasAuth, isLast = false) {
68
+ const branch = isLast ? "╰──" : "├──";
69
+ const coloredMethod = formatMethod(method);
70
+ const authIcon = hasAuth ? " 🔒" : "";
71
+
72
+ console.log(` ${colors.dim}${branch}${colors.reset} ${coloredMethod} ${routePath}${authIcon}`);
73
+ }
74
+
75
+ export function printHotReload(fileName, event, reloadTimeMs, resourceType = 'Route') {
76
+ const time = typeof reloadTimeMs === 'number' ? reloadTimeMs.toFixed(0) : reloadTimeMs;
77
+ console.log(`\n ${colors.cyan}${resourceType} updated:${colors.reset} ${colors.bold}${fileName}${colors.reset} ${colors.dim}(${event})${colors.reset}`);
78
+ console.log(` ${colors.dim}Remapped in ${time}ms${colors.reset}\n`);
79
+ }