From 5e8bdb1c05ce3dd999d6d8b7a5de83d10aa39da1 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Thu, 4 Dec 2025 11:26:05 +0100 Subject: [PATCH] Initial Commit --- README.md | 26 +++ electron_boilerplate.sh | 478 ++++++++++++++++++++++++++++++++++++++++ tauri_boilerplate.sh | 453 +++++++++++++++++++++++++++++++++++++ 3 files changed, 957 insertions(+) create mode 100644 README.md create mode 100755 electron_boilerplate.sh create mode 100755 tauri_boilerplate.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ab65e4 --- /dev/null +++ b/README.md @@ -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. diff --git a/electron_boilerplate.sh b/electron_boilerplate.sh new file mode 100755 index 0000000..9e63530 --- /dev/null +++ b/electron_boilerplate.sh @@ -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" < "$PROJ/backend/db.py" < "$PROJ/backend/main.py" < 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 ( +
+

React · FastAPI · SQLite · WebSocket · ESLint

+ {error &&

Error: {error}

} +

Likes from backend:

+
    + {allLikes.map(t => ( +
  • {t.title} – {t.artist}
  • + ))} +
+

WebSocket (attempt {wsAttempts}): {wsMsg}

+
+ ); +} +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( + + + , +) +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" <> 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 diff --git a/tauri_boilerplate.sh b/tauri_boilerplate.sh new file mode 100755 index 0000000..2de10f9 --- /dev/null +++ b/tauri_boilerplate.sh @@ -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" < "$PROJ/backend/db.py" < "$PROJ/backend/main.py" < 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 ( +
+

React · FastAPI · SQLite · WebSocket · ESLint

+ {error &&

Error: {error}

} +

Likes from backend:

+
    + {allLikes.map(t => ( +
  • {t.title} – {t.artist}
  • + ))} +
+

WebSocket (attempt {wsAttempts}): {wsMsg}

+
+ ); +} +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( + + + , +) +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" <> 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