#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="$ROOT_DIR/docker-compose.yaml" USERFRONT_DOCKERFILE="$ROOT_DIR/userfront/Dockerfile" USERFRONT_DEV_SERVER="$ROOT_DIR/userfront/scripts/dev-server.sh" fail() { echo "ERROR: $*" >&2 exit 1 } assert_contains() { local pattern="$1" grep -Fq -- "$pattern" "$COMPOSE_FILE" || fail "docker-compose.yaml must contain: $pattern" } assert_not_contains() { local pattern="$1" if grep -Fq -- "$pattern" "$COMPOSE_FILE"; then fail "docker-compose.yaml must not contain stale frontend mount: $pattern" fi } assert_service_contains() { local service="$1" local pattern="$2" awk -v service=" $service:" ' $0 == service { in_service = 1; next } in_service && /^ [[:alnum:]_-]+:/ { in_service = 0 } in_service { print } ' "$COMPOSE_FILE" | grep -Fq -- "$pattern" || fail "$service service must contain: $pattern" } assert_service_not_contains() { local service="$1" local pattern="$2" if awk -v service=" $service:" ' $0 == service { in_service = 1; next } in_service && /^ [[:alnum:]_-]+:/ { in_service = 0 } in_service { print } ' "$COMPOSE_FILE" | grep -Fq -- "$pattern"; then fail "$service service must not contain: $pattern" fi } for app in adminfront devfront orgfront; do assert_contains "./$app:/workspace/$app" assert_contains "/workspace/$app/node_modules" assert_not_contains "./$app:/app" assert_service_contains "$app" "target: dev" assert_service_contains "$app" "working_dir: /workspace/$app" assert_service_contains "$app" 'command: ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port",' assert_service_contains "$app" 'DEV_SERVER_WATCH_POLLING=${DEV_SERVER_WATCH_POLLING:-true}' assert_service_not_contains "$app" "serve_frontend_prod.mjs" grep -Fq -- "FROM deps AS dev" "$ROOT_DIR/$app/Dockerfile" || fail "$app Dockerfile must define a deps-based dev target" grep -Fq -- 'CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port",' "$ROOT_DIR/$app/Dockerfile" || fail "$app Dockerfile dev target must run Vite dev server" grep -Fq -- "FROM deps AS build" "$ROOT_DIR/$app/Dockerfile" || fail "$app Dockerfile must keep production build separate from dev" grep -Fq -- "FROM node:24-alpine AS production" "$ROOT_DIR/$app/Dockerfile" || fail "$app Dockerfile must keep production static serving target" done assert_contains 'target: ${USERFRONT_BUILD_TARGET:-dev}' assert_contains "./userfront/lib:/workspace/userfront/lib" assert_contains "./userfront/assets:/workspace/userfront/assets" assert_contains "./userfront/web:/workspace/userfront/web" assert_contains "./userfront/scripts:/workspace/userfront/scripts:ro" assert_contains "./scripts:/workspace/scripts:ro" assert_contains "./locales:/workspace/locales:ro" grep -Fq -- "AS dev" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must define a dev build target" grep -Fq -- "AS production" "$USERFRONT_DOCKERFILE" || fail "userfront Dockerfile must keep an explicit production target" grep -Fq -- "flutter run" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must use flutter run" grep -Fq -- "--wasm" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must keep WebAssembly enabled" grep -Fq -- "--dart-define=BACKEND_URL=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass backend URL through dart-define" grep -Fq -- "--dart-define=CLIENT_LOG_DEBUG=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass client log debug mode through dart-define" grep -Fq -- "--dart-define=APP_ENV=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass app env through dart-define" grep -Fq -- "--dart-define=USERFRONT_URL=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass userfront URL through dart-define" grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must accept optional Flutter run flags" grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS="${USERFRONT_FLUTTER_RUN_FLAGS:---debug}"' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must keep Flutter debug mode as the default" grep -Fq -- 'warm_userfront_once' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must run a one-shot boot warmup" grep -Fq -- 'rm -rf build/web' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must remove stale build/web before flutter run" grep -Fq -- 'USERFRONT_BOOT_BUILD_MARKER' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must track the current flutter run build start" grep -Fq -- 'USERFRONT_BOOT_LOG' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must capture flutter run output" grep -Fq -- 'wait_for_userfront_serve_log' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must wait for Flutter serve completion log" grep -Fq -- 'lib/main.dart is being served at' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must start only after Flutter serves main.dart" grep -Fq -- 'tee "$USERFRONT_BOOT_LOG"' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must preserve flutter output while tracking serve readiness" grep -Fq -- '-nt "$USERFRONT_BOOT_BUILD_MARKER"' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must only accept build/web from the current flutter run" grep -Fq -- 'USERFRONT_BOOT_WARMUP_LOCALES' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must declare the language matrix" grep -Fq -- 'USERFRONT_BOOT_WARMUP_VIEWPORTS' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must declare the viewport matrix" grep -Fq -- 'Accept-Language:' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must GET each route with the locale header" grep -Fq -- 'Viewport-Width:' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must GET each route with viewport width metadata" grep -Fq -- '/signin' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must cover signin routes" grep -Fq -- '/flutter_bootstrap.js' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must load the Flutter bootstrap entrypoint" grep -Fq -- 'warm_flutter_bootstrap_assets' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must discover hashed Flutter assets from flutter_bootstrap.js" if grep -Fq -- '/main.dart.mjs' "$USERFRONT_DEV_SERVER" || grep -Fq -- '/main.dart.wasm' "$USERFRONT_DEV_SERVER"; then fail "userfront boot warmup must not use stale unversioned Flutter wasm entrypoint paths" fi if grep -Fq -- 'client.navigate' "$USERFRONT_DEV_SERVER"; then fail "userfront dev service worker reset must not navigate clients during Flutter bootstrap" fi grep -Fq -- 'disable_userfront_dev_service_worker' "$USERFRONT_DEV_SERVER" || fail "userfront dev boot must disable Flutter service worker navigation loops" warmup_timer_line="$(grep -nF 'started_at="$(date +%s)"' "$USERFRONT_DEV_SERVER" | cut -d: -f1)" warmup_start_log_line="$(grep -nF 'one-shot warmup starting' "$USERFRONT_DEV_SERVER" | cut -d: -f1)" http_readiness_skip_line="$(grep -nF 'readiness attempts' "$USERFRONT_DEV_SERVER" | tail -n 1 | cut -d: -f1)" if [ "$warmup_timer_line" -le "$http_readiness_skip_line" ] || [ "$warmup_timer_line" -ge "$warmup_start_log_line" ]; then fail "userfront warmup timer must start after current Flutter server readiness and immediately before warmup start log" fi assert_contains 'CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}' assert_contains 'BACKEND_URL=${BACKEND_URL:-}' assert_contains 'USERFRONT_URL=${USERFRONT_URL}' assert_contains 'USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-}' if grep -Fq -- "while true" "$USERFRONT_DEV_SERVER"; then fail "userfront boot warmup must not run as a periodic health check" fi if grep -Fq -- "--release" "$USERFRONT_DEV_SERVER"; then fail "userfront dev server must not run Flutter in release mode" fi assert_contains "./common:/workspace/common" assert_contains "/workspace/common/node_modules" assert_contains "./locales:/workspace/locales" for runtime in \ "$ROOT_DIR/adminfront/scripts/runtime-mode.sh" \ "$ROOT_DIR/devfront/scripts/runtime-mode.sh" \ "$ROOT_DIR/orgfront/scripts/runtime-mode.sh" do grep -Fq -- "/workspace/common" "$runtime" || fail "$runtime must install dependencies from /workspace/common" grep -Fq -- "pnpm install --filter" "$runtime" || fail "$runtime must install only its workspace slice" grep -Fq -- "--frozen-lockfile --ignore-scripts" "$runtime" || fail "$runtime must preserve the workspace lockfile with pnpm" if grep -Fq -- "npm install --no-workspaces" "$runtime"; then fail "$runtime must not install common dependencies outside the workspace graph" fi done echo "OK: frontend dev containers bind-mount source into Dockerfile WORKDIR paths"