加入后端代码
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
# 使用 Ubuntu 20.04 镜像作为基础
|
||||
FROM ubuntu:20.04
|
||||
|
||||
# 设置环境变量,以避免交互式安装
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 设置清华大学软件源
|
||||
RUN sed -i 's/archive.ubuntu.com/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y tzdata
|
||||
|
||||
# 设置上海时区
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
|
||||
# 安装 certbot nginx
|
||||
RUN apt install -y certbot python3-certbot-nginx
|
||||
|
||||
# TURN服务器
|
||||
RUN apt-get install -y vim coturn
|
||||
|
||||
# redis服务
|
||||
RUN apt-get install -y redis-server
|
||||
|
||||
# 安装nodejs 20
|
||||
RUN apt-get install -y curl
|
||||
|
||||
# node.js
|
||||
## Import repository GPG key
|
||||
RUN apt install -y ca-certificates gnupg && mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
## Add Node.JS 20 LTS APT repository.
|
||||
ENV NODE_MAJOR=20
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
## Update package index.
|
||||
RUN apt-get update
|
||||
## Install Node.js, npm, pnpm
|
||||
RUN apt install -y nodejs
|
||||
RUN npm install -g pnpm
|
||||
## node -v -> v20.18.1;npm -v -> 10.8.2;pnpm -v -> 9.14.4
|
||||
## install Yarn package manager
|
||||
#curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null
|
||||
#echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
#apt update && apt-get install yarn -y
|
||||
|
||||
#clean up
|
||||
RUN apt-get clean autoclean
|
||||
RUN apt-get autoremove --yes
|
||||
RUN rm -rf /var/lib/{apt,cache,log}/ && rm -rf /tmp/*
|
||||
@@ -0,0 +1,21 @@
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
sudo apt-get install -y vim coturn
|
||||
|
||||
sudo apt-get install -y redis-server
|
||||
|
||||
sudo apt-get install -y curl
|
||||
|
||||
sudo apt install -y ca-certificates gnupg && sudo mkdir -p /etc/apt/keyrings
|
||||
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
|
||||
export NODE_MAJOR=20
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt install -y nodejs
|
||||
sudo npm install -g pnpm
|
||||
|
||||
sudo apt-get clean autoclean
|
||||
sudo apt-get autoremove --yes
|
||||
sudo rm -rf /var/lib/{apt,cache,log}/ && sudo rm -rf /tmp/*
|
||||
@@ -0,0 +1,5 @@
|
||||
cp default /etc/nginx/sites-available/
|
||||
cp nginx.conf /etc/nginx
|
||||
nginx -t
|
||||
|
||||
/etc/init.d/nginx restart
|
||||
@@ -0,0 +1,126 @@
|
||||
server {
|
||||
# 将 HTTP 重定向到 HTTPS
|
||||
listen 80;
|
||||
server_name securityshare.xyz www.securityshare.xyz;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2; # 监听 TCP 端口 443,支持 HTTP/2 和 SSL
|
||||
listen 443 quic reuseport; # 监听 UDP 端口 443,用于 QUIC 和 HTTP/3
|
||||
# 'reuseport' 允许多个 worker 进程共享同一个端口,推荐用于 QUIC
|
||||
|
||||
# 确保 SSL 协议至少包含 TLSv1.3,因为 HTTP/3 要求 TLSv1.3
|
||||
ssl_protocols TLSv1.3 TLSv1.2; # 确保 TLSv1.3 在前面
|
||||
|
||||
# 添加 HTTP/3 特定的头部,告知浏览器 HTTP/3 可用
|
||||
# Alt-Svc (Alternative Service) 头部
|
||||
# h3=":443" 表示 HTTP/3 在当前域名和 443 端口上可用
|
||||
# ma=86400 表示这个信息缓存 24 小时 (86400 秒)
|
||||
add_header Alt-Svc 'h3=":443"; ma=86400';
|
||||
|
||||
# (可选,但推荐) 启用 0-RTT 数据,可以进一步减少延迟
|
||||
# 需要客户端和服务器都支持
|
||||
ssl_early_data on;
|
||||
|
||||
server_name securityshare.xyz www.securityshare.xyz;
|
||||
|
||||
# SSL 配置
|
||||
ssl_certificate /etc/letsencrypt/live/securityshare.xyz/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/securityshare.xyz/privkey.pem;
|
||||
|
||||
# SSL 优化
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# 现代配置
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# HSTS (谨慎启用)
|
||||
# add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
|
||||
# 定义前端构建产物在容器内的根路径
|
||||
# !!! 重要: 请将此路径修改为您的前端项目构建后在Nginx容器内的实际路径 !!!
|
||||
set $frontend_build_root /home/ubuntu/workdir_atbj/clipboard_web;
|
||||
|
||||
# 1. 优先处理 Next.js 的核心静态资源 (_next/static)
|
||||
location /_next/static/ {
|
||||
alias $frontend_build_root/.next/static/;
|
||||
expires 365d; # 长时间缓存
|
||||
access_log off; # 关闭此路径的访问日志
|
||||
add_header Cache-Control "public"; # 明确告知浏览器可以公开缓存
|
||||
}
|
||||
|
||||
# WebSocket 信令服务器配置
|
||||
location /socket.io/ {
|
||||
proxy_pass http://localhost:3001/socket.io/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS 配置
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
|
||||
|
||||
# WebSocket 相关优化
|
||||
proxy_read_timeout 86400; # 24h
|
||||
proxy_send_timeout 86400; # 24h
|
||||
proxy_connect_timeout 7d;
|
||||
proxy_buffering off;
|
||||
}
|
||||
# 后端API地址--转发
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:3001/api/; # 后端API地址--转发
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 修改 CORS 配置,只设置一个 Origin
|
||||
add_header Access-Control-Allow-Origin "https://www.securityshare.xyz" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always;
|
||||
add_header Access-Control-Allow-Credentials "true" always;
|
||||
|
||||
}
|
||||
# Next.js 图片优化服务 (通常由 Next.js 应用处理)
|
||||
location /_next/image {
|
||||
proxy_pass http://localhost:3000; # 指向 Next.js 应用
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
# 2. 处理 public 目录下的静态文件和 Next.js 动态请求
|
||||
# 这个 location 应该在特定代理 (如 /api/, /socket.io/) 之后,
|
||||
# 但在 /_next/static/ 之前或之后都可以,因为它们匹配不同的路径。
|
||||
# 为了清晰,我们把它放在这里。
|
||||
location / {
|
||||
# root 指向 public 目录的父目录,即前端构建产物的根目录
|
||||
root $frontend_build_root/public;
|
||||
|
||||
# 尝试按顺序查找文件:
|
||||
# 1. $uri: 作为 public 目录下的文件 (例如 /image.png -> $frontend_build_root/public/image.png)
|
||||
# 2. $uri/: 作为 public 目录下的目录 (通常不直接用于 Next.js public 文件)
|
||||
# 3. @nextjs: 如果以上都未找到,则将请求传递给 Next.js 应用处理
|
||||
try_files $uri $uri/ @nextjs_app;
|
||||
}
|
||||
# 命名 location, 用于将请求代理到 Next.js 应用
|
||||
location @nextjs_app {
|
||||
proxy_pass http://localhost:3000; # 指向 Next.js 应用
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#运行用户,需要给文件目录访问权限
|
||||
user root;
|
||||
#启动进程,通常设置成和cpu的数量相等
|
||||
# worker_processes 1;
|
||||
worker_processes auto;
|
||||
pid /run/nginx.pid;
|
||||
#include /etc/nginx/modules-enabled/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 768;
|
||||
# multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
|
||||
##
|
||||
# Basic Settings
|
||||
##
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
# server_tokens off;
|
||||
|
||||
# server_names_hash_bucket_size 64;
|
||||
# server_name_in_redirect off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
##
|
||||
# SSL Settings
|
||||
##
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
##
|
||||
# Logging Settings
|
||||
##
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
##
|
||||
# Gzip Settings
|
||||
##
|
||||
|
||||
gzip on;
|
||||
|
||||
# gzip_vary on;
|
||||
# gzip_proxied any;
|
||||
# gzip_comp_level 6;
|
||||
# gzip_buffers 16 8k;
|
||||
# gzip_http_version 1.1;
|
||||
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
##
|
||||
# Virtual Host Configs
|
||||
##
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
|
||||
|
||||
#mail {
|
||||
# # See sample authentication script at:
|
||||
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
|
||||
#
|
||||
# # auth_http localhost/auth.php;
|
||||
# # pop3_capabilities "TOP" "USER";
|
||||
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
|
||||
#
|
||||
# server {
|
||||
# listen localhost:110;
|
||||
# protocol pop3;
|
||||
# proxy on;
|
||||
# }
|
||||
#
|
||||
# server {
|
||||
# listen localhost:143;
|
||||
# protocol imap;
|
||||
# proxy on;
|
||||
# }
|
||||
#}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# 证书监控续期脚本--自动检查,如果少于30天则续期, 手动执行:
|
||||
# cd /home/ubuntu/workdir_atbj/clipboard_backend_node/docker/nginx && bash renew_ssl.sh
|
||||
# crontab 自动任务
|
||||
# chmod +x /home/ubuntu/workdir_atbj/clipboard_backend_node/docker/nginx/renew_ssl.sh
|
||||
# crontab -e 打开编辑器
|
||||
# 0 0 * * * bash /home/ubuntu/workdir_atbj/clipboard_backend_node/docker/nginx/renew_ssl.sh >> /home/ubuntu/workdir_atbj/certbot-renew.log 2>&1
|
||||
|
||||
# 首先切换到脚本所在目录
|
||||
cd "$(dirname "$(readlink -f "$0")")" || exit 1
|
||||
|
||||
# 定义证书目录
|
||||
CERTBOT_DIR="/etc/letsencrypt/live"
|
||||
|
||||
# 遍历所有证书
|
||||
for CERT_PATH in "$CERTBOT_DIR"/*/fullchain.pem; do
|
||||
# 获取域名
|
||||
DOMAIN=$(basename "$(dirname "$CERT_PATH")")
|
||||
|
||||
# 检查证书有效期
|
||||
DAYS_REMAINING=$(openssl x509 -enddate -noout -in "$CERT_PATH" | cut -d= -f2 | xargs -I{} date -d "{}" +%s)
|
||||
NOW=$(date +%s)
|
||||
DAYS=$(( ($DAYS_REMAINING - $NOW) / 86400 ))
|
||||
|
||||
echo "Domain: $DOMAIN, Days left: $DAYS days"
|
||||
|
||||
# 如果剩余时间少于 30 天,自动续期
|
||||
if [ $DAYS -lt 30 ]; then
|
||||
echo "Warning: Certificate for $DOMAIN will expire in $DAYS days. Renewing..."
|
||||
# 运行续期命令之前要解除80端口占用--暂停ngnix
|
||||
sudo bash stop_clean-log.sh
|
||||
# 使用 Certbot 自动续期
|
||||
sudo certbot renew --force-renewal --cert-name "$DOMAIN"
|
||||
|
||||
# 检查续期是否成功
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Renewal successful for $DOMAIN"
|
||||
else
|
||||
echo "Failed to renew certificate for $DOMAIN"
|
||||
fi
|
||||
# 启动ngnix
|
||||
sudo bash cp_cfg_run.sh
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,2 @@
|
||||
/etc/init.d/nginx stop
|
||||
rm /var/log/nginx/*
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 检查是否指定了环境参数
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 [0--production|1--development]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 获取环境参数
|
||||
ENVIRONMENT=$1
|
||||
|
||||
# 根据环境参数执行操作
|
||||
if [ "$ENVIRONMENT" = "0" ]; then
|
||||
echo "Switching to production environment..."
|
||||
cp turnserver_production.conf /etc/turnserver.conf
|
||||
service coturn restart
|
||||
echo "Production environment is now active."
|
||||
elif [ "$ENVIRONMENT" = "1" ]; then
|
||||
echo "Switching to development environment..."
|
||||
cp turnserver_development.conf /etc/turnserver.conf
|
||||
service coturn restart
|
||||
echo "Development environment is now active."
|
||||
else
|
||||
echo "Invalid environment specified: $ENVIRONMENT"
|
||||
echo "Please specify either '0--production' or '1--development'."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,45 @@
|
||||
# /etc/turnserver.conf
|
||||
|
||||
# 监听所有接口
|
||||
listening-ip=0.0.0.0
|
||||
|
||||
# 使用你的服务器公网IP
|
||||
external-ip=49.235.189.26
|
||||
|
||||
# TURN 服务器端口
|
||||
listening-port=3478
|
||||
# 启用 TLS--TURNS(加密的 TURN)
|
||||
#tls-listening-port=5349
|
||||
|
||||
# 中继端口范围
|
||||
min-port=49152
|
||||
max-port=65535
|
||||
|
||||
# 长期证书机制
|
||||
lt-cred-mech
|
||||
|
||||
# TURN 服务器域名(如果有的话)
|
||||
# realm=turn.securityshare.xyz
|
||||
realm=49.235.189.26
|
||||
|
||||
# TURN 服务器证书和密钥(用于TLS)
|
||||
# cert=/etc/letsencrypt/live/turn.securityshare.xyz/fullchain.pem
|
||||
# pkey=/etc/letsencrypt/live/turn.securityshare.xyz/privkey.pem
|
||||
|
||||
# 用户名和密码(在生产环境中应使用更安全的方法)
|
||||
user=secureUser:QWERTY!@#456
|
||||
|
||||
# 启用详细日志
|
||||
verbose
|
||||
|
||||
# 允许回环地址
|
||||
# allow-loopback-peers
|
||||
|
||||
# 设置最大带宽(字节/秒)
|
||||
# max-bandwidth=0
|
||||
|
||||
# 禁用 TLS
|
||||
# no-tls
|
||||
|
||||
# 禁用 DTLS
|
||||
# no-dtls
|
||||
@@ -0,0 +1,45 @@
|
||||
# /etc/turnserver.conf
|
||||
|
||||
# 监听所有接口
|
||||
listening-ip=0.0.0.0
|
||||
|
||||
# 使用你的服务器公网IP
|
||||
external-ip=43.153.3.146
|
||||
|
||||
# TURN 服务器端口
|
||||
listening-port=3478
|
||||
# 启用 TLS--TURNS(加密的 TURN)
|
||||
tls-listening-port=5349
|
||||
|
||||
# 中继端口范围
|
||||
min-port=49152
|
||||
max-port=65535
|
||||
|
||||
# 长期证书机制
|
||||
lt-cred-mech
|
||||
|
||||
# TURN 服务器域名(如果有的话)
|
||||
realm=turn.securityshare.xyz
|
||||
# realm=43.153.3.146
|
||||
|
||||
# TURN 服务器证书和密钥(用于TLS)
|
||||
cert=/etc/letsencrypt/live/turn.securityshare.xyz/fullchain.pem
|
||||
pkey=/etc/letsencrypt/live/turn.securityshare.xyz/privkey.pem
|
||||
|
||||
# 用户名和密码(在生产环境中应使用更安全的方法)
|
||||
user=secureUser:QWERTY!@#456
|
||||
|
||||
# 启用详细日志
|
||||
verbose
|
||||
|
||||
# 允许回环地址
|
||||
# allow-loopback-peers
|
||||
|
||||
# 设置最大带宽(字节/秒)
|
||||
# max-bandwidth=0
|
||||
|
||||
# 禁用 TLS
|
||||
# no-tls
|
||||
|
||||
# 禁用 DTLS
|
||||
# no-dtls
|
||||
@@ -0,0 +1,18 @@
|
||||
module.exports = {
|
||||
apps: [{
|
||||
name: "signaling-server",
|
||||
script: "./dist/server.js", // 指向编译后的文件
|
||||
watch: false,
|
||||
env: {
|
||||
"NODE_ENV": "production",
|
||||
"PORT": 3001
|
||||
},
|
||||
log_date_format: "YYYY-MM-DD HH:mm:ss",
|
||||
error_file: "/var/log/signaling-server-error.log",
|
||||
out_file: "/var/log/signaling-server-out.log",
|
||||
max_memory_restart: "500M",
|
||||
instances: 1,
|
||||
exec_mode: "fork",
|
||||
group: "ssl-cert" // 添加这行,指定进程运行的组
|
||||
}]
|
||||
}
|
||||
+2369
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development tsx watch src/server.ts",
|
||||
"build": "rimraf dist && tsc",
|
||||
"start": "cross-env NODE_ENV=production node dist/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
// 定义配置对象的类型
|
||||
interface AppConfig {
|
||||
PORT: number;
|
||||
CORS_ORIGIN: string;
|
||||
NODE_ENV: 'development' | 'production';
|
||||
REDIS: {
|
||||
HOST: string;
|
||||
PORT: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 根据环境加载对应的.env文件
|
||||
dotenv.config({
|
||||
path: process.env.NODE_ENV === 'production'
|
||||
? path.resolve(process.cwd(), '.env.production.local')
|
||||
: path.resolve(process.cwd(), '.env.development.local')
|
||||
});
|
||||
|
||||
// 导出类型安全的配置对象
|
||||
export const CONFIG: AppConfig = {
|
||||
PORT: parseInt(process.env.PORT || '3001', 10),
|
||||
CORS_ORIGIN: process.env.CORS_ORIGIN!,
|
||||
NODE_ENV: (process.env.NODE_ENV as 'development' | 'production') || 'development',
|
||||
REDIS: {
|
||||
HOST: 'localhost',
|
||||
PORT: 6379
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { CorsOptions } from 'cors';
|
||||
import { CONFIG } from './env';
|
||||
// 配置 CORS
|
||||
export const corsOptions: CorsOptions = CONFIG.NODE_ENV === 'production'
|
||||
? {
|
||||
origin: CONFIG.CORS_ORIGIN,
|
||||
methods: ['GET', 'POST', 'OPTIONS'],
|
||||
credentials: true,
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}
|
||||
: {
|
||||
origin: true, // 开发环境允许所有源
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
};
|
||||
// 配置 Socket.IO 的 CORS
|
||||
export const corsWSOptions = CONFIG.NODE_ENV === 'production'
|
||||
? {
|
||||
origin: CONFIG.CORS_ORIGIN,// 允许的源,替换为你的Next.js应用的URL
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
}
|
||||
: {
|
||||
// 开发环境下允许多个源
|
||||
origin: [
|
||||
CONFIG.CORS_ORIGIN,
|
||||
/^http:\/\/192\.168\.\d+\.\d+:3000$/,// 匹配所有 192.168.x.x:3000 格式的局域网地址
|
||||
],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Router, RequestHandler } from 'express';
|
||||
import { redis } from '../services/redis';
|
||||
import * as roomService from '../services/room';
|
||||
import { ReferrerTrack, LogMessage } from '../types/room';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 定义接口提高代码可读性和类型安全性
|
||||
interface CreateRoomRequest {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
interface CheckRoomRequest {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
// 创建房间的路由处理函数
|
||||
const createRoomHandler: RequestHandler<{}, any, CreateRoomRequest> = async (req, res) => {
|
||||
const { roomId } = req.body;
|
||||
if (!roomId) {
|
||||
res.status(400).json({ error: 'Room ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const exists = await roomService.isRoomExist(roomId);
|
||||
const response = {
|
||||
success: !exists,
|
||||
message: exists ? 'roomId is already exists' : 'create room success'
|
||||
};
|
||||
|
||||
if (!exists) {
|
||||
await roomService.createRoom(roomId);
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error checking room:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取房间的路由处理函数
|
||||
const getRoomHandler: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const roomId = await roomService.getAvailableRoomId();
|
||||
await roomService.createRoom(roomId);
|
||||
res.json({ roomId });
|
||||
} catch (error) {
|
||||
console.error('Error getting room:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// 检查房间的路由处理函数
|
||||
const checkRoomHandler: RequestHandler<{}, any, CheckRoomRequest> = async (req, res) => {
|
||||
const { roomId } = req.body;
|
||||
if (!roomId) {
|
||||
res.status(400).json({ error: 'Room ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const exists = await roomService.isRoomExist(roomId);
|
||||
res.json({ available: !exists });
|
||||
} catch (error) {
|
||||
console.error('Error checking room:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// 设置跟踪的路由处理函数
|
||||
const setTrackHandler: RequestHandler<{}, any, ReferrerTrack> = async (req, res) => {
|
||||
if (req.method !== 'POST') {
|
||||
res.status(405).json({ message: 'Method not allowed' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { ref, timestamp, path } = req.body;
|
||||
// 按日期统计
|
||||
const date = new Date(timestamp).toISOString().split('T')[0];
|
||||
//"referrers:daily:2024-01-20" : { "producthunt": "5", "twitter": "3" }
|
||||
const dailyKey = `referrers:daily:${date}`;
|
||||
const thirtyDaysInSeconds = 30 * 24 * 60 * 60;
|
||||
|
||||
// 使用MULTI确保hincrby和expire的原子性
|
||||
await redis.multi()
|
||||
.hincrby(dailyKey, ref, 1) // \"referrers:daily:2024-01-20\" : { \"producthunt\": \"5\", \"twitter\": \"3\" }
|
||||
.expire(dailyKey, thirtyDaysInSeconds) // 设置30天过期
|
||||
.exec();
|
||||
|
||||
await redis.hincrby(`referrers:daily:${date}`, ref, 1);
|
||||
//"referrers:sources" : ["producthunt", "twitter", ...] // 来源集合
|
||||
await redis.sadd('referrers:sources', ref);
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Track API Error:', error);
|
||||
res.status(500).json({ success: false, error: 'Failed to track referrer' });
|
||||
}
|
||||
};
|
||||
|
||||
// 日志调试的路由处理函数
|
||||
const logsDebugHandler: RequestHandler<{}, any, LogMessage> = async (req, res) => {
|
||||
try {
|
||||
const { message, timestamp } = req.body;
|
||||
console.log(`logs----timestamp:${timestamp} message:${message}`);
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error checking room:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// 注册路由
|
||||
router.post('/api/creat_room', createRoomHandler);
|
||||
router.get('/api/get_room', getRoomHandler);
|
||||
router.post('/api/check_room', checkRoomHandler);
|
||||
router.post('/api/set_track', setTrackHandler);
|
||||
router.post('/api/logs_debug', logsDebugHandler);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,29 @@
|
||||
import express from 'express';//express: 用于创建一个简洁且灵活的Node.js web应用框架
|
||||
import cors from 'cors';
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';//实时通信库,基于WebSocket协议,实现双向通信
|
||||
import { CONFIG } from './config/env';
|
||||
import { corsOptions, corsWSOptions } from './config/server';
|
||||
import { redis } from './services/redis';
|
||||
import { cleanupData } from './utils/dataCleanup';
|
||||
import apiRouter from './routes/api';
|
||||
import { setupSocketHandlers } from './socket/handlers';
|
||||
// 设置定时清理任务,间隔24H
|
||||
const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
setInterval(() => {
|
||||
cleanupData(redis);
|
||||
}, CLEANUP_INTERVAL);
|
||||
|
||||
const app = express();//创建一个Express应用
|
||||
app.use(cors(corsOptions));// 添加 CORS 中间件
|
||||
app.use(express.json());
|
||||
app.use(apiRouter);
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
const io = new Server(server, { cors: corsWSOptions });
|
||||
setupSocketHandlers(io);
|
||||
|
||||
server.listen(CONFIG.PORT, () => {
|
||||
console.log(`Signaling server running in ${CONFIG.NODE_ENV} mode on port ${CONFIG.PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { redis } from './redis';
|
||||
|
||||
const RATE_LIMIT_PREFIX = 'ratelimit:join:';
|
||||
const RATE_WINDOW = 5; // 5秒时间窗口
|
||||
const RATE_LIMIT = 2; // 允许的最大请求次数
|
||||
|
||||
export async function checkRateLimit(ip: string): Promise<{
|
||||
allowed: boolean;
|
||||
remaining: number;
|
||||
resetAfter: number;
|
||||
}> {
|
||||
const key = `${RATE_LIMIT_PREFIX}${ip}`;
|
||||
const now = Date.now();
|
||||
const windowStart = now - (RATE_WINDOW * 1000);
|
||||
|
||||
// 使用 Redis 的 MULTI 命令开启事务
|
||||
const pipeline = redis.pipeline();
|
||||
|
||||
// 移除时间窗口之前的数据
|
||||
pipeline.zremrangebyscore(key, 0, windowStart);
|
||||
// 获取当前时间窗口内的请求次数
|
||||
pipeline.zcard(key);
|
||||
// 添加新的请求记录
|
||||
pipeline.zadd(key, now, `${now}`);
|
||||
// 设置过期时间
|
||||
pipeline.expire(key, RATE_WINDOW);
|
||||
|
||||
const results = await pipeline.exec();
|
||||
|
||||
if (!results) {
|
||||
throw new Error('Redis pipeline failed');
|
||||
}
|
||||
|
||||
const requestCount = results[1][1] as number;
|
||||
const allowed = requestCount <= RATE_LIMIT;
|
||||
const remaining = Math.max(RATE_LIMIT - requestCount, 0);
|
||||
const resetAfter = RATE_WINDOW - Math.floor((now - windowStart) / 1000);
|
||||
|
||||
return { allowed, remaining, resetAfter };
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Redis } from 'ioredis';
|
||||
import { CONFIG } from '../config/env';
|
||||
// 房间前缀和过期时间(秒)
|
||||
export const ROOM_PREFIX = 'room:';
|
||||
export const SOCKET_PREFIX = 'socket:';
|
||||
export const ROOM_EXPIRY = 3600 * 24; // 24 hours
|
||||
// Redis 配置选项
|
||||
const redisConfig = {
|
||||
host: CONFIG.REDIS.HOST,
|
||||
port: CONFIG.REDIS.PORT,
|
||||
// Redis 持久化配置需要在 redis.conf 中设置,而不是在客户端
|
||||
// appendonly: 'yes',// 启用 AOF 持久化
|
||||
// save: '900 1 300 10',// 启用 RDB 快照
|
||||
};
|
||||
|
||||
export const redis = new Redis(redisConfig);
|
||||
|
||||
// 可以在这里添加连接事件监听
|
||||
redis.on('connect', () => {
|
||||
console.log('Redis connected successfully');
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Redis connection error:', err);
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { redis, ROOM_PREFIX, SOCKET_PREFIX, ROOM_EXPIRY } from './redis';
|
||||
// 生成随机房间号--4位数字
|
||||
export async function generateRoomId(): Promise<string> {
|
||||
return Math.floor(1000 + Math.random() * 9000).toString();
|
||||
}
|
||||
// 检查房间是否存在
|
||||
export async function isRoomExist(roomId: string): Promise<boolean> {
|
||||
return await redis.hexists(ROOM_PREFIX + roomId, 'created_at') === 1;//hset和hexists方法操作哈希,created_at是字段名
|
||||
}
|
||||
// 创建新房间
|
||||
// (Hash)
|
||||
// "room:1234" : {
|
||||
// "created_at": "1705123456789"
|
||||
// }
|
||||
export async function createRoom(roomId: string): Promise<void> {
|
||||
await redis.multi()
|
||||
.hset(ROOM_PREFIX + roomId, 'created_at', Date.now())//设置hash,存储房间的创建时间;
|
||||
.expire(ROOM_PREFIX + roomId, ROOM_EXPIRY)//设置过期时间
|
||||
.exec();
|
||||
}
|
||||
// 删除房间
|
||||
export async function deleteRoom(roomId: string): Promise<void> {
|
||||
await redis.del(ROOM_PREFIX + roomId);
|
||||
}
|
||||
// 刷新房间过期时间
|
||||
export async function refreshRoom(roomId: string, expiry: number = 0): Promise<void> {
|
||||
const actualExpiry = expiry > 0 ? expiry : ROOM_EXPIRY;
|
||||
console.log(`EXPIRY of roomId:${roomId} is ${actualExpiry}`);
|
||||
await redis.expire(ROOM_PREFIX + roomId, actualExpiry);
|
||||
}
|
||||
// 获取可用房间号
|
||||
export async function getAvailableRoomId(): Promise<string> {
|
||||
let roomId: string;
|
||||
do {
|
||||
roomId = await generateRoomId();
|
||||
} while (await isRoomExist(roomId));
|
||||
return roomId;
|
||||
}
|
||||
// 将socket.id与房间号绑定
|
||||
export async function bindSocketToRoom(socketId: string, roomId: string): Promise<void> {
|
||||
await redis.multi()
|
||||
//字符串,存储与该socket ID相关联的房间号,"socket:abcd1234" : "1234"
|
||||
.set(SOCKET_PREFIX + socketId, roomId)
|
||||
//添加集合,房间内的 Socket 列表 (Set),"room:1234:sockets" : ["socket1", "socket2", ...]
|
||||
.sadd(ROOM_PREFIX + roomId + ':sockets', socketId)
|
||||
.exec();
|
||||
}
|
||||
// 获取socket.id对应的房间号
|
||||
export async function getRoomBySocketId(socketId: string): Promise<string | null> {
|
||||
return await redis.get(SOCKET_PREFIX + socketId);
|
||||
}
|
||||
// 解绑socket.id与房间号
|
||||
export async function unbindSocketFromRoom(socketId: string, roomId: string): Promise<void> {
|
||||
await redis.multi()
|
||||
.del(SOCKET_PREFIX + socketId)//解绑socket ID与房间号
|
||||
.srem(ROOM_PREFIX + roomId + ':sockets', socketId)//从房间的集合中移除socket ID
|
||||
.exec();
|
||||
}
|
||||
// 检查房间是否为空
|
||||
export async function isRoomEmpty(roomId: string): Promise<boolean> {
|
||||
const count = await redis.scard(ROOM_PREFIX + roomId + ':sockets');//返回集合中元素的数量
|
||||
return count === 0;
|
||||
}
|
||||
// 检查房间连接数
|
||||
export async function roomNumOfConnection(roomId: string): Promise<number> {
|
||||
return await redis.scard(ROOM_PREFIX + roomId + ':sockets');
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import * as roomService from '../services/room';
|
||||
import { JoinData, SignalingData, InitiatorData, RecipientData } from '../types/socket';
|
||||
import { checkRateLimit } from '../services/rateLimit';
|
||||
// 房间管理:
|
||||
// 使用 roomId 进行广播消息(socket.to(roomId).emit())
|
||||
// 场景:新用户加入通知、房间状态更新等
|
||||
// WebRTC 信令:
|
||||
// 使用 peerId 进行点对点通信(socket.to(peerId).emit())
|
||||
// 场景:offer、answer、ice-candidate 等 WebRTC 连接建立过程中的所有信令
|
||||
export function setupSocketHandlers(io: Server): void {
|
||||
io.on('connection', (socket: Socket) => {
|
||||
console.log('New client connected:', socket.id);
|
||||
|
||||
socket.on('join', async (data: JoinData) => {
|
||||
const { roomId } = data;
|
||||
try {
|
||||
// 获取客户端IP
|
||||
const clientIp = socket.handshake.headers['x-forwarded-for'] ||
|
||||
socket.handshake.address;
|
||||
// 检查频率限制
|
||||
const rateLimitCheck = await checkRateLimit(clientIp as string);
|
||||
if (!rateLimitCheck.allowed) {
|
||||
socket.emit('joinResponse', {
|
||||
success: false,
|
||||
message: `Rate limit exceeded. Please try again in ${rateLimitCheck.resetAfter} seconds. ` +
|
||||
`You have ${rateLimitCheck.remaining} attempts remaining.`,
|
||||
roomId: roomId
|
||||
});
|
||||
return;
|
||||
}
|
||||
const roomExist = await roomService.isRoomExist(roomId);
|
||||
console.log(`room ${roomId} roomExist:${roomExist}`);
|
||||
|
||||
if (roomExist) {//房间存在
|
||||
const existingRoomId = await roomService.getRoomBySocketId(socket.id);
|
||||
if (!existingRoomId) {//socket.id不在房间里面 才允许新连接进入房间
|
||||
socket.join(roomId);
|
||||
console.log(`Client ${socket.id} joined room ${roomId}`);
|
||||
await roomService.bindSocketToRoom(socket.id, roomId);
|
||||
}
|
||||
|
||||
await roomService.refreshRoom(roomId);
|
||||
// 通知用户加入成功
|
||||
socket.emit('joinResponse', {
|
||||
success: true,
|
||||
message: 'Successfully joined room',
|
||||
roomId: roomId
|
||||
});
|
||||
// 通知房间内所有其他用户有新成员加入
|
||||
socket.to(roomId).emit('ready', {
|
||||
peerId: socket.id
|
||||
});
|
||||
} else {
|
||||
console.log(`room ${roomId} roomExist:${roomExist},Room does not exist branch`);
|
||||
socket.emit('joinResponse', {
|
||||
success: false,
|
||||
message: 'Room does not exist',
|
||||
roomId: roomId
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error joining room:', error);
|
||||
socket.emit('joinResponse', {
|
||||
success: false,
|
||||
message: 'Server error while joining room',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
});
|
||||
// 处理WebRTC信令--直接转发
|
||||
// offer, answer, ice-candidate: 这些事件处理WebRTC连接的信令。它们负责转发客户端之间的连接请求和网络协商消息。
|
||||
// offer: 当一个客户端发起连接请求时,会发送一个offer给服务器,服务器将其转发给相同房间中的其他客户端。
|
||||
// answer: 被邀请的客户端接收到offer后,生成一个answer,通过服务器返回给发起连接的客户端。
|
||||
// ice-candidate: 当WebRTC需要穿透NAT防火墙时,会生成ICE候选者,客户端通过服务器相互交换这些信息,帮助建立P2P连接。
|
||||
socket.on('offer', (data: SignalingData) => {
|
||||
socket.to(data.peerId).emit('offer', {
|
||||
offer: data.offer,
|
||||
from: data.from,
|
||||
peerId: socket.id // 发送方的ID
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('answer', (data: SignalingData) => {
|
||||
socket.to(data.peerId).emit('answer', {
|
||||
answer: data.answer,
|
||||
from: data.from,
|
||||
peerId: socket.id
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('ice-candidate', (data: SignalingData) => {
|
||||
socket.to(data.peerId).emit('ice-candidate', {
|
||||
candidate: data.candidate,
|
||||
from: data.from,
|
||||
peerId: socket.id
|
||||
});
|
||||
});
|
||||
// 处理发起方重新上线的通知--广播给房间内的其他用户
|
||||
socket.on('initiator-online', (data: InitiatorData) => {
|
||||
socket.to(data.roomId).emit('initiator-online', {
|
||||
roomId: data.roomId
|
||||
});
|
||||
});
|
||||
// 处理接收方的响应
|
||||
socket.on('recipient-ready', (data: RecipientData) => {
|
||||
socket.to(data.roomId).emit('recipient-ready', {
|
||||
peerId: data.peerId
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
console.log('Disconnected:', socket.id);
|
||||
const roomId = await roomService.getRoomBySocketId(socket.id);
|
||||
if (roomId) {
|
||||
await roomService.unbindSocketFromRoom(socket.id, roomId);
|
||||
if (await roomService.isRoomEmpty(roomId)) {
|
||||
// await deleteRoom(roomId);
|
||||
await roomService.refreshRoom(roomId, 3600);
|
||||
console.log(`Room ${roomId} is empty and will deleted in 1 hour`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface RoomInfo {
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
export interface ReferrerTrack {
|
||||
ref: string;
|
||||
timestamp: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface LogMessage {
|
||||
message: string;
|
||||
timestamp: number;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export interface JoinData {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
// 添加 WebRTC 相关类型定义
|
||||
declare global {
|
||||
interface RTCSessionDescriptionInit {
|
||||
type: RTCSdpType;
|
||||
sdp: string;
|
||||
}
|
||||
|
||||
interface RTCIceCandidateInit {
|
||||
candidate: string;
|
||||
sdpMLineIndex?: number | null;
|
||||
sdpMid?: string | null;
|
||||
usernameFragment?: string | null;
|
||||
}
|
||||
|
||||
type RTCSdpType = 'answer' | 'offer' | 'pranswer' | 'rollback';
|
||||
}
|
||||
|
||||
export interface SignalingData {
|
||||
peerId: string;
|
||||
from?: string;
|
||||
offer?: RTCSessionDescriptionInit;
|
||||
answer?: RTCSessionDescriptionInit;
|
||||
candidate?: RTCIceCandidateInit;
|
||||
}
|
||||
|
||||
export interface InitiatorData {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export interface RecipientData {
|
||||
roomId: string;
|
||||
peerId: string;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//定时任务处理器
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
export const cleanupData = async (redis: Redis): Promise<void> => {
|
||||
try {
|
||||
// 1. 找出30天以前(过期)的每日统计数据
|
||||
const today = new Date();
|
||||
const thirtyDaysAgo = new Date(today.setDate(today.getDate() - 30));
|
||||
|
||||
// 获取所有 daily 统计 key
|
||||
const dailyKeys = await redis.keys('referrers:daily:*');
|
||||
|
||||
for (const key of dailyKeys) {
|
||||
const keyDate = key.split(':')[2];
|
||||
if (new Date(keyDate) < thirtyDaysAgo) {
|
||||
await redis.del(key);
|
||||
} else {
|
||||
// 为未过期的 key 重新设置过期时间
|
||||
await redis.expire(key, 60 * 60 * 24 * 30); // 30天
|
||||
}
|
||||
}
|
||||
console.log('Data cleanup completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Error during data cleanup:', error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
"lib": ["ES2020"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
"baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user