@seip/blue-bird 0.6.4 → 0.7.1

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/cli/init.js CHANGED
@@ -3,6 +3,10 @@
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import chalk from "chalk";
6
+ import readline from "node:readline/promises";
7
+ import { stdin as input, stdout as output } from "node:process";
8
+ import crypto from "node:crypto";
9
+ import { execSync } from "node:child_process";
6
10
 
7
11
  /**
8
12
  * Initializes a new Blue Bird project by copying the base structure.
@@ -19,13 +23,63 @@ class ProjectInit {
19
23
  async run() {
20
24
  console.log(chalk.cyan("Starting Blue Bird project initialization..."));
21
25
 
26
+ const rl = readline.createInterface({ input, output });
27
+
28
+ let title = "Blue-Bird";
29
+ let port = 3000;
30
+ let appUrl = "http://localhost:3000";
31
+ let useMysql = false;
32
+ let dbName = "blue_bird";
33
+ let dbUser = "root";
34
+ let dbPassword = "root";
35
+ let dbPort = 3306;
36
+
37
+ try {
38
+ const ask = async (query, defaultValue) => {
39
+ const formattedQuery = defaultValue !== undefined ? `${query} [${defaultValue}]: ` : `${query}: `;
40
+ const answer = await rl.question(formattedQuery);
41
+ return answer.trim() || defaultValue;
42
+ };
43
+
44
+ title = await ask("Project Title", title);
45
+ const portInput = await ask("Server Port", port);
46
+ port = parseInt(portInput, 10);
47
+ if (Number.isNaN(port)) {
48
+ port = 3000;
49
+ }
50
+
51
+ const defaultAppUrl = `http://localhost:${port}`;
52
+ appUrl = await ask("Application URL", defaultAppUrl);
53
+
54
+ const mysqlAns = await ask("Do you want to configure MySQL? (y/n)", "n");
55
+ useMysql = mysqlAns.toLowerCase() === "y" || mysqlAns.toLowerCase() === "yes";
56
+
57
+ if (useMysql) {
58
+ dbName = await ask("Database Name", dbName);
59
+ dbUser = await ask("Database User", dbUser);
60
+ dbPassword = await ask("Database Password", dbPassword);
61
+ const dbPortInput = await ask("Database Port", dbPort);
62
+ dbPort = parseInt(dbPortInput, 10);
63
+ if (Number.isNaN(dbPort)) {
64
+ dbPort = 3306;
65
+ }
66
+ }
67
+ } catch (error) {
68
+ console.error(chalk.red("[ERROR] Error reading configuration input:"), error.message);
69
+ rl.close();
70
+ return;
71
+ } finally {
72
+ rl.close();
73
+ }
74
+
22
75
  const itemsToCopy = [
23
76
  "backend",
24
77
  "frontend",
25
78
  "docker",
26
79
  "docker-compose.yml",
27
80
  ".env_example",
28
- "AGENTS.md"
81
+ "AGENTS.md",
82
+ "index.js"
29
83
  ];
30
84
 
31
85
  try {
@@ -36,31 +90,78 @@ class ProjectInit {
36
90
  if (fs.existsSync(src)) {
37
91
  if (!fs.existsSync(dest)) {
38
92
  this.copyRecursive(src, dest);
39
- console.log(chalk.green(`✓ Copied ${item} to root.`));
93
+ console.log(chalk.green(`[OK] Copied ${item} to root.`));
40
94
  } else {
41
- console.log(chalk.yellow(`! ${item} already exists, skipping.`));
95
+ console.log(chalk.yellow(`[SKIP] ${item} already exists, skipping.`));
42
96
  }
43
97
  } else {
44
- console.warn(chalk.red(`✗ Source ${item} not found in ${this.sourceDir}`));
98
+ console.warn(chalk.red(`[ERROR] Source ${item} not found in ${this.sourceDir}`));
45
99
  }
46
100
  });
47
101
 
48
102
  const envPath = path.join(this.appDir, ".env");
49
103
  const envExamplePath = path.join(this.appDir, ".env_example");
50
- if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
51
- fs.copyFileSync(envExamplePath, envPath);
52
- console.log(chalk.green("✓ Created .env from .env_example."));
104
+
105
+ if (fs.existsSync(envExamplePath)) {
106
+ let envContent = fs.readFileSync(envExamplePath, "utf-8");
107
+
108
+ const jwtSecret = crypto.randomBytes(32).toString("hex");
109
+
110
+ const updates = {
111
+ TITLE: title,
112
+ PORT: port,
113
+ APP_URL: appUrl,
114
+ JWT_SECRET: jwtSecret,
115
+ };
116
+
117
+ if (useMysql) {
118
+ updates.DB_NAME = dbName;
119
+ updates.DB_USER = dbUser;
120
+ updates.DB_PASSWORD = dbPassword;
121
+ updates.DB_PORT = dbPort;
122
+ updates.DATABASE_URL = `mysql://${dbUser}:${dbPassword}@localhost:${dbPort}/${dbName}`;
123
+ }
124
+
125
+ const lines = envContent.split(/\r?\n/);
126
+ const updatedLines = lines.map(line => {
127
+ const match = line.match(/^([A-Z_]+)=(.+)/);
128
+ if (match) {
129
+ const key = match[1];
130
+ if (updates[key] !== undefined) {
131
+ const value = updates[key];
132
+ if (typeof value === "string" && !value.startsWith('"')) {
133
+ return `${key}="${value}"`;
134
+ }
135
+ return `${key}=${value}`;
136
+ }
137
+ }
138
+ return line;
139
+ });
140
+ envContent = updatedLines.join("\n");
141
+
142
+ fs.writeFileSync(envPath, envContent, "utf-8");
143
+ console.log(chalk.green("[OK] Created and configured .env file."));
53
144
  }
54
145
 
55
146
  this.updatePackageJson();
56
147
 
148
+ if (useMysql) {
149
+ console.log(chalk.cyan("[INFO] Installing mysql2 and redis packages..."));
150
+ try {
151
+ execSync("npm install mysql2 redis", { stdio: "inherit", cwd: this.appDir });
152
+ console.log(chalk.green("[OK] Successfully installed mysql2 and redis."));
153
+ } catch (error) {
154
+ console.warn(chalk.yellow("[ERROR] Automatic package installation failed. Please run 'npm install mysql2 redis' manually."));
155
+ }
156
+ }
157
+
57
158
  console.log(chalk.blue("\nBlue Bird initialization completed!"));
58
159
  console.log(chalk.white("Next steps:"));
59
160
  console.log(chalk.bold(" npm install"));
60
161
  console.log(chalk.bold(" npm run dev"));
61
162
 
62
163
  } catch (error) {
63
- console.error(chalk.red("Error during initialization:"), error.message);
164
+ console.error(chalk.red("[ERROR] Error during initialization:"), error.message);
64
165
  }
65
166
  }
66
167
 
@@ -74,8 +175,11 @@ class ProjectInit {
74
175
  pkg.scripts = pkg.scripts || {};
75
176
 
76
177
  const scriptsToAdd = {
77
- "dev": "node --watch --env-file=.env backend/index.js",
78
- "start": "node --env-file=.env backend/index.js",
178
+ "dev": "node --watch --env-file=.env index.js",
179
+ "dev:astro": "astro dev --root frontend",
180
+ "dev:api": "node --watch --env-file=.env index.js",
181
+ "start": "node --env-file=.env index.js",
182
+ "build": "astro build --root frontend",
79
183
  "init": "blue-bird",
80
184
  "route": "blue-bird route",
81
185
  "swagger-install": "blue-bird swagger-install",
@@ -90,9 +194,14 @@ class ProjectInit {
90
194
  }
91
195
  }
92
196
 
197
+ if (pkg.type !== "module") {
198
+ pkg.type = "module";
199
+ updated = true;
200
+ }
201
+
93
202
  if (updated) {
94
203
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
95
- console.log(chalk.green(" Updated package.json scripts."));
204
+ console.log(chalk.green("[OK] Updated package.json configuration."));
96
205
  }
97
206
  }
98
207
  }
package/core/logger.js CHANGED
@@ -1,100 +1,99 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import Config from "./config.js"
3
+ import Config from "./config.js";
4
4
 
5
- const __dirname = Config.dirname()
5
+ const __dirname = Config.dirname();
6
6
 
7
7
  /**
8
8
  * Logger class for managing application logs by creating dated folders and log files.
9
9
  */
