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

454 lines
11 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 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