feat: up
This commit is contained in:
parent
15d61a005f
commit
57f964744f
553
design.md
Normal file
553
design.md
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
好的!我理解了,你需要:
|
||||||
|
|
||||||
|
1. **前端**:现代化框架(React/Vue)
|
||||||
|
2. **后端**:独立的轻量后端(不用 Next.js API Routes)
|
||||||
|
3. **部署**:都要简单方便
|
||||||
|
|
||||||
|
让我重新设计一个最佳方案!🎯 **最终推荐架构**
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
### **技术选型**
|
||||||
|
|
||||||
|
前端 (SPA):
|
||||||
|
- Framework: Vite + React 18 + TypeScript
|
||||||
|
- UI: Tailwind CSS + shadcn/ui
|
||||||
|
- State: Zustand + TanStack Query
|
||||||
|
- Charts: Recharts + D3.js
|
||||||
|
- 部署: Cloudflare Pages / Vercel (静态托管)
|
||||||
|
|
||||||
|
后端 (API):
|
||||||
|
- Framework: Fastify + TypeScript (比Express快2倍)
|
||||||
|
- GitHub SDK: Octokit
|
||||||
|
- Database: PostgreSQL (Supabase免费托管)
|
||||||
|
- Cache: Upstash Redis (Serverless)
|
||||||
|
- 部署: Railway.app ⭐ (一键部署,免费额度够用)
|
||||||
|
|
||||||
|
认证:
|
||||||
|
- GitHub OAuth 2.0
|
||||||
|
- JWT Token
|
||||||
|
|
||||||
|
实时通信:
|
||||||
|
- Server-Sent Events (SSE) - 比WebSocket简单
|
||||||
|
- 或 Pusher (免费版)
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
🏗️ **系统架构图**
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 用户浏览器 │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ React SPA (Vite) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │ │
|
||||||
|
│ │ │Dashboard │ │Repository │ │Analytics │ │ │
|
||||||
|
│ │ │ Page │ │Detail Page│ │ Page │ │ │
|
||||||
|
│ │ └────┬─────┘ └─────┬─────┘ └──────┬───────┘ │ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ │ └──────────────┴────────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌─────────▼──────────┐ │ │
|
||||||
|
│ │ │ TanStack Query │ (数据获取层) │ │
|
||||||
|
│ │ │ + Axios/Fetch │ │ │
|
||||||
|
│ │ └─────────┬──────────┘ │ │
|
||||||
|
│ └──────────────────────┼───────────────────────────────┘ │
|
||||||
|
└─────────────────────────┼──────────────────────────────────┘
|
||||||
|
│
|
||||||
|
HTTPS (REST API)
|
||||||
|
│
|
||||||
|
┌─────────────────────────▼──────────────────────────────────┐
|
||||||
|
│ 后端服务器 (Railway) │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Fastify API Server (Node.js) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
|
||||||
|
│ │ │ Auth │ │ Repos │ │ Webhooks │ │ │
|
||||||
|
│ │ │ /login │ │ /repos/* │ │ /webhooks/* │ │ │
|
||||||
|
│ │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ │ └─────────────┴────────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌──────────▼──────────┐ │ │
|
||||||
|
│ │ │ Service Layer │ │ │
|
||||||
|
│ │ │ • GitHubService │ │ │
|
||||||
|
│ │ │ • CacheService │ │ │
|
||||||
|
│ │ │ • WebhookService │ │ │
|
||||||
|
│ │ └──────────┬──────────┘ │ │
|
||||||
|
│ └─────────────────────┼────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────┼──────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ┌────▼─────┐ ┌────▼────┐ ┌──────▼──────┐ │
|
||||||
|
│ │ Octokit │ │ Prisma │ │ Redis Cache │ │
|
||||||
|
│ │ GitHub │ │ ORM │ │ (Upstash) │ │
|
||||||
|
│ │ API │ └────┬────┘ └─────────────┘ │
|
||||||
|
│ └──────────┘ │ │
|
||||||
|
└───────────────────────┼─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────▼──────┐
|
||||||
|
│ PostgreSQL │
|
||||||
|
│ (Supabase) │
|
||||||
|
└─────────────┘
|
||||||
|
|
||||||
|
┌────────────────────────────────────────────┐
|
||||||
|
│ 外部服务 │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────────┐ │
|
||||||
|
│ │ GitHub API │ │ GitHub Webhooks │ │
|
||||||
|
│ │ (公开接口) │ │ (实时事件推送) │ │
|
||||||
|
│ └──────────────┘ └──────────────────┘ │
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
📂 **项目结构**
|
||||||
|
|
||||||
|
github-dashboard/
|
||||||
|
├── frontend/ # 前端项目 (Vite)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ │ ├── dashboard/
|
||||||
|
│ │ │ │ ├── StatsCard.tsx
|
||||||
|
│ │ │ │ ├── ActivityChart.tsx
|
||||||
|
│ │ │ │ └── RepoList.tsx
|
||||||
|
│ │ │ ├── charts/
|
||||||
|
│ │ │ └── ui/ # shadcn/ui 组件
|
||||||
|
│ │ ├── pages/
|
||||||
|
│ │ │ ├── Dashboard.tsx
|
||||||
|
│ │ │ ├── RepoDetail.tsx
|
||||||
|
│ │ │ └── Analytics.tsx
|
||||||
|
│ │ ├── hooks/
|
||||||
|
│ │ │ ├── useRepos.ts
|
||||||
|
│ │ │ └── useRepoStats.ts
|
||||||
|
│ │ ├── lib/
|
||||||
|
│ │ │ ├── api.ts # API 请求封装
|
||||||
|
│ │ │ └── auth.ts # 认证逻辑
|
||||||
|
│ │ ├── stores/
|
||||||
|
│ │ │ └── authStore.ts # Zustand store
|
||||||
|
│ │ ├── App.tsx
|
||||||
|
│ │ └── main.tsx
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── vite.config.ts
|
||||||
|
│ └── tailwind.config.js
|
||||||
|
│
|
||||||
|
├── backend/ # 后端项目 (Fastify)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── routes/
|
||||||
|
│ │ │ ├── auth.ts # POST /auth/login
|
||||||
|
│ │ │ ├── repos.ts # GET /repos, /repos/:id
|
||||||
|
│ │ │ ├── webhooks.ts # POST /webhooks/github
|
||||||
|
│ │ │ └── stats.ts # GET /stats/*
|
||||||
|
│ │ ├── services/
|
||||||
|
│ │ │ ├── github.service.ts
|
||||||
|
│ │ │ ├── cache.service.ts
|
||||||
|
│ │ │ └── webhook.service.ts
|
||||||
|
│ │ ├── middlewares/
|
||||||
|
│ │ │ ├── auth.middleware.ts
|
||||||
|
│ │ │ └── error.middleware.ts
|
||||||
|
│ │ ├── prisma/
|
||||||
|
│ │ │ └── schema.prisma
|
||||||
|
│ │ ├── types/
|
||||||
|
│ │ │ └── index.ts
|
||||||
|
│ │ ├── utils/
|
||||||
|
│ │ │ └── logger.ts
|
||||||
|
│ │ ├── app.ts # Fastify 实例
|
||||||
|
│ │ └── server.ts # 启动文件
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── tsconfig.json
|
||||||
|
│ └── .env.example
|
||||||
|
│
|
||||||
|
└── README.md
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
🚀 **部署方案**
|
||||||
|
|
||||||
|
### **前端部署:Cloudflare Pages** ⭐
|
||||||
|
|
||||||
|
# 1. 构建
|
||||||
|
cd frontend
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 2. Cloudflare Pages 自动部署
|
||||||
|
# - 连接 GitHub 仓库
|
||||||
|
# - 构建命令: npm run build
|
||||||
|
# - 输出目录: dist
|
||||||
|
# - 自动部署: 推送到 main 分支即触发
|
||||||
|
|
||||||
|
**优势:**
|
||||||
|
|
||||||
|
* ✅ 完全免费
|
||||||
|
* ✅ 全球 CDN(超快)
|
||||||
|
* ✅ 自动 HTTPS
|
||||||
|
* ✅ 无限带宽
|
||||||
|
* ✅ 支持自定义域名
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
### **后端部署:Railway.app** ⭐⭐⭐
|
||||||
|
|
||||||
|
# 1. 安装 Railway CLI
|
||||||
|
npm install -g @railway/cli
|
||||||
|
|
||||||
|
# 2. 登录
|
||||||
|
railway login
|
||||||
|
|
||||||
|
# 3. 初始化项目
|
||||||
|
cd backend
|
||||||
|
railway init
|
||||||
|
|
||||||
|
# 4. 部署
|
||||||
|
railway up
|
||||||
|
|
||||||
|
# 5. 添加数据库
|
||||||
|
railway add postgresql
|
||||||
|
|
||||||
|
# 6. 设置环境变量
|
||||||
|
railway variables set GITHUB_CLIENT_ID=xxx
|
||||||
|
railway variables set GITHUB_CLIENT_SECRET=xxx
|
||||||
|
|
||||||
|
**Railway 配置文件 (`railway.json`)**: { "$schema": "https://railway.app/railway.schema.json", "build": { "builder": "NIXPACKS" }, "deploy": { "startCommand": "npm run start", "restartPolicyType": "ON_FAILURE", "restartPolicyMaxRetries": 10 } }
|
||||||
|
|
||||||
|
**优势:**
|
||||||
|
|
||||||
|
* ✅ 免费额度:$5/月(足够小项目)
|
||||||
|
* ✅ 一键部署,零配置
|
||||||
|
* ✅ 自动 HTTPS
|
||||||
|
* ✅ 内置 PostgreSQL
|
||||||
|
* ✅ 环境变量管理
|
||||||
|
* ✅ 自动 Git 同步部署
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
### **替代方案对比**
|
||||||
|
|
||||||
|
| 平台 | 免费额度 | 部署难度 | 启动速度 | 推荐度 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| **Railway** | $5/月 | ⭐⭐⭐⭐⭐ | 快 | ⭐⭐⭐⭐⭐ |
|
||||||
|
| Render | 750h/月 | ⭐⭐⭐⭐ | 慢(冷启动) | ⭐⭐⭐⭐ |
|
||||||
|
| Fly.io | 3个免费实例 | ⭐⭐⭐ | 快 | ⭐⭐⭐⭐ |
|
||||||
|
| Heroku | 无免费版 | ⭐⭐⭐⭐ | 中等 | ⭐⭐ |
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
💻 **核心代码示例**
|
||||||
|
|
||||||
|
### **后端 - Fastify 入口 (`backend/src/app.ts`)**
|
||||||
|
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import cors from '@fastify/cors';
|
||||||
|
import jwt from '@fastify/jwt';
|
||||||
|
import { authRoutes } from './routes/auth';
|
||||||
|
import { repoRoutes } from './routes/repos';
|
||||||
|
import { webhookRoutes } from './routes/webhooks';
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = Fastify({
|
||||||
|
logger: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 插件
|
||||||
|
app.register(cors, {
|
||||||
|
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.register(jwt, {
|
||||||
|
secret: process.env.JWT_SECRET!,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
app.register(authRoutes, { prefix: '/api/auth' });
|
||||||
|
app.register(repoRoutes, { prefix: '/api/repos' });
|
||||||
|
app.register(webhookRoutes, { prefix: '/api/webhooks' });
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
app.get('/health', async () => {
|
||||||
|
return { status: 'ok', timestamp: new Date().toISOString() };
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
### **后端 - GitHub Service (`backend/src/services/github.service.ts`)**
|
||||||
|
|
||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
import { Redis } from '@upstash/redis';
|
||||||
|
|
||||||
|
const redis = new Redis({
|
||||||
|
url: process.env.UPSTASH_REDIS_URL!,
|
||||||
|
token: process.env.UPSTASH_REDIS_TOKEN!,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class GitHubService {
|
||||||
|
private octokit: Octokit;
|
||||||
|
|
||||||
|
constructor(accessToken: string) {
|
||||||
|
this.octokit = new Octokit({ auth: accessToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRepositories() {
|
||||||
|
const cacheKey = 'repos:list';
|
||||||
|
|
||||||
|
// 尝试从缓存读取
|
||||||
|
const cached = await redis.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
// 从 GitHub API 获取
|
||||||
|
const { data } = await this.octokit.repos.listForAuthenticatedUser({
|
||||||
|
sort: 'updated',
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 缓存5分钟
|
||||||
|
await redis.set(cacheKey, data, { ex: 300 });
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRepositoryStats(owner: string, repo: string) {
|
||||||
|
const cacheKey = `repo:${owner}/${repo}:stats`;
|
||||||
|
const cached = await redis.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const [repoData, issues, pulls, commits] = await Promise.all([
|
||||||
|
this.octokit.repos.get({ owner, repo }),
|
||||||
|
this.octokit.issues.listForRepo({ owner, repo, state: 'open' }),
|
||||||
|
this.octokit.pulls.list({ owner, repo, state: 'open' }),
|
||||||
|
this.octokit.repos.listCommits({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
per_page: 100,
|
||||||
|
since: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
name: repoData.data.name,
|
||||||
|
stars: repoData.data.stargazers_count,
|
||||||
|
forks: repoData.data.forks_count,
|
||||||
|
openIssues: issues.data.length,
|
||||||
|
openPRs: pulls.data.length,
|
||||||
|
commitsLastMonth: commits.data.length,
|
||||||
|
language: repoData.data.language,
|
||||||
|
updatedAt: repoData.data.updated_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
await redis.set(cacheKey, stats, { ex: 300 });
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
### **前端 - API 客户端 (`frontend/src/lib/api.ts`)**
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 请求拦截器:添加 token
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 响应拦截器:处理错误
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default api;
|
||||||
|
|
||||||
|
### **前端 - 数据获取 Hook (`frontend/src/hooks/useRepos.ts`)**
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import api from '@/lib/api';
|
||||||
|
|
||||||
|
interface Repository {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
full_name: string;
|
||||||
|
description: string;
|
||||||
|
stargazers_count: number;
|
||||||
|
open_issues_count: number;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRepositories() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['repositories'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await api.get<Repository[]>('/repos');
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
staleTime: 5 * 60 * 1000, // 5分钟
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRepositoryStats(owner: string, repo: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['repo-stats', owner, repo],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await api.get(`/repos/${owner}/${repo}/stats`);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: !!owner && !!repo,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
🔐 **认证流程**
|
||||||
|
|
||||||
|
┌────────┐ ┌──────────┐ ┌─────────┐
|
||||||
|
│ 前端 │ │ 后端 │ │ GitHub │
|
||||||
|
└───┬────┘ └────┬─────┘ └────┬────┘
|
||||||
|
│ │ │
|
||||||
|
│ 1. 点击 "Login with GitHub" │ │
|
||||||
|
├──────────────────────────>│ │
|
||||||
|
│ │ │
|
||||||
|
│ 2. 重定向到 GitHub OAuth │ │
|
||||||
|
├───────────────────────────┼─────────────────────────>│
|
||||||
|
│ │ │
|
||||||
|
│ 3. 用户授权 │ │
|
||||||
|
│ │<──────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ 4. GitHub 回调 + code │ │
|
||||||
|
│<───────────────────────────┤ │
|
||||||
|
│ │ │
|
||||||
|
│ 5. 发送 code 到后端 │ │
|
||||||
|
├──────────────────────────>│ │
|
||||||
|
│ │ │
|
||||||
|
│ │ 6. 用 code 换 access_token│
|
||||||
|
│ ├─────────────────────────>│
|
||||||
|
│ │ │
|
||||||
|
│ │<──────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ 7. 返回 JWT token │ │
|
||||||
|
│<───────────────────────────┤ │
|
||||||
|
│ │ │
|
||||||
|
│ 8. 保存 token 到 localStorage │
|
||||||
|
│ │ │
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
📊 **数据流**
|
||||||
|
|
||||||
|
前端 Dashboard 页面加载:
|
||||||
|
|
||||||
|
1. 检查 localStorage 是否有 token
|
||||||
|
├─ 无 → 跳转登录页
|
||||||
|
└─ 有 → 继续
|
||||||
|
|
||||||
|
2. 发起并行请求(TanStack Query):
|
||||||
|
├─ GET /api/repos (获取仓库列表)
|
||||||
|
├─ GET /api/stats/overview (总体统计)
|
||||||
|
└─ GET /api/stats/activity (活动数据)
|
||||||
|
|
||||||
|
3. 后端处理:
|
||||||
|
├─ 验证 JWT token
|
||||||
|
├─ 检查 Redis 缓存
|
||||||
|
│ ├─ 命中 → 直接返回
|
||||||
|
│ └─ 未命中 → 调用 GitHub API + 写入缓存
|
||||||
|
└─ 返回数据
|
||||||
|
|
||||||
|
4. 前端渲染:
|
||||||
|
├─ TanStack Query 自动管理加载/错误状态
|
||||||
|
├─ 渲染图表(Recharts)
|
||||||
|
└─ 显示数据卡片
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
🎯 **开发步骤**
|
||||||
|
|
||||||
|
### **1. 初始化项目**
|
||||||
|
|
||||||
|
# 创建项目目录
|
||||||
|
mkdir github-dashboard
|
||||||
|
cd github-dashboard
|
||||||
|
|
||||||
|
# 初始化前端
|
||||||
|
npm create vite@latest frontend -- --template react-ts
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm install @tanstack/react-query axios zustand
|
||||||
|
npm install -D tailwindcss postcss autoprefixer
|
||||||
|
npx tailwindcss init -p
|
||||||
|
|
||||||
|
# 初始化后端
|
||||||
|
cd ..
|
||||||
|
mkdir backend && cd backend
|
||||||
|
npm init -y
|
||||||
|
npm install fastify @fastify/cors @fastify/jwt
|
||||||
|
npm install @octokit/rest @upstash/redis @prisma/client
|
||||||
|
npm install -D typescript @types/node tsx prisma
|
||||||
|
npx prisma init
|
||||||
|
|
||||||
|
### **2. 配置 Railway 部署**
|
||||||
|
|
||||||
|
cd backend
|
||||||
|
|
||||||
|
# 创建 railway.json
|
||||||
|
cat > railway.json << EOF
|
||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"builder": "NIXPACKS"
|
||||||
|
},
|
||||||
|
"deploy": {
|
||||||
|
"startCommand": "npm run start"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 部署
|
||||||
|
railway login
|
||||||
|
railway init
|
||||||
|
railway up
|
||||||
|
|
||||||
|
### **3. 配置 Cloudflare Pages**
|
||||||
|
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# 构建测试
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 在 Cloudflare Pages Dashboard:
|
||||||
|
# 1. 连接 GitHub 仓库
|
||||||
|
# 2. 设置构建命令: npm run build
|
||||||
|
# 3. 设置输出目录: dist
|
||||||
|
# 4. 添加环境变量: VITE_API_URL=你的Railway后端URL
|
||||||
|
|
||||||
|
* * *
|
||||||
|
|
||||||
|
🎉 **总结**
|
||||||
|
|
||||||
|
这个架构的优势:
|
||||||
|
|
||||||
|
✅ **前后端分离** - 各自独立部署和扩展✅ **轻量高效** - Fastify 性能优秀,Vite 开发体验好✅ **部署简单** - Railway + Cloudflare Pages 都是一键部署✅ **成本低廉** - 免费额度足够个人/小团队使用✅ **可扩展性** - 需要时可轻松升级到付费版✅ **现代化** - TypeScript 全栈,类型安全
|
||||||
|
|
||||||
|
需要我提供:
|
||||||
|
|
||||||
|
1. ✅ 完整的项目初始化脚本?
|
||||||
|
2. ✅ Prisma Schema 数据库设计?
|
||||||
|
3. ✅ 前端 Dashboard 组件代码?
|
||||||
|
4. ✅ 详细的部署文档?
|
||||||
|
|
||||||
|
选择任意方向深入!🚀
|
||||||
715
package-lock.json
generated
715
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -6,7 +6,7 @@
|
|||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node-dev --respawn --transpile-only --exit-child src/server.ts",
|
"dev": "ts-node-dev --respawn --transpile-only --exit-child src/server.ts",
|
||||||
"build": "tsc",
|
"build": "prisma generate && tsc",
|
||||||
"start": "node dist/server.js",
|
"start": "node dist/server.js",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev"
|
"prisma:migrate": "prisma migrate dev"
|
||||||
@ -14,17 +14,22 @@
|
|||||||
"author": "Google LLC",
|
"author": "Google LLC",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.2.3",
|
|
||||||
"@fastify/cors": "^11.1.0",
|
"@fastify/cors": "^11.1.0",
|
||||||
"@fastify/jwt": "^10.0.0",
|
"@fastify/jwt": "^10.0.0",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@fastify/swagger": "^9.5.2",
|
||||||
|
"@octokit/rest": "^19.0.13",
|
||||||
"@prisma/client": "^6.18.0",
|
"@prisma/client": "^6.18.0",
|
||||||
"@upstash/redis": "^1.35.6",
|
"@upstash/redis": "^1.35.6",
|
||||||
"fastify": "^5.6.1"
|
"axios": "^1.12.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"fastify": "^5.6.1",
|
||||||
|
"fastify-type-provider-zod": "^6.0.0",
|
||||||
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^20.12.12",
|
||||||
"nodemon": "^3.1.0",
|
"nodemon": "^3.1.0",
|
||||||
|
"pino-pretty": "^13.1.2",
|
||||||
"prisma": "^6.18.0",
|
"prisma": "^6.18.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
|
|||||||
@ -1,43 +1,67 @@
|
|||||||
import { FastifyInstance } from 'fastify';
|
import { FastifyInstance } from 'fastify';
|
||||||
|
import { z } from 'zod';
|
||||||
import { GitHubService } from '../services/github.service';
|
import { GitHubService } from '../services/github.service';
|
||||||
import { authMiddleware } from '../middlewares/auth.middleware';
|
import { authMiddleware } from '../middlewares/auth.middleware';
|
||||||
|
|
||||||
// Schema for the URL parameters
|
// Schema for request parameters
|
||||||
const repoParamsSchema = {
|
const repoParamsSchema = z.object({
|
||||||
type: 'object',
|
owner: z.string(),
|
||||||
required: ['owner', 'repo'],
|
repo: z.string(),
|
||||||
properties: {
|
});
|
||||||
owner: { type: 'string' },
|
|
||||||
repo: { type: 'string' },
|
|
||||||
},
|
|
||||||
} as const; // Using 'as const' for stronger type inference
|
|
||||||
|
|
||||||
// Schema for the response of the repository stats endpoint
|
// Schema for repository statistics response
|
||||||
const repoStatsSchema = {
|
const repoStatsSchema = z.object({
|
||||||
type: 'object',
|
stars: z.number(),
|
||||||
properties: {
|
forks: z.number(),
|
||||||
stars: { type: 'number' },
|
openIssues: z.number(),
|
||||||
forks: { type: 'number' },
|
commits: z.number(),
|
||||||
openIssues: { type: 'number' },
|
releases: z.number(),
|
||||||
commits: { type: 'number' },
|
contributors: z.number(),
|
||||||
releases: { type: 'number' },
|
});
|
||||||
contributors: { type: 'number' },
|
|
||||||
},
|
// Schema for repository list response
|
||||||
} as const;
|
const repoListItemSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
full_name: z.string(),
|
||||||
|
private: z.boolean(),
|
||||||
|
html_url: z.string().url(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
stargazers_count: z.number(),
|
||||||
|
watchers_count: z.number(),
|
||||||
|
forks_count: z.number(),
|
||||||
|
language: z.string().nullable(),
|
||||||
|
owner: z.object({
|
||||||
|
login: z.string(),
|
||||||
|
avatar_url: z.string().url(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const reposListResponseSchema = z.array(repoListItemSchema);
|
||||||
|
|
||||||
export async function repoRoutes(app: FastifyInstance) {
|
export async function repoRoutes(app: FastifyInstance) {
|
||||||
|
// All routes in this plugin require authentication
|
||||||
app.addHook('preHandler', authMiddleware);
|
app.addHook('preHandler', authMiddleware);
|
||||||
|
|
||||||
// This route retrieves all repositories for the authenticated user.
|
// Route to list repositories for the authenticated user
|
||||||
// The response type is inferred from Octokit, so a detailed schema is omitted for brevity,
|
app.get(
|
||||||
// but in a real-world scenario, you might want to define the properties you actually use.
|
'/',
|
||||||
app.get('/', async (request, reply) => {
|
{
|
||||||
const githubService = new GitHubService(request.user.accessToken);
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: reposListResponseSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
const { accessToken, username } = request.user!;
|
||||||
|
const githubService = new GitHubService(accessToken, username);
|
||||||
const repos = await githubService.getRepositories();
|
const repos = await githubService.getRepositories();
|
||||||
reply.send(repos);
|
return repos;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// This route gets specific stats for a single repository.
|
// Route to get statistics for a specific repository
|
||||||
app.get(
|
app.get(
|
||||||
'/:owner/:repo/stats',
|
'/:owner/:repo/stats',
|
||||||
{
|
{
|
||||||
@ -49,13 +73,10 @@ export async function repoRoutes(app: FastifyInstance) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
// request.params is now strongly typed based on repoParamsSchema!
|
const { owner, repo } = request.params as z.infer<typeof repoParamsSchema>;
|
||||||
const { owner, repo } = request.params;
|
const { accessToken, username } = request.user!;
|
||||||
const githubService = new GitHubService(request.user.accessToken);
|
const githubService = new GitHubService(accessToken, username);
|
||||||
|
|
||||||
const stats = await githubService.getRepositoryStats(owner, repo);
|
const stats = await githubService.getRepositoryStats(owner, repo);
|
||||||
|
|
||||||
// The return type is validated against repoStatsSchema
|
|
||||||
return stats;
|
return stats;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,21 @@
|
|||||||
import { FastifyInstance } from 'fastify';
|
import { FastifyInstance } from 'fastify';
|
||||||
|
import { StatsService } from '../services/stats.service';
|
||||||
|
import { authMiddleware } from '../middlewares/auth.middleware';
|
||||||
|
|
||||||
export async function statsRoutes(app: FastifyInstance) {
|
export async function statsRoutes(app: FastifyInstance) {
|
||||||
// TODO: Implement stats routes
|
app.addHook('preHandler', authMiddleware);
|
||||||
|
|
||||||
|
app.get('/overview', async (request, reply) => {
|
||||||
|
const { accessToken, username } = request.user!;
|
||||||
|
const statsService = new StatsService(accessToken, username);
|
||||||
|
const overviewStats = await statsService.getOverviewStats();
|
||||||
|
reply.send(overviewStats);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/activity', async (request, reply) => {
|
||||||
|
const { accessToken, username } = request.user!;
|
||||||
|
const statsService = new StatsService(accessToken, username);
|
||||||
|
const activityStats = await statsService.getActivityStats();
|
||||||
|
reply.send(activityStats);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
129
src/server.ts
129
src/server.ts
@ -1,40 +1,123 @@
|
|||||||
import fastify from 'fastify';
|
import fastify from 'fastify';
|
||||||
import { authRoutes } from './routes/auth';
|
import {
|
||||||
import { webhookRoutes } from './routes/webhooks';
|
serializerCompiler,
|
||||||
import { repoRoutes } from './routes/repos';
|
validatorCompiler,
|
||||||
|
ZodTypeProvider
|
||||||
|
} from 'fastify-type-provider-zod';
|
||||||
import fastifyJwt from '@fastify/jwt';
|
import fastifyJwt from '@fastify/jwt';
|
||||||
import fastifyCors from '@fastify/cors';
|
import fastifyCors from '@fastify/cors';
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
|
|
||||||
declare module 'fastify' {
|
import { authRoutes } from './routes/auth';
|
||||||
interface FastifyRequest {
|
import { repoRoutes } from './routes/repos';
|
||||||
user?: {
|
import { statsRoutes } from './routes/stats';
|
||||||
sub: string;
|
import { webhookRoutes } from './routes/webhooks';
|
||||||
username: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
accessToken: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
import swagger from '@fastify/swagger';
|
||||||
|
|
||||||
|
// Main function to bootstrap the server
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
// Initialize Fastify with ZodTypeProvider
|
||||||
const app = fastify({
|
const app = fastify({
|
||||||
logger: true,
|
logger: {
|
||||||
});
|
transport: {
|
||||||
|
target: 'pino-pretty',
|
||||||
|
options: {
|
||||||
|
translateTime: 'HH:MM:ss Z',
|
||||||
|
ignore: 'pid,hostname',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).withTypeProvider<ZodTypeProvider>();
|
||||||
|
|
||||||
app.register(fastifyJwt, {
|
// Set the validator and serializer for Zod
|
||||||
|
app.setValidatorCompiler(validatorCompiler);
|
||||||
|
app.setSerializerCompiler(serializerCompiler);
|
||||||
|
|
||||||
|
// Register plugins
|
||||||
|
await app.register(fastifyJwt, {
|
||||||
secret: process.env.JWT_SECRET!,
|
secret: process.env.JWT_SECRET!,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.register(fastifyCors, {
|
await app.register(fastifyCors, {
|
||||||
origin: process.env.FRONTEND_URL,
|
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||||
|
credentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.register(authRoutes, { prefix: '/auth' });
|
// Register Swagger for OpenAPI spec generation
|
||||||
app.register(webhookRoutes, { prefix: '/webhooks' });
|
await app.register(swagger, {
|
||||||
app.register(repoRoutes, { prefix: '/repos' });
|
openapi: {
|
||||||
|
info: {
|
||||||
|
title: 'Project Dash API',
|
||||||
|
description: 'Backend API documentation for Project Dash application.',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
bearerAuth: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await app.listen({ port: 3333, host: '0.0.0.0' });
|
// --- API Documentation with RapiDoc ---
|
||||||
|
app.get('/docs', (req, reply) => {
|
||||||
|
reply.type('text/html').send(`
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<rapi-doc
|
||||||
|
spec-url="/docs/json"
|
||||||
|
theme="dark"
|
||||||
|
render-style="view"
|
||||||
|
show-header="false"
|
||||||
|
allow-server-selection="false"
|
||||||
|
allow-authentication="true"
|
||||||
|
> </rapi-doc>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/docs/json', (req, reply) => {
|
||||||
|
reply.send(app.swagger());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
await app.register(authRoutes, { prefix: '/auth' });
|
||||||
|
await app.register(repoRoutes, { prefix: '/repos' });
|
||||||
|
await app.register(statsRoutes, { prefix: '/stats' });
|
||||||
|
await app.register(webhookRoutes, { prefix: '/webhooks' });
|
||||||
|
|
||||||
|
// Add a generic error handler
|
||||||
|
app.setErrorHandler((error, request, reply) => {
|
||||||
|
app.log.error(error);
|
||||||
|
reply.status(500).send({ error: 'Internal Server Error' });
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
// Start the server
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const app = await bootstrap();
|
||||||
|
await app.listen({ port: 3333, host: '0.0.0.0' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import { GitHubUser } from '../types/github';
|
|||||||
const GITHUB_API_BASE_URL = 'https://api.github.com';
|
const GITHUB_API_BASE_URL = 'https://api.github.com';
|
||||||
|
|
||||||
// Define a type for the repository list response data for clarity
|
// Define a type for the repository list response data for clarity
|
||||||
type ReposListForAuthenticatedUserResponse = Endpoints['GET /user/repos']['response']['data'];
|
type ReposListForAuthenticatedUserResponse =
|
||||||
|
Endpoints['GET /user/repos']['response']['data'];
|
||||||
|
|
||||||
export async function exchangeCodeForToken(code: string): Promise<string> {
|
export async function exchangeCodeForToken(code: string): Promise<string> {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
@ -35,27 +36,31 @@ export async function getGithubUser(accessToken: string): Promise<GitHubUser> {
|
|||||||
|
|
||||||
export class GitHubService {
|
export class GitHubService {
|
||||||
private octokit: Octokit;
|
private octokit: Octokit;
|
||||||
|
private username: string;
|
||||||
|
|
||||||
constructor(accessToken: string) {
|
constructor(accessToken: string, username: string) {
|
||||||
this.octokit = new Octokit({ auth: accessToken });
|
this.octokit = new Octokit({ auth: accessToken });
|
||||||
|
this.username = username;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
|
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
|
||||||
|
const cacheKey = `repos:${this.username}`;
|
||||||
|
const cachedRepos = await redis.get(cacheKey);
|
||||||
|
|
||||||
|
if (cachedRepos) {
|
||||||
|
return JSON.parse(cachedRepos as string);
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await this.octokit.repos.listForAuthenticatedUser();
|
const { data } = await this.octokit.repos.listForAuthenticatedUser();
|
||||||
|
await redis.set(cacheKey, JSON.stringify(data), { ex: 3600 }); // Cache for 1 hour
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRepositoryStats(owner: string, repo: string) {
|
async getRepositoryStats(owner: string, repo: string, since?: string) {
|
||||||
const cacheKey = `repo-stats:${owner}:${repo}`;
|
|
||||||
const cachedStats = await redis.get(cacheKey);
|
|
||||||
|
|
||||||
if (cachedStats) {
|
|
||||||
return JSON.parse(cachedStats as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [repoData, commits, releases, contributors] = await Promise.all([
|
const [repoData, commits, releases, contributors] = await Promise.all([
|
||||||
this.octokit.repos.get({ owner, repo }),
|
this.octokit.repos.get({ owner, repo }),
|
||||||
this.octokit.repos.listCommits({ owner, repo }),
|
this.octokit.repos.listCommits({ owner, repo, since }),
|
||||||
this.octokit.repos.listReleases({ owner, repo }),
|
this.octokit.repos.listReleases({ owner, repo }),
|
||||||
this.octokit.repos.listContributors({ owner, repo }),
|
this.octokit.repos.listContributors({ owner, repo }),
|
||||||
]);
|
]);
|
||||||
@ -69,8 +74,6 @@ export class GitHubService {
|
|||||||
contributors: contributors.data.length,
|
contributors: contributors.data.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
await redis.set(cacheKey, JSON.stringify(stats), { ex: 3600 }); // Cache for 1 hour
|
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/services/stats.service.ts
Normal file
42
src/services/stats.service.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { prisma } from '../lib/prisma';
|
||||||
|
import { GitHubService } from './github.service';
|
||||||
|
|
||||||
|
export class StatsService {
|
||||||
|
private githubService: GitHubService;
|
||||||
|
|
||||||
|
constructor(accessToken: string, username: string) {
|
||||||
|
this.githubService = new GitHubService(accessToken, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOverviewStats() {
|
||||||
|
const repos = await this.githubService.getRepositories();
|
||||||
|
const totalStars = repos.reduce((acc, repo) => acc + (repo.stargazers_count || 0), 0);
|
||||||
|
const totalForks = repos.reduce((acc, repo) => acc + (repo.forks_count || 0), 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalRepos: repos.length,
|
||||||
|
totalStars,
|
||||||
|
totalForks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActivityStats() {
|
||||||
|
const repos = await this.githubService.getRepositories();
|
||||||
|
const thirtyDaysAgo = new Date(
|
||||||
|
Date.now() - 30 * 24 * 60 * 60 * 1000,
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
const commitPromises = repos.map((repo) => {
|
||||||
|
const [owner, repoName] = repo.full_name.split('/');
|
||||||
|
return this.githubService.getRepositoryStats(owner, repoName, thirtyDaysAgo);
|
||||||
|
});
|
||||||
|
|
||||||
|
const allStats = await Promise.all(commitPromises);
|
||||||
|
|
||||||
|
const totalCommits = allStats.reduce((acc, stats) => acc + stats.commits, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCommitsLast30Days: totalCommits,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/types/fastify.d.ts
vendored
Normal file
14
src/types/fastify.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import '@fastify/jwt';
|
||||||
|
|
||||||
|
declare module '@fastify/jwt' {
|
||||||
|
interface FastifyJWT {
|
||||||
|
// This is the type for the payload that is signed
|
||||||
|
// and the type of the `request.user` object.
|
||||||
|
user: {
|
||||||
|
sub: string;
|
||||||
|
username: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
accessToken: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "ES2020", // More modern target
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"sourceMap": true,
|
"strict": true, // Enable all strict type-checking options
|
||||||
"outDir": "dist",
|
"esModuleInterop": true,
|
||||||
"esModuleInterop": true
|
"skipLibCheck": true, // Skip type checking of declaration files
|
||||||
}
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts", // Include all .ts files in the src directory
|
||||||
|
"src/types/**/*.d.ts" // Include our custom type definitions
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user