@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/.env_example +9 -10
- package/AGENTS.md +54 -158
- package/README.md +63 -135
- package/backend/routes/api.js +21 -17
- package/core/app.js +86 -81
- package/core/cache.js +117 -29
- package/core/cli/docker.js +47 -8
- package/core/cli/init.js +120 -11
- package/core/logger.js +77 -78
- package/core/router.js +2 -6
- package/docker/Dockerfile +3 -12
- package/docker/nginx.conf +83 -0
- package/docker-compose.yml +42 -19
- package/frontend/astro.config.mjs +35 -0
- package/frontend/public/css/app.css +319 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/http/api.js +24 -0
- package/frontend/src/layouts/Layout.astro +20 -0
- package/frontend/src/pages/about.astro +54 -0
- package/frontend/src/pages/index.astro +110 -0
- package/{backend/index.js → index.js} +11 -4
- package/package.json +16 -4
- package/backend/routes/frontend.js +0 -39
- package/core/seo.js +0 -113
- package/core/template.js +0 -319
- package/frontend/public/js/blue-bird.js +0 -1465
- package/frontend/public/js/tailwind.js +0 -8
- package/frontend/templates/about.html +0 -105
- package/frontend/templates/index.html +0 -146
- package/frontend/templates/preact_example.html +0 -80
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(
|
|
93
|
+
console.log(chalk.green(`[OK] Copied ${item} to root.`));
|
|
40
94
|
} else {
|
|
41
|
-
console.log(chalk.yellow(
|
|
95
|
+
console.log(chalk.yellow(`[SKIP] ${item} already exists, skipping.`));
|
|
42
96
|
}
|
|
43
97
|
} else {
|
|
44
|
-
console.warn(chalk.red(
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
78
|
-
"
|
|
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("
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
31
|
+
if (this._currentDay === today && this._currentDayFolder) {
|
|
32
|
+
return this._currentDayFolder;
|
|
33
|
+
}
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
fs.mkdirSync(folder, { recursive: true });
|
|
40
|
-
}
|
|
35
|
+
const folder = path.join(this.folder, today);
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return folder;
|
|
37
|
+
if (!fs.existsSync(folder)) {
|
|
38
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
45
39
|
}
|
|
46
40
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
+
}
|
package/docker-compose.yml
CHANGED
|
@@ -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
|
-
|
|
25
|
-
- "
|
|
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
|
+
});
|