mirror of
https://github.com/dtzp555-max/odo.git
synced 2026-07-19 09:44:37 +00:00
feat: Phase 1 scaffold — React + Vite + TypeScript + Dashboard (v0.10.0)
- Add Vite + React + TypeScript + Tailwind CSS to Electron app - Create Dashboard page with gateway control (start/stop/restart/status) - Expand preload.js IPC bridge with gateway and OCM API channels - Add IPC handlers in main.js for openclaw CLI and OCM proxy - Dev mode loads Vite dev server, production loads built dist/ - Bump version to 0.10.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { app, BrowserWindow, Tray, Menu, nativeImage, dialog } = require('electron');
|
const { app, BrowserWindow, Tray, Menu, nativeImage, dialog, ipcMain } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { fork } = require('child_process');
|
const { fork, execFile } = require('child_process');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
// ── Dev Mode Detection ──────────────────────────────────────
|
||||||
|
const isDev = !app.isPackaged;
|
||||||
|
|
||||||
// ── State ────────────────────────────────────────────────────
|
// ── State ────────────────────────────────────────────────────
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
@@ -58,7 +62,7 @@ function startServer() {
|
|||||||
}).then(({ response }) => {
|
}).then(({ response }) => {
|
||||||
if (response === 0) {
|
if (response === 0) {
|
||||||
startServer().then(() => {
|
startServer().then(() => {
|
||||||
if (mainWindow) mainWindow.loadURL(`http://127.0.0.1:${serverPort}`);
|
if (mainWindow) mainWindow.reload();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
app.quit();
|
app.quit();
|
||||||
@@ -102,7 +106,12 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.loadURL(`http://127.0.0.1:${serverPort}`);
|
// Load the React UI (Vite dev server in dev, built files in production)
|
||||||
|
if (isDev) {
|
||||||
|
mainWindow.loadURL('http://localhost:5173');
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(path.join(__dirname, 'dist', 'index.html'));
|
||||||
|
}
|
||||||
|
|
||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-show', () => {
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
@@ -166,7 +175,7 @@ function createTray() {
|
|||||||
click: async () => {
|
click: async () => {
|
||||||
stopServer();
|
stopServer();
|
||||||
await startServer();
|
await startServer();
|
||||||
if (mainWindow) mainWindow.loadURL(`http://127.0.0.1:${serverPort}`);
|
if (mainWindow) mainWindow.reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
@@ -202,6 +211,59 @@ function createTray() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── IPC Handlers ────────────────────────────────────────────
|
||||||
|
|
||||||
|
function runOpenclawCommand(args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
execFile('openclaw', args, { timeout: 30000 }, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
resolve(stderr || error.message);
|
||||||
|
} else {
|
||||||
|
resolve(stdout || 'OK');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxyOcmApi(method, urlPath, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: serverPort,
|
||||||
|
path: urlPath,
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => (data += chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
} catch {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => reject(err.message));
|
||||||
|
|
||||||
|
if (body && method.toUpperCase() !== 'GET') {
|
||||||
|
req.write(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('gateway:status', () => runOpenclawCommand(['gateway', 'status']));
|
||||||
|
ipcMain.handle('gateway:start', () => runOpenclawCommand(['gateway', 'start']));
|
||||||
|
ipcMain.handle('gateway:stop', () => runOpenclawCommand(['gateway', 'stop']));
|
||||||
|
ipcMain.handle('gateway:restart', () => runOpenclawCommand(['gateway', 'restart']));
|
||||||
|
ipcMain.handle('ocm:api', (_event, { method, path: urlPath, body }) =>
|
||||||
|
proxyOcmApi(method, urlPath, body)
|
||||||
|
);
|
||||||
|
|
||||||
// ── App Lifecycle ────────────────────────────────────────────
|
// ── App Lifecycle ────────────────────────────────────────────
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Generated
+2731
-4
File diff suppressed because it is too large
Load Diff
+22
-6
@@ -1,14 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "odo",
|
"name": "odo",
|
||||||
"version": "0.9.6",
|
"version": "0.10.0",
|
||||||
"description": "ODO — OpenClaw Dashboard Orchestrator",
|
"description": "ODO — OpenClaw Dashboard Orchestrator",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"build": "electron-builder",
|
"dev": "concurrently -k \"vite --config vite.config.ts\" \"sleep 3 && electron .\"",
|
||||||
"build:mac": "electron-builder --mac",
|
"build:renderer": "vite build --config vite.config.ts",
|
||||||
"build:win": "electron-builder --win",
|
"build": "npm run build:renderer && electron-builder",
|
||||||
"build:linux": "electron-builder --linux"
|
"build:mac": "npm run build:renderer && electron-builder --mac",
|
||||||
|
"build:win": "npm run build:renderer && electron-builder --win",
|
||||||
|
"build:linux": "npm run build:renderer && electron-builder --linux"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.openclaw.odo",
|
"appId": "com.openclaw.odo",
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
"main.js",
|
"main.js",
|
||||||
"preload.js",
|
"preload.js",
|
||||||
"server/openclaw-manager.js",
|
"server/openclaw-manager.js",
|
||||||
|
"dist/**/*",
|
||||||
"assets/**/*"
|
"assets/**/*"
|
||||||
],
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
@@ -40,8 +43,21 @@
|
|||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"concurrently": "^9.1.0",
|
||||||
"electron": "^33.0.0",
|
"electron": "^33.0.0",
|
||||||
"electron-builder": "^25.0.0"
|
"electron-builder": "^25.0.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
+15
-6
@@ -1,12 +1,21 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Preload script — runs in renderer before page loads
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
// Currently minimal; will be expanded for IPC bridge in future phases
|
|
||||||
|
|
||||||
const { contextBridge } = require('electron');
|
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('odo', {
|
contextBridge.exposeInMainWorld('odo', {
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
version: '0.9.6',
|
version: '0.10.0',
|
||||||
isElectron: true
|
isElectron: true,
|
||||||
|
|
||||||
|
gateway: {
|
||||||
|
status: () => ipcRenderer.invoke('gateway:status'),
|
||||||
|
start: () => ipcRenderer.invoke('gateway:start'),
|
||||||
|
stop: () => ipcRenderer.invoke('gateway:stop'),
|
||||||
|
restart: () => ipcRenderer.invoke('gateway:restart'),
|
||||||
|
},
|
||||||
|
|
||||||
|
ocm: {
|
||||||
|
api: (method, path, body) =>
|
||||||
|
ipcRenderer.invoke('ocm:api', { method, path, body }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import Dashboard from './pages/Dashboard';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// Router placeholder — will add react-router in a future phase
|
||||||
|
return <Dashboard />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for dark theme */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #0f1117;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #2a2d3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #3a3d4a;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' http://127.0.0.1:* http://localhost:* ws://localhost:*" />
|
||||||
|
<title>ODO</title>
|
||||||
|
</head>
|
||||||
|
<body class="bg-odo-bg text-odo-text">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
type GatewayStatus = 'unknown' | 'checking' | 'running' | 'stopped' | 'error';
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
const [gwStatus, setGwStatus] = useState<GatewayStatus>('unknown');
|
||||||
|
const [gwOutput, setGwOutput] = useState('');
|
||||||
|
const [actionInProgress, setActionInProgress] = useState(false);
|
||||||
|
|
||||||
|
const isElectron = typeof window !== 'undefined' && window.odo?.isElectron;
|
||||||
|
|
||||||
|
const checkStatus = useCallback(async () => {
|
||||||
|
if (!isElectron) {
|
||||||
|
setGwStatus('unknown');
|
||||||
|
setGwOutput('Not running in Electron');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setGwStatus('checking');
|
||||||
|
try {
|
||||||
|
const result = await window.odo.gateway.status();
|
||||||
|
// Parse the output to determine status
|
||||||
|
if (result.toLowerCase().includes('running') || result.toLowerCase().includes('active')) {
|
||||||
|
setGwStatus('running');
|
||||||
|
} else {
|
||||||
|
setGwStatus('stopped');
|
||||||
|
}
|
||||||
|
setGwOutput(result);
|
||||||
|
} catch (err) {
|
||||||
|
setGwStatus('error');
|
||||||
|
setGwOutput(String(err));
|
||||||
|
}
|
||||||
|
}, [isElectron]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkStatus();
|
||||||
|
// Poll every 15 seconds
|
||||||
|
const interval = setInterval(checkStatus, 15000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [checkStatus]);
|
||||||
|
|
||||||
|
const handleAction = async (action: 'start' | 'stop' | 'restart') => {
|
||||||
|
if (!isElectron) return;
|
||||||
|
setActionInProgress(true);
|
||||||
|
try {
|
||||||
|
const result = await window.odo.gateway[action]();
|
||||||
|
setGwOutput(result);
|
||||||
|
// Re-check status after action
|
||||||
|
setTimeout(checkStatus, 1000);
|
||||||
|
} catch (err) {
|
||||||
|
setGwOutput(String(err));
|
||||||
|
} finally {
|
||||||
|
setActionInProgress(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusColor: Record<GatewayStatus, string> = {
|
||||||
|
unknown: 'bg-gray-500',
|
||||||
|
checking: 'bg-yellow-500 animate-pulse',
|
||||||
|
running: 'bg-green-500',
|
||||||
|
stopped: 'bg-red-500',
|
||||||
|
error: 'bg-red-700',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-odo-bg p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-white tracking-tight">ODO</h1>
|
||||||
|
<p className="text-odo-text-dim text-sm mt-1">
|
||||||
|
OpenClaw Dashboard Orchestrator
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Gateway Control */}
|
||||||
|
<section className="bg-odo-surface border border-odo-border rounded-lg p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-white mb-4">
|
||||||
|
Gateway Control
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Status indicator */}
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<span
|
||||||
|
className={`inline-block w-3 h-3 rounded-full ${statusColor[gwStatus]}`}
|
||||||
|
/>
|
||||||
|
<span className="text-odo-text capitalize">{gwStatus}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex gap-3 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => handleAction('start')}
|
||||||
|
disabled={actionInProgress || !isElectron}
|
||||||
|
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAction('stop')}
|
||||||
|
disabled={actionInProgress || !isElectron}
|
||||||
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAction('restart')}
|
||||||
|
disabled={actionInProgress || !isElectron}
|
||||||
|
className="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={checkStatus}
|
||||||
|
disabled={!isElectron}
|
||||||
|
className="px-4 py-2 bg-odo-border hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Output */}
|
||||||
|
{gwOutput && (
|
||||||
|
<pre className="bg-odo-bg border border-odo-border rounded p-3 text-xs text-odo-text-dim overflow-x-auto whitespace-pre-wrap max-h-40">
|
||||||
|
{gwOutput}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Agent Overview (placeholder) */}
|
||||||
|
<section className="bg-odo-surface border border-odo-border rounded-lg p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-white mb-4">
|
||||||
|
Agent Overview
|
||||||
|
</h2>
|
||||||
|
<p className="text-odo-text-dim text-sm">
|
||||||
|
Agent management will be available in a future update.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Stats Summary (placeholder) */}
|
||||||
|
<section className="bg-odo-surface border border-odo-border rounded-lg p-6 lg:col-span-2">
|
||||||
|
<h2 className="text-lg font-semibold text-white mb-4">
|
||||||
|
Stats Summary
|
||||||
|
</h2>
|
||||||
|
<p className="text-odo-text-dim text-sm">
|
||||||
|
System statistics and metrics will be available in a future update.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="mt-8 text-center text-odo-text-dim text-xs">
|
||||||
|
ODO v{window.odo?.version ?? '0.10.0'} · OpenClaw Dashboard
|
||||||
|
Orchestrator
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface OdoAPI {
|
||||||
|
platform: string;
|
||||||
|
version: string;
|
||||||
|
isElectron: boolean;
|
||||||
|
gateway: {
|
||||||
|
status: () => Promise<string>;
|
||||||
|
start: () => Promise<string>;
|
||||||
|
stop: () => Promise<string>;
|
||||||
|
restart: () => Promise<string>;
|
||||||
|
};
|
||||||
|
ocm: {
|
||||||
|
api: (method: string, path: string, body?: unknown) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
odo: OdoAPI;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
odo: {
|
||||||
|
bg: '#0f1117',
|
||||||
|
surface: '#1a1d27',
|
||||||
|
border: '#2a2d3a',
|
||||||
|
accent: '#6366f1',
|
||||||
|
'accent-hover': '#818cf8',
|
||||||
|
text: '#e2e8f0',
|
||||||
|
'text-dim': '#94a3b8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
root: 'src',
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
outDir: '../dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user