@seip/blue-bird 0.3.3 → 0.3.5

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.
Files changed (43) hide show
  1. package/.env_example +23 -13
  2. package/LICENSE +21 -21
  3. package/README.md +79 -79
  4. package/backend/index.js +12 -12
  5. package/backend/routes/api.js +34 -34
  6. package/backend/routes/frontend.js +1 -8
  7. package/core/app.js +359 -359
  8. package/core/auth.js +69 -69
  9. package/core/cache.js +35 -35
  10. package/core/cli/component.js +42 -42
  11. package/core/cli/init.js +120 -118
  12. package/core/cli/react.js +383 -411
  13. package/core/cli/route.js +42 -42
  14. package/core/cli/scaffolding-auth.js +967 -0
  15. package/core/config.js +41 -41
  16. package/core/debug.js +248 -248
  17. package/core/logger.js +80 -80
  18. package/core/middleware.js +27 -27
  19. package/core/router.js +134 -134
  20. package/core/swagger.js +24 -24
  21. package/core/template.js +288 -288
  22. package/core/upload.js +76 -76
  23. package/core/validate.js +291 -290
  24. package/frontend/index.html +28 -22
  25. package/frontend/resources/js/App.jsx +28 -42
  26. package/frontend/resources/js/Main.jsx +17 -17
  27. package/frontend/resources/js/blue-bird/components/Button.jsx +67 -0
  28. package/frontend/resources/js/blue-bird/components/Card.jsx +17 -0
  29. package/frontend/resources/js/blue-bird/components/DataTable.jsx +126 -0
  30. package/frontend/resources/js/blue-bird/components/Input.jsx +21 -0
  31. package/frontend/resources/js/blue-bird/components/Label.jsx +12 -0
  32. package/frontend/resources/js/blue-bird/components/Modal.jsx +27 -0
  33. package/frontend/resources/js/blue-bird/components/Translate.jsx +12 -0
  34. package/frontend/resources/js/blue-bird/components/Typography.jsx +25 -0
  35. package/frontend/resources/js/blue-bird/contexts/LanguageContext.jsx +29 -0
  36. package/frontend/resources/js/blue-bird/contexts/SnackbarContext.jsx +38 -0
  37. package/frontend/resources/js/blue-bird/contexts/ThemeContext.jsx +49 -0
  38. package/frontend/resources/js/blue-bird/locales/en.json +30 -0
  39. package/frontend/resources/js/blue-bird/locales/es.json +30 -0
  40. package/frontend/resources/js/pages/About.jsx +33 -15
  41. package/frontend/resources/js/pages/Home.jsx +93 -68
  42. package/package.json +56 -55
  43. package/vite.config.js +21 -21
