mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-09 08:11:10 +08:00
【调整】申请证书配置CA选项增加liteSSL证书
This commit is contained in:
@@ -1,35 +0,0 @@
|
||||
# SPA Preview Server Configuration Template
|
||||
# 复制此文件为 .env 并根据需要修改配置
|
||||
|
||||
# 服务器配置
|
||||
# Server configuration
|
||||
PORT=5173
|
||||
HOST=0.0.0.0
|
||||
|
||||
# API 代理配置
|
||||
# API proxy configuration
|
||||
API_TARGET=http://localhost:3000
|
||||
API_PREFIX=/api
|
||||
# API 代理模式: 'prefix'(路径替换) 或 'include'(路径包含)
|
||||
# API proxy mode: 'prefix' (path replacement) or 'include' (path inclusion)
|
||||
API_PROXY_MODE=prefix
|
||||
|
||||
# 静态文件配置
|
||||
# Static files configuration
|
||||
PUBLIC_DIR=public
|
||||
FALLBACK_FILE=index.html
|
||||
|
||||
# 开发模式配置
|
||||
# Development mode configuration
|
||||
DEV_MODE=true
|
||||
LOG_LEVEL=info
|
||||
|
||||
# CORS 配置
|
||||
# CORS configuration
|
||||
CORS_ENABLED=true
|
||||
CORS_ORIGIN=*
|
||||
|
||||
# SPA 路由配置
|
||||
# SPA routing configuration
|
||||
SPA_FALLBACK_ENABLED=true
|
||||
SPA_EXCLUDE_EXTENSIONS=.js,.css,.png,.jpg,.jpeg,.gif,.svg,.ico,.woff,.woff2,.ttf,.eot
|
||||
33
frontend/apps/node-spa-preview/.gitignore
vendored
33
frontend/apps/node-spa-preview/.gitignore
vendored
@@ -1,33 +0,0 @@
|
||||
node_modules
|
||||
public/*
|
||||
!.gitkeep
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
@@ -1,287 +0,0 @@
|
||||
# SPA Preview Server
|
||||
|
||||
基于 Express 的单页应用(SPA)预览服务器,支持环境变量配置、API 反向代理和增强的路由处理。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🔧 **环境变量配置** - 通过 `.env` 文件管理所有配置
|
||||
- 📁 **静态文件服务** - 可配置的静态文件目录
|
||||
- 🔄 **API 反向代理** - 支持 API 请求代理到后端服务
|
||||
- 🛣️ **SPA 路由支持** - 智能的客户端路由回退机制
|
||||
- 🌐 **CORS 支持** - 可配置的跨域资源共享
|
||||
- 📝 **增强日志** - 多级别日志记录和彩色输出
|
||||
- ⚡ **缓存优化** - 生产环境静态资源缓存
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
#### 方法一:使用配置向导(推荐)
|
||||
|
||||
```bash
|
||||
# 运行交互式配置向导
|
||||
pnpm config
|
||||
|
||||
# 或者
|
||||
node src/utils/cli-config.js
|
||||
```
|
||||
|
||||
#### 方法二:手动配置
|
||||
|
||||
复制 `.env.example` 文件为 `.env` 并根据需要修改配置:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 3. 启动服务器
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
pnpm dev
|
||||
|
||||
# 或直接运行
|
||||
node src/server.js
|
||||
```
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
### 服务器配置
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `PORT` | `5173` | 服务器端口 |
|
||||
| `HOST` | `0.0.0.0` | 服务器主机地址 |
|
||||
|
||||
### 静态文件配置
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `PUBLIC_DIR` | `public` | 静态文件目录 |
|
||||
| `FALLBACK_FILE` | `index.html` | SPA 回退文件 |
|
||||
|
||||
### API 代理配置
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `API_TARGET` | `` | API 代理目标地址(如:`http://localhost:3000`) |
|
||||
| `API_PREFIX` | `/api` | API 路由前缀 |
|
||||
| `API_PROXY_MODE` | `prefix` | API 代理模式:<br>`prefix` - 路径替换模式,移除前缀后转发<br>`include` - 路径包含模式,保留完整路径转发 |
|
||||
|
||||
### 开发配置
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `DEV_MODE` | `true` | 开发模式开关 |
|
||||
| `LOG_LEVEL` | `info` | 日志级别(`debug`, `info`, `warn`, `error`) |
|
||||
|
||||
### CORS 配置
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `CORS_ENABLED` | `true` | 启用 CORS |
|
||||
| `CORS_ORIGIN` | `*` | 允许的源(多个用逗号分隔) |
|
||||
|
||||
### SPA 路由配置
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `SPA_FALLBACK_ENABLED` | `true` | 启用 SPA 路由回退 |
|
||||
| `SPA_EXCLUDE_EXTENSIONS` | `.js,.css,.png,...` | 排除的文件扩展名 |
|
||||
|
||||
## 配置工具
|
||||
|
||||
### 交互式配置向导
|
||||
|
||||
项目提供了一个交互式配置向导,帮助您轻松管理环境变量:
|
||||
|
||||
```bash
|
||||
# 启动配置向导
|
||||
pnpm config
|
||||
```
|
||||
|
||||
配置向导功能:
|
||||
- 🔍 **自动检测现有配置** - 读取并显示当前 `.env` 文件
|
||||
- ✅ **配置验证** - 实时验证配置值的有效性
|
||||
- 📋 **配置摘要** - 清晰显示所有配置项
|
||||
- 💾 **自动保存** - 生成 `.env` 和 `.env.example` 文件
|
||||
- 🎨 **彩色输出** - 友好的命令行界面
|
||||
|
||||
### 配置验证
|
||||
|
||||
配置工具会自动验证以下项目:
|
||||
- 端口号范围(1-65535)
|
||||
- 主机地址格式
|
||||
- API 目标 URL 格式
|
||||
- 日志级别有效性
|
||||
- CORS 源地址格式
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本 SPA 部署
|
||||
|
||||
1. 将构建好的 SPA 文件放入 `public` 目录
|
||||
2. 启动服务器
|
||||
3. 访问 `http://localhost:5173`
|
||||
|
||||
### 配置 API 代理
|
||||
|
||||
在 `.env` 文件中配置:
|
||||
|
||||
```env
|
||||
API_TARGET=http://localhost:3000
|
||||
API_PREFIX=/api
|
||||
```
|
||||
|
||||
这样,所有 `/api/*` 的请求都会被代理到 `http://localhost:3000/*`。
|
||||
|
||||
### 自定义 CORS
|
||||
|
||||
```env
|
||||
CORS_ORIGIN=http://localhost:3000,https://example.com
|
||||
```
|
||||
|
||||
### 调试模式
|
||||
|
||||
```env
|
||||
LOG_LEVEL=debug
|
||||
DEV_MODE=true
|
||||
```
|
||||
|
||||
## SPA 路由处理
|
||||
|
||||
服务器会智能处理 SPA 路由:
|
||||
|
||||
1. **静态文件请求** - 直接提供文件服务
|
||||
2. **API 请求** - 代理到配置的后端服务
|
||||
3. **SPA 路由** - 返回 `index.html` 让客户端路由处理
|
||||
|
||||
### 路由排除规则
|
||||
|
||||
以下类型的请求不会触发 SPA 回退:
|
||||
|
||||
- 带有文件扩展名的请求(如 `.js`, `.css`, `.png` 等)
|
||||
- API 路由请求(以 `API_PREFIX` 开头)
|
||||
|
||||
### 自定义排除扩展名
|
||||
|
||||
```env
|
||||
SPA_EXCLUDE_EXTENSIONS=.js,.css,.png,.jpg,.svg,.ico,.woff,.woff2
|
||||
```
|
||||
|
||||
## 生产环境部署
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```env
|
||||
DEV_MODE=false
|
||||
LOG_LEVEL=warn
|
||||
CORS_ORIGIN=https://yourdomain.com
|
||||
```
|
||||
|
||||
### 使用 PM2 部署
|
||||
|
||||
```bash
|
||||
# 安装 PM2
|
||||
npm install -g pm2
|
||||
|
||||
# 启动应用
|
||||
pm2 start src/server.js --name spa-preview
|
||||
|
||||
# 查看状态
|
||||
pm2 status
|
||||
|
||||
# 查看日志
|
||||
pm2 logs spa-preview
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
创建 `Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
CMD ["node", "src/server.js"]
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **端口被占用**
|
||||
```bash
|
||||
# 查看端口占用
|
||||
lsof -i :5173
|
||||
|
||||
# 修改端口
|
||||
echo "PORT=5174" >> .env
|
||||
```
|
||||
|
||||
2. **API 代理不工作**
|
||||
- 检查 `API_TARGET` 是否正确配置
|
||||
- 确认后端服务正在运行
|
||||
- 查看调试日志:`LOG_LEVEL=debug`
|
||||
|
||||
3. **SPA 路由 404**
|
||||
- 确认 `SPA_FALLBACK_ENABLED=true`
|
||||
- 检查 `public/index.html` 是否存在
|
||||
- 查看排除扩展名配置
|
||||
|
||||
### 调试技巧
|
||||
|
||||
启用详细日志:
|
||||
|
||||
```env
|
||||
LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
这将显示:
|
||||
- 代理请求详情
|
||||
- SPA 回退处理
|
||||
- 文件服务信息
|
||||
|
||||
## 开发
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
apps/spa-preview/
|
||||
├── .env # 环境变量配置
|
||||
├── .env.example # 配置模板
|
||||
├── .gitignore # Git 忽略文件
|
||||
├── package.json # 项目配置
|
||||
├── README.md # 项目文档
|
||||
├── public/ # 静态文件目录
|
||||
│ └── .gitkeep
|
||||
└── src/
|
||||
├── server.js # 主服务器文件
|
||||
└── utils/ # 工具函数
|
||||
```
|
||||
|
||||
### 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支
|
||||
3. 提交更改
|
||||
4. 推送到分支
|
||||
5. 创建 Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "@baota/spa-preview",
|
||||
"version": "1.0.0",
|
||||
"description": "基于 Express 的 SPA 预览与 API 反向代理服务器",
|
||||
"type": "module",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"dev": "node src/server.js",
|
||||
"start": "node src/server.js",
|
||||
"config": "node src/utils/cli-config.js",
|
||||
"config:wizard": "node src/utils/cli-config.js",
|
||||
"test:config": "node src/utils/test-config.js",
|
||||
"build": "echo 'No build step required for server'",
|
||||
"lint": "echo 'No linting configured'"
|
||||
},
|
||||
"keywords": [
|
||||
"spa",
|
||||
"preview",
|
||||
"proxy",
|
||||
"static-server"
|
||||
],
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.19.2",
|
||||
"http-proxy-middleware": "^2.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
/**
|
||||
* 增强的 Express 服务器,用于 SPA 预览和可配置的反向代理支持
|
||||
*
|
||||
* 功能特性:
|
||||
* - 基于环境变量的配置
|
||||
* - 从可配置目录提供静态资源服务
|
||||
* - 增强的单页应用 (SPA) 回退机制,支持路由排除
|
||||
* - 可配置的 API 请求反向代理
|
||||
* - 支持可配置源的 CORS
|
||||
* - 改进的日志记录和错误处理
|
||||
*
|
||||
* 使用方法:
|
||||
* - 通过 .env 文件或环境变量进行配置
|
||||
* - node src/server.js
|
||||
* - 或使用 package.json 脚本: pnpm dev
|
||||
*/
|
||||
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import { createProxyMiddleware } from "http-proxy-middleware";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import fs from "node:fs";
|
||||
import dotenv from "dotenv";
|
||||
import chalk from "chalk";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// 从 .env 文件加载环境变量
|
||||
dotenv.config({ path: path.resolve(__dirname, "..", ".env") });
|
||||
|
||||
/**
|
||||
* 解析相对于项目根目录的路径
|
||||
* @param {...string} segs - 路径片段
|
||||
* @returns {string} 解析后的路径
|
||||
*/
|
||||
function r(...segs) {
|
||||
return path.resolve(__dirname, "..", ...segs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置对象,包含环境变量默认值
|
||||
*/
|
||||
const config = {
|
||||
// 服务器配置
|
||||
port: Number(process.env.PORT) || 5173,
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
|
||||
// 静态文件配置
|
||||
publicDir: process.env.PUBLIC_DIR || "public",
|
||||
fallbackFile: process.env.FALLBACK_FILE || "index.html",
|
||||
|
||||
// API 代理配置
|
||||
apiTarget: process.env.API_TARGET || "",
|
||||
apiPrefix: process.env.API_PREFIX || "/api",
|
||||
apiProxyMode: process.env.API_PROXY_MODE || "prefix", // 'prefix' 或 'include'
|
||||
|
||||
// 开发配置
|
||||
devMode:
|
||||
process.env.DEV_MODE === "true" || process.env.NODE_ENV === "development",
|
||||
logLevel: process.env.LOG_LEVEL || "info",
|
||||
|
||||
// CORS 配置
|
||||
corsEnabled: process.env.CORS_ENABLED !== "false",
|
||||
corsOrigin: process.env.CORS_ORIGIN || "*",
|
||||
|
||||
// SPA 路由配置
|
||||
spaFallbackEnabled: process.env.SPA_FALLBACK_ENABLED !== "false",
|
||||
spaExcludeExtensions: (
|
||||
process.env.SPA_EXCLUDE_EXTENSIONS ||
|
||||
".js,.css,.png,.jpg,.jpeg,.gif,.svg,.ico,.woff,.woff2,.ttf,.eot"
|
||||
)
|
||||
.split(",")
|
||||
.map((ext) => ext.trim()),
|
||||
};
|
||||
|
||||
/**
|
||||
* 不同级别的日志工具
|
||||
*/
|
||||
const logger = {
|
||||
info: (message, ...args) => {
|
||||
if (["info", "debug"].includes(config.logLevel)) {
|
||||
console.log(chalk.blue("[INFO]"), message, ...args);
|
||||
}
|
||||
},
|
||||
warn: (message, ...args) => {
|
||||
if (["info", "warn", "debug"].includes(config.logLevel)) {
|
||||
console.warn(chalk.yellow("[WARN]"), message, ...args);
|
||||
}
|
||||
},
|
||||
error: (message, ...args) => {
|
||||
console.error(chalk.red("[ERROR]"), message, ...args);
|
||||
},
|
||||
debug: (message, ...args) => {
|
||||
if (config.logLevel === "debug") {
|
||||
console.log(chalk.gray("[DEBUG]"), message, ...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
// 如果配置了 CORS 则启用
|
||||
if (config.corsEnabled) {
|
||||
const corsOptions = {
|
||||
origin:
|
||||
config.corsOrigin === "*"
|
||||
? true
|
||||
: config.corsOrigin.split(",").map((o) => o.trim()),
|
||||
credentials: true,
|
||||
};
|
||||
app.use(cors(corsOptions));
|
||||
logger.debug("CORS enabled with origin:", config.corsOrigin);
|
||||
}
|
||||
|
||||
// 静态文件目录设置
|
||||
const publicDir = r(config.publicDir);
|
||||
const fallbackIndex = r(config.publicDir, config.fallbackFile);
|
||||
|
||||
/**
|
||||
* 确保 public 文件夹存在,如果缺少则创建基本的 index.html
|
||||
*/
|
||||
function ensurePublicDirectory() {
|
||||
if (!fs.existsSync(publicDir)) {
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
logger.info("Created public directory:", publicDir);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(fallbackIndex)) {
|
||||
const defaultContent = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>SPA Preview Server</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.container { background: #1e293b; padding: 2rem; border-radius: 8px; }
|
||||
.status { color: #10b981; }
|
||||
.config { background: #374151; padding: 1rem; border-radius: 4px; margin: 1rem 0; }
|
||||
code { background: #4b5563; padding: 0.2rem 0.4rem; border-radius: 3px; }
|
||||
a { color: #93c5fd; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 SPA Preview Server</h1>
|
||||
<p class="status">✅ Server is running successfully!</p>
|
||||
|
||||
<h2>Configuration</h2>
|
||||
<div class="config">
|
||||
<p><strong>Server:</strong> http://${config.host}:${config.port}</p>
|
||||
<p><strong>Public Directory:</strong> <code>${config.publicDir}</code></p>
|
||||
<p><strong>API Proxy:</strong> ${
|
||||
config.apiTarget
|
||||
? `<code>${config.apiPrefix}</code> → <code>${config.apiTarget}</code>`
|
||||
: "Disabled"
|
||||
}</p>
|
||||
<p><strong>SPA Fallback:</strong> ${
|
||||
config.spaFallbackEnabled ? "Enabled" : "Disabled"
|
||||
}</p>
|
||||
</div>
|
||||
|
||||
<h2>Getting Started</h2>
|
||||
<ol>
|
||||
<li>Place your built SPA files into <code>apps/spa-preview/${
|
||||
config.publicDir
|
||||
}</code></li>
|
||||
<li>Configure API proxy in <code>.env</code> file if needed</li>
|
||||
<li>Your SPA routes will be handled automatically</li>
|
||||
</ol>
|
||||
|
||||
<p><a href="https://github.com/your-repo" target="_blank">📖 Documentation</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(fallbackIndex, defaultContent);
|
||||
logger.info("Created default index.html:", fallbackIndex);
|
||||
}
|
||||
}
|
||||
|
||||
ensurePublicDirectory();
|
||||
|
||||
// 提供静态资源服务
|
||||
app.use(
|
||||
express.static(publicDir, {
|
||||
index: false,
|
||||
fallthrough: true,
|
||||
setHeaders: (res, path) => {
|
||||
// 在生产环境中为静态资源添加缓存头
|
||||
if (!config.devMode) {
|
||||
if (
|
||||
path.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/)
|
||||
) {
|
||||
res.setHeader("Cache-Control", "public, max-age=31536000"); // 1 year
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// 为 API 请求设置反向代理
|
||||
if (config.apiTarget) {
|
||||
const proxyOptions = {
|
||||
target: config.apiTarget,
|
||||
changeOrigin: true,
|
||||
pathRewrite: (pathStr) => {
|
||||
let rewritten = pathStr;
|
||||
|
||||
// 根据代理模式处理路径
|
||||
if (config.apiProxyMode === "prefix") {
|
||||
// 路径替换模式:移除前缀
|
||||
rewritten = pathStr.replace(new RegExp(`^${config.apiPrefix}`), "");
|
||||
}
|
||||
// 路径包含模式不修改路径
|
||||
|
||||
logger.debug(`Proxy rewrite: ${pathStr} → ${rewritten}`);
|
||||
return rewritten;
|
||||
},
|
||||
logLevel: config.logLevel === "debug" ? "debug" : "warn",
|
||||
onProxyReq: (proxyReq, req) => {
|
||||
logger.debug(`Proxying ${req.method} ${req.url} to ${config.apiTarget}`);
|
||||
},
|
||||
onError: (err, req, res) => {
|
||||
logger.error("Proxy error:", err.message);
|
||||
res.status(500).json({ error: "Proxy error", message: err.message });
|
||||
},
|
||||
};
|
||||
|
||||
// 根据代理模式设置中间件
|
||||
if (config.apiProxyMode === "prefix") {
|
||||
// 路径替换模式:只代理特定前缀的请求
|
||||
app.use(config.apiPrefix, createProxyMiddleware(proxyOptions));
|
||||
logger.info(
|
||||
`API proxy enabled (prefix mode): ${config.apiPrefix} → ${config.apiTarget}`
|
||||
);
|
||||
} else if (config.apiProxyMode === "include") {
|
||||
// 路径包含模式:代理所有包含特定路径的请求
|
||||
const pathFilter = (path) => path.includes(config.apiPrefix);
|
||||
app.use(pathFilter, createProxyMiddleware(proxyOptions));
|
||||
logger.info(
|
||||
`API proxy enabled (include mode): paths containing '${config.apiPrefix}' → ${config.apiTarget}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.info("API proxy disabled (set API_TARGET to enable)");
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强的 SPA 回退中间件
|
||||
* 通过为非文件请求提供 index.html 来处理客户端路由
|
||||
*/
|
||||
if (config.spaFallbackEnabled) {
|
||||
app.get("*", (req, res, next) => {
|
||||
const requestPath = req.path;
|
||||
const fileExtension = path.extname(requestPath);
|
||||
|
||||
// 如果请求有应该从 SPA 回退中排除的扩展名,则跳过
|
||||
if (fileExtension && config.spaExcludeExtensions.includes(fileExtension)) {
|
||||
logger.debug(`Skipping SPA fallback for file: ${requestPath}`);
|
||||
return next();
|
||||
}
|
||||
|
||||
// 跳过 API 路由
|
||||
if (
|
||||
config.apiProxyMode === "prefix" &&
|
||||
requestPath.startsWith(config.apiPrefix)
|
||||
) {
|
||||
return next();
|
||||
} else if (
|
||||
config.apiProxyMode === "include" &&
|
||||
requestPath.includes(config.apiPrefix)
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
logger.debug(`SPA fallback serving index.html for: ${requestPath}`);
|
||||
res.sendFile(fallbackIndex, (err) => {
|
||||
if (err) {
|
||||
logger.error("Error serving fallback index.html:", err.message);
|
||||
res.status(500).send("Internal Server Error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
logger.info("SPA fallback enabled");
|
||||
}
|
||||
|
||||
// 错误处理中间件
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error("Unhandled error:", err.message);
|
||||
res.status(500).json({ error: "Internal Server Error" });
|
||||
});
|
||||
|
||||
// 404 处理器
|
||||
app.use((req, res) => {
|
||||
logger.warn(`404 Not Found: ${req.method} ${req.url}`);
|
||||
res.status(404).json({ error: "Not Found", path: req.url });
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
app.listen(config.port, config.host, () => {
|
||||
console.log(chalk.green("🚀 SPA Preview Server Started"));
|
||||
console.log(chalk.cyan(`📍 Server: http://${config.host}:${config.port}`));
|
||||
console.log(chalk.cyan(`📁 Public: ${publicDir}`));
|
||||
|
||||
if (config.apiTarget) {
|
||||
console.log(
|
||||
chalk.cyan(`🔄 Proxy: ${config.apiPrefix} → ${config.apiTarget}`)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.devMode) {
|
||||
console.log(chalk.yellow("🔧 Development mode enabled"));
|
||||
}
|
||||
|
||||
console.log(chalk.gray("Press Ctrl+C to stop"));
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on("SIGTERM", () => {
|
||||
logger.info("Received SIGTERM, shutting down gracefully");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
logger.info("Received SIGINT, shutting down gracefully");
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -1,264 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* SPA 预览服务器的命令行配置工具
|
||||
* 用于交互式管理环境变量
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import readline from 'node:readline';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
parseConfig,
|
||||
validateConfig,
|
||||
generateEnvFile,
|
||||
readEnvFile,
|
||||
getConfigSummary
|
||||
} from './config.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* 创建 readline 接口
|
||||
*/
|
||||
function createInterface() {
|
||||
return readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 询问用户输入
|
||||
* @param {string} question - 问题
|
||||
* @param {string} defaultValue - 默认值
|
||||
* @returns {Promise<string>} 用户输入
|
||||
*/
|
||||
function askQuestion(question, defaultValue = '') {
|
||||
return new Promise((resolve) => {
|
||||
const rl = createInterface();
|
||||
const prompt = defaultValue
|
||||
? `${question} ${chalk.gray(`(default: ${defaultValue})`)}: `
|
||||
: `${question}: `;
|
||||
|
||||
rl.question(prompt, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim() || defaultValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 询问是/否问题
|
||||
* @param {string} question - 问题
|
||||
* @param {boolean} defaultValue - 默认值
|
||||
* @returns {Promise<boolean>} 用户选择
|
||||
*/
|
||||
function askYesNo(question, defaultValue = true) {
|
||||
return new Promise((resolve) => {
|
||||
const rl = createInterface();
|
||||
const defaultText = defaultValue ? 'Y/n' : 'y/N';
|
||||
const prompt = `${question} ${chalk.gray(`(${defaultText})`)}: `;
|
||||
|
||||
rl.question(prompt, (answer) => {
|
||||
rl.close();
|
||||
const normalized = answer.trim().toLowerCase();
|
||||
if (normalized === '') {
|
||||
resolve(defaultValue);
|
||||
} else {
|
||||
resolve(normalized === 'y' || normalized === 'yes');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示配置摘要
|
||||
* @param {object} config - 配置对象
|
||||
*/
|
||||
function displayConfigSummary(config) {
|
||||
const summary = getConfigSummary(config);
|
||||
|
||||
console.log(chalk.cyan('\n📋 配置摘要:'));
|
||||
console.log(chalk.cyan('─'.repeat(50)));
|
||||
|
||||
console.log(chalk.yellow('🖥️ Server:'));
|
||||
console.log(` URL: ${chalk.green(summary.server.url)}`);
|
||||
console.log(` Public Directory: ${chalk.green(summary.server.publicDir)}`);
|
||||
console.log(` Development Mode: ${summary.server.devMode ? chalk.green('Enabled') : chalk.red('Disabled')}`);
|
||||
|
||||
console.log(chalk.yellow('\n🔄 API Proxy:'));
|
||||
if (summary.proxy.enabled) {
|
||||
console.log(` Status: ${chalk.green('Enabled')}`);
|
||||
console.log(` Target: ${chalk.green(summary.proxy.target)}`);
|
||||
console.log(` Prefix: ${chalk.green(summary.proxy.prefix)}`);
|
||||
} else {
|
||||
console.log(` Status: ${chalk.red('Disabled')}`);
|
||||
}
|
||||
|
||||
console.log(chalk.yellow('\n🛣️ SPA Routing:'));
|
||||
console.log(` Fallback: ${summary.spa.fallbackEnabled ? chalk.green('Enabled') : chalk.red('Disabled')}`);
|
||||
console.log(` Excluded Extensions: ${chalk.green(summary.spa.excludeExtensions)} types`);
|
||||
|
||||
console.log(chalk.yellow('\n🌐 CORS:'));
|
||||
console.log(` Status: ${summary.cors.enabled ? chalk.green('Enabled') : chalk.red('Disabled')}`);
|
||||
console.log(` Origin: ${chalk.green(summary.cors.origin)}`);
|
||||
|
||||
console.log(chalk.cyan('─'.repeat(50)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 交互式配置向导
|
||||
* @returns {Promise<object>} 配置对象
|
||||
*/
|
||||
async function configWizard() {
|
||||
console.log(chalk.blue('🚀 SPA 预览服务器配置向导'));
|
||||
console.log(chalk.gray('按 Enter 键使用默认值\n'));
|
||||
|
||||
const config = {};
|
||||
|
||||
// 服务器配置
|
||||
console.log(chalk.yellow('📡 服务器配置:'));
|
||||
config.PORT = await askQuestion('端口', DEFAULT_CONFIG.PORT.toString());
|
||||
config.HOST = await askQuestion('主机地址', DEFAULT_CONFIG.HOST);
|
||||
|
||||
// 静态文件配置
|
||||
console.log(chalk.yellow('\n📁 静态文件配置:'));
|
||||
config.PUBLIC_DIR = await askQuestion('公共目录', DEFAULT_CONFIG.PUBLIC_DIR);
|
||||
config.FALLBACK_FILE = await askQuestion('回退文件', DEFAULT_CONFIG.FALLBACK_FILE);
|
||||
|
||||
// API 代理配置
|
||||
console.log(chalk.yellow('\n🔄 API 代理配置:'));
|
||||
const enableProxy = await askYesNo('启用 API 代理?', false);
|
||||
if (enableProxy) {
|
||||
config.API_TARGET = await askQuestion('API 目标 URL (例如: http://localhost:3000)');
|
||||
config.API_PREFIX = await askQuestion('API 前缀', DEFAULT_CONFIG.API_PREFIX);
|
||||
} else {
|
||||
config.API_TARGET = '';
|
||||
config.API_PREFIX = DEFAULT_CONFIG.API_PREFIX;
|
||||
}
|
||||
|
||||
// 开发配置
|
||||
console.log(chalk.yellow('\n🔧 开发配置:'));
|
||||
config.DEV_MODE = await askYesNo('启用开发模式?', DEFAULT_CONFIG.DEV_MODE);
|
||||
const logLevels = ['debug', 'info', 'warn', 'error'];
|
||||
console.log(`可用的日志级别: ${logLevels.join(', ')}`);
|
||||
config.LOG_LEVEL = await askQuestion('日志级别', DEFAULT_CONFIG.LOG_LEVEL);
|
||||
|
||||
// CORS 配置
|
||||
console.log(chalk.yellow('\n🌐 CORS 配置:'));
|
||||
config.CORS_ENABLED = await askYesNo('启用 CORS?', DEFAULT_CONFIG.CORS_ENABLED);
|
||||
if (config.CORS_ENABLED) {
|
||||
config.CORS_ORIGIN = await askQuestion('CORS 源地址 (* 表示所有,或逗号分隔的 URL)', DEFAULT_CONFIG.CORS_ORIGIN);
|
||||
} else {
|
||||
config.CORS_ORIGIN = DEFAULT_CONFIG.CORS_ORIGIN;
|
||||
}
|
||||
|
||||
// SPA 路由配置
|
||||
console.log(chalk.yellow('\n🛣️ SPA 路由配置:'));
|
||||
config.SPA_FALLBACK_ENABLED = await askYesNo('启用 SPA 回退?', DEFAULT_CONFIG.SPA_FALLBACK_ENABLED);
|
||||
if (config.SPA_FALLBACK_ENABLED) {
|
||||
config.SPA_EXCLUDE_EXTENSIONS = await askQuestion(
|
||||
'排除的文件扩展名 (逗号分隔)',
|
||||
DEFAULT_CONFIG.SPA_EXCLUDE_EXTENSIONS
|
||||
);
|
||||
} else {
|
||||
config.SPA_EXCLUDE_EXTENSIONS = DEFAULT_CONFIG.SPA_EXCLUDE_EXTENSIONS;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main() {
|
||||
const envPath = path.resolve(process.cwd(), '.env');
|
||||
const envExamplePath = path.resolve(process.cwd(), '.env.example');
|
||||
|
||||
try {
|
||||
console.log(chalk.blue('🔧 SPA 预览服务器配置工具\n'));
|
||||
|
||||
// 检查 .env 文件是否存在
|
||||
const envExists = fs.existsSync(envPath);
|
||||
if (envExists) {
|
||||
console.log(chalk.green('✅ 发现现有的 .env 文件'));
|
||||
const useExisting = await askYesNo('加载现有配置?', true);
|
||||
|
||||
if (useExisting) {
|
||||
const existingEnv = readEnvFile(envPath);
|
||||
const existingConfig = parseConfig(existingEnv);
|
||||
|
||||
console.log(chalk.blue('\n📖 当前配置:'));
|
||||
displayConfigSummary(existingConfig);
|
||||
|
||||
const modify = await askYesNo('\n修改配置?', false);
|
||||
if (!modify) {
|
||||
console.log(chalk.green('\n✅ 配置未更改'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 运行配置向导
|
||||
const config = await configWizard();
|
||||
|
||||
// 验证配置
|
||||
const validation = validateConfig(config);
|
||||
if (!validation.isValid) {
|
||||
console.log(chalk.red('\n❌ 配置验证失败:'));
|
||||
validation.errors.forEach(error => {
|
||||
console.log(chalk.red(` • ${error}`));
|
||||
});
|
||||
|
||||
const continueAnyway = await askYesNo('\n仍然保存配置?', false);
|
||||
if (!continueAnyway) {
|
||||
console.log(chalk.yellow('\n⚠️ 配置已取消'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示摘要
|
||||
displayConfigSummary(config);
|
||||
|
||||
// 确认保存
|
||||
const confirmSave = await askYesNo('\n保存此配置?', true);
|
||||
if (!confirmSave) {
|
||||
console.log(chalk.yellow('\n⚠️ 配置已取消'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const envContent = generateEnvFile(config);
|
||||
fs.writeFileSync(envPath, envContent, 'utf8');
|
||||
|
||||
console.log(chalk.green(`\n✅ 配置已保存到 ${envPath}`));
|
||||
|
||||
// 如果不存在则创建 .env.example
|
||||
if (!fs.existsSync(envExamplePath)) {
|
||||
const exampleConfig = { ...DEFAULT_CONFIG };
|
||||
const exampleContent = generateEnvFile(exampleConfig);
|
||||
fs.writeFileSync(envExamplePath, exampleContent, 'utf8');
|
||||
console.log(chalk.green(`✅ 示例配置已保存到 ${envExamplePath}`));
|
||||
}
|
||||
|
||||
console.log(chalk.blue('\n🚀 现在可以使用以下命令启动服务器:'));
|
||||
console.log(chalk.cyan(' node src/server.js'));
|
||||
console.log(chalk.cyan(' # 或者'));
|
||||
console.log(chalk.cyan(' pnpm dev'));
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red('\n❌ 错误:'), error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接调用则运行
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
export { main as configWizard };
|
||||
@@ -1,246 +0,0 @@
|
||||
/**
|
||||
* SPA 预览服务器的配置工具
|
||||
* 用于管理和验证环境变量配置
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* 默认配置值
|
||||
*/
|
||||
export const DEFAULT_CONFIG = {
|
||||
// 服务器配置
|
||||
PORT: 5173,
|
||||
HOST: '0.0.0.0',
|
||||
|
||||
// 静态文件配置
|
||||
PUBLIC_DIR: 'public',
|
||||
FALLBACK_FILE: 'index.html',
|
||||
|
||||
// API 代理配置
|
||||
API_TARGET: '',
|
||||
API_PREFIX: '/api',
|
||||
API_PROXY_MODE: 'prefix', // 'prefix' 或 'include'
|
||||
|
||||
// 开发配置
|
||||
DEV_MODE: true,
|
||||
LOG_LEVEL: 'info',
|
||||
|
||||
// CORS 配置
|
||||
CORS_ENABLED: true,
|
||||
CORS_ORIGIN: '*',
|
||||
|
||||
// SPA 路由配置
|
||||
SPA_FALLBACK_ENABLED: true,
|
||||
SPA_EXCLUDE_EXTENSIONS: '.js,.css,.png,.jpg,.jpeg,.gif,.svg,.ico,.woff,.woff2,.ttf,.eot'
|
||||
};
|
||||
|
||||
/**
|
||||
* 配置验证规则
|
||||
*/
|
||||
export const CONFIG_VALIDATORS = {
|
||||
PORT: (value) => {
|
||||
const port = Number(value);
|
||||
return port > 0 && port <= 65535 ? null : '端口必须在 1 到 65535 之间';
|
||||
},
|
||||
|
||||
HOST: (value) => {
|
||||
const validHosts = ['0.0.0.0', 'localhost', '127.0.0.1'];
|
||||
const isValidIP = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value);
|
||||
return validHosts.includes(value) || isValidIP ? null : '无效的主机地址';
|
||||
},
|
||||
|
||||
API_TARGET: (value) => {
|
||||
if (!value) return null; // 可选项
|
||||
const urlPattern = /^https?:\/\/.+/;
|
||||
return urlPattern.test(value) ? null : 'API_TARGET 必须是有效的 HTTP/HTTPS URL';
|
||||
},
|
||||
|
||||
API_PROXY_MODE: (value) => {
|
||||
const validModes = ['prefix', 'include'];
|
||||
return validModes.includes(value) ? null : `API_PROXY_MODE 必须是以下值之一: ${validModes.join(', ')}`;
|
||||
},
|
||||
|
||||
LOG_LEVEL: (value) => {
|
||||
const validLevels = ['debug', 'info', 'warn', 'error'];
|
||||
return validLevels.includes(value) ? null : `LOG_LEVEL 必须是以下值之一: ${validLevels.join(', ')}`;
|
||||
},
|
||||
|
||||
CORS_ORIGIN: (value) => {
|
||||
if (value === '*') return null;
|
||||
const origins = value.split(',').map(o => o.trim());
|
||||
const urlPattern = /^https?:\/\/.+/;
|
||||
const invalidOrigins = origins.filter(origin => !urlPattern.test(origin));
|
||||
return invalidOrigins.length === 0 ? null : `无效的 CORS 源: ${invalidOrigins.join(', ')}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析环境变量配置
|
||||
* @param {object} env - 环境变量对象
|
||||
* @returns {object} 解析后的配置对象
|
||||
*/
|
||||
export function parseConfig(env = process.env) {
|
||||
const config = {};
|
||||
|
||||
// 解析每个配置值
|
||||
Object.keys(DEFAULT_CONFIG).forEach(key => {
|
||||
const envValue = env[key];
|
||||
const defaultValue = DEFAULT_CONFIG[key];
|
||||
|
||||
if (envValue !== undefined) {
|
||||
// 解析布尔值
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
config[key] = envValue === 'true';
|
||||
}
|
||||
// 解析数字值
|
||||
else if (typeof defaultValue === 'number') {
|
||||
config[key] = Number(envValue) || defaultValue;
|
||||
}
|
||||
// 字符串值
|
||||
else {
|
||||
config[key] = envValue;
|
||||
}
|
||||
} else {
|
||||
config[key] = defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
* @param {object} config - 配置对象
|
||||
* @returns {object} 验证结果 { isValid: boolean, errors: string[] }
|
||||
*/
|
||||
export function validateConfig(config) {
|
||||
const errors = [];
|
||||
|
||||
Object.keys(CONFIG_VALIDATORS).forEach(key => {
|
||||
const validator = CONFIG_VALIDATORS[key];
|
||||
const value = config[key];
|
||||
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
const error = validator(value);
|
||||
if (error) {
|
||||
errors.push(`${key}: ${error}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 .env 文件内容
|
||||
* @param {object} config - 配置对象
|
||||
* @returns {string} .env 文件内容
|
||||
*/
|
||||
export function generateEnvFile(config) {
|
||||
// 与默认值合并以确保所有值都存在
|
||||
const fullConfig = { ...DEFAULT_CONFIG, ...config };
|
||||
|
||||
const lines = [
|
||||
'# SPA 预览服务器配置',
|
||||
'# 生成时间: ' + new Date().toISOString(),
|
||||
'',
|
||||
'# 服务器配置',
|
||||
`PORT=${fullConfig.PORT}`,
|
||||
`HOST=${fullConfig.HOST}`,
|
||||
'',
|
||||
'# 静态文件配置',
|
||||
`PUBLIC_DIR=${fullConfig.PUBLIC_DIR}`,
|
||||
`FALLBACK_FILE=${fullConfig.FALLBACK_FILE}`,
|
||||
'',
|
||||
'# API 代理配置',
|
||||
`API_TARGET=${fullConfig.API_TARGET}`,
|
||||
`API_PREFIX=${fullConfig.API_PREFIX}`,
|
||||
'',
|
||||
'# 开发配置',
|
||||
`DEV_MODE=${fullConfig.DEV_MODE}`,
|
||||
`LOG_LEVEL=${fullConfig.LOG_LEVEL}`,
|
||||
'',
|
||||
'# CORS 配置',
|
||||
`CORS_ENABLED=${fullConfig.CORS_ENABLED}`,
|
||||
`CORS_ORIGIN=${fullConfig.CORS_ORIGIN}`,
|
||||
'',
|
||||
'# SPA 路由配置',
|
||||
`SPA_FALLBACK_ENABLED=${fullConfig.SPA_FALLBACK_ENABLED}`,
|
||||
`SPA_EXCLUDE_EXTENSIONS=${fullConfig.SPA_EXCLUDE_EXTENSIONS}`
|
||||
];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 .env 文件
|
||||
* @param {string} envPath - .env 文件路径
|
||||
* @returns {object} 环境变量对象
|
||||
*/
|
||||
export function readEnvFile(envPath) {
|
||||
try {
|
||||
const content = fs.readFileSync(envPath, 'utf8');
|
||||
const env = {};
|
||||
|
||||
content.split('\n').forEach(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
env[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return env;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入 .env 文件
|
||||
* @param {string} envPath - .env 文件路径
|
||||
* @param {object} config - 配置对象
|
||||
*/
|
||||
export function writeEnvFile(envPath, config) {
|
||||
const content = generateEnvFile(config);
|
||||
fs.writeFileSync(envPath, content, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置摘要信息
|
||||
* @param {object} config - 配置对象
|
||||
* @returns {object} 配置摘要
|
||||
*/
|
||||
export function getConfigSummary(config) {
|
||||
return {
|
||||
server: {
|
||||
url: `http://${config.HOST}:${config.PORT}`,
|
||||
publicDir: config.PUBLIC_DIR,
|
||||
devMode: config.DEV_MODE
|
||||
},
|
||||
proxy: {
|
||||
enabled: !!config.API_TARGET,
|
||||
target: config.API_TARGET,
|
||||
prefix: config.API_PREFIX
|
||||
},
|
||||
spa: {
|
||||
fallbackEnabled: config.SPA_FALLBACK_ENABLED,
|
||||
excludeExtensions: config.SPA_EXCLUDE_EXTENSIONS.split(',').length
|
||||
},
|
||||
cors: {
|
||||
enabled: config.CORS_ENABLED,
|
||||
origin: config.CORS_ORIGIN
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
/**
|
||||
* 配置系统测试脚本
|
||||
* 用于验证环境变量配置功能
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
parseConfig,
|
||||
validateConfig,
|
||||
generateEnvFile,
|
||||
readEnvFile,
|
||||
getConfigSummary
|
||||
} from './config.js';
|
||||
|
||||
/**
|
||||
* 测试配置解析功能
|
||||
*/
|
||||
function testConfigParsing() {
|
||||
console.log(chalk.blue('🧪 测试配置解析功能...'));
|
||||
|
||||
const testEnv = {
|
||||
PORT: '8080',
|
||||
HOST: '127.0.0.1',
|
||||
API_TARGET: 'http://localhost:4000',
|
||||
DEV_MODE: 'false',
|
||||
CORS_ENABLED: 'true',
|
||||
LOG_LEVEL: 'debug'
|
||||
};
|
||||
|
||||
const config = parseConfig(testEnv);
|
||||
|
||||
console.log('✅ 解析的配置:', {
|
||||
port: config.PORT,
|
||||
host: config.HOST,
|
||||
apiTarget: config.API_TARGET,
|
||||
devMode: config.DEV_MODE,
|
||||
corsEnabled: config.CORS_ENABLED,
|
||||
logLevel: config.LOG_LEVEL
|
||||
});
|
||||
|
||||
// 验证类型
|
||||
const typeChecks = [
|
||||
{ key: 'PORT', expected: 'number', actual: typeof config.PORT },
|
||||
{ key: 'DEV_MODE', expected: 'boolean', actual: typeof config.DEV_MODE },
|
||||
{ key: 'CORS_ENABLED', expected: 'boolean', actual: typeof config.CORS_ENABLED }
|
||||
];
|
||||
|
||||
typeChecks.forEach(check => {
|
||||
if (check.expected === check.actual) {
|
||||
console.log(chalk.green(`✅ ${check.key}: ${check.actual}`));
|
||||
} else {
|
||||
console.log(chalk.red(`❌ ${check.key}: expected ${check.expected}, got ${check.actual}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试配置验证功能
|
||||
*/
|
||||
function testConfigValidation() {
|
||||
console.log(chalk.blue('\n🧪 测试配置验证功能...'));
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: '有效配置',
|
||||
config: {
|
||||
PORT: 3000,
|
||||
HOST: '0.0.0.0',
|
||||
API_TARGET: 'http://localhost:4000',
|
||||
LOG_LEVEL: 'info',
|
||||
CORS_ORIGIN: '*'
|
||||
},
|
||||
shouldPass: true
|
||||
},
|
||||
{
|
||||
name: '无效端口',
|
||||
config: {
|
||||
PORT: 99999,
|
||||
HOST: '0.0.0.0'
|
||||
},
|
||||
shouldPass: false
|
||||
},
|
||||
{
|
||||
name: '无效 API 目标',
|
||||
config: {
|
||||
API_TARGET: 'not-a-url'
|
||||
},
|
||||
shouldPass: false
|
||||
},
|
||||
{
|
||||
name: '无效日志级别',
|
||||
config: {
|
||||
LOG_LEVEL: 'invalid'
|
||||
},
|
||||
shouldPass: false
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const validation = validateConfig(testCase.config);
|
||||
const passed = validation.isValid === testCase.shouldPass;
|
||||
|
||||
if (passed) {
|
||||
console.log(chalk.green(`✅ ${testCase.name}`));
|
||||
} else {
|
||||
console.log(chalk.red(`❌ ${testCase.name}`));
|
||||
if (validation.errors.length > 0) {
|
||||
validation.errors.forEach(error => {
|
||||
console.log(chalk.red(` • ${error}`));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 .env 文件生成
|
||||
*/
|
||||
function testEnvFileGeneration() {
|
||||
console.log(chalk.blue('\n🧪 测试 .env 文件生成...'));
|
||||
|
||||
const testConfig = {
|
||||
PORT: 5000,
|
||||
HOST: 'localhost',
|
||||
API_TARGET: 'http://localhost:3000',
|
||||
API_PREFIX: '/api/v1',
|
||||
DEV_MODE: true,
|
||||
LOG_LEVEL: 'debug',
|
||||
CORS_ENABLED: true,
|
||||
CORS_ORIGIN: 'http://localhost:3000,https://example.com',
|
||||
SPA_FALLBACK_ENABLED: true,
|
||||
SPA_EXCLUDE_EXTENSIONS: '.js,.css,.png'
|
||||
};
|
||||
|
||||
const envContent = generateEnvFile(testConfig);
|
||||
|
||||
console.log(chalk.green('✅ 生成的 .env 内容:'));
|
||||
console.log(chalk.gray('─'.repeat(40)));
|
||||
console.log(envContent);
|
||||
console.log(chalk.gray('─'.repeat(40)));
|
||||
|
||||
// 测试解析生成的内容
|
||||
const lines = envContent.split('\n');
|
||||
const parsedEnv = {};
|
||||
|
||||
lines.forEach(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed && !trimmed.startsWith('#')) {
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
if (key && valueParts.length > 0) {
|
||||
parsedEnv[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const reparsedConfig = parseConfig(parsedEnv);
|
||||
const isConsistent = JSON.stringify(testConfig) === JSON.stringify(reparsedConfig);
|
||||
|
||||
if (isConsistent) {
|
||||
console.log(chalk.green('✅ 生成的内容解析正确'));
|
||||
} else {
|
||||
console.log(chalk.red('❌ 生成的内容解析不一致'));
|
||||
console.log('原始配置:', testConfig);
|
||||
console.log('重新解析:', reparsedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试配置摘要生成
|
||||
*/
|
||||
function testConfigSummary() {
|
||||
console.log(chalk.blue('\n🧪 测试配置摘要生成...'));
|
||||
|
||||
const testConfig = {
|
||||
PORT: 5173,
|
||||
HOST: '0.0.0.0',
|
||||
PUBLIC_DIR: 'dist',
|
||||
API_TARGET: 'http://localhost:3000',
|
||||
API_PREFIX: '/api',
|
||||
DEV_MODE: true,
|
||||
CORS_ENABLED: true,
|
||||
CORS_ORIGIN: '*',
|
||||
SPA_FALLBACK_ENABLED: true,
|
||||
SPA_EXCLUDE_EXTENSIONS: '.js,.css,.png,.jpg'
|
||||
};
|
||||
|
||||
const summary = getConfigSummary(testConfig);
|
||||
|
||||
console.log(chalk.green('✅ 生成的摘要:'));
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
|
||||
// 验证摘要结构
|
||||
const requiredKeys = ['server', 'proxy', 'spa', 'cors'];
|
||||
const hasAllKeys = requiredKeys.every(key => key in summary);
|
||||
|
||||
if (hasAllKeys) {
|
||||
console.log(chalk.green('✅ 摘要包含所有必需的键'));
|
||||
} else {
|
||||
console.log(chalk.red('❌ 摘要缺少必需的键'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行所有测试
|
||||
*/
|
||||
function runAllTests() {
|
||||
console.log(chalk.cyan('🚀 SPA 预览服务器配置测试\n'));
|
||||
|
||||
try {
|
||||
testConfigParsing();
|
||||
testConfigValidation();
|
||||
testEnvFileGeneration();
|
||||
testConfigSummary();
|
||||
|
||||
console.log(chalk.green('\n🎉 所有测试完成!'));
|
||||
console.log(chalk.blue('\n📝 测试摘要:'));
|
||||
console.log(chalk.green('✅ 配置解析'));
|
||||
console.log(chalk.green('✅ 配置验证'));
|
||||
console.log(chalk.green('✅ .env 文件生成'));
|
||||
console.log(chalk.green('✅ 配置摘要'));
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red('\n❌ 测试失败:'), error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接调用则运行测试
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
runAllTests();
|
||||
}
|
||||
|
||||
export { runAllTests };
|
||||
Reference in New Issue
Block a user