@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.
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/dist/app.d.ts +123 -0
- package/dist/app.js +106 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +9 -0
- package/dist/error-handler.d.ts +2 -0
- package/dist/error-handler.js +36 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +61 -0
- package/dist/middleware.d.ts +62 -0
- package/dist/middleware.js +59 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.js +151 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/wrapMiddleware.d.ts +6 -0
- package/dist/utils/wrapMiddleware.js +74 -0
- package/package.json +25 -0
- package/src/app.ts +233 -0
- package/src/banner.ts +11 -0
- package/src/error-handler.ts +48 -0
- package/src/index.ts +4 -0
- package/src/logger.ts +78 -0
- package/src/middleware.ts +107 -0
- package/src/router.ts +175 -0
- package/src/types/index.ts +13 -0
- package/src/utils/wrapMiddleware.ts +95 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +17 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { Application, Request, Response, NextFunction } from 'express';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converte caminho do arquivo em rota Express (somente arquivos route.ts)
|
|
9
|
+
*/
|
|
10
|
+
export function filePathToRoute(
|
|
11
|
+
apiDir: string,
|
|
12
|
+
filePath: string,
|
|
13
|
+
baseRoute: string
|
|
14
|
+
) {
|
|
15
|
+
const rel = path.relative(apiDir, filePath);
|
|
16
|
+
const parts = rel.split(path.sep);
|
|
17
|
+
const filename = parts.pop()!;
|
|
18
|
+
if (filename !== 'route.ts' && filename !== 'route.js') return null;
|
|
19
|
+
|
|
20
|
+
const segments = parts
|
|
21
|
+
.map((s) =>
|
|
22
|
+
s.startsWith('[') && s.endsWith(']') ? `:${s.slice(1, -1)}` : s
|
|
23
|
+
)
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
|
|
26
|
+
return `${baseRoute}/${segments.join('/')}`.replace(/\/+/g, '/');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Retorna todos arquivos .ts/.js recursivamente
|
|
31
|
+
*/
|
|
32
|
+
export function collectFiles(dir: string): string[] {
|
|
33
|
+
let out: string[] = [];
|
|
34
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
for (const it of items) {
|
|
36
|
+
const full = path.join(dir, it.name);
|
|
37
|
+
if (it.isDirectory()) out = out.concat(collectFiles(full));
|
|
38
|
+
else if (/\.(ts|js|mts|mjs)$/.test(it.name)) out.push(full);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wrapper para suportar return JSON/string/number e capturar erros runtime
|
|
45
|
+
*/
|
|
46
|
+
function wrapHandler(fn: Function, routePath: string, filePath: string) {
|
|
47
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
48
|
+
try {
|
|
49
|
+
const result = fn.length >= 2 ? await fn(req, res) : await fn(req);
|
|
50
|
+
if (res.headersSent) return;
|
|
51
|
+
if (result === undefined) return;
|
|
52
|
+
if (typeof result === 'string') return res.send(result);
|
|
53
|
+
if (typeof result === 'number') return res.send(String(result));
|
|
54
|
+
return res.json(result);
|
|
55
|
+
} catch (err: any) {
|
|
56
|
+
const stack = err?.stack?.split('\n').slice(0, 3).join('\n') || '';
|
|
57
|
+
// logger.error(
|
|
58
|
+
// `✗ Runtime Error in route [${req.method} ${routePath}]\n` +
|
|
59
|
+
// ` File: ${filePath}\n` +
|
|
60
|
+
// ` ${err.name}: ${err.message || 'Unknown error'}\n` +
|
|
61
|
+
// ` Stack: ${stack}`
|
|
62
|
+
// );
|
|
63
|
+
|
|
64
|
+
let fileInfo = '';
|
|
65
|
+
if (err.stack) {
|
|
66
|
+
const stackLine = err.stack.split('\n')[1]; // pega primeira linha depois do erro
|
|
67
|
+
const match = stackLine.match(/\((.*):(\d+):(\d+)\)/);
|
|
68
|
+
if (match) {
|
|
69
|
+
const [_, file, line, col] = match;
|
|
70
|
+
fileInfo = `${file}:${line}:${col}`;
|
|
71
|
+
|
|
72
|
+
// Tenta mostrar o trecho da linha que deu erro
|
|
73
|
+
if (fs.existsSync(file)) {
|
|
74
|
+
const codeLines = fs.readFileSync(file, 'utf-8').split('\n');
|
|
75
|
+
const codeSnippet = codeLines[parseInt(line) - 1].trim();
|
|
76
|
+
fileInfo += ` → ${codeSnippet}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// logger.group(`✗ Runtime Error in route [${req.method} ${routePath}]`);
|
|
82
|
+
logger.error(`${err.name}: ${err.message}`);
|
|
83
|
+
if (fileInfo) logger.error(`Location: ${fileInfo}`);
|
|
84
|
+
|
|
85
|
+
next(err);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Carrega todas as rotas do diretório apiDir
|
|
92
|
+
*/
|
|
93
|
+
export async function loadApiRoutes(
|
|
94
|
+
app: Application,
|
|
95
|
+
baseRoute: string,
|
|
96
|
+
apiDirectory: string
|
|
97
|
+
) {
|
|
98
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
99
|
+
|
|
100
|
+
const apiDir = path.join(process.cwd(), isDev ? apiDirectory : 'dist/api');
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(apiDir)) return 0;
|
|
103
|
+
|
|
104
|
+
const files = collectFiles(apiDir);
|
|
105
|
+
let cont = 0;
|
|
106
|
+
|
|
107
|
+
logger.group('Routes Loaded');
|
|
108
|
+
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
const route = filePathToRoute(apiDir, file, baseRoute);
|
|
111
|
+
if (!route) continue;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const fileUrl = pathToFileURL(file).href;
|
|
115
|
+
const mod = await import(fileUrl);
|
|
116
|
+
|
|
117
|
+
const httpMethods = [
|
|
118
|
+
'GET',
|
|
119
|
+
'POST',
|
|
120
|
+
'PUT',
|
|
121
|
+
'DELETE',
|
|
122
|
+
'PATCH',
|
|
123
|
+
'OPTIONS',
|
|
124
|
+
'HEAD',
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (const m of httpMethods) {
|
|
128
|
+
if (typeof mod[m] === 'function') {
|
|
129
|
+
(app as any)[m.toLowerCase()](
|
|
130
|
+
route,
|
|
131
|
+
wrapHandler(mod[m], route, file)
|
|
132
|
+
);
|
|
133
|
+
cont++;
|
|
134
|
+
logger.success(`Route: [${m}] ${route}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (mod.default && typeof mod.default === 'function') {
|
|
139
|
+
app.get(route, wrapHandler(mod.default, route, file));
|
|
140
|
+
cont++;
|
|
141
|
+
logger.success(`Route: [GET] ${route}`);
|
|
142
|
+
}
|
|
143
|
+
} catch (err: any) {
|
|
144
|
+
const stack = err?.stack?.split('\n').slice(0, 3).join('\n') || '';
|
|
145
|
+
// logger.error(
|
|
146
|
+
// `✗ Boot Error importing ${file}\n` +
|
|
147
|
+
// ` Message: ${err.message || 'Unknown error'}\n` +
|
|
148
|
+
// ` Stack: ${stack}`
|
|
149
|
+
// );
|
|
150
|
+
|
|
151
|
+
let fileInfo = '';
|
|
152
|
+
if (err.stack) {
|
|
153
|
+
const stackLine = err.stack.split('\n')[1]; // pega primeira linha depois do erro
|
|
154
|
+
const match = stackLine.match(/\((.*):(\d+):(\d+)\)/);
|
|
155
|
+
if (match) {
|
|
156
|
+
const [_, file, line, col] = match;
|
|
157
|
+
fileInfo = `${file}:${line}:${col}`;
|
|
158
|
+
|
|
159
|
+
// Tenta mostrar o trecho da linha que deu erro
|
|
160
|
+
if (fs.existsSync(file)) {
|
|
161
|
+
const codeLines = fs.readFileSync(file, 'utf-8').split('\n');
|
|
162
|
+
const codeSnippet = codeLines[parseInt(line) - 1].trim();
|
|
163
|
+
fileInfo += ` → ${codeSnippet}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// logger.group(`✗ Boot Error importing ${file}`);
|
|
169
|
+
logger.error(`${err.name}: ${err.message}`);
|
|
170
|
+
if (fileInfo) logger.error(`Location: ${fileInfo}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return cont;
|
|
175
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Request as ExpressRequest,
|
|
3
|
+
Response as ExpressResponse,
|
|
4
|
+
NextFunction,
|
|
5
|
+
} from 'express';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Request e Response do Express
|
|
9
|
+
* Podem ser usados nos handlers do usuário
|
|
10
|
+
*/
|
|
11
|
+
export type Request = ExpressRequest;
|
|
12
|
+
export type Response = ExpressResponse;
|
|
13
|
+
export type Next = NextFunction;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
|
|
4
|
+
export type MiddlewareFn = (
|
|
5
|
+
req: Request,
|
|
6
|
+
res: Response,
|
|
7
|
+
next: NextFunction
|
|
8
|
+
) => any;
|
|
9
|
+
|
|
10
|
+
const color = {
|
|
11
|
+
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
12
|
+
gray: (s: string) => `\x1b[90m${s}\x1b[0m`,
|
|
13
|
+
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
14
|
+
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
15
|
+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
16
|
+
white: (s: string) => `\x1b[37m${s}\x1b[0m`,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Formata uma função para exibição bonitinha
|
|
21
|
+
*/
|
|
22
|
+
function formatFunction(fn: Function): string {
|
|
23
|
+
let code = fn.toString();
|
|
24
|
+
|
|
25
|
+
// deixa com quebras de linha
|
|
26
|
+
code = code.replace(/;/g, ';\n');
|
|
27
|
+
|
|
28
|
+
// tenta dar indentação
|
|
29
|
+
code = code
|
|
30
|
+
.replace(/{/g, '{\n ')
|
|
31
|
+
.replace(/}/g, '\n}')
|
|
32
|
+
.replace(/\n\s*\n/g, '\n');
|
|
33
|
+
|
|
34
|
+
return code.trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Verifica se next() ou return existem
|
|
39
|
+
*/
|
|
40
|
+
function validateMiddlewareCode(mw: MiddlewareFn) {
|
|
41
|
+
const raw = mw.toString();
|
|
42
|
+
const name = mw.name || 'anonymous';
|
|
43
|
+
|
|
44
|
+
const cleaned = raw
|
|
45
|
+
.replace(/\/\/.*$/gm, '')
|
|
46
|
+
.replace(/\/\*[\s\S]*?\*\//gm, '');
|
|
47
|
+
|
|
48
|
+
const hasNext = /next\s*\(/.test(cleaned);
|
|
49
|
+
const hasReturn = /\breturn\b/.test(cleaned);
|
|
50
|
+
|
|
51
|
+
if (!hasNext && !hasReturn) {
|
|
52
|
+
const prettyCode = formatFunction(mw);
|
|
53
|
+
|
|
54
|
+
const message = [
|
|
55
|
+
`${color.red('⨯ Fastay Middleware Error')}`,
|
|
56
|
+
``,
|
|
57
|
+
`The middleware ${color.yellow(
|
|
58
|
+
`"${name}"`
|
|
59
|
+
)} does not call next() or return any value.`,
|
|
60
|
+
`This will halt the middleware chain and block the request pipeline.`,
|
|
61
|
+
``,
|
|
62
|
+
`▌ Middleware Source`,
|
|
63
|
+
color.gray(prettyCode),
|
|
64
|
+
``,
|
|
65
|
+
`${color.cyan('▌ How to fix')}`,
|
|
66
|
+
`Ensure your middleware ends with either:`,
|
|
67
|
+
` • ${color.green('next()')}`,
|
|
68
|
+
` • ${color.green('return ...')}`,
|
|
69
|
+
``,
|
|
70
|
+
`Fastay cannot continue until this middleware is fixed.`,
|
|
71
|
+
].join('\n');
|
|
72
|
+
|
|
73
|
+
const err = new Error(message);
|
|
74
|
+
err.name = 'FastayMiddlewareError';
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Wrapper final
|
|
81
|
+
*/
|
|
82
|
+
export function wrapMiddleware(mw: MiddlewareFn): MiddlewareFn {
|
|
83
|
+
const name = mw.name || 'anonymous';
|
|
84
|
+
validateMiddlewareCode(mw);
|
|
85
|
+
|
|
86
|
+
// retorna middleware "puro"
|
|
87
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
88
|
+
try {
|
|
89
|
+
await mw(req, res, next);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger.error(`[${name}] middleware error: ${err}`);
|
|
92
|
+
next(err);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|