feat: init_web_app bash pipeline — scaffold Go API + React frontend

Extiende init_api_app anadiendo la capa frontend: pnpm + vite + react +
@mantine/core. Genera frontend/ con vite.config.ts (proxy /api y /health al
backend + alias @fn_library a frontend/functions/ui), src/main.tsx con
MantineProvider, src/App.tsx con AppShell y src/pages/Home.tsx consumiendo
/api/v1/status.

Flags: --port N, --with-auth, --with-db (delegadas a init_api_app).

Docker compose para desarrollo. Verifica con pnpm install && pnpm build si
pnpm esta disponible (skippable con SKIP_PNPM_BUILD=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 17:52:34 +02:00
parent da58501723
commit dc0ebe8a49
2 changed files with 585 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
---
name: init_web_app
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "init_web_app(nombre: string, [--port N], [--with-auth], [--with-db]) -> void"
description: "Scaffold de full-stack app: Go HTTP API backend + React frontend con Mantine y @fn_library. Extiende init_api_app anadiendo frontend/ con pnpm + vite + react + mantine. Genera vite.config.ts con proxy al backend y alias @fn_library, src/main.tsx con MantineProvider, src/App.tsx con AppShell, src/pages/Home.tsx con ejemplo consumiendo /api/v1/status."
tags: [init, scaffold, web, fullstack, frontend, pipeline, bash, launcher]
uses_functions:
- assert_command_exists_bash_shell
- http_serve_go_infra
- http_router_go_infra
- http_middleware_chain_go_infra
- http_logger_middleware_go_infra
- http_cors_middleware_go_infra
- http_json_response_go_infra
- http_error_response_go_infra
- migration_up_go_infra
- mantine_provider_ts_ui
- app_shell_ts_ui
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: nombre
desc: "nombre de la app a crear (apps/{nombre}/)"
- name: "--port"
desc: "puerto del backend HTTP (default 8080); el frontend usa 5173 con proxy"
- name: "--with-auth"
desc: "anade jwt_middleware + handlers login/register + tabla users al backend"
- name: "--with-db"
desc: "anade store.go con helpers CRUD y setup SQLite al backend"
output: "app full-stack en apps/{nombre}/ con backend Go (main.go) y frontend/ (vite + react + mantine). Dev: terminal 1 go run .; terminal 2 cd frontend && pnpm dev."
tested: false
tests: []
test_file_path: ""
example: "fn run init_web_app my_dashboard --with-auth"
file_path: "bash/functions/pipelines/init_web_app.sh"
---
## Sinopsis
```bash
fn run init_web_app <nombre> [--port N] [--with-auth] [--with-db]
```
## Ejemplo rapido
```bash
fn run init_web_app inventory_dashboard --with-auth
cd apps/inventory_dashboard
make install # pnpm install del frontend
# Terminal 1:
CGO_ENABLED=1 go run .
# Terminal 2:
cd frontend && pnpm dev
# → http://localhost:5173 (frontend) proxea a :8080 (backend)
```
## Archivos generados
Todos los de `init_api_app`, mas:
| Archivo | Descripcion |
|---------|-------------|
| `frontend/package.json` | pnpm, vite, react, @mantine/core, @tabler/icons-react |
| `frontend/vite.config.ts` | Proxy `/api` y `/health` al backend + alias `@fn_library` |
| `frontend/tsconfig.json` | TS strict con paths `@fn_library/*` |
| `frontend/index.html` | Entry HTML minimo |
| `frontend/postcss.config.cjs` | postcss-preset-mantine + breakpoints |
| `frontend/src/main.tsx` | Root con `MantineProvider` + theme |
| `frontend/src/theme.ts` | `createTheme()` con primaryColor |
| `frontend/src/App.tsx` | `AppShell` con Burger + Navbar + Header |
| `frontend/src/pages/Home.tsx` | Pagina ejemplo que consume `/api/v1/status` |
| `docker-compose.yml` | Services: api + frontend (node alpine) |
| `Makefile` | Targets `install`, `build-frontend`, `build`, `dev` |
| `app.md` | Framework: `net/http + vite + react + mantine` |
## Flags
| Flag | Efecto |
|------|--------|
| `--port N` | Puerto del backend Go (default: 8080) — frontend Vite siempre en 5173 |
| `--with-auth` | JWT + tabla users al backend |
| `--with-db` | Store + SQLite setup al backend |
## Post-setup
```bash
cd apps/{nombre}
cp .env.example .env
make install # pnpm install del frontend
# Desarrollo (2 terminales):
CGO_ENABLED=1 go run . # Terminal 1: backend :PORT
cd frontend && pnpm dev # Terminal 2: frontend :5173
# Produccion:
make build # build frontend + binario Go
./<nombre> # sirve todo en :PORT
```
## Notas
Pipeline impuro: invoca primero `init_api_app` para el backend y luego
escribe el frontend. Si pnpm esta disponible y `SKIP_PNPM_BUILD` no es
`true`, ejecuta `pnpm install && pnpm build` como verificacion final.
El alias `@fn_library` en `vite.config.ts` apunta a
`../../../frontend/functions/ui` (relativo desde `apps/{nombre}/frontend/`).
Los componentes del registry se consumen sin duplicarlos.
Si `apps/{nombre}/` ya existe, aborta sin sobrescribir.
El tag `launcher` permite que aparezca en el Pipeline Launcher TUI.
+466
View File
@@ -0,0 +1,466 @@
#!/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 <nombre> [--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 <nombre> [--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" <<EOF
{
"name": "${NOMBRE}-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@mantine/core": "^8.0.0",
"@mantine/hooks": "^8.0.0",
"@mantine/notifications": "^8.0.0",
"@tabler/icons-react": "^3.30.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.7.0",
"vite": "^7.0.0"
}
}
EOF
# vite.config.ts
cat > "$APP_DIR/frontend/vite.config.ts" <<EOF
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
// Vite config: proxy API al backend Go + alias @fn_library a frontend/functions/ui del registry
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@fn_library": path.resolve(__dirname, "../../../frontend/functions/ui"),
},
},
server: {
port: 5173,
proxy: {
"/api": {
target: "http://localhost:${PORT}",
changeOrigin: true,
},
"/health": {
target: "http://localhost:${PORT}",
},
},
},
build: {
outDir: "dist",
},
});
EOF
# tsconfig.json
cat > "$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" <<EOF
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${NOMBRE}</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
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" <<EOF
import { createTheme } from "@mantine/core";
// Tema Mantine de ${NOMBRE}. Mantine genera sus propias CSS variables.
export const theme = createTheme({
primaryColor: "blue",
defaultRadius: "md",
});
EOF
# src/main.tsx
cat > "$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(
<React.StrictMode>
<MantineProvider theme={theme} defaultColorScheme="auto">
<App />
</MantineProvider>
</React.StrictMode>,
);
EOF
# src/App.tsx
cat > "$APP_DIR/frontend/src/App.tsx" <<EOF
import { AppShell, Title, Burger, Group, NavLink, Stack } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconHome, IconSettings } from "@tabler/icons-react";
import { Home } from "./pages/Home";
export function App() {
const [opened, { toggle }] = useDisclosure();
return (
<AppShell
header={{ height: 56 }}
navbar={{ width: 240, breakpoint: "sm", collapsed: { mobile: !opened } }}
padding="md"
>
<AppShell.Header>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Title order={4}>${NOMBRE}</Title>
</Group>
</AppShell.Header>
<AppShell.Navbar p="sm">
<Stack gap={4}>
<NavLink label="Home" leftSection={<IconHome size={16} />} active />
<NavLink label="Settings" leftSection={<IconSettings size={16} />} />
</Stack>
</AppShell.Navbar>
<AppShell.Main>
<Home />
</AppShell.Main>
</AppShell>
);
}
EOF
# src/pages/Home.tsx
cat > "$APP_DIR/frontend/src/pages/Home.tsx" <<EOF
import { useEffect, useState } from "react";
import { Stack, Title, Text, Paper, Group, Badge } from "@mantine/core";
type StatusResponse = {
app: string;
version: string;
};
export function Home() {
const [status, setStatus] = useState<StatusResponse | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/v1/status")
.then((r) => r.json())
.then(setStatus)
.catch((e) => setError(String(e)));
}, []);
return (
<Stack>
<Title order={2}>${NOMBRE}</Title>
<Text c="dimmed">Full-stack app scaffoldeada por init_web_app.</Text>
<Paper p="md" withBorder>
<Group justify="space-between">
<Text fw={500}>API status</Text>
{status ? (
<Badge color="green">{status.app} v{status.version}</Badge>
) : error ? (
<Badge color="red">{error}</Badge>
) : (
<Badge color="gray">loading...</Badge>
)}
</Group>
</Paper>
</Stack>
);
}
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" <<EOF
services:
api:
build: .
ports:
- "${PORT}:${PORT}"
env_file: .env
volumes:
- ./migrations:/app/migrations
frontend:
image: node:20-alpine
working_dir: /app
command: sh -c "corepack enable && pnpm install && pnpm dev --host"
ports:
- "5173:5173"
volumes:
- ./frontend:/app
EOF
# Actualizar Makefile con targets frontend + dev
cat > "$APP_DIR/Makefile" <<EOF
.PHONY: build build-frontend run dev test vet clean install
BIN=${NOMBRE}
install:
cd frontend && pnpm install
build-frontend:
cd frontend && pnpm build
build: build-frontend
CGO_ENABLED=1 go build -tags fts5 -o \$(BIN) .
run: build
./\$(BIN)
dev:
@echo "Arranca el backend (API en :${PORT}):"
@echo " CGO_ENABLED=1 go run ."
@echo "Arranca el frontend (Vite en :5173 con proxy):"
@echo " cd frontend && pnpm dev"
test:
CGO_ENABLED=1 go test -tags fts5 -v ./...
vet:
CGO_ENABLED=1 go vet -tags fts5 ./...
clean:
rm -f \$(BIN) *.db *.db-shm *.db-wal
rm -rf frontend/dist frontend/node_modules
EOF
# Actualizar app.md con framework + uses frontend
cat > "$APP_DIR/app.md" <<EOF
---
name: ${NOMBRE}
lang: go
domain: tools
description: "Full-stack app (Go API + React/Mantine frontend) generada por init_web_app."
tags: [service, web, frontend]
uses_functions:
- http_serve_go_infra
- http_router_go_infra
- http_middleware_chain_go_infra
- http_cors_middleware_go_infra
- http_logger_middleware_go_infra
- http_json_response_go_infra
- http_error_response_go_infra
- migration_up_go_infra
uses_types:
- Route_go_infra
- Middleware_go_infra
- HTTPError_go_infra
framework: "net/http + vite + react + mantine"
entry_point: "main.go"
dir_path: "apps/${NOMBRE}"
---
## Notas
App full-stack: backend en Go (\`main.go\`) + frontend en \`frontend/\` con
Vite + React + Mantine. El vite.config.ts hace proxy de \`/api\` y \`/health\`
al backend en :${PORT}, y usa alias \`@fn_library\` hacia \`frontend/functions/ui\`
del registry.
Dev: \`make install && make dev\` (dos terminales: backend con \`go run .\`,
frontend con \`cd frontend && pnpm dev\`).
Prod: \`make build\` genera \`frontend/dist\` + binario; el binario sirve los
static files del build embebido (pendiente embed en main.go).
EOF
# ── 3. Verificar frontend si pnpm disponible ─────────────────
echo ""
echo "[3/3] Verificacion..."
if [ "$SKIP_PNPM_BUILD" = "true" ]; then
echo " SKIP_PNPM_BUILD=true — saltando pnpm install/build"
elif command -v pnpm >/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 ""