@seip/blue-bird 0.1.7 → 0.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/core/app.js CHANGED
@@ -27,6 +27,23 @@ class App {
27
27
  * @param {boolean} [options.urlencoded=true] - Whether to enable URL-encoded body parsing.
28
28
  * @param {Object} [options.static={path: null, options: {}}] - Static file configuration.
29
29
  * @param {boolean} [options.cookieParser=true] - Whether to enable cookie parsing.
30
+ * @example
31
+ * const app = new App({
32
+ * routes: [],
33
+ * cors: {},
34
+ * middlewares: [],
35
+ * port: 3000,
36
+ * host: "http://localhost",
37
+ * logger: true,
38
+ * notFound: true,
39
+ * json: true,
40
+ * urlencoded: true,
41
+ * static: {
42
+ * path: "public",
43
+ * options: {}
44
+ * },
45
+ * cookieParser: true,
46
+ * });
30
47
  */
31
48
  constructor(options = {
32
49
  routes: [],
@@ -64,10 +81,25 @@ class App {
64
81
  /**
65
82
  * Registers a custom middleware or module in the Express application.
66
83
  * @param {Function|import('express').Router} record - The middleware function or Express router to register.
84
+ * @example
85
+ * app.use((req, res, next) => {
86
+ * console.log("Middleware");
87
+ * next();
88
+ * });
67
89
  */
68
90
  use(record) {
69
91
  this.app.use(record)
70
92
  }
93
+ /**
94
+ * Sets a configuration value in the Express application.
95
+ * @param {string} key - The configuration key.
96
+ * @param {*} value - The value to set for the configuration key.
97
+ * @example
98
+ * app.set("port", 3000);
99
+ */
100
+ set(key, value) {
101
+ this.app.set(key, value)
102
+ }
71
103
 
72
104
  /**
73
105
  * Bootstraps the application by configuring global middlewares and routes.
package/core/auth.js CHANGED
@@ -10,6 +10,10 @@ class Auth {
10
10
  * @param {string} [secret=process.env.JWT_SECRET] - The secret key.
11
11
  * @param {string|number} [expiresIn='24h'] - Expiration time.
12
12
  * @returns {string} The generated token.
13
+ * @example
14
+ * const token = Auth.generateToken({ id: 1 });
15
+ * console.log(token);
16
+ *
13
17
  */
14
18
  static generateToken(payload, secret = process.env.JWT_SECRET || 'blue-bird-secret', expiresIn = '24h') {
15
19
  return jwt.sign(payload, secret, { expiresIn });
@@ -20,6 +24,10 @@ class Auth {
20
24
  * @param {string} token - The token to verify.
21
25
  * @param {string} [secret=process.env.JWT_SECRET] - The secret key.
22
26
  * @returns {Object|null} The decoded payload or null if invalid.
27
+ * @example
28
+ * const token = Auth.generateToken({ id: 1 });
29
+ * const decoded = Auth.verifyToken(token);
30
+ * console.log(decoded);
23
31
  */
24
32
  static verifyToken(token, secret = process.env.JWT_SECRET || 'blue-bird-secret') {
25
33
  try {
@@ -34,6 +42,8 @@ class Auth {
34
42
  * @param {Object} options - Options for protection.
35
43
  * @param {string} [options.redirect] - URL to redirect if not authenticated (for web routes).
36
44
  * @returns {Function} Express middleware.
45
+ * @example
46
+ * app.use(Auth.protect({ redirect: "/login" }));
37
47
  */
38
48
  static protect(options = { redirect: null }) {
39
49
  return (req, res, next) => {
@@ -0,0 +1,42 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import Config from "../config.js";
4
+
5
+ const __dirname = Config.dirname();
6
+
7
+ class ComponentCLI {
8
+ /**
9
+ * Create component react
10
+ * @return {void}
11
+ * /
12
+ */
13
+
14
+ create() {
15
+ const folder = path.join(process.cwd(), "frontend/resources/components");
16
+ if (!fs.existsSync(folder)) {
17
+ fs.mkdirSync(folder, { recursive: true });
18
+ }
19
+ let nameComponent =`Component-${Math.random().toString(36).substring(7)}`;
20
+ const nameParam = process.argv[2];
21
+ if (nameParam.length > 0 && typeof nameParam === "string") {
22
+ nameComponent = nameParam;
23
+ nameComponent = nameComponent.charAt(0).toUpperCase() + nameComponent.slice(1);
24
+ }
25
+ const filePath = path.join(folder, `${nameComponent}.jsx`);
26
+ const content = `import React from 'react';
27
+
28
+ export default function ${nameComponent}() {
29
+ return (
30
+ <div>
31
+ <h1>${nameComponent} Component</h1>
32
+ </div>
33
+ );
34
+ }
35
+ `;
36
+ fs.writeFileSync(filePath, content);
37
+ console.log(`Component ${nameComponent} created at ${filePath}`);
38
+ }
39
+ }
40
+
41
+ const componentCLI = new ComponentCLI();
42
+ componentCLI.create();
@@ -0,0 +1,43 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import Config from "../config.js";
4
+
5
+ const __dirname = Config.dirname();
6
+
7
+ class RouteCLI {
8
+ /**
9
+ * Create route
10
+ */
11
+ create() {
12
+ let nameRoute = process.argv[2];
13
+ if (!nameRoute) {
14
+ console.log("Please provide a route name. Usage: npm run route <route-name>");
15
+ return;
16
+ }
17
+ nameRoute =nameRoute.charAt(0).toUpperCase() + nameRoute.slice(1);
18
+ const folder= path.join(__dirname, 'backend/routes');
19
+ if (!fs.existsSync(folder)){
20
+ fs.mkdirSync(folder, { recursive: true });
21
+ }
22
+ const filePath = path.join(folder, `${nameRoute}.js`);
23
+ if (fs.existsSync(filePath)) {
24
+ console.log(`Route ${nameRoute} already exists.`);
25
+ return;
26
+ }
27
+ const content =`import Router from "@seip/blue-bird/core/router.js"
28
+
29
+ const router${nameRoute} = new Router("/${nameRoute.toLowerCase()}");
30
+
31
+ router${nameRoute}.get("/", (req, res) => {
32
+ res.json({ message: "Hello from ${nameRoute} route!" });
33
+ });
34
+
35
+ export default router${nameRoute};
36
+ `;
37
+ fs.writeFileSync(filePath, content);
38
+ console.log(`Route ${nameRoute} created successfully at ${filePath}`);
39
+ }
40
+ }
41
+
42
+ const routeCLI = new RouteCLI();
43
+ routeCLI.create()
package/core/router.js CHANGED
@@ -11,6 +11,11 @@ class Router {
11
11
  /**
12
12
  * Creates a new Router instance.
13
13
  * @param {string} [path="/"] - The base path for this router.
14
+ * @example
15
+ * const router = new Router("/api")
16
+ * router.get("/", (req, res) => {
17
+ * res.json({ message: "Hello World!" })
18
+ * })
14
19
  */
15
20
  constructor(path = "/") {
16
21
  this.router = express.Router()
@@ -21,6 +26,20 @@ class Router {
21
26
  * Registers a GET route handler.
22
27
  * @param {string} path - The relative path for the GET route.
23
28
  * @param {...Function} callback - One or more handler functions (middlewares and controller).
29
+ * @example
30
+ * router.get("/users", (req, res) => {
31
+ * const users = [
32
+ * {
33
+ * name: "John Doe",
34
+ * email: "john.doe@example.com",
35
+ * },
36
+ * {
37
+ * name: "Jane Doe2",
38
+ * email: "jane.doe2@example.com",
39
+ * },
40
+ * ]
41
+ * res.json(users)
42
+ * })
24
43
  */
25
44
  get(path, ...callback) {
26
45
  if (path === "/*" || path === "*") {
@@ -33,6 +52,10 @@ class Router {
33
52
  * Registers a POST route handler.
34
53
  * @param {string} path - The relative path for the POST route.
35
54
  * @param {...Function} callback - One or more handler functions (middlewares and controller).
55
+ * @example
56
+ * router.post("/users", (req, res) => {
57
+ * return res.json({ message: "User created successfully" })
58
+ * })
36
59
  */
37
60
  post(path, ...callback) {
38
61
  if (path === "/*" || path === "*") {
@@ -45,6 +68,10 @@ class Router {
45
68
  * Registers a PUT route handler.
46
69
  * @param {string} path - The relative path for the PUT route.
47
70
  * @param {...Function} callback - One or more handler functions (middlewares and controller).
71
+ * @example
72
+ * router.put("/users", (req, res) => {
73
+ * return res.json({ message: "User updated successfully" })
74
+ * })
48
75
  */
49
76
  put(path, ...callback) {
50
77
  this.router.put(path, callback)
@@ -54,6 +81,10 @@ class Router {
54
81
  * Registers a DELETE route handler.
55
82
  * @param {string} path - The relative path for the DELETE route.
56
83
  * @param {...Function} callback - One or more handler functions (middlewares and controller).
84
+ * @example
85
+ * router.delete("/users", (req, res) => {
86
+ * return res.json({ message: "User deleted successfully" })
87
+ * })
57
88
  */
58
89
  delete(path, ...callback) {
59
90
  this.router.delete(path, callback)
@@ -63,6 +94,10 @@ class Router {
63
94
  * Registers a PATCH route handler.
64
95
  * @param {string} path - The relative path for the PATCH route.
65
96
  * @param {...Function} callback - One or more handler functions (middlewares and controller).
97
+ * @example
98
+ * router.patch("/users", (req, res) => {
99
+ * return res.json({ message: "User patched successfully" })
100
+ * })
66
101
  */
67
102
  patch(path, ...callback) {
68
103
  this.router.patch(path, callback)
@@ -72,6 +107,10 @@ class Router {
72
107
  * Registers an OPTIONS route handler.
73
108
  * @param {string} path - The relative path for the OPTIONS route.
74
109
  * @param {...Function} callback - One or more handler functions (middlewares and controller).
110
+ * @example
111
+ * router.options("/users", (req, res) => {
112
+ * return res.json({ message: "User options successfully" })
113
+ * })
75
114
  */
76
115
  options(path, ...callback) {
77
116
  this.router.options(path, callback)
package/core/template.js CHANGED
@@ -74,22 +74,44 @@ class Template {
74
74
  * Renders a React component as an HTML string.
75
75
  * @param {string} component - The React component name.
76
76
  * @param {Object} [componentProps={}] - Props to pass to the component.
77
- * @param {string} [classBody=""] - Class name to add to the body tag.
78
- * @param {Array<Object>} [optionsHead=[]] - Array of objects with tag and attributes for head tags.
77
+ * @options {Object} options - Options for the template.
79
78
  * @returns {string} The HTML string of the React component.
79
+ * @example
80
+ * const options = {
81
+ * head: [
82
+ * { tag: "meta", attrs: { name: "description", content: "Description" } },
83
+ * { tag: "link", attrs: { rel: "stylesheet", href: "style.css" } }
84
+ * ],
85
+ * classBody: "bg-gray-100",
86
+ * linkStyles: [
87
+ * { href: "style.css" }
88
+ * ],
89
+ * scriptScripts: [
90
+ * { src: "script.js" }
91
+ * ]
92
+ * };
93
+ *
94
+ * Template.renderReact(res, "App", { title: "Example title" }, options);
80
95
  */
81
- static renderReact(res, component = "App", propsReact = {}, classBody = "", optionsHead = []) {
96
+ static renderReact(res, component = "App", propsReact = {}, options = {}) {
97
+ const optionsHead = options.head || [];
98
+ const classBody = options.classBody || "";
99
+ const linkStyles = options.linkStyles || [];
100
+ const scriptScripts = options.scriptScripts || [];
82
101
  const html = `
83
102
  <!DOCTYPE html>
84
- <html lang="${props.langMeta}">
103
+ <html lang="${this.escapeHtml(props.langMeta)}">
85
104
  <head>
86
105
  <meta charset="UTF-8">
87
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
88
- <title>${props.titleMeta}</title>
89
- <meta name="description" content="${props.descriptionMeta}">
90
- <meta name="keywords" content="${props.keywordsMeta}">
91
- <meta name="author" content="${props.authorMeta}">
92
- ${optionsHead.map(item => `<${item.tag} ${Object.entries(item.attrs).map(([key, value]) => `${key}="${value}"`).join(" ")}></${item.tag}>`).join("")}
106
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
107
+ <title>${this.escapeHtml(props.titleMeta)}</title>
108
+ <link rel="icon" href="favicon.ico" />
109
+ <meta name="description" content="${this.escapeHtml(props.descriptionMeta)}"/>
110
+ <meta name="keywords" content="${this.escapeHtml(props.keywordsMeta)}"/>
111
+ <meta name="author" content="${this.escapeHtml(props.authorMeta)}"/>
112
+ ${linkStyles.map(item => `<link rel="stylesheet" href="${item.href}" />`).join("")}
113
+ ${scriptScripts.map(item => `<script src="${item.src}"></script>`).join("")}
114
+ ${optionsHead.map(item => `<${item.tag} ${Object.entries(item.attrs).map(([key, value]) => `${key}="${value}"`).join(" ")} />`).join("")}
93
115
  </head>
94
116
  <body class="${classBody}">
95
117
  ${this.react(component, propsReact)}
@@ -99,7 +121,6 @@ class Template {
99
121
  `
100
122
  res.type("text/html");
101
123
  res.status(200);
102
- res.setHeader("Content-Type", "text/html");
103
124
  return res.send(this.minifyHtml(html));
104
125
  }
105
126
 
@@ -109,8 +130,8 @@ class Template {
109
130
  * @param {Object} [componentProps={}] - Props to pass to the component.
110
131
  * @returns {string} The HTML container with data attributes for hydration.
111
132
  */
112
- static react(component, componentProps = {}) {
113
- const id = `react-${Math.random().toString(36).substr(2, 9)}`;
133
+ static react(component, componentProps = {}, divId = "root") {
134
+ const id = divId || `react-${Math.random().toString(36).substr(2, 9)}`;
114
135
  const propsJson = JSON.stringify(componentProps).replace(/'/g, "&apos;");
115
136
  return `<div id="${id}" data-react-component="${component}" data-props='${propsJson}'></div>`;
116
137
  }
@@ -151,10 +172,12 @@ class Template {
151
172
  const file = entry.file;
152
173
  const css = entry.css || [];
153
174
 
154
- let html = `<script type="module" src="/build/${file}"></script>`;
175
+ let html = "";
155
176
  css.forEach(cssFile => {
156
177
  html += `<link rel="stylesheet" href="/build/${cssFile}">`;
157
178
  });
179
+ html += `<script type="module" src="/build/${file}"></script>`;
180
+
158
181
  return html;
159
182
  }
160
183
  } catch (e) {
@@ -165,46 +188,7 @@ class Template {
165
188
  return `<!-- Vite Manifest not found at ${manifestPath} -->`;
166
189
  }
167
190
 
168
- /**
169
- * Renders a full HTML page with a React component as the main entry point.
170
- * Useful for SPAs or full-page React modules.
171
- * @param {import('express').Response} res - The Express response object.
172
- * @param {string} component - The React component name.
173
- * @param {Object} [componentProps={}] - Props to pass to the React component.
174
- * @param {Object} [metaOverrides={}] - Metadata overrides (title, description, keywords, etc.).
175
- */
176
- static reactRender(res, component, componentProps = {}, metaOverrides = {}) {
177
- const meta = {
178
- title: props.titleMeta,
179
- description: props.descriptionMeta,
180
- keywords: props.keywordsMeta,
181
- author: props.authorMeta,
182
- lang: props.langMeta,
183
- ...metaOverrides
184
- };
185
-
186
- const scriptsReact = this.vite_assets();
187
- const componentHtml = this.react(component, componentProps);
188
191
 
189
- const html = `
190
- <!DOCTYPE html>
191
- <html lang="${meta.lang}">
192
- <head>
193
- <meta charset="UTF-8">
194
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
195
- <title>${meta.title}</title>
196
- <meta name="description" content="${meta.description}">
197
- <meta name="keywords" content="${meta.keywords}">
198
- <meta name="author" content="${meta.author}">
199
- ${scriptsReact}
200
- </head>
201
- <body>
202
- ${componentHtml}
203
- </body>
204
- </html>`;
205
-
206
- res.send(this.minifyHtml(html));
207
- }
208
192
 
209
193
  /**
210
194
  * Minifies the HTML output by removing comments and excessive whitespace.
@@ -218,6 +202,19 @@ class Template {
218
202
  .replace(/\s{2,}/g, " ")
219
203
  .trim();
220
204
  }
205
+ /**
206
+ * Escapes HTML special characters in a string to prevent XSS attacks.
207
+ * @param {string} str - The input string.
208
+ * @returns {string} The escaped string.
209
+ */
210
+ static escapeHtml(str = "") {
211
+ return String(str)
212
+ .replace(/&/g, "&amp;")
213
+ .replace(/</g, "&lt;")
214
+ .replace(/>/g, "&gt;")
215
+ .replace(/"/g, "&quot;")
216
+ .replace(/'/g, "&#39;");
217
+ }
221
218
  }
222
219
 
223
220
  export default Template;
package/core/upload.js CHANGED
@@ -14,6 +14,8 @@ class Upload {
14
14
  * Configures storage for uploaded files.
15
15
  * @param {string} folder - The destination folder within the static path.
16
16
  * @returns {import('multer').StorageEngine}
17
+ * @example
18
+ * const storage = Upload.storage("uploads");
17
19
  */
18
20
  static storage(folder = "uploads") {
19
21
  const dest = path.join(__dirname, props.static.path, folder);
@@ -40,6 +42,8 @@ class Upload {
40
42
  * @param {number} [options.fileSize=5000000] - Max file size in bytes (default 5MB).
41
43
  * @param {Array<string>} [options.allowedTypes=[]] - Allowed mime types (e.g. ['image/png', 'image/jpeg']).
42
44
  * @returns {import('multer').Multer}
45
+ * @example
46
+ * const upload = Upload.disk({ folder: "uploads", fileSize: 5000000, allowedTypes: ["image/png", "image/jpeg"] });
43
47
  */
44
48
  static disk(options = {}) {
45
49
  const { folder = "uploads", fileSize = 5000000, allowedTypes = [] } = options;
@@ -61,6 +65,8 @@ class Upload {
61
65
  * @param {string} filename - The name of the file.
62
66
  * @param {string} [folder='uploads'] - The folder where the file is stored.
63
67
  * @returns {string} The full public URL.
68
+ * @example
69
+ * const url = Upload.url("file.jpg", "uploads");
64
70
  */
65
71
  static url(filename, folder = "uploads") {
66
72
  return `${props.host}:${props.port}/public/${folder}/${filename}`;
package/core/validate.js CHANGED
@@ -128,6 +128,13 @@ class Validator {
128
128
  * Validates the request body against the defined schema.
129
129
  * @param {import('express').Request} req - The Express request object containing the body to validate.
130
130
  * @returns {Promise<{success: boolean, error: boolean, errors: Array<{field: string, message: string}>, message: Array<string>, html: Array<string>}>} Validation results.
131
+ * @example
132
+ * const loginSchema = {
133
+ * email: { required: true, email: true },
134
+ * password: { required: true, min: 6 }
135
+ * };
136
+ * const loginValidator = new Validator(loginSchema, 'es');
137
+ * const result = await loginValidator.validate(req);
131
138
  */
132
139
  async validate(req) {
133
140
  const lang = this.lang_default ? this.lang_default : req?.session?.lang || "es";
@@ -260,6 +267,16 @@ class Validator {
260
267
  * Express middleware for automated validation of the request body.
261
268
  * Returns a 400 Bad Request response with validation results if errors occur.
262
269
  * @returns {Function} Express middleware function (req, res, next).
270
+ * @example
271
+ *
272
+ * const loginSchema = {
273
+ * email: { required: true, email: true },
274
+ * password: { required: true, min: 6 }
275
+ * };
276
+ * const loginValidator = new Validator(loginSchema, 'es');
277
+ * routerUsers.post('/login', loginValidator.middleware(), (req, res) => {
278
+ * res.json({ message: 'Login successful' });
279
+ * });
263
280
  */
264
281
  middleware() {
265
282
  return async (req, res, next) => {
Binary file
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seip/blue-bird",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "Express + React opinionated framework with island architecture and built-in JWT auth",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,8 @@
28
28
  "start": "node --env-file=.env backend/index.js",
29
29
  "create-react-app": "node core/cli/react.js",
30
30
  "react": "node core/cli/react.js",
31
+ "component": "node core/cli/component.js",
32
+ "route": "node core/cli/route.js",
31
33
  "init": "node core/cli/init.js"
32
34
  },
33
35
  "dependencies": {
@@ -39,4 +41,4 @@
39
41
  "jsonwebtoken": "^9.0.2",
40
42
  "multer": "^2.0.2"
41
43
  }
42
- }
44
+ }