【调整】申请证书配置CA选项增加liteSSL证书

This commit is contained in:
cai
2026-01-13 16:45:05 +08:00
parent 6c15ae35a1
commit 190e250095
2108 changed files with 58 additions and 401539 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}
}

View File

@@ -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);
});

View File

@@ -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 };

View File

@@ -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
}
};
}

View File

@@ -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 };