diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5ee638b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,52 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Testing +coverage +.nyc_output + +# Next.js +.next +out +build +dist + +# Misc +.DS_Store +*.pem +.vscode +.idea + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# Typescript +*.tsbuildinfo + +# Git +.git +.gitignore + +# Docker +Dockerfile +.dockerignore +docker-compose*.yml + +# Documentation +*.md +docs \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..13b0bfb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +# PR이 생성되거나 업데이트될 때 실행 +on: + pull_request: + branches: + - develop + - main + types: [opened, synchronize, reopened] + +jobs: + # 코드 품질 검사 + quality-check: + name: Code Quality Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # 전체 히스토리 (PR 비교용) + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: latest + + # 의존성 캐싱 + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + # 의존성 설치 + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Type check + run: pnpm tsc --noEmit + + + - name: Lint code + # 린트오류가 발생해도 CI가 실패하지 않도록 임시 설정 - 메인 배포때 옵션 제거 예정 + continue-on-error: true + run: pnpm lint + + - name: Build test + run: pnpm build \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..78bc04e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,190 @@ +name: Deploy to OCI + +on: + push: + branches: + - develop + # GitHub Actions 탭에서 수동으로 실행 가능 + workflow_dispatch: + +# 워크플로우 전체에서 사용할 환경 변수 설정 +env: + # 사용할 컨테이너 레지스트리(GitHub Container Registry) + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/global-nomad + +jobs: + # Job 1: Docker 이미지 빌드를 빌드하고 GHCR에 푸시 + build: + name: Build and Push Docker Image + runs-on: ubuntu-latest + # 해당 Job이 사용할 권한 설정 + permissions: + contents: read + # GHCR에 이미지를 쓰기(푸시) 위한 권한 + packages: write + + # Job의 결과를 다른 Job에서 사용할 수 있도록 출력 설정 + outputs: + # 정확한 이미지 태그 전달 (SHA 기반) - 배포할 정확한 이미지 주소, 버전 확인을 위한 커밋 해시 + image-url: ${{ steps.image.outputs.image-url }} + git-sha: ${{ steps.vars.outputs.sha-short }} + + steps: + # 코드 체크아웃 + - name: Checkout code + uses: actions/checkout@v4 + + # Docker Buildx 설정 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # GHCR 로그인 + # - secrets.GITHUB_TOKEN: 워크플로우 실행 시 GitHub이 자동으로 생성해주는 임시 토큰 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # 빌드에 사용할 변수 생성 (짧은 커밋 해시, 빌드 날짜) + - name: Generate build vars + id: vars + run: | + echo "sha-short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "build-date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + + # 도커 이미지 빌드 및 푸시 (불변 태그 사용) + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v5 + with: + # 현재 디렉토리를 빌드 컨텍스트로 사용 + context: . + # 빌드 후 레지스트리에 푸시 + push: true + # 이미지에 여러 태그를 부여 + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop-${{ steps.vars.outputs.sha-short }} + # 이미지에 메타 데이터 라벨 추가 + labels: | + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.created=${{ steps.vars.outputs.build-date }} + # GitHub Actions 캐시를 사용하여 빌드 속도 향상 + cache-from: type=gha + cache-to: type=gha,mode=max + # 빌드할 플랫폼 지정 + platforms: linux/amd64 + + # Job으로 전달할 정확한 이미지 URL 출력으로 설정 + - name: Set image output + id: image + run: | + echo "image-url=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop-${{ steps.vars.outputs.sha-short }}" >> $GITHUB_OUTPUT + + # Job 2: OCI 서버에 배포 + deploy: + name: Deploy to OCI Server + runs-on: ubuntu-latest + # 빌드 Job이 성공해야만 실행 + needs: build + + steps: + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.OCI_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -t rsa ${{ secrets.OCI_HOST }} >> ~/.ssh/known_hosts + + # 서버에 배포 스크립트 실행 + - name: Deploy to server + env: + IMAGE_URL: ${{ needs.build.outputs.image-url }} + GIT_SHA: ${{ needs.build.outputs.git-sha }} + run: | + ssh -i ~/.ssh/id_rsa ${{ secrets.OCI_USERNAME }}@${{ secrets.OCI_HOST }} << 'ENDSSH' + set -e + + echo "📦 Starting deployment..." + echo "🏷️ Image: ${IMAGE_URL}" + echo "🔖 Version: ${GIT_SHA}" + + # 프로젝트 디렉토리로 이동 + cd ~/projects/GlobalNomad + + # 최신 코드 가져오기 + echo "🔄 Pulling latest code..." + git pull origin develop + + # GHCR 로그인 (PAT 사용) + echo "🔐 Logging in to GHCR..." + echo "${{ secrets.GHCR_PAT }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + # 현재 실행 중인 이미지 태그 저장 (롤백용) + echo "💾 Saving current version..." + CURRENT_IMAGE=$(docker-compose ps -q nextjs | xargs docker inspect -f '{{.Config.Image}}' 2>/dev/null || echo "none") + echo "Current: $CURRENT_IMAGE" > ~/deployments/previous-version.txt + + # 기존 컨테이너 중지 + echo "🛑 Stopping existing containers..." + docker-compose down || true + + # PM2 정리(환경 테스트를 하기 위해 사용했던 이전 배포 방식 정리 ) + pm2 stop global-nomad || true + pm2 delete global-nomad || true + + # 최신 이미지 pull (정확한 버전) + echo "📥 Pulling image: ${IMAGE_URL}" + docker pull ${IMAGE_URL} + + # 환경 변수로 이미지 지정하여 실행 + echo "🚀 Starting containers..." + export NEXTJS_IMAGE="${IMAGE_URL}" + docker-compose up -d + + # 헬스 체크 + echo "❤️ Health check..." + for i in {1..30}; do + if curl -f http://localhost:3000 > /dev/null 2>&1; then + echo "✅ Service is healthy!" + break + fi + echo "⏳ Waiting for service... ($i/30)" + sleep 2 + done + + # 최종 확인 + if ! curl -f http://localhost:3000 > /dev/null 2>&1; then + echo "❌ Service health check failed after 60 seconds!" + docker-compose logs --tail 50 nextjs + exit 1 + fi + + # 배포 정보 기록 + echo "📝 Recording deployment..." + mkdir -p ~/deployments + echo "${IMAGE_URL}" > ~/deployments/current-version.txt + echo "$(date -u +'%Y-%m-%d %H:%M:%S UTC') - ${GIT_SHA}" >> ~/deployments/history.log + + # 오래된 이미지 정리 (최근 3개만 유지) + echo "🧹 Cleaning up old images..." + docker images | grep global-nomad | tail -n +4 | awk '{print $3}' | xargs -r docker rmi || true + + echo "✅ Deployment completed successfully!" + echo "📊 Deployed version: ${GIT_SHA}" + ENDSSH + + # 배포 결과 알림 (Slack, Discord 등 추가 가능) + - name: Notify deployment status + if: always() + run: | + if [ ${{ job.status }} == 'success' ]; then + echo "✅ 배포 성공!" + echo "🏷️ Image: ${{ needs.build.outputs.image-url }}" + echo "🔖 Version: ${{ needs.build.outputs.git-sha }}" + else + echo "❌ 배포 실패!" + fi \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..049854f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +# 1단계: 의존성 설치 +FROM node:22-alpine AS deps +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# pnpm 설치 +RUN corepack enable +RUN corepack prepare pnpm@latest --activate + +# 의존성 파일 복사 +COPY package.json pnpm-lock.yaml ./ + +# 의존성 설치: lock 파일과 정확히 일치하는 버전만 설치 +RUN pnpm install --frozen-lockfile + +# 2단계: 빌드 +FROM node:22-alpine AS builder +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# pnpm 설치 +RUN corepack enable +RUN corepack prepare pnpm@latest --activate + +# 의존성 복사 +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js 빌드 +ENV NEXT_TELEMETRY_DISABLED 1 +RUN pnpm build + +# 3단계: 실행 +FROM node:22-alpine AS runner +RUN apk add --no-cache libc6-compat + +WORKDIR /app + +# 프로덕션 환경 설정 +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +# 시스템 사용자 생성 +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# public 폴더 복사 (정적 파일) +COPY --from=builder /app/public ./public + +# standalone 폴더 복사 +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# 사용자 전환 +USER nextjs + +# 포트 설정 +EXPOSE 3000 + +# 환경 변수 설정 +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +# 실행 명령 +CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3d1e144 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +version: '3.8' + +services: + # Next.js 애플리케이션 + nextjs: + container_name: global-nomad-nextjs + # 환경 변수로 이미지 지정 (기본값 제공) + image: ${NEXTJS_IMAGE:-ghcr.io/cksgh5477/global-nomad:latest} + # 로컬 개발용 빌드 설정 (프로덕션에서는 무시됨) + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + environment: + - NODE_ENV=production + networks: + - app-network + # 외부 포트 노출하지 않고, Nginx를 통해서만 접근 + + # Nginx 리버스 프록시 + nginx: + container_name: global-nomad-nginx + image: nginx:alpine + restart: unless-stopped + ports: + - '80:80' + - '443:443' + volumes: + # Nginx 설정 파일 + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + # SSL 인증서 저장 위치 + - ./certbot/conf:/etc/letsencrypt:ro + - ./certbot/www:/var/www/certbot:ro + depends_on: + - nextjs + networks: + - app-network + command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"''' + + # Let's Encrypt 인증서 자동 갱신 + certbot: + container_name: global-nomad-certbot + image: certbot/certbot + restart: unless-stopped + volumes: + - ./certbot/conf:/etc/letsencrypt + - ./certbot/www:/var/www/certbot + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + +# 네트워크 설정 +networks: + app-network: + driver: bridge diff --git a/init-letsencrypt.sh b/init-letsencrypt.sh new file mode 100755 index 0000000..aad0598 --- /dev/null +++ b/init-letsencrypt.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# 도메인 설정 +domains=(globalnomad.site www.globalnomad.site) +rsa_key_size=4096 +data_path="./certbot" +email="cksgh5477@gmail.com" +# staging=0 # 실제 인증서 발급 (모든 테스트 완료 후 마지막에만 사용) +staging=1 # 테스트용 인증서 발급 (Rate Limit 회피를 위해 사용) + +# 기존 데이터가 있는지 확인 +if [ -d "$data_path" ]; then + read -p "기존 인증서 데이터가 있습니다. 삭제하고 계속하시겠습니까? (y/N) " decision + if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then + exit + fi +fi + +# 필요한 디렉토리 생성 +if [ ! -d "$data_path/conf" ]; then + mkdir -p "$data_path/conf" +fi + +if [ ! -d "$data_path/www" ]; then + mkdir -p "$data_path/www" +fi + +# TLS 파라미터 다운로드 +if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then + echo "### SSL 파라미터 다운로드 중..." + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" + echo +fi + +# 임시 인증서 생성 (nginx 시작을 위해) +echo "### 임시 자체 서명 인증서 생성 중..." +path="/etc/letsencrypt/live/globalnomad.site" +mkdir -p "$data_path/conf/live/globalnomad.site" +docker run --rm -v "$PWD/$data_path/conf:/etc/letsencrypt" \ + --entrypoint "/bin/sh" certbot/certbot \ + -c "openssl req -x509 -nodes -newkey rsa:1024 -days 1 \ + -keyout '$path/privkey.pem' \ + -out '$path/fullchain.pem' \ + -subj '/CN=localhost'" +echo + +# nginx 시작 +echo "### nginx 시작 중..." +docker-compose up -d nginx +echo + +# 임시 인증서 삭제 +echo "### 임시 인증서 삭제 중..." +docker run --rm -v "$PWD/$data_path/conf:/etc/letsencrypt" \ + --entrypoint "/bin/sh" certbot/certbot \ + -c "rm -rf /etc/letsencrypt/live/globalnomad.site && \ + rm -rf /etc/letsencrypt/archive/globalnomad.site && \ + rm -rf /etc/letsencrypt/renewal/globalnomad.site.conf" +echo + +# Let's Encrypt 인증서 요청 +echo "### Let's Encrypt 인증서 요청 중..." +domain_args="" +for domain in "${domains[@]}"; do + domain_args="$domain_args -d $domain" +done + +# 스테이징 환경 선택 +case "$staging" in + 1) staging_arg="--staging" ;; + *) staging_arg="" ;; +esac + +docker run --rm \ + -v "$PWD/$data_path/conf:/etc/letsencrypt" \ + -v "$PWD/$data_path/www:/var/www/certbot" \ + certbot/certbot certonly --webroot -w /var/www/certbot \ + $staging_arg \ + --email $email \ + --agree-tos \ + --no-eff-email \ + --force-renewal \ + $domain_args +echo + +# nginx 재시작 +echo "### nginx 재시작 중..." +docker-compose restart nginx \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index e9ffa30..45bbc3e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,9 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - /* config options here */ + // Docker 배포를 위한 standalone 모드 활성화 + // 해당 설정은 프로덕션 빌드 시 필요한 파일만 .next/standalone 폴더에 복사됨. + output: 'standalone', }; export default nextConfig; diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..8fdc973 --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,60 @@ +# HTTP → HTTPS 리다이렉트 +server { + listen 80; + listen [::]:80; + server_name globalnomad.site www.globalnomad.site; + + # Let's Encrypt 인증을 위한 경로 + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # 나머지 모든 요청은 HTTPS로 리다이렉트 + location / { + return 301 https://$server_name$request_uri; + } +} + +# HTTPS 서버 설정 +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name globalnomad.site www.globalnomad.site; + + # SSL 인증서 경로 (Let's Encrypt) + ssl_certificate /etc/letsencrypt/live/globalnomad.site/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/globalnomad.site/privkey.pem; + + # SSL 설정 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + # HSTS (HTTPS 강제) + add_header Strict-Transport-Security "max-age=63072000" always; + + # Next.js로 프록시 + location / { + proxy_pass http://nextjs:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # 타임아웃 설정 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # 정적 파일 캐싱 (Next.js의 _next/static) + location /_next/static { + proxy_pass http://nextjs:3000; + proxy_cache_valid 60m; + add_header Cache-Control "public, immutable"; + } +} \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..058ca9a --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,43 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # 로그 포맷 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # 성능 최적화 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip 압축 + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml application/atom+xml image/svg+xml; + + # 보안 헤더 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # 서버 설정 파일 포함 + include /etc/nginx/conf.d/*.conf; +} \ No newline at end of file diff --git a/package.json b/package.json index 3d21cb0..3655817 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,9 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.5", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-unused-imports": "^4.1.4", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.13", "tailwindcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e18a79..fa44db3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,15 @@ importers: eslint-config-next: specifier: 15.3.5 version: 15.3.5(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint-config-prettier: + specifier: ^10.1.5 + version: 10.1.5(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-simple-import-sort: + specifier: ^12.1.1 + version: 12.1.1(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-unused-imports: + specifier: ^4.1.4 + version: 4.1.4(@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -963,6 +972,12 @@ packages: typescript: optional: true + eslint-config-prettier@10.1.5: + resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} @@ -1028,6 +1043,20 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + eslint-plugin-simple-import-sort@12.1.1: + resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==} + peerDependencies: + eslint: '>=5.0.0' + + eslint-plugin-unused-imports@4.1.4: + resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2965,6 +2994,10 @@ snapshots: - eslint-plugin-import-x - supports-color + eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)): + dependencies: + eslint: 9.30.1(jiti@2.4.2) + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 @@ -3073,6 +3106,16 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 + eslint-plugin-simple-import-sort@12.1.1(eslint@9.30.1(jiti@2.4.2)): + dependencies: + eslint: 9.30.1(jiti@2.4.2) + + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)): + dependencies: + eslint: 9.30.1(jiti@2.4.2) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0