10
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
- }
11
+ /**
12
+ * Initializes the Logger instance and ensures the logs directory exists.
13
+ */
14
+ constructor() {
15
+ this.folder = path.join(__dirname, "backend", "logs");
16
+ this._currentDay = null;
17
+ this._currentDayFolder = null;
18
+ if (!fs.existsSync(this.folder)) {
19
+ fs.mkdirSync(this.folder, { recursive: true });
22
20
  }
21
+ }
23
22
 
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
- }
23
+ /**
24
+ * Ensures and returns the path to the log folder for the current day.
25
+ * Caches the folder path for the current day to avoid repeated fs checks.
26
+ * @returns {string} The absolute path to the current day's log folder.
27
+ */
28
+ nowFolder() {
29
+ const today = this.now();
35
30
 
36
- const folder = path.join(this.folder, today);
31
+ if (this._currentDay === today && this._currentDayFolder) {
32
+ return this._currentDayFolder;
33
+ }
37
34
 
38
- if (!fs.existsSync(folder)) {
39
- fs.mkdirSync(folder, { recursive: true });
40
- }
35
+ const folder = path.join(this.folder, today);
41
36
 
42
- this._currentDay = today;
43
- this._currentDayFolder = folder;
44
- return folder;
37
+ if (!fs.existsSync(folder)) {
38
+ fs.mkdirSync(folder, { recursive: true });
45
39
  }
46
40
 
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
- }
41
+ this._currentDay = today;
42
+ this._currentDayFolder = folder;
43
+ return folder;
44
+ }
54
45
 
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
- }
46
+ /**
47
+ * Gets the current date formatted as YYYY-MM-DD.
48
+ * @returns {string} The formatted date string.
49
+ */
50
+ now() {
51
+ return new Date().toISOString().split("T")[0];
52
+ }
65
53
 
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
- }
54
+ /**
55
+ * Appends an informational message to the info.log file (non-blocking).
56
+ * @param {string} message - The message to log.
57
+ */
58
+ info(message) {
59
+ const logFile = path.join(this.nowFolder(), "info.log");
60
+ fs.appendFile(logFile, `${message}\n`, (err) => {
61
+ if (err) console.error("Logger write error:", err.message);
62
+ });
63
+ }
76
64
 
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
- }
65
+ /**
66
+ * Appends an error message to the error.log file (non-blocking).
67
+ * @param {string} message - The error message to log.
68
+ */
69
+ error(message) {
70
+ const logFile = path.join(this.nowFolder(), "error.log");
71
+ fs.appendFile(logFile, `${message}\n`, (err) => {
72
+ if (err) console.error("Logger write error:", err.message);
73
+ });
74
+ }
87
75
 
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
- }
76
+ /**
77
+ * Appends a warning message to the warn.log file (non-blocking).
78
+ * @param {string} message - The warning message to log.
79
+ */
80
+ warning(message) {
81
+ const logFile = path.join(this.nowFolder(), "warn.log");
82
+ fs.appendFile(logFile, `${message}\n`, (err) => {
83
+ if (err) console.error("Logger write error:", err.message);
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Appends a debug message to the debug.log file (non-blocking).
89
+ * @param {string} message - The debug message to log.
90
+ */
91
+ debug(message) {
92
+ const logFile = path.join(this.nowFolder(), "debug.log");
93
+ fs.appendFile(logFile, `${message}\n`, (err) => {
94
+ if (err) console.error("Logger write error:", err.message);
95
+ });
96
+ }
98
97
  }
99
98
 
100
99
  export default Logger;
package/core/router.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import express from "express";
2
2
  import Config from "./config.js";
3
- import SEO from "./seo.js";
3
+
4
4
 
5
5
  const props = Config.props();
6
6
 
@@ -57,11 +57,7 @@ class Router {
57
57
  if (path === "/*" || path === "*") {
58
58
  path = /.*/;
59
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
- }
60
+
65
61
  this.router.get(path, callback);
66
62
  }
67
63
 
package/docker/Dockerfile CHANGED
@@ -1,25 +1,16 @@
1
- # ─────────────────────────────────────────────────────────────────────────────
2
- # Blue Bird Framework — Production Dockerfile
3
- # Node.js 24-alpine for lightweight, production-ready environment
4
- # ─────────────────────────────────────────────────────────────────────────────
5
1
  FROM node:24.7.0-alpine3.21
6
2
 
7
- # Set environment variables
8
3
  ENV NODE_ENV=production
9
4
 
10
5
  WORKDIR /app
11
6
 
12
- # Copy package files first (layer cache optimization)
13
7
  COPY package*.json ./
14
8
 
15
- # Install production dependencies
16
- RUN npm ci --omit=dev
9
+ RUN npm ci --omit=dev && npm install -g pm2
17
10
 
18
- # Copy project source
19
11
  COPY . .
20
12
 
21
- # Expose the application port
22
13
  EXPOSE 3000
23
14
 
24
- # Start the application
25
- CMD ["npm", "start"]
15
+ CMD ["sh", "-c", "pm2-runtime start index.js -i ${PM2_INSTANCES:-1}"]
16
+
@@ -0,0 +1,83 @@
1
+ user nginx;
2
+ worker_processes auto;
3
+
4
+ error_log /var/log/nginx/error.log notice;
5
+ pid /var/run/nginx.pid;
6
+
7
+ events {
8
+ worker_connections 1024;
9
+ }
10
+
11
+ http {
12
+ include /etc/nginx/mime.types;
13
+ default_type application/octet-stream;
14
+
15
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
16
+ '$status $body_bytes_sent "$http_referer" '
17
+ '"$http_user_agent" "$http_x_forwarded_for"';
18
+
19
+ access_log /var/log/nginx/access.log main;
20
+
21
+ sendfile on;
22
+ tcp_nopush on;
23
+ tcp_nodelay on;
24
+ keepalive_timeout 65;
25
+ types_hash_max_size 2048;
26
+
27
+ gzip on;
28
+ gzip_disable "msie6";
29
+ gzip_vary on;
30
+ gzip_proxied any;
31
+ gzip_comp_level 6;
32
+ gzip_buffers 16 8k;
33
+ gzip_http_version 1.1;
34
+ gzip_min_length 256;
35
+ gzip_types
36
+ text/plain
37
+ text/css
38
+ application/json
39
+ application/javascript
40
+ application/x-javascript
41
+ text/xml
42
+ application/xml
43
+ application/xml+rss
44
+ text/javascript
45
+ image/svg+xml;
46
+
47
+ limit_req_zone $binary_remote_addr zone=bluebird_limit:10m rate=10r/s;
48
+
49
+ server {
50
+ listen 80;
51
+ server_name localhost;
52
+ root /app/frontend/dist/client;
53
+
54
+ resolver 127.0.0.11 valid=5s;
55
+
56
+ location ~* /(\.env|\.git|wp-content|wp-admin|xmlrpc\.php) {
57
+ return 444;
58
+ }
59
+
60
+ location / {
61
+ try_files $uri @node_app;
62
+ }
63
+
64
+ location /_astro/ {
65
+ expires max;
66
+ add_header Cache-Control "public, max-age=31536000, immutable";
67
+ try_files $uri =404;
68
+ }
69
+
70
+ location @node_app {
71
+ limit_req zone=bluebird_limit burst=20 nodelay;
72
+
73
+ set $upstream_target http://app:3000;
74
+ proxy_pass $upstream_target;
75
+ proxy_http_version 1.1;
76
+ proxy_set_header Connection "";
77
+ proxy_set_header Host $host;
78
+ proxy_set_header X-Real-IP $remote_addr;
79
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
80
+ proxy_set_header X-Forwarded-Proto $scheme;
81
+ }
82
+ }
83
+ }
@@ -1,28 +1,12 @@
1
- # ─────────────────────────────────────────────────────────────────────────────
2
- # Blue Bird Framework — Docker Compose
3
- #
4
- # DEVELOPMENT / DATABASE ONLY:
5
- # npx blue-bird docker start mysql → Only MySQL container
6
- #
7
- # PRODUCTION:
8
- # npx blue-bird docker start prod → MySQL + Node.js app container
9
- # npx blue-bird docker stop → Stop all containers
10
- #
11
- # Multi-project VPS support:
12
- # Each project uses TITLE (or sanitised TITLE) to name
13
- # containers and networks uniquely.
14
- # ─────────────────────────────────────────────────────────────────────────────
15
-
16
1
  services:
17
- # ── Node.js App (Production only) ─────────────────────────────────────────
18
2
  app:
19
3
  build:
20
4
  context: .
21
5
  dockerfile: docker/Dockerfile
22
6
  container_name: ${TITLE:-bluebird}-app
23
7
  restart: unless-stopped
24
- ports:
25
- - "${PORT:-3000}:${PORT:-3000}"
8
+ expose:
9
+ - "3000"
26
10
  volumes:
27
11
  - .:/app
28
12
  - /app/node_modules
@@ -32,15 +16,36 @@ services:
32
16
  - NODE_ENV=production
33
17
  - DEBUG=false
34
18
  - DATABASE_URL=mysql://root:${DB_PASSWORD:-root}@mysql:3306/${DB_NAME:-blue_bird}
19
+ - REDIS_HOST=redis
20
+ - REDIS_PORT=6379
21
+ - PORT=3000
35
22
  depends_on:
36
23
  mysql:
37
24
  condition: service_healthy
25
+ redis:
26
+ condition: service_healthy
27
+ networks:
28
+ - bluebird_net
29
+ profiles:
30
+ - prod
31
+
32
+ nginx:
33
+ image: nginx:1.27-alpine
34
+ container_name: ${TITLE:-bluebird}-nginx
35
+ restart: unless-stopped
36
+ ports:
37
+ - "${PORT:-3000}:80"
38
+ volumes:
39
+ - .:/app:ro
40
+ - ./docker/nginx.conf:/etc/nginx/nginx.conf:ro
41
+ depends_on:
42
+ app:
43
+ condition: service_started
38
44
  networks:
39
45
  - bluebird_net
40
46
  profiles:
41
47
  - prod
42
48
 
43
- # ── MySQL Database ─────────────────────────────────────────────────────────
44
49
  mysql:
45
50
  image: mysql:8.0
46
51
  container_name: ${TITLE:-bluebird}-mysql
@@ -61,10 +66,28 @@ services:
61
66
  networks:
62
67
  - bluebird_net
63
68
 
69
+ redis:
70
+ image: redis:7-alpine
71
+ container_name: ${TITLE:-bluebird}-redis
72
+ restart: unless-stopped
73
+ ports:
74
+ - "${REDIS_PORT:-6379}:6379"
75
+ volumes:
76
+ - redis_data:/data
77
+ networks:
78
+ - bluebird_net
79
+ healthcheck:
80
+ test: ["CMD", "redis-cli", "ping"]
81
+ interval: 5s
82
+ timeout: 3s
83
+ retries: 5
84
+
64
85
  volumes:
65
86
  mysql_data:
87
+ redis_data:
66
88
 
67
89
  networks:
68
90
  bluebird_net:
69
91
  name: ${TITLE:-bluebird}_network
70
92
  driver: bridge
93
+
@@ -0,0 +1,35 @@
1
+ import { defineConfig } from 'astro/config';
2
+ import node from '@astrojs/node';
3
+ import { loadEnv } from 'vite';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, resolve } from 'path';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ const env = loadEnv(
10
+ process.env.NODE_ENV ?? 'development',
11
+ resolve(__dirname, '..'),
12
+ '',
13
+ );
14
+
15
+ const apiPort = env.PORT || '3000';
16
+ const apiHost = (env.HOST || 'localhost').replace(/^["']|["']$/g, '');
17
+ const apiTarget = `http://${apiHost}:${apiPort}`;
18
+
19
+ export default defineConfig({
20
+ output: 'server',
21
+ adapter: node({
22
+ mode: 'middleware',
23
+ }),
24
+ vite: {
25
+ envDir: resolve(__dirname, '..'),
26
+ server: {
27
+ proxy: {
28
+ '/api': {
29
+ target: apiTarget,
30
+ changeOrigin: true,
31
+ },
32
+ },
33
+ },
34
+ },
35
+ });