package/core/template.js CHANGED
@@ -1,288 +1,288 @@
1
- import path from "node:path";
2
- import fs from "node:fs";
3
- import Config from "./config.js";
4
- import Logger from "./logger.js";
5
-
6
- const __dirname = Config.dirname();
7
- const props = Config.props();
8
-
9
- const TEMPLATE_PATH = path.join(__dirname, "frontend", "index.html");
10
- const BASE_TEMPLATE = fs.readFileSync(TEMPLATE_PATH, "utf-8");
11
- let CACHE_TEMPLATE = {};
12
-
13
- /**
14
- * Lightweight HTML template renderer optimized for SPA environments.
15
- */
16
- class Template {
17
-
18
- /**
19
- * Renders the base HTML template for a React application using
20
- * string placeholder replacement and optional in-memory caching.
21
- *
22
- * This method injects:
23
- * - The root React component name
24
- * - Serialized component props
25
- * - SEO meta tags
26
- * - Custom <head> tags
27
- * - Stylesheets
28
- * - Scripts (head and body)
29
- * - Vite assets
30
- *
31
- * It supports basic HTML escaping, optional minification,
32
- * and template caching per component.
33
- *
34
- * @static
35
- * @method renderReact
36
- *
37
- * @param {import('express').Response} res
38
- * Express response object used to send the generated HTML.
39
- *
40
- * @param {string} [component="App"]
41
- * The root React component name to bootstrap on the client.
42
- * This value replaces the `__COMPONENT__` placeholder in the template.
43
- *
44
- * @param {Object<string, any>} [componentProps={}]
45
- * Props passed to the root React component.
46
- * These are serialized and injected into the template
47
- * via the `__PROPS__` placeholder.
48
- *
49
- * @param {Object} [options={}]
50
- * Rendering configuration options.
51
- *
52
- * @param {string} [options.langHtml="en"]
53
- * Value for the `<html lang="">` attribute.
54
- * Falls back to metaTags.langMeta if available.
55
- *
56
- * @param {string} [options.classBody="body"]
57
- * CSS class applied to the `<body>` tag.
58
- *
59
- * @param {Array<{tag:string, attrs:Object<string,string>}>} [options.head=[]]
60
- * Additional custom tags injected into `<head>`.
61
- * Example:
62
- * `{ tag: "meta", attrs: { name: "description", content: "Example" } }`
63
- *
64
- * @param {Array<{href:string}>} [options.linkStyles=[]]
65
- * Stylesheets injected as `<link rel="stylesheet" />` tags.
66
- *
67
- * @param {Array<{src:string}>} [options.scriptsInHead=[]]
68
- * Script files injected inside `<head>`.
69
- *
70
- * @param {Array<{src:string}>} [options.scriptsInBody=[]]
71
- * Script files injected before `</body>`.
72
- *
73
- * @param {boolean} [options.cache=true]
74
- * Enables in-memory caching of the generated HTML
75
- * per component name to improve performance.
76
- *
77
- * @param {Object} [options.metaTags]
78
- * SEO metadata configuration.
79
- *
80
- * @param {string} [options.metaTags.titleMeta]
81
- * Content for the `<title>` tag.
82
- *
83
- * @param {string} [options.metaTags.descriptionMeta]
84
- * Content for `<meta name="description">`.
85
- *
86
- * @param {string} [options.metaTags.keywordsMeta]
87
- * Content for `<meta name="keywords">`.
88
- *
89
- * @param {string} [options.metaTags.authorMeta]
90
- * Content for `<meta name="author">`.
91
- *
92
- * @param {string} [options.metaTags.langMeta]
93
- * Alternative language metadata value.
94
- *
95
- * @returns {void}
96
- * Sends a complete HTML response to the client.
97
- *
98
- * @throws {Error}
99
- * If template rendering fails, a 500 response is returned.
100
- *
101
- * @example
102
- * const options = {
103
- * cache: true,
104
- * classBody: "bg-gray-100",
105
- * head: [
106
- * { tag: "meta", attrs: { name: "robots", content: "index, follow" } }
107
- * ],
108
- * linkStyles: [
109
- * { href: "/css/style.css" }
110
- * ],
111
- * scriptsInHead: [
112
- * { src: "/js/head.js" }
113
- * ],
114
- * scriptsInBody: [
115
- * { src: "/js/body.js" }
116
- * ],
117
- * metaTags: {
118
- * titleMeta: "Example Title",
119
- * descriptionMeta: "Example description",
120
- * keywordsMeta: "express, react, framework",
121
- * authorMeta: "Blue Bird",
122
- * langMeta: "en"
123
- * }
124
- * };
125
- *
126
- * Template.renderReact(res, "App", { title: "Hello World" }, options);
127
- */
128
- static renderReact(res, component = "App", componentProps = {}, options = {}) {
129
- try {
130
- let {
131
- langHtml = options.langHtml || props.langMeta || "en",
132
- classBody = "body",
133
- head = [],
134
- linkStyles = [],
135
- scriptsInHead = [],
136
- scriptsInBody = [],
137
- cache = true,
138
- metaTags
139
- } = options;
140
- const metaTagsDefault = {
141
- titleMeta: props.titleMeta,
142
- descriptionMeta: props.descriptionMeta,
143
- keywordsMeta: props.keywordsMeta,
144
- authorMeta: props.authorMeta,
145
- langMeta: props.langMeta,
146
- }
147
- metaTags = {
148
- ...metaTagsDefault,
149
- ...metaTags
150
- }
151
-
152
- res.type("text/html");
153
- res.status(200);
154
- const cacheKey = `${component}_${metaTags.titleMeta}`;
155
- if (cache && CACHE_TEMPLATE[cacheKey]) {
156
- return res.send(CACHE_TEMPLATE[cacheKey]);
157
- }
158
-
159
- const title = this.escapeHtml(metaTags.titleMeta || "");
160
- const description = this.escapeHtml(metaTags.descriptionMeta || "");
161
- const keywords = this.escapeHtml(metaTags.keywordsMeta || "");
162
- const author = this.escapeHtml(metaTags.authorMeta || "");
163
-
164
- const headOptions = head
165
- .map(item => `<${item.tag} ${Object.entries(item.attrs).map(([k, v]) => `${k}="${v}"`).join(" ")} />`)
166
- .join("");
167
-
168
- const linkTags = linkStyles
169
- .map(item => `<link rel="stylesheet" href="${item.href}" />`)
170
- .join("");
171
-
172
- const scriptsHeadTags = scriptsInHead
173
- .map(item => `<script src="${item.src}"></script>`)
174
- .join("");
175
-
176
- const scriptsBodyTags = scriptsInBody
177
- .map(item => `<script src="${item.src}"></script>`)
178
- .join("");
179
-
180
- const propsJson = JSON.stringify(componentProps).replace(/'/g, "&#39;");
181
-
182
- let html = BASE_TEMPLATE
183
- .replace(/__LANG__/g, this.escapeHtml(langHtml))
184
- .replace(/__TITLE__/g, title)
185
- .replace(/__DESCRIPTION__/g, description)
186
- .replace(/__KEYWORDS__/g, keywords)
187
- .replace(/__AUTHOR__/g, author)
188
- .replace(/__HEAD_OPTIONS__/g, headOptions)
189
- .replace(/__LINK_STYLES__/g, linkTags)
190
- .replace(/__SCRIPTS_HEAD__/g, scriptsHeadTags)
191
- .replace(/__CLASS_BODY__/g, classBody)
192
- .replace(/__COMPONENT__/g, component)
193
- .replace(/__PROPS__/g, propsJson)
194
- .replace(/__VITE_ASSETS__/g, this.vite_assets())
195
- .replace(/__SCRIPTS_BODY__/g, scriptsBodyTags);
196
-
197
- html = this.minifyHtml(html);
198
- CACHE_TEMPLATE[cacheKey] = html;
199
- return res.send(html);
200
-
201
- } catch (error) {
202
- const logger = new Logger();
203
- logger.error(`Template render error: ${error.message}`);
204
-
205
- if (props.debug) {
206
- console.log(error)
207
- return res.status(500).send(`<pre>${error.stack}</pre>`);
208
- }
209
-
210
- return res.status(500).send("Internal Server Error");
211
- }
212
- }
213
-
214
- /**
215
- * Generates Vite asset tags depending on environment.
216
- * @returns {string}
217
- */
218
- static vite_assets() {
219
-
220
- if (props.debug) {
221
- return `
222
- <script type="module">
223
- import RefreshRuntime from "http://localhost:5173/build/@react-refresh";
224
- RefreshRuntime.injectIntoGlobalHook(window);
225
- window.$RefreshReg$ = () => {};
226
- window.$RefreshSig$ = () => (type) => type;
227
- window.__vite_plugin_react_preamble_installed__ = true;
228
- </script>
229
- <script type="module" src="http://localhost:5173/build/@vite/client"></script>
230
- <script type="module" src="http://localhost:5173/build/Main.jsx"></script>`;
231
- }
232
-
233
- const buildPath = path.join(__dirname, props.static.path, "build");
234
- let manifestPath = path.join(buildPath, "manifest.json");
235
-
236
- if (!fs.existsSync(manifestPath)) {
237
- manifestPath = path.join(buildPath, ".vite", "manifest.json");
238
- }
239
-
240
- if (fs.existsSync(manifestPath)) {
241
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
242
- const entry = manifest["Main.jsx"];
243
-
244
- if (entry) {
245
- const file = entry.file;
246
- const css = entry.css || [];
247
-
248
- let html = "";
249
- css.forEach(cssFile => {
250
- html += `<link rel="stylesheet" href="/build/${cssFile}">`;
251
- });
252
- html += `<script type="module" src="/build/${file}"></script>`;
253
- return html;
254
- }
255
- }
256
-
257
- return "";
258
- }
259
-
260
- /**
261
- * Minifies HTML output.
262
- * @param {string} html
263
- * @returns {string}
264
- */
265
- static minifyHtml(html) {
266
- return html
267
- .replace(/<!--(?!\[if).*?-->/gs, "")
268
- .replace(/>\s+</g, "><")
269
- .replace(/\s{2,}/g, " ")
270
- .trim();
271
- }
272
-
273
- /**
274
- * Escapes HTML special characters.
275
- * @param {string} str
276
- * @returns {string}
277
- */
278
- static escapeHtml(str = "") {
279
- return String(str)
280
- .replace(/&/g, "&amp;")
281
- .replace(/</g, "&lt;")
282
- .replace(/>/g, "&gt;")
283
- .replace(/"/g, "&quot;")
284
- .replace(/'/g, "&#39;");
285
- }
286
- }
287
-
288
- export default Template;
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import Config from "./config.js";
4
+ import Logger from "./logger.js";
5
+
6
+ const __dirname = Config.dirname();
7
+ const props = Config.props();
8
+
9
+ const TEMPLATE_PATH = path.join(__dirname, "frontend", "index.html");
10
+ const BASE_TEMPLATE = fs.readFileSync(TEMPLATE_PATH, "utf-8");
11
+ let CACHE_TEMPLATE = {};
12
+
13
+ /**
14
+ * Lightweight HTML template renderer optimized for SPA environments.
15
+ */
16
+ class Template {
17
+
18
+ /**
19
+ * Renders the base HTML template for a React application using
20
+ * string placeholder replacement and optional in-memory caching.
21
+ *
22
+ * This method injects:
23
+ * - The root React component name
24
+ * - Serialized component props
25
+ * - SEO meta tags
26
+ * - Custom <head> tags
27
+ * - Stylesheets
28
+ * - Scripts (head and body)
29
+ * - Vite assets
30
+ *
31
+ * It supports basic HTML escaping, optional minification,
32
+ * and template caching per component.
33
+ *
34
+ * @static
35
+ * @method renderReact
36
+ *
37
+ * @param {import('express').Response} res
38
+ * Express response object used to send the generated HTML.
39
+ *
40
+ * @param {string} [component="App"]
41
+ * The root React component name to bootstrap on the client.
42
+ * This value replaces the `__COMPONENT__` placeholder in the template.
43
+ *
44
+ * @param {Object<string, any>} [componentProps={}]
45
+ * Props passed to the root React component.
46
+ * These are serialized and injected into the template
47
+ * via the `__PROPS__` placeholder.
48
+ *
49
+ * @param {Object} [options={}]
50
+ * Rendering configuration options.
51
+ *
52
+ * @param {string} [options.langHtml="en"]
53
+ * Value for the `<html lang="">` attribute.
54
+ * Falls back to metaTags.langMeta if available.
55
+ *
56
+ * @param {string} [options.classBody="body"]
57
+ * CSS class applied to the `<body>` tag.
58
+ *
59
+ * @param {Array<{tag:string, attrs:Object<string,string>}>} [options.head=[]]
60
+ * Additional custom tags injected into `<head>`.
61
+ * Example:
62
+ * `{ tag: "meta", attrs: { name: "description", content: "Example" } }`
63
+ *
64
+ * @param {Array<{href:string}>} [options.linkStyles=[]]
65
+ * Stylesheets injected as `<link rel="stylesheet" />` tags.
66
+ *
67
+ * @param {Array<{src:string}>} [options.scriptsInHead=[]]
68
+ * Script files injected inside `<head>`.
69
+ *
70
+ * @param {Array<{src:string}>} [options.scriptsInBody=[]]
71
+ * Script files injected before `</body>`.
72
+ *
73
+ * @param {boolean} [options.cache=true]
74
+ * Enables in-memory caching of the generated HTML
75
+ * per component name to improve performance.
76
+ *
77
+ * @param {Object} [options.metaTags]
78
+ * SEO metadata configuration.
79
+ *
80
+ * @param {string} [options.metaTags.titleMeta]
81
+ * Content for the `<title>` tag.
82
+ *
83
+ * @param {string} [options.metaTags.descriptionMeta]
84
+ * Content for `<meta name="description">`.
85
+ *
86
+ * @param {string} [options.metaTags.keywordsMeta]
87
+ * Content for `<meta name="keywords">`.
88
+ *
89
+ * @param {string} [options.metaTags.authorMeta]
90
+ * Content for `<meta name="author">`.
91
+ *
92
+ * @param {string} [options.metaTags.langMeta]
93
+ * Alternative language metadata value.
94
+ *
95
+ * @returns {void}
96
+ * Sends a complete HTML response to the client.
97
+ *
98
+ * @throws {Error}
99
+ * If template rendering fails, a 500 response is returned.
100
+ *
101
+ * @example
102
+ * const options = {
103
+ * cache: true,
104
+ * classBody: "bg-gray-100",
105
+ * head: [
106
+ * { tag: "meta", attrs: { name: "robots", content: "index, follow" } }
107
+ * ],
108
+ * linkStyles: [
109
+ * { href: "/css/style.css" }
110
+ * ],
111
+ * scriptsInHead: [
112
+ * { src: "/js/head.js" }
113
+ * ],
114
+ * scriptsInBody: [
115
+ * { src: "/js/body.js" }
116
+ * ],
117
+ * metaTags: {
118
+ * titleMeta: "Example Title",
119
+ * descriptionMeta: "Example description",
120
+ * keywordsMeta: "express, react, framework",
121
+ * authorMeta: "Blue Bird",
122
+ * langMeta: "en"
123
+ * }
124
+ * };
125
+ *
126
+ * Template.renderReact(res, "App", { title: "Hello World" }, options);
127
+ */
128
+ static renderReact(res, component = "App", componentProps = {}, options = {}) {
129
+ try {
130
+ let {
131
+ langHtml = options.langHtml || props.langMeta || "en",
132
+ classBody = "body",
133
+ head = [],
134
+ linkStyles = [],
135
+ scriptsInHead = [],
136
+ scriptsInBody = [],
137
+ cache = true,
138
+ metaTags
139
+ } = options;
140
+ const metaTagsDefault = {
141
+ titleMeta: props.titleMeta,
142
+ descriptionMeta: props.descriptionMeta,
143
+ keywordsMeta: props.keywordsMeta,
144
+ authorMeta: props.authorMeta,
145
+ langMeta: props.langMeta,
146
+ }
147
+ metaTags = {
148
+ ...metaTagsDefault,
149
+ ...metaTags
150
+ }
151
+
152
+ res.type("text/html");
153
+ res.status(200);
154
+ const cacheKey = `${component}_${metaTags.titleMeta}`;
155
+ if (cache && CACHE_TEMPLATE[cacheKey]) {
156
+ return res.send(CACHE_TEMPLATE[cacheKey]);
157
+ }
158
+
159
+ const title = this.escapeHtml(metaTags.titleMeta || "");
160
+ const description = this.escapeHtml(metaTags.descriptionMeta || "");
161
+ const keywords = this.escapeHtml(metaTags.keywordsMeta || "");
162
+ const author = this.escapeHtml(metaTags.authorMeta || "");
163
+
164
+ const headOptions = head
165
+ .map(item => `<${item.tag} ${Object.entries(item.attrs).map(([k, v]) => `${k}="${v}"`).join(" ")} />`)
166
+ .join("");
167
+
168
+ const linkTags = linkStyles
169
+ .map(item => `<link rel="stylesheet" href="${item.href}" />`)
170
+ .join("");
171
+
172
+ const scriptsHeadTags = scriptsInHead
173
+ .map(item => `<script src="${item.src}"></script>`)
174
+ .join("");
175
+
176
+ const scriptsBodyTags = scriptsInBody
177
+ .map(item => `<script src="${item.src}"></script>`)
178
+ .join("");
179
+
180
+ const propsJson = JSON.stringify(componentProps).replace(/'/g, "&#39;");
181
+
182
+ let html = BASE_TEMPLATE
183
+ .replace(/__LANG__/g, this.escapeHtml(langHtml))
184
+ .replace(/__TITLE__/g, title)
185
+ .replace(/__DESCRIPTION__/g, description)
186
+ .replace(/__KEYWORDS__/g, keywords)
187
+ .replace(/__AUTHOR__/g, author)
188
+ .replace(/__HEAD_OPTIONS__/g, headOptions)
189
+ .replace(/__LINK_STYLES__/g, linkTags)
190
+ .replace(/__SCRIPTS_HEAD__/g, scriptsHeadTags)
191
+ .replace(/__CLASS_BODY__/g, classBody)
192
+ .replace(/__COMPONENT__/g, component)
193
+ .replace(/__PROPS__/g, propsJson)
194
+ .replace(/__VITE_ASSETS__/g, this.vite_assets())
195
+ .replace(/__SCRIPTS_BODY__/g, scriptsBodyTags);
196
+
197
+ html = this.minifyHtml(html);
198
+ CACHE_TEMPLATE[cacheKey] = html;
199
+ return res.send(html);
200
+
201
+ } catch (error) {
202
+ const logger = new Logger();
203
+ logger.error(`Template render error: ${error.message}`);
204
+
205
+ if (props.debug) {
206
+ console.log(error)
207
+ return res.status(500).send(`<pre>${error.stack}</pre>`);
208
+ }
209
+
210
+ return res.status(500).send("Internal Server Error");
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Generates Vite asset tags depending on environment.
216
+ * @returns {string}
217
+ */
218
+ static vite_assets() {
219
+
220
+ if (props.debug) {
221
+ return `
222
+ <script type="module">
223
+ import RefreshRuntime from "http://localhost:5173/build/@react-refresh";
224
+ RefreshRuntime.injectIntoGlobalHook(window);
225
+ window.$RefreshReg$ = () => {};
226
+ window.$RefreshSig$ = () => (type) => type;
227
+ window.__vite_plugin_react_preamble_installed__ = true;
228
+ </script>
229
+ <script type="module" src="http://localhost:5173/build/@vite/client"></script>
230
+ <script type="module" src="http://localhost:5173/build/Main.jsx"></script>`;
231
+ }
232
+
233
+ const buildPath = path.join(__dirname, props.static.path, "build");
234
+ let manifestPath = path.join(buildPath, "manifest.json");
235
+
236
+ if (!fs.existsSync(manifestPath)) {
237
+ manifestPath = path.join(buildPath, ".vite", "manifest.json");
238
+ }
239
+
240
+ if (fs.existsSync(manifestPath)) {
241
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
242
+ const entry = manifest["Main.jsx"];
243
+
244
+ if (entry) {
245
+ const file = entry.file;
246
+ const css = entry.css || [];
247
+
248
+ let html = "";
249
+ css.forEach(cssFile => {
250
+ html += `<link rel="stylesheet" href="/build/${cssFile}">`;
251
+ });
252
+ html += `<script type="module" src="/build/${file}"></script>`;
253
+ return html;
254
+ }
255
+ }
256
+
257
+ return "";
258
+ }
259
+
260
+ /**
261
+ * Minifies HTML output.
262
+ * @param {string} html
263
+ * @returns {string}
264
+ */
265
+ static minifyHtml(html) {
266
+ return html
267
+ .replace(/<!--(?!\[if).*?-->/gs, "")
268
+ .replace(/>\s+</g, "><")
269
+ .replace(/\s{2,}/g, " ")
270
+ .trim();
271
+ }
272
+
273
+ /**
274
+ * Escapes HTML special characters.
275
+ * @param {string} str
276
+ * @returns {string}
277
+ */
278
+ static escapeHtml(str = "") {
279
+ return String(str)
280
+ .replace(/&/g, "&amp;")
281
+ .replace(/</g, "&lt;")
282
+ .replace(/>/g, "&gt;")
283
+ .replace(/"/g, "&quot;")
284
+ .replace(/'/g, "&#39;");
285
+ }
286
+ }
287
+
288
+ export default Template;