@syntay/fastay 0.1.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.
@@ -0,0 +1,8 @@
1
+ import { Request as ExpressRequest, Response as ExpressResponse, NextFunction } from 'express';
2
+ /**
3
+ * Request e Response do Express
4
+ * Podem ser usados nos handlers do usuário
5
+ */
6
+ export type Request = ExpressRequest;
7
+ export type Response = ExpressResponse;
8
+ export type Next = NextFunction;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ export type MiddlewareFn = (req: Request, res: Response, next: NextFunction) => any;
3
+ /**
4
+ * Wrapper final
5
+ */
6
+ export declare function wrapMiddleware(mw: MiddlewareFn): MiddlewareFn;
@@ -0,0 +1,74 @@
1
+ import { logger } from '../logger.js';
2
+ const color = {
3
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
4
+ gray: (s) => `\x1b[90m${s}\x1b[0m`,
5
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
6
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
7
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
8
+ white: (s) => `\x1b[37m${s}\x1b[0m`,
9
+ };
10
+ /**
11
+ * Formata uma função para exibição bonitinha
12
+ */
13
+ function formatFunction(fn) {
14
+ let code = fn.toString();
15
+ // deixa com quebras de linha
16
+ code = code.replace(/;/g, ';\n');
17
+ // tenta dar indentação
18
+ code = code
19
+ .replace(/{/g, '{\n ')
20
+ .replace(/}/g, '\n}')
21
+ .replace(/\n\s*\n/g, '\n');
22
+ return code.trim();
23
+ }
24
+ /**
25
+ * Verifica se next() ou return existem
26
+ */
27
+ function validateMiddlewareCode(mw) {
28
+ const raw = mw.toString();
29
+ const name = mw.name || 'anonymous';
30
+ const cleaned = raw
31
+ .replace(/\/\/.*$/gm, '')
32
+ .replace(/\/\*[\s\S]*?\*\//gm, '');
33
+ const hasNext = /next\s*\(/.test(cleaned);
34
+ const hasReturn = /\breturn\b/.test(cleaned);
35
+ if (!hasNext && !hasReturn) {
36
+ const prettyCode = formatFunction(mw);
37
+ const message = [
38
+ `${color.red('⨯ Fastay Middleware Error')}`,
39
+ ``,
40
+ `The middleware ${color.yellow(`"${name}"`)} does not call next() or return any value.`,
41
+ `This will halt the middleware chain and block the request pipeline.`,
42
+ ``,
43
+ `▌ Middleware Source`,
44
+ color.gray(prettyCode),
45
+ ``,
46
+ `${color.cyan('▌ How to fix')}`,
47
+ `Ensure your middleware ends with either:`,
48
+ ` • ${color.green('next()')}`,
49
+ ` • ${color.green('return ...')}`,
50
+ ``,
51
+ `Fastay cannot continue until this middleware is fixed.`,
52
+ ].join('\n');
53
+ const err = new Error(message);
54
+ err.name = 'FastayMiddlewareError';
55
+ throw err;
56
+ }
57
+ }
58
+ /**
59
+ * Wrapper final
60
+ */
61
+ export function wrapMiddleware(mw) {
62
+ const name = mw.name || 'anonymous';
63
+ validateMiddlewareCode(mw);
64
+ // retorna middleware "puro"
65
+ return async (req, res, next) => {
66
+ try {
67
+ await mw(req, res, next);
68
+ }
69
+ catch (err) {
70
+ logger.error(`[${name}] middleware error: ${err}`);
71
+ next(err);
72
+ }
73
+ };
74
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@syntay/fastay",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.build.json",
9
+ "prepare": "npm run build"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "dependencies": {
15
+ "express": "^5.1.0",
16
+ "pino": "^10.1.0",
17
+ "pino-pretty": "^13.1.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/express": "^5.0.5",
21
+ "@types/node": "^20.19.25",
22
+ "ts-node": "^10.9.2",
23
+ "typescript": "^5.9.3"
24
+ }
25
+ }
package/src/app.ts ADDED
@@ -0,0 +1,233 @@
1
+ import express from 'express';
2
+ import path from 'path';
3
+ import { loadApiRoutes } from './router.js';
4
+ import {
5
+ MiddlewareMap,
6
+ loadFastayMiddlewares,
7
+ createMiddleware,
8
+ } from './middleware.js';
9
+ import { logger } from './logger.js';
10
+ import { printBanner } from './banner.js';
11
+ import type { ServeStaticOptions } from 'serve-static';
12
+ import { Next, Request, Response } from './types/index.js';
13
+
14
+ /**
15
+ * Express configuration options applied automatically by Fastay
16
+ * before internal middleware and route loading.
17
+ */
18
+ export interface ExpressOptions {
19
+ /**
20
+ * Global middlewares applied to all routes.
21
+ * Example: [cors(), helmet()]
22
+ */
23
+ middlewares?: express.RequestHandler[];
24
+
25
+ /**
26
+ * Options passed to express.json().
27
+ * Useful for customizing JSON payload limits or behavior.
28
+ */
29
+ jsonOptions?: Parameters<typeof express.json>[0];
30
+
31
+ /**
32
+ * Options passed to express.urlencoded().
33
+ * Useful when handling form submissions or URL-encoded bodies.
34
+ */
35
+ urlencodedOptions?: Parameters<typeof express.urlencoded>[0];
36
+
37
+ /**
38
+ * Custom global error handler.
39
+ * If provided, Fastay will use this instead of the default one.
40
+ */
41
+ errorHandler?: express.ErrorRequestHandler;
42
+
43
+ /**
44
+ * Static file serving configuration.
45
+ * Example:
46
+ * {
47
+ * path: "public",
48
+ * options: { maxAge: "1d" }
49
+ * }
50
+ */
51
+ static?: {
52
+ path: string;
53
+ options?: ServeStaticOptions;
54
+ };
55
+
56
+ /**
57
+ * View engine configuration for Express.
58
+ * Example:
59
+ * {
60
+ * engine: "pug",
61
+ * dir: "views"
62
+ * }
63
+ */
64
+ views?: {
65
+ engine: string;
66
+ dir: string;
67
+ };
68
+
69
+ /**
70
+ * Enables or disables Express' "trust proxy" mode.
71
+ * Typically required when using reverse proxies (Nginx, Cloudflare, etc.).
72
+ */
73
+ trustProxy?: boolean;
74
+
75
+ /**
76
+ * Local variables available to all templates and responses.
77
+ * Fastay automatically injects them into `response.locals`.
78
+ */
79
+ locals?: Record<string, any>;
80
+ }
81
+
82
+ /**
83
+ * Options applied when creating a Fastay.js application.
84
+ */
85
+ export type CreateAppOptions = {
86
+ /**
87
+ * Directory where API route modules are located.
88
+ * Default: "src/api"
89
+ */
90
+ apiDir?: string;
91
+
92
+ /**
93
+ * Base route where all API routes will be mounted.
94
+ * Default: "/api"
95
+ */
96
+ baseRoute?: string;
97
+
98
+ /**
99
+ * Port on which `.listen()` will run the server.
100
+ * Default: 3000
101
+ */
102
+ port?: number;
103
+
104
+ /**
105
+ * Express-level configuration such as middleware, body parsers,
106
+ * view engine, static assets, error handler, etc.
107
+ */
108
+ expressOptions?: ExpressOptions;
109
+
110
+ /**
111
+ * Internal Fastay middlewares applied after Express initialization
112
+ * but before route mounting.
113
+ */
114
+ middlewares?: MiddlewareMap;
115
+ };
116
+
117
+ /**
118
+ * Bootstraps and configures a Fastay application.
119
+ *
120
+ * Fastay automatically:
121
+ * - Discovers and registers routes defined in `apiDir`.
122
+ * - Applies both built-in and user-provided middlewares.
123
+ * - Exposes a health-check endpoint at `/_health`.
124
+ *
125
+ * @param opts - Configuration options for the Fastay application.
126
+ * @returns A Promise that resolves to an Express `Application` instance.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * import { createApp } from '@syntay/fastay';
131
+ * import cors from 'cors';
132
+ * import helmet from 'helmet';
133
+ *
134
+ * void (async () => {
135
+ * await createApp({
136
+ * apiDir: './src/api',
137
+ * baseRoute: '/api',
138
+ * port: 5555,
139
+ * expressOptions: {
140
+ * middlewares: [cors(), helmet()],
141
+ * },
142
+ * });
143
+ * })();
144
+ * ```
145
+ */
146
+ export async function createApp(opts?: CreateAppOptions) {
147
+ const start = logger.timeStart();
148
+
149
+ printBanner();
150
+
151
+ // logger.group('Fastay');
152
+ logger.info('Initializing server...');
153
+
154
+ const apiDir = opts?.apiDir ?? path.resolve(process.cwd(), 'src', 'api');
155
+ const baseRoute = opts?.baseRoute ?? '/api';
156
+
157
+ logger.success(`API directory: ${apiDir}`);
158
+ logger.success(`Base route: ${baseRoute}`);
159
+
160
+ const app = express();
161
+
162
+ if (opts?.expressOptions) {
163
+ for (const [key, value] of Object.entries(opts.expressOptions)) {
164
+ // Se for array → assume middleware global
165
+ if (Array.isArray(value)) {
166
+ value.forEach((mw) => app.use(mw));
167
+ }
168
+ // Se o app tiver método com esse nome
169
+ else if (typeof (app as any)[key] === 'function') {
170
+ // TS-safe
171
+ ((app as any)[key] as Function)(value);
172
+ }
173
+ // special cases
174
+ else if (key === 'static' && value && typeof value === 'object') {
175
+ const v = value as { path: string; options?: any };
176
+ app.use(express.static(v.path, v.options));
177
+ } else if (key === 'jsonOptions') {
178
+ app.use(express.json(value as any));
179
+ } else if (key === 'urlencodedOptions') {
180
+ app.use(express.urlencoded(value as any));
181
+ }
182
+ }
183
+ }
184
+
185
+ app.use(express.json());
186
+
187
+ const defaltPort = opts?.port ? opts.port : 6000;
188
+
189
+ app.listen(defaltPort, () => {
190
+ logger.success(
191
+ `Server running at http://localhost:${defaltPort}${baseRoute}`
192
+ );
193
+ });
194
+
195
+ // external middlewares
196
+ if (opts?.expressOptions?.middlewares) {
197
+ logger.group('Express Middlewares');
198
+ for (const mw of opts.expressOptions.middlewares) {
199
+ logger.gear(`Loaded: ${mw.name || 'anonymous'}`);
200
+ app.use(mw);
201
+ }
202
+ }
203
+
204
+ // Fastay middlewares
205
+ if (opts?.middlewares) {
206
+ logger.group('Fastay Middlewares');
207
+ const apply = createMiddleware(opts.middlewares);
208
+ apply(app);
209
+ }
210
+
211
+ // automatic middlewares
212
+ // logger.group('Fastay Auto-Middlewares');
213
+ const isMiddleware = await loadFastayMiddlewares(app);
214
+
215
+ // health check
216
+ app.get('/_health', (_, res) => res.json({ ok: true }));
217
+ app.use((_req: Request, res: Response, next: Next) => {
218
+ res.setHeader('X-Powered-By', 'Syntay Engine');
219
+ next();
220
+ });
221
+
222
+ // load routes
223
+ // logger.group('Routes Loaded');
224
+ const totalRoutes = await loadApiRoutes(app, baseRoute, apiDir);
225
+ logger.success(`Total routes loaded: ${totalRoutes}`);
226
+
227
+ // app.use(errorHandler);
228
+
229
+ const time = logger.timeEnd(start);
230
+ logger.success(`Boot completed in ${time}ms`);
231
+
232
+ return app;
233
+ }
package/src/banner.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { logger } from './logger.js';
2
+
3
+ export function printBanner() {
4
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
5
+ const white = (s: string) => `\x1b[37m${s}\x1b[0m`;
6
+
7
+ logger.raw('');
8
+ logger.raw(`${cyan('⥨ Fastay.js')} ${white('1.0.0')}`);
9
+ logger.raw(` ${white('- Runtime: Node.js\n')}`);
10
+ // logger.raw('\n');
11
+ }
@@ -0,0 +1,48 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { logger } from './logger';
3
+ import fs from 'fs';
4
+
5
+ export function errorHandler(
6
+ err: any,
7
+ req: Request,
8
+ res: Response,
9
+ next: NextFunction
10
+ ) {
11
+ const isSyntaxError =
12
+ err.name === 'SyntaxError' || err.message?.includes('Unexpected');
13
+ const route = `${req.method} ${req.originalUrl}`;
14
+
15
+ // Tenta extrair arquivo, linha e coluna (quando stack estiver presente)
16
+ let fileInfo = '';
17
+ if (err.stack) {
18
+ const stackLine = err.stack.split('\n')[1]; // pega primeira linha depois do erro
19
+ const match = stackLine.match(/\((.*):(\d+):(\d+)\)/);
20
+ if (match) {
21
+ const [_, file, line, col] = match;
22
+ fileInfo = `${file}:${line}:${col}`;
23
+
24
+ // Tenta mostrar o trecho da linha que deu erro
25
+ if (fs.existsSync(file)) {
26
+ const codeLines = fs.readFileSync(file, 'utf-8').split('\n');
27
+ const codeSnippet = codeLines[parseInt(line) - 1].trim();
28
+ fileInfo += ` → ${codeSnippet}`;
29
+ }
30
+ }
31
+ }
32
+
33
+ logger.group(`✗ Runtime Error in route [${route}]`);
34
+ logger.error(`${err.name}: ${err.message}`);
35
+ if (fileInfo) logger.error(`Location: ${fileInfo}`);
36
+
37
+ if (process.env.NODE_ENV === 'production') {
38
+ return res.status(500).json({
39
+ error: 'Internal server error',
40
+ });
41
+ }
42
+
43
+ return res.status(500).json({
44
+ error: err.message,
45
+ stack: err.stack,
46
+ file: fileInfo || undefined,
47
+ });
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { createApp } from './app.js';
2
+ export { createMiddleware } from './middleware.js';
3
+ export type { CreateAppOptions } from './app.js';
4
+ export { Request, Response, Next } from './types';
package/src/logger.ts ADDED
@@ -0,0 +1,78 @@
1
+ import pino, { LogDescriptor } from 'pino';
2
+ import pretty, { PrettyOptions } from 'pino-pretty';
3
+
4
+ // stream configurado para remover INFO:, timestamps e etc
5
+ const stream = pretty({
6
+ colorize: true,
7
+ ignore: 'pid,hostname,time,level',
8
+ levelFirst: false,
9
+
10
+ // Remove "INFO: " antes da msg
11
+ messageKey: 'msg',
12
+
13
+ // Maneira correta TS-safe
14
+ messageFormat: (log: LogDescriptor, messageKey: string) => {
15
+ const msg = log[messageKey];
16
+ return typeof msg === 'string' ? msg : String(msg);
17
+ },
18
+ } as PrettyOptions);
19
+
20
+ const base = pino(
21
+ {
22
+ level: 'info',
23
+ timestamp: false, // remove [HH:mm:ss]
24
+ base: undefined, // remove pid, hostname
25
+ },
26
+ stream
27
+ );
28
+
29
+ // helpers para cores ANSI
30
+ const colors = {
31
+ white: (s: string) => `\x1b[37m${s}\x1b[0m`,
32
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
33
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
34
+ cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
35
+ gray: (s: string) => `\x1b[90m${s}\x1b[0m`,
36
+ };
37
+
38
+ // emojis Fastay
39
+ const ICONS = {
40
+ info: '○',
41
+ success: '✓',
42
+ error: '✗',
43
+ gear: '⚙️',
44
+ };
45
+
46
+ export const logger = {
47
+ info: (msg: string) =>
48
+ base.info(` ${colors.white(ICONS.info)} ${colors.white(msg)}`),
49
+ warn: (msg: string) => base.info(` ${colors.red('⚠')} ${colors.white(msg)}`),
50
+ error: (msg: string) =>
51
+ base.info(` ${colors.red(ICONS.error)} ${colors.white(msg)}`),
52
+ success: (msg: string) =>
53
+ base.info(` ${colors.green(ICONS.success)} ${colors.white(msg)}`),
54
+ gear: (msg: string) => base.info(` ${ICONS.gear} ${colors.white(msg)}`),
55
+
56
+ space(lines: number = 1) {
57
+ for (let i = 0; i < lines; i++) base.info(' ');
58
+ },
59
+
60
+ group(title: string) {
61
+ this.space();
62
+ base.info('');
63
+ base.info(colors.cyan(title));
64
+ // this.space();
65
+ },
66
+
67
+ raw(msg: string) {
68
+ base.info(msg);
69
+ },
70
+
71
+ timeStart() {
72
+ return performance.now();
73
+ },
74
+
75
+ timeEnd(start: number) {
76
+ return (performance.now() - start).toFixed(1);
77
+ },
78
+ };
@@ -0,0 +1,107 @@
1
+ import { Application, Request, Response, NextFunction } from 'express';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { pathToFileURL } from 'url';
5
+ import { logger } from './logger.js';
6
+ import { wrapMiddleware } from './utils/wrapMiddleware.js';
7
+
8
+ type MiddlewareFn = (req: Request, res: Response, next: NextFunction) => any;
9
+
10
+ /**
11
+ * Defines a map of routes and the middleware functions that Fastay
12
+ * will automatically load and attach during boot.
13
+ *
14
+ * Keys represent route prefixes (e.g. `/auth`, `/admin`), and
15
+ * values are arrays of Fastay middleware functions.
16
+ *
17
+ * Middleware functions use Fastay’s extended `Request`, `Response`,
18
+ * and `Next` types — not the raw Express versions.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * // src/middleware.ts
23
+ * import { createMiddleware } from '@syntay/fastay';
24
+ * import { authMiddleware } from './auth';
25
+ * import { auditLogger } from './audit';
26
+ *
27
+ * export const middleware = createMiddleware({
28
+ * '/auth': [authMiddleware],
29
+ * '/admin': [auditLogger]
30
+ * });
31
+ * ```
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * // src/auth.ts
36
+ * import { Request, Response, Next } from '@syntay/fastay';
37
+ *
38
+ * export async function authMiddleware(req: Request, _res: Response, next: Next) {
39
+ * // Custom logic using extended Fastay types
40
+ * req.user = { id: 1, role: "admin" };
41
+ * next();
42
+ * }
43
+ * ```
44
+ */
45
+ export type MiddlewareMap = Record<string, MiddlewareFn[]>;
46
+
47
+ /**
48
+ * Creates a Fastay middleware loader.
49
+ *
50
+ * Fastay uses this internally to attach user-defined middleware to the
51
+ * Express application during boot. The framework automatically discovers
52
+ * and loads any `middleware` exported from the project's `src/` directory.
53
+ *
54
+ * Middleware functions are wrapped so both synchronous and asynchronous
55
+ * handlers behave consistently.
56
+ *
57
+ * @param map - A map of route prefixes and the middleware stack for each route.
58
+ * @returns A function that Fastay will call to register the mapped middleware.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * export const middleware = createMiddleware({
63
+ * '/auth': [authMiddleware],
64
+ * '/admin': [adminGuard, auditLogger]
65
+ * });
66
+ * ```
67
+ */
68
+ export function createMiddleware(map: Record<string, MiddlewareFn[]>) {
69
+ return (app: Application) => {
70
+ for (const [route, middlewares] of Object.entries(map)) {
71
+ for (const mw of middlewares) {
72
+ const wrapped = wrapMiddleware(mw);
73
+
74
+ app.use(route, wrapped);
75
+ }
76
+ }
77
+ };
78
+ }
79
+
80
+ export async function loadFastayMiddlewares(app: Application) {
81
+ const isDev = process.env.NODE_ENV !== 'production';
82
+ const mwDir = path.resolve(
83
+ process.cwd(),
84
+ isDev ? 'src/middlewares' : 'dist/middlewares'
85
+ );
86
+
87
+ const file = path.join(mwDir, isDev ? 'middleware.ts' : 'middleware.js');
88
+ if (!fs.existsSync(file)) return;
89
+
90
+ const mod = await import(pathToFileURL(file).href);
91
+
92
+ if (!mod.middleware) return;
93
+
94
+ logger.group('Fastay Auto-Middlewares');
95
+
96
+ if (typeof mod.middleware === 'function') {
97
+ mod.middleware(app);
98
+ logger.info('Loading Fastay core middleware...');
99
+ } else {
100
+ const map = mod.middleware as Record<string, any[]>;
101
+ for (const [route, middlewares] of Object.entries(map)) {
102
+ for (const mw of middlewares) {
103
+ app.use(route, mw);
104
+ }
105
+ }
106
+ }
107
+ }