Files
boilerplates-bootstraps/electron_boilerplate.sh
Victor Giers 5e8bdb1c05 Initial Commit
2025-12-04 11:26:05 +01:00

479 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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