#!/usr/bin/env bash # init_web_app # ------------ # Scaffold de full-stack app: Go HTTP API backend + React frontend con # Mantine y @fn_library. Extiende init_api_app anadiendo la capa frontend. # # Genera todo lo de init_api_app mas frontend/ con pnpm + vite + react + # mantine, vite.config.ts con alias @fn_library y proxy al backend, # src/main.tsx con FnMantineProvider, src/App.tsx, src/theme.ts y # src/pages/Home.tsx de ejemplo. # # USO: # ./init_web_app.sh [--port N] [--with-auth] [--with-db] # # EJEMPLO: # ./init_web_app.sh my_dashboard # ./init_web_app.sh my_dashboard --port 8080 --with-auth set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # Source funciones atomicas source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh" # ── Parsing ────────────────────────────────────────────────── NOMBRE="" PORT="8080" WITH_AUTH="false" WITH_DB="false" SKIP_PNPM_BUILD="${SKIP_PNPM_BUILD:-false}" while [ $# -gt 0 ]; do case "$1" in --port) PORT="$2"; shift 2 ;; --with-auth) WITH_AUTH="true"; shift ;; --with-db) WITH_DB="true"; shift ;; --skip-pnpm-build) SKIP_PNPM_BUILD="true"; shift ;; -h|--help) grep "^#" "$0" | sed 's/^# \?//' ; exit 0 ;; -*) echo "Flag desconocido: $1" >&2 ; exit 1 ;; *) if [ -z "$NOMBRE" ]; then NOMBRE="$1" else echo "Argumento extra ignorado: $1" >&2 fi shift ;; esac done if [ -z "$NOMBRE" ]; then echo "Uso: $0 [--port N] [--with-auth] [--with-db]" >&2 exit 1 fi APP_DIR="${REGISTRY_ROOT}/apps/${NOMBRE}" if [ -d "$APP_DIR" ]; then echo "ERROR: ${APP_DIR} ya existe. Abortando." >&2 exit 1 fi echo "" echo "════════════════════════════════════════════════════════════" echo " INIT WEB APP: ${NOMBRE}" echo " Directorio: ${APP_DIR}" echo " Puerto: ${PORT}" echo " Auth: ${WITH_AUTH}" echo " DB: ${WITH_DB}" echo "════════════════════════════════════════════════════════════" echo "" # ── 1. Invocar init_api_app para generar backend ───────────── echo "[1/3] Generando backend con init_api_app..." BACKEND_FLAGS=() BACKEND_FLAGS+=(--port "$PORT") [ "$WITH_AUTH" = "true" ] && BACKEND_FLAGS+=(--with-auth) [ "$WITH_DB" = "true" ] && BACKEND_FLAGS+=(--with-db) bash "$SCRIPT_DIR/init_api_app.sh" "$NOMBRE" "${BACKEND_FLAGS[@]}" # ── 2. Generar frontend ────────────────────────────────────── echo "" echo "[2/3] Generando frontend..." mkdir -p "$APP_DIR/frontend/src/pages" # package.json cat > "$APP_DIR/frontend/package.json" < "$APP_DIR/frontend/vite.config.ts" < "$APP_DIR/frontend/tsconfig.json" <<'EOF' { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "allowImportingTsExtensions": false, "baseUrl": ".", "paths": { "@fn_library/*": ["../../../frontend/functions/ui/*"] } }, "include": ["src"] } EOF # index.html cat > "$APP_DIR/frontend/index.html" < ${NOMBRE}
EOF # postcss.config.cjs (Mantine usa postcss-preset-mantine) cat > "$APP_DIR/frontend/postcss.config.cjs" <<'EOF' module.exports = { plugins: { "postcss-preset-mantine": {}, "postcss-simple-vars": { variables: { "mantine-breakpoint-xs": "36em", "mantine-breakpoint-sm": "48em", "mantine-breakpoint-md": "62em", "mantine-breakpoint-lg": "75em", "mantine-breakpoint-xl": "88em", }, }, }, }; EOF # src/theme.ts cat > "$APP_DIR/frontend/src/theme.ts" < "$APP_DIR/frontend/src/main.tsx" <<'EOF' import React from "react"; import ReactDOM from "react-dom/client"; import { MantineProvider } from "@mantine/core"; import "@mantine/core/styles.css"; import { theme } from "./theme"; import { App } from "./App"; ReactDOM.createRoot(document.getElementById("root")!).render( , ); EOF # src/App.tsx cat > "$APP_DIR/frontend/src/App.tsx" < ${NOMBRE} } active /> } /> ); } EOF # src/pages/Home.tsx cat > "$APP_DIR/frontend/src/pages/Home.tsx" <(null); const [error, setError] = useState(null); useEffect(() => { fetch("/api/v1/status") .then((r) => r.json()) .then(setStatus) .catch((e) => setError(String(e))); }, []); return ( ${NOMBRE} Full-stack app scaffoldeada por init_web_app. API status {status ? ( {status.app} v{status.version} ) : error ? ( {error} ) : ( loading... )} ); } EOF # .gitignore del frontend cat > "$APP_DIR/frontend/.gitignore" <<'EOF' node_modules/ dist/ *.log .DS_Store EOF # docker-compose.yml cat > "$APP_DIR/docker-compose.yml" < "$APP_DIR/Makefile" < "$APP_DIR/app.md" </dev/null 2>&1; then echo " pnpm detectado, ejecutando install + build..." ( cd "$APP_DIR/frontend" if pnpm install --silent 2>&1 | tail -5; then : fi if pnpm build 2>&1 | tail -5; then echo " frontend build OK" else echo " WARN: frontend build fallo (revisa el output arriba)" >&2 fi ) else echo " pnpm no disponible — saltando verificacion frontend" echo " Cuando este disponible: cd ${APP_DIR}/frontend && pnpm install && pnpm build" fi echo "" echo "════════════════════════════════════════════════════════════" echo " WEB APP '${NOMBRE}' LISTA" echo "════════════════════════════════════════════════════════════" echo "" echo " Pasos siguientes:" echo " cd apps/${NOMBRE}" echo " cp .env.example .env" echo " make install # pnpm install del frontend" echo "" echo " Desarrollo (2 terminales):" echo " Terminal 1: cd apps/${NOMBRE} && CGO_ENABLED=1 go run ." echo " Terminal 2: cd apps/${NOMBRE}/frontend && pnpm dev" echo "" echo " Abrir http://localhost:5173" echo ""