barsa 9b2ce83229 Refactor Environment Configuration and Update Script for BFF
- Updated app.config.ts to use process.cwd() for resolving environment file paths, improving compatibility with pnpm.
- Replaced ensure_env function with check_app_env in manage.sh to verify the existence of required environment files for BFF and optional files for Portal, enhancing error handling and user guidance.
2025-12-25 18:17:40 +09:00

467 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# 🔧 Development Environment Manager
# Clean, portable helper for local dev services & apps
set -Eeuo pipefail
IFS=$'\n\t'
########################################
# Config (override via env if you like)
########################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="${PROJECT_ROOT:-"$(cd "$SCRIPT_DIR/../.." && pwd)"}"
# Compose file selection
# Priority: explicit COMPOSE_FILE > DEV_STACK (full|services|auto) > default (full if present)
DEFAULT_COMPOSE_SERVICES="$PROJECT_ROOT/docker/dev/docker-compose.yml"
DEFAULT_COMPOSE_FULL="$PROJECT_ROOT/docker/dev/docker-compose.full.yml"
if [ -z "${COMPOSE_FILE:-}" ]; then
case "${DEV_STACK:-services}" in
full)
COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
;;
services)
COMPOSE_FILE="$DEFAULT_COMPOSE_SERVICES"
;;
auto)
if [ -f "$DEFAULT_COMPOSE_FULL" ]; then
COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
else
COMPOSE_FILE="$DEFAULT_COMPOSE_SERVICES"
fi
;;
*)
COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
;;
esac
fi
ENV_FILE="${ENV_FILE:-"$PROJECT_ROOT/.env"}"
ENV_EXAMPLE_FILE="${ENV_EXAMPLE_FILE:-"$PROJECT_ROOT/.env.example"}"
PROJECT_NAME="${PROJECT_NAME:-portal-dev}"
DB_USER_DEFAULT="dev"
DB_NAME_DEFAULT="portal_dev"
DB_WAIT_SECS="${DB_WAIT_SECS:-30}"
NEXT_PORT_DEFAULT=3000
BFF_PORT_DEFAULT=4000
########################################
# Colors (fallback if tput missing)
########################################
if command -v tput >/dev/null 2>&1 && [ -t 1 ]; then
GREEN="$(tput setaf 2)"
YELLOW="$(tput setaf 3)"
RED="$(tput setaf 1)"
NC="$(tput sgr0)"
else
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
fi
log() { echo -e "${GREEN}[DEV] $*${NC}"; }
warn() { echo -e "${YELLOW}[DEV] $*${NC}"; }
fail() { echo -e "${RED}[DEV] ERROR: $*${NC}"; exit 1; }
trap 'fail "Command failed (exit $?) at line $LINENO. See logs above."' ERR
########################################
# Docker Compose wrapper (v2 & v1)
########################################
detect_compose() {
if docker compose version >/dev/null 2>&1; then
echo "docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
else
fail "Docker Compose not found. Install Docker Desktop or docker-compose."
fi
}
COMPOSE_BIN="$(detect_compose)"
compose() {
# shellcheck disable=SC2086
eval $COMPOSE_BIN -f "$COMPOSE_FILE" -p "$PROJECT_NAME" "$@"
}
########################################
# Preflight checks
########################################
preflight() {
command -v docker >/dev/null 2>&1 || fail "Docker is required."
[ -f "$COMPOSE_FILE" ] || fail "Compose file not found: $COMPOSE_FILE (set DEV_STACK=services or COMPOSE_FILE to override)"
# Suggest Docker running if ps fails
if ! docker info >/dev/null 2>&1; then
fail "Docker daemon not reachable. Is Docker running?"
fi
# pnpm required for app tasks
if [[ "${1:-}" == "apps" || "${1:-}" == "migrate" ]]; then
command -v pnpm >/dev/null 2>&1 || fail "pnpm is required for app commands."
fi
}
########################################
# Env handling
########################################
# Check that per-app env files exist
check_app_env() {
local missing=0
if [ ! -f "$PROJECT_ROOT/apps/bff/.env" ] && [ ! -f "$PROJECT_ROOT/apps/bff/.env.development" ]; then
warn "BFF env file not found. Create one with:"
warn " cp apps/bff/.env.example apps/bff/.env"
missing=1
fi
# Portal env is optional - Next.js has sensible defaults for dev
if [ ! -f "$PROJECT_ROOT/apps/portal/.env.local" ] && [ ! -f "$PROJECT_ROOT/apps/portal/.env.development" ]; then
log "💡 Portal env file not found (optional). Create one with:"
log " cp apps/portal/.env.example apps/portal/.env.local"
fi
if [ "$missing" -eq 1 ]; then
fail "Required env files missing. See messages above."
fi
}
# Load root .env for Docker Compose services (postgres, redis config)
load_env_exported() {
# Export so child processes see env (compose, pnpm etc.)
set +u
set -a
[ -f "$ENV_FILE" ] && . "$ENV_FILE" || true
set +a
set -u
}
########################################
# Helpers
########################################
services_running() {
compose ps | grep -q "Up"
}
wait_for_postgres() {
local user="${POSTGRES_USER:-$DB_USER_DEFAULT}"
local db="${POSTGRES_DB:-$DB_NAME_DEFAULT}"
local timeout="$DB_WAIT_SECS"
log "⏳ Waiting for database ($db) to be ready (timeout: ${timeout}s)..."
local elapsed=0
local step=2
until compose exec -T postgres pg_isready -U "$user" -d "$db" >/dev/null 2>&1; do
sleep "$step"
elapsed=$((elapsed + step))
if (( elapsed >= timeout )); then
fail "Database failed to become ready within ${timeout}s"
fi
done
log "✅ Database is ready!"
}
kill_by_port() {
local port="$1"
# Prefer lsof on macOS; fall back to fuser on Linux
if command -v lsof >/dev/null 2>&1; then
if lsof -tiTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then
log " Killing process on port $port..."
lsof -tiTCP:"$port" -sTCP:LISTEN | xargs -r kill -9 2>/dev/null || true
fi
elif command -v fuser >/dev/null 2>&1; then
if fuser -n tcp "$port" >/dev/null 2>&1; then
log " Killing process on port $port..."
fuser -k -n tcp "$port" 2>/dev/null || true
fi
else
warn "Neither lsof nor fuser found; skipping port cleanup for $port."
fi
}
# Check if a port is free using Node (portable)
is_port_free() {
local port="$1"
if ! command -v node >/dev/null 2>&1; then
return 0 # assume free if node unavailable
fi
node -e "const net=require('net');const p=parseInt(process.argv[1],10);const s=net.createServer();s.once('error',()=>process.exit(1));s.once('listening',()=>s.close(()=>process.exit(0)));s.listen({port:p,host:'127.0.0.1'});" "$port"
}
# Find a free port starting from base, up to +50
find_free_port() {
local base="$1"
local limit=$((base+50))
local p="$base"
while [ "$p" -le "$limit" ]; do
if is_port_free "$p"; then
echo "$p"
return 0
fi
p=$((p+1))
done
echo "$base"
}
########################################
# Commands
########################################
start_services() {
preflight "start"
cd "$PROJECT_ROOT"
load_env_exported
log "🚀 Starting development services..."
log "🧩 Using compose: $COMPOSE_FILE"
compose up -d postgres redis
wait_for_postgres
local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
# Ensure desired ports are free; kill any listeners
kill_by_port "$next"
kill_by_port "$bff"
# If still busy, either auto-shift (if allowed) or fail
if ! is_port_free "$next"; then
if [ "${ALLOW_PORT_SHIFT:-0}" = "1" ]; then
local next_free
next_free="$(find_free_port "$next")"
warn "Port $next in use; assigning NEXT_PORT=$next_free"
export NEXT_PORT="$next_free"
next="$next_free"
else
fail "Port $next is in use. Stop the process or run '$0 cleanup'. Set ALLOW_PORT_SHIFT=1 to auto-assign another port."
fi
fi
if ! is_port_free "$bff"; then
if [ "${ALLOW_PORT_SHIFT:-0}" = "1" ]; then
local bff_free
bff_free="$(find_free_port "$bff")"
warn "Port $bff in use; assigning BFF_PORT=$bff_free"
export BFF_PORT="$bff_free"
bff="$bff_free"
else
fail "Port $bff is in use. Stop the process or run '$0 cleanup'. Set ALLOW_PORT_SHIFT=1 to auto-assign another port."
fi
fi
log "✅ Development services are running!"
log "🔗 Database: postgresql://${POSTGRES_USER:-$DB_USER_DEFAULT}:${POSTGRES_PASSWORD:-${POSTGRES_PASSWORD:-dev}}@localhost:5432/${POSTGRES_DB:-$DB_NAME_DEFAULT}"
log "🔗 Redis: redis://localhost:6379"
log "🔗 BFF API (expected): http://localhost:${bff}/api"
log "🔗 Frontend (expected): http://localhost:${next}"
}
start_with_tools() {
preflight "tools"
cd "$PROJECT_ROOT"
load_env_exported
log "🛠️ Starting development services with admin tools..."
log "🧩 Using compose: $COMPOSE_FILE"
# Explicitly start only services + admin tools to avoid building app containers in the full compose
compose --profile tools up -d postgres redis adminer redis-commander mailhog
wait_for_postgres
log "🔗 Database Admin: http://localhost:8080"
log "🔗 Redis Commander: http://localhost:8081"
}
stop_services() {
preflight "stop"
cd "$PROJECT_ROOT"
log "⏹️ Stopping development services..."
compose down --remove-orphans
log "✅ Services stopped"
}
show_status() {
preflight "status"
cd "$PROJECT_ROOT"
log "📊 Development Services Status:"
log "🧩 Using compose: $COMPOSE_FILE"
compose ps
}
show_logs() {
preflight "logs"
cd "$PROJECT_ROOT"
# Pass-through any service names after "logs"
# e.g. ./dev.sh logs postgres redis
log "🧩 Using compose: $COMPOSE_FILE"
compose logs -f --tail=100 "${@:2}"
}
cleanup_dev() {
log "🧹 Cleaning up all development processes and ports..."
# Pull ports from env if present; include common defaults
local ports=()
ports+=("${NEXT_PORT:-$NEXT_PORT_DEFAULT}")
ports+=("${BFF_PORT:-$BFF_PORT_DEFAULT}")
ports+=(5555) # Prisma Studio default
for p in "${ports[@]}"; do
kill_by_port "$p"
done
# Kill common dev processes by name
pkill -f "next dev" 2>/dev/null && log " Stopped Next.js dev server" || true
pkill -f "nest start --watch" 2>/dev/null && log " Stopped NestJS watch server" || true
pkill -f "next-server" 2>/dev/null && log " Stopped Next.js server process" || true
pkill -f "pnpm.*--parallel.*dev" 2>/dev/null && log " Stopped parallel dev processes" || true
pkill -f "prisma studio" 2>/dev/null && log " Stopped Prisma Studio" || true
pkill -f "apps/bff/scripts/dev-watch.sh" 2>/dev/null && log " Stopped BFF dev-watch script" || true
pkill -f "tsc -p tsconfig.build.json --watch" 2>/dev/null && log " Stopped BFF TypeScript watcher" || true
pkill -f "tsc-alias.*tsconfig.build.json.*-w" 2>/dev/null && log " Stopped BFF tsc-alias watcher" || true
pkill -f "node --watch dist/main.js" 2>/dev/null && log " Stopped BFF Node watcher" || true
sleep 1
log "✅ Development cleanup completed"
}
start_apps() {
preflight "apps"
cd "$PROJECT_ROOT"
# Verify app env files exist before proceeding
check_app_env
cleanup_dev
if ! services_running; then
start_services
fi
load_env_exported
# Build shared package first
log "🔨 Building shared package..."
pnpm --filter @customer-portal/domain build
pnpm --filter @customer-portal/validation build
# Build BFF before watch (ensures dist exists). Use Nest build for correct emit.
log "🔨 Building BFF for initial setup (ts emit)..."
(
cd "$PROJECT_ROOT/apps/bff" \
&& pnpm clean \
&& rm -f tsconfig.build.tsbuildinfo \
&& pnpm build || pnpm exec tsc -b --force tsconfig.build.json
)
if [ ! -d "$PROJECT_ROOT/apps/bff/dist" ]; then
warn "BFF dist not found after build; forcing TypeScript emit..."
(cd "$PROJECT_ROOT/apps/bff" && pnpm exec tsc -b --force tsconfig.build.json)
fi
if [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.js" ] && [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.cjs" ]; then
warn "BFF main output not found; will rely on watch to produce it."
fi
local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
log "🎯 Starting development applications..."
log "🔗 BFF API: http://localhost:${bff}/api"
log "🔗 Frontend: http://localhost:${next}"
log "🔗 Database: postgresql://${POSTGRES_USER:-$DB_USER_DEFAULT}:${POSTGRES_PASSWORD:-${POSTGRES_PASSWORD:-dev}}@localhost:5432/${POSTGRES_DB:-$DB_NAME_DEFAULT}"
log "🔗 Redis: redis://localhost:6379"
log "📚 API Docs: http://localhost:${bff}/api/docs"
log "Starting apps with hot-reload..."
# Prisma Studio can be started manually with: pnpm db:studio
# Run portal + bff in parallel with hot reload
pnpm --parallel \
--filter @customer-portal/domain \
--filter @customer-portal/validation \
--filter @customer-portal/portal \
--filter @customer-portal/bff run dev
}
reset_env() {
preflight "reset"
cd "$PROJECT_ROOT"
log "🔄 Resetting development environment..."
compose down -v --remove-orphans
docker system prune -f
log "✅ Development environment reset"
}
migrate_db() {
preflight "migrate"
cd "$PROJECT_ROOT"
if ! compose ps postgres | grep -q "Up"; then
fail "Database service not running. Run '$0 start' or '$0 apps' first."
fi
load_env_exported
log "🗄️ Running database migrations..."
pnpm db:migrate
log "✅ Database migrations completed"
}
studio_db() {
preflight "studio"
cd "$PROJECT_ROOT"
if ! compose ps postgres | grep -q "Up"; then
fail "Database service not running. Run '$0 start' or '$0 apps' first."
fi
load_env_exported
log "🔍 Starting Prisma Studio..."
pnpm --filter @customer-portal/bff run db:studio
}
usage() {
cat <<EOF
🔧 Development Environment Manager
Usage: $0 {command}
Commands:
start - Start dev services (PostgreSQL + Redis)
stop - Stop all dev services
restart - Restart dev services
status - Show service status
logs - Tail logs (optionally: specify services, e.g. '$0 logs postgres redis')
tools - Start services with admin tools
apps - Start services + run dev apps (auto-cleanup)
cleanup - Clean up local dev processes and ports
migrate - Run database migrations
studio - Open Prisma Studio database browser
reset - Reset development environment
help - Show this help
Setup (first time):
cp apps/bff/.env.example apps/bff/.env
cp apps/portal/.env.example apps/portal/.env.local # optional
Environment overrides:
PROJECT_ROOT, COMPOSE_FILE, PROJECT_NAME
DEV_STACK (full|services|auto) # default: full
POSTGRES_USER, POSTGRES_DB, POSTGRES_PASSWORD, DB_WAIT_SECS
NEXT_PORT, BFF_PORT
EOF
}
########################################
# Main
########################################
cmd="${1:-help}"
case "$cmd" in
start) start_services ;;
stop) stop_services ;;
restart) stop_services; start_services ;;
status) show_status ;;
logs) show_logs "$@" ;;
tools) start_with_tools ;;
apps) start_apps ;;
cleanup) cleanup_dev ;;
migrate) migrate_db ;;
studio) studio_db ;;
reset) reset_env ;;
help|*) usage; exit 0 ;;
esac