@seip/blue-bird 0.6.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/.env_example +38 -0
- package/AGENTS.md +276 -0
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/backend/index.js +21 -0
- package/backend/routes/api.js +53 -0
- package/backend/routes/frontend.js +29 -0
- package/core/app.js +397 -0
- package/core/auth.js +180 -0
- package/core/cache.js +72 -0
- package/core/cli/docker.js +319 -0
- package/core/cli/init.js +130 -0
- package/core/cli/route.js +43 -0
- package/core/cli/swagger.js +40 -0
- package/core/config.js +52 -0
- package/core/debug.js +249 -0
- package/core/logger.js +100 -0
- package/core/middleware.js +27 -0
- package/core/router.js +148 -0
- package/core/seo.js +113 -0
- package/core/swagger.js +40 -0
- package/core/template.js +254 -0
- package/core/upload.js +77 -0
- package/core/validate.js +380 -0
- package/docker/Dockerfile +25 -0
- package/docker-compose.yml +70 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/public/js/spa.js +183 -0
- package/frontend/public/js/tailwind.js +8 -0
- package/frontend/templates/about.html +101 -0
- package/frontend/templates/index.html +140 -0
- package/package.json +59 -0
package/core/debug.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import Router from "./router.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advanced Debug module for Blue Bird.
|
|
5
|
+
* Provides metrics history, route statistics and live monitoring.
|
|
6
|
+
*/
|
|
7
|
+
class Debug {
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.router = new Router("/debug");
|
|
11
|
+
this.limit = 50;
|
|
12
|
+
this.initStore();
|
|
13
|
+
this.registerRoutes();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
initStore() {
|
|
17
|
+
if (!global.__bluebird_debug_store__) {
|
|
18
|
+
global.__bluebird_debug_store__ = {
|
|
19
|
+
requests: [],
|
|
20
|
+
routes: {},
|
|
21
|
+
errors4xx: 0,
|
|
22
|
+
errors5xx: 0
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static shouldTrack(req) {
|
|
28
|
+
const url = req.originalUrl || "";
|
|
29
|
+
|
|
30
|
+
if (url.startsWith("/debug")) return false;
|
|
31
|
+
|
|
32
|
+
if (/\.(json|css|js|map|png|jpg|jpeg|gif|svg|ico|webp)$/i.test(url)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (req.method === "OPTIONS") return false;
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static middlewareMetrics(app) {
|
|
42
|
+
app.use((req, res, next) => {
|
|
43
|
+
|
|
44
|
+
if (!Debug.shouldTrack(req)) return next();
|
|
45
|
+
|
|
46
|
+
const start = process.hrtime();
|
|
47
|
+
|
|
48
|
+
res.on("finish", () => {
|
|
49
|
+
|
|
50
|
+
const diff = process.hrtime(start);
|
|
51
|
+
const responseTime = diff[0] * 1e3 + diff[1] / 1e6;
|
|
52
|
+
|
|
53
|
+
const memory = process.memoryUsage();
|
|
54
|
+
const ramUsedMB = memory.rss / 1024 / 1024;
|
|
55
|
+
|
|
56
|
+
const cpuUsage = process.cpuUsage();
|
|
57
|
+
const cpuUsedMS = (cpuUsage.user + cpuUsage.system) / 1000;
|
|
58
|
+
|
|
59
|
+
const record = {
|
|
60
|
+
method: req.method,
|
|
61
|
+
url: req.originalUrl,
|
|
62
|
+
status: res.statusCode,
|
|
63
|
+
responseTime: Number(responseTime.toFixed(2)),
|
|
64
|
+
ramUsedMB: Number(ramUsedMB.toFixed(2)),
|
|
65
|
+
cpuUsedMS: Number(cpuUsedMS.toFixed(2)),
|
|
66
|
+
date: new Date().toISOString()
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const store = global.__bluebird_debug_store__;
|
|
70
|
+
|
|
71
|
+
store.requests.unshift(record);
|
|
72
|
+
if (store.requests.length > 50) store.requests.pop();
|
|
73
|
+
|
|
74
|
+
const routeKey = `${req.method} ${req.route?.path || req.path}`;
|
|
75
|
+
|
|
76
|
+
if (!store.routes[routeKey]) {
|
|
77
|
+
store.routes[routeKey] = {
|
|
78
|
+
count: 0,
|
|
79
|
+
totalTime: 0
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
store.routes[routeKey].count += 1;
|
|
84
|
+
store.routes[routeKey].totalTime += record.responseTime;
|
|
85
|
+
|
|
86
|
+
if (record.status >= 400 && record.status < 500) store.errors4xx++;
|
|
87
|
+
if (record.status >= 500) store.errors5xx++;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
next();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
registerRoutes() {
|
|
95
|
+
|
|
96
|
+
this.router.get("/", (req, res) => {
|
|
97
|
+
|
|
98
|
+
const store = global.__bluebird_debug_store__;
|
|
99
|
+
|
|
100
|
+
if (req.query.fetch === "true") {
|
|
101
|
+
return res.json(store);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (req.query.reset === "true") {
|
|
105
|
+
global.__bluebird_debug_store__ = {
|
|
106
|
+
requests: [],
|
|
107
|
+
routes: {},
|
|
108
|
+
errors4xx: 0,
|
|
109
|
+
errors5xx: 0
|
|
110
|
+
};
|
|
111
|
+
return res.json({ ok: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.send(`
|
|
115
|
+
<!DOCTYPE html>
|
|
116
|
+
<html>
|
|
117
|
+
<head>
|
|
118
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
119
|
+
<title>Blue Bird Debug</title>
|
|
120
|
+
</head>
|
|
121
|
+
<body class="bg-gray-100 text-gray-800 p-10">
|
|
122
|
+
|
|
123
|
+
<div class="max-w-7xl mx-auto">
|
|
124
|
+
|
|
125
|
+
<div class="flex justify-between items-center mb-8">
|
|
126
|
+
<h1 class="text-3xl font-bold text-blue-600">Blue Bird Debug Panel</h1>
|
|
127
|
+
<button onclick="resetData()" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg shadow">
|
|
128
|
+
Reset
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="grid grid-cols-3 gap-6 mb-8">
|
|
133
|
+
|
|
134
|
+
<div class="bg-white p-6 rounded-xl shadow">
|
|
135
|
+
<h3 class="text-gray-500 text-sm">Total Requests</h3>
|
|
136
|
+
<p id="totalReq" class="text-2xl font-bold">0</p>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="bg-yellow-100 p-6 rounded-xl shadow">
|
|
140
|
+
<h3 class="text-yellow-600 text-sm">4xx Errors</h3>
|
|
141
|
+
<p id="err4" class="text-2xl font-bold text-yellow-700">0</p>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="bg-red-100 p-6 rounded-xl shadow">
|
|
145
|
+
<h3 class="text-red-600 text-sm">5xx Errors</h3>
|
|
146
|
+
<p id="err5" class="text-2xl font-bold text-red-700">0</p>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="grid grid-cols-2 gap-10">
|
|
152
|
+
|
|
153
|
+
<div class="bg-white p-6 rounded-xl shadow">
|
|
154
|
+
<h2 class="text-lg font-semibold mb-4">Route Stats</h2>
|
|
155
|
+
<table class="table-fixed w-full text-sm">
|
|
156
|
+
<thead>
|
|
157
|
+
<tr class="border-b">
|
|
158
|
+
<th class="text-left w-1/2 py-2">Route</th>
|
|
159
|
+
<th class="text-left w-1/4">Hits</th>
|
|
160
|
+
<th class="text-left w-1/4">Avg Time</th>
|
|
161
|
+
</tr>
|
|
162
|
+
</thead>
|
|
163
|
+
<tbody id="routesBody"></tbody>
|
|
164
|
+
</table>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="bg-white p-6 rounded-xl shadow">
|
|
168
|
+
<h2 class="text-lg font-semibold mb-4">Last Requests</h2>
|
|
169
|
+
<table class="table-fixed w-full text-sm">
|
|
170
|
+
<thead>
|
|
171
|
+
<tr class="border-b">
|
|
172
|
+
<th class="text-left w-1/2 py-2">URL</th>
|
|
173
|
+
<th class="text-left w-1/6">Method</th>
|
|
174
|
+
<th class="text-left w-1/6">Status</th>
|
|
175
|
+
<th class="text-left w-1/6">Time</th>
|
|
176
|
+
</tr>
|
|
177
|
+
</thead>
|
|
178
|
+
<tbody id="historyBody"></tbody>
|
|
179
|
+
</table>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<script>
|
|
187
|
+
async function loadData() {
|
|
188
|
+
const res = await fetch('/debug?fetch=true');
|
|
189
|
+
const data = await res.json();
|
|
190
|
+
|
|
191
|
+
document.getElementById("totalReq").innerText = data.requests.length;
|
|
192
|
+
document.getElementById("err4").innerText = data.errors4xx;
|
|
193
|
+
document.getElementById("err5").innerText = data.errors5xx;
|
|
194
|
+
|
|
195
|
+
const routesBody = document.getElementById("routesBody");
|
|
196
|
+
const historyBody = document.getElementById("historyBody");
|
|
197
|
+
|
|
198
|
+
routesBody.innerHTML = "";
|
|
199
|
+
historyBody.innerHTML = "";
|
|
200
|
+
|
|
201
|
+
Object.entries(data.routes).forEach(([key, value]) => {
|
|
202
|
+
const avg = (value.totalTime / value.count).toFixed(2);
|
|
203
|
+
let color = "";
|
|
204
|
+
if (avg > 500) color = "text-red-600 font-semibold";
|
|
205
|
+
else if (avg > 200) color = "text-yellow-600";
|
|
206
|
+
|
|
207
|
+
routesBody.innerHTML += \`
|
|
208
|
+
<tr class="border-b">
|
|
209
|
+
<td class="py-1 truncate">\${key}</td>
|
|
210
|
+
<td>\${value.count}</td>
|
|
211
|
+
<td class="\${color}">\${avg} ms</td>
|
|
212
|
+
</tr>\`;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
data.requests.forEach(r => {
|
|
216
|
+
historyBody.innerHTML += \`
|
|
217
|
+
<tr class="border-b text-xs">
|
|
218
|
+
<td class="truncate">\${r.url}</td>
|
|
219
|
+
<td>\${r.method}</td>
|
|
220
|
+
<td>\${r.status}</td>
|
|
221
|
+
<td>\${r.responseTime} ms</td>
|
|
222
|
+
</tr>\`;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function resetData() {
|
|
227
|
+
await fetch('/debug?reset=true');
|
|
228
|
+
loadData();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
loadData();
|
|
232
|
+
setInterval(loadData, 3000);
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
</body>
|
|
236
|
+
</html>
|
|
237
|
+
`);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getRouter() {
|
|
242
|
+
return {
|
|
243
|
+
path: this.router.getPath(),
|
|
244
|
+
router: this.router.getRouter()
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export default Debug;
|
package/core/logger.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import Config from "./config.js"
|
|
4
|
+
|
|
5
|
+
const __dirname = Config.dirname()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Logger class for managing application logs by creating dated folders and log files.
|
|
9
|
+
*/
|
|
10
|
+
class Logger {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initializes the Logger instance and ensures the logs directory exists.
|
|
14
|
+
*/
|
|
15
|
+
constructor() {
|
|
16
|
+
this.folder = path.join(__dirname, "logs");
|
|
17
|
+
this._currentDay = null;
|
|
18
|
+
this._currentDayFolder = null;
|
|
19
|
+
if (!fs.existsSync(this.folder)) {
|
|
20
|
+
fs.mkdirSync(this.folder, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensures and returns the path to the log folder for the current day.
|
|
26
|
+
* Caches the folder path for the current day to avoid repeated fs checks.
|
|
27
|
+
* @returns {string} The absolute path to the current day's log folder.
|
|
28
|
+
*/
|
|
29
|
+
nowFolder() {
|
|
30
|
+
const today = this.now();
|
|
31
|
+
|
|
32
|
+
if (this._currentDay === today && this._currentDayFolder) {
|
|
33
|
+
return this._currentDayFolder;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const folder = path.join(this.folder, today);
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(folder)) {
|
|
39
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this._currentDay = today;
|
|
43
|
+
this._currentDayFolder = folder;
|
|
44
|
+
return folder;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets the current date formatted as YYYY-MM-DD.
|
|
49
|
+
* @returns {string} The formatted date string.
|
|
50
|
+
*/
|
|
51
|
+
now() {
|
|
52
|
+
return new Date().toISOString().split("T")[0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Appends an informational message to the info.log file (non-blocking).
|
|
57
|
+
* @param {string} message - The message to log.
|
|
58
|
+
*/
|
|
59
|
+
info(message) {
|
|
60
|
+
const logFile = path.join(this.nowFolder(), 'info.log');
|
|
61
|
+
fs.appendFile(logFile, `${message}\n`, (err) => {
|
|
62
|
+
if (err) console.error('Logger write error:', err.message);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Appends an error message to the error.log file (non-blocking).
|
|
68
|
+
* @param {string} message - The error message to log.
|
|
69
|
+
*/
|
|
70
|
+
error(message) {
|
|
71
|
+
const logFile = path.join(this.nowFolder(), 'error.log');
|
|
72
|
+
fs.appendFile(logFile, `${message}\n`, (err) => {
|
|
73
|
+
if (err) console.error('Logger write error:', err.message);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Appends a warning message to the warn.log file (non-blocking).
|
|
79
|
+
* @param {string} message - The warning message to log.
|
|
80
|
+
*/
|
|
81
|
+
warning(message) {
|
|
82
|
+
const logFile = path.join(this.nowFolder(), 'warn.log');
|
|
83
|
+
fs.appendFile(logFile, `${message}\n`, (err) => {
|
|
84
|
+
if (err) console.error('Logger write error:', err.message);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Appends a debug message to the debug.log file (non-blocking).
|
|
90
|
+
* @param {string} message - The debug message to log.
|
|
91
|
+
*/
|
|
92
|
+
debug(message) {
|
|
93
|
+
const logFile = path.join(this.nowFolder(), 'debug.log');
|
|
94
|
+
fs.appendFile(logFile, `${message}\n`, (err) => {
|
|
95
|
+
if (err) console.error('Logger write error:', err.message);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default Logger;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Auth from "./auth.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common middlewares for the Blue Bird framework.
|
|
5
|
+
*/
|
|
6
|
+
const Middleware = {
|
|
7
|
+
/**
|
|
8
|
+
* Authentication protection middleware.
|
|
9
|
+
* @type {Function}
|
|
10
|
+
*/
|
|
11
|
+
auth: Auth.protect(),
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Web authentication protection middleware (redirects to home if fails).
|
|
15
|
+
* @type {Function}
|
|
16
|
+
*/
|
|
17
|
+
webAuth: Auth.protect({ redirect: "/" }),
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Logging middleware (can be extended).
|
|
21
|
+
*/
|
|
22
|
+
logger: (req, res, next) => {
|
|
23
|
+
next();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default Middleware;
|
package/core/router.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import Config from "./config.js";
|
|
3
|
+
import SEO from "./seo.js";
|
|
4
|
+
|
|
5
|
+
const props = Config.props();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Router wrapper class for handling Express routing logic.
|
|
9
|
+
* When created with { seo: true }, all GET routes registered on this router
|
|
10
|
+
* are automatically included in the generated sitemap.xml and robots.txt.
|
|
11
|
+
*/
|
|
12
|
+
class Router {
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new Router instance.
|
|
15
|
+
* @param {string} [path="/"] - The base path for this router.
|
|
16
|
+
* @param {Object} [options={}] - Router configuration options.
|
|
17
|
+
* @param {boolean} [options.seo=false] - When true, GET routes on this router are included in sitemap/robots.txt.
|
|
18
|
+
* @param {string[]} [options.languages=[]] - Language prefixes for SEO route generation (e.g., ["en", "es"]).
|
|
19
|
+
* @example
|
|
20
|
+
* const router = new Router("/", { seo: true, languages: ["en", "es"] });
|
|
21
|
+
* router.get("/", (req, res) => {
|
|
22
|
+
* Template.render(res, "index", { metaTags: { titleMeta: "Home" } });
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
constructor(path = "/", options = {}) {
|
|
26
|
+
this.router = express.Router();
|
|
27
|
+
this.path = path;
|
|
28
|
+
this._seo = options.seo ?? false;
|
|
29
|
+
this._languages = options.languages || [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registers a middleware on this router.
|
|
34
|
+
* @param {...Function} middleware - Middleware functions.
|
|
35
|
+
* @example
|
|
36
|
+
* router.use(Auth.protect());
|
|
37
|
+
* router.use(App.helmet());
|
|
38
|
+
*/
|
|
39
|
+
use(...middleware) {
|
|
40
|
+
this.router.use(...middleware);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Registers a GET route handler.
|
|
45
|
+
* If this router was created with { seo: true }, the route path is automatically
|
|
46
|
+
* registered for sitemap.xml and robots.txt generation.
|
|
47
|
+
* @param {string} path - The relative path for the GET route.
|
|
48
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
49
|
+
* @example
|
|
50
|
+
* router.get("/about", (req, res) => {
|
|
51
|
+
* Template.render(res, "about", {
|
|
52
|
+
* metaTags: { titleMeta: "About Us" }
|
|
53
|
+
* });
|
|
54
|
+
* });
|
|
55
|
+
*/
|
|
56
|
+
get(path, ...callback) {
|
|
57
|
+
if (path === "/*" || path === "*") {
|
|
58
|
+
path = /.*/;
|
|
59
|
+
}
|
|
60
|
+
if (this._seo && typeof path === "string") {
|
|
61
|
+
const fullPath = this.path === "/" ? path : `${this.path}${path}`;
|
|
62
|
+
const normalizedPath = fullPath === "//" ? "/" : fullPath.replace(/\/+/g, "/");
|
|
63
|
+
SEO.addRoute(normalizedPath, this._languages);
|
|
64
|
+
}
|
|
65
|
+
this.router.get(path, callback);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Registers a POST route handler.
|
|
70
|
+
* @param {string} path - The relative path for the POST route.
|
|
71
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
72
|
+
* @example
|
|
73
|
+
* router.post("/users", (req, res) => {
|
|
74
|
+
* res.json({ message: "User created successfully" })
|
|
75
|
+
* })
|
|
76
|
+
*/
|
|
77
|
+
post(path, ...callback) {
|
|
78
|
+
if (path === "/*" || path === "*") {
|
|
79
|
+
path = /.*/;
|
|
80
|
+
}
|
|
81
|
+
this.router.post(path, callback);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Registers a PUT route handler.
|
|
86
|
+
* @param {string} path - The relative path for the PUT route.
|
|
87
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
88
|
+
* @example
|
|
89
|
+
* router.put("/users/:id", (req, res) => {
|
|
90
|
+
* res.json({ message: "User updated successfully" })
|
|
91
|
+
* })
|
|
92
|
+
*/
|
|
93
|
+
put(path, ...callback) {
|
|
94
|
+
this.router.put(path, callback);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Registers a DELETE route handler.
|
|
99
|
+
* @param {string} path - The relative path for the DELETE route.
|
|
100
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
101
|
+
* @example
|
|
102
|
+
* router.delete("/users/:id", (req, res) => {
|
|
103
|
+
* res.json({ message: "User deleted successfully" })
|
|
104
|
+
* })
|
|
105
|
+
*/
|
|
106
|
+
delete(path, ...callback) {
|
|
107
|
+
this.router.delete(path, callback);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Registers a PATCH route handler.
|
|
112
|
+
* @param {string} path - The relative path for the PATCH route.
|
|
113
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
114
|
+
* @example
|
|
115
|
+
* router.patch("/users/:id", (req, res) => {
|
|
116
|
+
* res.json({ message: "User patched successfully" })
|
|
117
|
+
* })
|
|
118
|
+
*/
|
|
119
|
+
patch(path, ...callback) {
|
|
120
|
+
this.router.patch(path, callback);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Registers an OPTIONS route handler.
|
|
125
|
+
* @param {string} path - The relative path for the OPTIONS route.
|
|
126
|
+
* @param {...Function} callback - One or more handler functions (middlewares and controller).
|
|
127
|
+
*/
|
|
128
|
+
options(path, ...callback) {
|
|
129
|
+
this.router.options(path, callback);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns the underlying Express router instance.
|
|
134
|
+
* @returns {import('express').Router}
|
|
135
|
+
*/
|
|
136
|
+
getRouter() {
|
|
137
|
+
return this.router;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns the base path associated with this router.
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
getPath() {
|
|
145
|
+
return this.path;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export default Router;
|
package/core/seo.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Config from "./config.js";
|
|
2
|
+
|
|
3
|
+
const props = Config.props();
|
|
4
|
+
|
|
5
|
+
/** @type {Array<{path: string, languages: string[]}>} */
|
|
6
|
+
const _seoRoutes = [];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SEO utility class for generating sitemaps and robots.txt files.
|
|
10
|
+
* Routes are automatically registered from Router instances created with { seo: true }.
|
|
11
|
+
*/
|
|
12
|
+
class SEO {
|
|
13
|
+
/**
|
|
14
|
+
* Registers a route path for sitemap generation.
|
|
15
|
+
* Called internally by Router when seo option is enabled.
|
|
16
|
+
* @static
|
|
17
|
+
* @param {string} routePath - The route path to register.
|
|
18
|
+
* @param {string[]} [languages=[]] - Language prefixes for this route.
|
|
19
|
+
*/
|
|
20
|
+
static addRoute(routePath, languages = []) {
|
|
21
|
+
const exists = _seoRoutes.find(r => r.path === routePath);
|
|
22
|
+
if (!exists) {
|
|
23
|
+
_seoRoutes.push({ path: routePath, languages });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns all registered SEO routes.
|
|
29
|
+
* @static
|
|
30
|
+
* @returns {Array<{path: string, languages: string[]}>}
|
|
31
|
+
*/
|
|
32
|
+
static getRoutes() {
|
|
33
|
+
return _seoRoutes;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clears all registered SEO routes.
|
|
38
|
+
* @static
|
|
39
|
+
*/
|
|
40
|
+
static clearRoutes() {
|
|
41
|
+
_seoRoutes.length = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generates a sitemap.xml string from all registered SEO routes.
|
|
46
|
+
* @static
|
|
47
|
+
* @returns {string} The generated XML sitemap.
|
|
48
|
+
*/
|
|
49
|
+
static generateSitemap() {
|
|
50
|
+
const host = (props.appUrl || `${props.host}:${props.port}`).replace(/\/$/, "");
|
|
51
|
+
const date = new Date().toISOString().split("T")[0];
|
|
52
|
+
|
|
53
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
54
|
+
xml +=
|
|
55
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">';
|
|
56
|
+
|
|
57
|
+
_seoRoutes.forEach((route) => {
|
|
58
|
+
xml += `
|
|
59
|
+
<url>
|
|
60
|
+
<loc>${host}${route.path}</loc>
|
|
61
|
+
<lastmod>${date}</lastmod>
|
|
62
|
+
<priority>${route.path === "/" ? "1.0" : "0.8"}</priority>
|
|
63
|
+
</url>`;
|
|
64
|
+
if (route.languages && route.languages.length > 0) {
|
|
65
|
+
route.languages.forEach((lang) => {
|
|
66
|
+
const langPath = `/${lang}${route.path === "/" ? "" : route.path}`;
|
|
67
|
+
xml += `
|
|
68
|
+
<url>
|
|
69
|
+
<loc>${host}${langPath}</loc>
|
|
70
|
+
<lastmod>${date}</lastmod>
|
|
71
|
+
<priority>1</priority>
|
|
72
|
+
</url>`;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
xml += "\n</urlset>";
|
|
78
|
+
return xml.trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generates a robots.txt string.
|
|
83
|
+
* @static
|
|
84
|
+
* @returns {string} The generated robots.txt content.
|
|
85
|
+
*/
|
|
86
|
+
static generateRobots() {
|
|
87
|
+
const host = (props.appUrl || "http://localhost").replace(/\/$/, "");
|
|
88
|
+
return `User-agent: *
|
|
89
|
+
Allow: /
|
|
90
|
+
|
|
91
|
+
Sitemap: ${host}/sitemap.xml
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Registers /sitemap.xml and /robots.txt routes on the given Express router.
|
|
97
|
+
* @static
|
|
98
|
+
* @param {import('express').Router} expressRouter - The Express router instance.
|
|
99
|
+
*/
|
|
100
|
+
static registerEndpoints(expressRouter) {
|
|
101
|
+
expressRouter.get("/sitemap.xml", (req, res) => {
|
|
102
|
+
res.header("Content-Type", "application/xml");
|
|
103
|
+
res.send(SEO.generateSitemap());
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expressRouter.get("/robots.txt", (req, res) => {
|
|
107
|
+
res.header("Content-Type", "text/plain");
|
|
108
|
+
res.send(SEO.generateRobots());
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default SEO;
|
package/core/swagger.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
class SwaggerCli {
|
|
4
|
+
install() {
|
|
5
|
+
const dependencies = this.checkDependencies();
|
|
6
|
+
if (dependencies.missingDependencies.length > 0) {
|
|
7
|
+
console.log("Installing dependencies...");
|
|
8
|
+
console.log(`Installing swagger-jsdoc...`);
|
|
9
|
+
execSync(`npm install swagger-jsdoc@6.2.8`, { stdio: "inherit" });
|
|
10
|
+
console.log(`Installing swagger-ui-express...`);
|
|
11
|
+
execSync(`npm install swagger-ui-express@5.0.1`, { stdio: "inherit" });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
checkDependencies() {
|
|
15
|
+
const dependencies = [
|
|
16
|
+
"swagger-jsdoc",
|
|
17
|
+
"swagger-ui-express"
|
|
18
|
+
];
|
|
19
|
+
const missingDependencies = [];
|
|
20
|
+
dependencies.forEach(dependency => {
|
|
21
|
+
if (!this.checkDependency(dependency)) {
|
|
22
|
+
missingDependencies.push(dependency);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
missingDependencies
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
checkDependency(dependency) {
|
|
30
|
+
try {
|
|
31
|
+
require.resolve(dependency);
|
|
32
|
+
return true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const swaggerExecutor = new SwaggerCli();
|
|
40
|
+
swaggerExecutor.install();
|