Initial Commit
This commit is contained in:
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Electron & Tauri React/FastAPI Boilerplates
|
||||||
|
|
||||||
|
Two bootstrap scripts that generate a React (Vite) + FastAPI + SQLite stack, one packaged with Electron and one with Tauri.
|
||||||
|
Good for creating Full Stack web applications, with a Python backend, especially good for the use of machine learning models, a SQLite Database for maximum portability and readability, and a JavaScript frontend, for the best UI / UX designs possible.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
- `electron_boilerplate.sh` – scaffolds Electron + React + FastAPI, installs deps, and launches the app. Includes a `run.sh` helper in the generated project to start backend, Vite dev server, and Electron together.
|
||||||
|
- `tauri_boilerplate.sh` – scaffolds Tauri + React + FastAPI, installs deps, and launches the app. Generated `run.sh` starts the backend and `tauri dev`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- Node.js and npm
|
||||||
|
- Python 3 with venv support
|
||||||
|
- For Tauri: Rust toolchain (`cargo`) and platform-specific Tauri prerequisites
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```bash
|
||||||
|
./electron_boilerplate.sh [project-name]
|
||||||
|
./tauri_boilerplate.sh [project-name]
|
||||||
|
```
|
||||||
|
- If no name is provided, the script prompts for one (letters, numbers, `_`, `-`).
|
||||||
|
- Scripts create backend venv, install backend/frontend deps, then auto-run the app.
|
||||||
|
- Generated projects include:
|
||||||
|
- FastAPI backend with SQLite, REST + WebSocket, `uvicorn --reload`
|
||||||
|
- React + Vite frontend with ESLint and WS demo
|
||||||
|
- README and `run.sh` for one-command dev start
|
||||||
|
- Electron window title uses the project name; Tauri window/product name and identifier use the project name.
|
||||||
478
electron_boilerplate.sh
Executable file
478
electron_boilerplate.sh
Executable file
@@ -0,0 +1,478 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [project-name]"
|
||||||
|
echo "If no project name is given, you will be prompted."
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_name() {
|
||||||
|
[[ "$1" =~ ^[A-Za-z0-9_-]+$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1-}" == "-h" || "${1-}" == "--help" ]]; then
|
||||||
|
usage; exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJ="${1-}"
|
||||||
|
|
||||||
|
if [[ -n "$PROJ" ]] && ! valid_name "$PROJ"; then
|
||||||
|
echo "Invalid project name: $PROJ (use letters, numbers, hyphen, underscore)"
|
||||||
|
PROJ=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [[ -z "$PROJ" ]]; do
|
||||||
|
read -r -p "Enter project name (letters, numbers, -, _ only): " PROJ
|
||||||
|
if [[ -z "$PROJ" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! valid_name "$PROJ"; then
|
||||||
|
echo "Invalid project name. Use letters, numbers, hyphen, underscore."
|
||||||
|
PROJ=""
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ">> Creating Electron+React+FastAPI boilerplate in '$PROJ' ..."
|
||||||
|
|
||||||
|
# --- Directory Structure ---
|
||||||
|
mkdir -p "$PROJ/backend"
|
||||||
|
mkdir -p "$PROJ/frontend"
|
||||||
|
|
||||||
|
# --- Backend: Python FastAPI+SQLite ---
|
||||||
|
cat > "$PROJ/backend/requirements.txt" <<EOF
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$PROJ/backend/db.py" <<EOF
|
||||||
|
import sqlite3, os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB_FILE = os.environ.get("WS_DB_PATH", "appdata.db")
|
||||||
|
|
||||||
|
def get_conn():
|
||||||
|
conn = sqlite3.connect(DB_FILE, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = get_conn()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS likes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
artist TEXT,
|
||||||
|
album TEXT,
|
||||||
|
url TEXT,
|
||||||
|
duration INTEGER,
|
||||||
|
is_new INTEGER DEFAULT 1,
|
||||||
|
last_seen TIMESTAMP
|
||||||
|
)''')
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_likes(is_new=None):
|
||||||
|
conn = get_conn()
|
||||||
|
c = conn.cursor()
|
||||||
|
if is_new is None:
|
||||||
|
rows = c.execute('SELECT * FROM likes ORDER BY last_seen DESC').fetchall()
|
||||||
|
else:
|
||||||
|
rows = c.execute('SELECT * FROM likes WHERE is_new=? ORDER BY last_seen DESC', (1 if is_new else 0,)).fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
def add_demo_like():
|
||||||
|
conn = get_conn()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR IGNORE INTO likes (id, title, artist, album, url, duration, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
("track1", "Test Track", "Some Artist", "Test Album", "http://test", 123, datetime.utcnow())
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$PROJ/backend/main.py" <<EOF
|
||||||
|
from fastapi import FastAPI, WebSocket
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from starlette.websockets import WebSocketDisconnect
|
||||||
|
import db
|
||||||
|
|
||||||
|
app = FastAPI(title="$PROJ – React, FastAPI, SQLite, WebSocket")
|
||||||
|
db.init_db()
|
||||||
|
db.add_demo_like()
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/likes/all")
|
||||||
|
def api_all_likes():
|
||||||
|
return db.get_likes()
|
||||||
|
|
||||||
|
@app.get("/likes/new")
|
||||||
|
def api_new_likes():
|
||||||
|
return db.get_likes(is_new=True)
|
||||||
|
|
||||||
|
# Example WebSocket endpoint (send a message on connection)
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(ws: WebSocket):
|
||||||
|
await ws.accept()
|
||||||
|
try:
|
||||||
|
await ws.send_json({"type": "hello", "msg": "WebSocket is live!"})
|
||||||
|
while True:
|
||||||
|
data = await ws.receive_text()
|
||||||
|
await ws.send_json({"type": "echo", "data": data})
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
# Client disconnected; exit quietly.
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
# Any other exception: close connection gracefully.
|
||||||
|
await ws.close()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- Frontend: Vite+React+ESLint+Electron ---
|
||||||
|
cd "$PROJ/frontend"
|
||||||
|
CI=true npm_config_yes=true npm create vite@5.2.0 . -- --template react
|
||||||
|
|
||||||
|
cat > electron.cjs <<'EOF'
|
||||||
|
const { app, BrowserWindow } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
const distIndex = path.join(__dirname, 'dist', 'index.html');
|
||||||
|
const useDevServer = process.env.ELECTRON_DEV === 'true' ||
|
||||||
|
(!app.isPackaged && process.env.ELECTRON_DEV !== 'false' && !fs.existsSync(distIndex));
|
||||||
|
const skipBackend = ['1', 'true', 'yes'].includes(String(process.env.SKIP_ELECTRON_BACKEND || '').toLowerCase());
|
||||||
|
const skipFrontend = ['1', 'true', 'yes'].includes(String(process.env.SKIP_ELECTRON_FRONTEND || '').toLowerCase());
|
||||||
|
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||||
|
const appTitle = process.env.APP_TITLE || process.env.npm_package_name || "React, FastAPI, SQLite, WebSocket";
|
||||||
|
|
||||||
|
let backendProc = null;
|
||||||
|
let frontendProc = null;
|
||||||
|
|
||||||
|
function waitFor(url, timeoutMs = 20000, intervalMs = 300) {
|
||||||
|
const http = require('http');
|
||||||
|
const start = Date.now();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = () => {
|
||||||
|
http
|
||||||
|
.get(url, res => {
|
||||||
|
res.destroy();
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.on('error', () => {
|
||||||
|
if (Date.now() - start > timeoutMs) {
|
||||||
|
reject(new Error(`Timed out waiting for ${url}`));
|
||||||
|
} else {
|
||||||
|
setTimeout(check, intervalMs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (backendProc) backendProc.kill();
|
||||||
|
if (frontendProc) frontendProc.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWindow() {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
title: appTitle,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent the page title from overwriting our window title
|
||||||
|
win.on('page-title-updated', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (win.getTitle() !== appTitle) {
|
||||||
|
win.setTitle(appTitle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (useDevServer) {
|
||||||
|
if (!skipFrontend) {
|
||||||
|
frontendProc = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['run', 'dev'], {
|
||||||
|
cwd: __dirname,
|
||||||
|
shell: false,
|
||||||
|
stdio: 'ignore'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await waitFor(frontendUrl);
|
||||||
|
win.loadURL(frontendUrl);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
cleanup();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
} else if (fs.existsSync(distIndex)) {
|
||||||
|
win.loadFile(distIndex);
|
||||||
|
} else {
|
||||||
|
console.error('dist/index.html not found. Run "npm run build" or set ELECTRON_DEV=true to use the dev server.');
|
||||||
|
cleanup();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function backendPython() {
|
||||||
|
const venvBin = process.platform === 'win32' ? '.venv/Scripts/python.exe' : '.venv/bin/python';
|
||||||
|
const candidate = path.join(__dirname, '../backend', venvBin);
|
||||||
|
if (fs.existsSync(candidate)) return candidate;
|
||||||
|
return process.platform === 'win32' ? 'python' : 'python3';
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
if (!skipBackend) {
|
||||||
|
backendProc = spawn(backendPython(), ['-m', 'uvicorn', 'main:app', '--port', '8000', '--reload'], {
|
||||||
|
cwd: path.join(__dirname, '../backend'),
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: false,
|
||||||
|
});
|
||||||
|
backendProc.on('error', err => console.error('Failed to start backend:', err));
|
||||||
|
} else {
|
||||||
|
console.log('Skipping backend spawn (SKIP_ELECTRON_BACKEND set)');
|
||||||
|
}
|
||||||
|
createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit();
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
||||||
|
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cd src
|
||||||
|
|
||||||
|
cat > App.jsx <<'EOF'
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const API_URL = "http://localhost:8000";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [allLikes, setAllLikes] = useState([]);
|
||||||
|
const [wsMsg, setWsMsg] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [wsAttempts, setWsAttempts] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios.get(API_URL + "/likes/all")
|
||||||
|
.then(res => setAllLikes(res.data))
|
||||||
|
.catch(err => setError(err.message));
|
||||||
|
|
||||||
|
let socket;
|
||||||
|
let retryTimer;
|
||||||
|
|
||||||
|
const connectWs = (attempt = 0) => {
|
||||||
|
setWsAttempts(attempt + 1);
|
||||||
|
socket = new WebSocket("ws://localhost:8000/ws");
|
||||||
|
socket.onopen = () => {
|
||||||
|
setError("");
|
||||||
|
};
|
||||||
|
socket.onmessage = e => {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
setWsMsg(JSON.stringify(msg));
|
||||||
|
};
|
||||||
|
socket.onerror = () => {
|
||||||
|
setError("WebSocket connection failed");
|
||||||
|
};
|
||||||
|
socket.onclose = () => {
|
||||||
|
if (attempt < 4) {
|
||||||
|
retryTimer = setTimeout(() => connectWs(attempt + 1), 1000 * (attempt + 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
connectWs();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (retryTimer) clearTimeout(retryTimer);
|
||||||
|
if (socket) socket.close();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>React · FastAPI · SQLite · WebSocket · ESLint</h1>
|
||||||
|
{error && <p style={{ color: "#f66" }}>Error: {error}</p>}
|
||||||
|
<p>Likes from backend:</p>
|
||||||
|
<ul>
|
||||||
|
{allLikes.map(t => (
|
||||||
|
<li key={t.id}>{t.title} – {t.artist}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p>WebSocket (attempt {wsAttempts}): <code>{wsMsg}</code></p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default App;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > main.jsx <<'EOF'
|
||||||
|
import React from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > index.css <<'EOF'
|
||||||
|
body { font-family: sans-serif; margin: 2em; background: #202126; color: #fafafa; }
|
||||||
|
h1 { color: #6bf; }
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cd ../
|
||||||
|
|
||||||
|
# --- Patch package.json for Electron & deps ---
|
||||||
|
PROJ_NAME="$PROJ" node -e '
|
||||||
|
const fs = require("fs");
|
||||||
|
const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
|
||||||
|
pkg.name = process.env.PROJ_NAME || pkg.name;
|
||||||
|
pkg.main = "./electron.cjs";
|
||||||
|
pkg.scripts = { ...pkg.scripts, electron: "electron .", "electron-build": "npm run build && electron ." };
|
||||||
|
pkg.dependencies = pkg.dependencies || {};
|
||||||
|
pkg.devDependencies = pkg.devDependencies || {};
|
||||||
|
pkg.dependencies.axios = pkg.dependencies.axios || "latest";
|
||||||
|
pkg.devDependencies.electron = pkg.devDependencies.electron || "latest";
|
||||||
|
pkg.devDependencies["eslint-plugin-react"] = pkg.devDependencies["eslint-plugin-react"] || "latest";
|
||||||
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2));
|
||||||
|
'
|
||||||
|
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
cat > "$PROJ/.gitignore" <<'EOF'
|
||||||
|
backend/.venv
|
||||||
|
backend/__pycache__/
|
||||||
|
backend/appdata.db
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
.DS_Store
|
||||||
|
*.pyc
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$PROJ/run.sh" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BACKEND="$ROOT/backend"
|
||||||
|
FRONTEND="$ROOT/frontend"
|
||||||
|
VENV="$BACKEND/.venv"
|
||||||
|
APP_TITLE="${APP_TITLE:-$(basename "$ROOT")}"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[[ -n "${BACK_PID:-}" ]] && kill "$BACK_PID" 2>/dev/null || true
|
||||||
|
[[ -n "${FRONT_PID:-}" ]] && kill "$FRONT_PID" 2>/dev/null || true
|
||||||
|
[[ -n "${ELECTRON_PID:-}" ]] && kill "$ELECTRON_PID" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
echo ">> Backend: ensure venv and deps"
|
||||||
|
python3 -m venv "$VENV"
|
||||||
|
source "$VENV/bin/activate"
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r "$BACKEND/requirements.txt"
|
||||||
|
|
||||||
|
echo ">> Starting backend (uvicorn)..."
|
||||||
|
cd "$BACKEND"
|
||||||
|
python -m uvicorn main:app --port 8000 --reload &
|
||||||
|
BACK_PID=$!
|
||||||
|
|
||||||
|
echo ">> Installing frontend deps (npm install)..."
|
||||||
|
cd "$FRONTEND"
|
||||||
|
npm install
|
||||||
|
|
||||||
|
echo ">> Starting frontend (Vite dev server)..."
|
||||||
|
npm run dev -- --host &
|
||||||
|
FRONT_PID=$!
|
||||||
|
|
||||||
|
echo ">> Starting Electron (UI)..."
|
||||||
|
SKIP_ELECTRON_BACKEND=1 SKIP_ELECTRON_FRONTEND=1 ELECTRON_DEV=true FRONTEND_URL=http://localhost:5173 APP_TITLE="$APP_TITLE" npm run electron &
|
||||||
|
ELECTRON_PID=$!
|
||||||
|
|
||||||
|
echo ">> Backend PID: $BACK_PID Frontend PID: $FRONT_PID Electron PID: $ELECTRON_PID"
|
||||||
|
wait $BACK_PID $FRONT_PID $ELECTRON_PID
|
||||||
|
EOF
|
||||||
|
chmod +x "$PROJ/run.sh"
|
||||||
|
|
||||||
|
cat > "$PROJ/README.md" <<EOF
|
||||||
|
# $PROJ
|
||||||
|
|
||||||
|
## Quickstart (dev)
|
||||||
|
|
||||||
|
1. **Backend venv + deps**
|
||||||
|
cd $PROJ/backend
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
2. **Install frontend deps**
|
||||||
|
cd ../frontend
|
||||||
|
npm install
|
||||||
|
|
||||||
|
3. **Run Electron (dev)**
|
||||||
|
npm run electron
|
||||||
|
|
||||||
|
- **Alt: run both servers without Electron**
|
||||||
|
./run.sh
|
||||||
|
|
||||||
|
## What this does
|
||||||
|
|
||||||
|
- Starts FastAPI backend (REST & WebSocket, SQLite, uvicorn --reload)
|
||||||
|
- Starts React+Vite dev server with ESLint (auto-reloads/HMR)
|
||||||
|
- Opens Electron window
|
||||||
|
- React frontend talks to backend via HTTP+WebSocket
|
||||||
|
|
||||||
|
## Production-ish build
|
||||||
|
|
||||||
|
1. cd frontend
|
||||||
|
2. npm run build
|
||||||
|
3. npm run electron
|
||||||
|
|
||||||
|
(Electron loads built frontend from dist/ and still spawns backend)
|
||||||
|
|
||||||
|
## WebSocket
|
||||||
|
|
||||||
|
Check out App.jsx and main.py for example WS code.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ">> Creating Python venv and installing backend requirements..."
|
||||||
|
cd "$PROJ/backend"
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
deactivate
|
||||||
|
|
||||||
|
echo ">> Installing frontend npm dependencies..."
|
||||||
|
cd ../frontend
|
||||||
|
npm install
|
||||||
|
echo ">> All dependencies installed."
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
echo ">> Boilerplate created at '$PROJ'. See $PROJ/README.md for next steps."
|
||||||
|
|
||||||
|
echo ">> Launching Electron app..."
|
||||||
|
cd "$PROJ/frontend"
|
||||||
|
APP_TITLE="$PROJ" npm run electron
|
||||||
453
tauri_boilerplate.sh
Executable file
453
tauri_boilerplate.sh
Executable file
@@ -0,0 +1,453 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [project-name]"
|
||||||
|
echo "If no project name is given, you will be prompted."
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_name() {
|
||||||
|
[[ "$1" =~ ^[A-Za-z0-9_-]+$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1-}" == "-h" || "${1-}" == "--help" ]]; then
|
||||||
|
usage; exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROJ="${1-}"
|
||||||
|
|
||||||
|
if [[ -n "$PROJ" ]] && ! valid_name "$PROJ"; then
|
||||||
|
echo "Invalid project name: $PROJ (use letters, numbers, hyphen, underscore)"
|
||||||
|
PROJ=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [[ -z "$PROJ" ]]; do
|
||||||
|
read -r -p "Enter project name (letters, numbers, -, _ only): " PROJ
|
||||||
|
if [[ -z "$PROJ" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! valid_name "$PROJ"; then
|
||||||
|
echo "Invalid project name. Use letters, numbers, hyphen, underscore."
|
||||||
|
PROJ=""
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ">> Creating Tauri+React+FastAPI boilerplate in '$PROJ' ..."
|
||||||
|
|
||||||
|
# --- Directory Structure ---
|
||||||
|
mkdir -p "$PROJ/backend"
|
||||||
|
mkdir -p "$PROJ/frontend"
|
||||||
|
|
||||||
|
# --- Backend: Python FastAPI+SQLite ---
|
||||||
|
cat > "$PROJ/backend/requirements.txt" <<EOF
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$PROJ/backend/db.py" <<EOF
|
||||||
|
import sqlite3, os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DB_FILE = os.environ.get("WS_DB_PATH", "appdata.db")
|
||||||
|
|
||||||
|
def get_conn():
|
||||||
|
conn = sqlite3.connect(DB_FILE, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = get_conn()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS likes (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
artist TEXT,
|
||||||
|
album TEXT,
|
||||||
|
url TEXT,
|
||||||
|
duration INTEGER,
|
||||||
|
is_new INTEGER DEFAULT 1,
|
||||||
|
last_seen TIMESTAMP
|
||||||
|
)''')
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def get_likes(is_new=None):
|
||||||
|
conn = get_conn()
|
||||||
|
c = conn.cursor()
|
||||||
|
if is_new is None:
|
||||||
|
rows = c.execute('SELECT * FROM likes ORDER BY last_seen DESC').fetchall()
|
||||||
|
else:
|
||||||
|
rows = c.execute('SELECT * FROM likes WHERE is_new=? ORDER BY last_seen DESC', (1 if is_new else 0,)).fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
def add_demo_like():
|
||||||
|
conn = get_conn()
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR IGNORE INTO likes (id, title, artist, album, url, duration, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
("track1", "Test Track", "Some Artist", "Test Album", "http://test", 123, datetime.utcnow())
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$PROJ/backend/main.py" <<EOF
|
||||||
|
from fastapi import FastAPI, WebSocket
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from starlette.websockets import WebSocketDisconnect
|
||||||
|
import db
|
||||||
|
|
||||||
|
app = FastAPI(title="$PROJ – React, FastAPI, SQLite, WebSocket")
|
||||||
|
db.init_db()
|
||||||
|
db.add_demo_like()
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/likes/all")
|
||||||
|
def api_all_likes():
|
||||||
|
return db.get_likes()
|
||||||
|
|
||||||
|
@app.get("/likes/new")
|
||||||
|
def api_new_likes():
|
||||||
|
return db.get_likes(is_new=True)
|
||||||
|
|
||||||
|
# Example WebSocket endpoint (send a message on connection)
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(ws: WebSocket):
|
||||||
|
await ws.accept()
|
||||||
|
try:
|
||||||
|
await ws.send_json({"type": "hello", "msg": "WebSocket is live!"})
|
||||||
|
while True:
|
||||||
|
data = await ws.receive_text()
|
||||||
|
await ws.send_json({"type": "echo", "data": data})
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
# Client disconnected; exit quietly.
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
# Any other exception: close connection gracefully.
|
||||||
|
await ws.close()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# --- Frontend: Vite+React+ESLint+Tauri ---
|
||||||
|
cd "$PROJ/frontend"
|
||||||
|
CI=true npm_config_yes=true npm create vite@5.2.0 . -- --template react
|
||||||
|
|
||||||
|
# Patch package.json for Tauri & deps
|
||||||
|
node -e '
|
||||||
|
const fs = require("fs");
|
||||||
|
const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8"));
|
||||||
|
pkg.scripts = {
|
||||||
|
...pkg.scripts,
|
||||||
|
tauri: "tauri",
|
||||||
|
"tauri-dev": "tauri dev",
|
||||||
|
"tauri-build": "tauri build"
|
||||||
|
};
|
||||||
|
pkg.dependencies = pkg.dependencies || {};
|
||||||
|
pkg.devDependencies = pkg.devDependencies || {};
|
||||||
|
pkg.dependencies.axios = pkg.dependencies.axios || "latest";
|
||||||
|
pkg.dependencies["@tauri-apps/api"] = pkg.dependencies["@tauri-apps/api"] || "latest";
|
||||||
|
pkg.devDependencies["@tauri-apps/cli"] = pkg.devDependencies["@tauri-apps/cli"] || "latest";
|
||||||
|
pkg.devDependencies["eslint-plugin-react"] = pkg.devDependencies["eslint-plugin-react"] || "latest";
|
||||||
|
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2));
|
||||||
|
'
|
||||||
|
|
||||||
|
# Tauri config and Rust backend
|
||||||
|
mkdir -p src-tauri/src
|
||||||
|
mkdir -p src-tauri/icons
|
||||||
|
|
||||||
|
PROJ="$PROJ" python3 - <<'PY'
|
||||||
|
import json, os, re, pathlib
|
||||||
|
proj = os.environ["PROJ"]
|
||||||
|
identifier = "com.example." + (re.sub(r"[^A-Za-z0-9]", "", proj) or "app")
|
||||||
|
cfg = {
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": proj,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": identifier,
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"devUrl": "http://localhost:5173",
|
||||||
|
"frontendDist": "../dist",
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": f"{proj} – React, FastAPI, SQLite, WebSocket, ESLint",
|
||||||
|
"width": 1200,
|
||||||
|
"height": 800,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {"csp": None},
|
||||||
|
},
|
||||||
|
"bundle": {"active": True, "targets": "all"},
|
||||||
|
}
|
||||||
|
pathlib.Path("src-tauri/tauri.conf.json").write_text(json.dumps(cfg, indent=2))
|
||||||
|
PY
|
||||||
|
|
||||||
|
cat > src-tauri/Cargo.toml <<'EOF'
|
||||||
|
[package]
|
||||||
|
name = "tauri-react-fastapi-app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Tauri + React + FastAPI boilerplate"
|
||||||
|
authors = ["You"]
|
||||||
|
edition = "2021"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > src-tauri/build.rs <<'EOF'
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > src-tauri/src/main.rs <<'EOF'
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Minimal 32x32 RGBA icon required by Tauri bundle tooling
|
||||||
|
python3 - <<'PY'
|
||||||
|
import pathlib, struct, zlib
|
||||||
|
path = pathlib.Path("src-tauri/icons/icon.png")
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
w = h = 32
|
||||||
|
color = (48, 160, 255, 255) # rgba blue-ish
|
||||||
|
row = bytes([0]) + bytes(color) * w # filter byte + pixels
|
||||||
|
data = row * h
|
||||||
|
|
||||||
|
def chunk(t, d):
|
||||||
|
return (struct.pack("!I", len(d)) + t + d +
|
||||||
|
struct.pack("!I", zlib.crc32(t + d) & 0xFFFFFFFF))
|
||||||
|
|
||||||
|
ihdr = chunk(b'IHDR', struct.pack("!IIBBBBB", w, h, 8, 6, 0, 0, 0))
|
||||||
|
idat = chunk(b'IDAT', zlib.compress(data, 9))
|
||||||
|
iend = chunk(b'IEND', b'')
|
||||||
|
png = b'\x89PNG\r\n\x1a\n' + ihdr + idat + iend
|
||||||
|
path.write_bytes(png)
|
||||||
|
PY
|
||||||
|
|
||||||
|
cd src
|
||||||
|
|
||||||
|
cat > App.jsx <<'EOF'
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const API_URL = "http://localhost:8000";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [allLikes, setAllLikes] = useState([]);
|
||||||
|
const [wsMsg, setWsMsg] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [wsAttempts, setWsAttempts] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios.get(API_URL + "/likes/all")
|
||||||
|
.then(res => setAllLikes(res.data))
|
||||||
|
.catch(err => setError(err.message));
|
||||||
|
|
||||||
|
let socket;
|
||||||
|
let retryTimer;
|
||||||
|
|
||||||
|
const connectWs = (attempt = 0) => {
|
||||||
|
setWsAttempts(attempt + 1);
|
||||||
|
socket = new WebSocket("ws://localhost:8000/ws");
|
||||||
|
socket.onopen = () => {
|
||||||
|
setError("");
|
||||||
|
};
|
||||||
|
socket.onmessage = e => {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
setWsMsg(JSON.stringify(msg));
|
||||||
|
};
|
||||||
|
socket.onerror = () => {
|
||||||
|
setError("WebSocket connection failed");
|
||||||
|
};
|
||||||
|
socket.onclose = () => {
|
||||||
|
if (attempt < 4) {
|
||||||
|
retryTimer = setTimeout(() => connectWs(attempt + 1), 1000 * (attempt + 1));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
connectWs();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (retryTimer) clearTimeout(retryTimer);
|
||||||
|
if (socket) socket.close();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>React · FastAPI · SQLite · WebSocket · ESLint</h1>
|
||||||
|
{error && <p style={{ color: "#f66" }}>Error: {error}</p>}
|
||||||
|
<p>Likes from backend:</p>
|
||||||
|
<ul>
|
||||||
|
{allLikes.map(t => (
|
||||||
|
<li key={t.id}>{t.title} – {t.artist}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p>WebSocket (attempt {wsAttempts}): <code>{wsMsg}</code></p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default App;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > main.jsx <<'EOF'
|
||||||
|
import React from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > index.css <<'EOF'
|
||||||
|
body { font-family: sans-serif; margin: 2em; background: #202126; color: #fafafa; }
|
||||||
|
h1 { color: #6bf; }
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cd ../
|
||||||
|
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
cat > "$PROJ/.gitignore" <<'EOF'
|
||||||
|
backend/.venv
|
||||||
|
backend/__pycache__/
|
||||||
|
backend/appdata.db
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
frontend/src-tauri/target
|
||||||
|
frontend/src-tauri/gen
|
||||||
|
frontend/src-tauri/Cargo.lock
|
||||||
|
.DS_Store
|
||||||
|
*.pyc
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$PROJ/run.sh" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BACKEND="$ROOT/backend"
|
||||||
|
FRONTEND="$ROOT/frontend"
|
||||||
|
VENV="$BACKEND/.venv"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[[ -n "${BACK_PID:-}" ]] && kill "$BACK_PID" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
echo ">> Backend: ensure venv and deps"
|
||||||
|
python3 -m venv "$VENV"
|
||||||
|
source "$VENV/bin/activate"
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r "$BACKEND/requirements.txt"
|
||||||
|
|
||||||
|
echo ">> Starting backend (uvicorn)..."
|
||||||
|
cd "$BACKEND"
|
||||||
|
python -m uvicorn main:app --port 8000 --reload &
|
||||||
|
BACK_PID=$!
|
||||||
|
|
||||||
|
echo ">> Installing frontend deps (npm install)..."
|
||||||
|
cd "$FRONTEND"
|
||||||
|
npm install
|
||||||
|
|
||||||
|
echo ">> Launching Tauri dev (opens a window)..."
|
||||||
|
npm run tauri-dev
|
||||||
|
EOF
|
||||||
|
chmod +x "$PROJ/run.sh"
|
||||||
|
|
||||||
|
cat > "$PROJ/README.md" <<EOF
|
||||||
|
# $PROJ
|
||||||
|
|
||||||
|
## Quickstart (dev)
|
||||||
|
|
||||||
|
1. **Backend venv + deps**
|
||||||
|
cd $PROJ/backend
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
2. **Install frontend deps**
|
||||||
|
cd ../frontend
|
||||||
|
npm install
|
||||||
|
|
||||||
|
3. **Run Tauri (dev)**
|
||||||
|
npm run tauri-dev
|
||||||
|
|
||||||
|
- **Alt: run both servers and Tauri via helper**
|
||||||
|
./run.sh
|
||||||
|
|
||||||
|
## What this does
|
||||||
|
|
||||||
|
- Starts FastAPI backend (REST & WebSocket, SQLite)
|
||||||
|
- Starts React+Vite dev server (auto-reloads)
|
||||||
|
- Opens Tauri window
|
||||||
|
- React frontend talks to backend via HTTP+WebSocket
|
||||||
|
|
||||||
|
## Production-ish build
|
||||||
|
|
||||||
|
1. cd frontend
|
||||||
|
2. npm run build
|
||||||
|
3. npm run tauri-build
|
||||||
|
|
||||||
|
(Tauri will bundle the built frontend from dist/)
|
||||||
|
|
||||||
|
## WebSocket
|
||||||
|
|
||||||
|
Check out App.jsx and main.py for example WS code.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ">> Creating Python venv and installing backend requirements..."
|
||||||
|
cd "$PROJ/backend"
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
deactivate
|
||||||
|
|
||||||
|
echo ">> Installing frontend npm dependencies..."
|
||||||
|
cd ../frontend
|
||||||
|
npm install
|
||||||
|
echo ">> All dependencies installed."
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
echo ">> Boilerplate created at '$PROJ'. See $PROJ/README.md for next steps."
|
||||||
|
|
||||||
|
echo ">> Launching Tauri app..."
|
||||||
|
cd "$PROJ/backend"
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m uvicorn main:app --port 8000 --reload &
|
||||||
|
BACK_PID=$!
|
||||||
|
trap 'kill $BACK_PID 2>/dev/null || true' EXIT
|
||||||
|
cd ../frontend
|
||||||
|
npm run tauri-dev
|
||||||
Reference in New Issue
Block a user