Implement initial project structure and setup
This commit is contained in:
parent
440ea5b463
commit
6e347224d3
|
|
@ -0,0 +1,12 @@
|
|||
## 빌드/배포 산출물(Deliverables)
|
||||
|
||||
- Electron Windows `exe` 빌드 산출물
|
||||
- Gitea 릴리즈 아티팩트 업로드
|
||||
- 릴리즈 노트(버전, 변경 사항, 설치/실행 가이드)
|
||||
|
||||
## 배포 흐름(Release Flow)
|
||||
|
||||
1. 버전 태깅(Version tag)
|
||||
2. Windows `exe` 빌드
|
||||
3. Gitea 릴리즈 생성 및 `exe` 업로드
|
||||
4. 릴리즈 노트 작성
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
## 목표(Goals)
|
||||
|
||||
- SPT 4.0.11 기준 커스텀 런처 제공
|
||||
- Windows 11 환경에서 Electron `exe`로 배포
|
||||
- 단일 Git repo + Git LFS 기반 모드 동기화
|
||||
- `SPT Launcher.exe`를 대체하여 타르코프 클라이언트 직접 실행
|
||||
|
||||
## 범위( Scope )
|
||||
|
||||
- 서버 상태 확인(연결/헬스 체크) UI 제공
|
||||
- 로그인(Login) UI 제공(인증 방식은 추후 확정)
|
||||
- 모드 동기화 UI 제공(동기화 정책은 추후 확정)
|
||||
- 클라이언트 실행 버튼 제공(경로 탐지는 추후 확정)
|
||||
|
||||
## 고정 전제(Fixed Assumptions)
|
||||
|
||||
- 서버 URL은 빌드 시점에 고정: `http://pandoli365.com:5069/`
|
||||
- 모드 Git URL: `https://gitea.pandoli365.com/art/spt-mods`
|
||||
- 배포는 Gitea 릴리즈에서 `exe` 제공
|
||||
- 패키지 매니저: Yarn
|
||||
|
||||
## 정책(Policies, UI 기준)
|
||||
|
||||
- 로그인 상태는 UI에서 명확히 표시
|
||||
- 서버 상태는 Health 체크 결과를 상태 배지로 표시
|
||||
- 모드 동기화는 상태/진행률 표시, 실패 시 재시도 안내
|
||||
- 실행 버튼은 사전 조건(로그인/서버/동기화) 충족 여부 표시
|
||||
|
||||
## 범위 제외(Out of Scope)
|
||||
|
||||
- 서버/모드 동기화 상세 로직(추후 결정)
|
||||
- 인증 스키마/보안 저장소 구현(추후 결정)
|
||||
- 자동 업데이트 정책(추후 결정)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
## Electron 구조(Architecture)
|
||||
|
||||
- main/renderer 분리
|
||||
- IPC 채널은 최소화하고 명시적 요청/응답 형태로 설계
|
||||
|
||||
## 저장소/보안(Storage/Security)
|
||||
|
||||
- 로그인 토큰 저장 위치는 OS keychain 또는 암호화 파일 후보
|
||||
- 최종 결정은 인증 방식 확정 후 진행
|
||||
|
||||
## Git/Git LFS 처리
|
||||
|
||||
- 기본 전략: 시스템에 Git/Git LFS가 없으면 설치 안내 UI 표시
|
||||
- 번들/자동 설치 여부는 사용자 합의 후 결정
|
||||
|
||||
## 동기화 방식
|
||||
|
||||
- 단일 repo + LFS 기반
|
||||
- 동기화 트리거: 앱 시작 시 확인 + 수동 동기화 버튼
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
## 화면 목록(Screens)
|
||||
|
||||
1. **홈(Home)**
|
||||
- 서버 상태(Health)
|
||||
- 로그인 상태
|
||||
- 모드 동기화 상태
|
||||
- 게임 실행 버튼
|
||||
2. **로그인(Login)**
|
||||
- 계정 입력/로그인 버튼
|
||||
- 실패 메시지 영역
|
||||
3. **모드 동기화(Mod Sync)**
|
||||
- 최신 버전 표시
|
||||
- 동기화 진행률
|
||||
- 로그/오류 출력
|
||||
4. **설정(Settings)**
|
||||
- 서버 URL(읽기 전용, 빌드 고정)
|
||||
- 모드 Git URL(읽기 전용, 빌드 고정)
|
||||
- 로그 폴더 열기
|
||||
|
||||
## 핵심 플로우(Flows)
|
||||
|
||||
### 앱 시작(App Launch)
|
||||
|
||||
1. 서버 상태(Health) 체크
|
||||
2. 로그인 상태 확인(토큰 존재 여부 등)
|
||||
3. 모드 상태 표시(로컬/원격 버전 비교는 추후 결정)
|
||||
4. 사용자가 필요한 동작(로그인/동기화) 수행 후 실행
|
||||
|
||||
### 로그인(Login)
|
||||
|
||||
1. 로그인 화면 진입
|
||||
2. 자격 증명 입력
|
||||
3. 성공 시 홈으로 복귀, 실패 시 메시지 표시
|
||||
|
||||
### 모드 동기화(Sync)
|
||||
|
||||
1. 모드 동기화 화면 진입
|
||||
2. 진행률 표시 및 결과 표시
|
||||
3. 실패 시 재시도 제공
|
||||
|
||||
### 게임 실행(Launch)
|
||||
|
||||
1. 사전 조건 확인(서버/로그인/동기화)
|
||||
2. 클라이언트 실행
|
||||
3. 실패 시 오류 메시지 표시
|
||||
|
||||
## 상태/메시지 기준(States)
|
||||
|
||||
- **서버 상태**: 정상/지연/불가
|
||||
- **로그인 상태**: 로그인됨/로그아웃됨/오류
|
||||
- **동기화 상태**: 최신/진행중/실패
|
||||
- **실행 상태**: 준비됨/차단됨/실행중
|
||||
|
||||
## UX 원칙(UX Principles)
|
||||
|
||||
- 홈에서 현재 상태와 다음 행동을 즉시 알 수 있어야 함
|
||||
- 실패는 즉시 이유와 해결 방법을 제시
|
||||
- 동작 중에는 진행률과 남은 작업을 보여줌
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "spt-launcher",
|
||||
"version": "0.1.0",
|
||||
"description": "SPT 4.0.11 custom launcher",
|
||||
"main": "dist/main/main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently -k \"vite\" \"wait-on http://localhost:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .\"",
|
||||
"build:renderer": "vite build",
|
||||
"build:main": "tsc -p tsconfig.main.json",
|
||||
"build": "yarn build:renderer && yarn build:main",
|
||||
"pack": "yarn build && electron-builder --dir",
|
||||
"dist": "yarn build && electron-builder"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron": "^30.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron-builder": "^24.13.3",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.12",
|
||||
"wait-on": "^8.0.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.spt.launcher",
|
||||
"productName": "SPT Launcher",
|
||||
"files": [
|
||||
"dist/**",
|
||||
"package.json"
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "build"
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { app, BrowserWindow } from "electron";
|
||||
import path from "node:path";
|
||||
|
||||
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
|
||||
|
||||
const createWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1100,
|
||||
height: 720,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "../preload/preload.js")
|
||||
}
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL as string);
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
||||
}
|
||||
};
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import { contextBridge } from "electron";
|
||||
|
||||
contextBridge.exposeInMainWorld("sptLauncher", {
|
||||
appName: "SPT Launcher"
|
||||
});
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
type Screen = "serverCheck" | "login" | "signup" | "resetPassword" | "main";
|
||||
|
||||
const App = () => {
|
||||
const [screen, setScreen] = useState<Screen>("serverCheck");
|
||||
const [serverHealthy, setServerHealthy] = useState(true);
|
||||
const [hasSession, setHasSession] = useState(false);
|
||||
const [syncInProgress, setSyncInProgress] = useState(false);
|
||||
const [simulateSyncFail, setSimulateSyncFail] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (screen !== "serverCheck") {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setServerHealthy(true);
|
||||
setScreen(hasSession ? "main" : "login");
|
||||
}, 900);
|
||||
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [screen, hasSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (screen !== "main") {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
setServerHealthy((current) => current);
|
||||
}, 5000);
|
||||
|
||||
return () => window.clearInterval(interval);
|
||||
}, [screen]);
|
||||
|
||||
const serverStatusLabel = useMemo(() => {
|
||||
return serverHealthy ? "정상" : "불가";
|
||||
}, [serverHealthy]);
|
||||
|
||||
const handleLogin = () => {
|
||||
setHasSession(true);
|
||||
setScreen("main");
|
||||
};
|
||||
|
||||
const handleSignup = () => {
|
||||
window.alert("회원가입 요청이 접수되었습니다. 관리자 승인 후 이용 가능합니다.");
|
||||
setScreen("login");
|
||||
};
|
||||
|
||||
const handlePasswordReset = () => {
|
||||
window.alert("비밀번호 변경 요청이 접수되었습니다.");
|
||||
setScreen("login");
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
setHasSession(false);
|
||||
setScreen("login");
|
||||
};
|
||||
|
||||
const handleLaunch = () => {
|
||||
if (syncInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSyncInProgress(true);
|
||||
|
||||
window.setTimeout(() => {
|
||||
setSyncInProgress(false);
|
||||
|
||||
if (!simulateSyncFail) {
|
||||
window.alert("모드 동기화 성공. 게임을 실행합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const proceed = window.confirm(
|
||||
"모드 동기화가 실패했습니다. 그래도 실행할까요?"
|
||||
);
|
||||
|
||||
if (proceed) {
|
||||
window.alert("실패 상태로 게임을 실행합니다.");
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="app-header">
|
||||
<h1>SPT Launcher</h1>
|
||||
</header>
|
||||
|
||||
{screen === "serverCheck" && (
|
||||
<section className="card hero">
|
||||
<h2>서버 상태 확인 중</h2>
|
||||
<p className="status idle">체크 중...</p>
|
||||
<p className="helper">서버 상태 확인 후 로그인 화면으로 이동합니다.</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{screen === "login" && (
|
||||
<section className="card hero">
|
||||
<h2>로그인</h2>
|
||||
<p className="status idle">로그인 필요</p>
|
||||
<div className="login-form">
|
||||
<input type="text" placeholder="아이디" />
|
||||
<input type="password" placeholder="비밀번호" />
|
||||
</div>
|
||||
<button type="button" onClick={handleLogin}>
|
||||
로그인
|
||||
</button>
|
||||
<div className="link-row">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => setScreen("signup")}
|
||||
>
|
||||
회원가입
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => setScreen("resetPassword")}
|
||||
>
|
||||
비밀번호 변경
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => setHasSession(true)}
|
||||
>
|
||||
유효 세션 가정(데모)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{screen === "signup" && (
|
||||
<section className="card hero">
|
||||
<h2>회원가입</h2>
|
||||
<p className="status idle">관리자 승인 필요</p>
|
||||
<p className="notice">
|
||||
회원가입 후 서버 관리자 승인이 완료되어야 로그인 가능합니다.
|
||||
</p>
|
||||
<div className="login-form">
|
||||
<input type="text" placeholder="아이디" />
|
||||
<input type="password" placeholder="비밀번호" />
|
||||
<input type="password" placeholder="비밀번호 확인" />
|
||||
</div>
|
||||
<button type="button" onClick={handleSignup}>
|
||||
회원가입 요청
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={() => setScreen("login")}>
|
||||
로그인으로 돌아가기
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{screen === "resetPassword" && (
|
||||
<section className="card hero">
|
||||
<h2>비밀번호 변경</h2>
|
||||
<p className="status idle">본인 확인 필요</p>
|
||||
<div className="login-form">
|
||||
<input type="text" placeholder="아이디" />
|
||||
<input type="password" placeholder="기존 비밀번호" />
|
||||
<input type="password" placeholder="새 비밀번호" />
|
||||
<input type="password" placeholder="새 비밀번호 확인" />
|
||||
</div>
|
||||
<button type="button" onClick={handlePasswordReset}>
|
||||
비밀번호 변경 요청
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={() => setScreen("login")}>
|
||||
로그인으로 돌아가기
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{screen === "main" && (
|
||||
<main className="main-layout">
|
||||
<section className="card profile">
|
||||
<div>
|
||||
<h2>프로필 정보</h2>
|
||||
<p className="muted">닉네임: Pandoli</p>
|
||||
<p className="muted">레벨: 45</p>
|
||||
</div>
|
||||
<div className="profile-actions">
|
||||
<button type="button">프로필 다운로드</button>
|
||||
<button type="button" className="ghost">
|
||||
프로필 리셋
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card action">
|
||||
<h2>게임 시작</h2>
|
||||
<p className="status ok">준비됨</p>
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={simulateSyncFail}
|
||||
onChange={(event) => setSimulateSyncFail(event.target.checked)}
|
||||
/>
|
||||
모드 동기화 실패 시뮬레이션
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLaunch}
|
||||
disabled={syncInProgress}
|
||||
>
|
||||
{syncInProgress ? "동기화 중..." : "게임 시작"}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<footer className="footer">
|
||||
<div className="footer-left">
|
||||
<span className="label">서버 상태</span>
|
||||
<span className={`status ${serverHealthy ? "ok" : "blocked"}`}>
|
||||
{serverStatusLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="footer-right">
|
||||
<button type="button" className="ghost" onClick={handleLogout}>
|
||||
로그아웃
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>SPT Launcher</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./styles.css";
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
|
||||
if (rootElement) {
|
||||
createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
background: #0f1218;
|
||||
color: #f5f7fb;
|
||||
}
|
||||
|
||||
.app {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #171b22;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid #242a35;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.status.ok {
|
||||
background: #1f3d2b;
|
||||
color: #8ee3a5;
|
||||
}
|
||||
|
||||
.status.idle {
|
||||
background: #2b2f3a;
|
||||
color: #c0c7d4;
|
||||
}
|
||||
|
||||
.status.blocked {
|
||||
background: #3a2525;
|
||||
color: #f2a7a7;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2c3340;
|
||||
background: #0f1218;
|
||||
color: #f5f7fb;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #7c8597;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #2c78ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2566db;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
background: #2b3a55;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
background: transparent;
|
||||
border: 1px solid #2c78ff;
|
||||
color: #b8d1ff;
|
||||
}
|
||||
|
||||
.ghost:hover {
|
||||
background: #17233a;
|
||||
}
|
||||
|
||||
.hero {
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.helper {
|
||||
color: #a8b0c1;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 12px 0 16px;
|
||||
}
|
||||
|
||||
.link-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
color: #d5d9e6;
|
||||
background: #202635;
|
||||
border: 1px solid #2f3646;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
}
|
||||
|
||||
.profile {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #141923;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #242a35;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #a8b0c1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #a8b0c1;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface Window {
|
||||
sptLauncher: {
|
||||
appName: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["DOM", "ES2020"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src/renderer/**/*", "src/renderer/vite-env.d.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"lib": ["ES2020"],
|
||||
"types": ["node", "electron"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src/main/**/*", "src/preload/**/*"]
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "node:path";
|
||||
|
||||
export default defineConfig({
|
||||
root: path.resolve(__dirname, "src/renderer"),
|
||||
plugins: [react()],
|
||||
base: "./",
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "dist/renderer"),
|
||||
emptyOutDir: true
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
strictPort: true
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue